mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(gateway): remove dead rpc surfaces (#121387)
This commit is contained in:
committed by
GitHub
parent
0296785c5f
commit
bab4546b41
@@ -178,7 +178,6 @@ enum class GatewayMethod(
|
||||
DoctorMemoryResetGroundedShortTerm("doctor.memory.resetGroundedShortTerm"),
|
||||
DoctorMemoryRepairDreamingArtifacts("doctor.memory.repairDreamingArtifacts"),
|
||||
DoctorMemoryDedupeDreamDiary("doctor.memory.dedupeDreamDiary"),
|
||||
DoctorMemoryRemHarness("doctor.memory.remHarness"),
|
||||
LogsTail("logs.tail"),
|
||||
ChannelsStatus("channels.status"),
|
||||
ChannelsStart("channels.start"),
|
||||
@@ -241,11 +240,7 @@ enum class GatewayMethod(
|
||||
TalkClientToolCall("talk.client.toolCall"),
|
||||
TalkClientSteer("talk.client.steer"),
|
||||
TalkSessionCreate("talk.session.create"),
|
||||
TalkSessionJoin("talk.session.join"),
|
||||
TalkSessionAppendAudio("talk.session.appendAudio"),
|
||||
TalkSessionStartTurn("talk.session.startTurn"),
|
||||
TalkSessionEndTurn("talk.session.endTurn"),
|
||||
TalkSessionCancelTurn("talk.session.cancelTurn"),
|
||||
TalkSessionCancelOutput("talk.session.cancelOutput"),
|
||||
TalkSessionAcknowledgeMark("talk.session.acknowledgeMark"),
|
||||
TalkSessionSubmitToolResult("talk.session.submitToolResult"),
|
||||
@@ -343,17 +338,14 @@ enum class GatewayMethod(
|
||||
SecretsReload("secrets.reload"),
|
||||
SecretsResolve("secrets.resolve"),
|
||||
VoicewakeRoutingGet("voicewake.routing.get"),
|
||||
VoicewakeRoutingSet("voicewake.routing.set"),
|
||||
SessionsList("sessions.list"),
|
||||
SessionsSubscribe("sessions.subscribe"),
|
||||
SessionsUnsubscribe("sessions.unsubscribe"),
|
||||
SessionsMessagesSubscribe("sessions.messages.subscribe"),
|
||||
SessionsMessagesUnsubscribe("sessions.messages.unsubscribe"),
|
||||
SessionsViewersSet("sessions.viewers.set"),
|
||||
SessionsPreview("sessions.preview"),
|
||||
SessionsDescribe("sessions.describe"),
|
||||
SessionsCompactionList("sessions.compaction.list"),
|
||||
SessionsCompactionGet("sessions.compaction.get"),
|
||||
SessionsCompactionBranch("sessions.compaction.branch"),
|
||||
SessionsCompactionRestore("sessions.compaction.restore"),
|
||||
SessionsBranchesList("sessions.branches.list"),
|
||||
@@ -413,7 +405,6 @@ enum class GatewayMethod(
|
||||
CronRun("cron.run"),
|
||||
CronRuns("cron.runs"),
|
||||
GatewayIdentityGet("gateway.identity.get"),
|
||||
GatewayRestartPreflight("gateway.restart.preflight"),
|
||||
GatewayRestartRequest("gateway.restart.request"),
|
||||
SystemPresence("system-presence"),
|
||||
SystemEvent("system-event"),
|
||||
@@ -461,7 +452,6 @@ enum class GatewayMethod(
|
||||
WebLoginWait("web.login.wait"),
|
||||
TerminalAttach("terminal.attach"),
|
||||
TerminalList("terminal.list"),
|
||||
TerminalText("terminal.text"),
|
||||
ControlUiGithubPreview("controlUi.githubPreview"),
|
||||
SystemInfo("system.info"),
|
||||
AgentsWorkspaceList("agents.workspace.list"),
|
||||
|
||||
@@ -6734,28 +6734,6 @@ public struct SessionsCompactionListParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionGetParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionBranchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
@@ -6822,28 +6800,6 @@ public struct SessionsCompactionListResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionGetResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let key: String
|
||||
public let checkpoint: SessionCompactionCheckpoint
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
key: String,
|
||||
checkpoint: SessionCompactionCheckpoint)
|
||||
{
|
||||
self.ok = ok
|
||||
self.key = key
|
||||
self.checkpoint = checkpoint
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case key
|
||||
case checkpoint
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionBranchResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let sourcekey: String
|
||||
@@ -11102,28 +11058,6 @@ public struct TalkSessionCancelOutputParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TalkSessionCancelTurnParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let turnid: String?
|
||||
public let reason: String?
|
||||
|
||||
public init(
|
||||
sessionid: String,
|
||||
turnid: String? = nil,
|
||||
reason: String? = nil)
|
||||
{
|
||||
self.sessionid = sessionid
|
||||
self.turnid = turnid
|
||||
self.reason = reason
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
case turnid = "turnId"
|
||||
case reason
|
||||
}
|
||||
}
|
||||
|
||||
public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
public let sessionkey: String?
|
||||
public let spawnedby: String?
|
||||
@@ -11260,138 +11194,6 @@ public struct TalkSessionCreateResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TalkSessionJoinParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let token: String
|
||||
|
||||
public init(
|
||||
sessionid: String,
|
||||
token: String)
|
||||
{
|
||||
self.sessionid = sessionid
|
||||
self.token = token
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
case token
|
||||
}
|
||||
}
|
||||
|
||||
public struct TalkSessionJoinResult: Codable, Sendable {
|
||||
public let id: String
|
||||
public let roomid: String
|
||||
public let roomurl: String
|
||||
public let sessionkey: String
|
||||
public let sessionid: String?
|
||||
public let channel: String?
|
||||
public let target: String?
|
||||
public let provider: String?
|
||||
public let model: String?
|
||||
public let voice: String?
|
||||
public let mode: AnyCodable
|
||||
public let transport: AnyCodable
|
||||
public let brain: AnyCodable
|
||||
public let createdat: Double
|
||||
public let expiresat: Double
|
||||
public let room: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
roomid: String,
|
||||
roomurl: String,
|
||||
sessionkey: String,
|
||||
sessionid: String? = nil,
|
||||
channel: String? = nil,
|
||||
target: String? = nil,
|
||||
provider: String? = nil,
|
||||
model: String? = nil,
|
||||
voice: String? = nil,
|
||||
mode: AnyCodable,
|
||||
transport: AnyCodable,
|
||||
brain: AnyCodable,
|
||||
createdat: Double,
|
||||
expiresat: Double,
|
||||
room: [String: AnyCodable])
|
||||
{
|
||||
self.id = id
|
||||
self.roomid = roomid
|
||||
self.roomurl = roomurl
|
||||
self.sessionkey = sessionkey
|
||||
self.sessionid = sessionid
|
||||
self.channel = channel
|
||||
self.target = target
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.voice = voice
|
||||
self.mode = mode
|
||||
self.transport = transport
|
||||
self.brain = brain
|
||||
self.createdat = createdat
|
||||
self.expiresat = expiresat
|
||||
self.room = room
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case roomid = "roomId"
|
||||
case roomurl = "roomUrl"
|
||||
case sessionkey = "sessionKey"
|
||||
case sessionid = "sessionId"
|
||||
case channel
|
||||
case target
|
||||
case provider
|
||||
case model
|
||||
case voice
|
||||
case mode
|
||||
case transport
|
||||
case brain
|
||||
case createdat = "createdAt"
|
||||
case expiresat = "expiresAt"
|
||||
case room
|
||||
}
|
||||
}
|
||||
|
||||
public struct TalkSessionTurnParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let turnid: String?
|
||||
|
||||
public init(
|
||||
sessionid: String,
|
||||
turnid: String? = nil)
|
||||
{
|
||||
self.sessionid = sessionid
|
||||
self.turnid = turnid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
case turnid = "turnId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct TalkSessionTurnResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let turnid: String?
|
||||
public let events: [TalkEvent]?
|
||||
|
||||
public init(
|
||||
ok: Bool,
|
||||
turnid: String? = nil,
|
||||
events: [TalkEvent]? = nil)
|
||||
{
|
||||
self.ok = ok
|
||||
self.turnid = turnid
|
||||
self.events = events
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ok
|
||||
case turnid = "turnId"
|
||||
case events
|
||||
}
|
||||
}
|
||||
|
||||
public struct TalkSessionSteerParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let sessionkey: String?
|
||||
@@ -15394,34 +15196,6 @@ public struct TerminalListResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TerminalTextParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
|
||||
public init(
|
||||
sessionid: String)
|
||||
{
|
||||
self.sessionid = sessionid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionid = "sessionId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct TerminalTextResult: Codable, Sendable {
|
||||
public let text: String
|
||||
|
||||
public init(
|
||||
text: String)
|
||||
{
|
||||
self.text = text
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case text
|
||||
}
|
||||
}
|
||||
|
||||
public struct TerminalUploadParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let name: String
|
||||
|
||||
@@ -1,116 +1,116 @@
|
||||
1c88517e5da24dc4f66f9d84fd9ee7165a87af1188fa37379b4b476b806c4c2a module/account-core
|
||||
9669347cf05fa6a933201a02eeb7a35950e57d5da3cab89f3cf376d866316d3f module/account-helpers
|
||||
ecdcae2fe366048497da9665ec9b481d80fe2b2b9a63cb4416753c9475487bea module/account-core
|
||||
40d44524ca231f3e6ef2d5241a963b42050ed8ad3223ea64640fae4dc90e5291 module/account-helpers
|
||||
71522995185b956a0cc4927a472cc8d1153e5e998874bfd9a750513175174713 module/account-id
|
||||
2ccf6bdc0cae7e136a0ed9feba2cab10047f432fb1c50c4f711df1a5cc6e5414 module/account-resolution
|
||||
be1c933a14a9e218a28e7544ad2aac009913ea2211413a9c1258417ca0ac2bc9 module/account-resolution
|
||||
4fbb1c87e99399f842a20d75d5e35a4b7064a1b7f02115c23f9a2a7cdcfb57ee module/agent-config-primitives
|
||||
de9cff892a779401b945de7ef0171e6f9ea4e43944dc9d628dc9ea3e95d9d0e6 module/agent-harness
|
||||
a8ed8da8c0a5e3c34840f2b869ea96ff37c4cbac6721822cd59e2cf8d0e27ee5 module/agent-harness-runtime
|
||||
c4c918620045035603967551cf3fdcd606367ecd7c4ef56ad93699fb63b84c22 module/agent-harness
|
||||
286b0def9192f44999b482ea4209e69c91045a34ef1674def1dab16d3ecf1297 module/agent-harness-runtime
|
||||
cdf661f6e5b9118ae3b33f0c5e4aec1351ad89b0b61e8b3e02abb7409b4e16da module/agent-media-payload
|
||||
12b5557fde05afbfeac00f5024662eeed7f3f9356edc5b200c74da7c61de8650 module/agent-runtime
|
||||
9fa72f7d99675604759681e04b0228989b6c6305566eb3e3b2469eecd35324ac module/agent-scope-runtime
|
||||
d7e99bb6931d7f3a08099c41c80b76ae90bd94a25d7aa337a4648e8149ce4386 module/agent-runtime
|
||||
904a765c68c458d67c1d5b7a30ccd9b28e56615f818142dd37a5023c05cfe22a module/agent-scope-runtime
|
||||
8fecb210e22bce4532b6ab649b09465f0bd2c857a44abf40db7d683d6491e6da module/allow-from
|
||||
eba56a699695ffd3bcbf897da0aa93b51294abf1b9542907648b1c30db8c5763 module/allowlist-config-edit
|
||||
aaa2a8f8db2108108973a547eb952cd886339a6773551e4e8fab91bd4ab1aa6c module/approval-auth-runtime
|
||||
93dd8e4ff9f17ff60909d6fd313b9f79369dc18f1540c9e25eaf1a3ac6b64419 module/approval-client-runtime
|
||||
7d1810c8342c4b281371cc2333f4f000ee9e284336938de96ba3765ebbc0c3f7 module/approval-delivery-runtime
|
||||
0b11bcea0a4b248126d478b50111f922f888750f90a47eb7648d8f19cc36b0a3 module/approval-gateway-runtime
|
||||
df0e6aca83ecfb53d3b0154b3414e72448be5c46c6b0fb6162f6dc14286c010a module/allowlist-config-edit
|
||||
525ff6a6681c0c4c3ecddd5f49ce5f9127b3dd4991e0b10f802a208d658ff534 module/approval-auth-runtime
|
||||
f0287ea003b8af1980d40dcb60c31f2ce54660795bc79b2b36b24a472bebf550 module/approval-client-runtime
|
||||
19586b5567dd7063f5060096b82bfa50bf8ca84d4ae5b2875f7b34e06596ff46 module/approval-delivery-runtime
|
||||
dc7028580b3c3b8bf6812421e2082d226a10607268f04106b12eebc7850e7ffa module/approval-gateway-runtime
|
||||
eee50f080f9447135fe72dba2102cf0faf9194d37e5fb07e4a0dc78ac719f77d module/approval-handler-adapter-runtime
|
||||
2ee0442d19db08d563f61e7d71d5d45776d2a06c4d233317c5171e1a297dcef3 module/approval-handler-runtime
|
||||
74e71e8128d253324f7b6b9f7057d3e62674ba924e0a53d1e1a507d1995b8e93 module/approval-native-runtime
|
||||
aee9ee8745eba7a1ff464920ecb1807c0b4cab603d0506b42a3647c301cc132d module/approval-reply-runtime
|
||||
b331f4287768acb1a89ba7431355eb557f11e8a1c849984cbd6a53e402989e22 module/approval-runtime
|
||||
1536bafb393712324b4aaa68ccd150ac89aab8c2a8f437636c3418e5b2b6d79f module/approval-handler-runtime
|
||||
53d841e27fc25ed2d7dc599bab24273ae99c7cb532db1811362f7f9feffd84cc module/approval-native-runtime
|
||||
a15ac005239135ce90fabffd8cc9c2c939d8948f99d7ca6940154f9566564ab4 module/approval-reply-runtime
|
||||
42ebdb257a9f4df22b3fd2d3201bc7e4124b7d89a47a06ef5f0ab172285ccc85 module/approval-runtime
|
||||
01ca912836b8dec672f705e294f72d346e778557e4c591317d67558ea7669c0b module/archive
|
||||
d7e53de63b0ac11a266e4abdc18ba6e9401b80309f5c8f5f6a72a00f65dfe3bd module/boolean-param
|
||||
0d9b23b23425e07595d7457b3b158197244b7ba8615d9fbc358bad13dddb417f module/channel-actions
|
||||
07defbe9505e2fdf12066d8b235708bc4a60cce017b732043c85def65788a6ee module/channel-config-helpers
|
||||
10fc989f02a8d081fc34c9953623269a5a1290e2c9db2c85a49a7037a4113c08 module/channel-config-helpers
|
||||
c2cc71d5070b6071c51248b0648d1ad1a9468d3737df890adc77ec02025e8853 module/channel-config-primitives
|
||||
484894c32a2fa1f6ca75538d854569dafbfe48e30c081fc3231f813b3054686c module/channel-config-schema
|
||||
6ebf3439b567a420b56f6037f900e9c275d479851dfe8919236427a91b14cfa2 module/channel-contract
|
||||
965f3716cc3feba0cadbb63e91535e090f57d6ebc75e36eb8cd1d7bfb46eed54 module/channel-core
|
||||
6b10236b41e8ad618ed2c055a9e4d847ff6baf22be0c9f653849604fbdcc7af2 module/channel-dm-policy
|
||||
7660776f4a2d0db95a7144fccbdac9acec702a5900ea3a969417621371fbc1e0 module/channel-entry-contract
|
||||
eb997d5af42ac5363472a6dc47cfd9fe9a4c89e9908fbfac7927e919260b2979 module/channel-contract
|
||||
9a38aea0efbcf690b4b5796477ef96784827df804026b22ba913ce004412e4d0 module/channel-core
|
||||
c699a67bbbd046bcb4777eb6d288868f2fcfe90ed9faef2abb028d858311b780 module/channel-dm-policy
|
||||
312b48644d96413bdff93dba31f90b74dc389308a0bbdf34735be09b630defed module/channel-entry-contract
|
||||
421352c351ccf8044f0ba3045c40ba048e6992917f7caf9c92faff2b2e7a4711 module/channel-feedback
|
||||
7d637cf7d7036881afcc78aa13e976a77390826421354aed37a562449951cbd1 module/channel-inbound
|
||||
9a55a636ba4e714775b15afd72d5dcf0f3857fb5ed8380a624913ddd515cc8b7 module/channel-inbound
|
||||
378cbd56a4ff711bd748094a145be0f6a3a363f29f608c81f02a3fc3fb2f1edd module/channel-inbound-debounce
|
||||
5d19511cd325d1d902ee5b4c848de0f15ded5fce22c5a88f057d159c1c88d336 module/channel-ingress-runtime
|
||||
5b8ee0501ac6511e1632dd0b0e7040936869f567e795e744023b388817e903e7 module/channel-lifecycle
|
||||
423e100d8237df2a1cf4208ef2169c60ed3b30a9a0188661c65ade5de94730c2 module/channel-lifecycle
|
||||
0e47457e38d1df0bd572e1408cde2ca6a788b65205f43c585316b5ad3a8f2f16 module/channel-logging
|
||||
aca360581c758eebb19e4bc6feaef33c3815750f63747a5c9747658073490602 module/channel-message
|
||||
763f05d6de11e3721f438d9dec72f212f47d43b77e2d7eb121b49a45bd10c42b module/channel-outbound
|
||||
647eae943f2cfee9864fb7f3d9eadd53ce268709cfc18dd8ba4a0c21e6efa9d3 module/channel-pairing
|
||||
8900eb25e84fc06b4493501412b7bf0b705e7740c61c9464ca419bfa39b03aaa module/channel-plugin-common
|
||||
a29f22b55aba14d405aa20efe1dd5a7ae273da7269df9ef6308c734a35b1327a module/channel-policy
|
||||
586c10b30a014eec4954ebeddf3a54f7362bcc9ae34f15044156dc45e5f4e30e module/channel-reply-pipeline
|
||||
137785f5f6f6ef1c4e763818cc2f34aca5d94ddb6eb062d03c0ce1b6d4777a5b module/channel-message
|
||||
6fd0d9f7be5a14c2f81304ce9d1d137d45750ad07b47f4d973fbfbfac7c962bb module/channel-outbound
|
||||
05ad5c943facb81d915ea4d591163482eff1f0b73995dfd34830d07178616b5b module/channel-pairing
|
||||
5377d958426380546a2e967505df59e909ce3952cc797aad43fd674c51d8afa1 module/channel-plugin-common
|
||||
2b6f57ab0bd9128551a42c4c4ecd7c3bd4087af9766b86df56ff21f0446c8eff module/channel-policy
|
||||
5c9491d6fb202d8123638bcbdc7171ebce5ccb30a856efdd028d71696580cbcc module/channel-reply-pipeline
|
||||
482370e60135db9bfaf07f24bab549e5fde09ab265a6061a1f587c5d93929e91 module/channel-runtime-context
|
||||
4b7d11e77e58bf284f7081df8c4b00e2e658ec19cf645f063db1e506cd7cdf3c module/channel-secret-basic-runtime
|
||||
574046ce5310620dfa9d989a9bc17121ff2a7cf5d87a20bab7d6f8bf2e3578fb module/channel-secret-runtime
|
||||
fff80c8bbba1f326cb1dfdd7c2f7197566bc7dd09e59719cd1c0d0cd4bce7f1b module/channel-send-result
|
||||
5cdb34f390e21878296d6e8ff573f03137ce921721cb01029bfd3d9899784064 module/channel-setup
|
||||
6464055d5579ad2182ca9fd9c1f603b2554f2d97d9525afb581e21fdd057aa22 module/channel-status
|
||||
f8f5c78f42f917326012fe80d3dfc31500b1120a17258b6164a75564a608fdc9 module/channel-send-result
|
||||
84ea037642408624d7c8db58a999d7be6ce733581bb820a8065f76f351f3500f module/channel-setup
|
||||
aa15810e4698859e5e74a873f3bca85a9e537c83fbed6bb7879b7e606b6766d7 module/channel-status
|
||||
b227a529438b8765fef0a1af03effeed1b8594152a0e8cfb222a14caa4020a56 module/channel-streaming
|
||||
b2f920ff4a6b4190e6d6ea0a3effb001751e092f0e3ac0cf296721ff8c383d86 module/channel-streaming-config
|
||||
fdeffe356c7c4edeec9f8fd03edcadc375eabc7a9412e582b10c3180e3ef40fc module/cli-argv
|
||||
ad12670dbfe538f8d0ebf4fb2b68080e93a760278278e6b1ce9bb129d4b2d533 module/collection-runtime
|
||||
452fa6bff0199f4812f970fd8f0627c1e1ac8f9ffcd90215c61913a326734400 module/command-auth
|
||||
4c28bb503793e4a50f47c3a00a5f98eff82d7231801a526e47cb572e033b4777 module/command-auth-native
|
||||
450a6ce9aa82c78a5b76d1a53d44ae490597cf255eda76d458b9f6da8ee4d9b3 module/command-auth
|
||||
6f9ac353bbc3cae31911f300d36e0b554f41f5fb1699a06c5d22cb7301fc2f67 module/command-auth-native
|
||||
b99ff6081946a8cc08e8e63d852cb484462808c92add2fb23cb36633583cb66e module/command-detection
|
||||
9f6332a1db7cd6f2fcb92d8ac2e4af32379d694bfbeeab736f304815214b364a module/command-primitives-runtime
|
||||
e461ac9cb7520441e5867fe4ff7882341f3d1338b7a0c12934f2e1645ef37f91 module/command-status
|
||||
d55bb5460aa3de0685f0f34940d9bd9ad5970f6d7554ea0dd2089dd4a8dd1e62 module/config-contracts
|
||||
ec10d7ce3379d2d2d45dc3e6897b56d092adca57c74d4a27f3855f82437c6e81 module/config-contracts
|
||||
75a80626b1583434ebeffd6d48c066aaaffdb2c6e215abe98f106c6b927415f4 module/config-mutation
|
||||
c9819e1920b8cc1ea94ab60e7642aae1f1ba0e36f485bc7b2bbabdd607bd2bf4 module/config-runtime
|
||||
a7c4aa19a193bb2ec71e8a36b74f223a9fa6e035c2b97eb81ef2f13e35b20347 module/conversation-runtime
|
||||
39984c45f2bd4526aee7dd0d513bd54e2999e9329c29cafd142cca028db3ef8f module/core
|
||||
1d323dff55dc2f9e2fa2fed4f3843c356fe7ed2f33cdd4fe2064f9f69769fc00 module/dedupe-runtime
|
||||
fe1fb6ca8307528dda4fd6c48f180a79840d98ef18be6e633544f351ac0ab120 module/config-runtime
|
||||
d2ac206c6ed4f1492d4136c704fd55a4aa68de11bc36f610faaf16714bfa4862 module/conversation-runtime
|
||||
81e8859ae52f71f2f696a39cdc05ad7564b98fa628c37742949ed52964ecbc35 module/core
|
||||
72e018a028188038c6cd2d835e674ddc10a164be3640c4042a0312c53e592485 module/dedupe-runtime
|
||||
ebef0e650ab45e44c9335e2b3e15588c968cea6dadd125364a076f9c50ad1e8c module/device-bootstrap
|
||||
b8d4ff8d1a3f9d28962ad3ebe4215f713fba47a28fbce5f083aae72a9316e6c0 module/diagnostic-runtime
|
||||
769f3de78b553a4dcd6a94fe7fd58cdd4f8ecdd6ee6f21c4f138e5421f949ad6 module/directory-runtime
|
||||
f2989a9ed18babed3a248f1ed43c8f0ed30c2dc04f446ddef9c9211d86f81b26 module/discord
|
||||
4dc492621bb2ebde58fc4cf9d9d1f2a3ff95411bb88230fc179513725ddb1959 module/directory-runtime
|
||||
414109dd3f9bdb6f2da4b1268cce76266f9aa1fd27bd88c6aa876554e2b9fe43 module/discord
|
||||
64adc7f42bebf579531d8e18615b4f2384dcd9f35265f88c5129c10277b12eee module/error-runtime
|
||||
05ff25c56097b12fd9956115eb4bf9a5171574a2137fd04a4e34f195e3813e5f module/extension-shared
|
||||
dd9f6e0fd33cc88b22543c1ee30cc09cf4de4d8f30dff7b7f9cebef885c21543 module/gateway-method-runtime
|
||||
7d5e8139694e5072ba098a974f04d2baf20168dca41902078a4376cea84af340 module/gateway-runtime
|
||||
e28aacaa954810a8c006248e6c70375171c1cb874a3ca073e722eb99db8ac5d9 module/gateway-runtime
|
||||
1b1c6bd5bfc0cfb0c5bb9bd97f8ac1928750cb87232a3415dd066cc21d7b48b7 module/group-access
|
||||
7473b4186cff61f392bcd6f2ca34b90c7e4423a3423684b86fe09fc50af2d790 module/health
|
||||
8e397381a96f97b12dd34d66daa43e630a2373fab820e8c88331c3bfd48c564b module/hook-runtime
|
||||
16ca326bf06e43319841dc1c00e952601a5edfac77e337dead2d547c32f459d3 module/health
|
||||
eba928a25e2e53c039c968265a20d711037810d1f62f9960e7599f55a77315f2 module/hook-runtime
|
||||
f5e190bbfe0c21e76b7281a73cf5e9db806a1ed6e724fac2fb83ade5ccb827a0 module/inbound-envelope
|
||||
4928af5d2509f696b896f53ac790303a0742202dbcdae3e44fe6d1b434a9c1ba module/inbound-event-delivery
|
||||
ad34f303355f2ab08c5ebd637b475d99a2573c57046695d2268a58820ff5f482 module/inbound-reply-dispatch
|
||||
a8f4b2c417b3d14b3a1db5c6dbadffbcbee055b9617a15618bbb02349e8086f5 module/infra-runtime
|
||||
a7601573292ae23f895240d4dfe63eb1dd5d1abf61ea76c9d0581a3f71ad8f10 module/inbound-reply-dispatch
|
||||
02a6281f1cef538918987da2c2f13ee4eee52c70e5d01f6035c38b60b67400bb module/infra-runtime
|
||||
ce73721421f1b903dd04ead4df173582e59ea3e9990248102c448b419cc6d272 module/ingress-effect-once
|
||||
97742f0953ffc4270763b0d62f24875a0c3ed2ca6d90e96a18c52adc15a20cd0 module/interactive-runtime
|
||||
c2a7b1b42422ec85ec6c8655adcf987dbb410574ca8539ea6d259420a47fd296 module/interactive-runtime
|
||||
408d257ab5cc4b88a22b7e7595039cb8fc524b261c44141b294fbd0100ba62ee module/json-store
|
||||
e907fd3a98185f2c261f2aafcaa5a19ee1d7b459d519a498397d629f84c68312 module/lazy-runtime
|
||||
9874591cf115a4ad2c9a6cecd7fd6d2bb0a5d0a4c435d35b85a1ad3efdb85979 module/logging-core
|
||||
f1ca4ced4305d0769c2d8cc1291137ac7002fe0e6eaec2c1a71edad2204c8311 module/matrix
|
||||
ae17382b302c022012418146971c7bf69ed94316c51b62dbbf516e56ab4b327e module/media-local-roots
|
||||
f74d7295fe716aa140aa0bc9300d6259d71dab826de0808fca6bb02592bf5d6e module/media-mime
|
||||
f8913abdbc52f393eb85fc575025ef77444ae0b5dc8ad3421b478888831062ee module/media-runtime
|
||||
47a9fa2d97a666a6ed149ed810bbb5561d885503868f28c7fd7e274551c6a43f module/media-runtime
|
||||
6a52f93107335f88751704352cc01e62add06f854a5b7d765e2a5ee87c0313b6 module/media-store
|
||||
3a4e5c9a84a98b012fbe30f298dbe6168a0d0cdcf57b412f809cd413bc826e39 module/media-understanding
|
||||
7c05291f026d0fb27a0359a8f86e5c23902ff7e54fecfc1c4cce820e48f95288 module/media-understanding-runtime
|
||||
e2a03902d2553cb400e7346b228485e445c9e28a3781f04230a7cc68697ff70c module/meeting-runtime
|
||||
d1b7c9a8121e395df2152b46f6cf7c2762065ea48e9b11fe6588d40d9bf370ba module/media-understanding-runtime
|
||||
889ead61a31f459bfa2945dcc23900f20381b52a9802dd83d39ad012a644f5d7 module/meeting-runtime
|
||||
f457e2035a9ccefcb6010a0a14b415f92772dbead4dacedb796ecb5d564892c9 module/memory-core-host-engine-foundation
|
||||
b6fbb58ed1f9aea0d06b1f784df0c7b9c1dfaed884b8287a71a316a9f751058f module/memory-host-core
|
||||
e826728ee339ff50ae54fac274dc27b25398dfa73f16266b77f98a39073f958e module/memory-host-core
|
||||
1efa0aadc4261d1c6073058cbf3dcc9fa681424819bdd14333e19b249bbc4b18 module/messaging-targets
|
||||
a534dca8e9fa29467f3b6d7a8a8f85617c68c081fcbae7fb2418c042e02677e4 module/model-session-runtime
|
||||
297b0a7e58f3d3ced06fdad37133d9c52728c95e868d67268608448736203d32 module/models-provider-runtime
|
||||
662b73f9e9de8b083d8d8a6953ecdcc1dc29a97e8b6e8e5edbc64e3fdcf21a89 module/native-command-config-runtime
|
||||
01fbccef009c2a41162f5327f7ab087d1345b1288e00e342fe7042eb20a24e56 module/model-session-runtime
|
||||
3fc7a8d2d7dd9ac72172c82b77937b3686ce7fcc748fe397399e7e54a1107efa module/models-provider-runtime
|
||||
6e577f2b80c1923eea10467a57cf580bec7c323d365f59a5df3e118b7858cec4 module/native-command-config-runtime
|
||||
c241f194708a75e6f539b58b7837f1f5eb68b9f205ba9c74d2c5e149f1d742e0 module/native-command-registry
|
||||
35bc6e2da664788158dbbab1f733975409c4279694bf9b1b81a36a00796cdf2d module/param-readers
|
||||
ca7a56bb1a6169b4cf9befbf5aa21da280a8086fdc49fca4eec520a7a7c98549 module/persistent-dedupe
|
||||
e0a68ab64db24432eebb162b4aa376d359aece930f6de45ccd0af4e523496a66 module/plugin-config-runtime
|
||||
3aaea8abb68776110b234b26d035214a45034becb466cdcbd3e3f247ea5e220f module/plugin-entry
|
||||
fddd23b29272e17486033f0c7e386d9c686104543a08d6cc22d0883af56bbf6a module/plugin-runtime
|
||||
7eef18139d5d8814b259250d512bdf1cdd3c97f1e22718286bd362a9677ff758 module/provider-auth
|
||||
e0e0a6bd27b239056ccd90444adfc83927219169390fd5fd890f5ae1dbcea1a7 module/provider-catalog-runtime
|
||||
6029e593c85031ce6c7388f243d38edfa5e557de639f9808678eb26523e88b81 module/plugin-entry
|
||||
aeb0d99af2c470a793baee65c99373556705a15fc41ec2edab0f79581b827806 module/plugin-runtime
|
||||
3340c8a464cc9b0695aa164a5a6ffa468c199bfdcd3d2b9d460e70ffe4115501 module/provider-auth
|
||||
4c92396b10b9a0f198c652a230f1fb2069302d6f024e73d9850dc93b2b7b71c8 module/provider-catalog-runtime
|
||||
8131147d699394bd06503e2ea2f5f1a50b1594a87dded6d118b74a8d0328c8f6 module/proxy-capture
|
||||
da880c9378f0f1a7d38e0743a5ff11788fc9fa6fe6be6bb765045ba067d7cde8 module/question-gateway-runtime
|
||||
0618ab2d2265728db989716743dcf0b2bb9731f42b22af12feb256ad11db786c module/reply-chunking
|
||||
ba046ea97af8606e7d731b670a54f70109e865661378ae1b59cef638ff20f05d module/reply-dispatch-runtime
|
||||
eed49c747302e717039cb840e5ccb01fb766253ebfc80ba1e696c9aa8b5f5319 module/question-gateway-runtime
|
||||
3dff46dd2c25f3101d993179358d3e95797cb3c5f9c8359bec8af01c19187607 module/reply-chunking
|
||||
e693542047cb5fc52ae9bcba49f475aec6b8fa28ffde3048741e22b4b20e9180 module/reply-dispatch-runtime
|
||||
73f861fa3179d5af1159853c5acab0eec7a6c8f9398dcb75ea770e784fca6727 module/reply-history
|
||||
f56300977279980a614329933bba3df90fc3eb552bb7675cfd0bbfa3f02ef7b4 module/reply-payload
|
||||
01ca6d4d7e9f376d28626b7a70b7794a6de5de3bee16c83adc7d834f0693d7a8 module/reply-runtime
|
||||
d3c3aa7f8d77ba6adc07f080628119af0e6b0b27f3a06f1ddd3bc498a5decf81 module/reply-payload
|
||||
0d183e6869e075ce2c13d525ed1c196de916df1f6d022d544fef550dc97fe750 module/reply-runtime
|
||||
aa07d85d99fdd2b1e0cbe9975fb6dcae66b8bdce2607c6bd5402ae68bb15118c module/root-walk
|
||||
eee39bd28309cd706671cd86d598706286f99f5987f31615b77af8d81d696fff module/routing
|
||||
7877a7e58fa32a64107154e5b714c6d165e96989d4aa5f43e0afac085a187af0 module/run-command
|
||||
@@ -118,34 +118,34 @@ eee39bd28309cd706671cd86d598706286f99f5987f31615b77af8d81d696fff module/routing
|
||||
3269124490363a8063eb6230f703609ec9c4d4f02c137a035b19290aff831592 module/runtime-config-snapshot
|
||||
9e8651266b12dca1f0231a8d0956cc9142b43ff2b6c300f180a3f7c859bc96e7 module/runtime-env
|
||||
49e9b6a8195c89704eaa80656f176444af7cacbf639b759f41f2c78ae6bfcfd9 module/runtime-group-policy
|
||||
3ee1e17bb68396a8557bd30cea7293ee4979c9b9c86333115877e2b21ff179fa module/runtime-store
|
||||
83ea367056ccebb3f2ea1fcd1879e4d0a86682aa8dfbfb19975dbc9ab992010a module/runtime-store
|
||||
d17862c40825af1ddf0257b44f1e1cbb9c375e8e5ed668fae75d530d1a465cf9 module/secret-file
|
||||
8e2ac4d3973d8d8ce4478e3440d66ee5c0d9213b0fe9e927c421d14fd31e5e86 module/secret-input
|
||||
333ee3f8889687fc284902a7ffb2fe9dbb41a9f4e3f7fe1f6b0d8bc330de6225 module/secret-input-runtime
|
||||
91737225f39e684805fe8bede33933cbb5cefe0c0dbeb0346f69b4a9e29be929 module/secret-ref-runtime
|
||||
83778b8f94cd4d8a5b5f7db46b1e265adc5b4d03e02289685ca07d6bef9302cb module/security-runtime
|
||||
03629a7bde3bb0f7cb0861429ffb4a5948e502dc3aa373c4539f95b70c70f893 module/session-catalog
|
||||
c4905e0f528512ad687ff0f3971f8185e7cc84f934c3d9a3e2920a978cd1df55 module/session-discussion
|
||||
208e3cffe0ee35b7b3ac70dc58298d7d4e5c3ecdde0caab831249d59f8c23372 module/session-store-runtime
|
||||
bdac01dcf56887b055d4f8d5a8265b22ed09ce9034a598ce6228844b5ede90ba module/setup
|
||||
536e82f1ff0bf8249df31b22128c6b995df4b96605cc011e935cf64fbd7ace55 module/setup-runtime
|
||||
fdc9f9ac16fd4f781d76dd620df2bc580c8892c767f1fcecbcea756ea5e02b5c module/security-runtime
|
||||
35e8b82391dde381b1872d9be78e7d40f1175cde088fd9e31bcf7dbadde701de module/session-catalog
|
||||
97a094346b2f4ec802d0641f87fb7f4f5bbb98d5513775d107d63e0b9b9364f3 module/session-discussion
|
||||
4a906445ca358e6c54d641d7fe4e7d2a8f2f78af8bed7d920174192d5f83ca61 module/session-store-runtime
|
||||
337f63bf878dad8affd71b8e4e66252653ca4fdbadabd398ce527718deecd5c0 module/setup
|
||||
ad996e771d8ccf51e1a8944b54d4c0b3695da35617a4c64981bd5e3fdb865f96 module/setup-runtime
|
||||
d0cb4c5abb7484352088f556c1ba7c7b147d7b57977b8246e5cb7187937768b7 module/setup-tools
|
||||
1a46fed85c7276ad4ebb94dabde0a136e9f2cde30eef7c9a1dd693c9810b70a6 module/skill-commands-runtime
|
||||
e8db3d935a46133296dfe7f39fd11deb3cdadd83d4cb220d65966017950f439a module/speech-settings
|
||||
25a7806738a87c093d149ebdef506931789e08000c92d5da3ed12ed0256a8294 module/ssrf-policy
|
||||
17bf8e8c96a055e7600e9b145c31db1e9d9424b22d08cfe8fe6a2beb70421b95 module/ssrf-runtime
|
||||
07ddadba30645d0a3bb8f449cc68eb3c98cde57b3d0a97b646477536f0790509 module/skill-commands-runtime
|
||||
4bb230f9beb1b3948e1cbdf3c25d8a0c607af81d1566d347d1c02d20af088341 module/speech-settings
|
||||
05f645dc200055946e7386c2fd022bf9b6c9062e212b086aa22872ab2e8f2429 module/ssrf-policy
|
||||
ef358b38d1441e23557003c18d33fef9b2c390edab2201f1ad428f1fe80b782f module/ssrf-runtime
|
||||
cf7f004d754d995bd4989b09a8a539635588c9e679deba65ecc66d9c25e513b2 module/state-paths
|
||||
5168efd1af74791d92fda7120bfa79ed28a38c117db0fde69837b551c1437edd module/status-helpers
|
||||
58cf4ad2dbf36ac3dfc00ba9dfcf94f7e296a9cc28f1b880a466b99478529663 module/status-helpers
|
||||
f097d0096b21c8a052f0f649b7512ecf2aba4744ae6956f001950e053828b309 module/string-coerce-runtime
|
||||
ed66a1aa1464728d8f96977013bda42bffbe048d42f364cb45f5bf7a5e1bea46 module/telegram-account
|
||||
718c4ceabc0608514bbd90d12fd20e35a3ae536808193cd79a1d04bfcf862d83 module/telegram-account
|
||||
aef35bee2502cd6ed8765409b758e452aff8ac9469fd773e6a2a44c9a1bc3f66 module/temp-path
|
||||
87fa81b9e58d8fc04a4b4202d2d37fca339615f5225687d9db905151439e0f4d module/text-chunking
|
||||
0e3168845e5021738db84e85768500e54aacee269026da73a6b70f350a29362a module/text-runtime
|
||||
90530f4b8c562d80376cea1f7022025814e0caebdc1c9b3a158e6e82721ec095 module/tool-plugin
|
||||
e696b8fddc53b01c773eeaadaf5c9501555279b329ea334d18783200beb0ce4f module/tool-plugin
|
||||
dc1a073c59ab61e2789533b777b3f0cb9af689d64a97796b10e8aa82552510db module/tool-results
|
||||
60cdfe9308ac2b41be32162cc19f9749f4479bed825f95dfda706dfbfee1eed0 module/tool-send
|
||||
603abbf9716886fbcdf5cbb3ccac71c59be959695e3ca4822d3e482cf3851381 module/tool-send
|
||||
cda105b721d498df23a554c6b68be150b8fe66b8b9172185c31a0b3b0646b1dc module/web-media
|
||||
60274a16c028f96d9da2bbbf42aed4911e091dae7b7dd1ed1ca43f490b64e3b5 module/webhook-ingress
|
||||
83ed2035fa87753020e4977e088f8c09ecdcca03c9cf730484dea8f23f8c3619 module/webhook-ingress
|
||||
e7c422d17088a42544f2c9e5b78b3a2a460c2a16c78a34f2da3b089cd6e9f680 module/webhook-request-guards
|
||||
de59e86e126b75d13251cba7ebbe27b44d9b5588785d98df5ff4d6722374c81f module/widget-html
|
||||
9161b36ec0ab062ea41b363c894fcd672a7727f21cb726739f99f9c184fce69d module/zod
|
||||
|
||||
@@ -529,7 +529,6 @@ methods. Treat this as feature discovery, not a full enumeration of
|
||||
- `usage.cost` returns aggregated cost usage summaries for a date range. Pass `agentId` for one agent, or `agentScope: "all"` to aggregate configured agents.
|
||||
- `doctor.memory.status` returns vector-memory / cached embedding readiness for the active default agent workspace. Pass `{ "probe": true }` or `{ "deep": true }` only for an explicit live embedding provider ping. Pass `{ "agentId": "agent-id" }` to scope Dreaming store stats to one agent workspace; omitting it aggregates configured Dreaming workspaces.
|
||||
- `doctor.memory.dreamDiary`, `doctor.memory.backfillDreamDiary`, `doctor.memory.resetDreamDiary`, `doctor.memory.resetGroundedShortTerm`, `doctor.memory.repairDreamingArtifacts`, and `doctor.memory.dedupeDreamDiary` accept optional `{ "agentId": "agent-id" }`; omitted, they operate on the configured default agent workspace.
|
||||
- `doctor.memory.remHarness` returns a bounded, read-only REM harness preview for remote control-plane clients, including workspace paths, memory snippets, rendered grounded markdown, and deep promotion candidates. Requires `operator.read`.
|
||||
- `sessions.usage` returns per-session usage summaries. Pass `agentId` for one agent, or `agentScope: "all"` to list configured agents together.
|
||||
Both usage methods accept `mode: "specific"` with an IANA `timeZone` for DST-aware calendar-day boundaries and buckets. `utcOffset` remains supported for older clients and as a fallback when the Gateway runtime does not recognize the requested zone.
|
||||
- `sessions.usage.timeseries` returns timeseries usage for one session.
|
||||
@@ -569,7 +568,7 @@ methods. Treat this as feature discovery, not a full enumeration of
|
||||
- `terminal.upload` accepts one base64 file up to 16 MiB, stages it in a private 24-hour temporary directory on the session's Gateway or paired-node host, and returns the absolute path. The caller must still paste or otherwise use that path; the RPC never writes terminal input or executes a command.
|
||||
- `terminal.data` and `terminal.exit` events stream to the connection owner and attached viewers. Task-owned agent terminals close when their authoritative task reaches a terminal state; ordinary conversation-owned agent terminals remain persistent.
|
||||
- Sessions whose connection drops are detached, not killed: they stay reattachable for `gateway.terminal.detachedSessionTimeoutSeconds` (default 300; `0` restores kill-on-disconnect) while recent output accumulates in a bounded server-side buffer.
|
||||
- `terminal.list` returns attachable sessions; `terminal.attach` rebinds a live-or-detached session to the calling connection and returns the replay buffer (tmux-style take-over — a previous live owner receives `terminal.exit` with reason `detached`); `terminal.text` reads the buffer as plain text without attaching.
|
||||
- `terminal.list` returns attachable sessions; `terminal.attach` rebinds a live-or-detached session to the calling connection and returns the replay buffer (tmux-style take-over — a previous live owner receives `terminal.exit` with reason `detached`).
|
||||
- Every terminal method requires `operator.admin`; `gateway.terminal.enabled` is on by default and refuses every method when set to `false`. Fully sandboxed agents are refused, and an agent policy change closes existing and in-flight PTYs, detached ones included.
|
||||
|
||||
</Accordion>
|
||||
@@ -578,9 +577,7 @@ methods. Treat this as feature discovery, not a full enumeration of
|
||||
- `talk.catalog` returns the read-only Talk provider catalog for speech, streaming transcription, and realtime voice: canonical provider ids, registry aliases, labels, configured state, an optional group-level `ready` result, exposed model/voice ids, canonical modes, transports, brain strategies, and realtime audio/capability flags, without returning provider secrets or mutating global config. Current gateways set `ready` after applying runtime provider selection; treat its absence as unverified on older gateways.
|
||||
- `talk.config` returns the effective Talk config payload; `includeSecrets` requires `operator.talk.secrets` (or `operator.admin`).
|
||||
- `talk.session.create` (`operator.talk`) creates a gateway-owned Talk session for `realtime/gateway-relay`, `transcription/gateway-relay`, or `stt-tts/managed-room`. For `stt-tts/managed-room`, non-admin callers that pass `sessionKey` must also pass `spawnedBy` for scoped session-key visibility; unscoped `sessionKey` creation and `brain: "direct-tools"` require `operator.admin`.
|
||||
- `talk.session.join` validates a managed-room session token, emits `session.ready` or `session.replaced` as needed, and returns room/session metadata plus recent Talk events, never the plaintext token or its hash.
|
||||
- `talk.session.appendAudio` appends base64 PCM input audio to gateway-owned realtime relay and transcription sessions.
|
||||
- `talk.session.startTurn`, `talk.session.endTurn`, and `talk.session.cancelTurn` drive managed-room turn lifecycle with stale-turn rejection before state clears.
|
||||
- `talk.session.cancelOutput` stops assistant audio output, primarily for VAD-gated barge-in in gateway relay sessions.
|
||||
- `talk.session.submitToolResult` completes a provider tool call emitted by a gateway-owned realtime relay session. The request waits for any asynchronous completion signal exposed by the provider bridge; failed submissions keep the linked run active and do not emit a successful tool-result event. Pass `options: { willContinue: true }` for interim tool output or `options: { suppressResponse: true }` when the provider bridge advertises suppression support and the result should not start another response.
|
||||
- `talk.session.steer` sends active-run voice control into a gateway-owned agent-backed Talk session: `{ sessionId, text, mode? }`, where `mode` is `status`, `steer`, `cancel`, or `followup`; omitted mode is classified from the spoken text.
|
||||
@@ -635,7 +632,7 @@ methods. Treat this as feature discovery, not a full enumeration of
|
||||
|
||||
<Accordion title="Session control">
|
||||
- `sessions.list` returns the current session index, including per-row `agentRuntime` metadata when an agent runtime backend is configured. When cloud-worker placement is enabled or durable recovery state exists, session rows also include a closed `placement` state (`local`, `requested`, `provisioning`, `syncing`, `starting`, `active`, `draining`, `reconciling`, `reclaimed`, or `failed`) plus state-specific environment, owner-epoch, workspace, bundle, ACK-cursor, or recovery fields.
|
||||
- `sessions.subscribe` and `sessions.unsubscribe` toggle session change event subscriptions for the current WS client.
|
||||
- `sessions.subscribe` enables session change events for the current WebSocket client. The subscription ends when that client disconnects.
|
||||
- `sessions.messages.subscribe` and `sessions.messages.unsubscribe` toggle transcript/message event subscriptions for one session. Pass `includeApprovals: true` to also receive sanitized `session.approval` lifecycle events for approvals whose persisted audience includes that exact session and whose reviewer binding authorizes the subscribing client. The subscribe response then includes a bounded pending `approvalReplay`; it is authoritative when `truncated` is false. The opt-in is per subscribe call, not sticky: re-subscribing to the same session without `includeApprovals: true` removes an existing approval subscription. In addition to normal session-read authority, this opt-in requires `operator.admin`, or `operator.approvals` on a paired device.
|
||||
- `sessions.preview` returns bounded transcript previews for specific session keys.
|
||||
- `sessions.describe` returns one gateway session row for an exact session key.
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Talk mode covers five runtime shapes:
|
||||
- **iOS Talk (realtime)**: client-owned WebRTC for OpenAI realtime configs that select `webrtc` transport or omit transport, including framed and frameless transcript/audio events. Explicit `gateway-relay`, `provider-websocket`, and non-OpenAI realtime configs stay on the Gateway-owned relay; non-realtime configs use the native speech loop.
|
||||
- **Browser Talk**: `talk.client.create` for client-owned `webrtc`/`provider-websocket` sessions, or `talk.session.create` for Gateway-owned `gateway-relay` sessions. `managed-room` is reserved for Gateway handoff and walkie-talkie rooms.
|
||||
- **Android Talk (realtime)**: Android uses Gateway-owned relay realtime when `talk.catalog` reports the realtime group ready and the configured model passes the Android client gate; it never opens a client-owned WebRTC session. The Gateway now supports `gpt-live-*` relay sessions, but Android intentionally keeps those models on native speech recognition, Gateway chat, and `talk.speak` until the relay path is proven live from an Android device.
|
||||
- **Transcription-only clients**: `talk.session.create({ mode: "transcription", transport: "gateway-relay", brain: "none" })`, then `talk.session.appendAudio`, `talk.session.cancelTurn`, and `talk.session.close` for captions/dictation without an assistant voice response. One-shot uploaded voice notes still use the [media understanding](/nodes/media-understanding) audio path.
|
||||
- **Transcription-only clients**: `talk.session.create({ mode: "transcription", transport: "gateway-relay", brain: "none" })`, then `talk.session.appendAudio` and `talk.session.close` for captions/dictation without an assistant voice response. One-shot uploaded voice notes still use the [media understanding](/nodes/media-understanding) audio path.
|
||||
|
||||
Native Talk is a continuous loop: listen for speech, send the transcript to the model through the active session, wait for the response, then speak it via the configured Talk provider (`talk.speak`).
|
||||
|
||||
|
||||
@@ -29,10 +29,9 @@ Wake words and routing rules live in the Gateway state database, `~/.openclaw/st
|
||||
|
||||
### Routing (trigger to target)
|
||||
|
||||
| Method | Params | Result |
|
||||
| ----------------------- | ------------------------------------ | ------------------------------------ |
|
||||
| `voicewake.routing.get` | none | `{ config: VoiceWakeRoutingConfig }` |
|
||||
| `voicewake.routing.set` | `{ config: VoiceWakeRoutingConfig }` | `{ config: VoiceWakeRoutingConfig }` |
|
||||
| Method | Params | Result |
|
||||
| ----------------------- | ------ | ------------------------------------ |
|
||||
| `voicewake.routing.get` | none | `{ config: VoiceWakeRoutingConfig }` |
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -1012,30 +1012,26 @@ The supported `talk.session.create` combinations are intentionally small:
|
||||
Method map for readers migrating from the older `talk.realtime.*` /
|
||||
`talk.transcription.*` / `talk.handoff.*` families (all removed):
|
||||
|
||||
| Old | New |
|
||||
| -------------------------------- | -------------------------------------------------------- |
|
||||
| `talk.realtime.session` | `talk.client.create` |
|
||||
| `talk.realtime.toolCall` | `talk.client.toolCall` |
|
||||
| `talk.realtime.relayAudio` | `talk.session.appendAudio` |
|
||||
| `talk.realtime.relayCancel` | `talk.session.cancelOutput` or `talk.session.cancelTurn` |
|
||||
| `talk.realtime.relayToolResult` | `talk.session.submitToolResult` |
|
||||
| `talk.realtime.relayStop` | `talk.session.close` |
|
||||
| `talk.transcription.session` | `talk.session.create({ mode: "transcription" })` |
|
||||
| `talk.transcription.relayAudio` | `talk.session.appendAudio` |
|
||||
| `talk.transcription.relayCancel` | `talk.session.cancelTurn` |
|
||||
| `talk.transcription.relayStop` | `talk.session.close` |
|
||||
| `talk.handoff.create` | `talk.session.create({ transport: "managed-room" })` |
|
||||
| `talk.handoff.join` | `talk.session.join` |
|
||||
| `talk.handoff.revoke` | `talk.session.close` |
|
||||
| Old | New |
|
||||
| -------------------------------- | ---------------------------------------------------- |
|
||||
| `talk.realtime.session` | `talk.client.create` |
|
||||
| `talk.realtime.toolCall` | `talk.client.toolCall` |
|
||||
| `talk.realtime.relayAudio` | `talk.session.appendAudio` |
|
||||
| `talk.realtime.relayCancel` | `talk.session.cancelOutput` |
|
||||
| `talk.realtime.relayToolResult` | `talk.session.submitToolResult` |
|
||||
| `talk.realtime.relayStop` | `talk.session.close` |
|
||||
| `talk.transcription.session` | `talk.session.create({ mode: "transcription" })` |
|
||||
| `talk.transcription.relayAudio` | `talk.session.appendAudio` |
|
||||
| `talk.transcription.relayCancel` | `talk.session.close` |
|
||||
| `talk.transcription.relayStop` | `talk.session.close` |
|
||||
| `talk.handoff.create` | `talk.session.create({ transport: "managed-room" })` |
|
||||
| `talk.handoff.revoke` | `talk.session.close` |
|
||||
|
||||
The unified control vocabulary is also deliberately narrow:
|
||||
|
||||
| Method | Applies to | Contract |
|
||||
| ------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `talk.session.appendAudio` | `realtime/gateway-relay`, `transcription/gateway-relay` | Append a base64 PCM audio chunk to the provider session owned by the same Gateway connection. |
|
||||
| `talk.session.startTurn` | `stt-tts/managed-room` | Start a managed-room user turn. |
|
||||
| `talk.session.endTurn` | `stt-tts/managed-room` | End the active turn after stale-turn validation. |
|
||||
| `talk.session.cancelTurn` | all Gateway-owned sessions | Cancel active capture/provider/agent/TTS work for a turn. |
|
||||
| `talk.session.cancelOutput` | `realtime/gateway-relay` | Stop assistant audio output without necessarily ending the user turn. |
|
||||
| `talk.session.submitToolResult` | `realtime/gateway-relay` | Complete a provider tool call after any asynchronous completion exposed by its bridge; pass `options.willContinue` for interim output or, when supported, `options.suppressResponse` to avoid another assistant response. |
|
||||
| `talk.session.steer` | agent-backed Talk sessions | Send spoken `status`, `steer`, `cancel`, or `followup` control to the active embedded run resolved from the Talk session. |
|
||||
|
||||
@@ -411,7 +411,7 @@ Eligibility is per session and per host. Gateway-local sessions start the provid
|
||||
|
||||
Connection-owned sessions survive disconnects: a page reload, laptop sleep, or network blip detaches the session on the Gateway instead of killing it, and the same browser tab reattaches on reconnect with recent output replayed. Detached connection-owned sessions are killed after `gateway.terminal.detachedSessionTimeoutSeconds` (default 300 seconds; `0` restores kill-on-disconnect). Attaching one of these sessions remains tmux-style take-over.
|
||||
|
||||
Agent-owned sessions are not bound to a browser connection. `terminal.attach` adds each browser as a viewer without taking ownership, and closing a viewer tab detaches only that browser. Conversation-owned PTYs remain until the agent closes them, their process exits, policy disables them, or the Gateway shuts down. PTYs opened by a detached task close automatically when that task succeeds, fails, times out, is cancelled, or is lost. `terminal.list` marks each entry as connection- or agent-owned, and `terminal.text` lets an admin connection read recent plain-text output without attaching.
|
||||
Agent-owned sessions are not bound to a browser connection. `terminal.attach` adds each browser as a viewer without taking ownership, and closing a viewer tab detaches only that browser. Conversation-owned PTYs remain until the agent closes them, their process exits, policy disables them, or the Gateway shuts down. PTYs opened by a detached task close automatically when that task succeeds, fails, times out, is cancelled, or is lost. `terminal.list` marks each entry as connection- or agent-owned.
|
||||
|
||||
The terminal is also available as a [full-screen terminal document](/web/urls#special-documents-and-startup-modes). The iOS and Android apps embed this page in their Terminal screens, reusing the stored gateway credentials; availability follows the same `gateway.terminal.enabled` and `operator.admin` gate, and the page shows a notice when the connected Gateway does not offer the terminal.
|
||||
|
||||
|
||||
@@ -36,12 +36,9 @@ import {
|
||||
validateTalkClientToolCallParams,
|
||||
validateTalkSessionAppendAudioParams,
|
||||
validateTalkSessionCancelOutputParams,
|
||||
validateTalkSessionCancelTurnParams,
|
||||
validateTalkSessionCreateParams,
|
||||
validateTalkSessionJoinParams,
|
||||
validateTalkSessionSubmitToolResultParams,
|
||||
validateTalkSessionSteerParams,
|
||||
validateTalkSessionTurnParams,
|
||||
validateWakeParams,
|
||||
type ValidationError,
|
||||
} from "./index.js";
|
||||
@@ -796,14 +793,6 @@ describe("validateTalkSession", () => {
|
||||
);
|
||||
expectRejected(validateTalkSessionCreateParams, [{ mode: "realtime", language: "de-DE" }]);
|
||||
});
|
||||
|
||||
it("accepts managed-room join and turn lifecycle params", () => {
|
||||
expectAccepted(validateTalkSessionJoinParams, [talkSession({ token: "token-1" })]);
|
||||
expectAccepted(validateTalkSessionTurnParams, [talkSession({ turnId: "turn-1" })]);
|
||||
expectAccepted(validateTalkSessionCancelTurnParams, [
|
||||
talkSession({ turnId: "turn-1", reason: "barge-in" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkClientToolCallParams", () => {
|
||||
@@ -836,11 +825,10 @@ describe("validateTalkAgentControlParams", () => {
|
||||
});
|
||||
|
||||
describe("validateTalkSessionRelayParams", () => {
|
||||
it("accepts session audio, cancel, output cancel, and tool result params", () => {
|
||||
it("accepts session audio, output cancel, and tool result params", () => {
|
||||
expectAccepted(validateTalkSessionAppendAudioParams, [
|
||||
talkSession({ audioBase64: "aGVsbG8=", timestamp: 123 }),
|
||||
]);
|
||||
expectAccepted(validateTalkSessionCancelTurnParams, [talkSession({ reason: "barge-in" })]);
|
||||
expectAccepted(validateTalkSessionCancelOutputParams, [talkSession({ reason: "barge-in" })]);
|
||||
expectAccepted(validateTalkSessionSubmitToolResultParams, [
|
||||
talkSession({
|
||||
|
||||
@@ -218,7 +218,6 @@ export {
|
||||
SessionsDiffParamsSchema,
|
||||
SessionsDiffResultSchema,
|
||||
SessionsCompactionListParamsSchema,
|
||||
SessionsCompactionGetParamsSchema,
|
||||
SessionsCompactionBranchParamsSchema,
|
||||
SessionsCompactionRestoreParamsSchema,
|
||||
SessionBranchSchema,
|
||||
@@ -404,13 +403,8 @@ export {
|
||||
TalkSessionAppendAudioParamsSchema,
|
||||
TalkSessionAcknowledgeMarkParamsSchema,
|
||||
TalkSessionCancelOutputParamsSchema,
|
||||
TalkSessionCancelTurnParamsSchema,
|
||||
TalkSessionCreateParamsSchema,
|
||||
TalkSessionCreateResultSchema,
|
||||
TalkSessionJoinParamsSchema,
|
||||
TalkSessionJoinResultSchema,
|
||||
TalkSessionTurnParamsSchema,
|
||||
TalkSessionTurnResultSchema,
|
||||
TalkSessionSteerParamsSchema,
|
||||
TalkSessionSubmitToolResultParamsSchema,
|
||||
TalkSessionCloseParamsSchema,
|
||||
@@ -549,8 +543,6 @@ export {
|
||||
TerminalAttachResultSchema,
|
||||
TerminalSessionInfoSchema,
|
||||
TerminalListResultSchema,
|
||||
TerminalTextParamsSchema,
|
||||
TerminalTextResultSchema,
|
||||
TerminalUploadParamsSchema,
|
||||
TerminalUploadResultSchema,
|
||||
TerminalAckResultSchema,
|
||||
|
||||
@@ -285,12 +285,6 @@ export const TalkAgentControlResultSchema = closedObject({
|
||||
deliveredAtMs: Type.Optional(Type.Number()),
|
||||
});
|
||||
|
||||
/** Joins an existing managed-room Talk session. */
|
||||
export const TalkSessionJoinParamsSchema = closedObject({
|
||||
sessionId: NonEmptyString,
|
||||
token: NonEmptyString,
|
||||
});
|
||||
|
||||
/** Creates a gateway-managed Talk session for realtime, transcription, or relay use. */
|
||||
export const TalkSessionCreateParamsSchema = closedObject({
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
@@ -316,19 +310,6 @@ export const TalkSessionAppendAudioParamsSchema = closedObject({
|
||||
timestamp: Type.Optional(Type.Number()),
|
||||
});
|
||||
|
||||
/** Starts or advances a Talk turn within a session. */
|
||||
export const TalkSessionTurnParamsSchema = closedObject({
|
||||
sessionId: NonEmptyString,
|
||||
turnId: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
/** Cancels the active or named Talk turn. */
|
||||
export const TalkSessionCancelTurnParamsSchema = closedObject({
|
||||
sessionId: NonEmptyString,
|
||||
turnId: Type.Optional(Type.String()),
|
||||
reason: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
/** Cancels currently streaming Talk output without necessarily ending the turn. */
|
||||
export const TalkSessionCancelOutputParamsSchema = closedObject({
|
||||
sessionId: NonEmptyString,
|
||||
@@ -362,33 +343,6 @@ export const TalkSessionCloseParamsSchema = closedObject({
|
||||
sessionId: NonEmptyString,
|
||||
});
|
||||
|
||||
/** Mutable room state returned when a client joins a managed Talk room. */
|
||||
const TalkSessionManagedRoomStateSchema = closedObject({
|
||||
activeClientId: Type.Optional(Type.String()),
|
||||
activeTurnId: Type.Optional(Type.String()),
|
||||
recentTalkEvents: Type.Array(TalkEventSchema),
|
||||
});
|
||||
|
||||
/** Managed-room session record shared with browser clients. */
|
||||
const TalkSessionManagedRoomRecordSchema = closedObject({
|
||||
id: NonEmptyString,
|
||||
roomId: NonEmptyString,
|
||||
roomUrl: NonEmptyString,
|
||||
sessionKey: NonEmptyString,
|
||||
sessionId: Type.Optional(Type.String()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
target: Type.Optional(Type.String()),
|
||||
provider: Type.Optional(Type.String()),
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
mode: TalkModeSchema,
|
||||
transport: TalkTransportSchema,
|
||||
brain: TalkBrainSchema,
|
||||
createdAt: Type.Number(),
|
||||
expiresAt: Type.Number(),
|
||||
room: TalkSessionManagedRoomStateSchema,
|
||||
});
|
||||
|
||||
/** Empty request payload for reading configured Talk provider capabilities. */
|
||||
export const TalkCatalogParamsSchema = closedObject({});
|
||||
|
||||
@@ -473,16 +427,6 @@ export const TalkSessionCreateResultSchema = closedObject({
|
||||
expiresAt: Type.Optional(Type.Number()),
|
||||
});
|
||||
|
||||
/** Result for a Talk turn request, optionally including emitted events. */
|
||||
export const TalkSessionTurnResultSchema = closedObject({
|
||||
ok: Type.Boolean(),
|
||||
turnId: Type.Optional(Type.String()),
|
||||
events: Type.Optional(Type.Array(TalkEventSchema)),
|
||||
});
|
||||
|
||||
/** Managed-room record returned to clients after joining an existing Talk session. */
|
||||
export const TalkSessionJoinResultSchema = TalkSessionManagedRoomRecordSchema;
|
||||
|
||||
/** Generic success result for Talk session lifecycle calls. */
|
||||
export const TalkSessionOkResultSchema = closedObject({
|
||||
ok: Type.Boolean(),
|
||||
@@ -795,13 +739,8 @@ export type TalkClientCloseParams = Static<typeof TalkClientCloseParamsSchema>;
|
||||
export type TalkClientMutationResult = Static<typeof TalkClientMutationResultSchema>;
|
||||
export type TalkSessionCreateParams = Static<typeof TalkSessionCreateParamsSchema>;
|
||||
export type TalkSessionCreateResult = Static<typeof TalkSessionCreateResultSchema>;
|
||||
export type TalkSessionJoinParams = Static<typeof TalkSessionJoinParamsSchema>;
|
||||
export type TalkSessionJoinResult = Static<typeof TalkSessionJoinResultSchema>;
|
||||
export type TalkSessionAppendAudioParams = Static<typeof TalkSessionAppendAudioParamsSchema>;
|
||||
export type TalkSessionTurnParams = Static<typeof TalkSessionTurnParamsSchema>;
|
||||
export type TalkSessionCancelTurnParams = Static<typeof TalkSessionCancelTurnParamsSchema>;
|
||||
export type TalkSessionCancelOutputParams = Static<typeof TalkSessionCancelOutputParamsSchema>;
|
||||
export type TalkSessionTurnResult = Static<typeof TalkSessionTurnResultSchema>;
|
||||
export type TalkSessionSteerParams = Static<typeof TalkSessionSteerParamsSchema>;
|
||||
export type TalkSessionSubmitToolResultParams = Static<
|
||||
typeof TalkSessionSubmitToolResultParamsSchema
|
||||
|
||||
@@ -21,13 +21,8 @@ export const ChannelProtocolSchemas = {
|
||||
TalkSessionAppendAudioParams: channels.TalkSessionAppendAudioParamsSchema,
|
||||
TalkSessionAcknowledgeMarkParams: talkMarks.TalkSessionAcknowledgeMarkParamsSchema,
|
||||
TalkSessionCancelOutputParams: channels.TalkSessionCancelOutputParamsSchema,
|
||||
TalkSessionCancelTurnParams: channels.TalkSessionCancelTurnParamsSchema,
|
||||
TalkSessionCreateParams: channels.TalkSessionCreateParamsSchema,
|
||||
TalkSessionCreateResult: channels.TalkSessionCreateResultSchema,
|
||||
TalkSessionJoinParams: channels.TalkSessionJoinParamsSchema,
|
||||
TalkSessionJoinResult: channels.TalkSessionJoinResultSchema,
|
||||
TalkSessionTurnParams: channels.TalkSessionTurnParamsSchema,
|
||||
TalkSessionTurnResult: channels.TalkSessionTurnResultSchema,
|
||||
TalkSessionSteerParams: channels.TalkSessionSteerParamsSchema,
|
||||
TalkSessionSubmitToolResultParams: channels.TalkSessionSubmitToolResultParamsSchema,
|
||||
TalkSessionCloseParams: channels.TalkSessionCloseParamsSchema,
|
||||
|
||||
@@ -3,11 +3,9 @@ import * as sessions from "./sessions.js";
|
||||
|
||||
export const SessionLifecycleProtocolSchemas = {
|
||||
SessionsCompactionListParams: sessions.SessionsCompactionListParamsSchema,
|
||||
SessionsCompactionGetParams: sessions.SessionsCompactionGetParamsSchema,
|
||||
SessionsCompactionBranchParams: sessions.SessionsCompactionBranchParamsSchema,
|
||||
SessionsCompactionRestoreParams: sessions.SessionsCompactionRestoreParamsSchema,
|
||||
SessionsCompactionListResult: sessions.SessionsCompactionListResultSchema,
|
||||
SessionsCompactionGetResult: sessions.SessionsCompactionGetResultSchema,
|
||||
SessionsCompactionBranchResult: sessions.SessionsCompactionBranchResultSchema,
|
||||
SessionsCompactionRestoreResult: sessions.SessionsCompactionRestoreResultSchema,
|
||||
SessionsRewindParams: sessions.SessionsRewindParamsSchema,
|
||||
|
||||
@@ -582,13 +582,6 @@ export const SessionsCompactionListParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
/** Reads one compaction checkpoint by id. */
|
||||
export const SessionsCompactionGetParamsSchema = closedObject({
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
checkpointId: NonEmptyString,
|
||||
});
|
||||
|
||||
/** Creates a new branch from a compaction checkpoint. */
|
||||
export const SessionsCompactionBranchParamsSchema = closedObject({
|
||||
key: NonEmptyString,
|
||||
@@ -667,13 +660,6 @@ export const SessionsCompactionListResultSchema = closedObject({
|
||||
checkpoints: Type.Array(SessionCompactionCheckpointSchema),
|
||||
});
|
||||
|
||||
/** Get response for a single compaction checkpoint. */
|
||||
export const SessionsCompactionGetResultSchema = closedObject({
|
||||
ok: Type.Literal(true),
|
||||
key: NonEmptyString,
|
||||
checkpoint: SessionCompactionCheckpointSchema,
|
||||
});
|
||||
|
||||
/** Branch response with the newly created session key and entry metadata. */
|
||||
export const SessionsCompactionBranchResultSchema = closedObject({
|
||||
ok: Type.Literal(true),
|
||||
@@ -784,11 +770,9 @@ export type SessionsCompanionStateResult = Static<typeof SessionsCompanionStateR
|
||||
export type SessionsCompanionResetParams = Static<typeof SessionsCompanionResetParamsSchema>;
|
||||
export type SessionsCompanionResetResult = Static<typeof SessionsCompanionResetResultSchema>;
|
||||
export type SessionsCompactionListParams = Static<typeof SessionsCompactionListParamsSchema>;
|
||||
export type SessionsCompactionGetParams = Static<typeof SessionsCompactionGetParamsSchema>;
|
||||
export type SessionsCompactionBranchParams = Static<typeof SessionsCompactionBranchParamsSchema>;
|
||||
export type SessionsCompactionRestoreParams = Static<typeof SessionsCompactionRestoreParamsSchema>;
|
||||
export type SessionsCompactionListResult = Static<typeof SessionsCompactionListResultSchema>;
|
||||
export type SessionsCompactionGetResult = Static<typeof SessionsCompactionGetResultSchema>;
|
||||
export type SessionsCompactionBranchResult = Static<typeof SessionsCompactionBranchResultSchema>;
|
||||
export type SessionsCompactionRestoreResult = Static<typeof SessionsCompactionRestoreResultSchema>;
|
||||
export type SessionsRewindParams = Static<typeof SessionsRewindParamsSchema>;
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
TerminalOpenResultSchema,
|
||||
TerminalResizeParamsSchema,
|
||||
TerminalSessionInfoSchema,
|
||||
TerminalTextParamsSchema,
|
||||
TerminalTextResultSchema,
|
||||
TerminalUploadParamsSchema,
|
||||
TerminalUploadResultSchema,
|
||||
} from "./terminal.js";
|
||||
@@ -28,8 +26,6 @@ export const TerminalProtocolSchemas = {
|
||||
TerminalAttachResult: TerminalAttachResultSchema,
|
||||
TerminalSessionInfo: TerminalSessionInfoSchema,
|
||||
TerminalListResult: TerminalListResultSchema,
|
||||
TerminalTextParams: TerminalTextParamsSchema,
|
||||
TerminalTextResult: TerminalTextResultSchema,
|
||||
TerminalUploadParams: TerminalUploadParamsSchema,
|
||||
TerminalUploadResult: TerminalUploadResultSchema,
|
||||
TerminalAckResult: TerminalAckResultSchema,
|
||||
|
||||
@@ -126,14 +126,6 @@ export const TerminalListResultSchema = closedObject({
|
||||
});
|
||||
export type TerminalListResult = Static<typeof TerminalListResultSchema>;
|
||||
|
||||
/** Reads the current output buffer as plain text without attaching. */
|
||||
export const TerminalTextParamsSchema = closedObject({ sessionId: NonEmptyString });
|
||||
export type TerminalTextParams = Static<typeof TerminalTextParamsSchema>;
|
||||
|
||||
/** Plain-text buffer contents (ANSI stripped); an agent/LLM affordance. */
|
||||
export const TerminalTextResultSchema = closedObject({ text: Type.String() });
|
||||
export type TerminalTextResult = Static<typeof TerminalTextResultSchema>;
|
||||
|
||||
/** Shared ok/void result for input, resize, and close. */
|
||||
export const TerminalAckResultSchema = closedObject({ ok: Type.Boolean() });
|
||||
export type TerminalAckResult = Static<typeof TerminalAckResultSchema>;
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TerminalInputParamsSchema,
|
||||
TerminalOpenParamsSchema,
|
||||
TerminalResizeParamsSchema,
|
||||
TerminalTextParamsSchema,
|
||||
TerminalUploadParamsSchema,
|
||||
TerminalUploadResultSchema,
|
||||
} from "./schema/terminal.js";
|
||||
@@ -15,6 +14,5 @@ export const validateTerminalInputParams = lazyCompile(TerminalInputParamsSchema
|
||||
export const validateTerminalResizeParams = lazyCompile(TerminalResizeParamsSchema);
|
||||
export const validateTerminalCloseParams = lazyCompile(TerminalCloseParamsSchema);
|
||||
export const validateTerminalAttachParams = lazyCompile(TerminalAttachParamsSchema);
|
||||
export const validateTerminalTextParams = lazyCompile(TerminalTextParamsSchema);
|
||||
export const validateTerminalUploadParams = lazyCompile(TerminalUploadParamsSchema);
|
||||
export const validateTerminalUploadResult = lazyCompile(TerminalUploadResultSchema);
|
||||
|
||||
@@ -243,7 +243,6 @@ export const validateSessionsGroupsDeleteParams = compile(S.SessionsGroupsDelete
|
||||
export const validateSessionsGroupsMutationResult = compile(S.SessionsGroupsMutationResultSchema);
|
||||
export const validateSessionsCompactParams = compile(S.SessionsCompactParamsSchema);
|
||||
export const validateSessionsCompactionListParams = compile(S.SessionsCompactionListParamsSchema);
|
||||
export const validateSessionsCompactionGetParams = compile(S.SessionsCompactionGetParamsSchema);
|
||||
export const validateSessionsCompactionBranchParams = compile(
|
||||
S.SessionsCompactionBranchParamsSchema,
|
||||
);
|
||||
@@ -302,13 +301,10 @@ export const validateTalkClientToolCallResult = compile(S.TalkClientToolCallResu
|
||||
export const validateTalkClientTranscriptParams = compile(S.TalkClientTranscriptParamsSchema);
|
||||
export const validateTalkClientSteerParams = compile(S.TalkClientSteerParamsSchema);
|
||||
export const validateTalkSessionCreateParams = compile(S.TalkSessionCreateParamsSchema);
|
||||
export const validateTalkSessionJoinParams = compile(S.TalkSessionJoinParamsSchema);
|
||||
export const validateTalkSessionAppendAudioParams = compile(S.TalkSessionAppendAudioParamsSchema);
|
||||
export const validateTalkSessionAcknowledgeMarkParams = compile(
|
||||
S.TalkSessionAcknowledgeMarkParamsSchema,
|
||||
);
|
||||
export const validateTalkSessionTurnParams = compile(S.TalkSessionTurnParamsSchema);
|
||||
export const validateTalkSessionCancelTurnParams = compile(S.TalkSessionCancelTurnParamsSchema);
|
||||
export const validateTalkSessionCancelOutputParams = compile(S.TalkSessionCancelOutputParamsSchema);
|
||||
export const validateTalkSessionSteerParams = compile(S.TalkSessionSteerParamsSchema);
|
||||
export const validateTalkSessionSubmitToolResultParams = compile(
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
buildHashedArgPatternFromArgv,
|
||||
resolvePolicyTargetCandidatePath,
|
||||
} from "../infra/exec-command-resolution.js";
|
||||
import { createSafeGatewayRestartPreflight } from "../infra/restart-coordinator.js";
|
||||
import {
|
||||
getActiveGatewayRootWorkCount,
|
||||
markGatewayRestartDraining,
|
||||
@@ -2388,21 +2387,7 @@ EOF`,
|
||||
|
||||
suspension?.release();
|
||||
await spawnStarted;
|
||||
expect(
|
||||
createSafeGatewayRestartPreflight({
|
||||
getQueueSize: () => 0,
|
||||
getPendingReplies: () => 0,
|
||||
getEmbeddedRuns: () => 0,
|
||||
getCronRuns: () => 0,
|
||||
getBackgroundExecSessions: () => 0,
|
||||
getActiveTasks: () => 0,
|
||||
getTaskBlockers: () => [],
|
||||
}),
|
||||
).toMatchObject({
|
||||
safe: false,
|
||||
counts: { rootRequests: 1, totalActive: 1 },
|
||||
blockers: [{ kind: "root-request", count: 1 }],
|
||||
});
|
||||
expect(getActiveGatewayRootWorkCount()).toBe(1);
|
||||
allowSpawn();
|
||||
await vi.waitFor(() => {
|
||||
expect(markBackgroundedMock).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -100,11 +100,7 @@ describe("method scope resolution", () => {
|
||||
["talk.client.toolCall", ["operator.talk"]],
|
||||
["talk.client.steer", ["operator.talk"]],
|
||||
["talk.session.create", ["operator.talk"]],
|
||||
["talk.session.join", ["operator.talk"]],
|
||||
["talk.session.appendAudio", ["operator.talk"]],
|
||||
["talk.session.startTurn", ["operator.talk"]],
|
||||
["talk.session.endTurn", ["operator.talk"]],
|
||||
["talk.session.cancelTurn", ["operator.talk"]],
|
||||
["talk.session.cancelOutput", ["operator.talk"]],
|
||||
["talk.session.acknowledgeMark", ["operator.talk"]],
|
||||
["talk.session.submitToolResult", ["operator.talk"]],
|
||||
@@ -701,11 +697,7 @@ describe("operator scope authorization", () => {
|
||||
"talk.client.toolCall",
|
||||
"talk.client.steer",
|
||||
"talk.session.create",
|
||||
"talk.session.join",
|
||||
"talk.session.appendAudio",
|
||||
"talk.session.startTurn",
|
||||
"talk.session.endTurn",
|
||||
"talk.session.cancelTurn",
|
||||
"talk.session.cancelOutput",
|
||||
"talk.session.acknowledgeMark",
|
||||
"talk.session.submitToolResult",
|
||||
|
||||
@@ -26,7 +26,6 @@ const CURRENT_TRAIN_METHODS = [
|
||||
"terminal.close",
|
||||
"terminal.attach",
|
||||
"terminal.list",
|
||||
"terminal.text",
|
||||
"terminal.upload",
|
||||
"worktrees.list",
|
||||
"worktrees.branches",
|
||||
|
||||
@@ -43,7 +43,6 @@ const CORE_GATEWAY_METHOD_SPECS = [
|
||||
["doctor.memory.resetGroundedShortTerm", "doctor", "operator.write", "<=2026.7"],
|
||||
["doctor.memory.repairDreamingArtifacts", "doctor", "operator.write", "<=2026.7"],
|
||||
["doctor.memory.dedupeDreamDiary", "doctor", "operator.write", "<=2026.7"],
|
||||
["doctor.memory.remHarness", "doctor", "operator.read", "<=2026.7"],
|
||||
["logs.tail", "logs", "operator.read", "<=2026.7"],
|
||||
["channels.status", "channels", "operator.read", "<=2026.7"],
|
||||
["channels.start", "channels", "operator.admin", "<=2026.7"],
|
||||
@@ -109,11 +108,7 @@ const CORE_GATEWAY_METHOD_SPECS = [
|
||||
["talk.client.toolCall", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.client.steer", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.create", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.join", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.appendAudio", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.startTurn", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.endTurn", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.cancelTurn", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.cancelOutput", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.acknowledgeMark", "talk", "operator.talk", "<=2026.7"],
|
||||
["talk.session.submitToolResult", "talk", "operator.talk", "<=2026.7"],
|
||||
@@ -222,17 +217,14 @@ const CORE_GATEWAY_METHOD_SPECS = [
|
||||
["secrets.reload", null, "operator.admin", "<=2026.7"],
|
||||
["secrets.resolve", null, "operator.admin", "<=2026.7"],
|
||||
["voicewake.routing.get", "voicewake-routing", "operator.read", "<=2026.7"],
|
||||
["voicewake.routing.set", "voicewake-routing", "operator.write", "<=2026.7"],
|
||||
["sessions.list", "sessions-read", "operator.read", "<=2026.7", { startup: true }],
|
||||
["sessions.subscribe", "sessions-subscriptions", "operator.read", "<=2026.7"],
|
||||
["sessions.unsubscribe", "sessions-subscriptions", "operator.read", "<=2026.7"],
|
||||
["sessions.messages.subscribe", "sessions-subscriptions", "operator.read", "<=2026.7"],
|
||||
["sessions.messages.unsubscribe", "sessions-subscriptions", "operator.read", "<=2026.7"],
|
||||
["sessions.viewers.set", "sessions-subscriptions", "operator.read", "2026.7"],
|
||||
["sessions.preview", "sessions-read", "operator.read", "<=2026.7"],
|
||||
["sessions.describe", "sessions-read", "operator.read", "<=2026.7"],
|
||||
["sessions.compaction.list", "sessions-compaction-queries", "operator.read", "<=2026.7"],
|
||||
["sessions.compaction.get", "sessions-compaction-queries", "operator.read", "<=2026.7"],
|
||||
["sessions.compaction.branch", "sessions-compaction-checkpoints", "operator.write", "<=2026.7"],
|
||||
["sessions.compaction.restore", "sessions-compaction-checkpoints", "operator.admin", "<=2026.7"],
|
||||
["sessions.branches.list", "sessions-rewind", "operator.read", "<=2026.7"],
|
||||
@@ -307,7 +299,6 @@ const CORE_GATEWAY_METHOD_SPECS = [
|
||||
["cron.run", "cron", "operator.admin", "<=2026.7"],
|
||||
["cron.runs", "cron", "operator.read", "<=2026.7"],
|
||||
["gateway.identity.get", "system", "operator.read", "<=2026.7"],
|
||||
["gateway.restart.preflight", "restart", "operator.read", "<=2026.7"],
|
||||
["gateway.restart.request", "restart", "operator.admin", "<=2026.7", { controlPlaneWrite: true }],
|
||||
["system-presence", "system", "operator.read", "<=2026.7"],
|
||||
["system-event", "system", "operator.admin", "<=2026.7"],
|
||||
@@ -362,7 +353,6 @@ const CORE_GATEWAY_METHOD_SPECS = [
|
||||
// advertised method indices stay stable for older clients; new methods append.
|
||||
["terminal.attach", "terminal", "operator.admin", "2026.7"],
|
||||
["terminal.list", "terminal", "operator.admin", "2026.7"],
|
||||
["terminal.text", "terminal", "operator.admin", "2026.7"],
|
||||
["controlUi.githubPreview", "control-ui", "operator.read", "<=2026.7"],
|
||||
// Additive discovery methods append here so older clients keep stable indices.
|
||||
["system.info", "system", "operator.read", "<=2026.7"],
|
||||
|
||||
@@ -166,7 +166,7 @@ describe("listGatewayMethods", () => {
|
||||
"doctor.memory.dreamDiary",
|
||||
"doctor.memory.backfillDreamDiary",
|
||||
]);
|
||||
expect(methods.slice(32, 37)).toEqual([
|
||||
expect(methods.slice(31, 36)).toEqual([
|
||||
"exec.approvals.get",
|
||||
"exec.approvals.set",
|
||||
"exec.approvals.node.get",
|
||||
@@ -239,11 +239,7 @@ describe("listGatewayMethods", () => {
|
||||
expect(methods).toContain("talk.client.toolCall");
|
||||
expect(methods).toContain("talk.client.steer");
|
||||
expect(methods).toContain("talk.session.create");
|
||||
expect(methods).toContain("talk.session.join");
|
||||
expect(methods).toContain("talk.session.appendAudio");
|
||||
expect(methods).toContain("talk.session.startTurn");
|
||||
expect(methods).toContain("talk.session.endTurn");
|
||||
expect(methods).toContain("talk.session.cancelTurn");
|
||||
expect(methods).toContain("talk.session.cancelOutput");
|
||||
expect(methods).toContain("talk.session.acknowledgeMark");
|
||||
expect(methods).toContain("talk.session.submitToolResult");
|
||||
|
||||
@@ -8,7 +8,6 @@ export {
|
||||
dedupeDreamDiaryEntries,
|
||||
loadShortTermPromotionDreamingStats,
|
||||
previewGroundedRemMarkdown,
|
||||
previewRemHarness,
|
||||
removeBackfillDiaryEntries,
|
||||
removeGroundedShortTermCandidates,
|
||||
repairDreamingArtifacts,
|
||||
|
||||
@@ -22,7 +22,6 @@ const resolveMemorySearchConfig = vi.hoisted(() =>
|
||||
);
|
||||
const getMemorySearchManager = vi.hoisted(() => vi.fn());
|
||||
const previewGroundedRemMarkdown = vi.hoisted(() => vi.fn());
|
||||
const previewRemHarness = vi.hoisted(() => vi.fn());
|
||||
const dedupeDreamDiaryEntries = vi.hoisted(() => vi.fn());
|
||||
const writeBackfillDiaryEntries = vi.hoisted(() => vi.fn());
|
||||
const removeBackfillDiaryEntries = vi.hoisted(() => vi.fn());
|
||||
@@ -62,7 +61,6 @@ vi.mock("./doctor.memory-core-runtime.js", () => ({
|
||||
dedupeDreamDiaryEntries,
|
||||
loadShortTermPromotionDreamingStats,
|
||||
previewGroundedRemMarkdown,
|
||||
previewRemHarness,
|
||||
writeBackfillDiaryEntries,
|
||||
removeBackfillDiaryEntries,
|
||||
removeGroundedShortTermCandidates,
|
||||
@@ -83,9 +81,7 @@ const DOCTOR_MEMORY_TARGET_METHODS = [
|
||||
"doctor.memory.dedupeDreamDiary",
|
||||
] as const;
|
||||
|
||||
type DoctorMemoryMethod =
|
||||
| (typeof DOCTOR_MEMORY_TARGET_METHODS)[number]
|
||||
| "doctor.memory.remHarness";
|
||||
type DoctorMemoryMethod = (typeof DOCTOR_MEMORY_TARGET_METHODS)[number];
|
||||
|
||||
const invokeDoctorMemory = async (
|
||||
method: DoctorMemoryMethod,
|
||||
@@ -1470,277 +1466,4 @@ describe("doctor.memory.dreamDiary", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("doctor.memory.remHarness", () => {
|
||||
const makeHarnessPreview = (
|
||||
overrides: Partial<{
|
||||
workspaceDir: string;
|
||||
remSkipped: boolean;
|
||||
rem: Record<string, unknown>;
|
||||
grounded: Record<string, unknown> | null;
|
||||
deep: Record<string, unknown>;
|
||||
remConfig: Record<string, unknown>;
|
||||
deepConfig: Record<string, unknown>;
|
||||
}> = {},
|
||||
) => ({
|
||||
workspaceDir: overrides.workspaceDir ?? "/tmp/openclaw",
|
||||
nowMs: 0,
|
||||
remConfig: {
|
||||
enabled: true,
|
||||
lookbackDays: 7,
|
||||
limit: 25,
|
||||
minPatternStrength: 0.35,
|
||||
...overrides.remConfig,
|
||||
},
|
||||
deepConfig: {
|
||||
minScore: 0.75,
|
||||
minRecallCount: 3,
|
||||
minUniqueQueries: 2,
|
||||
recencyHalfLifeDays: 14,
|
||||
...overrides.deepConfig,
|
||||
},
|
||||
recallEntryCount: 0,
|
||||
remSkipped: overrides.remSkipped ?? false,
|
||||
rem: {
|
||||
sourceEntryCount: 0,
|
||||
reflections: [],
|
||||
candidateTruths: [],
|
||||
candidateKeys: [],
|
||||
bodyLines: [],
|
||||
...overrides.rem,
|
||||
},
|
||||
grounded: overrides.grounded ?? null,
|
||||
groundedInputPaths: [],
|
||||
deep: {
|
||||
candidateLimit: 25,
|
||||
candidateCount: 0,
|
||||
truncated: false,
|
||||
candidates: [],
|
||||
...overrides.deep,
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
getRuntimeConfig.mockClear().mockReturnValue({} as OpenClawConfig);
|
||||
resolveDefaultAgentId.mockClear().mockReturnValue("main");
|
||||
resolveAgentWorkspaceDir.mockReset().mockReturnValue("/tmp/openclaw");
|
||||
previewRemHarness.mockReset().mockResolvedValue(makeHarnessPreview());
|
||||
previewGroundedRemMarkdown.mockReset();
|
||||
});
|
||||
|
||||
it("returns an empty preview payload for an empty workspace", async () => {
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemory("doctor.memory.remHarness", respond);
|
||||
|
||||
expectRecordFields(mockCallArg(previewRemHarness), {
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
grounded: false,
|
||||
includePromoted: false,
|
||||
candidateLimit: 25,
|
||||
groundedFileLimit: 10,
|
||||
remPreviewLimit: 50,
|
||||
});
|
||||
expect(previewGroundedRemMarkdown).not.toHaveBeenCalled();
|
||||
const payload = respondPayload(respond);
|
||||
expectRecordFields(payload, {
|
||||
ok: true,
|
||||
agentId: "main",
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
grounded: null,
|
||||
});
|
||||
expectRecordFields(payload.rem, {
|
||||
skipped: false,
|
||||
sourceEntryCount: 0,
|
||||
reflections: [],
|
||||
candidateTruths: [],
|
||||
});
|
||||
expectRecordFields(payload.deep, {
|
||||
candidateLimit: 25,
|
||||
truncated: false,
|
||||
candidates: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("maps REM preview and deep candidates into the payload", async () => {
|
||||
previewRemHarness.mockResolvedValue(
|
||||
makeHarnessPreview({
|
||||
rem: {
|
||||
sourceEntryCount: 2,
|
||||
reflections: ["reflection line"],
|
||||
candidateTruths: [{ snippet: "truthy snippet", confidence: 0.72, evidence: "a" }],
|
||||
candidateKeys: ["a"],
|
||||
bodyLines: ["## REM", "- truthy snippet"],
|
||||
},
|
||||
deep: {
|
||||
candidates: [
|
||||
{
|
||||
key: "memory/2026-04-14.md:12:16",
|
||||
path: "memory/2026-04-14.md",
|
||||
startLine: 12,
|
||||
endLine: 16,
|
||||
source: "memory",
|
||||
snippet: "durable fact",
|
||||
recallCount: 4,
|
||||
uniqueQueries: 3,
|
||||
avgScore: 0.81,
|
||||
maxScore: 0.92,
|
||||
ageDays: 1,
|
||||
firstRecalledAt: "2026-04-13T10:00:00.000Z",
|
||||
lastRecalledAt: "2026-04-14T10:00:00.000Z",
|
||||
promotedAt: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemory("doctor.memory.remHarness", respond);
|
||||
|
||||
const payload = respondPayload(respond);
|
||||
expectRecordFields(payload, { ok: true });
|
||||
expectRecordFields(payload.rem, {
|
||||
reflections: ["reflection line"],
|
||||
candidateTruths: [{ snippet: "truthy snippet", confidence: 0.72 }],
|
||||
bodyLines: ["## REM", "- truthy snippet"],
|
||||
});
|
||||
const deep = expectRecordFields(payload.deep, {
|
||||
candidateLimit: 25,
|
||||
truncated: false,
|
||||
});
|
||||
expectRecordFields((deep.candidates as unknown[])[0], {
|
||||
key: "memory/2026-04-14.md:12:16",
|
||||
path: "memory/2026-04-14.md",
|
||||
snippet: "durable fact",
|
||||
recallCount: 4,
|
||||
uniqueQueries: 3,
|
||||
avgScore: 0.81,
|
||||
promoted: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("invokes grounded preview when grounded=true and daily files exist", async () => {
|
||||
previewRemHarness.mockResolvedValue(
|
||||
makeHarnessPreview({
|
||||
grounded: {
|
||||
scannedFiles: 2,
|
||||
files: [
|
||||
{ path: "memory/2026-04-13.md", renderedMarkdown: "## REM\n- a" },
|
||||
{ path: "memory/2026-04-14.md", renderedMarkdown: "## REM\n- b" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemory("doctor.memory.remHarness", respond, {
|
||||
params: { grounded: true },
|
||||
});
|
||||
|
||||
expectRecordFields(mockCallArg(previewRemHarness), { grounded: true });
|
||||
const payload = respondPayload(respond);
|
||||
expectRecordFields(payload.grounded, {
|
||||
scannedFiles: 2,
|
||||
files: [
|
||||
{ path: "memory/2026-04-13.md", renderedMarkdown: "## REM\n- a" },
|
||||
{ path: "memory/2026-04-14.md", renderedMarkdown: "## REM\n- b" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("passes bounded grounded and REM preview limits to the shared harness", async () => {
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemory("doctor.memory.remHarness", respond, {
|
||||
params: { grounded: true },
|
||||
});
|
||||
|
||||
expectRecordFields(mockCallArg(previewRemHarness), {
|
||||
grounded: true,
|
||||
groundedFileLimit: 10,
|
||||
remPreviewLimit: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps requested empty grounded preview into an empty payload", async () => {
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemory("doctor.memory.remHarness", respond, {
|
||||
params: { grounded: true },
|
||||
});
|
||||
|
||||
expectRecordFields(respondPayload(respond), {
|
||||
grounded: { scannedFiles: 0, files: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it("returns an error payload when the recall store read fails", async () => {
|
||||
previewRemHarness.mockRejectedValue(new Error("disk boom"));
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemory("doctor.memory.remHarness", respond);
|
||||
|
||||
const payload = respondPayload(respond);
|
||||
expectRecordFields(payload, {
|
||||
ok: false,
|
||||
agentId: "main",
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
});
|
||||
expect(String(payload.error)).toContain("disk boom");
|
||||
});
|
||||
|
||||
it("caps deep candidates and reports truncated when the store exceeds the limit", async () => {
|
||||
const overflowCandidate = (index: number) => ({
|
||||
key: `memory/2026-04-14.md:${index}:${index + 1}`,
|
||||
path: "memory/2026-04-14.md",
|
||||
startLine: index,
|
||||
endLine: index + 1,
|
||||
source: "memory",
|
||||
snippet: `snippet-${index}`,
|
||||
recallCount: 3,
|
||||
uniqueQueries: 2,
|
||||
avgScore: 0.6,
|
||||
maxScore: 0.9,
|
||||
ageDays: 1,
|
||||
firstRecalledAt: "2026-04-13T10:00:00.000Z",
|
||||
lastRecalledAt: "2026-04-14T10:00:00.000Z",
|
||||
promotedAt: undefined,
|
||||
});
|
||||
previewRemHarness.mockResolvedValue(
|
||||
makeHarnessPreview({
|
||||
deep: {
|
||||
candidateLimit: 25,
|
||||
candidateCount: 25,
|
||||
truncated: true,
|
||||
candidates: Array.from({ length: 25 }, (_unused, index) => overflowCandidate(index)),
|
||||
},
|
||||
}),
|
||||
);
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemory("doctor.memory.remHarness", respond);
|
||||
|
||||
expectRecordFields(mockCallArg(previewRemHarness), { candidateLimit: 25 });
|
||||
const payload = respondPayload(respond) as {
|
||||
ok: boolean;
|
||||
deep: { candidateLimit: number; truncated: boolean; candidates: unknown[] };
|
||||
};
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.deep.candidateLimit).toBe(25);
|
||||
expect(payload.deep.truncated).toBe(true);
|
||||
expect(payload.deep.candidates).toHaveLength(25);
|
||||
});
|
||||
|
||||
it("clamps caller-supplied limit within [1, REM_HARNESS_MAX_CANDIDATE_LIMIT]", async () => {
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemory("doctor.memory.remHarness", respond, { params: { limit: 500 } });
|
||||
|
||||
expectRecordFields(mockCallArg(previewRemHarness), { candidateLimit: 100 });
|
||||
const payload = respondPayload(respond) as {
|
||||
deep: { candidateLimit: number };
|
||||
};
|
||||
expect(payload.deep.candidateLimit).toBe(100);
|
||||
});
|
||||
});
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Doctor gateway methods inspect and repair memory dreaming artifacts, managed
|
||||
// cron state, and REM harness previews for operator diagnostics.
|
||||
// Doctor gateway methods inspect and repair memory dreaming artifacts and managed cron state.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
dedupeDreamDiaryEntries,
|
||||
loadShortTermPromotionDreamingStats,
|
||||
previewGroundedRemMarkdown,
|
||||
previewRemHarness,
|
||||
removeBackfillDiaryEntries,
|
||||
removeGroundedShortTermCandidates,
|
||||
repairDreamingArtifacts,
|
||||
@@ -39,10 +37,6 @@ const MANAGED_DEEP_SLEEP_CRON_NAME = "Memory Dreaming Promotion";
|
||||
const MANAGED_DEEP_SLEEP_CRON_TAG = "[managed-by=memory-core.short-term-promotion]";
|
||||
const DEEP_SLEEP_SYSTEM_EVENT_TEXT = "__openclaw_memory_core_short_term_promotion_dream__";
|
||||
const DREAM_DIARY_FILE_NAMES = ["DREAMS.md", "dreams.md"] as const;
|
||||
const REM_HARNESS_DEFAULT_CANDIDATE_LIMIT = 25;
|
||||
const REM_HARNESS_MAX_CANDIDATE_LIMIT = 100;
|
||||
const REM_HARNESS_MAX_GROUNDED_FILES = 10;
|
||||
const REM_HARNESS_MAX_REM_PREVIEW_LIMIT = 50;
|
||||
|
||||
type DoctorMemoryDreamingPhasePayload = {
|
||||
enabled: boolean;
|
||||
@@ -191,75 +185,6 @@ export type DoctorMemoryDreamActionPayload = {
|
||||
keptEntries?: number;
|
||||
};
|
||||
|
||||
export type DoctorMemoryRemHarnessCandidatePayload = {
|
||||
key: string;
|
||||
path: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
snippet: string;
|
||||
recallCount: number;
|
||||
uniqueQueries: number;
|
||||
avgScore: number;
|
||||
maxScore: number;
|
||||
ageDays: number;
|
||||
firstRecalledAt: string;
|
||||
lastRecalledAt: string;
|
||||
promoted: boolean;
|
||||
promotedAt?: string;
|
||||
};
|
||||
|
||||
export type DoctorMemoryRemHarnessCandidateTruthPayload = {
|
||||
snippet: string;
|
||||
confidence: number;
|
||||
};
|
||||
|
||||
export type DoctorMemoryRemHarnessGroundedFilePayload = {
|
||||
path: string;
|
||||
renderedMarkdown: string;
|
||||
};
|
||||
|
||||
export type DoctorMemoryRemHarnessSuccessPayload = {
|
||||
ok: true;
|
||||
agentId: string;
|
||||
workspaceDir: string;
|
||||
remConfig: {
|
||||
enabled: boolean;
|
||||
lookbackDays: number;
|
||||
limit: number;
|
||||
minPatternStrength: number;
|
||||
};
|
||||
deepConfig: {
|
||||
minScore: number;
|
||||
minRecallCount: number;
|
||||
minUniqueQueries: number;
|
||||
recencyHalfLifeDays: number;
|
||||
maxAgeDays: number | null;
|
||||
};
|
||||
rem: {
|
||||
skipped: boolean;
|
||||
sourceEntryCount: number;
|
||||
reflections: string[];
|
||||
candidateTruths: DoctorMemoryRemHarnessCandidateTruthPayload[];
|
||||
bodyLines: string[];
|
||||
};
|
||||
grounded: {
|
||||
scannedFiles: number;
|
||||
files: DoctorMemoryRemHarnessGroundedFilePayload[];
|
||||
} | null;
|
||||
deep: {
|
||||
candidateLimit: number;
|
||||
truncated: boolean;
|
||||
candidates: DoctorMemoryRemHarnessCandidatePayload[];
|
||||
};
|
||||
};
|
||||
|
||||
export type DoctorMemoryRemHarnessErrorPayload = {
|
||||
ok: false;
|
||||
agentId: string;
|
||||
workspaceDir: string;
|
||||
error: string;
|
||||
};
|
||||
|
||||
function extractIsoDayFromPath(filePath: string): string | null {
|
||||
const match = filePath.replaceAll("\\", "/").match(/(\d{4}-\d{2}-\d{2})(?:-[^/]+)?\.md$/i);
|
||||
return match?.[1] ?? null;
|
||||
@@ -1031,110 +956,5 @@ export const doctorHandlers: GatewayRequestHandlers = {
|
||||
};
|
||||
respond(true, payload, undefined);
|
||||
},
|
||||
"doctor.memory.remHarness": async ({ params, respond, context }) => {
|
||||
const cfg = context.getRuntimeConfig();
|
||||
const agentId = resolveDefaultAgentId(cfg);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const req = asOptionalRecord(params);
|
||||
const grounded = Boolean(req?.grounded);
|
||||
const includePromoted = Boolean(req?.includePromoted);
|
||||
const requestedLimit =
|
||||
typeof req?.limit === "number" && Number.isFinite(req.limit)
|
||||
? Math.floor(req.limit)
|
||||
: REM_HARNESS_DEFAULT_CANDIDATE_LIMIT;
|
||||
const candidateLimit = Math.max(1, Math.min(REM_HARNESS_MAX_CANDIDATE_LIMIT, requestedLimit));
|
||||
try {
|
||||
const preview = await previewRemHarness({
|
||||
workspaceDir,
|
||||
cfg,
|
||||
pluginConfig: resolveMemoryDreamingPluginConfig(cfg),
|
||||
grounded,
|
||||
includePromoted,
|
||||
candidateLimit,
|
||||
groundedFileLimit: REM_HARNESS_MAX_GROUNDED_FILES,
|
||||
remPreviewLimit: REM_HARNESS_MAX_REM_PREVIEW_LIMIT,
|
||||
});
|
||||
const groundedPayload: DoctorMemoryRemHarnessSuccessPayload["grounded"] = preview.grounded
|
||||
? {
|
||||
scannedFiles: preview.grounded.scannedFiles,
|
||||
files: preview.grounded.files.map((file) => ({
|
||||
path: file.path,
|
||||
renderedMarkdown: file.renderedMarkdown,
|
||||
})),
|
||||
}
|
||||
: grounded
|
||||
? { scannedFiles: 0, files: [] }
|
||||
: null;
|
||||
|
||||
const payload: DoctorMemoryRemHarnessSuccessPayload = {
|
||||
ok: true,
|
||||
agentId,
|
||||
workspaceDir,
|
||||
remConfig: {
|
||||
enabled: preview.remConfig.enabled,
|
||||
lookbackDays: preview.remConfig.lookbackDays,
|
||||
limit: preview.remConfig.limit,
|
||||
minPatternStrength: preview.remConfig.minPatternStrength,
|
||||
},
|
||||
deepConfig: {
|
||||
minScore: preview.deepConfig.minScore,
|
||||
minRecallCount: preview.deepConfig.minRecallCount,
|
||||
minUniqueQueries: preview.deepConfig.minUniqueQueries,
|
||||
recencyHalfLifeDays: preview.deepConfig.recencyHalfLifeDays,
|
||||
maxAgeDays:
|
||||
typeof preview.deepConfig.maxAgeDays === "number"
|
||||
? preview.deepConfig.maxAgeDays
|
||||
: null,
|
||||
},
|
||||
rem: {
|
||||
skipped: preview.remSkipped,
|
||||
sourceEntryCount: preview.rem.sourceEntryCount,
|
||||
reflections: [...preview.rem.reflections],
|
||||
candidateTruths: preview.rem.candidateTruths.map((truth) => ({
|
||||
snippet: truth.snippet,
|
||||
confidence: truth.confidence,
|
||||
})),
|
||||
bodyLines: [...preview.rem.bodyLines],
|
||||
},
|
||||
grounded: groundedPayload,
|
||||
deep: {
|
||||
candidateLimit,
|
||||
truncated: preview.deep.truncated,
|
||||
candidates: preview.deep.candidates.map((candidate) => {
|
||||
const promoted =
|
||||
typeof candidate.promotedAt === "string" && candidate.promotedAt.length > 0;
|
||||
const payloadLocal: DoctorMemoryRemHarnessCandidatePayload = {
|
||||
key: candidate.key,
|
||||
path: candidate.path,
|
||||
startLine: candidate.startLine,
|
||||
endLine: candidate.endLine,
|
||||
snippet: candidate.snippet,
|
||||
recallCount: candidate.recallCount,
|
||||
uniqueQueries: candidate.uniqueQueries,
|
||||
avgScore: candidate.avgScore,
|
||||
maxScore: candidate.maxScore,
|
||||
ageDays: candidate.ageDays,
|
||||
firstRecalledAt: candidate.firstRecalledAt,
|
||||
lastRecalledAt: candidate.lastRecalledAt,
|
||||
promoted,
|
||||
};
|
||||
if (promoted) {
|
||||
payloadLocal.promotedAt = candidate.promotedAt;
|
||||
}
|
||||
return payloadLocal;
|
||||
}),
|
||||
},
|
||||
};
|
||||
respond(true, payload, undefined);
|
||||
} catch (err) {
|
||||
const payload: DoctorMemoryRemHarnessErrorPayload = {
|
||||
ok: false,
|
||||
agentId,
|
||||
workspaceDir,
|
||||
error: `gateway rem-harness probe failed: ${formatError(err)}`,
|
||||
};
|
||||
respond(true, payload, undefined);
|
||||
}
|
||||
},
|
||||
};
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
@@ -11,18 +11,6 @@ const requestGatewayRestartWithSignalAdmission = vi.hoisted(() => vi.fn());
|
||||
const readActiveGatewayLockIdentity = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../infra/restart-coordinator.js", () => ({
|
||||
createSafeGatewayRestartPreflight: vi.fn(() => ({
|
||||
safe: true,
|
||||
counts: {
|
||||
queueSize: 0,
|
||||
pendingReplies: 0,
|
||||
embeddedRuns: 0,
|
||||
activeTasks: 0,
|
||||
totalActive: 0,
|
||||
},
|
||||
blockers: [],
|
||||
summary: "safe to restart now",
|
||||
})),
|
||||
requestSafeGatewayRestart: (opts: unknown) => requestSafeGatewayRestart(opts),
|
||||
}));
|
||||
|
||||
|
||||
@@ -3,10 +3,7 @@ import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coerci
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { readActiveGatewayLockIdentity } from "../../infra/gateway-lock.js";
|
||||
import {
|
||||
createSafeGatewayRestartPreflight,
|
||||
requestSafeGatewayRestart,
|
||||
} from "../../infra/restart-coordinator.js";
|
||||
import { requestSafeGatewayRestart } from "../../infra/restart-coordinator.js";
|
||||
import type { GatewayRestartIntent } from "../../infra/restart-intent.js";
|
||||
import { requestGatewayRestartWithSignalAdmission } from "../../infra/restart.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
@@ -162,7 +159,4 @@ export const restartHandlers: GatewayRequestHandlers = {
|
||||
});
|
||||
respond(true, result);
|
||||
},
|
||||
"gateway.restart.preflight": async ({ respond }) => {
|
||||
respond(true, createSafeGatewayRestartPreflight());
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
// Read-only compaction checkpoint queries.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
validateSessionsCompactionGetParams,
|
||||
validateSessionsCompactionListParams,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import {
|
||||
getSessionCompactionCheckpoint,
|
||||
listSessionCompactionCheckpoints,
|
||||
} from "../session-compaction-checkpoints.js";
|
||||
import { validateSessionsCompactionListParams } from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { listSessionCompactionCheckpoints } from "../session-compaction-checkpoints.js";
|
||||
import { resolveRequestedSessionAgentId as resolveRequestedGlobalAgentId } from "../session-request-agent.js";
|
||||
import { loadAccessorSessionEntryForGatewayTarget, requireSessionKey } from "./sessions-shared.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
@@ -53,55 +44,4 @@ export const sessionCheckpointQueryHandlers: GatewayRequestHandlers = {
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
"sessions.compaction.get": ({ params, respond, context }) => {
|
||||
if (
|
||||
!assertValidParams(
|
||||
params,
|
||||
validateSessionsCompactionGetParams,
|
||||
"sessions.compaction.get",
|
||||
respond,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const p = params;
|
||||
const key = requireSessionKey(p.key, respond);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const checkpointId = normalizeOptionalString(p.checkpointId) ?? "";
|
||||
if (!checkpointId) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "checkpointId required"));
|
||||
return;
|
||||
}
|
||||
const cfg = context.getRuntimeConfig();
|
||||
const requestedAgent = resolveRequestedGlobalAgentId(cfg, key, p.agentId);
|
||||
if (!requestedAgent.ok) {
|
||||
respond(false, undefined, requestedAgent.error);
|
||||
return;
|
||||
}
|
||||
const { entry, canonicalKey } = loadAccessorSessionEntryForGatewayTarget({
|
||||
key,
|
||||
cfg,
|
||||
agentId: requestedAgent.agentId,
|
||||
});
|
||||
const checkpoint = getSessionCompactionCheckpoint({ entry, checkpointId });
|
||||
if (!checkpoint) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, `checkpoint not found: ${checkpointId}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
key: canonicalKey,
|
||||
checkpoint,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,13 +24,6 @@ export const sessionSubscriptionHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
respond(true, { subscribed: Boolean(connId) }, undefined);
|
||||
},
|
||||
"sessions.unsubscribe": ({ client, context, respond }) => {
|
||||
const connId = client?.connId?.trim();
|
||||
if (connId) {
|
||||
context.unsubscribeSessionEvents(connId);
|
||||
}
|
||||
respond(true, { subscribed: false }, undefined);
|
||||
},
|
||||
"sessions.viewers.set": ({ params, client, context, respond }) => {
|
||||
if (
|
||||
!assertValidParams(
|
||||
|
||||
@@ -7,7 +7,6 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
|
||||
import { createDeferred } from "../../../test/helpers/promise.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { createSafeGatewayRestartPreflight } from "../../infra/restart-coordinator.js";
|
||||
import type { SystemAgentApprovalRequestPayload } from "../../infra/system-agent-approvals.js";
|
||||
import { resetPluginStateStoreForTests } from "../../plugin-state/plugin-state-store.js";
|
||||
import { getCommandLaneSnapshot } from "../../process/command-queue.js";
|
||||
@@ -914,12 +913,6 @@ describe("openclaw.chat", () => {
|
||||
await approvalStarted.promise;
|
||||
try {
|
||||
expect(systemAgentLane()).toMatchObject({ activeCount: 1, queuedCount: 0 });
|
||||
const restartPreflight = createSafeGatewayRestartPreflight();
|
||||
expect(restartPreflight.safe).toBe(false);
|
||||
expect(restartPreflight.counts.queueSize).toBe(1);
|
||||
expect(restartPreflight.blockers).toContainEqual(
|
||||
expect.objectContaining({ kind: "queue", count: 1 }),
|
||||
);
|
||||
} finally {
|
||||
releaseApproval.resolve();
|
||||
}
|
||||
@@ -936,7 +929,6 @@ describe("openclaw.chat", () => {
|
||||
summary: "Scheduled Gateway restart",
|
||||
});
|
||||
expect(getActiveGatewayRootWorkCount()).toBe(0);
|
||||
expect(createSafeGatewayRestartPreflight().counts.queueSize).toBe(0);
|
||||
});
|
||||
|
||||
it("reuses a live session, then requires fresh fallback verification after failure", async () => {
|
||||
|
||||
@@ -7,13 +7,10 @@ import {
|
||||
errorShape,
|
||||
validateTalkSessionAppendAudioParams,
|
||||
validateTalkSessionCancelOutputParams,
|
||||
validateTalkSessionCancelTurnParams,
|
||||
validateTalkSessionCloseParams,
|
||||
validateTalkSessionCreateParams,
|
||||
validateTalkSessionJoinParams,
|
||||
validateTalkSessionSteerParams,
|
||||
validateTalkSessionSubmitToolResultParams,
|
||||
validateTalkSessionTurnParams,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { buildAgentMainSessionKey } from "../../routing/session-key.js";
|
||||
import { REALTIME_VOICE_AGENT_CONSULT_TOOL } from "../../talk/agent-consult-tool.js";
|
||||
@@ -24,16 +21,7 @@ import { ensureClientVoiceAgentSessionEntry } from "../../talk/client-voice-sess
|
||||
import { resolveConfiguredRealtimeVoiceProvider } from "../../talk/provider-resolver.js";
|
||||
import { ADMIN_SCOPE } from "../operator-scopes.js";
|
||||
import { resolveSessionKeyFromResolveParams } from "../sessions-resolve.js";
|
||||
import {
|
||||
cancelTalkHandoffTurn,
|
||||
createTalkHandoff,
|
||||
endTalkHandoffTurn,
|
||||
getTalkHandoff,
|
||||
joinTalkHandoff,
|
||||
revokeTalkHandoff,
|
||||
startTalkHandoffTurn,
|
||||
type TalkHandoffTurnResult,
|
||||
} from "../talk-handoff.js";
|
||||
import { createTalkHandoff, getTalkHandoff, revokeTalkHandoff } from "../talk-handoff.js";
|
||||
import {
|
||||
cancelTalkRealtimeRelayTurn,
|
||||
createTalkRealtimeRelaySession,
|
||||
@@ -47,10 +35,8 @@ import {
|
||||
getUnifiedTalkSession,
|
||||
rememberUnifiedTalkSession,
|
||||
requireUnifiedTalkSessionConn,
|
||||
type UnifiedTalkSessionRecord,
|
||||
} from "../talk-session-registry.js";
|
||||
import {
|
||||
cancelTalkTranscriptionRelayTurn,
|
||||
createTalkTranscriptionRelaySession,
|
||||
sendTalkTranscriptionRelayAudio,
|
||||
stopTalkTranscriptionRelaySession,
|
||||
@@ -70,14 +56,10 @@ import {
|
||||
resolveConfiguredRealtimeTranscriptionProvider,
|
||||
resolveTalkRealtimeProviderInstructions,
|
||||
resolveTalkRealtimeGatewayRelayLaunch,
|
||||
talkHandoffErrorCode,
|
||||
} from "./talk-shared.js";
|
||||
import type { GatewayRequestContext, GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import { assertValidParams } from "./validation.js";
|
||||
|
||||
/** Gateway-managed Talk sessions resolve public ids through connection-owned unified records. */
|
||||
type ManagedRoomTalkSession = Extract<UnifiedTalkSessionRecord, { kind: "managed-room" }>;
|
||||
|
||||
function isActiveManagedRoomClient(
|
||||
session: { handoffId: string },
|
||||
connId: string | undefined,
|
||||
@@ -135,44 +117,6 @@ function respondOk(respond: RespondFn, payload: unknown = { ok: true }) {
|
||||
respond(true, payload, undefined);
|
||||
}
|
||||
|
||||
function respondManagedRoomTurn(params: {
|
||||
session: UnifiedTalkSessionRecord;
|
||||
connId?: string;
|
||||
context: GatewayRequestContext;
|
||||
respond: RespondFn;
|
||||
method: "talk.session.startTurn" | "talk.session.endTurn" | "talk.session.cancelTurn";
|
||||
ownershipAction: "startTurn" | "endTurn" | "cancelTurn";
|
||||
failureVerb: "start" | "end" | "cancel";
|
||||
run: (session: ManagedRoomTalkSession) => TalkHandoffTurnResult;
|
||||
}) {
|
||||
if (params.session.kind !== "managed-room") {
|
||||
respondInvalidRequest(params.respond, `${params.method} requires managed-room`);
|
||||
return;
|
||||
}
|
||||
if (!isActiveManagedRoomClient(params.session, params.connId)) {
|
||||
params.respond(false, undefined, managedRoomOwnershipError(params.ownershipAction));
|
||||
return;
|
||||
}
|
||||
const result = params.run(params.session);
|
||||
if (!result.ok) {
|
||||
params.respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
talkHandoffErrorCode(result.reason),
|
||||
`talk turn ${params.failureVerb} failed: ${result.reason}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
broadcastTalkRoomEvents(params.context, result.record.room.activeClientId, {
|
||||
handoffId: result.record.id,
|
||||
roomId: result.record.roomId,
|
||||
events: result.events,
|
||||
});
|
||||
respondOk(params.respond, { ok: true, turnId: result.turnId, events: result.events });
|
||||
}
|
||||
|
||||
/** RPC handlers for gateway-managed Talk sessions and room lifecycle. */
|
||||
export const talkSessionHandlers: GatewayRequestHandlers = {
|
||||
"talk.session.create": async ({ params, respond, context, client }) => {
|
||||
@@ -389,43 +333,6 @@ export const talkSessionHandlers: GatewayRequestHandlers = {
|
||||
respondUnavailable(respond, err);
|
||||
}
|
||||
},
|
||||
"talk.session.join": async ({ params, respond, client, context }) => {
|
||||
if (!assertValidParams(params, validateTalkSessionJoinParams, "talk.session.join", respond)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const session = getUnifiedTalkSession(params.sessionId);
|
||||
if (session.kind !== "managed-room") {
|
||||
respondInvalidRequest(respond, "talk.session.join requires a managed-room session");
|
||||
return;
|
||||
}
|
||||
const result = joinTalkHandoff(session.handoffId, params.token, { clientId: client?.connId });
|
||||
if (!result.ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
result.reason === "invalid_token" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE,
|
||||
`talk session join failed: ${result.reason}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
broadcastTalkRoomEvents(context, result.replacedClientId, {
|
||||
handoffId: result.record.id,
|
||||
roomId: result.record.roomId,
|
||||
events: result.replacementEvents,
|
||||
});
|
||||
broadcastTalkRoomEvents(context, client?.connId, {
|
||||
handoffId: result.record.id,
|
||||
roomId: result.record.roomId,
|
||||
events: result.activeClientEvents,
|
||||
});
|
||||
respondOk(respond, result.record);
|
||||
} catch (err) {
|
||||
respondUnavailable(respond, err);
|
||||
}
|
||||
},
|
||||
"talk.session.appendAudio": async ({ params, respond, client }) => {
|
||||
if (
|
||||
!assertValidParams(
|
||||
@@ -468,108 +375,6 @@ export const talkSessionHandlers: GatewayRequestHandlers = {
|
||||
respondUnavailable(respond, err);
|
||||
}
|
||||
},
|
||||
"talk.session.startTurn": async ({ params, respond, client, context }) => {
|
||||
if (
|
||||
!assertValidParams(params, validateTalkSessionTurnParams, "talk.session.startTurn", respond)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const session = getUnifiedTalkSession(params.sessionId);
|
||||
respondManagedRoomTurn({
|
||||
session,
|
||||
connId: client?.connId,
|
||||
context,
|
||||
respond,
|
||||
method: "talk.session.startTurn",
|
||||
ownershipAction: "startTurn",
|
||||
failureVerb: "start",
|
||||
run: (managedSession) =>
|
||||
startTalkHandoffTurn(managedSession.handoffId, managedSession.token, {
|
||||
turnId: params.turnId,
|
||||
clientId: client?.connId,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
respondUnavailable(respond, err);
|
||||
}
|
||||
},
|
||||
"talk.session.endTurn": async ({ params, respond, client, context }) => {
|
||||
if (
|
||||
!assertValidParams(params, validateTalkSessionTurnParams, "talk.session.endTurn", respond)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const session = getUnifiedTalkSession(params.sessionId);
|
||||
respondManagedRoomTurn({
|
||||
session,
|
||||
connId: client?.connId,
|
||||
context,
|
||||
respond,
|
||||
method: "talk.session.endTurn",
|
||||
ownershipAction: "endTurn",
|
||||
failureVerb: "end",
|
||||
run: (managedSession) =>
|
||||
endTalkHandoffTurn(managedSession.handoffId, managedSession.token, {
|
||||
turnId: params.turnId,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
respondUnavailable(respond, err);
|
||||
}
|
||||
},
|
||||
"talk.session.cancelTurn": async ({ params, respond, client, context }) => {
|
||||
if (
|
||||
!assertValidParams(
|
||||
params,
|
||||
validateTalkSessionCancelTurnParams,
|
||||
"talk.session.cancelTurn",
|
||||
respond,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const session = getUnifiedTalkSession(params.sessionId);
|
||||
if (session.kind === "realtime-relay") {
|
||||
const connId = requireUnifiedTalkSessionConn(session, client?.connId);
|
||||
cancelTalkRealtimeRelayTurn({
|
||||
relaySessionId: session.relaySessionId,
|
||||
connId,
|
||||
reason: normalizeOptionalString(params.reason),
|
||||
});
|
||||
respondOk(respond);
|
||||
return;
|
||||
}
|
||||
if (session.kind === "transcription-relay") {
|
||||
const connId = requireUnifiedTalkSessionConn(session, client?.connId);
|
||||
cancelTalkTranscriptionRelayTurn({
|
||||
transcriptionSessionId: session.transcriptionSessionId,
|
||||
connId,
|
||||
reason: normalizeOptionalString(params.reason),
|
||||
});
|
||||
respondOk(respond);
|
||||
return;
|
||||
}
|
||||
respondManagedRoomTurn({
|
||||
session,
|
||||
connId: client?.connId,
|
||||
context,
|
||||
respond,
|
||||
method: "talk.session.cancelTurn",
|
||||
ownershipAction: "cancelTurn",
|
||||
failureVerb: "cancel",
|
||||
run: (managedSession) =>
|
||||
cancelTalkHandoffTurn(managedSession.handoffId, managedSession.token, {
|
||||
turnId: params.turnId,
|
||||
reason: params.reason,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
respondUnavailable(respond, err);
|
||||
}
|
||||
},
|
||||
"talk.session.cancelOutput": async ({ params, respond, client }) => {
|
||||
if (
|
||||
!assertValidParams(
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { resolveRealtimeBootstrapContextInstructions } from "../../agents/realtime-bootstrap-context.js";
|
||||
import type { TalkRealtimeConfig } from "../../config/types.gateway.js";
|
||||
import type { OpenClawConfig } from "../../config/types.js";
|
||||
@@ -33,7 +32,6 @@ import {
|
||||
type VoiceModelProvider,
|
||||
} from "../../tts/voice-models.js";
|
||||
import { ADMIN_SCOPE } from "../operator-scopes.js";
|
||||
import type { TalkHandoffTurnResult } from "../talk-handoff.js";
|
||||
|
||||
/** Resolve the Talk session mode, defaulting managed-room transports to stt-tts. */
|
||||
export function normalizeTalkSessionMode(params: { mode?: string; transport?: string }): TalkMode {
|
||||
@@ -130,14 +128,6 @@ export function broadcastTalkRoomEvents(
|
||||
}
|
||||
}
|
||||
|
||||
type TalkHandoffFailureReason = Extract<TalkHandoffTurnResult, { ok: false }>["reason"];
|
||||
|
||||
export function talkHandoffErrorCode(reason: TalkHandoffFailureReason) {
|
||||
return reason === "invalid_token" || reason === "no_active_turn" || reason === "stale_turn"
|
||||
? ErrorCodes.INVALID_REQUEST
|
||||
: ErrorCodes.UNAVAILABLE;
|
||||
}
|
||||
|
||||
function getRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return asOptionalRecord(value) ?? undefined;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ const mocks = vi.hoisted(() => ({
|
||||
submitTalkRealtimeRelayToolResult: vi.fn(),
|
||||
createTalkTranscriptionRelaySession: vi.fn(),
|
||||
sendTalkTranscriptionRelayAudio: vi.fn(),
|
||||
cancelTalkTranscriptionRelayTurn: vi.fn(),
|
||||
stopTalkTranscriptionRelaySession: vi.fn(),
|
||||
chatSend: vi.fn(),
|
||||
controlRealtimeVoiceAgentRun: vi.fn(),
|
||||
@@ -222,7 +221,6 @@ vi.mock("../talk-transcription-relay.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../talk-transcription-relay.js")>();
|
||||
return {
|
||||
...actual,
|
||||
cancelTalkTranscriptionRelayTurn: mocks.cancelTalkTranscriptionRelayTurn,
|
||||
createTalkTranscriptionRelaySession: mocks.createTalkTranscriptionRelaySession,
|
||||
sendTalkTranscriptionRelayAudio: mocks.sendTalkTranscriptionRelayAudio,
|
||||
stopTalkTranscriptionRelaySession: mocks.stopTalkTranscriptionRelaySession,
|
||||
@@ -2183,154 +2181,6 @@ describe("talk.session unified handlers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("creates and controls managed-room sessions through the unified API", async () => {
|
||||
const broadcastToConnIds = vi.fn();
|
||||
const createRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.create", {
|
||||
params: {
|
||||
mode: "stt-tts",
|
||||
transport: "managed-room",
|
||||
sessionKey: "session:main",
|
||||
ttlMs: 5000,
|
||||
},
|
||||
client: { connId: "conn-1", connect: { scopes: ["operator.admin"] } },
|
||||
respond: createRespond,
|
||||
context: {
|
||||
getRuntimeConfig: () => ({}) as OpenClawConfig,
|
||||
},
|
||||
});
|
||||
const session = mockCallArg(createRespond, 0, 1) as { sessionId: string; token: string };
|
||||
|
||||
const createResult = expectRespondOk(createRespond, {
|
||||
transport: "managed-room",
|
||||
brain: "agent-consult",
|
||||
}) as Record<string, unknown>;
|
||||
expect(createResult.sessionId).toBeTypeOf("string");
|
||||
expect(createResult.handoffId).toBeTypeOf("string");
|
||||
expect(createResult.roomId).toMatch(/^talk_/);
|
||||
expect(createResult.token).toBeTypeOf("string");
|
||||
expect(mocks.resolveSessionKeyFromResolveParams).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
client: { connId: "conn-1", connect: { scopes: ["operator.admin"] } },
|
||||
p: {
|
||||
key: "session:main",
|
||||
includeGlobal: true,
|
||||
includeUnknown: true,
|
||||
},
|
||||
});
|
||||
|
||||
const joinRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.join", {
|
||||
params: { sessionId: session.sessionId, token: session.token },
|
||||
id: "2",
|
||||
respond: joinRespond,
|
||||
context: {
|
||||
broadcastToConnIds,
|
||||
},
|
||||
});
|
||||
const joinResult = expectRespondOk(joinRespond, { id: session.sessionId }) as {
|
||||
room?: Record<string, unknown>;
|
||||
};
|
||||
expectRecordFields(joinResult.room, { activeClientId: "conn-1" });
|
||||
expect(mockCallArg(broadcastToConnIds)).toBe("talk.event");
|
||||
const readyEventPayload = expectRecordFields(mockCallArg(broadcastToConnIds, 0, 1), {
|
||||
handoffId: session.sessionId,
|
||||
});
|
||||
expectRecordFields(readyEventPayload.talkEvent, { type: "session.ready" });
|
||||
expect(mockCallArg(broadcastToConnIds, 0, 2)).toEqual(new Set(["conn-1"]));
|
||||
expect(mockCallArg(broadcastToConnIds, 0, 3)).toEqual({ dropIfSlow: true });
|
||||
|
||||
const startRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.startTurn", {
|
||||
params: { sessionId: session.sessionId, turnId: "turn-1" },
|
||||
id: "3",
|
||||
respond: startRespond,
|
||||
context: {
|
||||
getRuntimeConfig: () => ({}) as OpenClawConfig,
|
||||
broadcastToConnIds,
|
||||
},
|
||||
});
|
||||
|
||||
const startResult = expectRespondOk(startRespond, { ok: true, turnId: "turn-1" }) as {
|
||||
events?: unknown[];
|
||||
};
|
||||
expect(startResult.events).toHaveLength(1);
|
||||
expectRecordFields(startResult.events?.[0], { type: "turn.started", turnId: "turn-1" });
|
||||
expect(mockCallArg(broadcastToConnIds, 1)).toBe("talk.event");
|
||||
const startEventPayload = expectRecordFields(mockCallArg(broadcastToConnIds, 1, 1), {
|
||||
handoffId: session.sessionId,
|
||||
});
|
||||
expectRecordFields(startEventPayload.talkEvent, {
|
||||
type: "turn.started",
|
||||
turnId: "turn-1",
|
||||
});
|
||||
expect(mockCallArg(broadcastToConnIds, 1, 2)).toEqual(new Set(["conn-1"]));
|
||||
expect(mockCallArg(broadcastToConnIds, 1, 3)).toEqual({ dropIfSlow: true });
|
||||
|
||||
const mismatchedSteerRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.steer", {
|
||||
params: {
|
||||
sessionId: session.sessionId,
|
||||
sessionKey: "session:other",
|
||||
text: "use the safer plan",
|
||||
mode: "steer",
|
||||
},
|
||||
id: "4",
|
||||
respond: mismatchedSteerRespond,
|
||||
context: {
|
||||
broadcastToConnIds,
|
||||
},
|
||||
});
|
||||
expectRespondError(mismatchedSteerRespond, {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "talk.session.steer sessionKey does not match the managed-room session",
|
||||
});
|
||||
expect(mocks.controlRealtimeVoiceAgentRun).not.toHaveBeenCalled();
|
||||
|
||||
const steerRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.steer", {
|
||||
params: {
|
||||
sessionId: session.sessionId,
|
||||
text: "use the safer plan",
|
||||
mode: "steer",
|
||||
},
|
||||
id: "5",
|
||||
respond: steerRespond,
|
||||
context: {
|
||||
broadcastToConnIds,
|
||||
},
|
||||
});
|
||||
expect(mocks.controlRealtimeVoiceAgentRun).toHaveBeenCalledWith({
|
||||
sessionKey: "session:main",
|
||||
text: "use the safer plan",
|
||||
mode: "steer",
|
||||
recentEvents: expect.any(Array),
|
||||
});
|
||||
expectRespondOk(steerRespond, {
|
||||
ok: true,
|
||||
mode: "steer",
|
||||
sessionKey: "session:main",
|
||||
});
|
||||
|
||||
const closeRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.close", {
|
||||
params: { sessionId: session.sessionId },
|
||||
id: "6",
|
||||
respond: closeRespond,
|
||||
context: {
|
||||
broadcastToConnIds,
|
||||
},
|
||||
});
|
||||
expect(closeRespond).toHaveBeenCalledWith(true, { ok: true }, undefined);
|
||||
expect(mockCallArg(broadcastToConnIds, 2)).toBe("talk.event");
|
||||
const closedEventPayload = expectRecordFields(mockCallArg(broadcastToConnIds, 2, 1), {
|
||||
handoffId: session.sessionId,
|
||||
});
|
||||
expectRecordFields(closedEventPayload.talkEvent, { type: "session.closed", final: true });
|
||||
expect(mockCallArg(broadcastToConnIds, 2, 2)).toEqual(new Set(["conn-1"]));
|
||||
expect(mockCallArg(broadcastToConnIds, 2, 3)).toEqual({ dropIfSlow: true });
|
||||
});
|
||||
|
||||
it("passes managed-room spawnedBy visibility scope to session resolution", async () => {
|
||||
const createRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.create", {
|
||||
@@ -2386,110 +2236,6 @@ describe("talk.session unified handlers", () => {
|
||||
expect(mocks.resolveSessionKeyFromResolveParams).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires managed-room ownership before turn control", async () => {
|
||||
const broadcastToConnIds = vi.fn();
|
||||
const createRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.create", {
|
||||
params: {
|
||||
mode: "stt-tts",
|
||||
transport: "managed-room",
|
||||
sessionKey: "session:main",
|
||||
},
|
||||
client: { connId: "creator", connect: { scopes: ["operator.admin"] } },
|
||||
respond: createRespond,
|
||||
context: {
|
||||
getRuntimeConfig: () => ({}) as OpenClawConfig,
|
||||
},
|
||||
});
|
||||
const session = mockCallArg(createRespond, 0, 1) as { sessionId: string; token: string };
|
||||
|
||||
const unjoinedStartRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.startTurn", {
|
||||
params: { sessionId: session.sessionId, turnId: "turn-1" },
|
||||
id: "2",
|
||||
client: { connId: "creator" },
|
||||
respond: unjoinedStartRespond,
|
||||
context: { broadcastToConnIds },
|
||||
});
|
||||
expectRespondError(unjoinedStartRespond, {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "talk.session.startTurn requires the active managed-room connection",
|
||||
});
|
||||
|
||||
await callTalkHandler("talk.session.join", {
|
||||
params: { sessionId: session.sessionId, token: session.token },
|
||||
id: "3",
|
||||
respond: vi.fn(),
|
||||
context: { broadcastToConnIds },
|
||||
});
|
||||
|
||||
const staleStartRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.startTurn", {
|
||||
params: { sessionId: session.sessionId, turnId: "turn-1" },
|
||||
id: "4",
|
||||
client: { connId: "conn-2" },
|
||||
respond: staleStartRespond,
|
||||
context: { broadcastToConnIds },
|
||||
});
|
||||
expectRespondError(staleStartRespond, {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "talk.session.startTurn requires the active managed-room connection",
|
||||
});
|
||||
|
||||
await callTalkHandler("talk.session.startTurn", {
|
||||
params: { sessionId: session.sessionId, turnId: "turn-1" },
|
||||
id: "5",
|
||||
respond: vi.fn(),
|
||||
context: { broadcastToConnIds },
|
||||
});
|
||||
|
||||
const staleEndRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.endTurn", {
|
||||
params: { sessionId: session.sessionId, turnId: "turn-1" },
|
||||
id: "6",
|
||||
client: { connId: "conn-2" },
|
||||
respond: staleEndRespond,
|
||||
context: { broadcastToConnIds },
|
||||
});
|
||||
expectRespondError(staleEndRespond, {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "talk.session.endTurn requires the active managed-room connection",
|
||||
});
|
||||
|
||||
const staleCancelRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.cancelTurn", {
|
||||
params: { sessionId: session.sessionId, turnId: "turn-1" },
|
||||
id: "7",
|
||||
client: { connId: "conn-2" },
|
||||
respond: staleCancelRespond,
|
||||
context: { broadcastToConnIds },
|
||||
});
|
||||
expectRespondError(staleCancelRespond, {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "talk.session.cancelTurn requires the active managed-room connection",
|
||||
});
|
||||
|
||||
const staleCloseRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.close", {
|
||||
params: { sessionId: session.sessionId },
|
||||
id: "8",
|
||||
client: { connId: "conn-2" },
|
||||
respond: staleCloseRespond,
|
||||
context: { broadcastToConnIds },
|
||||
});
|
||||
expectRespondError(staleCloseRespond, {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "talk.session.close requires the active managed-room connection",
|
||||
});
|
||||
|
||||
await callTalkHandler("talk.session.close", {
|
||||
params: { sessionId: session.sessionId },
|
||||
id: "9",
|
||||
respond: vi.fn(),
|
||||
context: { broadcastToConnIds },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps direct-tools managed-room sessions behind admin scope", async () => {
|
||||
const rejectedRespond = vi.fn();
|
||||
await callTalkHandler("talk.session.create", {
|
||||
|
||||
@@ -861,22 +861,4 @@ describe("terminal gateway policy", () => {
|
||||
});
|
||||
expect(result).toEqual({ path: "/tmp/node/report.pdf", size: 4 });
|
||||
});
|
||||
|
||||
it("sanitizes terminal snapshots before returning plain text", async () => {
|
||||
const { opts, sessions, respond } = makeOpts({ sessionId: "s1" }, { enabled: true });
|
||||
const finals = Array.from({ length: 0x7e - 0x40 + 1 }, (_, offset) =>
|
||||
String.fromCharCode(0x40 + offset),
|
||||
);
|
||||
const sequences = ["\u001B[", "\u009B"]
|
||||
.flatMap((introducer) => finals.map((finalByte) => introducer + finalByte))
|
||||
.join("");
|
||||
sessions.snapshot.mockReturnValue(`before${sequences}after`);
|
||||
|
||||
await expectDefined(
|
||||
terminalHandlers["terminal.text"],
|
||||
'terminalHandlers["terminal.text"] test invariant',
|
||||
)(opts);
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(true, { text: "beforeafter" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
validateTerminalInputParams,
|
||||
validateTerminalOpenParams,
|
||||
validateTerminalResizeParams,
|
||||
validateTerminalTextParams,
|
||||
validateTerminalUploadResult,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { NODE_TERMINAL_UPLOAD_COMMAND } from "../../infra/node-commands.js";
|
||||
@@ -25,7 +24,6 @@ import { mergeProcessEnv } from "../../infra/process-env.js";
|
||||
import type { TerminalUploadFile } from "../../infra/terminal-file-upload.js";
|
||||
import type { SessionCatalogTerminalPlan } from "../../plugins/session-catalog.js";
|
||||
import { applyPluginNodeInvokePolicy } from "../node-invoke-plugin-policy.js";
|
||||
import { renderTerminalBufferText } from "../terminal/buffer-text.js";
|
||||
import { buildTerminalEnv, type TerminalLaunchResolution } from "../terminal/launch.js";
|
||||
import { createNodeRelayBackend } from "../terminal/node-relay.js";
|
||||
import {
|
||||
@@ -618,30 +616,4 @@ export const terminalHandlers: GatewayRequestHandlers = {
|
||||
: [];
|
||||
respond(true, { sessions });
|
||||
},
|
||||
|
||||
"terminal.text": async (opts) => {
|
||||
const { params, respond, context } = opts;
|
||||
if (!assertValidParams(params, validateTerminalTextParams, "terminal.text", respond)) {
|
||||
return;
|
||||
}
|
||||
const connId = requireConnId(opts);
|
||||
if (!connId) {
|
||||
return;
|
||||
}
|
||||
const p = params as { sessionId: string };
|
||||
if (!context.terminalSessions || !terminalEnabled(context)) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "terminal is not available"));
|
||||
return;
|
||||
}
|
||||
const raw = context.terminalSessions.snapshot(p.sessionId);
|
||||
if (raw === undefined) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, `unknown terminal session "${p.sessionId}"`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
respond(true, { text: renderTerminalBufferText(raw) });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,42 +1,10 @@
|
||||
// Gateway RPC handlers for voice wake routing configuration.
|
||||
import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js";
|
||||
import {
|
||||
loadVoiceWakeRoutingConfig,
|
||||
normalizeVoiceWakeRoutingConfig,
|
||||
setVoiceWakeRoutingConfig,
|
||||
validateVoiceWakeRoutingConfigInput,
|
||||
} from "../../infra/voicewake-routing.js";
|
||||
import { loadVoiceWakeRoutingConfig } from "../../infra/voicewake-routing.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
/** Gateway request handlers for reading and updating voice wake routing. */
|
||||
/** Gateway request handlers for reading voice wake routing. */
|
||||
export const voicewakeRoutingHandlers: GatewayRequestHandlers = {
|
||||
"voicewake.routing.get": async ({ respond }) => {
|
||||
respond(true, { config: await loadVoiceWakeRoutingConfig() });
|
||||
},
|
||||
"voicewake.routing.set": async ({ params, respond, context }) => {
|
||||
if (
|
||||
!params ||
|
||||
params.config === null ||
|
||||
typeof params.config !== "object" ||
|
||||
Array.isArray(params.config)
|
||||
) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "voicewake.routing.set requires config: object"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const validated = validateVoiceWakeRoutingConfigInput(params.config);
|
||||
if (!validated.ok) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, validated.message));
|
||||
return;
|
||||
}
|
||||
// Validate first for caller-friendly errors, then normalize before
|
||||
// persistence so broadcasts carry the canonical routing shape.
|
||||
const normalized = normalizeVoiceWakeRoutingConfig(params.config);
|
||||
const config = await setVoiceWakeRoutingConfig(normalized);
|
||||
context.broadcastVoiceWakeRoutingChanged(config);
|
||||
respond(true, { config });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createDeferred } from "../../../test/helpers/promise.js";
|
||||
import { createSafeGatewayRestartPreflight } from "../../infra/restart-coordinator.js";
|
||||
import {
|
||||
getActiveGatewayRootWorkCount,
|
||||
resetGatewayWorkAdmission,
|
||||
@@ -167,15 +166,10 @@ describe("wizard setup ownership", () => {
|
||||
});
|
||||
|
||||
expect(getActiveGatewayRootWorkCount()).toBe(1);
|
||||
expect(createSafeGatewayRestartPreflight()).toMatchObject({
|
||||
safe: false,
|
||||
blockers: [expect.objectContaining({ kind: "root-request", count: 1 })],
|
||||
});
|
||||
runnerSettled.resolve();
|
||||
await vi.waitFor(() => {
|
||||
expect(getActiveGatewayRootWorkCount()).toBe(0);
|
||||
});
|
||||
expect(createSafeGatewayRestartPreflight().safe).toBe(true);
|
||||
} finally {
|
||||
runnerSettled.resolve();
|
||||
resetGatewayWorkAdmission();
|
||||
|
||||
@@ -438,134 +438,25 @@ describe("gateway server models + voicewake", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("voicewake.routing.get/set persists and broadcasts", { timeout: 60_000 }, async () => {
|
||||
await withTempHome(async (homeDir) => {
|
||||
const initial = await rpcReq<{
|
||||
config?: { version?: number; defaultTarget?: unknown; routes?: unknown[] };
|
||||
}>(ws, "voicewake.routing.get");
|
||||
expect(initial.ok).toBe(true);
|
||||
expect(initial.payload?.config?.version).toBe(1);
|
||||
expect(initial.payload?.config?.defaultTarget).toEqual({ mode: "current" });
|
||||
expect(initial.payload?.config?.routes).toStrictEqual([]);
|
||||
test("voicewake.routing.get returns the default routing", async () => {
|
||||
const result = await rpcReq<{
|
||||
config?: { version?: number; defaultTarget?: unknown; routes?: unknown[] };
|
||||
}>(ws, "voicewake.routing.get");
|
||||
|
||||
const changedP = onceMessage<{
|
||||
type: "event";
|
||||
event: string;
|
||||
payload?: Record<string, unknown> | null;
|
||||
}>(ws, (o) => o.type === "event" && o.event === "voicewake.routing.changed");
|
||||
|
||||
const setRes = await rpcReq<{
|
||||
config?: { routes?: Array<{ trigger?: string; target?: unknown }>; updatedAtMs?: number };
|
||||
}>(ws, "voicewake.routing.set", {
|
||||
config: {
|
||||
defaultTarget: { mode: "current" },
|
||||
routes: [{ trigger: " Robot Wake ", target: { agentId: "main" } }],
|
||||
},
|
||||
});
|
||||
expect(setRes.ok).toBe(true);
|
||||
expect(setRes.payload?.config?.routes).toEqual([
|
||||
{ trigger: "robot wake", target: { agentId: "main" } },
|
||||
]);
|
||||
expect(typeof setRes.payload?.config?.updatedAtMs).toBe("number");
|
||||
|
||||
const changed = await changedP;
|
||||
expect(changed.event).toBe("voicewake.routing.changed");
|
||||
expect(
|
||||
(changed.payload as { config?: { routes?: unknown } } | undefined)?.config?.routes,
|
||||
).toEqual([{ trigger: "robot wake", target: { agentId: "main" } }]);
|
||||
|
||||
const after = await rpcReq<{
|
||||
config?: { routes?: Array<{ trigger?: string; target?: unknown }> };
|
||||
}>(ws, "voicewake.routing.get");
|
||||
expect(after.ok).toBe(true);
|
||||
expect(after.payload?.config?.routes).toEqual([
|
||||
{ trigger: "robot wake", target: { agentId: "main" } },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(homeDir, ".openclaw", "settings", "voicewake-routing.json"), "utf8"),
|
||||
).rejects.toThrow(/ENOENT/u);
|
||||
|
||||
const invalid = await rpcReq(ws, "voicewake.routing.set", { config: null });
|
||||
expect(invalid.ok).toBe(false);
|
||||
expect(invalid.error?.message ?? "").toMatch(
|
||||
/voicewake\.routing\.set requires config: object/i,
|
||||
);
|
||||
|
||||
const badRoutes = await rpcReq(ws, "voicewake.routing.set", {
|
||||
config: { routes: "oops" },
|
||||
});
|
||||
expect(badRoutes.ok).toBe(false);
|
||||
expect(badRoutes.error?.message ?? "").toMatch(/config\.routes must be an array/i);
|
||||
|
||||
const badTarget = await rpcReq(ws, "voicewake.routing.set", {
|
||||
config: {
|
||||
routes: [
|
||||
{ trigger: "robot wake", target: { agentId: "main", sessionKey: "agent:main:main" } },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(badTarget.ok).toBe(false);
|
||||
expect(badTarget.error?.message ?? "").toMatch(
|
||||
/config\.routes\[0\]\.target cannot include both agentId and sessionKey/i,
|
||||
);
|
||||
|
||||
const badAgentId = await rpcReq(ws, "voicewake.routing.set", {
|
||||
config: {
|
||||
routes: [{ trigger: "robot wake", target: { agentId: "!!!" } }],
|
||||
},
|
||||
});
|
||||
expect(badAgentId.ok).toBe(false);
|
||||
expect(badAgentId.error?.message ?? "").toMatch(
|
||||
/config\.routes\[0\]\.target\.agentId must be a valid agent id/i,
|
||||
);
|
||||
|
||||
const badSessionKey = await rpcReq(ws, "voicewake.routing.set", {
|
||||
config: {
|
||||
routes: [{ trigger: "robot wake", target: { sessionKey: "agent::main" } }],
|
||||
},
|
||||
});
|
||||
expect(badSessionKey.ok).toBe(false);
|
||||
expect(badSessionKey.error?.message ?? "").toMatch(
|
||||
/config\.routes\[0\]\.target\.sessionKey must be a canonical agent session key/i,
|
||||
);
|
||||
|
||||
const stillStored = await rpcReq<{
|
||||
config?: { routes?: Array<{ trigger?: string; target?: unknown }> };
|
||||
}>(ws, "voicewake.routing.get");
|
||||
expect(stillStored.ok).toBe(true);
|
||||
expect(stillStored.payload?.config?.routes).toEqual([
|
||||
{ trigger: "robot wake", target: { agentId: "main" } },
|
||||
]);
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.payload?.config).toMatchObject({
|
||||
version: 1,
|
||||
defaultTarget: { mode: "current" },
|
||||
routes: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("pushes voicewake.routing.changed to nodes on connect and on updates", async () => {
|
||||
await withConnectedNodeEvent("voicewake.routing.changed", async (nodeWs, first) => {
|
||||
test("pushes voicewake.routing.changed to nodes on connect", async () => {
|
||||
await withConnectedNodeEvent("voicewake.routing.changed", async (_nodeWs, first) => {
|
||||
expect(first.event).toBe("voicewake.routing.changed");
|
||||
expect(
|
||||
(first.payload as { config?: { routes?: unknown[] } } | undefined)?.config?.routes,
|
||||
).toStrictEqual([]);
|
||||
|
||||
const broadcastP = onceMessage<{
|
||||
type: "event";
|
||||
event: string;
|
||||
payload?: Record<string, unknown> | null;
|
||||
}>(nodeWs, (o) => o.type === "event" && o.event === "voicewake.routing.changed");
|
||||
|
||||
const setRes = await rpcReq(ws, "voicewake.routing.set", {
|
||||
config: {
|
||||
defaultTarget: { mode: "current" },
|
||||
routes: [{ trigger: "hello", target: { sessionKey: "agent:main:main" } }],
|
||||
},
|
||||
});
|
||||
expect(setRes.ok).toBe(true);
|
||||
|
||||
const broadcast = await broadcastP;
|
||||
expect(broadcast.event).toBe("voicewake.routing.changed");
|
||||
expect(
|
||||
(broadcast.payload as { config?: { routes?: unknown } } | undefined)?.config?.routes,
|
||||
).toEqual([{ trigger: "hello", target: { sessionKey: "agent:main:main" } }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -324,18 +324,6 @@ test("sessions.compaction.* lists checkpoints and branches or restores from comp
|
||||
sessionKey: checkpointEntry.sessionKey,
|
||||
});
|
||||
|
||||
const checkpoint = await rpcReq<{
|
||||
ok: true;
|
||||
key: string;
|
||||
checkpoint: { checkpointId: string; preCompaction: { sessionFile?: string } };
|
||||
}>(ws, "sessions.compaction.get", {
|
||||
key: "main",
|
||||
checkpointId: "checkpoint-1",
|
||||
});
|
||||
expect(checkpoint.ok).toBe(true);
|
||||
expect(checkpoint.payload?.checkpoint.checkpointId).toBe("checkpoint-1");
|
||||
expect(checkpoint.payload?.checkpoint.preCompaction.sessionFile).toBeUndefined();
|
||||
|
||||
const sessionManagerOpenSpy = vi.spyOn(SessionManager, "open");
|
||||
let branched: Awaited<
|
||||
ReturnType<
|
||||
@@ -553,18 +541,6 @@ test("sessions.compaction list/get scopes selected global checkpoints to the req
|
||||
summary: "work checkpoint",
|
||||
});
|
||||
|
||||
const got = await directSessionReq<{
|
||||
checkpoint?: { checkpointId?: string; summary?: string };
|
||||
}>(
|
||||
"sessions.compaction.get",
|
||||
{ key: "global", agentId: "work", checkpointId: "checkpoint-work" },
|
||||
{ context: { getRuntimeConfig: () => runtimeConfig } },
|
||||
);
|
||||
expect(got.ok).toBe(true);
|
||||
expect(got.payload?.checkpoint).toMatchObject({
|
||||
checkpointId: "checkpoint-work",
|
||||
summary: "work checkpoint",
|
||||
});
|
||||
expect(
|
||||
loadSessionEntry({ agentId: "main", sessionKey: "global", storePath: mainStorePath })
|
||||
?.sessionId,
|
||||
|
||||
@@ -3,15 +3,7 @@
|
||||
*/
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
cancelTalkHandoffTurn,
|
||||
createTalkHandoff,
|
||||
endTalkHandoffTurn,
|
||||
getTalkHandoff,
|
||||
joinTalkHandoff,
|
||||
revokeTalkHandoff,
|
||||
startTalkHandoffTurn,
|
||||
} from "./talk-handoff.js";
|
||||
import { createTalkHandoff, getTalkHandoff, revokeTalkHandoff } from "./talk-handoff.js";
|
||||
|
||||
const requireRecord = createRequireRecord("record", "expected-label-capitalized");
|
||||
|
||||
@@ -34,10 +26,6 @@ function expectFields(
|
||||
return record;
|
||||
}
|
||||
|
||||
function requireRoom(value: unknown, label = "handoff room"): Record<string, unknown> {
|
||||
return requireRecord(requireRecord(value, label).room, `${label} room`);
|
||||
}
|
||||
|
||||
function requireEvents(value: unknown, label = "handoff result"): unknown[] {
|
||||
return requireArray(requireRecord(value, label).events, `${label} events`);
|
||||
}
|
||||
@@ -103,22 +91,22 @@ describe("talk handoff store", () => {
|
||||
});
|
||||
|
||||
it("expires handoffs immediately when the creation clock is invalid", () => {
|
||||
const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
|
||||
try {
|
||||
const handoff = createTalkHandoff({
|
||||
sessionKey: "session:main",
|
||||
ttlMs: 5000,
|
||||
});
|
||||
const handoff = (() => {
|
||||
const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
|
||||
try {
|
||||
return createTalkHandoff({
|
||||
sessionKey: "session:main",
|
||||
ttlMs: 5000,
|
||||
});
|
||||
} finally {
|
||||
dateNow.mockRestore();
|
||||
}
|
||||
})();
|
||||
|
||||
expect(handoff.createdAt).toBe(0);
|
||||
expect(handoff.expiresAt).toBe(0);
|
||||
expect(joinTalkHandoff(handoff.id, handoff.token)).toEqual({
|
||||
ok: false,
|
||||
reason: "expired",
|
||||
});
|
||||
} finally {
|
||||
dateNow.mockRestore();
|
||||
}
|
||||
expect(handoff.createdAt).toBe(0);
|
||||
expect(handoff.expiresAt).toBe(0);
|
||||
expect(getTalkHandoff(handoff.id)).toBeUndefined();
|
||||
expect(revokeTalkHandoff(handoff.id)).toEqual({ revoked: false, events: [] });
|
||||
});
|
||||
|
||||
it("expires handoffs immediately when expiry would exceed Date bounds", () => {
|
||||
@@ -131,83 +119,23 @@ describe("talk handoff store", () => {
|
||||
});
|
||||
|
||||
expect(handoff.expiresAt).toBe(0);
|
||||
expect(joinTalkHandoff(handoff.id, handoff.token)).toEqual({
|
||||
ok: false,
|
||||
reason: "expired",
|
||||
});
|
||||
expect(getTalkHandoff(handoff.id)).toBeUndefined();
|
||||
expect(revokeTalkHandoff(handoff.id)).toEqual({ revoked: false, events: [] });
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("joins and revokes handoffs with only the bearer token", () => {
|
||||
it("revokes a handoff and records its final close event", () => {
|
||||
const handoff = createTalkHandoff({ sessionKey: "session:main" });
|
||||
|
||||
expect(joinTalkHandoff(handoff.id, "wrong")).toEqual({
|
||||
ok: false,
|
||||
reason: "invalid_token",
|
||||
});
|
||||
const join = joinTalkHandoff(handoff.id, handoff.token);
|
||||
const joinRecord = expectFields(join, "join result", {
|
||||
ok: true,
|
||||
});
|
||||
expectEventFields(requireEvents(joinRecord), 0, { type: "session.ready" });
|
||||
expectFields(joinRecord.record, "joined record", {
|
||||
id: handoff.id,
|
||||
expectFields(getTalkHandoff(handoff.id), "stored handoff", {
|
||||
roomId: handoff.roomId,
|
||||
sessionKey: "session:main",
|
||||
});
|
||||
|
||||
expectFields(revokeTalkHandoff(handoff.id), "revoke result", { revoked: true });
|
||||
expect(joinTalkHandoff(handoff.id, handoff.token)).toEqual({
|
||||
ok: false,
|
||||
reason: "not_found",
|
||||
});
|
||||
});
|
||||
|
||||
it("records managed-room ready, replacement, and close lifecycle events", () => {
|
||||
const handoff = createTalkHandoff({ sessionKey: "session:main" });
|
||||
|
||||
const firstJoin = joinTalkHandoff(handoff.id, handoff.token, { clientId: "conn-1" });
|
||||
expectFields(firstJoin, "first join", {
|
||||
ok: true,
|
||||
});
|
||||
const firstReady = expectEventFields(requireEvents(firstJoin, "first join"), 0, {
|
||||
type: "session.ready",
|
||||
sessionId: handoff.roomId,
|
||||
});
|
||||
expect(requireRecord(firstReady.payload, "first ready payload").clientId).toBe("conn-1");
|
||||
expect(
|
||||
requireRoom(requireRecord(firstJoin, "first join").record, "first join record")
|
||||
.activeClientId,
|
||||
).toBe("conn-1");
|
||||
|
||||
const secondJoin = joinTalkHandoff(handoff.id, handoff.token, { clientId: "conn-2" });
|
||||
expectFields(secondJoin, "second join", {
|
||||
ok: true,
|
||||
});
|
||||
const secondEvents = requireEvents(secondJoin, "second join");
|
||||
const replaced = expectEventFields(secondEvents, 0, {
|
||||
type: "session.replaced",
|
||||
sessionId: handoff.roomId,
|
||||
});
|
||||
expectFields(requireRecord(replaced.payload, "replaced payload"), "replaced payload", {
|
||||
previousClientId: "conn-1",
|
||||
nextClientId: "conn-2",
|
||||
});
|
||||
const ready = expectEventFields(secondEvents, 1, {
|
||||
type: "session.ready",
|
||||
sessionId: handoff.roomId,
|
||||
});
|
||||
expect(requireRecord(ready.payload, "ready payload").clientId).toBe("conn-2");
|
||||
expect(
|
||||
requireRoom(requireRecord(secondJoin, "second join").record, "second join record")
|
||||
.activeClientId,
|
||||
).toBe("conn-2");
|
||||
|
||||
const revoked = revokeTalkHandoff(handoff.id);
|
||||
expectFields(revoked, "revoke result", {
|
||||
revoked: true,
|
||||
activeClientId: "conn-2",
|
||||
roomId: handoff.roomId,
|
||||
});
|
||||
const closed = expectEventFields(requireEvents(revoked, "revoke result"), 0, {
|
||||
type: "session.closed",
|
||||
@@ -215,92 +143,8 @@ describe("talk handoff store", () => {
|
||||
final: true,
|
||||
});
|
||||
expect(requireRecord(closed.payload, "closed payload").reason).toBe("revoked");
|
||||
});
|
||||
|
||||
it("records managed-room turn start, end, and cancellation events", () => {
|
||||
const handoff = createTalkHandoff({ sessionKey: "session:main" });
|
||||
joinTalkHandoff(handoff.id, handoff.token, { clientId: "conn-1" });
|
||||
|
||||
const start = startTalkHandoffTurn(handoff.id, handoff.token, {
|
||||
clientId: "conn-1",
|
||||
turnId: "turn-1",
|
||||
});
|
||||
expectFields(start, "turn start", {
|
||||
ok: true,
|
||||
turnId: "turn-1",
|
||||
});
|
||||
expectEventFields(requireEvents(start, "turn start"), 0, {
|
||||
type: "turn.started",
|
||||
turnId: "turn-1",
|
||||
});
|
||||
expectFields(
|
||||
requireRoom(requireRecord(start, "turn start").record, "turn start record"),
|
||||
"turn room",
|
||||
{
|
||||
activeClientId: "conn-1",
|
||||
activeTurnId: "turn-1",
|
||||
},
|
||||
);
|
||||
|
||||
const ended = endTalkHandoffTurn(handoff.id, handoff.token);
|
||||
expectFields(ended, "turn end", {
|
||||
ok: true,
|
||||
turnId: "turn-1",
|
||||
});
|
||||
expectEventFields(requireEvents(ended, "turn end"), 0, {
|
||||
type: "turn.ended",
|
||||
turnId: "turn-1",
|
||||
final: true,
|
||||
});
|
||||
expect(
|
||||
requireRoom(requireRecord(ended, "turn end").record, "turn end record").activeTurnId,
|
||||
).toBeUndefined();
|
||||
|
||||
expect(cancelTalkHandoffTurn(handoff.id, handoff.token)).toEqual({
|
||||
ok: false,
|
||||
reason: "no_active_turn",
|
||||
});
|
||||
|
||||
startTalkHandoffTurn(handoff.id, handoff.token, { turnId: "turn-2" });
|
||||
const cancelled = cancelTalkHandoffTurn(handoff.id, handoff.token, { reason: "barge-in" });
|
||||
expectFields(cancelled, "turn cancellation", {
|
||||
ok: true,
|
||||
turnId: "turn-2",
|
||||
});
|
||||
const cancelledEvent = expectEventFields(requireEvents(cancelled, "turn cancellation"), 0, {
|
||||
type: "turn.cancelled",
|
||||
turnId: "turn-2",
|
||||
final: true,
|
||||
});
|
||||
expect(requireRecord(cancelledEvent.payload, "cancelled payload").reason).toBe("barge-in");
|
||||
});
|
||||
|
||||
it("rejects stale managed-room turn completion without clearing the active turn", () => {
|
||||
const handoff = createTalkHandoff({ sessionKey: "session:main" });
|
||||
|
||||
startTalkHandoffTurn(handoff.id, handoff.token, { turnId: "turn-old" });
|
||||
startTalkHandoffTurn(handoff.id, handoff.token, { turnId: "turn-current" });
|
||||
|
||||
expect(endTalkHandoffTurn(handoff.id, handoff.token, { turnId: "turn-old" })).toEqual({
|
||||
ok: false,
|
||||
reason: "stale_turn",
|
||||
});
|
||||
expect(getTalkHandoff(handoff.id)?.room.talk.activeTurnId).toBe("turn-current");
|
||||
|
||||
expect(cancelTalkHandoffTurn(handoff.id, handoff.token, { turnId: "turn-old" })).toEqual({
|
||||
ok: false,
|
||||
reason: "stale_turn",
|
||||
});
|
||||
expect(getTalkHandoff(handoff.id)?.room.talk.activeTurnId).toBe("turn-current");
|
||||
|
||||
expectFields(
|
||||
endTalkHandoffTurn(handoff.id, handoff.token, { turnId: "turn-current" }),
|
||||
"current turn end",
|
||||
{
|
||||
ok: true,
|
||||
turnId: "turn-current",
|
||||
},
|
||||
);
|
||||
expect(getTalkHandoff(handoff.id)).toBeUndefined();
|
||||
expect(revokeTalkHandoff(handoff.id)).toEqual({ revoked: false, events: [] });
|
||||
});
|
||||
|
||||
it("isolates simultaneous handoffs for different sessions on the same host", () => {
|
||||
@@ -319,32 +163,14 @@ describe("talk handoff store", () => {
|
||||
expect(first.id).not.toBe(second.id);
|
||||
expect(first.roomId).not.toBe(second.roomId);
|
||||
expect(first.token).not.toBe(second.token);
|
||||
expect(joinTalkHandoff(first.id, second.token)).toEqual({
|
||||
ok: false,
|
||||
reason: "invalid_token",
|
||||
});
|
||||
expect(joinTalkHandoff(second.id, first.token)).toEqual({
|
||||
ok: false,
|
||||
reason: "invalid_token",
|
||||
});
|
||||
const firstJoin = joinTalkHandoff(first.id, first.token);
|
||||
const firstJoinRecord = expectFields(firstJoin, "first join", {
|
||||
ok: true,
|
||||
});
|
||||
expectEventFields(requireEvents(firstJoin), 0, { type: "session.ready" });
|
||||
expectFields(firstJoinRecord.record, "first joined record", {
|
||||
expectFields(getTalkHandoff(first.id), "first stored handoff", {
|
||||
roomId: first.roomId,
|
||||
sessionKey: "agent:main:first",
|
||||
channel: "browser",
|
||||
target: "host:local",
|
||||
provider: "openai",
|
||||
});
|
||||
const secondJoin = joinTalkHandoff(second.id, second.token);
|
||||
const secondJoinRecord = expectFields(secondJoin, "second join", {
|
||||
ok: true,
|
||||
});
|
||||
expectEventFields(requireEvents(secondJoin), 0, { type: "session.ready" });
|
||||
expectFields(secondJoinRecord.record, "second joined record", {
|
||||
expectFields(getTalkHandoff(second.id), "second stored handoff", {
|
||||
roomId: second.roomId,
|
||||
sessionKey: "agent:main:second",
|
||||
channel: "browser",
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
resolveDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "@openclaw/normalization-core/number-coercion";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { sha256Base64Url } from "../infra/crypto-digest.js";
|
||||
import { resolveGlobalMap } from "../shared/global-singleton.js";
|
||||
import { recordTalkObservabilityEvent } from "../talk/observability.js";
|
||||
@@ -73,17 +72,6 @@ type TalkHandoffCreateResult = TalkHandoffPublicRecord & {
|
||||
token: string;
|
||||
};
|
||||
|
||||
type TalkHandoffJoinResult =
|
||||
| {
|
||||
ok: true;
|
||||
record: TalkHandoffPublicRecord;
|
||||
events: TalkEvent[];
|
||||
replacedClientId?: string;
|
||||
replacementEvents: TalkEvent[];
|
||||
activeClientEvents: TalkEvent[];
|
||||
}
|
||||
| { ok: false; reason: "not_found" | "expired" | "invalid_token" };
|
||||
|
||||
type TalkHandoffRevokeResult = {
|
||||
revoked: boolean;
|
||||
roomId?: string;
|
||||
@@ -91,18 +79,6 @@ type TalkHandoffRevokeResult = {
|
||||
events: TalkEvent[];
|
||||
};
|
||||
|
||||
export type TalkHandoffTurnResult =
|
||||
| {
|
||||
ok: true;
|
||||
record: TalkHandoffPublicRecord;
|
||||
turnId: string;
|
||||
events: TalkEvent[];
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
reason: "not_found" | "expired" | "invalid_token" | "no_active_turn" | "stale_turn";
|
||||
};
|
||||
|
||||
type TalkHandoffRoomState = {
|
||||
activeClientId?: string;
|
||||
talk: TalkSessionController;
|
||||
@@ -163,116 +139,6 @@ export function getTalkHandoff(id: string): TalkHandoffRecord | undefined {
|
||||
return handoffs.get(id);
|
||||
}
|
||||
|
||||
/** Joins a managed room, replacing any previous active client for that room. */
|
||||
export function joinTalkHandoff(
|
||||
id: string,
|
||||
token: string,
|
||||
opts: { clientId?: string } = {},
|
||||
): TalkHandoffJoinResult {
|
||||
const access = resolveTalkHandoffAccess(id, token);
|
||||
if (!access.ok) {
|
||||
return access;
|
||||
}
|
||||
const record = access.record;
|
||||
const previousClientId = record.room.activeClientId;
|
||||
const events = joinTalkHandoffRoom(record, opts.clientId);
|
||||
const replacedClientId =
|
||||
previousClientId && previousClientId !== opts.clientId ? previousClientId : undefined;
|
||||
const replacementEvents = replacedClientId
|
||||
? events.filter((event) => event.type === "session.replaced")
|
||||
: [];
|
||||
const activeClientEvents = replacedClientId
|
||||
? events.filter((event) => event.type !== "session.replaced")
|
||||
: events;
|
||||
return {
|
||||
ok: true,
|
||||
record: toPublicTalkHandoffRecord(record),
|
||||
events,
|
||||
replacedClientId,
|
||||
replacementEvents,
|
||||
activeClientEvents,
|
||||
};
|
||||
}
|
||||
|
||||
/** Starts a client turn in a joined managed room. */
|
||||
export function startTalkHandoffTurn(
|
||||
id: string,
|
||||
token: string,
|
||||
opts: { turnId?: string; clientId?: string } = {},
|
||||
): TalkHandoffTurnResult {
|
||||
const access = resolveTalkHandoffAccess(id, token);
|
||||
if (!access.ok) {
|
||||
return access;
|
||||
}
|
||||
const record = access.record;
|
||||
if (opts.clientId) {
|
||||
record.room.activeClientId = opts.clientId;
|
||||
}
|
||||
const turnId = normalizeOptionalString(opts.turnId) ?? randomUUID();
|
||||
const turn = record.room.talk.startTurn({
|
||||
turnId,
|
||||
payload: { handoffId: id, roomId: record.roomId, clientId: record.room.activeClientId },
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
record: toPublicTalkHandoffRecord(record),
|
||||
turnId,
|
||||
events: turn.event ? [turn.event] : [],
|
||||
};
|
||||
}
|
||||
|
||||
/** Ends the active managed-room turn and returns the emitted Talk event. */
|
||||
export function endTalkHandoffTurn(
|
||||
id: string,
|
||||
token: string,
|
||||
opts: { turnId?: string } = {},
|
||||
): TalkHandoffTurnResult {
|
||||
const access = resolveTalkHandoffAccess(id, token);
|
||||
if (!access.ok) {
|
||||
return access;
|
||||
}
|
||||
const record = access.record;
|
||||
const result = record.room.talk.endTurn({
|
||||
turnId: normalizeOptionalString(opts.turnId),
|
||||
payload: { handoffId: id, roomId: record.roomId },
|
||||
});
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
record: toPublicTalkHandoffRecord(record),
|
||||
turnId: result.turnId,
|
||||
events: [result.event],
|
||||
};
|
||||
}
|
||||
|
||||
/** Cancels the active managed-room turn with a client-visible reason. */
|
||||
export function cancelTalkHandoffTurn(
|
||||
id: string,
|
||||
token: string,
|
||||
opts: { reason?: string; turnId?: string } = {},
|
||||
): TalkHandoffTurnResult {
|
||||
const access = resolveTalkHandoffAccess(id, token);
|
||||
if (!access.ok) {
|
||||
return access;
|
||||
}
|
||||
const record = access.record;
|
||||
const result = record.room.talk.cancelTurn({
|
||||
turnId: normalizeOptionalString(opts.turnId),
|
||||
payload: { handoffId: id, roomId: record.roomId, reason: opts.reason ?? "client-cancelled" },
|
||||
});
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
record: toPublicTalkHandoffRecord(record),
|
||||
turnId: result.turnId,
|
||||
events: [result.event],
|
||||
};
|
||||
}
|
||||
|
||||
/** Revokes a handoff and emits the final room-close event if it existed. */
|
||||
export function revokeTalkHandoff(id: string): TalkHandoffRevokeResult {
|
||||
pruneExpiredTalkHandoffs();
|
||||
@@ -294,11 +160,6 @@ export function revokeTalkHandoff(id: string): TalkHandoffRevokeResult {
|
||||
};
|
||||
}
|
||||
|
||||
/** Verifies the caller token without exposing the stored token hash. */
|
||||
function verifyTalkHandoffToken(record: TalkHandoffRecord, token: string): boolean {
|
||||
return record.tokenHash === hashTalkHandoffToken(token);
|
||||
}
|
||||
|
||||
function normalizeTtlMs(value: number | undefined): number {
|
||||
if (!Number.isFinite(value) || value === undefined) {
|
||||
return DEFAULT_TALK_HANDOFF_TTL_MS;
|
||||
@@ -360,58 +221,6 @@ function createTalkHandoffRoom(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTalkHandoffAccess(
|
||||
id: string,
|
||||
token: string,
|
||||
):
|
||||
| { ok: true; record: TalkHandoffRecord }
|
||||
| { ok: false; reason: "not_found" | "expired" | "invalid_token" } {
|
||||
const record = handoffs.get(id);
|
||||
if (!record) {
|
||||
return { ok: false, reason: "not_found" };
|
||||
}
|
||||
if (!isFutureDateTimestampMs(record.expiresAt)) {
|
||||
// Expiry emits the same close event as explicit revocation so room clients
|
||||
// can reconcile state without knowing which cleanup path won the race.
|
||||
appendTalkHandoffRoomEvent(record, {
|
||||
type: "session.closed",
|
||||
payload: { reason: "expired", handoffId: id, roomId: record.roomId },
|
||||
final: true,
|
||||
});
|
||||
handoffs.delete(id);
|
||||
return { ok: false, reason: "expired" };
|
||||
}
|
||||
if (!verifyTalkHandoffToken(record, token)) {
|
||||
return { ok: false, reason: "invalid_token" };
|
||||
}
|
||||
return { ok: true, record };
|
||||
}
|
||||
|
||||
function appendTalkHandoffRoomEvent(record: TalkHandoffRecord, input: TalkEventInput): TalkEvent {
|
||||
return record.room.talk.emit(input);
|
||||
}
|
||||
|
||||
function joinTalkHandoffRoom(record: TalkHandoffRecord, clientId: string | undefined): TalkEvent[] {
|
||||
const events: TalkEvent[] = [];
|
||||
if (record.room.activeClientId && record.room.activeClientId !== clientId) {
|
||||
events.push(
|
||||
appendTalkHandoffRoomEvent(record, {
|
||||
type: "session.replaced",
|
||||
payload: {
|
||||
handoffId: record.id,
|
||||
roomId: record.roomId,
|
||||
previousClientId: record.room.activeClientId,
|
||||
nextClientId: clientId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
record.room.activeClientId = clientId;
|
||||
events.push(
|
||||
appendTalkHandoffRoomEvent(record, {
|
||||
type: "session.ready",
|
||||
payload: { handoffId: record.id, roomId: record.roomId, clientId },
|
||||
}),
|
||||
);
|
||||
return events;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { formatError } from "./server-utils.js";
|
||||
|
||||
type TalkConnectionCleanupKind = "browser-control" | "realtime-relay" | "transcription-relay";
|
||||
|
||||
export type UnifiedTalkSessionRecord =
|
||||
type UnifiedTalkSessionRecord =
|
||||
| {
|
||||
kind: "realtime-relay";
|
||||
connId: string;
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
rememberUnifiedTalkSession,
|
||||
} from "./talk-session-registry.js";
|
||||
import {
|
||||
cancelTalkTranscriptionRelayTurn,
|
||||
createTalkTranscriptionRelaySession,
|
||||
sendTalkTranscriptionRelayAudio,
|
||||
stopTalkTranscriptionRelaySession,
|
||||
@@ -775,48 +774,4 @@ describe("talk transcription gateway relay", () => {
|
||||
).toThrow("Transcription relay session expiry is outside the supported Date range");
|
||||
expect(provider.createSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels an active transcription turn and closes the provider session", async () => {
|
||||
let sttRequest: RealtimeTranscriptionSessionCreateRequest | undefined;
|
||||
const sttSession = createSttSessionMock(async () => {
|
||||
sttRequest?.onSpeechStart?.();
|
||||
});
|
||||
const { events, session } = await createStartedRelaySession(sttSession, {}, (req) => {
|
||||
sttRequest = req;
|
||||
});
|
||||
sttSession.close.mockImplementationOnce(() => {
|
||||
sttRequest?.onTranscript?.("cancelled provider transcript");
|
||||
});
|
||||
|
||||
cancelTalkTranscriptionRelayTurn({
|
||||
transcriptionSessionId: session.transcriptionSessionId,
|
||||
connId: "conn-1",
|
||||
reason: "barge-in",
|
||||
});
|
||||
|
||||
expect(sttSession.close).toHaveBeenCalledOnce();
|
||||
const cancelledPayload = findPayloadByTalkEventType(events, "turn.cancelled");
|
||||
expectRecordFields(cancelledPayload, "cancelled payload", {
|
||||
transcriptionSessionId: session.transcriptionSessionId,
|
||||
});
|
||||
expectTalkEventFields(cancelledPayload, {
|
||||
type: "turn.cancelled",
|
||||
turnId: "turn-1",
|
||||
payload: { reason: "barge-in" },
|
||||
final: true,
|
||||
});
|
||||
|
||||
const closePayload = findPayloadByType(events, "close");
|
||||
expectRecordFields(closePayload, "close payload", {
|
||||
transcriptionSessionId: session.transcriptionSessionId,
|
||||
type: "close",
|
||||
reason: "completed",
|
||||
});
|
||||
expect(
|
||||
events.some(
|
||||
(event) =>
|
||||
isRecord(event.payload) && event.payload.text === "cancelled provider transcript",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -507,25 +507,3 @@ export function stopTalkTranscriptionRelaySession(params: {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancels the active transcription turn and closes the relay. */
|
||||
export function cancelTalkTranscriptionRelayTurn(params: {
|
||||
transcriptionSessionId: string;
|
||||
connId: string;
|
||||
reason?: string;
|
||||
}): void {
|
||||
const session = getTranscriptionSession(params.transcriptionSessionId, params.connId);
|
||||
const turnId = ensureTranscriptionTurn(session);
|
||||
const cancelled = session.talk.cancelTurn({
|
||||
turnId,
|
||||
payload: { reason: params.reason ?? "client-cancelled" },
|
||||
});
|
||||
broadcastToOwner(session.context, session.connId, {
|
||||
transcriptionSessionId: session.id,
|
||||
type: "transcript",
|
||||
text: "",
|
||||
final: true,
|
||||
talkEvent: cancelled.ok ? cancelled.event : undefined,
|
||||
});
|
||||
closeTranscriptionSession(session, "completed");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Renders a session's raw output ring as plain text for terminal.text — an
|
||||
// agent/LLM affordance that wants readable output, not escape sequences.
|
||||
// Renders a session's raw output ring as plain text for the agent terminal tool,
|
||||
// which needs readable output rather than escape sequences.
|
||||
import { stripAnsiSequences } from "../../../packages/terminal-core/src/ansi.js";
|
||||
|
||||
// Built at runtime so the source stays free of literal control characters and
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Bounds concurrent shells so a client cannot exhaust host processes. */
|
||||
export const DEFAULT_MAX_SESSIONS = 24;
|
||||
/**
|
||||
* Rolling output retained per session for reattach replay and terminal.text,
|
||||
* Rolling output retained per session for reattach replay and agent tool reads,
|
||||
* in UTF-16 code units. The session cap keeps worst-case memory bounded.
|
||||
*/
|
||||
export const DEFAULT_SCROLLBACK_CHARS = 256 * 1024;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Tests infra store file persistence and recovery.
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||
@@ -16,11 +15,7 @@ import {
|
||||
resetDiagnosticEventsForTest,
|
||||
} from "./diagnostic-events.js";
|
||||
import { readSessionStoreJson5 } from "./state-migrations.fs.js";
|
||||
import {
|
||||
loadVoiceWakeRoutingConfig,
|
||||
resolveVoiceWakeRouteByTrigger,
|
||||
setVoiceWakeRoutingConfig,
|
||||
} from "./voicewake-routing.js";
|
||||
import { loadVoiceWakeRoutingConfig, resolveVoiceWakeRouteByTrigger } from "./voicewake-routing.js";
|
||||
import {
|
||||
defaultVoiceWakeTriggers,
|
||||
loadVoiceWakeConfig,
|
||||
@@ -129,25 +124,6 @@ describe("infra store", () => {
|
||||
});
|
||||
|
||||
describe("voicewake routing store", () => {
|
||||
it("normalizes and persists routing config", async () => {
|
||||
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-routing-"));
|
||||
const saved = await setVoiceWakeRoutingConfig(
|
||||
{
|
||||
defaultTarget: { mode: "current" },
|
||||
routes: [
|
||||
{ trigger: " Hello Bot ", target: { agentId: "main" } },
|
||||
{ trigger: "", target: { sessionKey: "agent:main:main" } },
|
||||
],
|
||||
},
|
||||
baseDir,
|
||||
);
|
||||
expect(saved.routes).toEqual([{ trigger: "hello bot", target: { agentId: "main" } }]);
|
||||
expect(saved.updatedAtMs).toBeGreaterThan(0);
|
||||
|
||||
const loaded = await loadVoiceWakeRoutingConfig(baseDir);
|
||||
expect(loaded.routes).toEqual([{ trigger: "hello bot", target: { agentId: "main" } }]);
|
||||
});
|
||||
|
||||
it("resolves routes by normalized trigger", () => {
|
||||
expect(
|
||||
resolveVoiceWakeRouteByTrigger({
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
resetGatewayWorkAdmission,
|
||||
tryBeginGatewayRootWorkAdmission,
|
||||
} from "../process/gateway-work-admission.js";
|
||||
import {
|
||||
createSafeGatewayRestartPreflight,
|
||||
requestSafeGatewayRestart,
|
||||
} from "./restart-coordinator.js";
|
||||
import { requestSafeGatewayRestart } from "./restart-coordinator.js";
|
||||
|
||||
const scheduleGatewaySigusr1Restart = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -17,6 +14,15 @@ vi.mock("./restart.js", () => ({
|
||||
|
||||
beforeEach(() => {
|
||||
resetGatewayWorkAdmission();
|
||||
scheduleGatewaySigusr1Restart.mockReset().mockReturnValue({
|
||||
ok: true,
|
||||
pid: 123,
|
||||
signal: "SIGUSR1",
|
||||
delayMs: 0,
|
||||
mode: "emit",
|
||||
coalesced: false,
|
||||
cooldownMsApplied: 0,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -24,8 +30,12 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("safe gateway restart coordinator", () => {
|
||||
const requestPreflight = (
|
||||
inspect: NonNullable<Parameters<typeof requestSafeGatewayRestart>[0]>["inspect"],
|
||||
) => requestSafeGatewayRestart({ inspect }).preflight;
|
||||
|
||||
it("reports safe when no restart blockers are active", () => {
|
||||
const preflight = createSafeGatewayRestartPreflight({
|
||||
const preflight = requestPreflight({
|
||||
getQueueSize: () => 0,
|
||||
getPendingReplies: () => 0,
|
||||
getEmbeddedRuns: () => 0,
|
||||
@@ -54,7 +64,7 @@ describe("safe gateway restart coordinator", () => {
|
||||
});
|
||||
|
||||
it("returns structured blockers for active work", () => {
|
||||
const preflight = createSafeGatewayRestartPreflight({
|
||||
const preflight = requestPreflight({
|
||||
getQueueSize: () => 2,
|
||||
getPendingReplies: () => 1,
|
||||
getEmbeddedRuns: () => 1,
|
||||
@@ -89,7 +99,7 @@ describe("safe gateway restart coordinator", () => {
|
||||
});
|
||||
|
||||
it("defers restart for aggregate background exec sessions", () => {
|
||||
const preflight = createSafeGatewayRestartPreflight({
|
||||
const preflight = requestPreflight({
|
||||
getQueueSize: () => 0,
|
||||
getPendingReplies: () => 0,
|
||||
getEmbeddedRuns: () => 0,
|
||||
@@ -123,7 +133,7 @@ describe("safe gateway restart coordinator", () => {
|
||||
|
||||
try {
|
||||
await request?.run(async () => {
|
||||
const preflight = createSafeGatewayRestartPreflight({
|
||||
const preflight = requestPreflight({
|
||||
getQueueSize: () => 0,
|
||||
getPendingReplies: () => 0,
|
||||
getEmbeddedRuns: () => 0,
|
||||
@@ -149,7 +159,7 @@ describe("safe gateway restart coordinator", () => {
|
||||
});
|
||||
|
||||
it("keeps truncated task titles on complete UTF-16 code points", () => {
|
||||
const preflight = createSafeGatewayRestartPreflight({
|
||||
const preflight = requestPreflight({
|
||||
getQueueSize: () => 0,
|
||||
getPendingReplies: () => 0,
|
||||
getEmbeddedRuns: () => 0,
|
||||
|
||||
@@ -55,7 +55,7 @@ export type SafeGatewayRestartRequestResult = {
|
||||
restart: ScheduledRestart;
|
||||
};
|
||||
|
||||
export function createSafeGatewayRestartPreflight(
|
||||
function createSafeGatewayRestartPreflight(
|
||||
inspectors: Partial<SafeRestartInspectors> = {},
|
||||
): SafeGatewayRestartPreflight {
|
||||
const snapshot = createGatewayActiveWorkSnapshot({
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
closeOpenClawStateDatabaseForTest,
|
||||
openOpenClawStateDatabase,
|
||||
OPENCLAW_STATE_SCHEMA_VERSION,
|
||||
runOpenClawStateWriteTransaction,
|
||||
} from "../state/openclaw-state-db.js";
|
||||
import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js";
|
||||
import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js";
|
||||
@@ -51,7 +52,7 @@ import {
|
||||
migrateLegacyCurrentConversationBindings,
|
||||
migrateLegacyPluginBindingApprovals,
|
||||
} from "./state-migrations.runtime-state.js";
|
||||
import { loadVoiceWakeRoutingConfig, setVoiceWakeRoutingConfig } from "./voicewake-routing.js";
|
||||
import { loadVoiceWakeRoutingConfig } from "./voicewake-routing.js";
|
||||
import { loadVoiceWakeConfig, setVoiceWakeTriggers } from "./voicewake.js";
|
||||
|
||||
type DetectLegacyStateParams = Parameters<typeof detectLegacyStateMigrationsWithSurfaces>[0];
|
||||
@@ -449,6 +450,44 @@ function createEnv(stateDir: string): NodeJS.ProcessEnv {
|
||||
};
|
||||
}
|
||||
|
||||
type VoiceWakeRoutingTestDatabase = Pick<
|
||||
OpenClawStateKyselyDatabase,
|
||||
"voicewake_routing_config" | "voicewake_routing_routes"
|
||||
>;
|
||||
|
||||
function seedCanonicalVoiceWakeRouting(stateDir: string, trigger: string): void {
|
||||
const updatedAtMs = Date.now();
|
||||
runOpenClawStateWriteTransaction(
|
||||
({ db }) => {
|
||||
const routingDb = getNodeSqliteKysely<VoiceWakeRoutingTestDatabase>(db);
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
routingDb.insertInto("voicewake_routing_config").values({
|
||||
config_key: "default",
|
||||
version: 1,
|
||||
default_target_mode: "current",
|
||||
default_target_agent_id: null,
|
||||
default_target_session_key: null,
|
||||
updated_at_ms: updatedAtMs,
|
||||
}),
|
||||
);
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
routingDb.insertInto("voicewake_routing_routes").values({
|
||||
config_key: "default",
|
||||
position: 0,
|
||||
trigger,
|
||||
target_mode: "agent",
|
||||
target_agent_id: "main",
|
||||
target_session_key: null,
|
||||
updated_at_ms: updatedAtMs,
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ env: createEnv(stateDir) },
|
||||
);
|
||||
}
|
||||
|
||||
type MixedCommitFailureFixture = {
|
||||
env: NodeJS.ProcessEnv;
|
||||
expectedWarning: string;
|
||||
@@ -2327,13 +2366,7 @@ describe("state migrations", () => {
|
||||
const triggersPath = path.join(settingsDir, "voicewake.json");
|
||||
const routingPath = path.join(settingsDir, "voicewake-routing.json");
|
||||
await setVoiceWakeTriggers(["wake"], stateDir);
|
||||
await setVoiceWakeRoutingConfig(
|
||||
{
|
||||
defaultTarget: { mode: "current" },
|
||||
routes: [{ trigger: "robot wake", target: { agentId: "main" } }],
|
||||
},
|
||||
stateDir,
|
||||
);
|
||||
seedCanonicalVoiceWakeRouting(stateDir, "robot wake");
|
||||
await fs.mkdir(settingsDir, { recursive: true });
|
||||
await fs.writeFile(triggersPath, JSON.stringify({ triggers: ["wake"] }), "utf8");
|
||||
await fs.writeFile(
|
||||
@@ -2446,13 +2479,7 @@ describe("state migrations", () => {
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
const cfg = createConfig();
|
||||
const routingPath = path.join(stateDir, "settings", "voicewake-routing.json");
|
||||
await setVoiceWakeRoutingConfig(
|
||||
{
|
||||
defaultTarget: { mode: "current" },
|
||||
routes: [{ trigger: "sqlite wake", target: { agentId: "main" } }],
|
||||
},
|
||||
stateDir,
|
||||
);
|
||||
seedCanonicalVoiceWakeRouting(stateDir, "sqlite wake");
|
||||
await fs.mkdir(path.dirname(routingPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
routingPath,
|
||||
@@ -2486,13 +2513,7 @@ describe("state migrations", () => {
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
const cfg = createConfig();
|
||||
const routingPath = path.join(stateDir, "settings", "voicewake-routing.json");
|
||||
await setVoiceWakeRoutingConfig(
|
||||
{
|
||||
defaultTarget: { mode: "current" },
|
||||
routes: [{ trigger: "sqlite wake", target: { agentId: "main" } }],
|
||||
},
|
||||
stateDir,
|
||||
);
|
||||
seedCanonicalVoiceWakeRouting(stateDir, "sqlite wake");
|
||||
await fs.mkdir(path.dirname(routingPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
routingPath,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Covers voice wake routing normalization and validation.
|
||||
// Covers voice wake routing normalization and resolution.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
normalizeVoiceWakeRoutingConfig,
|
||||
resolveVoiceWakeRouteByTrigger,
|
||||
validateVoiceWakeRoutingConfigInput,
|
||||
} from "./voicewake-routing.js";
|
||||
|
||||
describe("voicewake routing normalization", () => {
|
||||
@@ -25,81 +24,4 @@ describe("voicewake routing normalization", () => {
|
||||
sessionKey: "agent:main:voice",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid route agent ids instead of normalizing them to main", () => {
|
||||
expect(
|
||||
validateVoiceWakeRoutingConfigInput({
|
||||
routes: [{ trigger: "wake", target: { agentId: "!!!" } }],
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
message: "config.routes[0].target.agentId must be a valid agent id",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed session keys instead of persisting dead routes", () => {
|
||||
expect(
|
||||
validateVoiceWakeRoutingConfigInput({
|
||||
routes: [{ trigger: "wake", target: { sessionKey: "agent::main" } }],
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
message: "config.routes[0].target.sessionKey must be a canonical agent session key",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects session keys with empty path segments", () => {
|
||||
expect(
|
||||
validateVoiceWakeRoutingConfigInput({
|
||||
routes: [{ trigger: "wake", target: { sessionKey: "agent:main:main:" } }],
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
message: "config.routes[0].target.sessionKey must be a canonical agent session key",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects duplicate triggers after normalization", () => {
|
||||
expect(
|
||||
validateVoiceWakeRoutingConfigInput({
|
||||
routes: [
|
||||
{ trigger: "Hey Bot", target: { mode: "current" } },
|
||||
{ trigger: "hey, bot", target: { agentId: "main" } },
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
message: "config.routes[1].trigger duplicates config.routes[0].trigger after normalization",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects oversized route lists", () => {
|
||||
expect(
|
||||
validateVoiceWakeRoutingConfigInput({
|
||||
routes: Array.from({ length: 33 }, (_, index) => ({
|
||||
trigger: `wake ${index}`,
|
||||
target: { mode: "current" as const },
|
||||
})),
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
message: "config.routes must contain at most 32 entries",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects oversized triggers", () => {
|
||||
expect(
|
||||
validateVoiceWakeRoutingConfigInput({
|
||||
routes: [
|
||||
{
|
||||
trigger: "x".repeat(65),
|
||||
target: { mode: "current" as const },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
message: "config.routes[0].trigger must be at most 64 characters",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
// Persists and resolves voice wake routing rules.
|
||||
import { isRecord as isPlainObject } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
classifySessionKeyShape,
|
||||
isValidAgentId,
|
||||
normalizeAgentId,
|
||||
} from "../routing/session-key.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
|
||||
import {
|
||||
openOpenClawStateDatabase,
|
||||
runOpenClawStateWriteTransaction,
|
||||
} from "../state/openclaw-state-db.js";
|
||||
import { openOpenClawStateDatabase } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
executeSqliteQuerySync,
|
||||
executeSqliteQueryTakeFirstSync,
|
||||
@@ -36,8 +28,6 @@ export type VoiceWakeRoutingConfig = {
|
||||
updatedAtMs: number;
|
||||
};
|
||||
|
||||
const MAX_VOICEWAKE_ROUTES = 32;
|
||||
const MAX_VOICEWAKE_TRIGGER_LENGTH = 64;
|
||||
const VOICEWAKE_ROUTING_CONFIG_KEY = "default";
|
||||
|
||||
const DEFAULT_ROUTING: VoiceWakeRoutingConfig = {
|
||||
@@ -108,139 +98,6 @@ function normalizeRouteRule(value: unknown): VoiceWakeRouteRule | null {
|
||||
return { trigger, target };
|
||||
}
|
||||
|
||||
function isCanonicalAgentSessionKey(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
if (classifySessionKeyShape(trimmed) !== "agent") {
|
||||
return false;
|
||||
}
|
||||
return !trimmed.split(":").some((part) => part.length === 0);
|
||||
}
|
||||
|
||||
function validateRouteTargetInput(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): { ok: true } | { ok: false; message: string } {
|
||||
if (!isPlainObject(value)) {
|
||||
return { ok: false, message: `${label} must be an object` };
|
||||
}
|
||||
const rec = value as { mode?: unknown; agentId?: unknown; sessionKey?: unknown };
|
||||
const mode = normalizeOptionalString(rec.mode);
|
||||
const agentId = normalizeOptionalString(rec.agentId);
|
||||
const sessionKey = normalizeOptionalString(rec.sessionKey);
|
||||
if (mode !== undefined) {
|
||||
if (mode !== "current") {
|
||||
return {
|
||||
ok: false,
|
||||
message: `${label}.mode must be "current" when provided`,
|
||||
};
|
||||
}
|
||||
if (agentId !== undefined || sessionKey !== undefined) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `${label} cannot mix mode with agentId or sessionKey`,
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
if (agentId !== undefined && sessionKey !== undefined) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `${label} cannot include both agentId and sessionKey`,
|
||||
};
|
||||
}
|
||||
if (agentId !== undefined) {
|
||||
if (!isValidAgentId(agentId)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `${label}.agentId must be a valid agent id`,
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
if (sessionKey !== undefined) {
|
||||
if (!isCanonicalAgentSessionKey(sessionKey)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `${label}.sessionKey must be a canonical agent session key`,
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
message: `${label} must include mode, agentId, or sessionKey`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Validate user-provided voice wake routing config before persistence. */
|
||||
export function validateVoiceWakeRoutingConfigInput(
|
||||
input: unknown,
|
||||
): { ok: true } | { ok: false; message: string } {
|
||||
if (!isPlainObject(input)) {
|
||||
return { ok: false, message: "config must be an object" };
|
||||
}
|
||||
const rec = input as {
|
||||
defaultTarget?: unknown;
|
||||
routes?: unknown;
|
||||
};
|
||||
if (rec.defaultTarget !== undefined) {
|
||||
const validatedDefaultTarget = validateRouteTargetInput(
|
||||
rec.defaultTarget,
|
||||
"config.defaultTarget",
|
||||
);
|
||||
if (!validatedDefaultTarget.ok) {
|
||||
return validatedDefaultTarget;
|
||||
}
|
||||
}
|
||||
if (rec.routes !== undefined && !Array.isArray(rec.routes)) {
|
||||
return { ok: false, message: "config.routes must be an array" };
|
||||
}
|
||||
if (Array.isArray(rec.routes)) {
|
||||
if (rec.routes.length > MAX_VOICEWAKE_ROUTES) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `config.routes must contain at most ${MAX_VOICEWAKE_ROUTES} entries`,
|
||||
};
|
||||
}
|
||||
const normalizedTriggers = new Map<string, number>();
|
||||
for (const [index, route] of rec.routes.entries()) {
|
||||
if (!isPlainObject(route)) {
|
||||
return { ok: false, message: `config.routes[${index}] must be an object` };
|
||||
}
|
||||
const trigger = normalizeOptionalString(route.trigger);
|
||||
const normalizedTrigger = trigger ? normalizeVoiceWakeTriggerWord(trigger) : "";
|
||||
if (!trigger || !normalizedTrigger) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `config.routes[${index}].trigger must be a non-empty string`,
|
||||
};
|
||||
}
|
||||
if (trigger.length > MAX_VOICEWAKE_TRIGGER_LENGTH) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `config.routes[${index}].trigger must be at most ${MAX_VOICEWAKE_TRIGGER_LENGTH} characters`,
|
||||
};
|
||||
}
|
||||
const duplicateIndex = normalizedTriggers.get(normalizedTrigger);
|
||||
if (duplicateIndex !== undefined) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `config.routes[${index}].trigger duplicates config.routes[${duplicateIndex}].trigger after normalization`,
|
||||
};
|
||||
}
|
||||
normalizedTriggers.set(normalizedTrigger, index);
|
||||
const validatedTarget = validateRouteTargetInput(
|
||||
route.target,
|
||||
`config.routes[${index}].target`,
|
||||
);
|
||||
if (!validatedTarget.ok) {
|
||||
return validatedTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** Normalize persisted or user-provided voice wake routing config. */
|
||||
export function normalizeVoiceWakeRoutingConfig(input: unknown): VoiceWakeRoutingConfig {
|
||||
if (!input || typeof input !== "object") {
|
||||
@@ -270,20 +127,6 @@ export function normalizeVoiceWakeRoutingConfig(input: unknown): VoiceWakeRoutin
|
||||
};
|
||||
}
|
||||
|
||||
function targetColumns(target: VoiceWakeRouteTarget): {
|
||||
targetAgentId: string | null;
|
||||
targetMode: string;
|
||||
targetSessionKey: string | null;
|
||||
} {
|
||||
if ("agentId" in target && target.agentId) {
|
||||
return { targetAgentId: target.agentId, targetMode: "agent", targetSessionKey: null };
|
||||
}
|
||||
if ("sessionKey" in target && target.sessionKey) {
|
||||
return { targetAgentId: null, targetMode: "session", targetSessionKey: target.sessionKey };
|
||||
}
|
||||
return { targetAgentId: null, targetMode: "current", targetSessionKey: null };
|
||||
}
|
||||
|
||||
function targetFromColumns(params: {
|
||||
agentId: string | null;
|
||||
mode: string;
|
||||
@@ -341,69 +184,6 @@ export async function loadVoiceWakeRoutingConfig(
|
||||
};
|
||||
}
|
||||
|
||||
/** Persist normalized voice wake routing config. */
|
||||
export async function setVoiceWakeRoutingConfig(
|
||||
config: unknown,
|
||||
baseDir?: string,
|
||||
): Promise<VoiceWakeRoutingConfig> {
|
||||
const normalized = normalizeVoiceWakeRoutingConfig(config);
|
||||
const updatedAtMs = Date.now();
|
||||
const next: VoiceWakeRoutingConfig = {
|
||||
...normalized,
|
||||
updatedAtMs,
|
||||
};
|
||||
runOpenClawStateWriteTransaction(
|
||||
({ db }) => {
|
||||
const routingDb = getNodeSqliteKysely<VoiceWakeRoutingDatabase>(db);
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
routingDb
|
||||
.deleteFrom("voicewake_routing_routes")
|
||||
.where("config_key", "=", VOICEWAKE_ROUTING_CONFIG_KEY),
|
||||
);
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
routingDb
|
||||
.deleteFrom("voicewake_routing_config")
|
||||
.where("config_key", "=", VOICEWAKE_ROUTING_CONFIG_KEY),
|
||||
);
|
||||
const defaultTarget = targetColumns(next.defaultTarget);
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
routingDb.insertInto("voicewake_routing_config").values({
|
||||
config_key: VOICEWAKE_ROUTING_CONFIG_KEY,
|
||||
version: 1,
|
||||
default_target_mode: defaultTarget.targetMode,
|
||||
default_target_agent_id: defaultTarget.targetAgentId,
|
||||
default_target_session_key: defaultTarget.targetSessionKey,
|
||||
updated_at_ms: updatedAtMs,
|
||||
}),
|
||||
);
|
||||
if (next.routes.length > 0) {
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
routingDb.insertInto("voicewake_routing_routes").values(
|
||||
next.routes.map((route, position) => {
|
||||
const target = targetColumns(route.target);
|
||||
return {
|
||||
config_key: VOICEWAKE_ROUTING_CONFIG_KEY,
|
||||
position,
|
||||
trigger: route.trigger,
|
||||
target_mode: target.targetMode,
|
||||
target_agent_id: target.targetAgentId,
|
||||
target_session_key: target.targetSessionKey,
|
||||
updated_at_ms: updatedAtMs,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
baseDir ? { env: { ...process.env, OPENCLAW_STATE_DIR: baseDir } } : {},
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
type VoiceWakeResolvedRoute = { mode: "current" } | { agentId: string } | { sessionKey: string };
|
||||
|
||||
function resolveVoiceWakeRouteTarget(
|
||||
|
||||
Reference in New Issue
Block a user