diff --git a/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt b/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt index 1b0faff3a0cf..86a7dc0d6d3d 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt @@ -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"), diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index c9e96253eb50..ee9a04db2160 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -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 diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 2959c433bb8a..88150e91e6a9 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -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 diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index 37f44be98206..c0274098f45a 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -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. @@ -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 - `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. diff --git a/docs/nodes/talk.md b/docs/nodes/talk.md index feff4684dc8c..48f10a45bdc6 100644 --- a/docs/nodes/talk.md +++ b/docs/nodes/talk.md @@ -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`). diff --git a/docs/nodes/voicewake.md b/docs/nodes/voicewake.md index 2bf246e337dc..fdf980bc9316 100644 --- a/docs/nodes/voicewake.md +++ b/docs/nodes/voicewake.md @@ -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 { diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 6f95a0e8402d..05105b66b61c 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -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. | diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index e8ed76147469..eecdcd3baa25 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -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. diff --git a/packages/gateway-protocol/src/index.test.ts b/packages/gateway-protocol/src/index.test.ts index fc63e9bc9806..ca5e1ca010cf 100644 --- a/packages/gateway-protocol/src/index.test.ts +++ b/packages/gateway-protocol/src/index.test.ts @@ -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({ diff --git a/packages/gateway-protocol/src/index.ts b/packages/gateway-protocol/src/index.ts index d9fd67bf99c3..3971e6cd86ac 100644 --- a/packages/gateway-protocol/src/index.ts +++ b/packages/gateway-protocol/src/index.ts @@ -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, diff --git a/packages/gateway-protocol/src/schema/channels.ts b/packages/gateway-protocol/src/schema/channels.ts index 137f9a3e1faf..c313c1594d61 100644 --- a/packages/gateway-protocol/src/schema/channels.ts +++ b/packages/gateway-protocol/src/schema/channels.ts @@ -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; export type TalkClientMutationResult = Static; export type TalkSessionCreateParams = Static; export type TalkSessionCreateResult = Static; -export type TalkSessionJoinParams = Static; -export type TalkSessionJoinResult = Static; export type TalkSessionAppendAudioParams = Static; -export type TalkSessionTurnParams = Static; -export type TalkSessionCancelTurnParams = Static; export type TalkSessionCancelOutputParams = Static; -export type TalkSessionTurnResult = Static; export type TalkSessionSteerParams = Static; export type TalkSessionSubmitToolResultParams = Static< typeof TalkSessionSubmitToolResultParamsSchema diff --git a/packages/gateway-protocol/src/schema/protocol-schema-fragment-channels.ts b/packages/gateway-protocol/src/schema/protocol-schema-fragment-channels.ts index 220d698c527e..1a698c298656 100644 --- a/packages/gateway-protocol/src/schema/protocol-schema-fragment-channels.ts +++ b/packages/gateway-protocol/src/schema/protocol-schema-fragment-channels.ts @@ -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, diff --git a/packages/gateway-protocol/src/schema/protocol-schema-fragment-sessions-lifecycle.ts b/packages/gateway-protocol/src/schema/protocol-schema-fragment-sessions-lifecycle.ts index 891dedeea8eb..f58f0f2a2d59 100644 --- a/packages/gateway-protocol/src/schema/protocol-schema-fragment-sessions-lifecycle.ts +++ b/packages/gateway-protocol/src/schema/protocol-schema-fragment-sessions-lifecycle.ts @@ -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, diff --git a/packages/gateway-protocol/src/schema/sessions.ts b/packages/gateway-protocol/src/schema/sessions.ts index 490c9a400d3b..461ffe2e633a 100644 --- a/packages/gateway-protocol/src/schema/sessions.ts +++ b/packages/gateway-protocol/src/schema/sessions.ts @@ -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; export type SessionsCompanionResetResult = Static; export type SessionsCompactionListParams = Static; -export type SessionsCompactionGetParams = Static; export type SessionsCompactionBranchParams = Static; export type SessionsCompactionRestoreParams = Static; export type SessionsCompactionListResult = Static; -export type SessionsCompactionGetResult = Static; export type SessionsCompactionBranchResult = Static; export type SessionsCompactionRestoreResult = Static; export type SessionsRewindParams = Static; diff --git a/packages/gateway-protocol/src/schema/terminal-protocol-schemas.ts b/packages/gateway-protocol/src/schema/terminal-protocol-schemas.ts index 627a94c4a8e8..32bc852ea8fa 100644 --- a/packages/gateway-protocol/src/schema/terminal-protocol-schemas.ts +++ b/packages/gateway-protocol/src/schema/terminal-protocol-schemas.ts @@ -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, diff --git a/packages/gateway-protocol/src/schema/terminal.ts b/packages/gateway-protocol/src/schema/terminal.ts index 5397a5e7c5ca..5672a1c1acc3 100644 --- a/packages/gateway-protocol/src/schema/terminal.ts +++ b/packages/gateway-protocol/src/schema/terminal.ts @@ -126,14 +126,6 @@ export const TerminalListResultSchema = closedObject({ }); export type TerminalListResult = Static; -/** Reads the current output buffer as plain text without attaching. */ -export const TerminalTextParamsSchema = closedObject({ sessionId: NonEmptyString }); -export type TerminalTextParams = Static; - -/** Plain-text buffer contents (ANSI stripped); an agent/LLM affordance. */ -export const TerminalTextResultSchema = closedObject({ text: Type.String() }); -export type TerminalTextResult = Static; - /** Shared ok/void result for input, resize, and close. */ export const TerminalAckResultSchema = closedObject({ ok: Type.Boolean() }); export type TerminalAckResult = Static; diff --git a/packages/gateway-protocol/src/terminal-validators.ts b/packages/gateway-protocol/src/terminal-validators.ts index e630219706d6..b5ce3bc145b8 100644 --- a/packages/gateway-protocol/src/terminal-validators.ts +++ b/packages/gateway-protocol/src/terminal-validators.ts @@ -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); diff --git a/packages/gateway-protocol/src/validator-registry.ts b/packages/gateway-protocol/src/validator-registry.ts index 94fe91c228c5..130d821b1d69 100644 --- a/packages/gateway-protocol/src/validator-registry.ts +++ b/packages/gateway-protocol/src/validator-registry.ts @@ -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( diff --git a/src/agents/bash-tools.exec-host-gateway.test.ts b/src/agents/bash-tools.exec-host-gateway.test.ts index 8d273b342d91..6a26b86c60e7 100644 --- a/src/agents/bash-tools.exec-host-gateway.test.ts +++ b/src/agents/bash-tools.exec-host-gateway.test.ts @@ -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(); diff --git a/src/gateway/method-scopes.test.ts b/src/gateway/method-scopes.test.ts index 5c597a7b754b..3917f7445b6e 100644 --- a/src/gateway/method-scopes.test.ts +++ b/src/gateway/method-scopes.test.ts @@ -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", diff --git a/src/gateway/methods/core-descriptors.since.test.ts b/src/gateway/methods/core-descriptors.since.test.ts index 7e5f4ae5af81..3d909c888d97 100644 --- a/src/gateway/methods/core-descriptors.since.test.ts +++ b/src/gateway/methods/core-descriptors.since.test.ts @@ -26,7 +26,6 @@ const CURRENT_TRAIN_METHODS = [ "terminal.close", "terminal.attach", "terminal.list", - "terminal.text", "terminal.upload", "worktrees.list", "worktrees.branches", diff --git a/src/gateway/methods/core-descriptors.ts b/src/gateway/methods/core-descriptors.ts index b400543487af..54528b9f9ff7 100644 --- a/src/gateway/methods/core-descriptors.ts +++ b/src/gateway/methods/core-descriptors.ts @@ -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"], diff --git a/src/gateway/server-methods-list.test.ts b/src/gateway/server-methods-list.test.ts index 60e6bdb94036..2436efadc3e1 100644 --- a/src/gateway/server-methods-list.test.ts +++ b/src/gateway/server-methods-list.test.ts @@ -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"); diff --git a/src/gateway/server-methods/doctor.memory-core-runtime.ts b/src/gateway/server-methods/doctor.memory-core-runtime.ts index 3938292e9ec8..0316a8987bb9 100644 --- a/src/gateway/server-methods/doctor.memory-core-runtime.ts +++ b/src/gateway/server-methods/doctor.memory-core-runtime.ts @@ -8,7 +8,6 @@ export { dedupeDreamDiaryEntries, loadShortTermPromotionDreamingStats, previewGroundedRemMarkdown, - previewRemHarness, removeBackfillDiaryEntries, removeGroundedShortTermCandidates, repairDreamingArtifacts, diff --git a/src/gateway/server-methods/doctor.test.ts b/src/gateway/server-methods/doctor.test.ts index 2924077037bd..4186d60087c7 100644 --- a/src/gateway/server-methods/doctor.test.ts +++ b/src/gateway/server-methods/doctor.test.ts @@ -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; - grounded: Record | null; - deep: Record; - remConfig: Record; - deepConfig: Record; - }> = {}, - ) => ({ - 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. */ diff --git a/src/gateway/server-methods/doctor.ts b/src/gateway/server-methods/doctor.ts index cc116e757565..36f7e40e2a33 100644 --- a/src/gateway/server-methods/doctor.ts +++ b/src/gateway/server-methods/doctor.ts @@ -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. */ diff --git a/src/gateway/server-methods/restart.test.ts b/src/gateway/server-methods/restart.test.ts index fd326333f02b..426959e4e729 100644 --- a/src/gateway/server-methods/restart.test.ts +++ b/src/gateway/server-methods/restart.test.ts @@ -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), })); diff --git a/src/gateway/server-methods/restart.ts b/src/gateway/server-methods/restart.ts index eeb228e907d3..d5987e5f3f12 100644 --- a/src/gateway/server-methods/restart.ts +++ b/src/gateway/server-methods/restart.ts @@ -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()); - }, }; diff --git a/src/gateway/server-methods/sessions-compaction-queries.ts b/src/gateway/server-methods/sessions-compaction-queries.ts index 931a74ab8fa0..dd6b8e4e91a7 100644 --- a/src/gateway/server-methods/sessions-compaction-queries.ts +++ b/src/gateway/server-methods/sessions-compaction-queries.ts @@ -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, - ); - }, }; diff --git a/src/gateway/server-methods/sessions-subscriptions.ts b/src/gateway/server-methods/sessions-subscriptions.ts index 1ab6dff360fe..de80e0055527 100644 --- a/src/gateway/server-methods/sessions-subscriptions.ts +++ b/src/gateway/server-methods/sessions-subscriptions.ts @@ -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( diff --git a/src/gateway/server-methods/system-agent.test.ts b/src/gateway/server-methods/system-agent.test.ts index 7aaf9bd4f5ca..26e213481623 100644 --- a/src/gateway/server-methods/system-agent.test.ts +++ b/src/gateway/server-methods/system-agent.test.ts @@ -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 () => { diff --git a/src/gateway/server-methods/talk-session.ts b/src/gateway/server-methods/talk-session.ts index a1367d8fd75d..cf2150f67a32 100644 --- a/src/gateway/server-methods/talk-session.ts +++ b/src/gateway/server-methods/talk-session.ts @@ -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; - 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( diff --git a/src/gateway/server-methods/talk-shared.ts b/src/gateway/server-methods/talk-shared.ts index 8f18b000779f..a11e0118feac 100644 --- a/src/gateway/server-methods/talk-shared.ts +++ b/src/gateway/server-methods/talk-shared.ts @@ -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["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 | undefined { return asOptionalRecord(value) ?? undefined; } diff --git a/src/gateway/server-methods/talk.test.ts b/src/gateway/server-methods/talk.test.ts index 489b4fb7e84d..0f689fa2f99e 100644 --- a/src/gateway/server-methods/talk.test.ts +++ b/src/gateway/server-methods/talk.test.ts @@ -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(); 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; - 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; - }; - 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", { diff --git a/src/gateway/server-methods/terminal.test.ts b/src/gateway/server-methods/terminal.test.ts index bd8d943abfcc..d4c1e8ba4964 100644 --- a/src/gateway/server-methods/terminal.test.ts +++ b/src/gateway/server-methods/terminal.test.ts @@ -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" }); - }); }); diff --git a/src/gateway/server-methods/terminal.ts b/src/gateway/server-methods/terminal.ts index a6941cb31a94..23f44dc05890 100644 --- a/src/gateway/server-methods/terminal.ts +++ b/src/gateway/server-methods/terminal.ts @@ -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) }); - }, }; diff --git a/src/gateway/server-methods/voicewake-routing.ts b/src/gateway/server-methods/voicewake-routing.ts index db36ee601472..5b94484f732a 100644 --- a/src/gateway/server-methods/voicewake-routing.ts +++ b/src/gateway/server-methods/voicewake-routing.ts @@ -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 }); - }, }; diff --git a/src/gateway/server-methods/wizard.test.ts b/src/gateway/server-methods/wizard.test.ts index 214471fffae0..09a60578e43d 100644 --- a/src/gateway/server-methods/wizard.test.ts +++ b/src/gateway/server-methods/wizard.test.ts @@ -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(); diff --git a/src/gateway/server.models-voicewake-misc.test.ts b/src/gateway/server.models-voicewake-misc.test.ts index 90aed1058314..01994df0df34 100644 --- a/src/gateway/server.models-voicewake-misc.test.ts +++ b/src/gateway/server.models-voicewake-misc.test.ts @@ -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 | 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 | 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" } }]); }); }); diff --git a/src/gateway/server.sessions.compaction.test.ts b/src/gateway/server.sessions.compaction.test.ts index 6f11858a8a09..2e5a306c5b11 100644 --- a/src/gateway/server.sessions.compaction.test.ts +++ b/src/gateway/server.sessions.compaction.test.ts @@ -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, diff --git a/src/gateway/talk-handoff.test.ts b/src/gateway/talk-handoff.test.ts index 1df4f3582eb0..a620c324a7ec 100644 --- a/src/gateway/talk-handoff.test.ts +++ b/src/gateway/talk-handoff.test.ts @@ -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 { - 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", diff --git a/src/gateway/talk-handoff.ts b/src/gateway/talk-handoff.ts index ef401ea7652d..c9bda3f61adf 100644 --- a/src/gateway/talk-handoff.ts +++ b/src/gateway/talk-handoff.ts @@ -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; -} diff --git a/src/gateway/talk-session-registry.ts b/src/gateway/talk-session-registry.ts index f02a140103df..181e304985a9 100644 --- a/src/gateway/talk-session-registry.ts +++ b/src/gateway/talk-session-registry.ts @@ -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; diff --git a/src/gateway/talk-transcription-relay.test.ts b/src/gateway/talk-transcription-relay.test.ts index ddcc236d4038..0ecf1caea0e7 100644 --- a/src/gateway/talk-transcription-relay.test.ts +++ b/src/gateway/talk-transcription-relay.test.ts @@ -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); - }); }); diff --git a/src/gateway/talk-transcription-relay.ts b/src/gateway/talk-transcription-relay.ts index 6af76f5184c3..e78d75b6ce85 100644 --- a/src/gateway/talk-transcription-relay.ts +++ b/src/gateway/talk-transcription-relay.ts @@ -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"); -} diff --git a/src/gateway/terminal/buffer-text.ts b/src/gateway/terminal/buffer-text.ts index 95b83368679e..6a7a45a8a189 100644 --- a/src/gateway/terminal/buffer-text.ts +++ b/src/gateway/terminal/buffer-text.ts @@ -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 diff --git a/src/gateway/terminal/session-limits.ts b/src/gateway/terminal/session-limits.ts index aa8b76c08521..7427a2f350db 100644 --- a/src/gateway/terminal/session-limits.ts +++ b/src/gateway/terminal/session-limits.ts @@ -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; diff --git a/src/infra/infra-store.test.ts b/src/infra/infra-store.test.ts index 3c87037da96c..37ced5692dba 100644 --- a/src/infra/infra-store.test.ts +++ b/src/infra/infra-store.test.ts @@ -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({ diff --git a/src/infra/restart-coordinator.test.ts b/src/infra/restart-coordinator.test.ts index 1da6769a9ae9..973ba193eceb 100644 --- a/src/infra/restart-coordinator.test.ts +++ b/src/infra/restart-coordinator.test.ts @@ -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[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, diff --git a/src/infra/restart-coordinator.ts b/src/infra/restart-coordinator.ts index 2ac563ebb83c..610cefd2beb4 100644 --- a/src/infra/restart-coordinator.ts +++ b/src/infra/restart-coordinator.ts @@ -55,7 +55,7 @@ export type SafeGatewayRestartRequestResult = { restart: ScheduledRestart; }; -export function createSafeGatewayRestartPreflight( +function createSafeGatewayRestartPreflight( inspectors: Partial = {}, ): SafeGatewayRestartPreflight { const snapshot = createGatewayActiveWorkSnapshot({ diff --git a/src/infra/state-migrations.test.ts b/src/infra/state-migrations.test.ts index 1cebcc1d19cb..49abb567cd3c 100644 --- a/src/infra/state-migrations.test.ts +++ b/src/infra/state-migrations.test.ts @@ -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[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(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, diff --git a/src/infra/voicewake-routing.test.ts b/src/infra/voicewake-routing.test.ts index a318f121dc17..42e2d6063db0 100644 --- a/src/infra/voicewake-routing.test.ts +++ b/src/infra/voicewake-routing.test.ts @@ -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", - }); - }); }); diff --git a/src/infra/voicewake-routing.ts b/src/infra/voicewake-routing.ts index 93900f1195ab..9a249d1d2279 100644 --- a/src/infra/voicewake-routing.ts +++ b/src/infra/voicewake-routing.ts @@ -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(); - 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 { - const normalized = normalizeVoiceWakeRoutingConfig(config); - const updatedAtMs = Date.now(); - const next: VoiceWakeRoutingConfig = { - ...normalized, - updatedAtMs, - }; - runOpenClawStateWriteTransaction( - ({ db }) => { - const routingDb = getNodeSqliteKysely(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(