diff --git a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift index 5c35ef7dce2c..20d18412cb3a 100644 --- a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift +++ b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift @@ -7,6 +7,7 @@ import OSLog struct IOSGatewayChatTransport: OpenClawChatTransport { static let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "ios.chat.transport") static let defaultChatSendTimeoutMs = 30000 + static let compactionRequestTimeoutSeconds = 0 private let gateway: GatewayNodeSession private struct CreateSessionParams: Codable { @@ -205,7 +206,11 @@ struct IOSGatewayChatTransport: OpenClawChatTransport { func compactSession(sessionKey: String) async throws { let json = try Self.makeSessionKeyParamsJSON(sessionKey) - _ = try await self.gateway.request(method: "sessions.compact", paramsJSON: json, timeoutSeconds: 10) + let response = try await self.gateway.request( + method: "sessions.compact", + paramsJSON: json, + timeoutSeconds: Self.compactionRequestTimeoutSeconds) + try OpenClawSessionsCompactResponse.requireSuccess(from: response) } func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload { diff --git a/apps/ios/Tests/IOSGatewayChatTransportTests.swift b/apps/ios/Tests/IOSGatewayChatTransportTests.swift index 8216c0e62bd8..38d85b7d332e 100644 --- a/apps/ios/Tests/IOSGatewayChatTransportTests.swift +++ b/apps/ios/Tests/IOSGatewayChatTransportTests.swift @@ -27,6 +27,10 @@ import Testing #expect(IOSGatewayChatTransport.agentWaitRequestTimeoutSeconds(timeoutMs: 30000) == 35) } + @Test func compactionLeavesTerminalTimeoutToGateway() { + #expect(IOSGatewayChatTransport.compactionRequestTimeoutSeconds == 0) + } + @Test func agentWaitCompletionDecodesFallbackRunId() throws { let data = Data(#"{"status":"completed"}"#.utf8) let completion = try IOSGatewayChatTransport.decodeAgentWaitCompletion(data, fallbackRunId: "run-local") diff --git a/apps/macos/Sources/OpenClaw/ControlChannel.swift b/apps/macos/Sources/OpenClaw/ControlChannel.swift index 2cfe7482e5fc..90e9de4627f8 100644 --- a/apps/macos/Sources/OpenClaw/ControlChannel.swift +++ b/apps/macos/Sources/OpenClaw/ControlChannel.swift @@ -245,7 +245,8 @@ final class ControlChannel { func request( method: String, params: [String: AnyHashable]? = nil, - timeoutMs: Double? = nil) async throws -> Data + timeoutMs: Double? = nil, + retryTransportFailures: Bool = true) async throws -> Data { do { let rawParams = params?.reduce(into: [String: OpenClawKit.AnyCodable]()) { @@ -254,7 +255,8 @@ final class ControlChannel { let data = try await GatewayConnection.shared.request( method: method, params: rawParams, - timeoutMs: timeoutMs) + timeoutMs: timeoutMs, + retryTransportFailures: retryTransportFailures) self.setStateThrottled(.connected) return data } catch { diff --git a/apps/macos/Sources/OpenClaw/GatewayConnection.swift b/apps/macos/Sources/OpenClaw/GatewayConnection.swift index f6e4dd1a2dbd..e8478d4d3b34 100644 --- a/apps/macos/Sources/OpenClaw/GatewayConnection.swift +++ b/apps/macos/Sources/OpenClaw/GatewayConnection.swift @@ -163,7 +163,8 @@ actor GatewayConnection { func request( method: String, params: [String: AnyCodable]?, - timeoutMs: Double? = nil) async throws -> Data + timeoutMs: Double? = nil, + retryTransportFailures: Bool = true) async throws -> Data { let cfg = try await self.configProvider() await self.configure(url: cfg.url, token: cfg.token, password: cfg.password) @@ -174,7 +175,7 @@ actor GatewayConnection { do { return try await client.request(method: method, params: params, timeoutMs: timeoutMs) } catch { - if error is GatewayResponseError || error is GatewayDecodingError { + if !retryTransportFailures || error is GatewayResponseError || error is GatewayDecodingError { throw error } diff --git a/apps/macos/Sources/OpenClaw/SessionActions.swift b/apps/macos/Sources/OpenClaw/SessionActions.swift index 10a3c7641d4f..c5e27ed56e7f 100644 --- a/apps/macos/Sources/OpenClaw/SessionActions.swift +++ b/apps/macos/Sources/OpenClaw/SessionActions.swift @@ -1,5 +1,6 @@ import AppKit import Foundation +import OpenClawKit enum SessionActions { static func patchSession( @@ -32,9 +33,12 @@ enum SessionActions { } static func compactSession(key: String, maxLines: Int = 400) async throws { - _ = try await ControlChannel.shared.request( + let response = try await ControlChannel.shared.request( method: "sessions.compact", - params: ["key": AnyHashable(key), "maxLines": AnyHashable(maxLines)]) + params: ["key": AnyHashable(key), "maxLines": AnyHashable(maxLines)], + timeoutMs: 0, + retryTransportFailures: false) + try OpenClawSessionsCompactResponse.requireSuccess(from: response) } @MainActor diff --git a/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift b/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift index a47d5a1393f2..4460f73767b2 100644 --- a/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift +++ b/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift @@ -131,10 +131,12 @@ struct MacGatewayChatTransport: OpenClawChatTransport { } func compactSession(sessionKey: String) async throws { - _ = try await GatewayConnection.shared.request( + let response = try await GatewayConnection.shared.request( method: "sessions.compact", params: ["key": AnyCodable(sessionKey)], - timeoutMs: 10000) + timeoutMs: 0, + retryTransportFailures: false) + try OpenClawSessionsCompactResponse.requireSuccess(from: response) } func setActiveSessionKey(_ sessionKey: String) async throws { diff --git a/apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift b/apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift index 7ad66edef3ce..93dfe72227f3 100644 --- a/apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift @@ -92,6 +92,30 @@ struct GatewayConnectionTests { #expect(session.snapshotMakeCount() == 1) } + @Test func `request can disable retries for non idempotent mutations`() async throws { + let session = GatewayTestWebSocketSession( + taskFactory: { + GatewayTestWebSocketTask(sendHook: { _, _, sendIndex in + if sendIndex > 0 { + throw URLError(.timedOut) + } + }) + }) + let (conn, _) = try self.makeConnection(session: session) + + do { + _ = try await conn.request( + method: "sessions.compact", + params: nil, + timeoutMs: 10, + retryTransportFailures: false) + Issue.record("expected sessions.compact transport failure") + } catch {} + + #expect(session.snapshotMakeCount() == 1) + #expect(session.latestTask()?.snapshotSendCount() == 2) + } + @Test func `subscribe replays latest snapshot`() async throws { let session = self.makeSession() let (conn, _) = try self.makeConnection(session: session) diff --git a/apps/macos/Tests/OpenClawIPCTests/GatewayWebSocketTestSupport.swift b/apps/macos/Tests/OpenClawIPCTests/GatewayWebSocketTestSupport.swift index 076654c11a46..e995c498f12d 100644 --- a/apps/macos/Tests/OpenClawIPCTests/GatewayWebSocketTestSupport.swift +++ b/apps/macos/Tests/OpenClawIPCTests/GatewayWebSocketTestSupport.swift @@ -175,6 +175,10 @@ final class GatewayTestWebSocketTask: WebSocketTasking, @unchecked Sendable { self.lock.withLock { self.connectRequestID } } + func snapshotSendCount() -> Int { + self.lock.withLock { self.sendCount } + } + func resume() { self.state = .running } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift index 7a0ba9add5d7..d206934a8b82 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift @@ -241,6 +241,10 @@ private enum GatewayConnectErrorCodes { } public actor GatewayChannelActor { + nonisolated static func resolveRequestTimeoutMs(_ timeoutMs: Double?, defaultMs: Double) -> Double? { + timeoutMs == 0 ? nil : (timeoutMs ?? defaultMs) + } + private let logger = Logger(subsystem: "ai.openclaw", category: "gateway") private var task: WebSocketTaskBox? private var pending: [String: CheckedContinuation] = [:] @@ -1174,14 +1178,17 @@ public actor GatewayChannelActor { timeoutMs: Double? = nil) async throws -> Data { try await self.connectOrThrow(context: "gateway connect") - let effectiveTimeout = timeoutMs ?? self.defaultRequestTimeoutMs + // Zero leaves terminal-operation deadlines to the Gateway owner. + let effectiveTimeout = Self.resolveRequestTimeoutMs(timeoutMs, defaultMs: self.defaultRequestTimeoutMs) let payload = try self.encodeRequest(method: method, params: params, kind: "request") let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in self.pending[payload.id] = cont - Task { [weak self] in - guard let self else { return } - try? await Task.sleep(nanoseconds: UInt64(effectiveTimeout * 1_000_000)) - await self.timeoutRequest(id: payload.id, timeoutMs: effectiveTimeout) + if let effectiveTimeout { + Task { [weak self] in + guard let self else { return } + try? await Task.sleep(nanoseconds: UInt64(effectiveTimeout * 1_000_000)) + await self.timeoutRequest(id: payload.id, timeoutMs: effectiveTimeout) + } } Task { do { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/SessionMutationResponses.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/SessionMutationResponses.swift new file mode 100644 index 000000000000..808130bd4223 --- /dev/null +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/SessionMutationResponses.swift @@ -0,0 +1,26 @@ +import Foundation + +public struct OpenClawSessionsCompactResponse: Decodable, Sendable { + public let ok: Bool + public let reason: String? + + public static func requireSuccess(from data: Data) throws { + let response = try JSONDecoder().decode(Self.self, from: data) + guard response.ok else { + throw OpenClawSessionsCompactError(reason: response.reason) + } + } +} + +public struct OpenClawSessionsCompactError: Error, LocalizedError, Sendable { + public let reason: String? + + public var errorDescription: String? { + let detail = self.reason?.trimmingCharacters(in: .whitespacesAndNewlines) + return detail?.isEmpty == false ? detail : "Session compaction failed" + } + + public init(reason: String?) { + self.reason = reason + } +} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index a8f9f7f2e6fb..39e862de9fb3 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -1640,6 +1640,7 @@ public struct SessionsListParams: Codable, Sendable { public let spawnedby: String? public let agentid: String? public let search: String? + public let archived: Bool? public init( limit: Int?, @@ -1653,7 +1654,8 @@ public struct SessionsListParams: Codable, Sendable { label: String?, spawnedby: String?, agentid: String? = nil, - search: String?) + search: String?, + archived: Bool? = nil) { self.limit = limit self.offset = offset @@ -1667,6 +1669,7 @@ public struct SessionsListParams: Codable, Sendable { self.spawnedby = spawnedby self.agentid = agentid self.search = search + self.archived = archived } private enum CodingKeys: String, CodingKey { @@ -1682,6 +1685,7 @@ public struct SessionsListParams: Codable, Sendable { case spawnedby = "spawnedBy" case agentid = "agentId" case search + case archived } } @@ -2433,6 +2437,8 @@ public struct SessionsPatchParams: Codable, Sendable { public let key: String public let agentid: String? public let label: AnyCodable? + public let archived: Bool? + public let pinned: Bool? public let thinkinglevel: AnyCodable? public let fastmode: AnyCodable? public let verboselevel: AnyCodable? @@ -2460,6 +2466,8 @@ public struct SessionsPatchParams: Codable, Sendable { key: String, agentid: String? = nil, label: AnyCodable?, + archived: Bool? = nil, + pinned: Bool? = nil, thinkinglevel: AnyCodable?, fastmode: AnyCodable?, verboselevel: AnyCodable?, @@ -2486,6 +2494,8 @@ public struct SessionsPatchParams: Codable, Sendable { self.key = key self.agentid = agentid self.label = label + self.archived = archived + self.pinned = pinned self.thinkinglevel = thinkinglevel self.fastmode = fastmode self.verboselevel = verboselevel @@ -2514,6 +2524,8 @@ public struct SessionsPatchParams: Codable, Sendable { case key case agentid = "agentId" case label + case archived + case pinned case thinkinglevel = "thinkingLevel" case fastmode = "fastMode" case verboselevel = "verboseLevel" @@ -2617,17 +2629,26 @@ public struct SessionsDeleteParams: Codable, Sendable { public let key: String public let agentid: String? public let deletetranscript: Bool? + public let expectedsessionid: String? + public let expectedlifecyclerevision: String? + public let expectedsessionupdatedat: Double? public let emitlifecyclehooks: Bool? public init( key: String, agentid: String? = nil, deletetranscript: Bool?, + expectedsessionid: String? = nil, + expectedlifecyclerevision: String? = nil, + expectedsessionupdatedat: Double? = nil, emitlifecyclehooks: Bool?) { self.key = key self.agentid = agentid self.deletetranscript = deletetranscript + self.expectedsessionid = expectedsessionid + self.expectedlifecyclerevision = expectedlifecyclerevision + self.expectedsessionupdatedat = expectedsessionupdatedat self.emitlifecyclehooks = emitlifecyclehooks } @@ -2635,6 +2656,9 @@ public struct SessionsDeleteParams: Codable, Sendable { case key case agentid = "agentId" case deletetranscript = "deleteTranscript" + case expectedsessionid = "expectedSessionId" + case expectedlifecyclerevision = "expectedLifecycleRevision" + case expectedsessionupdatedat = "expectedSessionUpdatedAt" case emitlifecyclehooks = "emitLifecycleHooks" } } diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift index 41015861ca18..ca1f13e50af8 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift @@ -916,6 +916,13 @@ struct GatewayNodeSessionTests { #expect(response.error == nil) } + @Test + func `gateway request timeout zero disables the client deadline`() { + #expect(GatewayChannelActor.resolveRequestTimeoutMs(0, defaultMs: 15000) == nil) + #expect(GatewayChannelActor.resolveRequestTimeoutMs(nil, defaultMs: 15000) == 15000) + #expect(GatewayChannelActor.resolveRequestTimeoutMs(30000, defaultMs: 15000) == 30000) + } + @Test func `emits synthetic seq gap after reconnect snapshot`() async throws { let session = FakeGatewayWebSocketSession() diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/SessionMutationResponsesTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/SessionMutationResponsesTests.swift new file mode 100644 index 000000000000..da4f5fbc2b76 --- /dev/null +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/SessionMutationResponsesTests.swift @@ -0,0 +1,26 @@ +import Foundation +import OpenClawKit +import Testing + +struct SessionMutationResponsesTests { + @Test + func compactResponseAcceptsSuccess() throws { + try OpenClawSessionsCompactResponse.requireSuccess( + from: Data(#"{"ok":true,"key":"agent:main:main","compacted":true}"#.utf8)) + } + + @Test + func compactResponseSurfacesGatewayFailureReason() { + let data = Data( + #"{"ok":false,"key":"agent:main:main","compacted":false,"reason":"turn failed"}"#.utf8) + do { + try OpenClawSessionsCompactResponse.requireSuccess( + from: data) + Issue.record("expected failed compaction response to throw") + } catch let error as OpenClawSessionsCompactError { + #expect(error.errorDescription == "turn failed") + } catch { + Issue.record("unexpected error: \(error)") + } + } +} diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 772b1bbebfa8..47a25e559af6 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -acdf418738f79d29f58cb6cfe5b7ab2d353cbcce2252861a4f527f85ea0c54af config-baseline.json +f5e127927b1810ec9769f43ed15fbabde4c79a8778cc486e12119527df9b5ebb config-baseline.json 4c98c716bc78e65c274ec374757357c1dcc9b5ec75c9e00ea4c20851531b7d1a config-baseline.core.json c68853362689981ac1cc1e55b9061286c2002104ff1c10bc44ee99a6080e169e config-baseline.channel.json -859aa272b0dad53b7080c6fefcf775347ae79a1998ec39dd18b732c90d9df90c config-baseline.plugin.json +4721a543fc05a2dee5d6af676a145069adf7849fe50e18c12a8515f9d9fe8b73 config-baseline.plugin.json diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index df17f17c77e5..78a87469f00d 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -a4f18fb57ec58de32a554b6481190016e421246fef04fc8f8045271f4de843fe plugin-sdk-api-baseline.json -80d3e71a2183c95e0177df970429e639e9702a23a76bd7b852325eef32e7acd1 plugin-sdk-api-baseline.jsonl +95e17039bab1ad1dd3294948868740fb0315151b982e8f5c31859ba68fea2c0b plugin-sdk-api-baseline.json +1c936a4d3ffcf00a16df5aa9bbbba109ed856c4adfd6af6118841557e2e4ec88 plugin-sdk-api-baseline.jsonl diff --git a/docs/cli/sessions.md b/docs/cli/sessions.md index 556e1416f7ee..bf7bea3e9051 100644 --- a/docs/cli/sessions.md +++ b/docs/cli/sessions.md @@ -179,11 +179,11 @@ openclaw sessions compact "agent:main:main" --max-lines 200 openclaw sessions compact "agent:work:main" --agent work --json ``` -- Without `--max-lines`, the gateway LLM-summarizes the transcript. This can be slow, so the default `--timeout` is `180000` ms. +- Without `--max-lines`, the gateway LLM-summarizes the transcript. The CLI does not impose a client deadline by default; the gateway owns the configured compaction lifecycle. - With `--max-lines `, it truncates to the last `n` transcript lines and archives the prior transcript as a `.bak` sidecar. - `--agent `: agent that owns the session; required for `global` keys. - `--url` / `--token` / `--password`: gateway connection overrides. -- `--timeout `: RPC timeout in milliseconds. +- `--timeout `: optional client-side RPC timeout in milliseconds. - `--json`: print the raw RPC payload. The command exits non-zero when the gateway reports a failed compaction or is unreachable, so crons and scripts never mistake a silent no-op for success. diff --git a/docs/concepts/session-tool.md b/docs/concepts/session-tool.md index 6e321948b83e..2763999a6ec1 100644 --- a/docs/concepts/session-tool.md +++ b/docs/concepts/session-tool.md @@ -14,7 +14,7 @@ orchestrate sub-agents. | Tool | What it does | | ------------------ | --------------------------------------------------------------------------- | -| `sessions_list` | List sessions with optional filters (kind, label, agent, recency, preview) | +| `sessions_list` | List sessions with optional filters (kind, label, agent, archive, preview) | | `sessions_history` | Read the transcript of a specific session | | `sessions_send` | Send a message to another session and optionally wait | | `sessions_spawn` | Spawn an isolated sub-agent session for background work | @@ -48,7 +48,9 @@ effective tool list. `sessions_list` returns sessions with their key, agentId, kind, channel, model, token counts, and timestamps. Filter by kind (`main`, `group`, `cron`, `hook`, `node`), exact `label`, exact `agentId`, search text, or recency -(`activeMinutes`). When you need mailbox-style triage, it can also ask for a +(`activeMinutes`). Active sessions are returned by default; pass `archived: true` +to inspect archived sessions. Rows include their pinned and archived state. When +you need mailbox-style triage, it can also ask for a visibility-scoped derived title, a last-message preview snippet, or bounded recent messages on each row. Derived titles and previews are produced only for sessions the caller can already see under the configured session tool visibility policy, so diff --git a/docs/plugins/codex-harness-runtime.md b/docs/plugins/codex-harness-runtime.md index 94169c0f148c..41dde95bff81 100644 --- a/docs/plugins/codex-harness-runtime.md +++ b/docs/plugins/codex-harness-runtime.md @@ -225,9 +225,17 @@ history, search, `/new`, `/reset`, and future model or harness switching. Explicit compaction requests, such as `/compact` or a plugin-requested manual compact operation, start native Codex compaction with `thread/compact/start`. -OpenClaw returns after starting that native operation. It does not wait for -completion, impose a separate OpenClaw timeout, restart the shared Codex -app-server, or record the operation as an OpenClaw-completed compaction. +OpenClaw keeps the request and shared-client lease open until Codex emits the +matching `contextCompaction` completion item and then reports the compaction turn +as completed. If that terminal turn exceeds the configured compaction timeout, +OpenClaw requests a native turn interrupt. The lease and per-thread compaction +fence remain held until Codex reports terminal state or confirms the interrupt RPC. +If Codex does not confirm within the interrupt grace period, OpenClaw retires +the connection before releasing the fence. Remote connections also detach the +matching thread binding so later work cannot overlap an unconfirmed remote +turn. Other turns on a retired connection fail and can retry on a fresh client. +Client closure, request cancellation, or a failed compaction turn returns a +failed operation. When a context engine requests Codex thread-bootstrap projection, OpenClaw projects tool-call names and ids, input shapes, and redacted tool-result content @@ -235,10 +243,10 @@ into the fresh Codex thread. It does not copy raw tool-call argument values into that projection. The mirror includes the user prompt, final assistant text, and lightweight Codex -reasoning or plan records when the app-server emits them. Today, OpenClaw only -records explicit native compaction start signals when it requests compaction. It -does not expose a human-readable compaction summary or an auditable list of -which entries Codex kept after compaction. +reasoning or plan records when the app-server emits them. OpenClaw records the +native compaction start and terminal status, but it does not expose a +human-readable compaction summary or an auditable list of which entries Codex +kept after compaction. Because Codex owns the canonical native thread, `tool_result_persist` does not currently rewrite Codex-native tool result records. It only applies when diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md index abac42024d89..660993fa70df 100644 --- a/docs/plugins/sdk-runtime.md +++ b/docs/plugins/sdk-runtime.md @@ -160,10 +160,20 @@ two-party event loops that do not go through the shared inbound reply runner. sessionKey, update: (entry) => ({ thinkingLevel: "high" }), }); + + const storePath = api.runtime.agent.session.resolveStorePath(cfg.session?.store, { agentId }); + await api.runtime.agent.session.runWithWorkAdmission( + { storePath, sessionKey }, + async (signal) => { + // Create or update the session, then pass signal to the admitted agent run. + }, + ); ``` Prefer `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, or `upsertSessionEntry(...)` for session workflows. These helpers address sessions by agent/session identity so plugins do not depend on the legacy `sessions.json` storage shape. Use `preserveActivity: true` for metadata-only patches that should not refresh session activity, and `replaceEntry: true` only when the callback returns a complete entry and deleted fields must stay deleted. + Use `runWithWorkAdmission(...)` when a plugin starts work on a persisted session. The callback rejects archived or concurrently replaced sessions, keeps archive/reset/delete mutations coordinated through completion, and receives an `AbortSignal` that must be forwarded to the agent run. + For transcript reads and writes, import `openclaw/plugin-sdk/session-transcript-runtime` and use `resolveSessionTranscriptIdentity(...)`, `resolveSessionTranscriptTarget(...)`, `readSessionTranscriptEvents(...)`, `appendSessionTranscriptMessageByIdentity(...)`, `publishSessionTranscriptUpdateByIdentity(...)`, or `withSessionTranscriptWriteLock(...)` with `{ agentId, sessionKey, sessionId }`. These APIs let plugins identify a transcript, read its events, append messages, publish updates, and run related operations under the same transcript write lock. Passing `sessionFile`, using `resolveSessionTranscriptLegacyFileTarget(...)`, or importing low-level `appendSessionTranscriptMessage(...)` / `emitSessionTranscriptUpdate(...)` from `openclaw/plugin-sdk/agent-harness-runtime` is deprecated; those paths exist only for legacy code that already receives an active transcript artifact. `loadSessionStore(...)`, `saveSessionStore(...)`, `updateSessionStore(...)`, `resolveSessionFilePath(...)`, and `resolveAndPersistSessionFile(...)` are deprecated compatibility helpers for plugins that still intentionally depend on the legacy whole-store or transcript-file shape. New plugin code must not use those helpers, and existing callers should migrate to entry helpers and transcript identity helpers. diff --git a/docs/reference/session-management-compaction.md b/docs/reference/session-management-compaction.md index a64c16909f63..289f092f6c25 100644 --- a/docs/reference/session-management-compaction.md +++ b/docs/reference/session-management-compaction.md @@ -191,6 +191,18 @@ Key fields (not exhaustive): time for idle freshness. - `updatedAt`: last store-row mutation timestamp, used for listing, pruning, and bookkeeping. It is not the authority for daily/idle reset freshness. +- `archivedAt`: optional archive timestamp. Archived sessions stay in the store + with their transcript intact and are excluded from normal active listings. +- `pinnedAt`: optional pin timestamp. Active pinned sessions sort ahead of + unpinned sessions; archiving a session clears its pin. +- Codex thread interop: both fields follow the Codex thread-management shape — + the `archived`/`pinned` booleans on the wire are always derived from the + timestamp and stamped server-side, matching Codex `threads.archived_at` + semantics and camelCase serialization. OpenClaw timestamps are epoch + milliseconds while Codex uses epoch seconds, so bridges convert at the codex + plugin seam. Codex has no pin API yet (`thread/archive`/`thread/unarchive` + only); pinned state stays OpenClaw-side until one exists, at which point the + matching shape lets bound sessions round-trip pin state mechanically. - `sessionFile`: optional explicit transcript path override - `chatType`: `direct | group | room` (helps UIs and send policy) - `provider`, `subject`, `room`, `space`, `displayName`: metadata for group/channel labeling diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 7209ecda630d..43d414b48b37 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -141,7 +141,7 @@ Imported themes are stored only in the current browser profile. They are not wri - Channels: built-in plus bundled/external plugin channels status, QR login, and per-channel config (`channels.status`, `web.login.*`, `config.patch`). - Channel probe refreshes keep the previous snapshot visible while slow provider checks finish, and partial snapshots are labeled when a probe or audit exceeds its UI budget. - Instances: presence list + refresh (`system-presence`). - - Sessions: list configured-agent sessions by default, fall back from stale unconfigured agent session keys, and apply per-session model/thinking/fast/verbose/trace/reasoning overrides (`sessions.list`, `sessions.patch`). + - Sessions: list configured-agent sessions by default, pin frequent sessions, rename them, archive or restore inactive sessions, fall back from stale unconfigured agent session keys, and apply per-session model/thinking/fast/verbose/trace/reasoning overrides (`sessions.list`, `sessions.patch`). Pinned sessions sort above recent unpinned sessions; archived sessions live in the Sessions page's archived view and keep their transcripts. - Dreams: dreaming status, enable/disable toggle, and Dream Diary reader (`doctor.memory.status`, `doctor.memory.dreamDiary`, `config.patch`). @@ -222,6 +222,7 @@ Activity entries keep only sanitized summaries and redacted, truncated output pr - Live `chat` events are delivery state, while `chat.history` is rebuilt from the durable session transcript. After tool-final events the Control UI reloads history and merges only a small optimistic tail; the transcript boundary is documented in [WebChat](/web/webchat). - `chat.inject` appends an assistant note to the session transcript and broadcasts a `chat` event for UI-only updates (no agent run, no channel delivery). - The sidebar lists recent sessions with a New Session action, an All Sessions link, and a session search button that opens the full session picker (scoped by the selected agent, with search and pagination). Switching agents shows only sessions tied to that agent and falls back to that agent's main session when it has no saved dashboard sessions yet. + - Each session-picker row can rename, pin, or archive the session. An active run and an agent's main session cannot be archived. Archiving the currently selected session switches Chat back to that agent's main session. - On desktop widths, chat controls stay on one compact row and collapse while scrolling down the transcript; scrolling up, returning to the top, or reaching the bottom restores the controls. - Consecutive duplicate text-only messages render as one bubble with a count badge. Messages that carry images, attachments, tool output, or canvas previews are left uncollapsed. - The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options. diff --git a/extensions/codex/src/app-server/client.ts b/extensions/codex/src/app-server/client.ts index ba37a5f0714f..e700436674fd 100644 --- a/extensions/codex/src/app-server/client.ts +++ b/extensions/codex/src/app-server/client.ts @@ -345,9 +345,9 @@ export class CodexAppServerClient { async closeAndWait(options?: { exitTimeoutMs?: number; forceKillDelayMs?: number; - }): Promise { + }): Promise { this.markClosed(new Error("codex app-server client is closed")); - await closeCodexAppServerTransportAndWait(this.child, options); + return await closeCodexAppServerTransportAndWait(this.child, options); } private writeMessage(message: RpcRequest | RpcResponse, onError?: (error: Error) => void): void { diff --git a/extensions/codex/src/app-server/compact.test.ts b/extensions/codex/src/app-server/compact.test.ts index 9d6bf6782e00..2aa85874749f 100644 --- a/extensions/codex/src/app-server/compact.test.ts +++ b/extensions/codex/src/app-server/compact.test.ts @@ -8,7 +8,7 @@ import { } from "openclaw/plugin-sdk/agent-harness-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { CodexAppServerClientFactory } from "./client-factory.js"; -import type { CodexAppServerClient } from "./client.js"; +import { CodexAppServerRpcError, type CodexAppServerClient } from "./client.js"; import { maybeCompactCodexAppServerSession as maybeCompactCodexAppServerSessionImpl } from "./compact.js"; import type { CodexServerNotification } from "./protocol.js"; import { @@ -129,7 +129,7 @@ describe("maybeCompactCodexAppServerSession", () => { await fs.rm(tempDir, { recursive: true, force: true }); }); - it("starts native app-server compaction without waiting for completion", async () => { + it("waits for native app-server compaction completion", async () => { const fake = createFakeCodexClient(); setCodexAppServerClientFactoryForTest(async () => fake.client); const sessionFile = await writeTestBinding(); @@ -139,16 +139,17 @@ describe("maybeCompactCodexAppServerSession", () => { ); expect(fake.request).toHaveBeenCalledWith("thread/compact/start", { threadId: "thread-1" }); - expect(fake.client["addNotificationHandler"]).not.toHaveBeenCalled(); + expect(fake.client["addNotificationHandler"]).toHaveBeenCalledTimes(1); expect(result.ok).toBe(true); - expect(result.compacted).toBe(false); + expect(result.compacted).toBe(true); expect(result.result?.tokensBefore).toBe(123); expect(result.result?.tokensAfter).toBeUndefined(); const details = compactDetails(result); expect(details.backend).toBe("codex-app-server"); expect(details.threadId).toBe("thread-1"); expect(details.signal).toBe("thread/compact/start"); - expect(details.pending).toBe(true); + expect(details.pending).toBe(false); + expect(details.completed).toBe(true); }); it("skips native app-server compaction for automatic budget triggers", async () => { @@ -217,14 +218,15 @@ describe("maybeCompactCodexAppServerSession", () => { { timeoutMs: 60_000 }, ); expect(result.ok).toBe(true); - expect(result.compacted).toBe(false); + expect(result.compacted).toBe(true); expect(result.reason).toBeUndefined(); expect(result.result?.tokensBefore).toBe(456); expect(compactDetails(result)).toMatchObject({ backend: "codex-app-server", threadId: "thread-1", signal: "thread/compact/start", - pending: true, + pending: false, + completed: true, request: "after_context_engine", trigger: "budget", }); @@ -397,13 +399,15 @@ describe("maybeCompactCodexAppServerSession", () => { let externalWriteStarted = false; let externalWriteFinished = false; const fake = createFakeCodexClient(); - fake.request.mockImplementation(() => - expectExternalMutationBlockedDuringNativeRequest({ + fake.request.mockImplementation(async () => { + const response = await expectExternalMutationBlockedDuringNativeRequest({ releaseExternalMutation: releaseExternalWrite, isExternalMutationStarted: () => externalWriteStarted, isExternalMutationFinished: () => externalWriteFinished, - }), - ); + }); + setImmediate(fake.completeCompaction); + return response; + }); setCodexAppServerClientFactoryForTest(async () => fake.client); const sessionFile = await writeTestBinding({ contextEngine: { @@ -459,7 +463,7 @@ describe("maybeCompactCodexAppServerSession", () => { { timeoutMs: 60_000 }, ); expect(result.ok).toBe(true); - expect(result.compacted).toBe(false); + expect(result.compacted).toBe(true); expect(await readCodexAppServerBinding(sessionFile)).toMatchObject({ threadId: "thread-2", contextEngine: { @@ -479,13 +483,15 @@ describe("maybeCompactCodexAppServerSession", () => { let externalClearStarted = false; let externalClearFinished = false; const fake = createFakeCodexClient(); - fake.request.mockImplementation(() => - expectExternalMutationBlockedDuringNativeRequest({ + fake.request.mockImplementation(async () => { + const response = await expectExternalMutationBlockedDuringNativeRequest({ releaseExternalMutation: releaseExternalClear, isExternalMutationStarted: () => externalClearStarted, isExternalMutationFinished: () => externalClearFinished, - }), - ); + }); + setImmediate(fake.completeCompaction); + return response; + }); setCodexAppServerClientFactoryForTest(async () => fake.client); const sessionFile = await writeTestBinding({ contextEngine: { @@ -529,7 +535,7 @@ describe("maybeCompactCodexAppServerSession", () => { { timeoutMs: 60_000 }, ); expect(result.ok).toBe(true); - expect(result.compacted).toBe(false); + expect(result.compacted).toBe(true); await expect(readCodexAppServerBinding(sessionFile)).resolves.toBeUndefined(); }); @@ -591,37 +597,509 @@ describe("maybeCompactCodexAppServerSession", () => { expect(fake.request).not.toHaveBeenCalled(); }); - it("does not consume native completion notifications after forwarding the request", async () => { - const fake = createFakeCodexClient(); + it("does not finish until the matching native compaction turn completes", async () => { + const fake = createFakeCodexClient({ autoCompleteCompaction: false }); setCodexAppServerClientFactoryForTest(async () => fake.client); const sessionFile = await writeTestBinding(); - const result = requireCompactResult( - await startCompaction(sessionFile, { currentTokenCount: 123 }), - ); - fake.emit({ - method: "thread/compacted", - params: { threadId: "thread-1", turnId: "turn-1" }, + let settled = false; + const pendingResult = startCompaction(sessionFile, { currentTokenCount: 123 }).finally(() => { + settled = true; }); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledWith("thread/compact/start", { threadId: "thread-1" }); + }); + await flushAsyncTasks(); + expect(settled).toBe(false); + fake.emit({ - method: "thread/tokenUsage/updated", + method: "item/started", params: { threadId: "thread-1", - tokenUsage: { - last_token_usage: { - total_tokens: 0, - }, - }, + turnId: "turn-1", + item: { id: "compact-item-1", type: "contextCompaction" }, }, }); + fake.emit({ + method: "item/completed", + params: { + threadId: "thread-1", + turnId: "turn-1", + item: { id: "compact-item-1", type: "contextCompaction" }, + }, + }); + await flushAsyncTasks(); + expect(settled).toBe(false); + fake.emit({ + method: "turn/completed", + params: { + threadId: "thread-1", + turn: { id: "turn-1", threadId: "thread-1", status: "completed" }, + }, + }); + const result = requireCompactResult(await pendingResult); expect(result.ok).toBe(true); - expect(result.compacted).toBe(false); + expect(result.compacted).toBe(true); expect(result.result?.tokensAfter).toBeUndefined(); expect(compactDetails(result).tokenUsageSource).toBeUndefined(); expect(compactDetails(result).signal).toBe("thread/compact/start"); }); + it("lets terminal interruption win after the compaction item completes", async () => { + const fake = createFakeCodexClient({ autoCompleteCompaction: false }); + setCodexAppServerClientFactoryForTest(async () => fake.client); + const sessionFile = await writeTestBinding(); + + const pendingResult = startCompaction(sessionFile); + await vi.waitFor(() => expect(fake.request).toHaveBeenCalledOnce()); + fake.emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-hook", threadId: "thread-1", status: "inProgress" }, + }, + }); + for (const method of ["item/started", "item/completed"] as const) { + fake.emit({ + method, + params: { + threadId: "thread-1", + turnId: "compact-turn-hook", + item: { id: "compact-item-hook", type: "contextCompaction" }, + }, + }); + } + fake.emit({ + method: "turn/completed", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-hook", threadId: "thread-1", status: "interrupted" }, + }, + }); + + await expect(pendingResult).resolves.toMatchObject({ + ok: false, + compacted: false, + reason: "codex app-server compaction turn ended with status interrupted", + }); + }); + + it("fails when the native compaction turn terminates before its item starts", async () => { + const fake = createFakeCodexClient({ autoCompleteCompaction: false }); + setCodexAppServerClientFactoryForTest(async () => fake.client); + const sessionFile = await writeTestBinding(); + + const pendingResult = startCompaction(sessionFile); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledOnce(); + }); + fake.emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-failed", threadId: "thread-1", status: "inProgress" }, + }, + }); + fake.emit({ + method: "turn/completed", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-failed", threadId: "thread-1", status: "failed" }, + }, + }); + + await expect(pendingResult).resolves.toMatchObject({ + ok: false, + compacted: false, + reason: "codex app-server compaction turn ended with status failed", + }); + }); + + it("accepts the terminal interrupt response when its notification is missing", async () => { + const fake = createFakeCodexClient({ autoCompleteCompaction: false }); + const sessionFile = await writeTestBinding(); + + const pendingResult = maybeCompactCodexAppServerSessionImpl( + { + sessionId: "session-1", + sessionKey: "agent:main:session-1", + sessionFile, + workspaceDir: tempDir, + trigger: "manual", + }, + { clientFactory: async () => fake.client, nativeCompletionTimeoutMs: 10 }, + ); + await vi.waitFor(() => expect(fake.request).toHaveBeenCalledOnce()); + fake.emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-stalled", threadId: "thread-1", status: "inProgress" }, + }, + }); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledWith( + "turn/interrupt", + { + threadId: "thread-1", + turnId: "compact-turn-stalled", + }, + { timeoutMs: 30_000 }, + ); + }); + expect(fake.close).not.toHaveBeenCalled(); + expect(fake.closeAndWait).not.toHaveBeenCalled(); + + await expect(pendingResult).resolves.toMatchObject({ + ok: false, + compacted: false, + reason: "codex app-server confirmed native compaction interruption", + }); + }); + + it("accepts an already-terminal interrupt after the completion notification is dropped", async () => { + const fake = createFakeCodexClient({ + autoCompleteCompaction: false, + interruptError: new CodexAppServerRpcError( + { code: -32_600, message: "no active turn to interrupt" }, + "turn/interrupt", + ), + }); + const sessionFile = await writeTestBinding(); + + const pendingResult = maybeCompactCodexAppServerSessionImpl( + { + sessionId: "session-1", + sessionKey: "agent:main:session-1", + sessionFile, + workspaceDir: tempDir, + trigger: "manual", + }, + { clientFactory: async () => fake.client, nativeCompletionTimeoutMs: 10 }, + ); + await vi.waitFor(() => expect(fake.request).toHaveBeenCalledOnce()); + fake.emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-finished", threadId: "thread-1", status: "inProgress" }, + }, + }); + for (const method of ["item/started", "item/completed"] as const) { + fake.emit({ + method, + params: { + threadId: "thread-1", + turnId: "compact-turn-finished", + item: { id: "compact-item-finished", type: "contextCompaction" }, + }, + }); + } + + await expect(pendingResult).resolves.toMatchObject({ ok: true, compacted: true }); + expect(fake.closeAndWait).not.toHaveBeenCalled(); + await expect(readCodexAppServerBinding(sessionFile)).resolves.toBeDefined(); + }); + + it("retires a stalled client when interruption cannot be confirmed", async () => { + const fake = createFakeCodexClient({ + autoCompleteCompaction: false, + rejectInterrupt: true, + }); + const sessionFile = await writeTestBinding(); + + const pendingResult = maybeCompactCodexAppServerSessionImpl( + { + sessionId: "session-1", + sessionKey: "agent:main:session-1", + sessionFile, + workspaceDir: tempDir, + trigger: "manual", + }, + { + clientFactory: async () => fake.client, + nativeCompletionTimeoutMs: 250, + nativeInterruptGraceMs: 10, + }, + ); + await vi.waitFor(() => expect(fake.request).toHaveBeenCalledOnce()); + fake.emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-stuck", threadId: "thread-1", status: "inProgress" }, + }, + }); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledWith( + "turn/interrupt", + { + threadId: "thread-1", + turnId: "compact-turn-stuck", + }, + { timeoutMs: 10 }, + ); + expect(fake.closeAndWait).toHaveBeenCalledWith({ + exitTimeoutMs: 5_000, + forceKillDelayMs: 250, + }); + expect(fake.close).toHaveBeenCalledTimes(1); + }); + + await expect(pendingResult).resolves.toMatchObject({ + ok: false, + compacted: false, + reason: "codex app-server compaction did not reach terminal state after interruption", + }); + }); + + it("uses the configured compaction timeout for native completion", async () => { + const fake = createFakeCodexClient({ + autoCompleteCompaction: false, + rejectInterrupt: true, + }); + const sessionFile = await writeTestBinding(); + + const pendingResult = maybeCompactCodexAppServerSessionImpl( + { + sessionId: "session-1", + sessionKey: "agent:main:session-1", + sessionFile, + workspaceDir: tempDir, + trigger: "manual", + config: { agents: { defaults: { compaction: { timeoutSeconds: 1 } } } }, + }, + { + clientFactory: async () => fake.client, + nativeInterruptGraceMs: 10, + }, + ); + await vi.waitFor(() => expect(fake.request).toHaveBeenCalledOnce()); + fake.emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-configured", threadId: "thread-1", status: "inProgress" }, + }, + }); + + await expect(pendingResult).resolves.toMatchObject({ + ok: false, + compacted: false, + reason: "codex app-server compaction did not reach terminal state after interruption", + }); + expect(fake.request).toHaveBeenCalledWith( + "turn/interrupt", + { + threadId: "thread-1", + turnId: "compact-turn-configured", + }, + { timeoutMs: 10 }, + ); + }); + + it("detaches a remote thread when its interrupted turn cannot be confirmed", async () => { + const fake = createFakeCodexClient({ + autoCompleteCompaction: false, + rejectInterrupt: true, + }); + const sessionFile = await writeTestBinding(); + + const pendingResult = maybeCompactCodexAppServerSessionImpl( + { + sessionId: "session-1", + sessionKey: "agent:main:session-1", + sessionFile, + workspaceDir: tempDir, + trigger: "manual", + }, + { + clientFactory: async () => fake.client, + pluginConfig: { + appServer: { transport: "websocket", url: "ws://127.0.0.1:45001" }, + }, + nativeCompletionTimeoutMs: 250, + nativeInterruptGraceMs: 10, + }, + ); + await vi.waitFor(() => expect(fake.request).toHaveBeenCalledOnce()); + fake.emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-remote", threadId: "thread-1", status: "inProgress" }, + }, + }); + + await expect(pendingResult).resolves.toMatchObject({ ok: false, compacted: false }); + await expect(readCodexAppServerBinding(sessionFile)).resolves.toBeUndefined(); + }); + + it("cancels a native compaction after the start request", async () => { + const fake = createFakeCodexClient({ autoCompleteCompaction: false }); + setCodexAppServerClientFactoryForTest(async () => fake.client); + const sessionFile = await writeTestBinding(); + const abortController = new AbortController(); + + let settled = false; + const pendingResult = maybeCompactCodexAppServerSession({ + sessionId: "session-1", + sessionKey: "agent:main:session-1", + sessionFile, + workspaceDir: tempDir, + trigger: "manual", + abortSignal: abortController.signal, + }).finally(() => { + settled = true; + }); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledOnce(); + }); + abortController.abort(); + await flushAsyncTasks(); + expect(settled).toBe(false); + + fake.emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-aborted", threadId: "thread-1", status: "inProgress" }, + }, + }); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledWith( + "turn/interrupt", + { + threadId: "thread-1", + turnId: "compact-turn-aborted", + }, + { timeoutMs: 30_000 }, + ); + }); + + await expect(pendingResult).resolves.toMatchObject({ + ok: false, + compacted: false, + reason: "codex app-server confirmed native compaction interruption", + }); + }); + + it("serializes native compaction requests for the same Codex thread", async () => { + const fake = createFakeCodexClient({ autoCompleteCompaction: false }); + setCodexAppServerClientFactoryForTest(async () => fake.client); + const firstSessionFile = await writeTestBinding(); + const secondSessionFile = path.join(tempDir, "second-session.jsonl"); + await writeCodexAppServerBinding(secondSessionFile, { + threadId: "thread-1", + cwd: tempDir, + }); + + const first = startCompaction(firstSessionFile); + const second = startCompaction(secondSessionFile); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledTimes(1); + }); + + fake.completeCompaction(); + await expect(first).resolves.toMatchObject({ ok: true, compacted: true }); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledTimes(2); + }); + + fake.completeCompaction(); + await expect(second).resolves.toMatchObject({ ok: true, compacted: true }); + }); + + it("cancels a queued same-thread compaction before acquiring a client", async () => { + const fake = createFakeCodexClient({ autoCompleteCompaction: false }); + const factory = vi.fn(async () => fake.client); + setCodexAppServerClientFactoryForTest(factory); + const firstSessionFile = await writeTestBinding(); + const secondSessionFile = path.join(tempDir, "queued-session.jsonl"); + await writeCodexAppServerBinding(secondSessionFile, { + threadId: "thread-1", + cwd: tempDir, + }); + const abortController = new AbortController(); + + const first = startCompaction(firstSessionFile); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledTimes(1); + }); + const second = maybeCompactCodexAppServerSession({ + sessionId: "session-2", + sessionKey: "agent:main:session-2", + sessionFile: secondSessionFile, + workspaceDir: tempDir, + trigger: "manual", + abortSignal: abortController.signal, + }); + await flushAsyncTasks(); + expect(factory).toHaveBeenCalledTimes(1); + + abortController.abort(); + await expect(second).resolves.toMatchObject({ + ok: false, + compacted: false, + reason: "codex app-server compaction aborted while waiting to start", + }); + expect(factory).toHaveBeenCalledTimes(1); + expect(fake.request).toHaveBeenCalledTimes(1); + + fake.completeCompaction(); + await expect(first).resolves.toMatchObject({ ok: true, compacted: true }); + }); + + it("keeps later compactions behind an active request after a queued waiter cancels", async () => { + const fake = createFakeCodexClient({ autoCompleteCompaction: false }); + const factory = vi.fn(async () => fake.client); + setCodexAppServerClientFactoryForTest(factory); + const firstSessionFile = await writeTestBinding(); + const secondSessionFile = path.join(tempDir, "canceled-queued-session.jsonl"); + const thirdSessionFile = path.join(tempDir, "later-session.jsonl"); + for (const sessionFile of [secondSessionFile, thirdSessionFile]) { + await writeCodexAppServerBinding(sessionFile, { + threadId: "thread-1", + cwd: tempDir, + }); + } + const abortController = new AbortController(); + + const first = startCompaction(firstSessionFile); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledTimes(1); + }); + const second = maybeCompactCodexAppServerSession({ + sessionId: "session-2", + sessionKey: "agent:main:session-2", + sessionFile: secondSessionFile, + workspaceDir: tempDir, + trigger: "manual", + abortSignal: abortController.signal, + }); + abortController.abort(); + await expect(second).resolves.toMatchObject({ + ok: false, + compacted: false, + reason: "codex app-server compaction aborted while waiting to start", + }); + + const third = startCompaction(thirdSessionFile); + await flushAsyncTasks(); + expect(factory).toHaveBeenCalledTimes(1); + expect(fake.request).toHaveBeenCalledTimes(1); + + fake.completeCompaction(); + await expect(first).resolves.toMatchObject({ ok: true, compacted: true }); + await vi.waitFor(() => { + expect(fake.request).toHaveBeenCalledTimes(2); + }); + + fake.completeCompaction(); + await expect(third).resolves.toMatchObject({ ok: true, compacted: true }); + }); + it("reuses the bound auth profile for native compaction", async () => { const fake = createFakeCodexClient(); let seenAuthProfileId: string | undefined; @@ -653,7 +1131,12 @@ describe("maybeCompactCodexAppServerSession", () => { it("preserves stale thread binding metadata for recovery and reports failed native compaction", async () => { const fake = createFakeCodexClient(); - fake.request.mockRejectedValueOnce(new Error("thread not found: thread-1")); + fake.request.mockRejectedValueOnce( + new CodexAppServerRpcError( + { code: -32_602, message: "thread not found: thread-1" }, + "thread/compact/start", + ), + ); setCodexAppServerClientFactoryForTest(async () => fake.client); const sessionFile = await writeTestBinding({ authProfileId: "openai:work", @@ -680,9 +1163,83 @@ describe("maybeCompactCodexAppServerSession", () => { expect(result.reason).toBe("thread not found: thread-1"); expect(result.failure?.reason).toBe("stale_thread_binding"); expect(result.result).toBeUndefined(); + expect(fake.closeAndWait).not.toHaveBeenCalled(); }); - it("does not impose an OpenClaw timeout after Codex accepts native compaction", async () => { + it("retires the client before releasing an unconfirmed compaction start", async () => { + const fake = createFakeCodexClient(); + fake.request.mockRejectedValueOnce(new Error("thread/compact/start timed out")); + setCodexAppServerClientFactoryForTest(async () => fake.client); + const sessionFile = await writeTestBinding(); + + const result = requireCompactResult(await startCompaction(sessionFile)); + + expect(result).toMatchObject({ + ok: false, + compacted: false, + reason: "thread/compact/start timed out", + }); + expect(fake.closeAndWait).toHaveBeenCalledWith({ + exitTimeoutMs: 5_000, + forceKillDelayMs: 250, + }); + expect(fake.close).toHaveBeenCalledTimes(1); + }); + + it("keeps the lifecycle fence when an unconfirmed stdio process does not stop", async () => { + const fake = createFakeCodexClient(); + fake.request.mockRejectedValueOnce(new Error("thread/compact/start timed out")); + fake.closeAndWait.mockResolvedValueOnce(false); + setCodexAppServerClientFactoryForTest(async () => fake.client); + const sessionFile = await writeTestBinding({ threadId: "thread-stuck-stdio" }); + + const outcome = await Promise.race([ + startCompaction(sessionFile).then(() => "settled" as const), + new Promise<"pending">((resolve) => { + setTimeout(() => resolve("pending"), 20); + }), + ]); + + expect(outcome).toBe("pending"); + expect(fake.closeAndWait).toHaveBeenCalledOnce(); + await expect(readCodexAppServerBinding(sessionFile)).resolves.toBeDefined(); + }); + + it("detaches a guarded remote start after releasing the binding lock", async () => { + const fake = createFakeCodexClient(); + fake.request.mockRejectedValueOnce(new Error("thread/compact/start timed out")); + fake.closeAndWait.mockResolvedValueOnce(false); + const sessionFile = await writeTestBinding(); + + const result = requireCompactResult( + await maybeCompactCodexAppServerSessionImpl( + { + sessionId: "session-1", + sessionKey: "agent:main:session-1", + sessionFile, + workspaceDir: tempDir, + trigger: "budget", + }, + { + allowNonManualNativeRequest: true, + clientFactory: async () => fake.client, + pluginConfig: { + appServer: { transport: "websocket", url: "ws://127.0.0.1:45001" }, + }, + }, + ), + ); + + expect(result).toMatchObject({ + ok: false, + compacted: false, + reason: "thread/compact/start timed out", + }); + expect(fake.closeAndWait).toHaveBeenCalledOnce(); + await expect(readCodexAppServerBinding(sessionFile)).resolves.toBeUndefined(); + }); + + it("retains the shared client lease through native compaction completion", async () => { const fake = createFakeCodexClient(); const factory = vi.fn(async () => fake.client); setCodexAppServerClientFactoryForTest(factory); @@ -693,12 +1250,13 @@ describe("maybeCompactCodexAppServerSession", () => { ); expect(result.ok).toBe(true); - expect(result.compacted).toBe(false); + expect(result.compacted).toBe(true); expect(compactDetails(result)).toMatchObject({ backend: "codex-app-server", threadId: "thread-1", signal: "thread/compact/start", - pending: true, + pending: false, + completed: true, }); expect(factory).toHaveBeenCalledTimes(1); expect(fake.close).not.toHaveBeenCalled(); @@ -1009,12 +1567,13 @@ describe("maybeCompactCodexAppServerSession", () => { expect(fake.request).toHaveBeenCalledWith("thread/compact/start", { threadId: "thread-1" }); expect(result.ok).toBe(true); - expect(result.compacted).toBe(false); + expect(result.compacted).toBe(true); expect(compactDetails(result)).toMatchObject({ backend: "codex-app-server", threadId: "thread-1", signal: "thread/compact/start", - pending: true, + pending: false, + completed: true, }); expect(compact).not.toHaveBeenCalled(); expect(maintain).not.toHaveBeenCalled(); @@ -1058,15 +1617,116 @@ describe("maybeCompactCodexAppServerSession", () => { }); }); -function createFakeCodexClient(): { +function createFakeCodexClient( + options: { + autoCompleteCompaction?: boolean; + interruptError?: Error; + rejectInterrupt?: boolean; + } = {}, +): { client: CodexAppServerClient; request: ReturnType>; close: ReturnType; + closeAndWait: ReturnType; emit: (notification: CodexServerNotification) => void; + completeCompaction: () => void; } { const handlers = new Set<(notification: CodexServerNotification) => void>(); - const request = vi.fn(async () => ({})); - const close = vi.fn(); + const closeHandlers = new Set<() => void>(); + const emit = (notification: CodexServerNotification): void => { + for (const handler of handlers) { + handler(notification); + } + }; + const completeCompaction = (): void => { + emit({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-1", threadId: "thread-1", status: "inProgress" }, + }, + }); + emit({ + method: "item/started", + params: { + threadId: "thread-1", + turnId: "compact-turn-1", + item: { id: "compact-item-1", type: "contextCompaction" }, + }, + }); + emit({ + method: "item/completed", + params: { + threadId: "thread-1", + turnId: "compact-turn-1", + item: { id: "compact-item-1", type: "contextCompaction" }, + }, + }); + emit({ + method: "turn/completed", + params: { + threadId: "thread-1", + turn: { id: "compact-turn-1", threadId: "thread-1", status: "completed" }, + }, + }); + }; + const request = vi.fn( + async (method: string, params?: unknown) => { + if (method === "turn/interrupt" && options.interruptError) { + throw options.interruptError; + } + if (method === "turn/interrupt" && options.rejectInterrupt) { + throw new Error("interrupt unavailable"); + } + if (method === "thread/compact/start" && options.autoCompleteCompaction !== false) { + const threadId = (params as { threadId?: unknown }).threadId; + if (typeof threadId !== "string") { + throw new Error("thread/compact/start requires threadId"); + } + // Codex may emit item notifications before acknowledging the start RPC. + emit({ + method: "turn/started", + params: { + threadId, + turn: { id: "compact-turn-1", threadId, status: "inProgress" }, + }, + }); + emit({ + method: "item/started", + params: { + threadId, + turnId: "compact-turn-1", + item: { id: "compact-item-1", type: "contextCompaction" }, + }, + }); + emit({ + method: "item/completed", + params: { + threadId, + turnId: "compact-turn-1", + item: { id: "compact-item-1", type: "contextCompaction" }, + }, + }); + emit({ + method: "turn/completed", + params: { + threadId, + turn: { id: "compact-turn-1", threadId, status: "completed" }, + }, + }); + } + return {}; + }, + ); + const close = vi.fn(() => { + for (const handler of closeHandlers) { + handler(); + } + }); + const closeAndWait = vi.fn(async () => { + close(); + return true; + }); const addNotificationHandler = vi.fn( (handler: (notification: CodexServerNotification) => void) => { handlers.add(handler); @@ -1077,14 +1737,17 @@ function createFakeCodexClient(): { client: { request, close, + closeAndWait, addNotificationHandler, + addCloseHandler: vi.fn((handler: () => void) => { + closeHandlers.add(handler); + return () => closeHandlers.delete(handler); + }), } as unknown as CodexAppServerClient, request, close, - emit(notification: CodexServerNotification): void { - for (const handler of handlers) { - handler(notification); - } - }, + closeAndWait, + emit, + completeCompaction, }; } diff --git a/extensions/codex/src/app-server/compact.ts b/extensions/codex/src/app-server/compact.ts index c9a241731463..cf126861b2bd 100644 --- a/extensions/codex/src/app-server/compact.ts +++ b/extensions/codex/src/app-server/compact.ts @@ -3,18 +3,26 @@ */ import { embeddedAgentLog, + resolveCompactionTimeoutMs, type CompactEmbeddedAgentSessionParams, type EmbeddedAgentCompactResult, } from "openclaw/plugin-sdk/agent-harness-runtime"; +import { readCodexNotificationItem } from "./attempt-notifications.js"; import { defaultLeasedCodexAppServerClientFactory, type CodexAppServerClientFactory, } from "./client-factory.js"; +import { CodexAppServerRpcError, type CodexAppServerClient } from "./client.js"; import { resolveCodexAppServerRuntimeOptions } from "./config.js"; -import type { JsonObject } from "./protocol.js"; +import { + readCodexNotificationThreadId, + readCodexNotificationTurnId, +} from "./notification-correlation.js"; +import { isJsonObject, type JsonObject } from "./protocol.js"; import { resolveCodexNativeExecutionBlock } from "./sandbox-guard.js"; import { CODEX_APP_SERVER_BINDING_GUARDED_REQUEST_TIMEOUT_MS, + clearCodexAppServerBindingForThread, readCodexAppServerBinding, withCodexAppServerBindingLock, writeCodexAppServerBinding, @@ -23,12 +31,329 @@ import { import { releaseLeasedSharedCodexAppServerClient } from "./shared-client.js"; const warnedIgnoredCompactionOverrides = new Set(); +const codexNativeCompactionQueues = new Map>(); +const CODEX_NATIVE_COMPACTION_INTERRUPT_GRACE_MS = 30_000; +const CODEX_NO_ACTIVE_TURN_ERROR_CODE = -32_600; +const CODEX_NO_ACTIVE_TURN_ERROR_MESSAGE = "no active turn to interrupt"; type CodexAppServerCompactOptions = { pluginConfig?: unknown; clientFactory?: CodexAppServerClientFactory; allowNonManualNativeRequest?: boolean; + nativeCompletionTimeoutMs?: number; + nativeInterruptGraceMs?: number; }; +type CodexNativeCompactionCompletion = { completed: true } | { completed: false; reason: string }; + +function isAlreadyTerminalInterruptError(error: unknown): error is CodexAppServerRpcError { + return ( + error instanceof CodexAppServerRpcError && + error.code === CODEX_NO_ACTIVE_TURN_ERROR_CODE && + error.message === CODEX_NO_ACTIVE_TURN_ERROR_MESSAGE + ); +} + +function watchCodexNativeCompactionCompletion(params: { + client: CodexAppServerClient; + threadId: string; + signal?: AbortSignal; + timeoutMs: number; + interruptGraceMs: number; + retireUnconfirmed: () => Promise; +}): { + completion: Promise; + beginRequest: () => void; + confirmRequestRejected: () => void; + retireUnconfirmedRequest: (reason: string) => Promise; + cancel: () => void; +} { + let settled = false; + let requestStarted = false; + let abortRequested = false; + let interruptRequested = false; + let retirementStarted = false; + let compactionTurnId: string | undefined; + let compactionItemId: string | undefined; + let compactionItemCompleted = false; + let resolveCompletion = (_result: CodexNativeCompactionCompletion) => {}; + const completion = new Promise((resolve) => { + resolveCompletion = resolve; + }); + let removeNotificationHandler = () => {}; + let removeCloseHandler = () => {}; + let removeAbortHandler = () => {}; + let completionTimeout: ReturnType | undefined; + let interruptGraceTimeout: ReturnType | undefined; + const finish = (result: CodexNativeCompactionCompletion) => { + if (settled) { + return; + } + settled = true; + removeNotificationHandler(); + removeCloseHandler(); + removeAbortHandler(); + clearTimeout(completionTimeout); + clearTimeout(interruptGraceTimeout); + resolveCompletion(result); + }; + const retireUnconfirmed = (reason: string) => { + if (settled || retirementStarted) { + return; + } + retirementStarted = true; + void params + .retireUnconfirmed() + .then(() => finish({ completed: false, reason })) + .catch((error: unknown) => { + embeddedAgentLog.error("failed to retire unconfirmed codex app-server compaction", { + threadId: params.threadId, + turnId: compactionTurnId, + reason: formatCompactionError(error), + }); + // Keep the lifecycle fence held when neither terminal state nor thread + // retirement can be proven. Releasing would permit same-thread overlap. + }); + }; + const requestInterrupt = () => { + if (settled || !requestStarted || !abortRequested || !compactionTurnId || interruptRequested) { + return; + } + interruptRequested = true; + void params.client + .request( + "turn/interrupt", + { + threadId: params.threadId, + turnId: compactionTurnId, + }, + { timeoutMs: Math.max(1, params.interruptGraceMs) }, + ) + .then(() => { + // Codex answers turn/interrupt only after terminal abort handling, so + // the RPC response is sufficient when its notification was dropped. + finish({ + completed: false, + reason: "codex app-server confirmed native compaction interruption", + }); + }) + .catch((error: unknown) => { + // Codex holds normal interrupt RPCs until TurnAborted. This exact + // InvalidRequest instead proves the target turn was already terminal. + if (isAlreadyTerminalInterruptError(error)) { + finish( + compactionItemCompleted + ? { completed: true } + : { + completed: false, + reason: + "codex app-server compaction reached terminal state without a completed compaction item", + }, + ); + return; + } + embeddedAgentLog.warn("codex app-server compaction interrupt request failed", { + threadId: params.threadId, + turnId: compactionTurnId, + reason: formatCompactionError(error), + }); + }); + }; + const beginInterruptGrace = () => { + if (settled || !requestStarted || interruptGraceTimeout) { + return; + } + requestInterrupt(); + interruptGraceTimeout = setTimeout( + () => { + embeddedAgentLog.warn( + "codex app-server compaction did not reach terminal state after interruption", + { + threadId: params.threadId, + turnId: compactionTurnId, + interruptGraceMs: params.interruptGraceMs, + }, + ); + retireUnconfirmed( + "codex app-server compaction did not reach terminal state after interruption", + ); + }, + Math.max(1, params.interruptGraceMs), + ); + interruptGraceTimeout.unref?.(); + }; + const beginCompletionTimeout = () => { + completionTimeout = setTimeout( + () => { + abortRequested = true; + beginInterruptGrace(); + // Keep the shared client lease and per-thread fence through terminal state or + // forced process retirement; releasing earlier could overlap the same transcript. + embeddedAgentLog.warn("codex app-server compaction exceeded its completion budget", { + threadId: params.threadId, + timeoutMs: params.timeoutMs, + interruptRequested, + }); + }, + Math.max(1, params.timeoutMs), + ); + completionTimeout.unref?.(); + }; + removeNotificationHandler = params.client.addNotificationHandler((notification) => { + if (!requestStarted) { + return; + } + if (!isJsonObject(notification.params)) { + return; + } + if (readCodexNotificationThreadId(notification.params) !== params.threadId) { + return; + } + const notificationTurnId = readCodexNotificationTurnId(notification.params); + if (notification.method === "turn/started") { + compactionTurnId = notificationTurnId; + requestInterrupt(); + return; + } + if (compactionTurnId && notificationTurnId !== compactionTurnId) { + return; + } + const item = readCodexNotificationItem(notification.params); + if (item?.type === "contextCompaction") { + if (notification.method === "item/started") { + compactionTurnId = compactionTurnId ?? notificationTurnId; + compactionItemId = item.id; + requestInterrupt(); + return; + } + if (notification.method === "item/completed" && compactionItemId === item.id) { + compactionItemCompleted = true; + return; + } + } + if ( + notification.method !== "turn/completed" || + !compactionTurnId || + notificationTurnId !== compactionTurnId + ) { + return; + } + const turn = isJsonObject(notification.params.turn) ? notification.params.turn : undefined; + const status = typeof turn?.status === "string" ? turn.status : undefined; + if (status !== "completed") { + finish({ + completed: false, + reason: `codex app-server compaction turn ended with status ${status ?? "unknown"}`, + }); + return; + } + if (!compactionItemId) { + finish({ + completed: false, + reason: "codex app-server compaction turn completed without a compaction item", + }); + return; + } + if (!compactionItemCompleted) { + finish({ + completed: false, + reason: "codex app-server compaction turn completed before its compaction item", + }); + return; + } + finish({ completed: true }); + }); + removeCloseHandler = params.client.addCloseHandler(() => { + retireUnconfirmed("codex app-server closed before native compaction completed"); + }); + if (params.signal) { + const onAbort = () => { + abortRequested = true; + beginInterruptGrace(); + }; + params.signal.addEventListener("abort", onAbort, { once: true }); + removeAbortHandler = () => params.signal?.removeEventListener("abort", onAbort); + if (params.signal.aborted) { + onAbort(); + } + } + return { + completion, + beginRequest: () => { + requestStarted = true; + beginCompletionTimeout(); + if (abortRequested) { + beginInterruptGrace(); + } + }, + confirmRequestRejected: () => + finish({ completed: false, reason: "codex app-server rejected the compaction request" }), + retireUnconfirmedRequest: async (reason) => { + retireUnconfirmed(reason); + return await completion; + }, + cancel: () => { + if (!requestStarted) { + finish({ completed: false, reason: "compaction request did not start" }); + } + }, + }; +} + +async function runExclusiveCodexNativeCompaction( + threadId: string, + signal: AbortSignal | undefined, + run: () => Promise, +): Promise { + const previous = codexNativeCompactionQueues.get(threadId) ?? Promise.resolve(); + let releaseCurrent!: () => void; + const current = new Promise((resolve) => { + releaseCurrent = resolve; + }); + const queued = previous.then( + () => current, + () => current, + ); + codexNativeCompactionQueues.set(threadId, queued); + try { + await waitForCodexNativeCompactionQueue(previous, signal); + signal?.throwIfAborted(); + return await run(); + } finally { + releaseCurrent(); + // A canceled waiter must remain in the chain until its predecessor settles; + // otherwise a later request can skip the still-active compaction. + void queued.then(() => { + if (codexNativeCompactionQueues.get(threadId) === queued) { + codexNativeCompactionQueues.delete(threadId); + } + }); + } +} + +async function waitForCodexNativeCompactionQueue( + previous: Promise, + signal: AbortSignal | undefined, +): Promise { + if (!signal) { + await previous.catch(() => undefined); + return; + } + signal.throwIfAborted(); + let removeAbortListener = () => {}; + const aborted = new Promise((_, reject) => { + const onAbort = () => { + reject(signal.reason instanceof Error ? signal.reason : new Error("compaction aborted")); + }; + removeAbortListener = () => signal.removeEventListener("abort", onAbort); + signal.addEventListener("abort", onAbort, { once: true }); + }); + try { + await Promise.race([previous.catch(() => undefined), aborted]); + } finally { + removeAbortListener(); + } +} + /** * Starts native Codex compaction for a manually requested bound session, or * reports why Codex-owned automatic compaction should handle the trigger. @@ -40,7 +365,7 @@ export async function maybeCompactCodexAppServerSession( warnIfIgnoringOpenClawCompactionOverrides(params); // Codex owns automatic context-pressure compaction for Codex runtime sessions. // This entry point starts native Codex compaction for the bound thread and - // returns immediately; Codex applies the compaction inside its app-server. + // retains the lease until Codex reports the context-compaction item complete. return compactCodexNativeThread(params, options); } @@ -202,128 +527,234 @@ async function compactCodexNativeThread( } const shouldReleaseDefaultLease = !options.clientFactory; const clientFactory = options.clientFactory ?? defaultLeasedCodexAppServerClientFactory; - const client = await clientFactory( - appServer.start, - requestedAuthProfileId ?? binding.authProfileId, - params.agentDir, - params.config, - ); try { - if (options.allowNonManualNativeRequest) { - const guardedResult = await withCodexAppServerBindingLock(params.sessionFile, async () => { - const currentBinding = await readCodexAppServerBinding(params.sessionFile, { - config: params.config, - }); - if (params.abortSignal?.aborted) { - return { - started: false as const, - result: skippedCodexNativeCompactionResult(params, { - reason: "codex app-server compaction aborted before native compaction", - code: "aborted_before_native_compaction", - expectedThreadId: binding.threadId, - currentThreadId: currentBinding?.threadId, - }), - }; - } - if (!currentBinding || !isSameNativeCompactionBinding(currentBinding, binding)) { - embeddedAgentLog.warn( - "skipping codex app-server compaction because the thread binding changed", - { - sessionId: params.sessionId, - sessionKey: params.sessionKey, - expectedThreadId: binding.threadId, - currentThreadId: currentBinding?.threadId, - }, - ); - return { - started: false as const, - result: skippedCodexNativeCompactionResult(params, { - reason: "codex app-server binding changed before native compaction", - code: "binding_changed_before_native_compaction", - expectedThreadId: binding.threadId, - currentThreadId: currentBinding?.threadId, - }), - }; - } - binding = currentBinding; - await clearContextEngineProjectionBeforeNativeCompaction({ - sessionId: params.sessionId, - sessionFile: params.sessionFile, - binding, - config: params.config, - }); - await client.request( - "thread/compact/start", - { - threadId: binding.threadId, - }, - { - timeoutMs: Math.min( - appServer.requestTimeoutMs, - CODEX_APP_SERVER_BINDING_GUARDED_REQUEST_TIMEOUT_MS, - ), - }, + return await runExclusiveCodexNativeCompaction( + binding.threadId, + params.abortSignal, + async () => { + const client = await clientFactory( + appServer.start, + requestedAuthProfileId ?? binding.authProfileId, + params.agentDir, + params.config, ); - return { started: true as const }; - }); - if (!guardedResult.started) { - return guardedResult.result; - } - } else { - await client.request("thread/compact/start", { - threadId: binding.threadId, - }); - } - embeddedAgentLog.info("started codex app-server compaction", { - sessionId: params.sessionId, - threadId: binding.threadId, - }); - } catch (error) { - if (isCodexThreadNotFoundError(error)) { - return failedCodexThreadBindingCompactionResult(params, { - threadId: binding.threadId, - reason: formatCompactionError(error), - recovery: "stale_thread_binding", - }); - } - embeddedAgentLog.warn("codex app-server compaction failed", { - sessionId: params.sessionId, - sessionKey: params.sessionKey, - threadId: binding.threadId, - reason: formatCompactionError(error), - }); - return { - ok: false, - compacted: false, - reason: formatCompactionError(error), - }; - } finally { - if (shouldReleaseDefaultLease) { - releaseLeasedSharedCodexAppServerClient(client); - } - } - const resultDetails: JsonObject = { - backend: "codex-app-server", - threadId: binding.threadId, - signal: "thread/compact/start", - pending: true, - ...(options.allowNonManualNativeRequest - ? { - request: "after_context_engine", - trigger: params.trigger ?? "unknown", + const completionWatch = watchCodexNativeCompactionCompletion({ + client, + threadId: binding.threadId, + signal: params.abortSignal, + timeoutMs: options.nativeCompletionTimeoutMs ?? resolveCompactionTimeoutMs(params.config), + interruptGraceMs: + options.nativeInterruptGraceMs ?? CODEX_NATIVE_COMPACTION_INTERRUPT_GRACE_MS, + retireUnconfirmed: async () => { + const transportStopped = await client.closeAndWait({ + exitTimeoutMs: 5_000, + forceKillDelayMs: 250, + }); + if (appServer.start.transport === "stdio") { + if (transportStopped) { + return; + } + // A local thread remains runnable with its stdio process. Keep + // the lifecycle fence held unless process exit is observed. + throw new Error("failed to stop unconfirmed codex app-server process"); + } + // Closing a WebSocket proves only that the connection ended, not + // that its remote turn stopped. Detach this exact thread before + // allowing future work to acquire the session lifecycle fence. + const bindingCleared = await clearCodexAppServerBindingForThread( + params.sessionFile, + binding.threadId, + { config: params.config }, + ); + if (bindingCleared) { + return; + } + const currentBinding = await readCodexAppServerBinding(params.sessionFile, { + config: params.config, + }); + if (currentBinding?.threadId !== binding.threadId) { + return; + } + throw new Error("failed to detach unconfirmed codex app-server thread binding"); + }, + }); + const beginNativeCompactionRequest = async (timeoutMs?: number) => { + completionWatch.beginRequest(); + const requestParams = { threadId: binding.threadId }; + if (timeoutMs === undefined) { + await client.request("thread/compact/start", requestParams); + } else { + await client.request("thread/compact/start", requestParams, { timeoutMs }); + } + }; + const settleNativeCompactionRequestError = async (error: unknown) => { + if (error instanceof CodexAppServerRpcError) { + completionWatch.confirmRequestRejected(); + } else { + // Transport errors after the write leave the server-side start + // ambiguous. Retire or detach the thread before releasing its fence. + await completionWatch.retireUnconfirmedRequest( + `codex app-server compaction start was unconfirmed: ${formatCompactionError(error)}`, + ); + } + }; + try { + if (options.allowNonManualNativeRequest) { + const guardedResult = await withCodexAppServerBindingLock( + params.sessionFile, + async () => { + const currentBinding = await readCodexAppServerBinding(params.sessionFile, { + config: params.config, + }); + if (params.abortSignal?.aborted) { + return { + started: false as const, + result: skippedCodexNativeCompactionResult(params, { + reason: "codex app-server compaction aborted before native compaction", + code: "aborted_before_native_compaction", + expectedThreadId: binding.threadId, + currentThreadId: currentBinding?.threadId, + }), + }; + } + if (!currentBinding || !isSameNativeCompactionBinding(currentBinding, binding)) { + embeddedAgentLog.warn( + "skipping codex app-server compaction because the thread binding changed", + { + sessionId: params.sessionId, + sessionKey: params.sessionKey, + expectedThreadId: binding.threadId, + currentThreadId: currentBinding?.threadId, + }, + ); + return { + started: false as const, + result: skippedCodexNativeCompactionResult(params, { + reason: "codex app-server binding changed before native compaction", + code: "binding_changed_before_native_compaction", + expectedThreadId: binding.threadId, + currentThreadId: currentBinding?.threadId, + }), + }; + } + binding = currentBinding; + await clearContextEngineProjectionBeforeNativeCompaction({ + sessionId: params.sessionId, + sessionFile: params.sessionFile, + binding, + config: params.config, + }); + try { + await beginNativeCompactionRequest( + Math.min( + appServer.requestTimeoutMs, + CODEX_APP_SERVER_BINDING_GUARDED_REQUEST_TIMEOUT_MS, + ), + ); + return { started: true as const, accepted: true as const }; + } catch (error) { + // Retire outside the binding lock: remote detach acquires this + // same lock and would otherwise deadlock the failure path. + return { started: true as const, accepted: false as const, error }; + } + }, + ); + if (!guardedResult.started) { + return guardedResult.result; + } + if (!guardedResult.accepted) { + await settleNativeCompactionRequestError(guardedResult.error); + throw guardedResult.error; + } + } else { + params.abortSignal?.throwIfAborted(); + try { + await beginNativeCompactionRequest(); + } catch (error) { + await settleNativeCompactionRequestError(error); + throw error; + } + } + embeddedAgentLog.info("started codex app-server compaction", { + sessionId: params.sessionId, + threadId: binding.threadId, + }); + const completion = await completionWatch.completion; + if (!completion.completed) { + throw new Error(completion.reason); + } + embeddedAgentLog.info("completed codex app-server compaction", { + sessionId: params.sessionId, + threadId: binding.threadId, + }); + } catch (error) { + if (isCodexThreadNotFoundError(error)) { + return failedCodexThreadBindingCompactionResult(params, { + threadId: binding.threadId, + reason: formatCompactionError(error), + recovery: "stale_thread_binding", + }); + } + embeddedAgentLog.warn("codex app-server compaction failed", { + sessionId: params.sessionId, + sessionKey: params.sessionKey, + threadId: binding.threadId, + reason: formatCompactionError(error), + }); + return { + ok: false, + compacted: false, + reason: formatCompactionError(error), + }; + } finally { + completionWatch.cancel(); + if (shouldReleaseDefaultLease) { + releaseLeasedSharedCodexAppServerClient(client); + } } - : {}), - }; - return { - ok: true, - compacted: false, - result: { - summary: "", - firstKeptEntryId: "", - tokensBefore: params.currentTokenCount ?? 0, - details: resultDetails, - }, - }; + const resultDetails: JsonObject = { + backend: "codex-app-server", + threadId: binding.threadId, + signal: "thread/compact/start", + pending: false, + completed: true, + ...(options.allowNonManualNativeRequest + ? { + request: "after_context_engine", + trigger: params.trigger ?? "unknown", + } + : {}), + }; + return { + ok: true, + compacted: true, + result: { + summary: "", + firstKeptEntryId: "", + tokensBefore: params.currentTokenCount ?? 0, + details: resultDetails, + }, + }; + }, + ); + } catch (error) { + if (params.abortSignal?.aborted) { + if (options.allowNonManualNativeRequest) { + return skippedCodexNativeCompactionResult(params, { + reason: "codex app-server compaction aborted before native compaction", + code: "aborted_before_native_compaction", + expectedThreadId: initialBinding.threadId, + currentThreadId: binding.threadId, + }); + } + return { + ok: false, + compacted: false, + reason: "codex app-server compaction aborted while waiting to start", + }; + } + throw error; + } } function skippedCodexNativeCompactionResult( diff --git a/extensions/voice-call/src/response-generator.test.ts b/extensions/voice-call/src/response-generator.test.ts index 2efdaea7835d..3fca9d77d20f 100644 --- a/extensions/voice-call/src/response-generator.test.ts +++ b/extensions/voice-call/src/response-generator.test.ts @@ -17,6 +17,7 @@ type TestSessionEntry = { }; type EmbeddedAgentArgs = { + abortSignal?: AbortSignal; extraSystemPrompt: string; provider?: string; model?: string; @@ -75,6 +76,12 @@ function createAgentRuntime(payloads: Array>) { payloads, meta: { durationMs: 12, aborted: false }, })); + const runWithWorkAdmission = vi.fn( + async ( + _params: { storePath: string; sessionKey: string }, + run: (signal: AbortSignal) => Promise, + ) => await run(new AbortController().signal), + ); const resolveAgentDir = vi.fn((_cfg: CoreConfig, agentId: string) => { return `/tmp/openclaw/agents/${agentId}`; }); @@ -113,6 +120,7 @@ function createAgentRuntime(payloads: Array>) { getSessionEntry, patchSessionEntry, upsertSessionEntry, + runWithWorkAdmission, resolveSessionFilePath, }, } as unknown as CoreAgentDeps; @@ -120,6 +128,7 @@ function createAgentRuntime(payloads: Array>) { return { runtime, runEmbeddedAgent, + runWithWorkAdmission, saveSessionStore, updateSessionStore, patchSessionEntry, @@ -181,7 +190,7 @@ async function runGenerateVoiceResponse( describe("generateVoiceResponse", () => { it("suppresses reasoning payloads and reads structured spoken output", async () => { - const { runtime, runEmbeddedAgent } = createAgentRuntime([ + const { runtime, runEmbeddedAgent, runWithWorkAdmission } = createAgentRuntime([ { text: "Reasoning: hidden", isReasoning: true }, { text: '{"spoken":"Hello from JSON."}' }, ]); @@ -193,6 +202,29 @@ describe("generateVoiceResponse", () => { expect(args.extraSystemPrompt).toContain('{"spoken":"..."}'); expect(args.provider).toBe("together"); expect(args.model).toBe("Qwen/Qwen2.5-7B-Instruct-Turbo"); + expect(args.abortSignal).toBeInstanceOf(AbortSignal); + expect(runWithWorkAdmission).toHaveBeenCalledWith( + { + storePath: "/tmp/openclaw/main/sessions.json", + sessionKey: "agent:main:voice:15550001111", + }, + expect.any(Function), + ); + }); + + it("returns the lifecycle rejection without starting the embedded agent", async () => { + const { runtime, runEmbeddedAgent, runWithWorkAdmission } = createAgentRuntime([]); + runWithWorkAdmission.mockRejectedValueOnce( + new Error('Session "agent:main:voice:15550001111" is archived.'), + ); + + const { result } = await runGenerateVoiceResponse([], { runtime }); + + expect(result).toEqual({ + text: null, + error: 'Error: Session "agent:main:voice:15550001111" is archived.', + }); + expect(runEmbeddedAgent).not.toHaveBeenCalled(); }); it("extracts spoken text from fenced JSON", async () => { diff --git a/extensions/voice-call/src/response-generator.ts b/extensions/voice-call/src/response-generator.ts index 46ad358dce74..3661aa50ef67 100644 --- a/extensions/voice-call/src/response-generator.ts +++ b/extensions/voice-call/src/response-generator.ts @@ -241,117 +241,126 @@ export async function generateVoiceResponse( // Resolve paths const storePath = agentRuntime.session.resolveStorePath(cfg.session?.store, { agentId }); - const agentDir = agentRuntime.resolveAgentDir(cfg, agentId); - const workspaceDir = agentRuntime.resolveAgentWorkspaceDir(cfg, agentId); + try { + return await agentRuntime.session.runWithWorkAdmission( + { storePath, sessionKey: resolvedSessionKey }, + async (abortSignal) => { + const agentDir = agentRuntime.resolveAgentDir(cfg, agentId); + const workspaceDir = agentRuntime.resolveAgentWorkspaceDir(cfg, agentId); - // Ensure workspace exists - await agentRuntime.ensureAgentWorkspace({ dir: workspaceDir }); + // Ensure workspace exists + await agentRuntime.ensureAgentWorkspace({ dir: workspaceDir }); - // Load or create session entry - const now = Date.now(); - const existingSessionEntry = agentRuntime.session.getSessionEntry({ - storePath, - sessionKey: resolvedSessionKey, - }); + // Load or create session entry + const now = Date.now(); + const existingSessionEntry = agentRuntime.session.getSessionEntry({ + storePath, + sessionKey: resolvedSessionKey, + }); - // Resolve model from config - const { provider, model } = resolveVoiceResponseModel({ voiceConfig, agentRuntime }); + // Resolve model from config + const { provider, model } = resolveVoiceResponseModel({ voiceConfig, agentRuntime }); - let sessionEntry = existingSessionEntry; - if (!sessionEntry?.sessionId || voiceConfig.responseModel) { - sessionEntry = - (await agentRuntime.session.patchSessionEntry({ - storePath, - sessionKey: resolvedSessionKey, - replaceEntry: true, - fallbackEntry: sessionEntry ?? { - sessionId: crypto.randomUUID(), - updatedAt: now, - }, - update: (entry) => { - const next = entry.sessionId - ? { ...entry } - : { - ...entry, + let sessionEntry = existingSessionEntry; + if (!sessionEntry?.sessionId || voiceConfig.responseModel) { + sessionEntry = + (await agentRuntime.session.patchSessionEntry({ + storePath, + sessionKey: resolvedSessionKey, + replaceEntry: true, + fallbackEntry: sessionEntry ?? { sessionId: crypto.randomUUID(), updatedAt: now, - }; - if (voiceConfig.responseModel) { - applyModelOverrideToSessionEntry({ - entry: next, - selection: { provider, model }, - selectionSource: "auto", - }); - } - return next; - }, - })) ?? undefined; - } - if (!sessionEntry?.sessionId) { - return { text: null, error: "Voice response session could not be initialized" }; - } - const sessionId = sessionEntry.sessionId; + }, + update: (entry) => { + const next = entry.sessionId + ? { ...entry } + : { + ...entry, + sessionId: crypto.randomUUID(), + updatedAt: now, + }; + if (voiceConfig.responseModel) { + applyModelOverrideToSessionEntry({ + entry: next, + selection: { provider, model }, + selectionSource: "auto", + }); + } + return next; + }, + })) ?? undefined; + } + if (!sessionEntry?.sessionId) { + return { text: null, error: "Voice response session could not be initialized" }; + } + const sessionId = sessionEntry.sessionId; - // Resolve thinking level - const thinkLevel = agentRuntime.resolveThinkingDefault({ cfg, provider, model }); + // Resolve thinking level + const thinkLevel = agentRuntime.resolveThinkingDefault({ cfg, provider, model }); - // Resolve agent identity for personalized prompt - const identity = agentRuntime.resolveAgentIdentity(cfg, agentId); - const agentName = identity?.name?.trim() || "assistant"; + // Resolve agent identity for personalized prompt + const identity = agentRuntime.resolveAgentIdentity(cfg, agentId); + const agentName = identity?.name?.trim() || "assistant"; - // Build system prompt with conversation history - const basePrompt = - voiceConfig.responseSystemPrompt ?? - `You are ${agentName}, a helpful voice assistant on a phone call. Keep responses brief and conversational (1-2 sentences max). Be natural and friendly. The caller's phone number is ${from}. You have access to tools - use them when helpful.`; + // Build system prompt with conversation history + const basePrompt = + voiceConfig.responseSystemPrompt ?? + `You are ${agentName}, a helpful voice assistant on a phone call. Keep responses brief and conversational (1-2 sentences max). Be natural and friendly. The caller's phone number is ${from}. You have access to tools - use them when helpful.`; - let extraSystemPrompt = basePrompt; - if (transcript.length > 0) { - const history = transcript - .map((entry) => `${entry.speaker === "bot" ? "You" : "Caller"}: ${entry.text}`) - .join("\n"); - extraSystemPrompt = `${basePrompt}\n\nConversation so far:\n${history}`; - } - extraSystemPrompt = `${extraSystemPrompt}\n\n${VOICE_SPOKEN_OUTPUT_CONTRACT}`; + let extraSystemPrompt = basePrompt; + if (transcript.length > 0) { + const history = transcript + .map((entry) => `${entry.speaker === "bot" ? "You" : "Caller"}: ${entry.text}`) + .join("\n"); + extraSystemPrompt = `${basePrompt}\n\nConversation so far:\n${history}`; + } + extraSystemPrompt = `${extraSystemPrompt}\n\n${VOICE_SPOKEN_OUTPUT_CONTRACT}`; - // Resolve timeout - const timeoutMs = voiceConfig.responseTimeoutMs ?? agentRuntime.resolveAgentTimeoutMs({ cfg }); - const runId = `voice:${callId}:${Date.now()}`; + // Resolve timeout + const timeoutMs = + voiceConfig.responseTimeoutMs ?? agentRuntime.resolveAgentTimeoutMs({ cfg }); + const runId = `voice:${callId}:${Date.now()}`; - try { - const result = await agentRuntime.runEmbeddedAgent({ - sessionId, - sessionKey: resolvedSessionKey, - sessionTarget: { - agentId, - sessionId, - sessionKey: resolvedSessionKey, - storePath, + const result = await agentRuntime.runEmbeddedAgent({ + sessionId, + sessionKey: resolvedSessionKey, + sessionTarget: { + agentId, + sessionId, + sessionKey: resolvedSessionKey, + storePath, + }, + sandboxSessionKey: resolveVoiceSandboxSessionKey(agentId, resolvedSessionKey), + agentId, + messageProvider: "voice", + workspaceDir, + config: cfg, + prompt: userMessage, + provider, + model, + thinkLevel, + verboseLevel: "off", + timeoutMs, + runId, + lane: "voice", + extraSystemPrompt, + agentDir, + toolsAllow, + abortSignal, + }); + + const text = extractSpokenTextFromPayloads( + (result.payloads ?? []) as VoiceResponsePayload[], + ); + + if (!text && result.meta?.aborted) { + return { text: null, error: "Response generation was aborted" }; + } + + return { text }; }, - sandboxSessionKey: resolveVoiceSandboxSessionKey(agentId, resolvedSessionKey), - agentId, - messageProvider: "voice", - workspaceDir, - config: cfg, - prompt: userMessage, - provider, - model, - thinkLevel, - verboseLevel: "off", - timeoutMs, - runId, - lane: "voice", - extraSystemPrompt, - agentDir, - toolsAllow, - }); - - const text = extractSpokenTextFromPayloads((result.payloads ?? []) as VoiceResponsePayload[]); - - if (!text && result.meta?.aborted) { - return { text: null, error: "Response generation was aborted" }; - } - - return { text }; + ); } catch (err) { console.error(`[voice-call] Response generation failed:`, err); return { text: null, error: String(err) }; diff --git a/packages/gateway-protocol/src/schema/sessions.ts b/packages/gateway-protocol/src/schema/sessions.ts index d938f80f68cb..e63f7c22dae3 100644 --- a/packages/gateway-protocol/src/schema/sessions.ts +++ b/packages/gateway-protocol/src/schema/sessions.ts @@ -185,6 +185,8 @@ export const SessionsListParamsSchema = Type.Object( spawnedBy: Type.Optional(NonEmptyString), agentId: Type.Optional(NonEmptyString), search: Type.Optional(Type.String()), + /** True lists archived sessions; false or omitted lists active sessions. */ + archived: Type.Optional(Type.Boolean()), }, { additionalProperties: false }, ); @@ -301,6 +303,8 @@ export const SessionsPatchParamsSchema = Type.Object( key: NonEmptyString, agentId: Type.Optional(NonEmptyString), label: Type.Optional(Type.Union([SessionLabelString, Type.Null()])), + archived: Type.Optional(Type.Boolean()), + pinned: Type.Optional(Type.Boolean()), thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), fastMode: Type.Optional(Type.Union([Type.Boolean(), Type.Literal("auto"), Type.Null()])), verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), @@ -382,6 +386,10 @@ export const SessionsDeleteParamsSchema = Type.Object( key: NonEmptyString, agentId: Type.Optional(NonEmptyString), deleteTranscript: Type.Optional(Type.Boolean()), + // Internal compare-and-delete guard for lifecycle-owned cleanup. + expectedSessionId: Type.Optional(NonEmptyString), + expectedLifecycleRevision: Type.Optional(NonEmptyString), + expectedSessionUpdatedAt: Type.Optional(Type.Number({ minimum: 0 })), // Internal control: when false, still unbind thread bindings but skip hook emission. emitLifecycleHooks: Type.Optional(Type.Boolean()), }, diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 5f015bb4f7ac..3cb3b69c4c17 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -723,7 +723,12 @@ export class Session { } async compact(params?: { maxLines?: number }): Promise { - return await this.client.request("sessions.compact", { key: this.key, ...params }); + return await this.client.request( + "sessions.compact", + { key: this.key, ...params }, + // The server owns the configurable terminal compaction deadline. + { timeoutMs: null }, + ); } } diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index 4d5a451dcebb..8a58e32473e6 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -1349,12 +1349,14 @@ describe("OpenClaw SDK", () => { const transport = new FakeTransport({ "sessions.create": { key: "session-main", label: "Main" }, "sessions.send": { status: "accepted", runId: "run_session" }, + "sessions.compact": { ok: true, compacted: true }, }); const oc = new OpenClaw({ transport }); const session = await oc.sessions.create({ key: "session-main" }); const run = await session.send({ message: "continue", thinking: "medium", timeoutMs: 1_500 }); const noTimeoutRun = await session.send({ message: "continue without timeout", timeoutMs: 0 }); + await session.compact(); expect(run.id).toBe("run_session"); expect(noTimeoutRun.id).toBe("run_session"); @@ -1374,6 +1376,11 @@ describe("OpenClaw SDK", () => { options: { expectFinal: true, timeoutMs: null }, params: { key: "session-main", message: "continue without timeout", timeoutMs: 0 }, }, + { + method: "sessions.compact", + options: { timeoutMs: null }, + params: { key: "session-main" }, + }, ]); }); diff --git a/scripts/protocol-gen-swift.ts b/scripts/protocol-gen-swift.ts index 17d86fa0236d..dd1a3ff93393 100644 --- a/scripts/protocol-gen-swift.ts +++ b/scripts/protocol-gen-swift.ts @@ -52,9 +52,13 @@ const DEFAULTED_OPTIONAL_INIT_PARAM_ENTRIES: readonly [string, readonly string[] ["SessionsMessagesSubscribeParams", ["agentId"]], ["SessionsMessagesUnsubscribeParams", ["agentId"]], ["SessionsAbortParams", ["agentId"]], - ["SessionsPatchParams", ["agentId"]], + ["SessionsListParams", ["archived"]], + ["SessionsPatchParams", ["agentId", "archived", "pinned"]], ["SessionsResetParams", ["agentId"]], - ["SessionsDeleteParams", ["agentId"]], + [ + "SessionsDeleteParams", + ["agentId", "expectedSessionId", "expectedLifecycleRevision", "expectedSessionUpdatedAt"], + ], ["SessionsCompactParams", ["agentId"]], ["SessionsResolveParams", ["allowMissing"]], ["SessionsUsageParams", ["agentId", "agentScope"]], diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index b7f175c48880..f5f803592e1a 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -40,6 +40,7 @@ const state = vi.hoisted(() => ({ ), resolveEffectiveModelFallbacksMock: vi.fn().mockReturnValue(undefined), hasLegacyAutoFallbackWithoutOriginMock: vi.fn((_entry: unknown) => false), + applyModelOverrideToSessionEntryMock: vi.fn((_params: unknown) => ({ updated: false })), resolveAutoFallbackPrimaryProbeMock: vi.fn((_params: unknown) => undefined as unknown), resolveChannelModelOverrideMock: vi.fn((_params: unknown) => null as unknown), assertLifecycleCurrentMock: vi.fn(), @@ -151,6 +152,8 @@ vi.mock("./command/run-context.js", () => ({ })); vi.mock("./command/session-store.runtime.js", () => ({ + loadSessionEntry: ({ sessionKey }: { sessionKey: string }) => + (state.sessionStoreMock as Record | undefined)?.[sessionKey], updateSessionStoreAfterAgentRun: (...args: unknown[]) => state.updateSessionStoreAfterAgentRunMock(...args), })); @@ -347,7 +350,8 @@ vi.mock("../sessions/level-overrides.js", () => ({ })); vi.mock("../sessions/model-overrides.js", () => ({ - applyModelOverrideToSessionEntry: () => ({ updated: false }), + applyModelOverrideToSessionEntry: (params: unknown) => + state.applyModelOverrideToSessionEntryMock(params), repairProviderWrappedModelOverride: () => ({ updated: false }), })); @@ -958,6 +962,7 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => { state.resolveAgentSkillsFilterMock.mockReturnValue(undefined); state.loadManifestModelCatalogMock.mockReturnValue([]); state.hasLegacyAutoFallbackWithoutOriginMock.mockReturnValue(false); + state.applyModelOverrideToSessionEntryMock.mockReturnValue({ updated: false }); state.resolveAutoFallbackPrimaryProbeMock.mockReturnValue(undefined); state.resolveChannelModelOverrideMock.mockImplementation((params: unknown) => { const input = params as { @@ -1259,17 +1264,19 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => { it("preserves restart ownership when an aborted attempt resolves normally", async () => { setupSingleAttemptFallback(); - state.runAgentAttemptMock.mockResolvedValue({ - payloads: [], - meta: { - durationMs: 100, - aborted: true, - stopReason: "end_turn", - agentMeta: { provider: "anthropic", model: "claude" }, - }, - }); const controller = new AbortController(); - controller.abort(createAgentRunRestartAbortError()); + state.runAgentAttemptMock.mockImplementation(async () => { + controller.abort(createAgentRunRestartAbortError()); + return { + payloads: [], + meta: { + durationMs: 100, + aborted: true, + stopReason: "end_turn", + agentMeta: { provider: "anthropic", model: "claude" }, + }, + }; + }); await expect( agentCommand({ @@ -1348,9 +1355,13 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => { expect.objectContaining({ runId: "session-1", sessionKey: "agent:main:main", - abortSignal: controller.signal, }), ); + const lifecycleError = state.emitAcpLifecycleErrorMock.mock.calls[0]?.[0] as + | { abortSignal?: AbortSignal } + | undefined; + expect(lifecycleError?.abortSignal?.aborted).toBe(true); + expect(lifecycleError?.abortSignal?.reason).toBe(controller.signal.reason); expect(state.persistAcpTurnTranscriptMock).toHaveBeenCalledTimes(1); expect(state.buildAcpResultMock).not.toHaveBeenCalled(); expect(state.deliverAgentCommandResultMock).not.toHaveBeenCalled(); @@ -1676,6 +1687,64 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => { expect(fallbackParams.model).toBe("channel-model"); }); + it("uses a concurrent user override adopted during legacy fallback repair", async () => { + setupSingleAttemptFallback(); + state.applyModelOverrideToSessionEntryMock.mockImplementation((params: unknown) => { + const { entry } = params as { entry: SessionEntry }; + delete entry.providerOverride; + delete entry.modelOverride; + delete entry.modelOverrideSource; + return { updated: true }; + }); + state.hasLegacyAutoFallbackWithoutOriginMock.mockImplementation( + (entry: unknown) => + (entry as SessionEntry | undefined)?.modelOverride === "stale-fallback-model", + ); + state.runtimeConfigMock = { + agents: { + defaults: { + model: "anthropic/default-model", + models: { + "anthropic/default-model": {}, + "anthropic/stale-fallback-model": {}, + "google/gemini-3-pro": {}, + }, + }, + }, + }; + const sessionEntry = { + sessionId: "session-1", + updatedAt: 1, + providerOverride: "anthropic", + modelOverride: "stale-fallback-model", + modelOverrideSource: "auto", + skillsSnapshot: { prompt: "", skills: [], version: 0 }, + } satisfies SessionEntry; + state.sessionEntryMock = sessionEntry; + state.sessionStoreMock = { "agent:main:main": sessionEntry }; + state.storePathMock = "/tmp/openclaw-session-store.json"; + state.persistSessionEntryMock.mockImplementation(async (...args: unknown[]) => { + const params = args[0] as { entry?: SessionEntry }; + if (params.entry?.modelOverride === "stale-fallback-model") { + return params.entry; + } + return { + ...sessionEntry, + updatedAt: 2, + providerOverride: "google", + modelOverride: "gemini-3-pro", + modelOverrideSource: "user", + }; + }); + state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("google", "gemini-3-pro")); + + await runBasicAgentCommand(); + + const fallbackParams = mockCallArg(state.runWithModelFallbackMock) as FallbackRunnerParams; + expect(fallbackParams.provider).toBe("google"); + expect(fallbackParams.model).toBe("gemini-3-pro"); + }); + it("probes the channel primary when a session is pinned to an auto fallback", async () => { setupSingleAttemptFallback(); state.resolveAutoFallbackPrimaryProbeMock.mockReturnValue({ @@ -2970,8 +3039,10 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => { it("marks lifecycle errors aborted when cancellation reaches post-turn handling", async () => { const abortController = new AbortController(); - abortController.abort(); - state.runWithModelFallbackMock.mockRejectedValueOnce(new Error("request aborted")); + state.runWithModelFallbackMock.mockImplementationOnce(async () => { + abortController.abort(); + throw new Error("request aborted"); + }); await expect( agentCommand({ diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index f7f210f2302d..cda3d38a5d2b 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -15,6 +15,7 @@ import { resolveChannelModelOverride } from "../channels/model-overrides.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { CliDeps } from "../cli/deps.types.js"; import { getRuntimeConfig } from "../config/io.js"; +import { resolveSessionWorkStartError } from "../config/sessions/lifecycle.js"; import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { withLocalGatewayRequestScope } from "../gateway/local-request-context.js"; @@ -54,6 +55,7 @@ import { } from "../sessions/model-overrides.js"; import { resolveSendPolicy } from "../sessions/send-policy.js"; import { createUserTurnTranscriptRecorder } from "../sessions/user-turn-transcript.js"; +import { beginSessionWorkAdmission } from "../sessions/session-lifecycle-admission.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; import { resolveEffectiveAgentSkillFilter } from "../skills/discovery/agent-filter.js"; import type { getRemoteSkillEligibility } from "../skills/runtime/remote.js"; @@ -137,6 +139,7 @@ import { import { listOpenAIAuthProfileProvidersForAgentRuntime } from "./openai-routing.js"; import { resolveProviderIdForAuth } from "./provider-auth-aliases.js"; import { + createAgentRunRestartAbortError, isAgentRunDirectAbortReason, isAgentRunRestartAbortReason, resolveAgentRunAbortLifecycleFields, @@ -378,38 +381,10 @@ type PersistSessionEntryParams = { sessionStore: Record; sessionKey: string; storePath: string; + initialEntry: SessionEntry; entry: SessionEntry; }; -type OverrideFieldClearedByDelete = - | "providerOverride" - | "modelOverride" - | "modelOverrideSource" - | "modelOverrideFallbackOriginProvider" - | "modelOverrideFallbackOriginModel" - | "authProfileOverride" - | "authProfileOverrideSource" - | "authProfileOverrideCompactionCount" - | "fallbackNoticeSelectedModel" - | "fallbackNoticeActiveModel" - | "fallbackNoticeReason" - | "claudeCliSessionId"; - -const OVERRIDE_FIELDS_CLEARED_BY_DELETE: OverrideFieldClearedByDelete[] = [ - "providerOverride", - "modelOverride", - "modelOverrideSource", - "modelOverrideFallbackOriginProvider", - "modelOverrideFallbackOriginModel", - "authProfileOverride", - "authProfileOverrideSource", - "authProfileOverrideCompactionCount", - "fallbackNoticeSelectedModel", - "fallbackNoticeActiveModel", - "fallbackNoticeReason", - "claudeCliSessionId", -]; - const OVERRIDE_VALUE_MAX_LENGTH = 256; async function persistSessionEntry( @@ -417,10 +392,7 @@ async function persistSessionEntry( shouldPersist?: (entry: SessionEntry | undefined) => boolean; }, ): Promise { - return await persistSessionEntryBase({ - ...params, - clearedFields: OVERRIDE_FIELDS_CLEARED_BY_DELETE, - }); + return await persistSessionEntryBase(params); } function clearPendingFinalDeliveryFields(entry: SessionEntry, updatedAt: number): SessionEntry { @@ -861,7 +833,13 @@ async function agentCommandInternal( const preserveUserFacingSessionModelState = initialOpts.preserveUserFacingSessionModelState === true; const prepared = await prepareAgentCommandExecution(initialOpts, runtime); - const opts = prepared.opts; + const lifecycleAbortController = new AbortController(); + const opts = { + ...prepared.opts, + abortSignal: prepared.opts.abortSignal + ? AbortSignal.any([prepared.opts.abortSignal, lifecycleAbortController.signal]) + : lifecycleAbortController.signal, + }; const { body, transcriptBody, @@ -901,797 +879,860 @@ async function agentCommandInternal( let sessionReboundDuringRun = false; let trackedRestartRecoveryDeliveryContext = false; let currentRunDeliveryContext: DeliveryContext | undefined; + const preparedSessionId = sessionEntry?.sessionId; + const sessionStoreRuntime = storePath && sessionKey ? await loadSessionStoreRuntime() : undefined; + + // Reset marks its mutation before interrupting work. An aborted run must not + // queue behind that mutation or reset would wait on the run holding the queue. + const sessionWorkAdmission = await beginSessionWorkAdmission({ + scope: storePath ?? `agent:${sessionAgentId}`, + identities: [sessionKey, sessionId], + signal: opts.abortSignal, + onInterrupt: () => lifecycleAbortController.abort(createAgentRunRestartAbortError()), + assertAllowed: () => { + const currentEntry = + sessionStoreRuntime && storePath && sessionKey + ? sessionStoreRuntime.loadSessionEntry({ + storePath, + sessionKey, + readConsistency: "latest", + }) + : sessionEntry; + if (!currentEntry && preparedSessionId) { + throw new Error(`Session "${sessionKey ?? sessionId}" changed while starting work. Retry.`); + } + const matchesIntentionalRollover = + isNewSession && currentEntry?.sessionId === preparedSessionId; + if (currentEntry && currentEntry.sessionId !== sessionId && !matchesIntentionalRollover) { + throw new Error(`Session "${sessionKey ?? sessionId}" changed while starting work. Retry.`); + } + const archivedSessionError = resolveSessionWorkStartError( + sessionKey ?? sessionId, + currentEntry, + ); + if (archivedSessionError) { + throw new Error(archivedSessionError); + } + sessionEntry = currentEntry; + if (sessionStore && sessionKey) { + if (currentEntry) { + sessionStore[sessionKey] = currentEntry; + } else { + delete sessionStore[sessionKey]; + } + } + }, + }); try { - if (opts.deliver === true) { - const sendPolicy = resolveSendPolicy({ - cfg, - entry: sessionEntry, - sessionKey, - channel: sessionEntry?.channel, - chatType: sessionEntry?.chatType, - }); - if (sendPolicy === "deny") { - throw new Error("send blocked by session policy"); - } - } - - if (!isRawModelRun && acpResolution?.kind === "stale") { - throw acpResolution.error; - } - - if ( - sessionStore && - sessionKey && - !suppressVisibleSessionEffects && - !isSubagentSessionKey(sessionKey) - ) { - const now = Date.now(); - const currentStoreEntry = sessionStore[sessionKey]; - const allowCreateRestartRecoveryEntry = - currentStoreEntry === undefined && sessionEntry === undefined; - const entry = currentStoreEntry ?? - sessionEntry ?? { sessionId, updatedAt: now, sessionStartedAt: now }; - currentRunDeliveryContext = await resolveCurrentRunDeliveryContext({ - cfg, - opts, - sessionEntry: entry, - }); - assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); - const next: SessionEntry = { - ...entry, - sessionId, - updatedAt: now, - restartRecoveryDeliveryContext: currentRunDeliveryContext, - restartRecoveryDeliveryRunId: currentRunDeliveryContext ? runId : undefined, - }; - const persisted = await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry: next, - shouldPersist: (current) => - shouldPersistRestartRecoveryContextClaim( - current, - sessionId, - runId, - allowCreateRestartRecoveryEntry, - ), - }); - sessionEntry = persisted ?? sessionEntry; - trackedRestartRecoveryDeliveryContext = - Boolean(persisted?.restartRecoveryDeliveryContext) && - persisted?.restartRecoveryDeliveryRunId === runId; - } - - if (!isRawModelRun && acpResolution?.kind === "ready" && sessionKey) { - assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); - const attemptExecutionRuntime = await loadAttemptExecutionRuntime(); - const startedAt = Date.now(); - registerAgentRunContext( - runId, - suppressVisibleSessionEffects - ? { isControlUiVisible: false, lifecycleGeneration } - : { - sessionKey, - sessionId, - lifecycleGeneration, - }, - ); - attemptExecutionRuntime.emitAcpLifecycleStart({ runId, startedAt, lifecycleGeneration }); - - const visibleTextAccumulator = attemptExecutionRuntime.createAcpVisibleTextAccumulator(); - let stopReason: string | undefined; - try { - const { - resolveAcpAgentPolicyError, - resolveAcpDispatchPolicyError, - resolveAcpExplicitTurnPolicyError, - } = await loadAcpPolicyRuntime(); - const turnPolicyError = - opts.acpTurnSource === "manual_spawn" - ? resolveAcpExplicitTurnPolicyError(cfg) - : resolveAcpDispatchPolicyError(cfg); - if (turnPolicyError) { - throw turnPolicyError; - } - const acpAgent = normalizeAgentId( - acpResolution.meta.agent || resolveAgentIdFromSessionKey(sessionKey), - ); - const agentPolicyError = resolveAcpAgentPolicyError(cfg, acpAgent); - if (agentPolicyError) { - throw agentPolicyError; - } - - const acpImageAttachments = resolveInlineAgentImageAttachments(opts.images); - assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); - await acpManager.runTurn({ + return await sessionWorkAdmission.run(async () => { + if (opts.deliver === true) { + const sendPolicy = resolveSendPolicy({ cfg, + entry: sessionEntry, sessionKey, - text: body, - attachments: acpImageAttachments.length > 0 ? acpImageAttachments : undefined, - mode: "prompt", - requestId: runId, - signal: opts.abortSignal, - onLifecycle: (event) => { - if (event.type === "prompt_submitted") { - attemptExecutionRuntime.emitAcpPromptSubmitted({ - runId, - sessionKey, - at: event.at, - }); - } - }, - onEvent: (event) => { - if (event.type !== "text_delta") { - attemptExecutionRuntime.emitAcpRuntimeEvent({ - runId, - sessionKey, - event, - }); - } - if (event.type === "done") { - stopReason = event.stopReason; - return; - } - if (event.type !== "text_delta") { - return; - } - if (event.stream && event.stream !== "output") { - return; - } - if (!event.text) { - return; - } - const visibleUpdate = visibleTextAccumulator.consume(event.text); - if (!visibleUpdate) { - return; - } - attemptExecutionRuntime.emitAcpAssistantDelta({ - runId, - text: visibleUpdate.text, - delta: visibleUpdate.delta, - }); - }, + channel: sessionEntry?.channel, + chatType: sessionEntry?.chatType, }); - if (isAgentRunRestartAbortReason(opts.abortSignal?.reason)) { - throw opts.abortSignal?.reason; + if (sendPolicy === "deny") { + throw new Error("send blocked by session policy"); } - } catch (error) { - const { toAcpRuntimeError } = await loadAcpRuntimeErrorsRuntime(); - const acpError = toAcpRuntimeError({ - error, - fallbackCode: "ACP_TURN_FAILED", - fallbackMessage: "ACP turn failed before completion.", - }); - attemptExecutionRuntime.emitAcpLifecycleError({ - runId, - error: acpError, - sessionKey, - lifecycleGeneration, - abortSignal: opts.abortSignal, - }); - throw acpError; } - const finalTextRaw = visibleTextAccumulator.finalizeRaw(); - const finalText = visibleTextAccumulator.finalize(); - try { - const [{ resolveAcpSessionCwd }, { resolveSessionTranscriptFile }] = await Promise.all([ - loadAcpSessionIdentifiersRuntime(), - loadTranscriptResolveRuntime(), - ]); - const internalSource = suppressVisibleSessionEffects - ? await resolveSessionTranscriptFile({ - sessionId, - sessionKey, - sessionEntry, - agentId: sessionAgentId, - threadId: opts.threadId, - }) - : undefined; - const internalSessionFile = suppressVisibleSessionEffects - ? await prepareInternalSessionEffectsTranscript({ - sessionFile: internalSource?.sessionFile, - runId, - }) - : undefined; - const transcriptSessionEntry: SessionEntry | undefined = internalSessionFile - ? { - ...(sessionEntry ?? { - sessionId, - updatedAt: Date.now(), - sessionStartedAt: Date.now(), - }), - sessionId, - sessionFile: internalSessionFile, - } - : sessionEntry; - const transcriptResult = await attemptExecutionRuntime.persistAcpTurnTranscript({ - body, - transcriptBody, - finalText: finalTextRaw, + if (!isRawModelRun && acpResolution?.kind === "stale") { + throw acpResolution.error; + } + + if ( + sessionStore && + sessionKey && + !suppressVisibleSessionEffects && + !isSubagentSessionKey(sessionKey) + ) { + const now = Date.now(); + const currentStoreEntry = sessionStore[sessionKey]; + const allowCreateRestartRecoveryEntry = + currentStoreEntry === undefined && sessionEntry === undefined; + const entry = currentStoreEntry ?? + sessionEntry ?? { sessionId, updatedAt: now, sessionStartedAt: now }; + currentRunDeliveryContext = await resolveCurrentRunDeliveryContext({ + cfg, + opts, + sessionEntry: entry, + }); + assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); + const next: SessionEntry = { + ...entry, sessionId, + updatedAt: now, + restartRecoveryDeliveryContext: currentRunDeliveryContext, + restartRecoveryDeliveryRunId: currentRunDeliveryContext ? runId : undefined, + }; + const persisted = await persistSessionEntry({ + sessionStore, sessionKey, - sessionEntry: transcriptSessionEntry, - sessionStore: suppressVisibleSessionEffects ? undefined : sessionStore, - storePath: suppressVisibleSessionEffects ? undefined : storePath, - sessionAgentId, - threadId: opts.threadId, - sessionCwd: resolveAcpSessionCwd(acpResolution.meta) ?? workspaceDir, - config: cfg, + storePath, + initialEntry: entry, + entry: next, + shouldPersist: (current) => + shouldPersistRestartRecoveryContextClaim( + current, + sessionId, + runId, + allowCreateRestartRecoveryEntry, + ), }); - sessionEntry = transcriptResult.sessionEntry; - if (internalSessionFile) { - sessionEntry = prepared.sessionEntry; - } - } catch (error) { - log.warn( - `ACP transcript persistence failed for ${sessionKey}: ${formatErrorMessage(error)}`, - ); + sessionEntry = persisted ?? sessionEntry; + trackedRestartRecoveryDeliveryContext = + Boolean(persisted?.restartRecoveryDeliveryContext) && + persisted?.restartRecoveryDeliveryRunId === runId; } - const restartAbortReason = opts.abortSignal?.reason; - if (isAgentRunRestartAbortReason(restartAbortReason)) { - attemptExecutionRuntime.emitAcpLifecycleError({ + + if (!isRawModelRun && acpResolution?.kind === "ready" && sessionKey) { + assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); + const attemptExecutionRuntime = await loadAttemptExecutionRuntime(); + const startedAt = Date.now(); + registerAgentRunContext( + runId, + suppressVisibleSessionEffects + ? { isControlUiVisible: false, lifecycleGeneration } + : { + sessionKey, + sessionId, + lifecycleGeneration, + }, + ); + attemptExecutionRuntime.emitAcpLifecycleStart({ runId, startedAt, lifecycleGeneration }); + + const visibleTextAccumulator = attemptExecutionRuntime.createAcpVisibleTextAccumulator(); + let stopReason: string | undefined; + try { + const { + resolveAcpAgentPolicyError, + resolveAcpDispatchPolicyError, + resolveAcpExplicitTurnPolicyError, + } = await loadAcpPolicyRuntime(); + const turnPolicyError = + opts.acpTurnSource === "manual_spawn" + ? resolveAcpExplicitTurnPolicyError(cfg) + : resolveAcpDispatchPolicyError(cfg); + if (turnPolicyError) { + throw turnPolicyError; + } + const acpAgent = normalizeAgentId( + acpResolution.meta.agent || resolveAgentIdFromSessionKey(sessionKey), + ); + const agentPolicyError = resolveAcpAgentPolicyError(cfg, acpAgent); + if (agentPolicyError) { + throw agentPolicyError; + } + + const acpImageAttachments = resolveInlineAgentImageAttachments(opts.images); + assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); + await acpManager.runTurn({ + cfg, + sessionKey, + text: body, + attachments: acpImageAttachments.length > 0 ? acpImageAttachments : undefined, + mode: "prompt", + requestId: runId, + signal: opts.abortSignal, + onLifecycle: (event) => { + if (event.type === "prompt_submitted") { + attemptExecutionRuntime.emitAcpPromptSubmitted({ + runId, + sessionKey, + at: event.at, + }); + } + }, + onEvent: (event) => { + if (event.type !== "text_delta") { + attemptExecutionRuntime.emitAcpRuntimeEvent({ + runId, + sessionKey, + event, + }); + } + if (event.type === "done") { + stopReason = event.stopReason; + return; + } + if (event.type !== "text_delta") { + return; + } + if (event.stream && event.stream !== "output") { + return; + } + if (!event.text) { + return; + } + const visibleUpdate = visibleTextAccumulator.consume(event.text); + if (!visibleUpdate) { + return; + } + attemptExecutionRuntime.emitAcpAssistantDelta({ + runId, + text: visibleUpdate.text, + delta: visibleUpdate.delta, + }); + }, + }); + if (isAgentRunRestartAbortReason(opts.abortSignal?.reason)) { + throw opts.abortSignal?.reason; + } + } catch (error) { + const { toAcpRuntimeError } = await loadAcpRuntimeErrorsRuntime(); + const acpError = toAcpRuntimeError({ + error, + fallbackCode: "ACP_TURN_FAILED", + fallbackMessage: "ACP turn failed before completion.", + }); + attemptExecutionRuntime.emitAcpLifecycleError({ + runId, + error: acpError, + sessionKey, + lifecycleGeneration, + abortSignal: opts.abortSignal, + }); + throw acpError; + } + + const finalTextRaw = visibleTextAccumulator.finalizeRaw(); + const finalText = visibleTextAccumulator.finalize(); + try { + const [{ resolveAcpSessionCwd }, { resolveSessionTranscriptFile }] = await Promise.all([ + loadAcpSessionIdentifiersRuntime(), + loadTranscriptResolveRuntime(), + ]); + const internalSource = suppressVisibleSessionEffects + ? await resolveSessionTranscriptFile({ + sessionId, + sessionKey, + sessionEntry, + agentId: sessionAgentId, + threadId: opts.threadId, + }) + : undefined; + const internalSessionFile = suppressVisibleSessionEffects + ? await prepareInternalSessionEffectsTranscript({ + sessionFile: internalSource?.sessionFile, + runId, + }) + : undefined; + const transcriptSessionEntry: SessionEntry | undefined = internalSessionFile + ? { + ...(sessionEntry ?? { + sessionId, + updatedAt: Date.now(), + sessionStartedAt: Date.now(), + }), + sessionId, + sessionFile: internalSessionFile, + } + : sessionEntry; + const transcriptResult = await attemptExecutionRuntime.persistAcpTurnTranscript({ + body, + transcriptBody, + finalText: finalTextRaw, + sessionId, + sessionKey, + sessionEntry: transcriptSessionEntry, + sessionStore: suppressVisibleSessionEffects ? undefined : sessionStore, + storePath: suppressVisibleSessionEffects ? undefined : storePath, + sessionAgentId, + threadId: opts.threadId, + sessionCwd: resolveAcpSessionCwd(acpResolution.meta) ?? workspaceDir, + config: cfg, + }); + sessionEntry = transcriptResult.sessionEntry; + if (internalSessionFile) { + sessionEntry = prepared.sessionEntry; + } + } catch (error) { + log.warn( + `ACP transcript persistence failed for ${sessionKey}: ${formatErrorMessage(error)}`, + ); + } + const restartAbortReason = opts.abortSignal?.reason; + if (isAgentRunRestartAbortReason(restartAbortReason)) { + attemptExecutionRuntime.emitAcpLifecycleError({ + runId, + error: restartAbortReason, + sessionKey, + lifecycleGeneration, + abortSignal: opts.abortSignal, + }); + throw restartAbortReason; + } + attemptExecutionRuntime.emitAcpLifecycleEnd({ runId, - error: restartAbortReason, - sessionKey, lifecycleGeneration, abortSignal: opts.abortSignal, }); - throw restartAbortReason; - } - attemptExecutionRuntime.emitAcpLifecycleEnd({ - runId, - lifecycleGeneration, - abortSignal: opts.abortSignal, - }); - const result = applyAgentRunAbortMetadata( - attemptExecutionRuntime.buildAcpResult({ - payloadText: finalText, - startedAt, - stopReason, - abortSignal: opts.abortSignal, - }), - opts.abortSignal, - ); - const payloads = result.payloads; - const { deliverAgentCommandResult } = await loadDeliveryRuntime(); - - return await deliverAgentCommandResult({ - cfg, - deps: resolvedDeps, - runtime, - opts, - outboundSession, - sessionEntry, - result, - payloads, - assertDeliveryCurrent: () => assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration), - }); - } - - let resolvedThinkLevel = thinkOnce ?? thinkOverride ?? persistedThinking; - const resolvedVerboseLevel = - verboseOverride ?? persistedVerbose ?? (agentCfg?.verboseDefault as VerboseLevel | undefined); - - assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); - if (sessionKey || suppressVisibleSessionEffects) { - registerAgentRunContext(runId, { - ...(sessionKey && !suppressVisibleSessionEffects ? { sessionKey, sessionId } : {}), - lifecycleGeneration, - verboseLevel: resolvedVerboseLevel, - isControlUiVisible: !suppressVisibleSessionEffects, - }); - } - - const skillFilter = resolveEffectiveAgentSkillFilter(cfg, sessionAgentId); - const currentSkillsSnapshot = sessionEntry?.skillsSnapshot; - const [ - { getRemoteSkillEligibility, resolveReusableWorkspaceSkillSnapshot }, - { canExecRequestNode }, - ] = await Promise.all([loadSkillsRuntime(), loadExecDefaultsRuntime()]); - const skillSnapshotState = resolveReusableWorkspaceSkillSnapshot({ - workspaceDir, - config: cfg, - agentId: sessionAgentId, - existingSnapshot: isNewSession ? undefined : currentSkillsSnapshot, - skillFilter, - eligibility: { - remote: getRemoteSkillEligibility({ - advertiseExecNode: canExecRequestNode({ - cfg, - sessionEntry, - sessionKey, - agentId: sessionAgentId, + const result = applyAgentRunAbortMetadata( + attemptExecutionRuntime.buildAcpResult({ + payloadText: finalText, + startedAt, + stopReason, + abortSignal: opts.abortSignal, }), - }), - }, - watch: false, - }); - const needsSkillsSnapshot = - isNewSession || !currentSkillsSnapshot || skillSnapshotState.shouldRefresh; - const skillsSnapshot = skillSnapshotState.snapshot; + opts.abortSignal, + ); + const payloads = result.payloads; + const { deliverAgentCommandResult } = await loadDeliveryRuntime(); - if ( - skillsSnapshot && - sessionStore && - sessionKey && - needsSkillsSnapshot && - !suppressVisibleSessionEffects - ) { - const now = Date.now(); - const current = sessionEntry ?? { - sessionId, - updatedAt: now, - sessionStartedAt: now, - }; - const next: SessionEntry = { - ...current, - sessionId, - updatedAt: now, - sessionStartedAt: current.sessionStartedAt ?? now, - skillsSnapshot, - }; - await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry: next, - }); - sessionEntry = next; - } - - // Persist explicit /command overrides to the session store when we have a key. - const hasInitialSessionOverrides = Boolean(thinkOverride || verboseOverride); - const shouldPersistInitialSessionTouch = - opts.skipInitialSessionTouch !== true || hasInitialSessionOverrides; - if ( - sessionStore && - sessionKey && - !suppressVisibleSessionEffects && - shouldPersistInitialSessionTouch - ) { - const now = Date.now(); - const entry = sessionStore[sessionKey] ?? - sessionEntry ?? { sessionId, updatedAt: now, sessionStartedAt: now }; - const next: SessionEntry = { - ...entry, - sessionId, - updatedAt: now, - sessionStartedAt: entry.sessionStartedAt ?? now, - lastInteractionAt: now, - }; - if (thinkOverride) { - next.thinkingLevel = thinkOverride; + return await deliverAgentCommandResult({ + cfg, + deps: resolvedDeps, + runtime, + opts, + outboundSession, + sessionEntry, + result, + payloads, + assertDeliveryCurrent: () => + assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration), + }); } - applyVerboseOverride(next, verboseOverride); - await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry: next, + + let resolvedThinkLevel = thinkOnce ?? thinkOverride ?? persistedThinking; + const resolvedVerboseLevel = + verboseOverride ?? + persistedVerbose ?? + (agentCfg?.verboseDefault as VerboseLevel | undefined); + + assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration); + if (sessionKey || suppressVisibleSessionEffects) { + registerAgentRunContext(runId, { + ...(sessionKey && !suppressVisibleSessionEffects ? { sessionKey, sessionId } : {}), + lifecycleGeneration, + verboseLevel: resolvedVerboseLevel, + isControlUiVisible: !suppressVisibleSessionEffects, + }); + } + + const skillFilter = resolveEffectiveAgentSkillFilter(cfg, sessionAgentId); + const currentSkillsSnapshot = sessionEntry?.skillsSnapshot; + const [ + { getRemoteSkillEligibility, resolveReusableWorkspaceSkillSnapshot }, + { canExecRequestNode }, + ] = await Promise.all([loadSkillsRuntime(), loadExecDefaultsRuntime()]); + const skillSnapshotState = resolveReusableWorkspaceSkillSnapshot({ + workspaceDir, + config: cfg, + agentId: sessionAgentId, + existingSnapshot: isNewSession ? undefined : currentSkillsSnapshot, + skillFilter, + eligibility: { + remote: getRemoteSkillEligibility({ + advertiseExecNode: canExecRequestNode({ + cfg, + sessionEntry, + sessionKey, + agentId: sessionAgentId, + }), + }), + }, + watch: false, }); - sessionEntry = next; - } + const needsSkillsSnapshot = + isNewSession || !currentSkillsSnapshot || skillSnapshotState.shouldRefresh; + const skillsSnapshot = skillSnapshotState.snapshot; - const configuredDefaultRef = resolveDefaultModelForAgent({ - cfg, - agentId: sessionAgentId, - allowPluginNormalization: pluginsEnabled, - ...modelManifestContext, - }); - const runContext = resolveAgentRunContext(opts); - const { provider: defaultProvider, model: defaultModel } = normalizeAgentCommandDefaultModelRef( - cfg, - configuredDefaultRef.provider, - configuredDefaultRef.model, - modelManifestContext, - ); - let provider = defaultProvider; - let model = defaultModel; - const hasAllowlist = agentCfg?.models && Object.keys(agentCfg.models).length > 0; - const hasStoredOverride = Boolean( - sessionEntry?.modelOverride || sessionEntry?.providerOverride, - ); - let storedModelOverrideSource = hasStoredOverride - ? sessionEntry?.modelOverrideSource - : undefined; - const hasStoredAutoFallbackProvenance = - hasStoredOverride && hasSessionAutoModelFallbackProvenance(sessionEntry); - const hasLegacyAutoFallbackOverrideWithoutOrigin = - hasStoredOverride && hasLegacyAutoFallbackWithoutOrigin(sessionEntry); - const explicitProviderOverride = - typeof opts.provider === "string" - ? normalizeExplicitOverrideInput(opts.provider, "provider") - : undefined; - const explicitModelOverride = - typeof opts.model === "string" - ? normalizeExplicitOverrideInput(opts.model, "model") - : undefined; - const hasExplicitRunOverride = Boolean(explicitProviderOverride || explicitModelOverride); - if (hasExplicitRunOverride && opts.allowModelOverride !== true) { - throw new Error("Model override is not authorized for this caller."); - } - const needsModelCatalog = Boolean(hasAllowlist); - let allowedModelCatalog: ReturnType = []; - let modelCatalog: ReturnType | null = null; - let visibilityPolicy: ModelVisibilityPolicy = createModelVisibilityPolicy({ - cfg, - catalog: [], - defaultProvider, - defaultModel, - allowManifestNormalization: true, - allowPluginNormalization: pluginsEnabled, - ...modelManifestContext, - }); + if ( + skillsSnapshot && + sessionStore && + sessionKey && + needsSkillsSnapshot && + !suppressVisibleSessionEffects + ) { + const now = Date.now(); + const current = sessionEntry ?? { + sessionId, + updatedAt: now, + sessionStartedAt: now, + }; + const next: SessionEntry = { + ...current, + sessionId, + updatedAt: now, + sessionStartedAt: current.sessionStartedAt ?? now, + skillsSnapshot, + }; + const persisted = await persistSessionEntry({ + sessionStore, + sessionKey, + storePath, + initialEntry: current, + entry: next, + }); + sessionEntry = persisted ?? sessionEntry; + } - if (needsModelCatalog) { - modelCatalog = pluginsEnabled ? loadManifestModelCatalog({ config: cfg, workspaceDir }) : []; - visibilityPolicy = createModelVisibilityPolicy({ + // Persist explicit /command overrides to the session store when we have a key. + const hasInitialSessionOverrides = Boolean(thinkOverride || verboseOverride); + const shouldPersistInitialSessionTouch = + opts.skipInitialSessionTouch !== true || hasInitialSessionOverrides; + if ( + sessionStore && + sessionKey && + !suppressVisibleSessionEffects && + shouldPersistInitialSessionTouch + ) { + const now = Date.now(); + const entry = sessionStore[sessionKey] ?? + sessionEntry ?? { sessionId, updatedAt: now, sessionStartedAt: now }; + const next: SessionEntry = { + ...entry, + sessionId, + updatedAt: now, + sessionStartedAt: entry.sessionStartedAt ?? now, + lastInteractionAt: now, + }; + if (thinkOverride) { + next.thinkingLevel = thinkOverride; + } + applyVerboseOverride(next, verboseOverride); + const persisted = await persistSessionEntry({ + sessionStore, + sessionKey, + storePath, + initialEntry: entry, + entry: next, + }); + sessionEntry = persisted ?? sessionEntry; + } + + const configuredDefaultRef = resolveDefaultModelForAgent({ cfg, - catalog: modelCatalog, + agentId: sessionAgentId, + allowPluginNormalization: pluginsEnabled, + ...modelManifestContext, + }); + const runContext = resolveAgentRunContext(opts); + const { provider: defaultProvider, model: defaultModel } = + normalizeAgentCommandDefaultModelRef( + cfg, + configuredDefaultRef.provider, + configuredDefaultRef.model, + modelManifestContext, + ); + let provider = defaultProvider; + let model = defaultModel; + const hasAllowlist = agentCfg?.models && Object.keys(agentCfg.models).length > 0; + const hasStoredOverride = Boolean( + sessionEntry?.modelOverride || sessionEntry?.providerOverride, + ); + let storedModelOverrideSource = hasStoredOverride + ? sessionEntry?.modelOverrideSource + : undefined; + let hasStoredAutoFallbackProvenance = + hasStoredOverride && hasSessionAutoModelFallbackProvenance(sessionEntry); + let hasLegacyAutoFallbackOverrideWithoutOrigin = + hasStoredOverride && hasLegacyAutoFallbackWithoutOrigin(sessionEntry); + const explicitProviderOverride = + typeof opts.provider === "string" + ? normalizeExplicitOverrideInput(opts.provider, "provider") + : undefined; + const explicitModelOverride = + typeof opts.model === "string" + ? normalizeExplicitOverrideInput(opts.model, "model") + : undefined; + const hasExplicitRunOverride = Boolean(explicitProviderOverride || explicitModelOverride); + if (hasExplicitRunOverride && opts.allowModelOverride !== true) { + throw new Error("Model override is not authorized for this caller."); + } + const needsModelCatalog = Boolean(hasAllowlist); + let allowedModelCatalog: ReturnType = []; + let modelCatalog: ReturnType | null = null; + let visibilityPolicy: ModelVisibilityPolicy = createModelVisibilityPolicy({ + cfg, + catalog: [], defaultProvider, defaultModel, - agentId: sessionAgentId, allowManifestNormalization: true, allowPluginNormalization: pluginsEnabled, ...modelManifestContext, }); - allowedModelCatalog = visibilityPolicy.allowedCatalog; - } - if ( - sessionEntry && - sessionStore && - sessionKey && - hasStoredOverride && - !suppressVisibleSessionEffects - ) { - const entry = sessionEntry; - if (hasLegacyAutoFallbackOverrideWithoutOrigin) { - const { updated } = applyModelOverrideToSessionEntry({ - entry, - selection: { provider: defaultProvider, model: defaultModel, isDefault: true }, - }); - if (updated) { - storedModelOverrideSource = undefined; - await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry, - }); - } - } - const repaired = repairProviderWrappedModelOverride({ - entry, - defaultProvider, - defaultModel, - }); - if (repaired.updated) { - await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry, - }); - } - const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider; - const overrideModel = sessionEntry.modelOverride?.trim(); - if (overrideModel) { - const normalizedOverride = normalizeAgentCommandModelRef( + if (needsModelCatalog) { + modelCatalog = pluginsEnabled + ? loadManifestModelCatalog({ config: cfg, workspaceDir }) + : []; + visibilityPolicy = createModelVisibilityPolicy({ cfg, - overrideProvider, - overrideModel, - modelManifestContext, - ); - const key = modelKey(normalizedOverride.provider, normalizedOverride.model); - if (!visibilityPolicy.allowsKey(key)) { + catalog: modelCatalog, + defaultProvider, + defaultModel, + agentId: sessionAgentId, + allowManifestNormalization: true, + allowPluginNormalization: pluginsEnabled, + ...modelManifestContext, + }); + allowedModelCatalog = visibilityPolicy.allowedCatalog; + } + + if ( + sessionEntry && + sessionStore && + sessionKey && + hasStoredOverride && + !suppressVisibleSessionEffects + ) { + const initialEntry = sessionEntry; + const entry = { ...sessionEntry }; + let entryUpdated = false; + if (hasLegacyAutoFallbackOverrideWithoutOrigin) { const { updated } = applyModelOverrideToSessionEntry({ entry, selection: { provider: defaultProvider, model: defaultModel, isDefault: true }, }); if (updated) { - await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry, - }); + storedModelOverrideSource = undefined; + entryUpdated = true; } } - } - } - - const storedProviderOverride = hasLegacyAutoFallbackOverrideWithoutOrigin - ? undefined - : sessionEntry?.providerOverride?.trim(); - let storedModelOverride = hasLegacyAutoFallbackOverrideWithoutOrigin - ? undefined - : sessionEntry?.modelOverride?.trim(); - const currentRunModelChannel = [ - runContext.messageChannel, - opts.replyChannel, - opts.channel, - ].find((channel): channel is string => - Boolean(channel && isDeliverableMessageChannel(channel)), - ); - const channelOverrideGroupId = currentRunModelChannel - ? (runContext.groupId ?? sessionEntry?.groupId ?? runContext.currentChannelId) - : (sessionEntry?.groupId ?? runContext.groupId ?? runContext.currentChannelId); - const channelModelOverride = - cfg.channels?.modelByChannel && !hasExplicitRunOverride - ? resolveChannelModelOverride({ - cfg, - channel: - currentRunModelChannel ?? - sessionEntry?.channel ?? - sessionEntry?.lastChannel ?? - sessionEntry?.origin?.provider, - groupId: channelOverrideGroupId, - groupChatType: sessionEntry?.chatType ?? sessionEntry?.origin?.chatType, - groupChannel: runContext.groupChannel ?? sessionEntry?.groupChannel, - groupSubject: sessionEntry?.subject, - parentSessionKey: sessionEntry?.parentSessionKey ?? sessionKey, - directUserIds: [ - sessionEntry?.origin?.nativeDirectUserId, - sessionEntry?.origin?.from, - sessionEntry?.origin?.to, - ], - }) - : null; - const normalizedChannelOverride = channelModelOverride - ? parseAgentCommandModelRef( - cfg, - channelModelOverride.model, + const repaired = repairProviderWrappedModelOverride({ + entry, defaultProvider, - modelManifestContext, - ) - : null; - const primaryProvider = normalizedChannelOverride?.provider ?? defaultProvider; - const primaryModel = normalizedChannelOverride?.model ?? defaultModel; - const hasEffectiveStoredOverride = Boolean(storedProviderOverride || storedModelOverride); - if (normalizedChannelOverride && !hasEffectiveStoredOverride) { - provider = normalizedChannelOverride.provider; - model = normalizedChannelOverride.model; - } - if (storedModelOverride) { - const candidateProvider = storedProviderOverride || defaultProvider; - const normalizedStored = normalizeAgentCommandModelRef( - cfg, - candidateProvider, - storedModelOverride, - modelManifestContext, + defaultModel, + }); + if (repaired.updated) { + entryUpdated = true; + } + const overrideProvider = entry.providerOverride?.trim() || defaultProvider; + const overrideModel = entry.modelOverride?.trim(); + if (overrideModel) { + const normalizedOverride = normalizeAgentCommandModelRef( + cfg, + overrideProvider, + overrideModel, + modelManifestContext, + ); + const key = modelKey(normalizedOverride.provider, normalizedOverride.model); + if (!visibilityPolicy.allowsKey(key)) { + const { updated } = applyModelOverrideToSessionEntry({ + entry, + selection: { provider: defaultProvider, model: defaultModel, isDefault: true }, + }); + if (updated) { + entryUpdated = true; + } + } + } + if (entryUpdated) { + const persisted = await persistSessionEntry({ + sessionStore, + sessionKey, + storePath, + initialEntry, + entry, + }); + sessionEntry = persisted ?? sessionEntry; + const adoptedHasStoredOverride = Boolean( + sessionEntry.modelOverride || sessionEntry.providerOverride, + ); + storedModelOverrideSource = adoptedHasStoredOverride + ? sessionEntry.modelOverrideSource + : undefined; + hasStoredAutoFallbackProvenance = + adoptedHasStoredOverride && hasSessionAutoModelFallbackProvenance(sessionEntry); + hasLegacyAutoFallbackOverrideWithoutOrigin = + adoptedHasStoredOverride && hasLegacyAutoFallbackWithoutOrigin(sessionEntry); + } + } + + const storedProviderOverride = hasLegacyAutoFallbackOverrideWithoutOrigin + ? undefined + : sessionEntry?.providerOverride?.trim(); + let storedModelOverride = hasLegacyAutoFallbackOverrideWithoutOrigin + ? undefined + : sessionEntry?.modelOverride?.trim(); + const currentRunModelChannel = [ + runContext.messageChannel, + opts.replyChannel, + opts.channel, + ].find((channel): channel is string => + Boolean(channel && isDeliverableMessageChannel(channel)), ); - const key = modelKey(normalizedStored.provider, normalizedStored.model); - if (visibilityPolicy.allowsKey(key)) { - provider = normalizedStored.provider; - model = normalizedStored.model; - } - } - const autoFallbackPrimaryProbe = !hasExplicitRunOverride - ? resolveAutoFallbackPrimaryProbe({ - entry: sessionEntry, - sessionKey, - primaryProvider, - primaryModel, - }) - : undefined; - let autoFallbackPrimaryProbeSessionEntry: SessionEntry | undefined; - if (autoFallbackPrimaryProbe && sessionEntry) { - provider = autoFallbackPrimaryProbe.provider; - model = autoFallbackPrimaryProbe.model; - autoFallbackPrimaryProbeSessionEntry = { ...sessionEntry }; - clearAutoFallbackPrimaryProbeSelection(autoFallbackPrimaryProbeSessionEntry); - } - let providerForAuthProfileValidation = provider; - if (hasExplicitRunOverride) { - const explicitRef = explicitModelOverride - ? explicitProviderOverride - ? normalizeAgentCommandModelRef( + const channelOverrideGroupId = currentRunModelChannel + ? (runContext.groupId ?? sessionEntry?.groupId ?? runContext.currentChannelId) + : (sessionEntry?.groupId ?? runContext.groupId ?? runContext.currentChannelId); + const channelModelOverride = + cfg.channels?.modelByChannel && !hasExplicitRunOverride + ? resolveChannelModelOverride({ cfg, - explicitProviderOverride, - explicitModelOverride, - modelManifestContext, - ) - : parseAgentCommandModelRef(cfg, explicitModelOverride, provider, modelManifestContext) - : explicitProviderOverride - ? normalizeAgentCommandModelRef( - cfg, - explicitProviderOverride, - model, - modelManifestContext, - ) + channel: + currentRunModelChannel ?? + sessionEntry?.channel ?? + sessionEntry?.lastChannel ?? + sessionEntry?.origin?.provider, + groupId: channelOverrideGroupId, + groupChatType: sessionEntry?.chatType ?? sessionEntry?.origin?.chatType, + groupChannel: runContext.groupChannel ?? sessionEntry?.groupChannel, + groupSubject: sessionEntry?.subject, + parentSessionKey: sessionEntry?.parentSessionKey ?? sessionKey, + directUserIds: [ + sessionEntry?.origin?.nativeDirectUserId, + sessionEntry?.origin?.from, + sessionEntry?.origin?.to, + ], + }) : null; - if (!explicitRef) { - throw new Error("Invalid model override."); + const normalizedChannelOverride = channelModelOverride + ? parseAgentCommandModelRef( + cfg, + channelModelOverride.model, + defaultProvider, + modelManifestContext, + ) + : null; + const primaryProvider = normalizedChannelOverride?.provider ?? defaultProvider; + const primaryModel = normalizedChannelOverride?.model ?? defaultModel; + const hasEffectiveStoredOverride = Boolean(storedProviderOverride || storedModelOverride); + if (normalizedChannelOverride && !hasEffectiveStoredOverride) { + provider = normalizedChannelOverride.provider; + model = normalizedChannelOverride.model; } - const explicitKey = modelKey(explicitRef.provider, explicitRef.model); - if (!visibilityPolicy.allowsKey(explicitKey)) { + if (storedModelOverride) { + const candidateProvider = storedProviderOverride || defaultProvider; + const normalizedStored = normalizeAgentCommandModelRef( + cfg, + candidateProvider, + storedModelOverride, + modelManifestContext, + ); + const key = modelKey(normalizedStored.provider, normalizedStored.model); + if (visibilityPolicy.allowsKey(key)) { + provider = normalizedStored.provider; + model = normalizedStored.model; + } + } + const autoFallbackPrimaryProbe = !hasExplicitRunOverride + ? resolveAutoFallbackPrimaryProbe({ + entry: sessionEntry, + sessionKey, + primaryProvider, + primaryModel, + }) + : undefined; + let autoFallbackPrimaryProbeSessionEntry: SessionEntry | undefined; + if (autoFallbackPrimaryProbe && sessionEntry) { + provider = autoFallbackPrimaryProbe.provider; + model = autoFallbackPrimaryProbe.model; + autoFallbackPrimaryProbeSessionEntry = { ...sessionEntry }; + clearAutoFallbackPrimaryProbeSelection(autoFallbackPrimaryProbeSessionEntry); + } + let providerForAuthProfileValidation = provider; + if (hasExplicitRunOverride) { + const explicitRef = explicitModelOverride + ? explicitProviderOverride + ? normalizeAgentCommandModelRef( + cfg, + explicitProviderOverride, + explicitModelOverride, + modelManifestContext, + ) + : parseAgentCommandModelRef(cfg, explicitModelOverride, provider, modelManifestContext) + : explicitProviderOverride + ? normalizeAgentCommandModelRef( + cfg, + explicitProviderOverride, + model, + modelManifestContext, + ) + : null; + if (!explicitRef) { + throw new Error("Invalid model override."); + } + const explicitKey = modelKey(explicitRef.provider, explicitRef.model); + if (!visibilityPolicy.allowsKey(explicitKey)) { + throw new Error( + `Model override "${sanitizeForLog(explicitRef.provider)}/${sanitizeForLog(explicitRef.model)}" is not allowed for agent "${sessionAgentId}".`, + ); + } + provider = explicitRef.provider; + model = explicitRef.model; + } + const allowedInitialSelection = visibilityPolicy.resolveSelection({ + provider, + model, + }); + if (!allowedInitialSelection) { throw new Error( - `Model override "${sanitizeForLog(explicitRef.provider)}/${sanitizeForLog(explicitRef.model)}" is not allowed for agent "${sessionAgentId}".`, + `Configured default model "${modelKey(provider, model)}" is not allowed by agents.defaults.models, and no allowed model is available.`, ); } - provider = explicitRef.provider; - model = explicitRef.model; - } - const allowedInitialSelection = visibilityPolicy.resolveSelection({ - provider, - model, - }); - if (!allowedInitialSelection) { - throw new Error( - `Configured default model "${modelKey(provider, model)}" is not allowed by agents.defaults.models, and no allowed model is available.`, - ); - } - provider = allowedInitialSelection.provider; - model = allowedInitialSelection.model; - providerForAuthProfileValidation = provider; + provider = allowedInitialSelection.provider; + model = allowedInitialSelection.model; + providerForAuthProfileValidation = provider; - await ensureSelectedAgentHarnessPlugin({ - config: cfg, - provider, - modelId: model, - agentId: sessionAgentId, - sessionKey, - workspaceDir, - }); + await ensureSelectedAgentHarnessPlugin({ + config: cfg, + provider, + modelId: model, + agentId: sessionAgentId, + sessionKey, + workspaceDir, + }); - let sessionEntryForAttempt = autoFallbackPrimaryProbeSessionEntry ?? sessionEntry; - if (sessionEntryForAttempt) { - const authProfileId = sessionEntryForAttempt.authProfileOverride; - if (authProfileId) { - const entry = sessionEntryForAttempt; - const store = ensureAuthProfileStore(); - const profile = store.profiles[authProfileId]; - const validationHarnessPolicy = resolveAvailableAgentHarnessPolicy({ - provider: providerForAuthProfileValidation, - modelId: model, - config: cfg, - agentId: sessionAgentId, - sessionKey, - }); - const acceptedAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({ - provider: providerForAuthProfileValidation, - harnessRuntime: validationHarnessPolicy.runtime, - config: cfg, - }).map((candidateProvider) => - pluginsEnabled - ? resolveProviderIdForAuth(candidateProvider, { + let sessionEntryForAttempt = autoFallbackPrimaryProbeSessionEntry ?? sessionEntry; + if (sessionEntryForAttempt) { + const authProfileId = sessionEntryForAttempt.authProfileOverride; + if (authProfileId) { + const entry = sessionEntryForAttempt; + const store = ensureAuthProfileStore(); + const profile = store.profiles[authProfileId]; + const validationHarnessPolicy = resolveAvailableAgentHarnessPolicy({ + provider: providerForAuthProfileValidation, + modelId: model, + config: cfg, + agentId: sessionAgentId, + sessionKey, + }); + const acceptedAuthProviders = listOpenAIAuthProfileProvidersForAgentRuntime({ + provider: providerForAuthProfileValidation, + harnessRuntime: validationHarnessPolicy.runtime, + config: cfg, + }).map((candidateProvider) => + pluginsEnabled + ? resolveProviderIdForAuth(candidateProvider, { + config: cfg, + workspaceDir, + ...(manifestMetadataSnapshot + ? { metadataSnapshot: manifestMetadataSnapshot } + : {}), + }) + : candidateProvider, + ); + const authAliasLookupParams = pluginsEnabled + ? { config: cfg, workspaceDir, ...(manifestMetadataSnapshot ? { metadataSnapshot: manifestMetadataSnapshot } : {}), - }) - : candidateProvider, - ); - const authAliasLookupParams = pluginsEnabled - ? { - config: cfg, - workspaceDir, - ...(manifestMetadataSnapshot ? { metadataSnapshot: manifestMetadataSnapshot } : {}), + } + : { + config: cfg, + workspaceDir, + metadataSnapshot: { plugins: [] }, + }; + const profileMatchesRuntime = + profile && + acceptedAuthProviders.some((candidateProvider) => + isStoredCredentialCompatibleWithAuthProvider({ + cfg, + authAliasLookupParams, + provider: candidateProvider, + credential: profile, + }), + ); + if (!profileMatchesRuntime) { + if (hasExplicitRunOverride || autoFallbackPrimaryProbe) { + sessionEntryForAttempt = { + ...entry, + authProfileOverride: undefined, + authProfileOverrideSource: undefined, + authProfileOverrideCompactionCount: undefined, + }; + } else if (sessionStore && sessionKey && !suppressVisibleSessionEffects) { + await clearSessionAuthProfileOverride({ + sessionEntry: entry, + sessionStore, + sessionKey, + storePath, + }); } - : { - config: cfg, - workspaceDir, - metadataSnapshot: { plugins: [] }, - }; - const profileMatchesRuntime = - profile && - acceptedAuthProviders.some((candidateProvider) => - isStoredCredentialCompatibleWithAuthProvider({ - cfg, - authAliasLookupParams, - provider: candidateProvider, - credential: profile, - }), - ); - if (!profileMatchesRuntime) { - if (hasExplicitRunOverride || autoFallbackPrimaryProbe) { - sessionEntryForAttempt = { - ...entry, - authProfileOverride: undefined, - authProfileOverrideSource: undefined, - authProfileOverrideCompactionCount: undefined, - }; - } else if (sessionStore && sessionKey && !suppressVisibleSessionEffects) { - await clearSessionAuthProfileOverride({ - sessionEntry: entry, - sessionStore, - sessionKey, - storePath, - }); } } } - } - const catalogForThinking = - allowedModelCatalog.length > 0 - ? allowedModelCatalog - : modelCatalog && modelCatalog.length > 0 - ? modelCatalog - : configuredThinkingCatalog; - const thinkingCatalog = catalogForThinking.length > 0 ? catalogForThinking : undefined; - if (!resolvedThinkLevel) { - resolvedThinkLevel = - normalizeThinkLevel(resolveAgentConfig(cfg, sessionAgentId)?.thinkingDefault) ?? - resolveThinkingDefault({ - cfg, + const catalogForThinking = + allowedModelCatalog.length > 0 + ? allowedModelCatalog + : modelCatalog && modelCatalog.length > 0 + ? modelCatalog + : configuredThinkingCatalog; + const thinkingCatalog = catalogForThinking.length > 0 ? catalogForThinking : undefined; + if (!resolvedThinkLevel) { + resolvedThinkLevel = + normalizeThinkLevel(resolveAgentConfig(cfg, sessionAgentId)?.thinkingDefault) ?? + resolveThinkingDefault({ + cfg, + provider, + model, + catalog: thinkingCatalog, + }); + } + if ( + !isThinkingLevelSupported({ provider, model, + level: resolvedThinkLevel, + catalog: thinkingCatalog, + }) + ) { + const explicitThink = Boolean(thinkOnce || thinkOverride); + const isSubagentSpawnRun = isSubagentLane && isSubagentSessionKey(sessionKey); + // Spawn-lane subagents are fire-and-forget; the orchestrator already got + // an "accepted" ack, so throwing here strands the run and half-fails fan-outs. + // Clamp like the embedded runner; interactive --thinking keeps the throw. + if (explicitThink && !isSubagentSpawnRun) { + throw new Error( + `Thinking level "${resolvedThinkLevel}" is not supported for ${provider}/${model}. Use one of: ${formatThinkingLevels(provider, model, ", ", thinkingCatalog)}.`, + ); + } + const fallbackThinkLevel = resolveSupportedThinkingLevel({ + provider, + model, + level: resolvedThinkLevel, catalog: thinkingCatalog, }); - } - if ( - !isThinkingLevelSupported({ - provider, - model, - level: resolvedThinkLevel, - catalog: thinkingCatalog, - }) - ) { - const explicitThink = Boolean(thinkOnce || thinkOverride); - const isSubagentSpawnRun = isSubagentLane && isSubagentSessionKey(sessionKey); - // Spawn-lane subagents are fire-and-forget; the orchestrator already got - // an "accepted" ack, so throwing here strands the run and half-fails fan-outs. - // Clamp like the embedded runner; interactive --thinking keeps the throw. - if (explicitThink && !isSubagentSpawnRun) { - throw new Error( - `Thinking level "${resolvedThinkLevel}" is not supported for ${provider}/${model}. Use one of: ${formatThinkingLevels(provider, model, ", ", thinkingCatalog)}.`, - ); + if (fallbackThinkLevel !== resolvedThinkLevel) { + // Execution fallbacks are turn-local; directive/model persistence owns + // durable thinking remaps so explicit session overrides survive runs. + resolvedThinkLevel = fallbackThinkLevel; + } } - const fallbackThinkLevel = resolveSupportedThinkingLevel({ - provider, - model, - level: resolvedThinkLevel, - catalog: thinkingCatalog, - }); - if (fallbackThinkLevel !== resolvedThinkLevel) { - // Execution fallbacks are turn-local; directive/model persistence owns - // durable thinking remaps so explicit session overrides survive runs. - resolvedThinkLevel = fallbackThinkLevel; + const { resolveSessionTranscriptFile } = await loadTranscriptResolveRuntime(); + let sessionFile: string | undefined; + if (sessionStore && sessionKey) { + const resolvedSessionFile = await resolveSessionTranscriptFile({ + sessionId, + sessionKey, + sessionStore: suppressVisibleSessionEffects ? undefined : sessionStore, + storePath: suppressVisibleSessionEffects ? undefined : storePath, + sessionEntry, + agentId: sessionAgentId, + threadId: opts.threadId, + }); + sessionFile = resolvedSessionFile.sessionFile; + sessionEntry = resolvedSessionFile.sessionEntry; } - } - const { resolveSessionTranscriptFile } = await loadTranscriptResolveRuntime(); - let sessionFile: string | undefined; - if (sessionStore && sessionKey) { - const resolvedSessionFile = await resolveSessionTranscriptFile({ - sessionId, - sessionKey, - sessionStore: suppressVisibleSessionEffects ? undefined : sessionStore, - storePath: suppressVisibleSessionEffects ? undefined : storePath, - sessionEntry, - agentId: sessionAgentId, - threadId: opts.threadId, - }); - sessionFile = resolvedSessionFile.sessionFile; - sessionEntry = resolvedSessionFile.sessionEntry; - } - if (!sessionFile) { - const resolvedSessionFile = await resolveSessionTranscriptFile({ - sessionId, - sessionKey: sessionKey ?? sessionId, - storePath, - sessionEntry, - agentId: sessionAgentId, - threadId: opts.threadId, - }); - sessionFile = resolvedSessionFile.sessionFile; - sessionEntry = resolvedSessionFile.sessionEntry; - } - const attemptSessionFile = suppressVisibleSessionEffects - ? await prepareInternalSessionEffectsTranscript({ sessionFile, runId }) - : sessionFile; + if (!sessionFile) { + const resolvedSessionFile = await resolveSessionTranscriptFile({ + sessionId, + sessionKey: sessionKey ?? sessionId, + storePath, + sessionEntry, + agentId: sessionAgentId, + threadId: opts.threadId, + }); + sessionFile = resolvedSessionFile.sessionFile; + sessionEntry = resolvedSessionFile.sessionEntry; + } + const attemptSessionFile = suppressVisibleSessionEffects + ? await prepareInternalSessionEffectsTranscript({ sessionFile, runId }) + : sessionFile; - const startedAt = Date.now(); - const attemptLifecycleState: AgentAttemptLifecycleState = { - currentTurnUserMessagePersisted: false, - lifecycleFinishing: false, - lifecycleEnded: false, - }; - const attemptLifecycleCallbacks = createAgentAttemptLifecycleCallbacks(attemptLifecycleState); + const startedAt = Date.now(); + const attemptLifecycleState: AgentAttemptLifecycleState = { + currentTurnUserMessagePersisted: false, + lifecycleFinishing: false, + lifecycleEnded: false, + }; + const attemptLifecycleCallbacks = createAgentAttemptLifecycleCallbacks(attemptLifecycleState); const suppressUserTurnPersistence = opts.suppressPromptPersistence === true || opts.transcriptMessage === ""; const recorderTranscriptText = transcriptBody || undefined; @@ -1713,704 +1754,713 @@ async function agentCommandInternal( if (suppressUserTurnPersistence) { userTurnTranscriptRecorder.markBlocked(); } - let lifecycleFinishingEmitted = false; - const emitLifecycleFinishing = (runResult: AgentAttemptResult) => { - if ( - attemptLifecycleState.lifecycleEnded || - attemptLifecycleState.lifecycleFinishing || - lifecycleFinishingEmitted - ) { - return; - } - lifecycleFinishingEmitted = true; - attemptLifecycleState.lifecycleFinishing = true; - emitAgentEvent({ - runId, - lifecycleGeneration, - stream: "lifecycle", - data: { - phase: "finishing", - startedAt, - endedAt: Date.now(), - aborted: runResult.meta.aborted ?? false, - stopReason: runResult.meta.stopReason, - ...resolveAgentRunAbortLifecycleFields(opts.abortSignal), - }, - }); - }; - const emitLifecycleEnd = (runResult: AgentAttemptResult) => { - if (attemptLifecycleState.lifecycleEnded) { - return; - } - attemptLifecycleState.lifecycleEnded = true; - const stopReason = runResult.meta.stopReason; - if (stopReason && stopReason !== "end_turn") { - console.error(`[agent] run ${runId} ended with stopReason=${stopReason}`); - } - emitAgentEvent({ - runId, - lifecycleGeneration, - stream: "lifecycle", - data: { - phase: "end", - startedAt, - endedAt: Date.now(), - aborted: runResult.meta.aborted ?? false, - stopReason, - ...resolveAgentRunAbortLifecycleFields(opts.abortSignal), - }, - }); - }; - const resolveLifecycleResultError = ( - runResult: AgentAttemptResult, - includeErrorPayload: boolean, - ) => - attemptLifecycleState.lifecycleError ?? - (includeErrorPayload - ? runResult.payloads?.find( - (payload) => payload.isError === true && typeof payload.text === "string", - )?.text - : undefined) ?? - (runResult.meta.error ? "Agent run failed" : undefined); - const emitLifecycleResultError = ( - runResult: AgentAttemptResult, - fallbackExhausted: boolean, - ) => { - if (attemptLifecycleState.lifecycleEnded) { - return; - } - attemptLifecycleState.lifecycleEnded = true; - const error = - resolveLifecycleResultError(runResult, fallbackExhausted) ?? - (fallbackExhausted ? "All model fallback candidates failed" : "Agent run failed"); - emitAgentEvent({ - runId, - lifecycleGeneration, - stream: "lifecycle", - data: { - phase: "error", - startedAt, - endedAt: Date.now(), - error, - ...(runResult.meta.stopReason ? { stopReason: runResult.meta.stopReason } : {}), - ...(runResult.meta.livenessState ? { livenessState: runResult.meta.livenessState } : {}), - ...(runResult.meta.timeoutPhase ? { timeoutPhase: runResult.meta.timeoutPhase } : {}), - ...(typeof runResult.meta.providerStarted === "boolean" - ? { providerStarted: runResult.meta.providerStarted } - : {}), - ...(typeof runResult.meta.aborted === "boolean" - ? { aborted: runResult.meta.aborted } - : {}), - ...(runResult.meta.replayInvalid === true ? { replayInvalid: true } : {}), - ...(runResult.meta.yielded === true ? { yielded: true } : {}), - ...(fallbackExhausted ? { fallbackExhaustedFailure: true } : {}), - }, - }); - }; - const emitLifecyclePostTurnError = (error: unknown) => { - if (attemptLifecycleState.lifecycleEnded) { - return; - } - attemptLifecycleState.lifecycleEnded = true; - emitAgentEvent({ - runId, - lifecycleGeneration, - stream: "lifecycle", - data: { - phase: "error", - startedAt, - endedAt: Date.now(), - error: error instanceof Error ? error.message : "Agent run failed", - ...resolveAgentRunAbortLifecycleFields(opts.abortSignal), - }, - }); - }; - const attemptExecutionRuntime = await loadAttemptExecutionRuntime(); - const messageChannel = resolveMessageChannel( - runContext.messageChannel, - opts.replyChannel ?? opts.channel, - ); - - let result: AgentAttemptResult; - let fallbackProvider = provider; - let fallbackModel = model; - let fallbackExhausted = false; - const MAX_LIVE_SWITCH_RETRIES = 5; - let liveSwitchRetries = 0; - let autoFallbackPrimaryProbeInterruptedByLiveSwitch = false; - const fastModeStartedAtMs = Date.now(); - const fallbackTrajectoryRecorder = createTrajectoryRuntimeRecorder({ - cfg, - runId, - sessionId, - sessionKey, - sessionFile, - provider, - modelId: model, - workspaceDir, - }); - for (;;) { - try { - const spawnedBy = normalizedSpawned.spawnedBy ?? sessionEntry?.spawnedBy; - const effectiveFallbacksOverride = resolveEffectiveModelFallbacks({ - cfg, - agentId: sessionAgentId, - sessionKey, - hasSessionModelOverride: - hasExplicitRunOverride || Boolean(storedProviderOverride || storedModelOverride), - modelOverrideSource: hasExplicitRunOverride ? "user" : storedModelOverrideSource, - hasAutoFallbackProvenance: hasExplicitRunOverride - ? false - : hasStoredAutoFallbackProvenance, - }); - - let fallbackAttemptIndex = 0; - const fallbackRuntimeState: { originRuntime?: "cli" | "embedded" } = {}; - attemptLifecycleState.currentTurnUserMessagePersisted = false; - const fallbackResult = await runWithModelFallback({ - cfg, - provider, - model, - ...modelManifestContext, + let lifecycleFinishingEmitted = false; + const emitLifecycleFinishing = (runResult: AgentAttemptResult) => { + if ( + attemptLifecycleState.lifecycleEnded || + attemptLifecycleState.lifecycleFinishing || + lifecycleFinishingEmitted + ) { + return; + } + lifecycleFinishingEmitted = true; + attemptLifecycleState.lifecycleFinishing = true; + emitAgentEvent({ runId, - agentDir, - agentId: sessionAgentId, - sessionId, - sessionKey: sessionKey ?? sessionId, - prepareAgentHarnessRuntime: async ({ - provider: providerValue, - model: modelValue, - agentHarnessRuntimeOverride, - }) => { - await ensureSelectedAgentHarnessPlugin({ - config: cfg, + lifecycleGeneration, + stream: "lifecycle", + data: { + phase: "finishing", + startedAt, + endedAt: Date.now(), + aborted: runResult.meta.aborted ?? false, + stopReason: runResult.meta.stopReason, + ...resolveAgentRunAbortLifecycleFields(opts.abortSignal), + }, + }); + }; + const emitLifecycleEnd = (runResult: AgentAttemptResult) => { + if (attemptLifecycleState.lifecycleEnded) { + return; + } + attemptLifecycleState.lifecycleEnded = true; + const stopReason = runResult.meta.stopReason; + if (stopReason && stopReason !== "end_turn") { + console.error(`[agent] run ${runId} ended with stopReason=${stopReason}`); + } + emitAgentEvent({ + runId, + lifecycleGeneration, + stream: "lifecycle", + data: { + phase: "end", + startedAt, + endedAt: Date.now(), + aborted: runResult.meta.aborted ?? false, + stopReason, + ...resolveAgentRunAbortLifecycleFields(opts.abortSignal), + }, + }); + }; + const resolveLifecycleResultError = ( + runResult: AgentAttemptResult, + includeErrorPayload: boolean, + ) => + attemptLifecycleState.lifecycleError ?? + (includeErrorPayload + ? runResult.payloads?.find( + (payload) => payload.isError === true && typeof payload.text === "string", + )?.text + : undefined) ?? + (runResult.meta.error ? "Agent run failed" : undefined); + const emitLifecycleResultError = ( + runResult: AgentAttemptResult, + fallbackExhausted: boolean, + ) => { + if (attemptLifecycleState.lifecycleEnded) { + return; + } + attemptLifecycleState.lifecycleEnded = true; + const error = + resolveLifecycleResultError(runResult, fallbackExhausted) ?? + (fallbackExhausted ? "All model fallback candidates failed" : "Agent run failed"); + emitAgentEvent({ + runId, + lifecycleGeneration, + stream: "lifecycle", + data: { + phase: "error", + startedAt, + endedAt: Date.now(), + error, + ...(runResult.meta.stopReason ? { stopReason: runResult.meta.stopReason } : {}), + ...(runResult.meta.livenessState + ? { livenessState: runResult.meta.livenessState } + : {}), + ...(runResult.meta.timeoutPhase ? { timeoutPhase: runResult.meta.timeoutPhase } : {}), + ...(typeof runResult.meta.providerStarted === "boolean" + ? { providerStarted: runResult.meta.providerStarted } + : {}), + ...(typeof runResult.meta.aborted === "boolean" + ? { aborted: runResult.meta.aborted } + : {}), + ...(runResult.meta.replayInvalid === true ? { replayInvalid: true } : {}), + ...(runResult.meta.yielded === true ? { yielded: true } : {}), + ...(fallbackExhausted ? { fallbackExhaustedFailure: true } : {}), + }, + }); + }; + const emitLifecyclePostTurnError = (error: unknown) => { + if (attemptLifecycleState.lifecycleEnded) { + return; + } + attemptLifecycleState.lifecycleEnded = true; + emitAgentEvent({ + runId, + lifecycleGeneration, + stream: "lifecycle", + data: { + phase: "error", + startedAt, + endedAt: Date.now(), + error: error instanceof Error ? error.message : "Agent run failed", + ...resolveAgentRunAbortLifecycleFields(opts.abortSignal), + }, + }); + }; + const attemptExecutionRuntime = await loadAttemptExecutionRuntime(); + const messageChannel = resolveMessageChannel( + runContext.messageChannel, + opts.replyChannel ?? opts.channel, + ); + + let result: AgentAttemptResult; + let fallbackProvider = provider; + let fallbackModel = model; + let fallbackExhausted = false; + const MAX_LIVE_SWITCH_RETRIES = 5; + let liveSwitchRetries = 0; + let autoFallbackPrimaryProbeInterruptedByLiveSwitch = false; + const fastModeStartedAtMs = Date.now(); + const fallbackTrajectoryRecorder = createTrajectoryRuntimeRecorder({ + cfg, + runId, + sessionId, + sessionKey, + sessionFile, + provider, + modelId: model, + workspaceDir, + }); + for (;;) { + try { + const spawnedBy = normalizedSpawned.spawnedBy ?? sessionEntry?.spawnedBy; + const effectiveFallbacksOverride = resolveEffectiveModelFallbacks({ + cfg, + agentId: sessionAgentId, + sessionKey, + hasSessionModelOverride: + hasExplicitRunOverride || Boolean(storedProviderOverride || storedModelOverride), + modelOverrideSource: hasExplicitRunOverride ? "user" : storedModelOverrideSource, + hasAutoFallbackProvenance: hasExplicitRunOverride + ? false + : hasStoredAutoFallbackProvenance, + }); + + let fallbackAttemptIndex = 0; + const fallbackRuntimeState: { originRuntime?: "cli" | "embedded" } = {}; + attemptLifecycleState.currentTurnUserMessagePersisted = false; + const fallbackResult = await runWithModelFallback({ + cfg, + provider, + model, + ...modelManifestContext, + runId, + agentDir, + agentId: sessionAgentId, + sessionId, + sessionKey: sessionKey ?? sessionId, + prepareAgentHarnessRuntime: async ({ provider: providerValue, - modelId: modelValue, - agentId: sessionAgentId, - sessionKey, + model: modelValue, agentHarnessRuntimeOverride, - workspaceDir, - }); - }, - fallbacksOverride: effectiveFallbacksOverride, - onFallbackStep: (step) => { - fallbackTrajectoryRecorder?.recordEvent("model.fallback_step", step); - }, - classifyResult: ({ provider: providerLocal, model: modelLocal, result: resultLocal }) => - classifyEmbeddedAgentRunResultForModelFallback({ - provider: providerLocal, - model: modelLocal, - result: resultLocal, - }), - mergeExhaustedResult: mergeEmbeddedAgentRunResultForModelFallbackExhaustion, - abortSignal: opts.abortSignal, - run: async (providerOverride, modelOverride, runOptions) => { - attemptLifecycleState.lifecycleError = undefined; - attemptLifecycleState.lifecycleFinishing = false; - attemptLifecycleState.lifecycleEnded = false; - const isAutoFallbackPrimaryProbeCandidate = - autoFallbackPrimaryProbe && - providerOverride === autoFallbackPrimaryProbe.provider && - modelOverride === autoFallbackPrimaryProbe.model; - const attemptSessionEntry = - autoFallbackPrimaryProbe && - providerOverride === autoFallbackPrimaryProbe.fallbackProvider && - !isAutoFallbackPrimaryProbeCandidate - ? sessionEntry - : sessionEntryForAttempt; - if (isAutoFallbackPrimaryProbeCandidate) { - markAutoFallbackPrimaryProbe({ - probe: autoFallbackPrimaryProbe, + }) => { + await ensureSelectedAgentHarnessPlugin({ + config: cfg, + provider: providerValue, + modelId: modelValue, + agentId: sessionAgentId, sessionKey, + agentHarnessRuntimeOverride, + workspaceDir, }); + }, + fallbacksOverride: effectiveFallbacksOverride, + onFallbackStep: (step) => { + fallbackTrajectoryRecorder?.recordEvent("model.fallback_step", step); + }, + classifyResult: ({ provider: providerLocal, model: modelLocal, result: resultLocal }) => + classifyEmbeddedAgentRunResultForModelFallback({ + provider: providerLocal, + model: modelLocal, + result: resultLocal, + }), + mergeExhaustedResult: mergeEmbeddedAgentRunResultForModelFallbackExhaustion, + abortSignal: opts.abortSignal, + run: async (providerOverride, modelOverride, runOptions) => { + attemptLifecycleState.lifecycleError = undefined; + attemptLifecycleState.lifecycleFinishing = false; + attemptLifecycleState.lifecycleEnded = false; + const isAutoFallbackPrimaryProbeCandidate = + autoFallbackPrimaryProbe && + providerOverride === autoFallbackPrimaryProbe.provider && + modelOverride === autoFallbackPrimaryProbe.model; + const attemptSessionEntry = + autoFallbackPrimaryProbe && + providerOverride === autoFallbackPrimaryProbe.fallbackProvider && + !isAutoFallbackPrimaryProbeCandidate + ? sessionEntry + : sessionEntryForAttempt; + if (isAutoFallbackPrimaryProbeCandidate) { + markAutoFallbackPrimaryProbe({ + probe: autoFallbackPrimaryProbe, + sessionKey, + }); + } + const isFallbackRetry = fallbackAttemptIndex > 0; + fallbackAttemptIndex += 1; + opts.onActiveModelSelected?.({ + provider: providerOverride, + model: modelOverride, + }); + const fastModeState = resolveFastModeState({ + cfg, + provider: providerOverride, + model: modelOverride, + agentId: sessionAgentId, + sessionEntry, + }); + const fastMode = opts.fastMode ?? fastModeState.mode; + return attemptExecutionRuntime.runAgentAttempt({ + providerOverride, + modelOverride, + modelFallbacksOverride: effectiveFallbacksOverride, + originalProvider: provider, + cfg, + sessionEntry: attemptSessionEntry, + sessionId, + sessionKey, + sessionAgentId, + sessionFile: attemptSessionFile, + workspaceDir, + cwd, + body, + transcriptBody, + isFallbackRetry, + resolvedThinkLevel, + fastMode, + fastModeStartedAtMs, + fastModeAutoOnSeconds: + fastMode === "auto" + ? (opts.fastModeAutoOnSeconds ?? fastModeState.fastAutoOnSeconds) + : fastModeState.fastAutoOnSeconds, + isFinalFallbackAttempt: runOptions?.isFinalFallbackAttempt, + timeoutMs, + runTimeoutOverrideMs, + runId, + lifecycleGeneration, + opts, + runContext, + spawnedBy, + messageChannel, + skillsSnapshot, + resolvedVerboseLevel, + agentDir, + authProfileProvider: providerForAuthProfileValidation, + sessionStore: suppressVisibleSessionEffects ? undefined : sessionStore, + storePath: suppressVisibleSessionEffects ? undefined : storePath, + pluginsEnabled, + ...(manifestMetadataSnapshot ? { metadataSnapshot: manifestMetadataSnapshot } : {}), + allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe, + sessionHasHistory: + !isNewSession || + (await attemptExecutionRuntime.sessionFileHasContent(attemptSessionFile)), + fallbackRuntimeState, + suppressPromptPersistenceOnRetry: + suppressUserTurnPersistence || + userTurnTranscriptRecorder.hasPersisted() || + userTurnTranscriptRecorder.isBlocked() || + (isFallbackRetry && attemptLifecycleState.currentTurnUserMessagePersisted), + userTurnTranscriptRecorder, + onUserMessagePersisted: attemptLifecycleCallbacks.onUserMessagePersisted, + onLifecycleGenerationChanged: (nextLifecycleGeneration) => { + lifecycleGeneration = nextLifecycleGeneration; + }, + onAgentEvent: attemptLifecycleCallbacks.onAgentEvent, + deferTerminalLifecycle: true, + }); + }, + }); + result = applyAgentRunAbortMetadata(fallbackResult.result, opts.abortSignal); + if (isAgentRunRestartAbortReason(opts.abortSignal?.reason)) { + throw opts.abortSignal?.reason; + } + fallbackProvider = fallbackResult.provider; + fallbackModel = fallbackResult.model; + fallbackExhausted = fallbackResult.outcome === "exhausted"; + if ( + !fallbackExhausted && + autoFallbackPrimaryProbe && + !autoFallbackPrimaryProbeInterruptedByLiveSwitch && + sessionEntry && + sessionStore && + sessionKey && + !suppressVisibleSessionEffects && + !preserveUserFacingSessionModelState && + entryMatchesAutoFallbackPrimaryProbe(sessionEntry, autoFallbackPrimaryProbe) + ) { + const nextSessionEntry = { ...sessionEntry }; + if ( + fallbackProvider === autoFallbackPrimaryProbe.provider && + fallbackModel === autoFallbackPrimaryProbe.model + ) { + clearAutoFallbackPrimaryProbeSelection(nextSessionEntry); + } else { + nextSessionEntry.providerOverride = fallbackProvider; + nextSessionEntry.modelOverride = fallbackModel; + nextSessionEntry.modelOverrideSource = "auto"; + nextSessionEntry.modelOverrideFallbackOriginProvider = + autoFallbackPrimaryProbe.provider; + nextSessionEntry.modelOverrideFallbackOriginModel = autoFallbackPrimaryProbe.model; + if ( + nextSessionEntry.authProfileOverrideSource === "auto" && + fallbackProvider !== autoFallbackPrimaryProbe.fallbackProvider + ) { + delete nextSessionEntry.authProfileOverride; + delete nextSessionEntry.authProfileOverrideSource; + delete nextSessionEntry.authProfileOverrideCompactionCount; + } + nextSessionEntry.updatedAt = Date.now(); } - const isFallbackRetry = fallbackAttemptIndex > 0; - fallbackAttemptIndex += 1; - opts.onActiveModelSelected?.({ - provider: providerOverride, - model: modelOverride, - }); - const fastModeState = resolveFastModeState({ - cfg, - provider: providerOverride, - model: modelOverride, - agentId: sessionAgentId, - sessionEntry, - }); - const fastMode = opts.fastMode ?? fastModeState.mode; - return attemptExecutionRuntime.runAgentAttempt({ - providerOverride, - modelOverride, - modelFallbacksOverride: effectiveFallbacksOverride, - originalProvider: provider, - cfg, - sessionEntry: attemptSessionEntry, - sessionId, + const persistedEntry = await persistSessionEntry({ + sessionStore, sessionKey, - sessionAgentId, - sessionFile: attemptSessionFile, - workspaceDir, - cwd, - body, - transcriptBody, - isFallbackRetry, - resolvedThinkLevel, - fastMode, - fastModeStartedAtMs, - fastModeAutoOnSeconds: - fastMode === "auto" - ? (opts.fastModeAutoOnSeconds ?? fastModeState.fastAutoOnSeconds) - : fastModeState.fastAutoOnSeconds, - isFinalFallbackAttempt: runOptions?.isFinalFallbackAttempt, - timeoutMs, - runTimeoutOverrideMs, + storePath, + initialEntry: sessionEntry, + entry: nextSessionEntry, + shouldPersist: (current) => + Boolean( + current && + entryMatchesAutoFallbackPrimaryProbe(current, autoFallbackPrimaryProbe), + ), + }); + sessionEntry = persistedEntry ?? sessionEntry; + } + if (fallbackResult.attempts.length > 0 && result.meta.agentMeta) { + result = { + ...result, + meta: { + ...result.meta, + agentMeta: { + ...result.meta.agentMeta, + fallbackAttempts: fallbackResult.attempts, + }, + }, + }; + } + if (!fallbackExhausted) { + emitLifecycleFinishing(result); + } + break; + } catch (err) { + if (err instanceof LiveSessionModelSwitchError) { + liveSwitchRetries++; + if (liveSwitchRetries > MAX_LIVE_SWITCH_RETRIES) { + log.error( + `Live session model switch in subagent run ${runId}: exceeded maximum retries (${MAX_LIVE_SWITCH_RETRIES})`, + ); + if (!attemptLifecycleState.lifecycleEnded) { + emitAgentEvent({ + runId, + lifecycleGeneration, + stream: "lifecycle", + data: { + phase: "error", + startedAt, + endedAt: Date.now(), + error: "Agent run failed", + }, + }); + } + await fallbackTrajectoryRecorder?.flush(); + throw new Error( + `Exceeded maximum live model switch retries (${MAX_LIVE_SWITCH_RETRIES})`, + { cause: err }, + ); + } + const switchRef = normalizeAgentCommandModelRef( + cfg, + err.provider, + err.model, + modelManifestContext, + ); + const switchKey = modelKey(switchRef.provider, switchRef.model); + if (!visibilityPolicy.allowsKey(switchKey)) { + log.info( + `Live session model switch in subagent run ${runId}: ` + + `rejected ${sanitizeForLog(err.provider)}/${sanitizeForLog(err.model)} (not in allowlist)`, + ); + if (!attemptLifecycleState.lifecycleEnded) { + emitAgentEvent({ + runId, + lifecycleGeneration, + stream: "lifecycle", + data: { + phase: "error", + startedAt, + endedAt: Date.now(), + error: "Agent run failed", + }, + }); + } + await fallbackTrajectoryRecorder?.flush(); + throw new Error( + `Live model switch rejected: ${sanitizeForLog(err.provider)}/${sanitizeForLog(err.model)} is not in the agent allowlist`, + { cause: err }, + ); + } + const previousProvider = provider; + const previousModel = model; + if (autoFallbackPrimaryProbe) { + autoFallbackPrimaryProbeInterruptedByLiveSwitch = true; + } + provider = err.provider; + model = err.model; + fallbackProvider = err.provider; + fallbackModel = err.model; + providerForAuthProfileValidation = err.provider; + if (sessionEntry) { + sessionEntry = { ...sessionEntry }; + sessionEntry.authProfileOverride = err.authProfileId; + sessionEntry.authProfileOverrideSource = err.authProfileId + ? err.authProfileIdSource + : undefined; + sessionEntry.authProfileOverrideCompactionCount = undefined; + } + if ( + storedModelOverride || + err.model !== previousModel || + err.provider !== previousProvider + ) { + storedModelOverride = err.model; + storedModelOverrideSource = "user"; + } + attemptLifecycleState.lifecycleEnded = false; + log.info( + `Live session model switch in subagent run ${runId}: switching to ${sanitizeForLog(err.provider)}/${sanitizeForLog(err.model)}`, + ); + continue; + } + if (!attemptLifecycleState.lifecycleEnded) { + const abortLifecycleFields = isAgentRunDirectAbortReason(err) + ? { aborted: true as const, stopReason: "aborted" as const } + : resolveAgentRunAbortLifecycleFields(opts.abortSignal); + emitAgentEvent({ runId, lifecycleGeneration, - opts, - runContext, - spawnedBy, - messageChannel, - skillsSnapshot, - resolvedVerboseLevel, - agentDir, - authProfileProvider: providerForAuthProfileValidation, + stream: "lifecycle", + data: { + phase: "error", + startedAt, + endedAt: Date.now(), + error: err instanceof Error ? err.message : "Agent run failed", + ...abortLifecycleFields, + }, + }); + } + await fallbackTrajectoryRecorder?.flush(); + throw err; + } + } + try { + await fallbackTrajectoryRecorder?.flush(); + + const rotatedSessionFile = result.meta.agentMeta?.sessionFile; + const effectiveSessionId = rotatedSessionFile + ? (result.meta.agentMeta?.sessionId ?? sessionId) + : sessionId; + const effectiveSessionFile = rotatedSessionFile ?? attemptSessionFile; + + // Update token+model fields in the session store. + if (sessionStore && sessionKey && !suppressVisibleSessionEffects) { + const isHeartbeatLifecycleRun = isHeartbeatLifecycleRunKind(opts.bootstrapContextRunKind); + const { updateSessionStoreAfterAgentRun } = await loadSessionStoreRuntime(); + await updateSessionStoreAfterAgentRun({ + cfg, + contextTokensOverride: agentCfg?.contextTokens, + sessionId: effectiveSessionId, + sessionKey, + storePath, + sessionStore, + defaultProvider: provider, + defaultModel: model, + fallbackProvider, + fallbackModel, + result, + touchInteraction: + opts.bootstrapContextRunKind !== "cron" && + !isHeartbeatLifecycleRun && + !opts.internalEvents?.length, + preserveRuntimeModel: + fallbackExhausted || isHeartbeatLifecycleRun || preserveUserFacingSessionModelState, + preserveUserFacingSessionModelState, + }); + sessionEntry = sessionStore[sessionKey] ?? sessionEntry; + } + + const transcriptPersistenceRunner = result.meta.executionTrace?.runner; + const embeddedAssistantGapFill = + transcriptPersistenceRunner === "embedded" || + (transcriptPersistenceRunner === undefined && + Boolean(result.meta.finalAssistantVisibleText?.trim())); + if ( + !sessionReboundDuringRun && + (transcriptPersistenceRunner === "cli" || embeddedAssistantGapFill) + ) { + let persistedCliTurnTranscript = false; + try { + const transcriptSessionEntry: SessionEntry | undefined = suppressVisibleSessionEffects + ? { + ...(sessionEntry ?? { + sessionId: effectiveSessionId, + updatedAt: Date.now(), + sessionStartedAt: Date.now(), + }), + sessionId: effectiveSessionId, + sessionFile: effectiveSessionFile, + } + : sessionEntry; + const transcriptResult = await attemptExecutionRuntime.persistCliTurnTranscript({ + body, + transcriptBody, + result, + sessionId: effectiveSessionId, + sessionKey: sessionKey ?? effectiveSessionId, + sessionEntry: transcriptSessionEntry, sessionStore: suppressVisibleSessionEffects ? undefined : sessionStore, storePath: suppressVisibleSessionEffects ? undefined : storePath, - pluginsEnabled, - ...(manifestMetadataSnapshot ? { metadataSnapshot: manifestMetadataSnapshot } : {}), - allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe, - sessionHasHistory: - !isNewSession || - (await attemptExecutionRuntime.sessionFileHasContent(attemptSessionFile)), - fallbackRuntimeState, - suppressPromptPersistenceOnRetry: + sessionAgentId, + threadId: opts.threadId, + sessionCwd: effectiveCwd, + config: cfg, + embeddedAssistantGapFill, + skipUserTurn: suppressUserTurnPersistence || userTurnTranscriptRecorder.hasPersisted() || - userTurnTranscriptRecorder.isBlocked() || - (isFallbackRetry && attemptLifecycleState.currentTurnUserMessagePersisted), - userTurnTranscriptRecorder, - onUserMessagePersisted: attemptLifecycleCallbacks.onUserMessagePersisted, - onLifecycleGenerationChanged: (nextLifecycleGeneration) => { - lifecycleGeneration = nextLifecycleGeneration; - }, - onAgentEvent: attemptLifecycleCallbacks.onAgentEvent, - deferTerminalLifecycle: true, + userTurnTranscriptRecorder.isBlocked(), }); - }, - }); - result = applyAgentRunAbortMetadata(fallbackResult.result, opts.abortSignal); - if (isAgentRunRestartAbortReason(opts.abortSignal?.reason)) { - throw opts.abortSignal?.reason; + sessionEntry = transcriptResult.sessionEntry; + sessionReboundDuringRun = transcriptResult.kind === "session-rebound"; + if (suppressVisibleSessionEffects) { + sessionEntry = prepared.sessionEntry; + } + persistedCliTurnTranscript = transcriptResult.kind === "persisted"; + } catch (error) { + log.warn( + `Turn transcript persistence failed for ${sessionKey ?? sessionId}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + if (persistedCliTurnTranscript && !suppressVisibleSessionEffects) { + sessionEntry = await ( + await loadCliCompactionRuntime() + ).runCliTurnCompactionLifecycle({ + cfg, + sessionId: effectiveSessionId, + sessionKey: sessionKey ?? effectiveSessionId, + sessionEntry, + sessionStore, + storePath, + sessionAgentId, + workspaceDir, + cwd: effectiveCwd, + agentDir, + provider: result.meta.agentMeta?.provider ?? provider, + model: result.meta.agentMeta?.model ?? model, + skillsSnapshot, + messageChannel, + agentAccountId: runContext.accountId, + senderIsOwner: opts.senderIsOwner, + thinkLevel: resolvedThinkLevel, + extraSystemPrompt: opts.extraSystemPrompt, + }); + } } - fallbackProvider = fallbackResult.provider; - fallbackModel = fallbackResult.model; - fallbackExhausted = fallbackResult.outcome === "exhausted"; + + const payloads = result.payloads ?? []; + let pendingFinalDeliveryTextForThisRun: string | undefined; + + // Phase 2: Persist pending final delivery for main sessions before attempting delivery. + // This ensures that if the process restarts during delivery, the payload is durable. if ( - !fallbackExhausted && - autoFallbackPrimaryProbe && - !autoFallbackPrimaryProbeInterruptedByLiveSwitch && - sessionEntry && + opts.deliver === true && sessionStore && sessionKey && !suppressVisibleSessionEffects && - !preserveUserFacingSessionModelState && - entryMatchesAutoFallbackPrimaryProbe(sessionEntry, autoFallbackPrimaryProbe) + !sessionReboundDuringRun && + payloads.length > 0 && + !isSubagentSessionKey(sessionKey) ) { - const nextSessionEntry = { ...sessionEntry }; - if ( - fallbackProvider === autoFallbackPrimaryProbe.provider && - fallbackModel === autoFallbackPrimaryProbe.model - ) { - clearAutoFallbackPrimaryProbeSelection(nextSessionEntry); - } else { - nextSessionEntry.providerOverride = fallbackProvider; - nextSessionEntry.modelOverride = fallbackModel; - nextSessionEntry.modelOverrideSource = "auto"; - nextSessionEntry.modelOverrideFallbackOriginProvider = - autoFallbackPrimaryProbe.provider; - nextSessionEntry.modelOverrideFallbackOriginModel = autoFallbackPrimaryProbe.model; - if ( - nextSessionEntry.authProfileOverrideSource === "auto" && - fallbackProvider !== autoFallbackPrimaryProbe.fallbackProvider - ) { - delete nextSessionEntry.authProfileOverride; - delete nextSessionEntry.authProfileOverrideSource; - delete nextSessionEntry.authProfileOverrideCompactionCount; - } - nextSessionEntry.updatedAt = Date.now(); - } - const persistedEntry = await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry: nextSessionEntry, - shouldPersist: (current) => - Boolean( - current && entryMatchesAutoFallbackPrimaryProbe(current, autoFallbackPrimaryProbe), - ), - }); - sessionEntry = persistedEntry ?? sessionEntry; - } - if (fallbackResult.attempts.length > 0 && result.meta.agentMeta) { - result = { - ...result, - meta: { - ...result.meta, - agentMeta: { - ...result.meta.agentMeta, - fallbackAttempts: fallbackResult.attempts, - }, - }, - }; - } - if (!fallbackExhausted) { - emitLifecycleFinishing(result); - } - break; - } catch (err) { - if (err instanceof LiveSessionModelSwitchError) { - liveSwitchRetries++; - if (liveSwitchRetries > MAX_LIVE_SWITCH_RETRIES) { - log.error( - `Live session model switch in subagent run ${runId}: exceeded maximum retries (${MAX_LIVE_SWITCH_RETRIES})`, - ); - if (!attemptLifecycleState.lifecycleEnded) { - emitAgentEvent({ - runId, - lifecycleGeneration, - stream: "lifecycle", - data: { - phase: "error", - startedAt, - endedAt: Date.now(), - error: "Agent run failed", - }, - }); - } - await fallbackTrajectoryRecorder?.flush(); - throw new Error( - `Exceeded maximum live model switch retries (${MAX_LIVE_SWITCH_RETRIES})`, - { cause: err }, - ); - } - const switchRef = normalizeAgentCommandModelRef( - cfg, - err.provider, - err.model, - modelManifestContext, + const now = Date.now(); + const combinedPayload = sanitizePendingFinalDeliveryText( + payloads + .map((p) => (typeof p.text === "string" ? p.text : "")) + .filter(Boolean) + .join("\n\n"), ); - const switchKey = modelKey(switchRef.provider, switchRef.model); - if (!visibilityPolicy.allowsKey(switchKey)) { - log.info( - `Live session model switch in subagent run ${runId}: ` + - `rejected ${sanitizeForLog(err.provider)}/${sanitizeForLog(err.model)} (not in allowlist)`, - ); - if (!attemptLifecycleState.lifecycleEnded) { - emitAgentEvent({ - runId, - lifecycleGeneration, - stream: "lifecycle", - data: { - phase: "error", - startedAt, - endedAt: Date.now(), - error: "Agent run failed", - }, - }); - } - await fallbackTrajectoryRecorder?.flush(); - throw new Error( - `Live model switch rejected: ${sanitizeForLog(err.provider)}/${sanitizeForLog(err.model)} is not in the agent allowlist`, - { cause: err }, - ); - } - const previousProvider = provider; - const previousModel = model; - if (autoFallbackPrimaryProbe) { - autoFallbackPrimaryProbeInterruptedByLiveSwitch = true; - } - provider = err.provider; - model = err.model; - fallbackProvider = err.provider; - fallbackModel = err.model; - providerForAuthProfileValidation = err.provider; - if (sessionEntry) { - sessionEntry = { ...sessionEntry }; - sessionEntry.authProfileOverride = err.authProfileId; - sessionEntry.authProfileOverrideSource = err.authProfileId - ? err.authProfileIdSource - : undefined; - sessionEntry.authProfileOverrideCompactionCount = undefined; - } - if ( - storedModelOverride || - err.model !== previousModel || - err.provider !== previousProvider - ) { - storedModelOverride = err.model; - storedModelOverrideSource = "user"; - } - attemptLifecycleState.lifecycleEnded = false; - log.info( - `Live session model switch in subagent run ${runId}: switching to ${sanitizeForLog(err.provider)}/${sanitizeForLog(err.model)}`, - ); - continue; - } - if (!attemptLifecycleState.lifecycleEnded) { - const abortLifecycleFields = isAgentRunDirectAbortReason(err) - ? { aborted: true as const, stopReason: "aborted" as const } - : resolveAgentRunAbortLifecycleFields(opts.abortSignal); - emitAgentEvent({ - runId, - lifecycleGeneration, - stream: "lifecycle", - data: { - phase: "error", - startedAt, - endedAt: Date.now(), - error: err instanceof Error ? err.message : "Agent run failed", - ...abortLifecycleFields, - }, - }); - } - await fallbackTrajectoryRecorder?.flush(); - throw err; - } - } - try { - await fallbackTrajectoryRecorder?.flush(); + pendingFinalDeliveryTextForThisRun = combinedPayload || undefined; - const rotatedSessionFile = result.meta.agentMeta?.sessionFile; - const effectiveSessionId = rotatedSessionFile - ? (result.meta.agentMeta?.sessionId ?? sessionId) - : sessionId; - const effectiveSessionFile = rotatedSessionFile ?? attemptSessionFile; + if (combinedPayload) { + const entry = sessionStore[sessionKey] ?? sessionEntry; + const next: SessionEntry = { + ...entry, + pendingFinalDelivery: true, + pendingFinalDeliveryText: combinedPayload, + pendingFinalDeliveryContext: currentRunDeliveryContext, + pendingFinalDeliveryCreatedAt: now, + updatedAt: now, + }; + const persisted = await persistSessionEntry({ + sessionStore, + sessionKey, + storePath, + initialEntry: entry, + entry: next, + shouldPersist: (current) => shouldPersistCurrentRunSessionCleanup(current, sessionId), + }); + sessionEntry = persisted ?? sessionEntry; + } + } - // Update token+model fields in the session store. - if (sessionStore && sessionKey && !suppressVisibleSessionEffects) { - const isHeartbeatLifecycleRun = isHeartbeatLifecycleRunKind(opts.bootstrapContextRunKind); - const { updateSessionStoreAfterAgentRun } = await loadSessionStoreRuntime(); - await updateSessionStoreAfterAgentRun({ + const { deliverAgentCommandResult } = await loadDeliveryRuntime(); + const resolveFreshSessionEntryForDelivery = + sessionStore && sessionKey && !suppressVisibleSessionEffects + ? async (): Promise => { + const { loadSessionStore } = await loadSessionStoreRuntime(); + const freshStore = loadSessionStore(storePath, { + skipCache: true, + clone: false, + }); + const freshEntry = freshStore[sessionKey]; + if (!freshEntry || freshEntry.sessionId !== effectiveSessionId) { + return undefined; + } + sessionStore[sessionKey] = freshEntry; + return freshEntry; + } + : undefined; + const deliveryParams = { cfg, - contextTokensOverride: agentCfg?.contextTokens, - sessionId: effectiveSessionId, - sessionKey, - storePath, - sessionStore, - defaultProvider: provider, - defaultModel: model, - fallbackProvider, - fallbackModel, + deps: resolvedDeps, + runtime, + opts, + outboundSession, + sessionEntry, result, - touchInteraction: - opts.bootstrapContextRunKind !== "cron" && - !isHeartbeatLifecycleRun && - !opts.internalEvents?.length, - preserveRuntimeModel: - fallbackExhausted || isHeartbeatLifecycleRun || preserveUserFacingSessionModelState, - preserveUserFacingSessionModelState, - }); - sessionEntry = sessionStore[sessionKey] ?? sessionEntry; - } - - const transcriptPersistenceRunner = result.meta.executionTrace?.runner; - const embeddedAssistantGapFill = - transcriptPersistenceRunner === "embedded" || - (transcriptPersistenceRunner === undefined && - Boolean(result.meta.finalAssistantVisibleText?.trim())); - if ( - !sessionReboundDuringRun && - (transcriptPersistenceRunner === "cli" || embeddedAssistantGapFill) - ) { - let persistedCliTurnTranscript = false; - try { - const transcriptSessionEntry: SessionEntry | undefined = suppressVisibleSessionEffects + payloads, + assertDeliveryCurrent: () => + assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration), + }; + const deliveryResult = await deliverAgentCommandResult( + resolveFreshSessionEntryForDelivery ? { - ...(sessionEntry ?? { - sessionId: effectiveSessionId, - updatedAt: Date.now(), - sessionStartedAt: Date.now(), - }), - sessionId: effectiveSessionId, - sessionFile: effectiveSessionFile, + ...deliveryParams, + expectedSessionIdForFreshDelivery: effectiveSessionId, + resolveFreshSessionEntryForDelivery, } - : sessionEntry; - const transcriptResult = await attemptExecutionRuntime.persistCliTurnTranscript({ - body, - transcriptBody, - result, - sessionId: effectiveSessionId, - sessionKey: sessionKey ?? effectiveSessionId, - sessionEntry: transcriptSessionEntry, - sessionStore: suppressVisibleSessionEffects ? undefined : sessionStore, - storePath: suppressVisibleSessionEffects ? undefined : storePath, - sessionAgentId, - threadId: opts.threadId, - sessionCwd: effectiveCwd, - config: cfg, - embeddedAssistantGapFill, - skipUserTurn: - suppressUserTurnPersistence || - userTurnTranscriptRecorder.hasPersisted() || - userTurnTranscriptRecorder.isBlocked(), - }); - sessionEntry = transcriptResult.sessionEntry; - sessionReboundDuringRun = transcriptResult.kind === "session-rebound"; - if (suppressVisibleSessionEffects) { - sessionEntry = prepared.sessionEntry; - } - persistedCliTurnTranscript = transcriptResult.kind === "persisted"; - } catch (error) { - log.warn( - `Turn transcript persistence failed for ${sessionKey ?? sessionId}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - if (persistedCliTurnTranscript && !suppressVisibleSessionEffects) { - sessionEntry = await ( - await loadCliCompactionRuntime() - ).runCliTurnCompactionLifecycle({ - cfg, - sessionId: effectiveSessionId, - sessionKey: sessionKey ?? effectiveSessionId, - sessionEntry, - sessionStore, - storePath, - sessionAgentId, - workspaceDir, - cwd: effectiveCwd, - agentDir, - provider: result.meta.agentMeta?.provider ?? provider, - model: result.meta.agentMeta?.model ?? model, - skillsSnapshot, - messageChannel, - agentAccountId: runContext.accountId, - senderIsOwner: opts.senderIsOwner, - thinkLevel: resolvedThinkLevel, - extraSystemPrompt: opts.extraSystemPrompt, - }); - } - } - - const payloads = result.payloads ?? []; - let pendingFinalDeliveryTextForThisRun: string | undefined; - - // Phase 2: Persist pending final delivery for main sessions before attempting delivery. - // This ensures that if the process restarts during delivery, the payload is durable. - if ( - opts.deliver === true && - sessionStore && - sessionKey && - !suppressVisibleSessionEffects && - !sessionReboundDuringRun && - payloads.length > 0 && - !isSubagentSessionKey(sessionKey) - ) { - const now = Date.now(); - const combinedPayload = sanitizePendingFinalDeliveryText( - payloads - .map((p) => (typeof p.text === "string" ? p.text : "")) - .filter(Boolean) - .join("\n\n"), + : deliveryParams, ); - pendingFinalDeliveryTextForThisRun = combinedPayload || undefined; - if (combinedPayload) { + // Phase 2: Clear pending delivery payload after successful delivery. + if ( + sessionStore && + sessionKey && + !isSubagentSessionKey(sessionKey) && + !suppressVisibleSessionEffects && + !sessionReboundDuringRun + ) { const entry = sessionStore[sessionKey] ?? sessionEntry; - const next: SessionEntry = { - ...entry, - pendingFinalDelivery: true, - pendingFinalDeliveryText: combinedPayload, - pendingFinalDeliveryContext: currentRunDeliveryContext, - pendingFinalDeliveryCreatedAt: now, - updatedAt: now, - }; - const persisted = await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry: next, - shouldPersist: (current) => shouldPersistCurrentRunSessionCleanup(current, sessionId), - }); - sessionEntry = persisted ?? sessionEntry; + const noPendingTextForThisRun = + opts.deliver === true && + pendingFinalDeliveryTextForThisRun === undefined && + entry.pendingFinalDelivery === true && + !entry.pendingFinalDeliveryText; + if (deliveryResult?.deliverySucceeded === true || noPendingTextForThisRun) { + const next = clearPendingFinalDeliveryFields(entry, Date.now()); + const persisted = await persistSessionEntry({ + sessionStore, + sessionKey, + storePath, + initialEntry: entry, + entry: next, + shouldPersist: (current) => shouldPersistCurrentRunSessionCleanup(current, sessionId), + }); + sessionEntry = persisted ?? sessionEntry; + } } - } - const { deliverAgentCommandResult } = await loadDeliveryRuntime(); - const resolveFreshSessionEntryForDelivery = - sessionStore && sessionKey && !suppressVisibleSessionEffects - ? async (): Promise => { - const { loadSessionStore } = await loadSessionStoreRuntime(); - const freshStore = loadSessionStore(storePath, { - skipCache: true, - clone: false, - }); - const freshEntry = freshStore[sessionKey]; - if (!freshEntry || freshEntry.sessionId !== effectiveSessionId) { - return undefined; - } - sessionStore[sessionKey] = freshEntry; - return freshEntry; - } - : undefined; - const deliveryParams = { - cfg, - deps: resolvedDeps, - runtime, - opts, - outboundSession, - sessionEntry, - result, - payloads, - assertDeliveryCurrent: () => assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration), - }; - const deliveryResult = await deliverAgentCommandResult( - resolveFreshSessionEntryForDelivery - ? { - ...deliveryParams, - expectedSessionIdForFreshDelivery: effectiveSessionId, - resolveFreshSessionEntryForDelivery, - } - : deliveryParams, - ); - - // Phase 2: Clear pending delivery payload after successful delivery. - if ( - sessionStore && - sessionKey && - !isSubagentSessionKey(sessionKey) && - !suppressVisibleSessionEffects && - !sessionReboundDuringRun - ) { - const entry = sessionStore[sessionKey] ?? sessionEntry; - const noPendingTextForThisRun = - opts.deliver === true && - pendingFinalDeliveryTextForThisRun === undefined && - entry.pendingFinalDelivery === true && - !entry.pendingFinalDeliveryText; - if (deliveryResult?.deliverySucceeded === true || noPendingTextForThisRun) { - const next = clearPendingFinalDeliveryFields(entry, Date.now()); - const persisted = await persistSessionEntry({ - sessionStore, - sessionKey, - storePath, - entry: next, - shouldPersist: (current) => shouldPersistCurrentRunSessionCleanup(current, sessionId), - }); - sessionEntry = persisted ?? sessionEntry; + if (fallbackExhausted || resolveLifecycleResultError(result, false)) { + emitLifecycleResultError(result, fallbackExhausted); + } else { + emitLifecycleEnd(result); } + return deliveryResult; + } catch (error) { + emitLifecyclePostTurnError(error); + throw error; } - - if (fallbackExhausted || resolveLifecycleResultError(result, false)) { - emitLifecycleResultError(result, fallbackExhausted); - } else { - emitLifecycleEnd(result); - } - return deliveryResult; - } catch (error) { - emitLifecyclePostTurnError(error); - throw error; - } + }); } finally { + sessionWorkAdmission.release(); if ( !sessionReboundDuringRun && trackedRestartRecoveryDeliveryContext && @@ -2430,6 +2480,7 @@ async function agentCommandInternal( sessionStore, sessionKey, storePath, + initialEntry: entry, entry: next, shouldPersist: (current) => shouldPersistRestartRecoveryCleanup(current, sessionId, runId), diff --git a/src/agents/auth-profiles/session-override.test.ts b/src/agents/auth-profiles/session-override.test.ts index ebd6ef7a3281..1fd78ba45e8b 100644 --- a/src/agents/auth-profiles/session-override.test.ts +++ b/src/agents/auth-profiles/session-override.test.ts @@ -4,14 +4,23 @@ * updates without loading the real auth store implementation. */ import fs from "node:fs/promises"; +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { + loadSessionEntry, + patchSessionEntry, + replaceSessionEntry, +} from "../../config/sessions/session-accessor.js"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { type OpenClawTestState, withOpenClawTestState, } from "../../test-utils/openclaw-test-state.js"; -import { resolveSessionAuthProfileOverride } from "./session-override.js"; +import { + clearSessionAuthProfileOverride, + resolveSessionAuthProfileOverride, +} from "./session-override.js"; import type { AuthProfileStore } from "./types.js"; const authStoreMocks = vi.hoisted(() => { @@ -562,4 +571,100 @@ describe("resolveSessionAuthProfileOverride", () => { expect(sessionEntry.authProfileOverrideSource).toBe("auto"); }); }); + + it("clears auth state without restoring concurrent session management fields", async () => { + await withAuthState(async (state) => { + const sessionKey = "agent:main:main"; + const storePath = path.join(state.sessionsDir(), "sessions.json"); + const scope = { storePath, sessionKey }; + await replaceSessionEntry(scope, { + sessionId: "s1", + updatedAt: 1, + label: "before", + pinnedAt: 1, + authProfileOverride: TEST_PRIMARY_PROFILE_ID, + authProfileOverrideSource: "user", + }); + const sessionEntry = loadSessionEntry({ ...scope, readConsistency: "latest" }); + expect(sessionEntry).toBeDefined(); + const sessionStore = { [sessionKey]: sessionEntry! }; + + await patchSessionEntry(scope, () => ({ label: "renamed", pinnedAt: undefined })); + await clearSessionAuthProfileOverride({ + sessionEntry: sessionEntry!, + sessionStore, + sessionKey, + storePath, + }); + + const persisted = loadSessionEntry({ ...scope, readConsistency: "latest" }); + expect(persisted?.label).toBe("renamed"); + expect(persisted?.pinnedAt).toBeUndefined(); + expect(persisted?.authProfileOverride).toBeUndefined(); + expect(sessionStore[sessionKey]?.label).toBe("renamed"); + expect(sessionStore[sessionKey]?.pinnedAt).toBeUndefined(); + }); + }); + + it("rotates auth state without restoring concurrent session management fields", async () => { + await withAuthState(async (state) => { + const agentDir = state.agentDir(); + await fs.mkdir(agentDir, { recursive: true }); + authStoreMocks.state.hasSource = true; + authStoreMocks.state.store = createAuthStoreWithProfiles({ + profiles: { + [TEST_PRIMARY_PROFILE_ID]: { + type: "api_key", + provider: "openai", + key: "sk-primary", + }, + [TEST_SECONDARY_PROFILE_ID]: { + type: "api_key", + provider: "openai", + key: "sk-secondary", + }, + }, + order: { + openai: [TEST_PRIMARY_PROFILE_ID, TEST_SECONDARY_PROFILE_ID], + }, + }); + + const sessionKey = "agent:main:main"; + const storePath = path.join(state.sessionsDir(), "sessions.json"); + const scope = { storePath, sessionKey }; + await replaceSessionEntry(scope, { + sessionId: "s1", + updatedAt: 1, + label: "before", + pinnedAt: 1, + compactionCount: 1, + authProfileOverride: TEST_PRIMARY_PROFILE_ID, + authProfileOverrideSource: "auto", + authProfileOverrideCompactionCount: 0, + }); + const sessionEntry = loadSessionEntry({ ...scope, readConsistency: "latest" }); + expect(sessionEntry).toBeDefined(); + const sessionStore = { [sessionKey]: sessionEntry! }; + + await patchSessionEntry(scope, () => ({ label: "renamed", pinnedAt: undefined })); + const resolved = await resolveSessionAuthProfileOverride({ + cfg: {} as OpenClawConfig, + provider: "openai", + agentDir, + sessionEntry: sessionEntry!, + sessionStore, + sessionKey, + storePath, + isNewSession: false, + }); + + expect(resolved).toBe(TEST_SECONDARY_PROFILE_ID); + const persisted = loadSessionEntry({ ...scope, readConsistency: "latest" }); + expect(persisted?.label).toBe("renamed"); + expect(persisted?.pinnedAt).toBeUndefined(); + expect(persisted?.authProfileOverride).toBe(TEST_SECONDARY_PROFILE_ID); + expect(sessionStore[sessionKey]?.label).toBe("renamed"); + expect(sessionStore[sessionKey]?.pinnedAt).toBeUndefined(); + }); + }); }); diff --git a/src/agents/auth-profiles/session-override.ts b/src/agents/auth-profiles/session-override.ts index 095fd0124dc4..c1f4bf69a95c 100644 --- a/src/agents/auth-profiles/session-override.ts +++ b/src/agents/auth-profiles/session-override.ts @@ -24,6 +24,63 @@ function loadSessionAccessor() { return sessionAccessorLoader.load(); } +type SessionAuthProfileOverrideState = Pick< + SessionEntry, + "authProfileOverride" | "authProfileOverrideSource" | "authProfileOverrideCompactionCount" +>; + +function applySessionAuthProfileOverrideState( + entry: SessionEntry, + state: SessionAuthProfileOverrideState, + updatedAt: number, +): void { + if (state.authProfileOverride === undefined) { + delete entry.authProfileOverride; + } else { + entry.authProfileOverride = state.authProfileOverride; + } + if (state.authProfileOverrideSource === undefined) { + delete entry.authProfileOverrideSource; + } else { + entry.authProfileOverrideSource = state.authProfileOverrideSource; + } + if (state.authProfileOverrideCompactionCount === undefined) { + delete entry.authProfileOverrideCompactionCount; + } else { + entry.authProfileOverrideCompactionCount = state.authProfileOverrideCompactionCount; + } + entry.updatedAt = Math.max(entry.updatedAt ?? 0, updatedAt); +} + +async function persistSessionAuthProfileOverrideState(params: { + sessionEntry: SessionEntry; + sessionStore: Record; + sessionKey: string; + state: SessionAuthProfileOverrideState; + storePath?: string; +}): Promise { + const { sessionEntry, sessionStore, sessionKey, state, storePath } = params; + const updatedAt = Date.now(); + applySessionAuthProfileOverrideState(sessionEntry, state, updatedAt); + sessionStore[sessionKey] = sessionEntry; + if (!storePath) { + return; + } + const persisted = await ( + await loadSessionAccessor() + ).patchSessionEntry( + { storePath, sessionKey }, + (current) => ({ + ...state, + updatedAt: Math.max(current.updatedAt ?? 0, updatedAt), + }), + { fallbackEntry: sessionEntry }, + ); + if (persisted) { + sessionStore[sessionKey] = persisted; + } +} + // Current session overrides are only valid when the selected provider can use // that profile, including configured aws-sdk profiles without stored secrets. function isProfileForProvider(params: { @@ -76,20 +133,17 @@ export async function clearSessionAuthProfileOverride(params: { storePath?: string; }) { const { sessionEntry, sessionStore, sessionKey, storePath } = params; - delete sessionEntry.authProfileOverride; - delete sessionEntry.authProfileOverrideSource; - delete sessionEntry.authProfileOverrideCompactionCount; - sessionEntry.updatedAt = Date.now(); - sessionStore[sessionKey] = sessionEntry; - if (storePath) { - await ( - await loadSessionAccessor() - ).patchSessionEntry( - { storePath, sessionKey }, - () => sessionEntry, - { fallbackEntry: sessionEntry, replaceEntry: true }, - ); - } + await persistSessionAuthProfileOverrideState({ + sessionEntry, + sessionStore, + sessionKey, + state: { + authProfileOverride: undefined, + authProfileOverrideSource: undefined, + authProfileOverrideCompactionCount: undefined, + }, + storePath, + }); } /** Resolves and optionally rotates the session auth-profile override. */ @@ -231,20 +285,17 @@ export async function resolveSessionAuthProfileOverride(params: { sessionEntry.authProfileOverrideSource !== "auto" || sessionEntry.authProfileOverrideCompactionCount !== compactionCount; if (shouldPersist) { - sessionEntry.authProfileOverride = next; - sessionEntry.authProfileOverrideSource = "auto"; - sessionEntry.authProfileOverrideCompactionCount = compactionCount; - sessionEntry.updatedAt = Date.now(); - sessionStore[sessionKey] = sessionEntry; - if (storePath) { - await ( - await loadSessionAccessor() - ).patchSessionEntry( - { storePath, sessionKey }, - () => sessionEntry, - { fallbackEntry: sessionEntry, replaceEntry: true }, - ); - } + await persistSessionAuthProfileOverrideState({ + sessionEntry, + sessionStore, + sessionKey, + state: { + authProfileOverride: next, + authProfileOverrideSource: "auto", + authProfileOverrideCompactionCount: compactionCount, + }, + storePath, + }); } return next; diff --git a/src/agents/command/attempt-execution.shared.test.ts b/src/agents/command/attempt-execution.shared.test.ts index b4ff57715b4e..e661e136e45b 100644 --- a/src/agents/command/attempt-execution.shared.test.ts +++ b/src/agents/command/attempt-execution.shared.test.ts @@ -1,9 +1,14 @@ // Covers shared attempt-execution helpers for prompt materialization and // guarded session-store persistence. -import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; +import { + clearSessionStoreCacheForTest, + loadSessionStore, + saveSessionStore, +} from "../../config/sessions/store.js"; +import type { SessionEntry } from "../../config/sessions/types.js"; import { INTERNAL_RUNTIME_CONTEXT_BEGIN, INTERNAL_RUNTIME_CONTEXT_END, @@ -15,6 +20,8 @@ import { } from "./attempt-execution.shared.js"; import type { AgentCommandOpts } from "./types.js"; +const tempDirs = useAutoCleanupTempDirTracker(afterEach); + function makeTaskCompletionEvents(): NonNullable { // The result deliberately contains internal markers to prove child output // cannot spoof OpenClaw runtime-context envelopes. @@ -88,7 +95,7 @@ describe("attempt execution prompt materialization", () => { describe("persistSessionEntry", () => { it("clears stale local entries when guarded persistence sees no persisted entry", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-store-")); + const dir = tempDirs.make("openclaw-session-store-"); try { const storePath = path.join(dir, "sessions.json"); const sessionStore = { @@ -104,6 +111,7 @@ describe("persistSessionEntry", () => { sessionStore, sessionKey: "main", storePath, + initialEntry: sessionStore.main, entry: { sessionId: "stale", updatedAt: 2, @@ -114,7 +122,112 @@ describe("persistSessionEntry", () => { expect(persisted).toBeUndefined(); expect(sessionStore.main).toBeUndefined(); } finally { - fs.rmSync(dir, { recursive: true, force: true }); + clearSessionStoreCacheForTest(); + } + }); + + it.each([ + { + name: "rename and unpin", + current: { label: "Renamed", pinnedAt: undefined }, + expected: { label: "Renamed", pinnedAt: undefined }, + }, + { + name: "label clear and pin", + current: { label: undefined, pinnedAt: 300 }, + expected: { label: undefined, pinnedAt: 300 }, + }, + ])("preserves a concurrent $name", async ({ current, expected }) => { + const dir = tempDirs.make("openclaw-session-store-"); + try { + const storePath = path.join(dir, "sessions.json"); + const staleEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 100, + label: "Old label", + pinnedAt: 200, + }; + const currentEntry: SessionEntry = { + ...staleEntry, + ...current, + updatedAt: 400, + }; + if (current.label === undefined) { + delete currentEntry.label; + } + if (current.pinnedAt === undefined) { + delete currentEntry.pinnedAt; + } + await saveSessionStore(storePath, { main: currentEntry }, { skipMaintenance: true }); + const sessionStore = { main: staleEntry }; + + const persisted = await persistSessionEntry({ + sessionStore, + sessionKey: "main", + storePath, + initialEntry: staleEntry, + entry: { + ...staleEntry, + model: "gpt-5.5", + updatedAt: 250, + }, + }); + + expect(persisted).toMatchObject({ sessionId: "session-1", model: "gpt-5.5" }); + expect(persisted?.label).toBe(expected.label); + expect(persisted?.pinnedAt).toBe(expected.pinnedAt); + expect(persisted?.updatedAt).toBeGreaterThanOrEqual(currentEntry.updatedAt); + expect(sessionStore.main).toEqual(persisted); + expect(loadSessionStore(storePath, { skipCache: true }).main).toEqual(persisted); + } finally { + clearSessionStoreCacheForTest(); + } + }); + + it("does not restore policy fields revoked during an active turn", async () => { + const dir = tempDirs.make("openclaw-session-store-"); + try { + const storePath = path.join(dir, "sessions.json"); + const initialEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 100, + model: "gpt-5.4", + elevatedLevel: "full", + inheritedToolAllow: ["exec"], + sendPolicy: "allow", + }; + const currentEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 400, + model: "gpt-5.4", + sendPolicy: "deny", + }; + await saveSessionStore(storePath, { main: currentEntry }, { skipMaintenance: true }); + const sessionStore = { main: initialEntry }; + + const persisted = await persistSessionEntry({ + sessionStore, + sessionKey: "main", + storePath, + initialEntry, + entry: { + ...initialEntry, + model: "gpt-5.5", + updatedAt: 250, + }, + }); + + expect(persisted).toMatchObject({ + sessionId: "session-1", + model: "gpt-5.5", + sendPolicy: "deny", + updatedAt: 400, + }); + expect(persisted?.elevatedLevel).toBeUndefined(); + expect(persisted?.inheritedToolAllow).toBeUndefined(); + expect(loadSessionStore(storePath, { skipCache: true }).main).toEqual(persisted); + } finally { + clearSessionStoreCacheForTest(); } }); }); diff --git a/src/agents/command/attempt-execution.shared.ts b/src/agents/command/attempt-execution.shared.ts index 2fac7fe500ea..e423413c5729 100644 --- a/src/agents/command/attempt-execution.shared.ts +++ b/src/agents/command/attempt-execution.shared.ts @@ -3,7 +3,8 @@ * execution paths. */ import { patchSessionEntry } from "../../config/sessions/session-accessor.js"; -import { mergeSessionEntry, type SessionEntry } from "../../config/sessions/types.js"; +import { mergeSessionSnapshotChanges } from "../../config/sessions/session-snapshot-merge.js"; +import type { SessionEntry } from "../../config/sessions/types.js"; import { formatAgentInternalEventsForPlainPrompt, formatAgentInternalEventsForPrompt, @@ -19,18 +20,12 @@ type PersistSessionEntryParams = { sessionStore: Record; sessionKey: string; storePath: string; + initialEntry: SessionEntry; entry: SessionEntry; - clearedFields?: string[]; - preserveTranscriptMarkerUpdatedAt?: boolean; shouldPersist?: (entry: SessionEntry | undefined) => boolean; }; /** Persists one session entry while keeping the caller's in-memory store aligned. */ - -function normalizeTranscriptMarkerUpdatedAt(value: number | undefined): number | undefined { - return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; -} - export async function persistSessionEntry( params: PersistSessionEntryParams, ): Promise { @@ -42,24 +37,19 @@ export async function persistSessionEntry( rejectedMissingEntry = !context.existingEntry; return null; } - const merged = mergeSessionEntry(context.existingEntry, params.entry); - if (params.preserveTranscriptMarkerUpdatedAt) { - const currentUpdatedAt = normalizeTranscriptMarkerUpdatedAt( - context.existingEntry?.updatedAt, - ); - const markerUpdatedAt = normalizeTranscriptMarkerUpdatedAt(params.entry.updatedAt); - if (markerUpdatedAt !== undefined) { - merged.updatedAt = Math.max(currentUpdatedAt ?? 0, markerUpdatedAt); - } + if (!context.existingEntry) { + return params.entry; } - for (const field of params.clearedFields ?? []) { - // Cleared fields only apply when the replacement entry did not set the - // field again; this preserves explicit false/null updates. - if (!Object.hasOwn(params.entry, field)) { - Reflect.deleteProperty(merged, field); - } + if (context.existingEntry.sessionId !== params.initialEntry.sessionId) { + return null; } - return merged; + // Agent turns persist broad snapshots. Project only this turn's changes + // so a stale snapshot cannot restore fields changed or cleared meanwhile. + return mergeSessionSnapshotChanges({ + initial: params.initialEntry, + next: params.entry, + current: context.existingEntry, + }); }, { fallbackEntry: params.sessionStore[params.sessionKey] ?? params.entry, diff --git a/src/agents/command/session-store.runtime.ts b/src/agents/command/session-store.runtime.ts index d385e3a59874..498f4fb6c166 100644 --- a/src/agents/command/session-store.runtime.ts +++ b/src/agents/command/session-store.runtime.ts @@ -2,3 +2,4 @@ // config/session persistence until an agent run needs to save state. export { updateSessionStoreAfterAgentRun } from "./session-store.js"; export { loadSessionStore } from "../../config/sessions.js"; +export { loadSessionEntry } from "../../config/sessions/session-accessor.js"; diff --git a/src/agents/command/session-store.test.ts b/src/agents/command/session-store.test.ts index fe2ae73da2c5..e16748e8ac69 100644 --- a/src/agents/command/session-store.test.ts +++ b/src/agents/command/session-store.test.ts @@ -170,6 +170,69 @@ async function withTempSessionStore( } describe("updateSessionStoreAfterAgentRun", () => { + it("preserves a concurrent rename and unpin during final accounting", async () => { + await withTempSessionStore(async ({ storePath }) => { + const sessionKey = "agent:main:explicit:test-management-race"; + const sessionId = "test-management-race-session"; + const staleEntry: SessionEntry = { + sessionId, + updatedAt: 1, + label: "Old label", + pinnedAt: 100, + chatType: "direct", + elevatedLevel: "full", + inheritedToolAllow: ["exec"], + sendPolicy: "allow", + }; + const sessionStore = { [sessionKey]: staleEntry }; + const concurrentEntry: SessionEntry = { + ...staleEntry, + chatType: "group", + label: "Renamed while running", + sendPolicy: "deny", + updatedAt: 2, + }; + delete concurrentEntry.elevatedLevel; + delete concurrentEntry.inheritedToolAllow; + delete concurrentEntry.pinnedAt; + await fs.writeFile( + storePath, + JSON.stringify({ + [sessionKey]: concurrentEntry, + }), + ); + + await updateSessionStoreAfterAgentRun({ + cfg: {} as OpenClawConfig, + sessionId, + sessionKey, + storePath, + sessionStore, + defaultProvider: "openai", + defaultModel: "gpt-5.5", + result: { + meta: { + durationMs: 1, + agentMeta: { sessionId, provider: "openai", model: "gpt-5.5" }, + }, + }, + }); + + expect(sessionStore[sessionKey]).toMatchObject({ + chatType: "group", + label: "Renamed while running", + model: "gpt-5.5", + sendPolicy: "deny", + }); + expect(sessionStore[sessionKey]?.elevatedLevel).toBeUndefined(); + expect(sessionStore[sessionKey]?.inheritedToolAllow).toBeUndefined(); + expect(sessionStore[sessionKey]?.pinnedAt).toBeUndefined(); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toEqual( + sessionStore[sessionKey], + ); + }); + }); + it("passes resolved maintenance config to the gateway turn store write", async () => { sessionStoreMocks.updateSessionStore.mockClear(); await withTempSessionStore(async ({ storePath }) => { diff --git a/src/agents/command/session-store.ts b/src/agents/command/session-store.ts index 7c15059643ae..a290eb75c173 100644 --- a/src/agents/command/session-store.ts +++ b/src/agents/command/session-store.ts @@ -8,6 +8,7 @@ import { type SessionEntry, } from "../../config/sessions.js"; import { patchSessionEntry } from "../../config/sessions/session-accessor.js"; +import { projectSessionSnapshotChanges } from "../../config/sessions/session-snapshot-merge.js"; import { resolveMaintenanceConfigFromInput } from "../../config/sessions/store-maintenance.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; @@ -37,15 +38,6 @@ function resolvePositiveInteger(value: number | undefined): number | undefined { return Math.floor(value); } -function removeLifecycleStateFromMetadataPatch(entry: SessionEntry): SessionEntry { - const next = { ...entry }; - delete next.status; - delete next.startedAt; - delete next.endedAt; - delete next.runtimeMs; - return next; -} - /** Applies run result metadata, usage, and CLI bindings to a session entry. */ export async function updateSessionStoreAfterAgentRun(params: { cfg: OpenClawConfig; @@ -283,14 +275,14 @@ export async function updateSessionStoreAfterAgentRun(params: { updatedAt: next.updatedAt, ...(touchInteraction ? { lastInteractionAt: next.lastInteractionAt } : {}), } - : removeLifecycleStateFromMetadataPatch(next); + : next; const maintenanceConfig = resolveMaintenanceConfigFromInput(cfg.session?.maintenance); const persisted = await patchSessionEntry( { storePath, sessionKey, }, - (_currentEntry, context) => { + (currentEntry, context) => { if ( (!preserveUserFacingRunState && context.existingEntry && @@ -301,7 +293,9 @@ export async function updateSessionStoreAfterAgentRun(params: { // Do not merge stale finalizer metadata after a delete or a competing reset. return null; } - return metadataPatch; + return preserveUserFacingRunState + ? metadataPatch + : projectSessionSnapshotChanges({ initial: entry, next, current: currentEntry }); }, { ...(preserveUserFacingRunState ? {} : { fallbackEntry: entry }), diff --git a/src/agents/embedded-agent-runner/compact.hooks.harness.ts b/src/agents/embedded-agent-runner/compact.hooks.harness.ts index e25c5c2d097b..567d04f8aab7 100644 --- a/src/agents/embedded-agent-runner/compact.hooks.harness.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.harness.ts @@ -173,6 +173,42 @@ export const resolveSandboxContextMock = vi.fn(async () => null); export const maybeCompactAgentHarnessSessionMock: Mock< (params?: unknown, options?: unknown) => Promise > = vi.fn(async () => undefined); +async function runCompactWithSafetyTimeoutMock( + compact: () => Promise, + _timeoutMs?: number, + opts?: { abortSignal?: AbortSignal; onCancel?: () => void }, +): Promise { + const abortSignal = opts?.abortSignal; + if (!abortSignal) { + return await compact(); + } + const cancelAndCreateError = () => { + opts?.onCancel?.(); + const reason = "reason" in abortSignal ? abortSignal.reason : undefined; + if (reason instanceof Error) { + return reason; + } + const err = new Error("aborted"); + err.name = "AbortError"; + return err; + }; + if (abortSignal.aborted) { + throw cancelAndCreateError(); + } + return await Promise.race([ + compact(), + new Promise((_, reject) => { + abortSignal.addEventListener( + "abort", + () => { + reject(cancelAndCreateError()); + }, + { once: true }, + ); + }), + ]); +} +export const compactWithSafetyTimeoutMock = vi.fn(runCompactWithSafetyTimeoutMock); export const rotateTranscriptAfterCompactionMock: Mock< (_params?: unknown) => Promise > = vi.fn(async () => ({ @@ -376,6 +412,8 @@ export function resetCompactHooksHarnessMocks(): void { reason: undefined, result: { summary: "engine-summary", tokensAfter: 50 }, }); + compactWithSafetyTimeoutMock.mockReset(); + compactWithSafetyTimeoutMock.mockImplementation(runCompactWithSafetyTimeoutMock); resolveModelMock.mockReset(); resolveModelMock.mockReturnValue({ @@ -672,45 +710,8 @@ export async function loadCompactHooksHarness(): Promise<{ })); vi.doMock("./compaction-safety-timeout.js", () => { - const compactWithSafetyTimeout = vi.fn( - async ( - compact: () => Promise, - _timeoutMs?: number, - opts?: { abortSignal?: AbortSignal; onCancel?: () => void }, - ) => { - const abortSignal = opts?.abortSignal; - if (!abortSignal) { - return await compact(); - } - const cancelAndCreateError = () => { - opts?.onCancel?.(); - const reason = "reason" in abortSignal ? abortSignal.reason : undefined; - if (reason instanceof Error) { - return reason; - } - const err = new Error("aborted"); - err.name = "AbortError"; - return err; - }; - if (abortSignal.aborted) { - throw cancelAndCreateError(); - } - return await Promise.race([ - compact(), - new Promise((_, reject) => { - abortSignal.addEventListener( - "abort", - () => { - reject(cancelAndCreateError()); - }, - { once: true }, - ); - }), - ]); - }, - ); return { - compactWithSafetyTimeout, + compactWithSafetyTimeout: compactWithSafetyTimeoutMock, resolveCompactionTimeoutMs: vi.fn(() => 30_000), // Mirror the real wrapper: bound the engine's compact() with the // (mocked) safety timeout and thread the abort signal into its params. @@ -721,7 +722,7 @@ export async function loadCompactHooksHarness(): Promise<{ timeoutMs?: number, abortSignal?: AbortSignal, ) => - compactWithSafetyTimeout( + compactWithSafetyTimeoutMock( () => contextEngine.compact(abortSignal ? { ...params, abortSignal } : params), timeoutMs, abortSignal ? { abortSignal } : undefined, diff --git a/src/agents/embedded-agent-runner/compact.hooks.test.ts b/src/agents/embedded-agent-runner/compact.hooks.test.ts index 27558714e70a..10845746ca07 100644 --- a/src/agents/embedded-agent-runner/compact.hooks.test.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.test.ts @@ -6,6 +6,7 @@ import { applyAgentCompactionSettingsFromConfigMock, buildEmbeddedSystemPromptMock, contextEngineCompactMock, + compactWithSafetyTimeoutMock, createAgentSessionMock, createPreparedEmbeddedAgentSettingsManagerMock, createOpenClawCodingToolsMock, @@ -2218,7 +2219,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { } as never); maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({ ok: true, - compacted: false, + compacted: true, result: { summary: "", firstKeptEntryId: "", @@ -2226,7 +2227,8 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { details: { backend: "codex-app-server", signal: "thread/compact/start", - pending: true, + pending: false, + completed: true, }, }, }); @@ -2262,18 +2264,66 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { | undefined; expect(details?.codexNativeCompaction).toMatchObject({ ok: true, - compacted: false, + compacted: true, result: { tokensBefore: 333, details: { backend: "codex-app-server", signal: "thread/compact/start", - pending: true, + pending: false, + completed: true, }, }, }); }); + it("holds the queued lane until secondary Codex compaction reaches its terminal event", async () => { + resolveAgentHarnessPolicyMock.mockReturnValue({ + runtime: "codex", + runtimeSource: "model", + } as never); + const nativeTerminal = createDeferred<{ + ok: true; + compacted: true; + result: { summary: string; firstKeptEntryId: string; tokensBefore: number }; + }>(); + maybeCompactAgentHarnessSessionMock.mockReturnValueOnce(nativeTerminal.promise); + compactWithSafetyTimeoutMock + .mockImplementation(async () => { + throw new Error("Compaction timed out"); + }) + .mockImplementationOnce(async (compact) => await compact()); + + let settled = false; + const resultPromise = compactEmbeddedAgentSession( + wrappedCompactionArgs({ + provider: "codex", + model: "gpt-5.4", + agentHarnessId: "codex", + trigger: "budget", + }), + ).finally(() => { + settled = true; + }); + + await vi.waitFor(() => { + expect(maybeCompactAgentHarnessSessionMock).toHaveBeenCalledTimes(1); + }); + expect(settled).toBe(false); + expect(compactWithSafetyTimeoutMock).toHaveBeenCalledTimes(1); + + nativeTerminal.resolve({ + ok: true, + compacted: true, + result: { + summary: "", + firstKeptEntryId: "", + tokensBefore: 333, + }, + }); + await expect(resultPromise).resolves.toMatchObject({ ok: true, compacted: true }); + }); + it("keeps context-engine compaction successful when the secondary Codex bridge gets a provider 4xx", async () => { resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex", diff --git a/src/agents/embedded-agent-runner/compact.queued.ts b/src/agents/embedded-agent-runner/compact.queued.ts index 411bdaae2a60..88e3c5156ca8 100644 --- a/src/agents/embedded-agent-runner/compact.queued.ts +++ b/src/agents/embedded-agent-runner/compact.queued.ts @@ -44,7 +44,6 @@ import { resolveEmbeddedCompactionTarget, } from "./compaction-runtime-context.js"; import { - compactWithSafetyTimeout, compactContextEngineWithSafetyTimeout, resolveCompactionTimeoutMs, } from "./compaction-safety-timeout.js"; @@ -554,22 +553,19 @@ export async function compactEmbeddedAgentSession( attemptNativeHarnessCompaction ) { try { - secondaryNativeHarnessCompaction = await compactWithSafetyTimeout( - (compactAbortSignal) => - maybeCompactAgentHarnessSession( - { - ...params, - sessionId: postCompactionSessionId, - sessionFile: postCompactionSessionFile, - contextEngine, - contextTokenBudget, - contextEngineRuntimeContext, - abortSignal: compactAbortSignal, - }, - { nativeCompactionRequest: "after_context_engine" }, - ), - resolveCompactionTimeoutMs(params.config), - params.abortSignal ? { abortSignal: params.abortSignal } : undefined, + // The native bridge owns its terminal-event watchdog. Keep this lane held until + // that bridge settles; an outer timeout would release transcript ownership while + // the harness could still be compacting the same session. + secondaryNativeHarnessCompaction = await maybeCompactAgentHarnessSession( + { + ...params, + sessionId: postCompactionSessionId, + sessionFile: postCompactionSessionFile, + contextEngine, + contextTokenBudget, + contextEngineRuntimeContext, + }, + { nativeCompactionRequest: "after_context_engine" }, ); if (secondaryNativeHarnessCompaction && !secondaryNativeHarnessCompaction.ok) { log.warn( diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index 6f35b050e382..5ea9424afa0e 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -496,6 +496,7 @@ describe("sessions tools", () => { params: { activeMinutes: undefined, agentId: "main", + archived: false, includeDerivedTitles: false, includeLastMessage: false, includeGlobal: true, @@ -631,6 +632,10 @@ describe("sessions tools", () => { channel: "unknown", origin: undefined, spawnedBy: undefined, + archived: false, + archivedAt: undefined, + pinned: false, + pinnedAt: undefined, label: undefined, displayName: undefined, derivedTitle: "Visible project kickoff", diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 7b211df54dfd..785b3b672157 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -263,7 +263,7 @@ async function callSubagentGateway( { expectFinal: request.expectFinal, ...(scopes != null ? { forceSyntheticClient: true } : {}), - timeoutMs: request.timeoutMs, + ...(typeof request.timeoutMs === "number" ? { timeoutMs: request.timeoutMs } : {}), ...(scopes != null ? { syntheticScopes: scopes } : {}), }, ); diff --git a/src/agents/tool-description-presets.ts b/src/agents/tool-description-presets.ts index b518d01df063..be4bbda61ab9 100644 --- a/src/agents/tool-description-presets.ts +++ b/src/agents/tool-description-presets.ts @@ -14,7 +14,7 @@ export const UPDATE_PLAN_TOOL_DISPLAY_SUMMARY = "Track short work plan."; /** Describes the sessions_list tool for model-facing instructions. */ export function describeSessionsListTool(): string { return [ - "List visible sessions; filter by kind, label, agentId, search, activity.", + "List visible sessions; filter by kind, label, agentId, search, activity, archive state.", "Use before sessions_history or sessions_send target selection.", ].join(" "); } diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index bdab0d552b9a..63085edb8fe6 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -57,6 +57,10 @@ export type SessionListRow = { parentSessionKey?: string; deliveryContext?: SessionListDeliveryContext; updatedAt?: number | null; + archived?: boolean; + archivedAt?: number; + pinned?: boolean; + pinnedAt?: number; sessionId?: string; model?: string; contextTokens?: number | null; diff --git a/src/agents/tools/sessions-list-tool.test.ts b/src/agents/tools/sessions-list-tool.test.ts index c65de0f93180..5dfd675ceffc 100644 --- a/src/agents/tools/sessions-list-tool.test.ts +++ b/src/agents/tools/sessions-list-tool.test.ts @@ -46,6 +46,10 @@ type SessionsListDetails = { effectiveFastModeSource?: "session" | "agent" | "config" | "default"; fastMode?: boolean | "auto"; fastAutoOnSeconds?: number; + archived?: boolean; + archivedAt?: number; + pinned?: boolean; + pinnedAt?: number; reasoningLevel?: string; responseUsage?: string; thinkingLevel?: string; @@ -208,6 +212,36 @@ describe("sessions-list-tool", () => { expect(session?.responseUsage).toBe("full"); }); + it("requests archived sessions and keeps management metadata", async () => { + mocks.gatewayCall.mockResolvedValue({ + path: "/tmp/sessions.json", + sessions: [ + { + key: "agent:main:dashboard:archived", + kind: "direct", + archived: true, + archivedAt: 20, + pinned: false, + }, + ], + }); + const tool = createSessionsListTool({ config: {} as never }); + + const result = await tool.execute("call-archived", { archived: true }); + + expect(mocks.gatewayCall).toHaveBeenCalledWith( + expect.objectContaining({ + method: "sessions.list", + params: expect.objectContaining({ archived: true }), + }), + ); + expect(getSessionsListDetails(result).sessions?.[0]).toMatchObject({ + archived: true, + archivedAt: 20, + pinned: false, + }); + }); + it.each([ [{ limit: 1.5 }, "limit must be a positive integer"], [{ activeMinutes: 0 }, "activeMinutes must be a positive integer"], diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index 0a596590601c..7568bb678150 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -62,6 +62,7 @@ const SessionsListToolSchema = Type.Object({ label: Type.Optional(Type.String({ minLength: 1 })), agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })), search: Type.Optional(Type.String({ minLength: 1 })), + archived: Type.Optional(Type.Boolean()), includeDerivedTitles: Type.Optional(Type.Boolean()), includeLastMessage: Type.Optional(Type.Boolean()), }); @@ -123,6 +124,7 @@ export function createSessionsListTool(opts?: { const label = readStringParam(params, "label"); const agentId = readStringParam(params, "agentId"); const search = readStringParam(params, "search"); + const archived = params.archived === true; const includeDerivedTitles = params.includeDerivedTitles === true; const includeLastMessage = params.includeLastMessage === true; const gatewayCall = opts?.callGateway ?? callGateway; @@ -137,6 +139,7 @@ export function createSessionsListTool(opts?: { label, agentId, search, + archived, includeDerivedTitles: false, includeLastMessage: false, includeGlobal: !restrictToSpawned, @@ -312,6 +315,10 @@ export function createSessionsListTool(opts?: { } : undefined, updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : undefined, + archived: entry.archived === true, + archivedAt: typeof entry.archivedAt === "number" ? entry.archivedAt : undefined, + pinned: entry.pinned === true, + pinnedAt: typeof entry.pinnedAt === "number" ? entry.pinnedAt : undefined, sessionId, model: readStringValue(entry.model), contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : undefined, diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index 9a4720907660..3062803bac50 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -465,6 +465,7 @@ function createMockReplyOperation(): { terminalRecovery: false, phase: "running", result: null, + hasOwnedSessionId: vi.fn((sessionId: string) => sessionId === "session"), setPhase: vi.fn(), updateSessionId: updateSessionIdMock, attachBackend: vi.fn(), diff --git a/src/auto-reply/reply/agent-runner-memory.test.ts b/src/auto-reply/reply/agent-runner-memory.test.ts index 7b26dfdf74b0..2bb18bdddecd 100644 --- a/src/auto-reply/reply/agent-runner-memory.test.ts +++ b/src/auto-reply/reply/agent-runner-memory.test.ts @@ -49,6 +49,7 @@ function createReplyOperation(): TestReplyOperation { terminalRecovery: false, phase: "queued", result: null, + hasOwnedSessionId: vi.fn((sessionId: string) => sessionId === "session"), setPhase: vi.fn(), updateSessionId: vi.fn(), attachBackend: vi.fn(), diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index d6003bcf8dc4..2696f4d86d76 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -1445,6 +1445,8 @@ export async function runReplyAgent(params: { const admission = await admitReplyTurn({ sessionId: followupRun.run.sessionId, sessionKey: replySessionKey ?? "", + expectedSessionId: activeSessionEntry?.sessionId, + storePath, kind: replyTurnKind, resetTriggered: effectiveResetTriggered, routeThreadId: replyRouteThreadId, diff --git a/src/auto-reply/reply/commands-compact.test.ts b/src/auto-reply/reply/commands-compact.test.ts index 77204ea964ea..d69e3ecb0757 100644 --- a/src/auto-reply/reply/commands-compact.test.ts +++ b/src/auto-reply/reply/commands-compact.test.ts @@ -141,6 +141,7 @@ describe("handleCompactCommand", () => { ok: true, compacted: false, }); + const abortController = new AbortController(); const result = await handleCompactCommand( { @@ -161,6 +162,7 @@ describe("handleCompactCommand", () => { SenderE164: "+15551234567", }, agentDir: "/tmp/openclaw-agent-compact", + opts: { abortSignal: abortController.signal }, sessionEntry: { sessionId: "session-1", updatedAt: Date.now(), @@ -179,6 +181,7 @@ describe("handleCompactCommand", () => { expect(vi.mocked(compactEmbeddedAgentSession)).toHaveBeenCalledOnce(); const call = requireCompactEmbeddedAgentSessionCall(); expect(call.sessionId).toBe("session-1"); + expect(call.abortSignal).toBe(abortController.signal); expect(call.sessionKey).toBe("agent:main:main"); expect(call.allowGatewaySubagentBinding).toBe(true); expect(call.trigger).toBe("manual"); @@ -465,39 +468,35 @@ describe("handleCompactCommand", () => { expect(call.tokensAfter).toBe(321); }); - it("reports started Codex native compaction without incrementing completed compaction state", async () => { + it("reports unknown context when terminal compaction omits the post-compaction count", async () => { vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({ ok: true, - compacted: false, + compacted: true, result: { summary: "", firstKeptEntryId: "", - tokensBefore: 199_000, - details: { - backend: "codex-app-server", - threadId: "thread-1", - signal: "thread/compact/start", - pending: true, - }, + tokensBefore: 999, }, }); - const result = await handleCompactCommand( + await handleCompactCommand( { ...buildCompactParams("/compact", { commands: { text: true }, channels: { whatsapp: { allowFrom: ["*"] } }, } as OpenClawConfig), sessionEntry: { - sessionId: "live-session", + sessionId: "target-session", updatedAt: Date.now(), + totalTokens: 999, + totalTokensFresh: true, }, } as HandleCommandsParams, true, ); - expect(result?.reply?.text).toContain("Codex compaction started"); - expect(vi.mocked(incrementCompactionCount)).not.toHaveBeenCalled(); + expect(vi.mocked(incrementCompactionCount)).toHaveBeenCalledOnce(); + expect(vi.mocked(formatContextUsageShort)).toHaveBeenLastCalledWith(null, null); }); it("resolves /compact context budget from the active Codex runtime config instead of stale session metadata", async () => { diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index 70ada78321df..b223aed68bf4 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -85,19 +85,6 @@ function formatCompactionReason(reason?: string): string | undefined { } } -function isCodexNativeCompactionStartedResult(result: { result?: { details?: unknown } }): boolean { - const details = result.result?.details; - if (!details || typeof details !== "object" || Array.isArray(details)) { - return false; - } - const record = details as Record; - return ( - record.backend === "codex-app-server" && - record.signal === "thread/compact/start" && - record.pending === true - ); -} - function resolveManualCompactContextTokenBudget(params: { cfg: OpenClawConfig; provider?: string; @@ -246,6 +233,7 @@ export const handleCompactCommand: CommandHandler = async (params) => { persistedContextTokens: targetSessionEntry.contextTokens, }); const result = await runtime.compactEmbeddedAgentSession({ + abortSignal: params.opts?.abortSignal, sessionId, sessionKey: params.sessionKey, allowGatewaySubagentBinding: true, @@ -287,20 +275,17 @@ export const handleCompactCommand: CommandHandler = async (params) => { ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined, }); - const codexNativeCompactionStarted = isCodexNativeCompactionStartedResult(result); const compactLabel = result.ok || isCompactionSkipReason(result.reason) - ? codexNativeCompactionStarted - ? "Codex compaction started" - : result.compacted - ? result.result?.tokensBefore != null && result.result?.tokensAfter != null - ? `Compacted (${runtime.formatTokenCount(result.result.tokensBefore)} → ${runtime.formatTokenCount(result.result.tokensAfter)})` - : result.result?.tokensBefore - ? `Compacted (${runtime.formatTokenCount(result.result.tokensBefore)} before)` - : "Compacted" - : "Compaction skipped" + ? result.compacted + ? result.result?.tokensBefore != null && result.result?.tokensAfter != null + ? `Compacted (${runtime.formatTokenCount(result.result.tokensBefore)} → ${runtime.formatTokenCount(result.result.tokensAfter)})` + : result.result?.tokensBefore + ? `Compacted (${runtime.formatTokenCount(result.result.tokensBefore)} before)` + : "Compacted" + : "Compaction skipped" : "Compaction failed"; - if (result.ok && result.compacted && !codexNativeCompactionStarted) { + if (result.ok && result.compacted) { await runtime.incrementCompactionCount({ cfg: params.cfg, sessionEntry: targetSessionEntry, @@ -316,7 +301,9 @@ export const handleCompactCommand: CommandHandler = async (params) => { // Use the post-compaction token count for context summary if available const tokensAfterCompaction = result.result?.tokensAfter; const totalTokens = - tokensAfterCompaction ?? runtime.resolveFreshSessionTotalTokens(targetSessionEntry); + result.ok && result.compacted + ? tokensAfterCompaction + : runtime.resolveFreshSessionTotalTokens(targetSessionEntry); const contextSummary = runtime.formatContextUsageShort( typeof totalTokens === "number" && totalTokens > 0 ? totalTokens : null, contextTokenBudget ?? null, diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index 979fc3111740..ed890ce39064 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -35,7 +35,11 @@ export async function handleCommands(params: HandleCommandsParams): Promise(run: (storePath: string) => Promise): Promise } describe("commands session store persistence", () => { - it("persists a single command session entry through the accessor", async () => { + it("persists command state without reverting concurrent session management", async () => { await withTempStore(async (storePath) => { const sessionKey = "agent:main:command"; const otherKey = "agent:main:other"; @@ -24,39 +24,175 @@ describe("commands session store persistence", () => { sessionId: "command-session", updatedAt: 1, model: "gpt-5.5", + label: "Before rename", + pinnedAt: 100, }; const otherEntry: SessionEntry = { sessionId: "other-session", updatedAt: 2, }; + const concurrentUpdatedAt = 300; await saveSessionStore( storePath, { - [sessionKey]: { ...entry }, + [sessionKey]: { + ...entry, + updatedAt: concurrentUpdatedAt, + label: "After rename", + pinnedAt: undefined, + }, [otherKey]: { ...otherEntry }, }, { skipMaintenance: true }, ); const sessionStore: Record = { [sessionKey]: entry }; + const nowSpy = vi.spyOn(Date, "now").mockReturnValueOnce(200).mockReturnValue(400); + + try { + await expect( + persistSessionEntry({ + sessionEntry: entry, + sessionStore, + sessionKey, + storePath, + }), + ).resolves.toBe(true); + } finally { + nowSpy.mockRestore(); + } + + const persisted = loadSessionStore(storePath, { skipCache: true }); + expect(entry.updatedAt).not.toBe(1); + expect(sessionStore[sessionKey]).toMatchObject({ + sessionId: "command-session", + label: "After rename", + model: "gpt-5.5", + updatedAt: concurrentUpdatedAt, + }); + expect(sessionStore[sessionKey]?.pinnedAt).toBeUndefined(); + expect(persisted[sessionKey]).toMatchObject({ + sessionId: "command-session", + label: "After rename", + model: "gpt-5.5", + updatedAt: concurrentUpdatedAt, + }); + expect(persisted[sessionKey]?.pinnedAt).toBeUndefined(); + expect(persisted[otherKey]).toStrictEqual(otherEntry); + }); + }); + + it("rejects command persistence after the session rotates", async () => { + await withTempStore(async (storePath) => { + const sessionKey = "agent:main:command"; + const initialEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 1, + queueMode: "collect", + }; + const sessionEntry: SessionEntry = { + ...initialEntry, + queueMode: "followup", + }; + const rotatedEntry: SessionEntry = { + sessionId: "session-2", + updatedAt: 3, + queueMode: "interrupt", + }; + await saveSessionStore(storePath, { [sessionKey]: rotatedEntry }, { skipMaintenance: true }); + const sessionStore = { [sessionKey]: sessionEntry }; await expect( persistSessionEntry({ - sessionEntry: entry, + initialSessionEntry: initialEntry, + sessionEntry, sessionStore, sessionKey, storePath, }), - ).resolves.toBe(true); + ).resolves.toBe(false); - const persisted = loadSessionStore(storePath, { skipCache: true }); - expect(sessionStore[sessionKey]).toBe(entry); - expect(entry.updatedAt).not.toBe(1); - expect(persisted[sessionKey]).toMatchObject({ - sessionId: "command-session", - model: "gpt-5.5", - updatedAt: entry.updatedAt, + expect(sessionStore[sessionKey]).toEqual(rotatedEntry); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toEqual(rotatedEntry); + }); + }); + + it("rejects an explicit same-value command after a concurrent change", async () => { + await withTempStore(async (storePath) => { + const sessionKey = "agent:main:command"; + const initialEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 1, + sendPolicy: "deny", + }; + const sessionEntry = { ...initialEntry }; + const concurrentEntry: SessionEntry = { + ...initialEntry, + updatedAt: 2, + sendPolicy: "allow", + }; + await saveSessionStore( + storePath, + { [sessionKey]: concurrentEntry }, + { skipMaintenance: true }, + ); + const sessionStore = { [sessionKey]: sessionEntry }; + + await expect( + persistSessionEntry({ + initialSessionEntry: initialEntry, + sessionEntry, + sessionStore, + sessionKey, + storePath, + touchedFields: ["sendPolicy"], + }), + ).resolves.toBe(false); + + expect(sessionStore[sessionKey]).toMatchObject({ + sessionId: "session-1", + sendPolicy: "allow", }); - expect(persisted[otherKey]).toStrictEqual(otherEntry); + }); + }); + + it("rejects a grouped command before committing any non-conflicting field", async () => { + await withTempStore(async (storePath) => { + const sessionKey = "agent:main:command"; + const initialEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 1, + groupActivation: "mention", + groupActivationNeedsSystemIntro: true, + }; + const sessionEntry: SessionEntry = { + ...initialEntry, + groupActivation: "always", + }; + const concurrentEntry: SessionEntry = { + ...initialEntry, + updatedAt: 2, + groupActivationNeedsSystemIntro: false, + }; + await saveSessionStore( + storePath, + { [sessionKey]: concurrentEntry }, + { skipMaintenance: true }, + ); + const sessionStore = { [sessionKey]: sessionEntry }; + + await expect( + persistSessionEntry({ + initialSessionEntry: initialEntry, + sessionEntry, + sessionStore, + sessionKey, + storePath, + touchedFields: ["groupActivation", "groupActivationNeedsSystemIntro"], + }), + ).resolves.toBe(false); + + expect(sessionStore[sessionKey]).toEqual(concurrentEntry); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toEqual(concurrentEntry); }); }); diff --git a/src/auto-reply/reply/commands-session-store.ts b/src/auto-reply/reply/commands-session-store.ts index f07dc49d325c..2afc9faeffc8 100644 --- a/src/auto-reply/reply/commands-session-store.ts +++ b/src/auto-reply/reply/commands-session-store.ts @@ -1,14 +1,16 @@ // Shared session-store helpers for command handlers that mutate sessions. import { resolveSessionStoreEntry, type SessionEntry } from "../../config/sessions.js"; import { patchSessionEntry } from "../../config/sessions/session-accessor.js"; +import { sessionSnapshotChangesApplied } from "../../config/sessions/session-snapshot-merge.js"; import { applyAbortCutoffToSessionEntry, type AbortCutoff } from "./abort-cutoff.js"; -import type { CommandHandler } from "./commands-types.js"; +import type { CommandHandler, CommandHandlerResult } from "./commands-types.js"; +import { persistReplySessionEntry } from "./session-entry-persistence.js"; type CommandParams = Parameters[0]; type PersistSessionEntryParams = Pick< CommandParams, - "sessionEntry" | "sessionStore" | "sessionKey" | "storePath" ->; + "sessionEntry" | "initialSessionEntry" | "sessionStore" | "sessionKey" | "storePath" +> & { touchedFields?: ReadonlyArray }; /** Resolves a command target entry through canonical and legacy session keys. */ export function resolveCommandSessionEntryForKey( @@ -33,25 +35,45 @@ export async function persistSessionEntry(params: PersistSessionEntryParams): Pr return false; } const sessionEntry = params.sessionEntry; + const initialEntry = params.initialSessionEntry ?? { ...sessionEntry }; sessionEntry.updatedAt = Date.now(); params.sessionStore[params.sessionKey] = sessionEntry; if (params.storePath) { // Slash commands mutate one known session entry; skipping global session // maintenance avoids scanning the whole sessions directory for simple // command-only writes. - await patchSessionEntry( - { storePath: params.storePath, sessionKey: params.sessionKey }, - () => sessionEntry, - { - fallbackEntry: sessionEntry, - replaceEntry: true, - skipMaintenance: true, - }, - ); + const persistence = await persistReplySessionEntry({ + storePath: params.storePath, + sessionKey: params.sessionKey, + initialEntry, + entry: sessionEntry, + skipMaintenance: true, + touchedFields: params.touchedFields, + }); + if (persistence.status === "lifecycle-invalidated") { + if (persistence.entry) { + params.sessionStore[params.sessionKey] = persistence.entry; + } + return false; + } + params.sessionStore[params.sessionKey] = persistence.entry; + return sessionSnapshotChangesApplied({ + initial: initialEntry, + next: sessionEntry, + current: persistence.entry, + touchedFields: params.touchedFields, + }); } return true; } +export function sessionEntryPersistenceConflictReply(): CommandHandlerResult { + return { + shouldContinue: false, + reply: { text: "⚠️ Session changed before this setting could be saved. Retry the command." }, + }; +} + export async function persistAbortTargetEntry(params: { entry?: SessionEntry; key?: string; diff --git a/src/auto-reply/reply/commands-session.activation.test.ts b/src/auto-reply/reply/commands-session.activation.test.ts index c59ee1f3a4b9..9b8a3e328275 100644 --- a/src/auto-reply/reply/commands-session.activation.test.ts +++ b/src/auto-reply/reply/commands-session.activation.test.ts @@ -3,9 +3,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { HandleCommandsParams } from "./commands-types.js"; const persistSessionEntryMock = vi.hoisted(() => vi.fn(async () => true)); +const persistenceConflictReply = vi.hoisted(() => ({ + shouldContinue: false, + reply: { text: "retry session command" }, +})); vi.mock("./commands-session-store.js", () => ({ persistSessionEntry: persistSessionEntryMock, + sessionEntryPersistenceConflictReply: () => persistenceConflictReply, })); function buildActivationParams( @@ -64,6 +69,7 @@ function buildActivationParams( describe("handleActivationCommand", () => { beforeEach(() => { persistSessionEntryMock.mockClear(); + persistSessionEntryMock.mockResolvedValue(true); }); it("rejects authorized non-owner senders without changing group activation", async () => { @@ -90,6 +96,17 @@ describe("handleActivationCommand", () => { }); expect(params.sessionEntry?.groupActivation).toBe("always"); expect(params.sessionEntry?.groupActivationNeedsSystemIntro).toBe(true); - expect(persistSessionEntryMock).toHaveBeenCalledWith(params); + expect(persistSessionEntryMock).toHaveBeenCalledWith({ + ...params, + touchedFields: ["groupActivation", "groupActivationNeedsSystemIntro"], + }); + }); + + it("reports a concurrent session change instead of acknowledging persistence", async () => { + const { handleActivationCommand } = await import("./commands-session.js"); + const params = buildActivationParams(); + persistSessionEntryMock.mockResolvedValueOnce(false); + + await expect(handleActivationCommand(params, true)).resolves.toEqual(persistenceConflictReply); }); }); diff --git a/src/auto-reply/reply/commands-session.ts b/src/auto-reply/reply/commands-session.ts index 5b80e50d10a5..edc30f7bdc35 100644 --- a/src/auto-reply/reply/commands-session.ts +++ b/src/auto-reply/reply/commands-session.ts @@ -48,7 +48,10 @@ import { import { resolveCommandSurfaceChannel } from "./channel-context.js"; import { rejectNonOwnerCommand, rejectUnauthorizedCommand } from "./command-gates.js"; import { handleAbortTrigger, handleStopCommand } from "./commands-session-abort.js"; -import { persistSessionEntry } from "./commands-session-store.js"; +import { + persistSessionEntry, + sessionEntryPersistenceConflictReply, +} from "./commands-session-store.js"; import type { CommandHandler, HandleCommandsParams } from "./commands-types.js"; import { resolveConversationBindingContextFromAcpCommand } from "./conversation-binding-input.js"; @@ -230,7 +233,14 @@ export const handleActivationCommand: CommandHandler = async (params, allowTextC if (params.sessionEntry && params.sessionStore && params.sessionKey) { params.sessionEntry.groupActivation = activationCommand.mode; params.sessionEntry.groupActivationNeedsSystemIntro = true; - await persistSessionEntry(params); + if ( + !(await persistSessionEntry({ + ...params, + touchedFields: ["groupActivation", "groupActivationNeedsSystemIntro"], + })) + ) { + return sessionEntryPersistenceConflictReply(); + } } return { shouldContinue: false, @@ -268,7 +278,9 @@ export const handleSendPolicyCommand: CommandHandler = async (params, allowTextC } else { params.sessionEntry.sendPolicy = sendPolicyCommand.mode; } - await persistSessionEntry(params); + if (!(await persistSessionEntry({ ...params, touchedFields: ["sendPolicy"] }))) { + return sessionEntryPersistenceConflictReply(); + } } const label = sendPolicyCommand.mode === "inherit" @@ -357,7 +369,15 @@ export const handleUsageCommand: CommandHandler = async (params, allowTextComman if (targetSessionEntry && params.sessionStore && params.sessionKey) { delete targetSessionEntry.responseUsage; params.sessionStore[params.sessionKey] = targetSessionEntry; - await persistSessionEntry({ ...params, sessionEntry: targetSessionEntry }); + if ( + !(await persistSessionEntry({ + ...params, + sessionEntry: targetSessionEntry, + touchedFields: ["responseUsage"], + })) + ) { + return sessionEntryPersistenceConflictReply(); + } } return { shouldContinue: false, @@ -377,7 +397,15 @@ export const handleUsageCommand: CommandHandler = async (params, allowTextComman if (targetSessionEntry && params.sessionStore && params.sessionKey) { targetSessionEntry.responseUsage = next; params.sessionStore[params.sessionKey] = targetSessionEntry; - await persistSessionEntry({ ...params, sessionEntry: targetSessionEntry }); + if ( + !(await persistSessionEntry({ + ...params, + sessionEntry: targetSessionEntry, + touchedFields: ["responseUsage"], + })) + ) { + return sessionEntryPersistenceConflictReply(); + } } return { @@ -437,7 +465,15 @@ export const handleFastCommand: CommandHandler = async (params, allowTextCommand if (resetsToDefault) { if (targetSessionEntry && params.sessionStore && params.sessionKey) { delete targetSessionEntry.fastMode; - await persistSessionEntry({ ...params, sessionEntry: targetSessionEntry }); + if ( + !(await persistSessionEntry({ + ...params, + sessionEntry: targetSessionEntry, + touchedFields: ["fastMode"], + })) + ) { + return sessionEntryPersistenceConflictReply(); + } } return { shouldContinue: false, @@ -452,7 +488,15 @@ export const handleFastCommand: CommandHandler = async (params, allowTextCommand if (targetSessionEntry && params.sessionStore && params.sessionKey) { targetSessionEntry.fastMode = nextMode; - await persistSessionEntry({ ...params, sessionEntry: targetSessionEntry }); + if ( + !(await persistSessionEntry({ + ...params, + sessionEntry: targetSessionEntry, + touchedFields: ["fastMode"], + })) + ) { + return sessionEntryPersistenceConflictReply(); + } } return { diff --git a/src/auto-reply/reply/commands-tts.ts b/src/auto-reply/reply/commands-tts.ts index c5945f65cbca..ceecd9cbbe85 100644 --- a/src/auto-reply/reply/commands-tts.ts +++ b/src/auto-reply/reply/commands-tts.ts @@ -33,7 +33,10 @@ import { } from "../../tts/tts.js"; import { isSilentReplyPayloadText } from "../tokens.js"; import type { ReplyPayload } from "../types.js"; -import { persistSessionEntry } from "./commands-session-store.js"; +import { + persistSessionEntry, + sessionEntryPersistenceConflictReply, +} from "./commands-session-store.js"; import type { CommandHandler } from "./commands-types.js"; type ParsedTtsCommand = { @@ -228,17 +231,23 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand } if (requested === "on") { params.sessionEntry.ttsAuto = "always"; - await persistSessionEntry(params); + if (!(await persistSessionEntry({ ...params, touchedFields: ["ttsAuto"] }))) { + return sessionEntryPersistenceConflictReply(); + } return { shouldContinue: false, reply: { text: "🔊 TTS enabled for this chat." } }; } if (requested === "off") { params.sessionEntry.ttsAuto = "off"; - await persistSessionEntry(params); + if (!(await persistSessionEntry({ ...params, touchedFields: ["ttsAuto"] }))) { + return sessionEntryPersistenceConflictReply(); + } return { shouldContinue: false, reply: { text: "🔇 TTS disabled for this chat." } }; } if (requested === "default" || requested === "inherit" || requested === "clear") { delete params.sessionEntry.ttsAuto; - await persistSessionEntry(params); + if (!(await persistSessionEntry({ ...params, touchedFields: ["ttsAuto"] }))) { + return sessionEntryPersistenceConflictReply(); + } return { shouldContinue: false, reply: { text: "🔊 TTS chat override cleared." } }; } return { shouldContinue: false, reply: ttsUsage() }; @@ -289,7 +298,14 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand params.sessionEntry.lastTtsReadLatestHash = hash; params.sessionEntry.lastTtsReadLatestAt = Date.now(); - await persistSessionEntry(params); + if ( + !(await persistSessionEntry({ + ...params, + touchedFields: ["lastTtsReadLatestHash", "lastTtsReadLatestAt"], + })) + ) { + return sessionEntryPersistenceConflictReply(); + } return { shouldContinue: false, reply: audio.reply }; } diff --git a/src/auto-reply/reply/commands-types.ts b/src/auto-reply/reply/commands-types.ts index 3082508069c9..29e18c18365d 100644 --- a/src/auto-reply/reply/commands-types.ts +++ b/src/auto-reply/reply/commands-types.ts @@ -1,9 +1,9 @@ +import type { FastMode } from "@openclaw/normalization-core/string-coerce"; /** Shared command handler context and result contracts. */ import type { BlockReplyChunking } from "../../agents/embedded-agent-block-chunker.js"; import type { ChannelId } from "../../channels/plugins/types.public.js"; import type { SessionEntry, SessionScope } from "../../config/sessions.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import type { FastMode } from "@openclaw/normalization-core/string-coerce"; import type { SkillCommandSpec } from "../../skills/types.js"; import type { MsgContext } from "../templating.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; @@ -49,6 +49,8 @@ export type HandleCommandsParams = { failures: Array<{ gate: string; key: string }>; }; sessionEntry?: SessionEntry; + /** Snapshot captured before command handlers mutate the active entry. */ + initialSessionEntry?: SessionEntry; previousSessionEntry?: SessionEntry; sessionStore?: Record; sessionKey: string; diff --git a/src/auto-reply/reply/directive-handling.fast-lane.ts b/src/auto-reply/reply/directive-handling.fast-lane.ts index 151d6a5578d1..81d34db095ac 100644 --- a/src/auto-reply/reply/directive-handling.fast-lane.ts +++ b/src/auto-reply/reply/directive-handling.fast-lane.ts @@ -7,7 +7,12 @@ import type { ApplyInlineDirectivesFastLaneParams } from "./directive-handling.p export async function applyInlineDirectivesFastLane( params: ApplyInlineDirectivesFastLaneParams, -): Promise<{ directiveAck?: ReplyPayload; provider: string; model: string }> { +): Promise<{ + directiveAck?: ReplyPayload; + provider: string; + model: string; + sessionChangesApplied: boolean; +}> { const { directives, commandAuthorized, @@ -45,7 +50,7 @@ export async function applyInlineDirectivesFastLane( isGroup, }) ) { - return { directiveAck: undefined, provider, model }; + return { directiveAck: undefined, provider, model, sessionChangesApplied: true }; } const agentCfg = params.agentCfg; @@ -63,6 +68,7 @@ export async function applyInlineDirectivesFastLane( : async () => undefined, }); + const persistenceState = { sessionChangesApplied: true }; const directiveAck = await handleDirectiveOnly({ cfg, directives, @@ -97,6 +103,7 @@ export async function applyInlineDirectivesFastLane( commandAuthorized, senderIsOwner: params.senderIsOwner, workspaceDir: params.workspaceDir, + persistenceState, }); if (sessionEntry?.providerOverride) { @@ -106,5 +113,5 @@ export async function applyInlineDirectivesFastLane( model = sessionEntry.modelOverride; } - return { directiveAck, provider, model }; + return { directiveAck, provider, model, ...persistenceState }; } diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index 75c3bd3f2fc5..d8400888aa5b 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -10,7 +10,11 @@ import { resolveFastModeState, } from "../../agents/fast-mode.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; -import { replaceSessionEntry } from "../../config/sessions/session-accessor.js"; +import { + adoptPersistedSessionSnapshot, + sessionModelOverrideChangesApplied, + sessionSnapshotChangesApplied, +} from "../../config/sessions/session-snapshot-merge.js"; import { triggerSessionPatchHook } from "../../gateway/session-patch-hooks.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; import { applyTraceOverride, applyVerboseOverride } from "../../sessions/level-overrides.js"; @@ -34,11 +38,13 @@ import { formatInternalVerboseCurrentReplyOnlyText, formatInternalVerbosePersistenceDeniedText, enqueueModeSwitchEvents, + resolveDirectiveTouchedSessionFields, withOptions, } from "./directive-handling.shared.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel } from "./directives.js"; import { refreshQueuedFollowupSession } from "./queue.js"; import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js"; +import { persistReplySessionEntry } from "./session-entry-persistence.js"; /** Handles inline directives that can be acknowledged without a model turn. */ export async function handleDirectiveOnly( @@ -372,6 +378,17 @@ export async function handleDirectiveOnly( elevatedEnabled && elevatedAllowed; let modelSelectionUpdated = false; + let modelSelectionApplied = true; + let sessionChangesApplied = true; + let appliedSessionEntry = sessionEntry; + const touchedSessionFields = resolveDirectiveTouchedSessionFields({ + directives, + allowInternalExecPersistence, + allowInternalVerbosePersistence, + }); + if (shouldRemapUnsupportedThinkLevel && !touchedSessionFields.includes("thinkingLevel")) { + touchedSessionFields.push("thinkingLevel"); + } const shouldPersistSessionEntry = (directives.hasThinkDirective && (Boolean(directives.thinkLevel) || directives.clearThinkLevel)) || @@ -395,6 +412,7 @@ export async function handleDirectiveOnly( let reasoningChanged = directives.hasReasoningDirective && directives.reasoningLevel !== undefined; if (shouldPersistSessionEntry) { + const initialSessionEntry = { ...sessionEntry }; if (directives.clearThinkLevel) { delete sessionEntry.thinkingLevel; } else if ( @@ -485,12 +503,64 @@ export async function handleDirectiveOnly( sessionEntry.updatedAt = Date.now(); sessionStore[sessionKey] = sessionEntry; if (storePath) { - await replaceSessionEntry({ storePath, sessionKey }, sessionEntry); + const persistence = await persistReplySessionEntry({ + storePath, + sessionKey, + initialEntry: initialSessionEntry, + entry: sessionEntry, + reassertLiveModelSwitchPending: + modelSelectionUpdated && sessionEntry.liveModelSwitchPending === true, + touchedFields: touchedSessionFields, + }); + if (persistence.status === "current") { + const persistedEntry = persistence.entry; + sessionStore[sessionKey] = persistedEntry; + sessionChangesApplied = sessionSnapshotChangesApplied({ + initial: initialSessionEntry, + next: sessionEntry, + current: persistedEntry, + touchedFields: touchedSessionFields, + }); + if (modelSelection) { + modelSelectionApplied = + sessionChangesApplied && + sessionModelOverrideChangesApplied({ + initial: initialSessionEntry, + next: sessionEntry, + current: persistedEntry, + reassertLiveModelSwitchPending: + modelSelectionUpdated && sessionEntry.liveModelSwitchPending === true, + }); + } + adoptPersistedSessionSnapshot(sessionEntry, persistedEntry); + appliedSessionEntry = sessionEntry; + } else { + if (persistence.entry) { + sessionStore[sessionKey] = persistence.entry; + } + sessionChangesApplied = false; + if (modelSelection) { + modelSelectionApplied = false; + } + } } - if (modelSelection && modelSelectionUpdated && sessionKey) { + if (modelSelection && !modelSelectionApplied) { + sessionChangesApplied = false; + } + if (!sessionChangesApplied) { + if (params.persistenceState) { + params.persistenceState.sessionChangesApplied = false; + } + return { + text: modelSelection + ? "Model change was not applied because the session changed. Retry." + : "Session settings were not applied because the session changed. Retry.", + }; + } + if (modelSelection && modelSelectionUpdated && modelSelectionApplied && sessionKey) { triggerSessionPatchHook({ cfg: params.cfg, - sessionEntry, + sessionEntry: appliedSessionEntry, sessionKey, patch: { key: sessionKey, @@ -511,7 +581,7 @@ export async function handleDirectiveOnly( }); } } - if (modelSelection) { + if (modelSelection && modelSelectionApplied) { const nextLabel = `${modelSelection.provider}/${modelSelection.model}`; if (nextLabel !== initialModelLabel) { enqueueSystemEvent(formatModelSwitchEvent(nextLabel, modelSelection.alias), { @@ -522,7 +592,7 @@ export async function handleDirectiveOnly( } enqueueModeSwitchEvents({ enqueueSystemEvent, - sessionEntry, + sessionEntry: appliedSessionEntry, sessionKey, elevatedChanged, reasoningChanged, @@ -637,7 +707,7 @@ export async function handleDirectiveOnly( `Thinking level set to ${remappedUnsupportedThinkLevel} (${nextThinkLevel} not supported for ${resolvedProvider}/${resolvedModel}).`, ); } - if (modelSelection) { + if (modelSelection && modelSelectionApplied) { const label = `${modelSelection.provider}/${modelSelection.model}`; const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label; parts.push( @@ -648,6 +718,8 @@ export async function handleDirectiveOnly( if (profileOverride) { parts.push(`Auth profile set to ${profileOverride}.`); } + } else if (modelSelection) { + parts.push("Model change was not applied because the session changed. Retry."); } if (directives.hasQueueDirective && directives.queueMode) { parts.push(formatDirectiveAck(`Queue mode set to ${directives.queueMode}.`)); diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index 2e4845ddb5b0..8c6a1096f2c6 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -281,6 +281,7 @@ import { import type { ModelAliasIndex } from "../../agents/model-selection.js"; import type { ModelDefinitionConfig, OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; +import { loadSessionStore, saveSessionStore } from "../../config/sessions/store.js"; import { clearInternalHooks, registerInternalHook, @@ -1449,7 +1450,6 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { { provider: "openai", id: "gpt-4o", name: "GPT-4o" }, ]; const sessionKey = "agent:main:dm:1"; - const storePath = "/tmp/sessions.json"; type HandleParams = Parameters[0]; @@ -1464,7 +1464,7 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { cfg: baseConfig(), directives: rest.directives ?? parseInlineDirectives(""), sessionKey, - storePath, + storePath: undefined, elevatedEnabled: false, elevatedAllowed: false, defaultProvider: "anthropic", @@ -1611,6 +1611,164 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { }); }); + it("suppresses model side effects when a concurrent switch wins", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-directive-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const sessionEntry = createSessionEntry({ + providerOverride: "anthropic", + modelOverride: "claude-opus-4-6", + modelOverrideSource: "user", + }); + const concurrentEntry: SessionEntry = { + ...sessionEntry, + updatedAt: sessionEntry.updatedAt + 1, + providerOverride: "openai", + modelOverride: "gpt-5.5", + }; + await saveSessionStore(storePath, { [sessionKey]: concurrentEntry }, { skipMaintenance: true }); + const sessionStore = { [sessionKey]: sessionEntry }; + const persistenceState = { sessionChangesApplied: true }; + + try { + const result = await handleDirectiveOnly( + createHandleParams({ + directives: parseInlineDirectives("/model openai/gpt-4o"), + sessionEntry, + sessionStore, + storePath, + persistenceState, + }), + ); + + expect(result?.text).toContain("Model change was not applied"); + expect(persistenceState.sessionChangesApplied).toBe(false); + expect(queueMocks.refreshQueuedFollowupSession).not.toHaveBeenCalled(); + expect(enqueueSystemEvent).not.toHaveBeenCalledWith( + expect.stringContaining("openai/gpt-4o"), + expect.anything(), + ); + expect(sessionStore[sessionKey]).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-5.5", + }); + expect(sessionStore[sessionKey]?.liveModelSwitchPending).toBeUndefined(); + expect(sessionEntry).toEqual(sessionStore[sessionKey]); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toEqual( + sessionStore[sessionKey], + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("reports a rejected non-model directive after session rotation", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-elevated-directive-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const sessionEntry = createSessionEntry({ elevatedLevel: "full" }); + const rotatedEntry: SessionEntry = { + sessionId: "s2", + updatedAt: sessionEntry.updatedAt + 1, + elevatedLevel: "full", + }; + await saveSessionStore(storePath, { [sessionKey]: rotatedEntry }, { skipMaintenance: true }); + const sessionStore = { [sessionKey]: sessionEntry }; + + try { + const result = await handleDirectiveOnly( + createHandleParams({ + directives: parseInlineDirectives("/elevated off"), + sessionEntry, + sessionStore, + storePath, + elevatedEnabled: true, + elevatedAllowed: true, + currentElevatedLevel: "full", + }), + ); + + expect(result?.text).toContain("Session settings were not applied"); + expect(result?.text).not.toContain("Elevated mode disabled"); + expect(enqueueSystemEvent).not.toHaveBeenCalledWith( + expect.stringContaining("Elevated"), + expect.anything(), + ); + expect(sessionStore[sessionKey]).toEqual(rotatedEntry); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("rejects an explicit same-value directive after a concurrent change", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-elevated-directive-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const sessionEntry = createSessionEntry({ elevatedLevel: "off" }); + const concurrentEntry: SessionEntry = { + ...sessionEntry, + updatedAt: sessionEntry.updatedAt + 1, + elevatedLevel: "full", + }; + await saveSessionStore(storePath, { [sessionKey]: concurrentEntry }, { skipMaintenance: true }); + + try { + const result = await handleDirectiveOnly( + createHandleParams({ + directives: parseInlineDirectives("/elevated off"), + sessionEntry, + sessionStore: { [sessionKey]: sessionEntry }, + storePath, + elevatedEnabled: true, + elevatedAllowed: true, + currentElevatedLevel: "off", + }), + ); + + expect(result?.text).toContain("Session settings were not applied"); + expect(sessionEntry).toMatchObject({ sessionId: "s1", elevatedLevel: "full" }); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("rejects a grouped directive when its implicit thinking remap conflicts", async () => { + setDirectiveTestProviders([ + { + id: "anthropic", + label: "Anthropic", + auth: [], + resolveThinkingProfile: () => ({ + levels: [{ id: "off" }, { id: "low" }, { id: "high" }], + }), + }, + ]); + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-thinking-remap-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const sessionEntry = createSessionEntry({ thinkingLevel: "xhigh" }); + const concurrentEntry: SessionEntry = { + ...sessionEntry, + updatedAt: sessionEntry.updatedAt + 1, + thinkingLevel: "low", + }; + await saveSessionStore(storePath, { [sessionKey]: concurrentEntry }, { skipMaintenance: true }); + + try { + const result = await handleDirectiveOnly( + createHandleParams({ + directives: parseInlineDirectives("/fast on"), + sessionEntry, + sessionStore: { [sessionKey]: sessionEntry }, + storePath, + }), + ); + + expect(result?.text).toContain("Session settings were not applied"); + expect(sessionEntry).toMatchObject({ thinkingLevel: "low" }); + expect(sessionEntry.fastMode).toBeUndefined(); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toEqual(concurrentEntry); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + it("persists auth profile overrides for alias model directives", async () => { setAuthProfiles({ "anthropic:work": { @@ -2079,6 +2237,125 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { }); describe("persistInlineDirectives session directive persistence policy", () => { + it("checks an explicit same-value model selection against persisted state", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-inline-model-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const sessionKey = "agent:main:dm:same-model"; + const sessionEntry = createSessionEntry({ + providerOverride: "openai", + modelOverride: "gpt-4o", + modelOverrideSource: "user", + }); + const concurrentEntry: SessionEntry = { + ...sessionEntry, + updatedAt: sessionEntry.updatedAt + 1, + modelOverride: "gpt-5.5", + }; + await saveSessionStore(storePath, { [sessionKey]: concurrentEntry }, { skipMaintenance: true }); + const directives = parseInlineDirectives("hello /model openai/gpt-4o"); + + try { + const result = await persistInlineDirectives({ + directives, + effectiveModelDirective: directives.rawModelDirective, + cfg: baseConfig(), + agentDir: TEST_AGENT_DIR, + sessionEntry, + sessionStore: { [sessionKey]: sessionEntry }, + sessionKey, + storePath, + elevatedEnabled: false, + elevatedAllowed: false, + defaultProvider: "anthropic", + defaultModel: "claude-opus-4-6", + aliasIndex: baseAliasIndex(), + allowedModelKeys: new Set(["openai/gpt-4o"]), + modelCatalog: [{ provider: "openai", id: "gpt-4o", name: "GPT-4o" }], + provider: "openai", + model: "gpt-4o", + initialModelLabel: "openai/gpt-4o", + formatModelSwitchEvent: (label) => `Switched to ${label}`, + agentCfg: undefined, + }); + + expect(result.sessionChangesApplied).toBe(false); + expect(result).toMatchObject({ provider: "openai", model: "gpt-5.5" }); + expect(sessionEntry).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-5.5", + }); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("returns the concurrent model winner without emitting switch side effects", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-inline-model-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const sessionKey = "agent:main:dm:race"; + const sessionEntry = createSessionEntry({ + providerOverride: "anthropic", + modelOverride: "claude-opus-4-6", + modelOverrideSource: "user", + }); + const concurrentEntry: SessionEntry = { + ...sessionEntry, + updatedAt: sessionEntry.updatedAt + 1, + providerOverride: "openai", + modelOverride: "gpt-5.5", + }; + await saveSessionStore(storePath, { [sessionKey]: concurrentEntry }, { skipMaintenance: true }); + const sessionStore = { [sessionKey]: sessionEntry }; + const directives = parseInlineDirectives("hello /model openai/gpt-4o"); + const patchEvents: InternalHookEvent[] = []; + registerInternalHook("session:patch", async (event) => { + patchEvents.push(event); + }); + + try { + const result = await persistInlineDirectives({ + directives, + effectiveModelDirective: directives.rawModelDirective, + cfg: baseConfig(), + agentDir: TEST_AGENT_DIR, + sessionEntry, + sessionStore, + sessionKey, + storePath, + elevatedEnabled: false, + elevatedAllowed: false, + defaultProvider: "anthropic", + defaultModel: "claude-opus-4-6", + aliasIndex: baseAliasIndex(), + allowedModelKeys: new Set(["anthropic/claude-opus-4-6", "openai/gpt-4o"]), + modelCatalog: [ + { provider: "anthropic", id: "claude-opus-4-6", name: "Claude Opus 4.5" }, + { provider: "openai", id: "gpt-4o", name: "GPT-4o" }, + ], + provider: "anthropic", + model: "claude-opus-4-6", + initialModelLabel: "anthropic/claude-opus-4-6", + formatModelSwitchEvent: (label) => `Switched to ${label}`, + agentCfg: undefined, + }); + + expect(result).toMatchObject({ provider: "openai", model: "gpt-5.5" }); + expect(result.sessionChangesApplied).toBe(false); + expect(enqueueSystemEvent).not.toHaveBeenCalledWith( + expect.stringContaining("openai/gpt-4o"), + expect.anything(), + ); + expect(patchEvents).toEqual([]); + expect(sessionStore[sessionKey]).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-5.5", + }); + expect(sessionEntry).toEqual(sessionStore[sessionKey]); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + it("skips exec persistence for internal operator.write callers", async () => { const sessionEntry = await persistInternalOperatorWriteDirective( "/exec host=node security=allowlist ask=always node=worker-1", diff --git a/src/auto-reply/reply/directive-handling.params.ts b/src/auto-reply/reply/directive-handling.params.ts index 0044f5469c69..69248bab15f5 100644 --- a/src/auto-reply/reply/directive-handling.params.ts +++ b/src/auto-reply/reply/directive-handling.params.ts @@ -1,9 +1,9 @@ +import type { FastMode } from "@openclaw/normalization-core/string-coerce"; /** Parameter contracts shared by directive-only and fast-lane directive handlers. */ import type { ModelCatalogEntry } from "../../agents/model-catalog.js"; import type { ModelAliasIndex } from "../../agents/model-selection.js"; import type { SessionEntry } from "../../config/sessions.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import type { FastMode } from "@openclaw/normalization-core/string-coerce"; import type { MsgContext } from "../templating.js"; import type { InlineDirectives } from "./directive-handling.parse.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js"; @@ -49,6 +49,8 @@ export type HandleDirectiveOnlyParams = HandleDirectiveOnlyCoreParams & { gatewayClientScopes?: string[]; commandAuthorized?: boolean; senderIsOwner?: boolean; + /** Internal handoff for mixed inline directives to avoid retrying rejected writes. */ + persistenceState?: { sessionChangesApplied: boolean }; }; /** Inputs for applying inline directives before the full reply run is prepared. */ diff --git a/src/auto-reply/reply/directive-handling.persist.ts b/src/auto-reply/reply/directive-handling.persist.ts index 14759b3df395..e0096e2dd75e 100644 --- a/src/auto-reply/reply/directive-handling.persist.ts +++ b/src/auto-reply/reply/directive-handling.persist.ts @@ -13,7 +13,11 @@ import { type ModelAliasIndex, } from "../../agents/model-selection.js"; import { resolveContextConfigProviderForRuntime } from "../../agents/openai-routing.js"; -import { replaceSessionEntry } from "../../config/sessions/session-accessor.js"; +import { + adoptPersistedSessionSnapshot, + sessionModelOverrideChangesApplied, + sessionSnapshotChangesApplied, +} from "../../config/sessions/session-snapshot-merge.js"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { triggerSessionPatchHook } from "../../gateway/session-patch-hooks.js"; @@ -26,9 +30,11 @@ import type { InlineDirectives } from "./directive-handling.parse.js"; import { canPersistSessionDirectiveDefaults, enqueueModeSwitchEvents, + resolveDirectiveTouchedSessionFields, } from "./directive-handling.shared.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel } from "./directives.js"; import { resolveContextTokens } from "./model-selection.js"; +import { persistReplySessionEntry } from "./session-entry-persistence.js"; export type PersistedThinkingLevelRemap = { from: ThinkLevel; @@ -109,6 +115,7 @@ export async function persistInlineDirectives(params: { provider: string; model: string; contextTokens: number; + sessionChangesApplied: boolean; thinkingRemap?: PersistedThinkingLevelRemap; }> { const { @@ -130,6 +137,7 @@ export async function persistInlineDirectives(params: { } = params; let { provider, model } = params; let thinkingRemap: PersistedThinkingLevelRemap | undefined; + let sessionChangesApplied = true; const allowInternalExecPersistence = canPersistSessionDirectiveDefaults({ messageProvider: params.messageProvider, surface: params.surface, @@ -144,6 +152,11 @@ export async function persistInlineDirectives(params: { commandAuthorized: params.commandAuthorized, senderIsOwner: params.senderIsOwner, }); + const touchedSessionFields = resolveDirectiveTouchedSessionFields({ + directives, + allowInternalExecPersistence, + allowInternalVerbosePersistence, + }); const thinkingCatalog = params.thinkingCatalog && params.thinkingCatalog.length > 0 ? params.thinkingCatalog @@ -155,6 +168,8 @@ export async function persistInlineDirectives(params: { const agentDir = resolveAgentDir(cfg, activeAgentId) ?? params.agentDir; if (sessionEntry && sessionStore && sessionKey) { + const initialSessionEntry = { ...sessionEntry }; + let appliedSessionEntry = sessionEntry; const prevElevatedLevel = (sessionEntry.elevatedLevel as ElevatedLevel | undefined) ?? (agentCfg?.elevatedDefault as ElevatedLevel | undefined) ?? @@ -250,6 +265,9 @@ export async function persistInlineDirectives(params: { ? params.effectiveModelDirective : undefined; let modelUpdated = false; + let modelApplied = true; + let modelRuntimeEvent: { contextKey: string; text: string } | undefined; + let modelSwitchEvent: { alias?: string; label: string } | undefined; if (modelDirective) { const modelResolution = resolveModelSelectionFromDirective({ directives: { @@ -281,32 +299,23 @@ export async function persistInlineDirectives(params: { if (runtimeOverride?.kind === "clear") { if (sessionEntry.agentRuntimeOverride) { delete sessionEntry.agentRuntimeOverride; - updated = true; } } else if (runtimeOverride?.kind === "set") { if (sessionEntry.agentRuntimeOverride) { delete sessionEntry.agentRuntimeOverride; - updated = true; } - enqueueSystemEvent( - `Ignored session runtime ${runtimeOverride.runtime}; configure provider or model runtime policy instead.`, - { - sessionKey, - contextKey: `model-runtime:${modelResolution.modelSelection.provider}:${runtimeOverride.runtime}:ignored-session-runtime`, - }, - ); + modelRuntimeEvent = { + text: `Ignored session runtime ${runtimeOverride.runtime}; configure provider or model runtime policy instead.`, + contextKey: `model-runtime:${modelResolution.modelSelection.provider}:${runtimeOverride.runtime}:ignored-session-runtime`, + }; } else if (runtimeOverride?.kind === "invalid") { if (sessionEntry.agentRuntimeOverride) { delete sessionEntry.agentRuntimeOverride; - updated = true; } - enqueueSystemEvent( - `Ignored unsupported runtime ${runtimeOverride.runtime} for ${modelResolution.modelSelection.provider}.`, - { - sessionKey, - contextKey: `model-runtime:${modelResolution.modelSelection.provider}:${runtimeOverride.runtime}`, - }, - ); + modelRuntimeEvent = { + text: `Ignored unsupported runtime ${runtimeOverride.runtime} for ${modelResolution.modelSelection.provider}.`, + contextKey: `model-runtime:${modelResolution.modelSelection.provider}:${runtimeOverride.runtime}`, + }; } modelUpdated = appliedModelOverride.updated; provider = modelResolution.modelSelection.provider; @@ -336,20 +345,20 @@ export async function persistInlineDirectives(params: { provider, model, }; - updated = true; } } const nextLabel = `${provider}/${model}`; if (nextLabel !== initialModelLabel) { - enqueueSystemEvent( - formatModelSwitchEvent(nextLabel, modelResolution.modelSelection.alias), - { - sessionKey, - contextKey: `model:${nextLabel}`, - }, - ); + modelSwitchEvent = { + label: nextLabel, + ...(modelResolution.modelSelection.alias + ? { alias: modelResolution.modelSelection.alias } + : {}), + }; } - updated = updated || modelUpdated; + // Explicit model selections must still perform the atomic persisted + // winner check when their value matches the local snapshot. + updated = true; } } if (directives.hasQueueDirective && directives.queueReset) { @@ -364,22 +373,86 @@ export async function persistInlineDirectives(params: { sessionEntry.updatedAt = Date.now(); sessionStore[sessionKey] = sessionEntry; if (storePath) { - await replaceSessionEntry({ storePath, sessionKey }, sessionEntry); + const persistence = await persistReplySessionEntry({ + storePath, + sessionKey, + initialEntry: initialSessionEntry, + entry: sessionEntry, + reassertLiveModelSwitchPending: + modelUpdated && + params.markLiveSwitchPending === true && + sessionEntry.liveModelSwitchPending === true, + touchedFields: touchedSessionFields, + }); + if (persistence.status === "current") { + const persistedEntry = persistence.entry; + sessionStore[sessionKey] = persistedEntry; + sessionChangesApplied = sessionSnapshotChangesApplied({ + initial: initialSessionEntry, + next: sessionEntry, + current: persistedEntry, + touchedFields: touchedSessionFields, + }); + if (modelDirective) { + modelApplied = + sessionChangesApplied && + sessionModelOverrideChangesApplied({ + initial: initialSessionEntry, + next: sessionEntry, + current: persistedEntry, + reassertLiveModelSwitchPending: + modelUpdated && + params.markLiveSwitchPending === true && + sessionEntry.liveModelSwitchPending === true, + }); + } + adoptPersistedSessionSnapshot(sessionEntry, persistedEntry); + appliedSessionEntry = sessionEntry; + } else { + if (persistence.entry) { + sessionStore[sessionKey] = persistence.entry; + } + sessionChangesApplied = false; + if (modelDirective) { + modelApplied = false; + } + } } - if (modelDirective && modelUpdated) { + if (modelDirective && !modelApplied) { + sessionChangesApplied = false; + const persistedEntry = sessionStore[sessionKey]; + provider = persistedEntry?.providerOverride?.trim() || defaultProvider; + model = persistedEntry?.modelOverride?.trim() || defaultModel; + thinkingRemap = undefined; + } + if (modelDirective && modelUpdated && modelApplied) { triggerSessionPatchHook({ cfg, - sessionEntry, + sessionEntry: appliedSessionEntry, sessionKey, patch: { key: sessionKey, model: modelDirective }, }); } - enqueueModeSwitchEvents({ - enqueueSystemEvent, - sessionEntry, + if (sessionChangesApplied) { + enqueueModeSwitchEvents({ + enqueueSystemEvent, + sessionEntry: appliedSessionEntry, + sessionKey, + elevatedChanged, + reasoningChanged, + }); + } + } + if (modelRuntimeEvent && modelApplied) { + enqueueSystemEvent(modelRuntimeEvent.text, { sessionKey, - elevatedChanged, - reasoningChanged, + contextKey: modelRuntimeEvent.contextKey, + }); + } + if (modelSwitchEvent && modelApplied) { + enqueueSystemEvent(formatModelSwitchEvent(modelSwitchEvent.label, modelSwitchEvent.alias), { + sessionKey, + contextKey: `model:${modelSwitchEvent.label}`, }); } } @@ -391,6 +464,7 @@ export async function persistInlineDirectives(params: { provider, model, thinkingRemap, + sessionChangesApplied, contextTokens: resolveContextTokens({ cfg, agentCfg, diff --git a/src/auto-reply/reply/directive-handling.shared.ts b/src/auto-reply/reply/directive-handling.shared.ts index 803e9f5c4bf7..89a8319c9d0f 100644 --- a/src/auto-reply/reply/directive-handling.shared.ts +++ b/src/auto-reply/reply/directive-handling.shared.ts @@ -1,8 +1,11 @@ // Shared directive parsing helpers used by model and auth directive handlers. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { formatCliCommand } from "../../cli/command-format.js"; +import { SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS } from "../../config/sessions/session-snapshot-merge.js"; +import type { SessionEntry } from "../../config/sessions/types.js"; import { SYSTEM_MARK, prefixSystemMessage } from "../../infra/system-message.js"; import { isInternalMessageChannel } from "../../utils/message-channel.js"; +import type { InlineDirectives } from "./directive-handling.parse.js"; import type { ElevatedLevel, ReasoningLevel } from "./directives.js"; export const formatDirectiveAck = (text: string): string => { @@ -47,6 +50,68 @@ export function canPersistSessionDirectiveDefaults(params: { return params.commandAuthorized === true || params.senderIsOwner === true; } +/** Names explicit directive writes that snapshot equality cannot infer. */ +export function resolveDirectiveTouchedSessionFields(params: { + directives: InlineDirectives; + allowInternalExecPersistence: boolean; + allowInternalVerbosePersistence: boolean; +}): Array { + const { directives } = params; + const fields = new Set(); + if (directives.hasThinkDirective) { + fields.add("thinkingLevel"); + } + if (directives.hasFastDirective) { + fields.add("fastMode"); + } + if (directives.hasVerboseDirective && params.allowInternalVerbosePersistence) { + fields.add("verboseLevel"); + } + if (directives.hasTraceDirective) { + fields.add("traceLevel"); + } + if (directives.hasReasoningDirective) { + fields.add("reasoningLevel"); + } + if (directives.hasElevatedDirective) { + fields.add("elevatedLevel"); + } + if (directives.hasModelDirective) { + for (const field of SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS) { + fields.add(field); + } + } + if (directives.hasExecDirective && params.allowInternalExecPersistence) { + if (directives.execHost) { + fields.add("execHost"); + } + if (directives.execSecurity) { + fields.add("execSecurity"); + } + if (directives.execAsk) { + fields.add("execAsk"); + } + if (directives.execNode) { + fields.add("execNode"); + } + } + if (directives.hasQueueDirective) { + if (directives.queueReset || directives.queueMode) { + fields.add("queueMode"); + } + if (directives.queueReset || typeof directives.debounceMs === "number") { + fields.add("queueDebounceMs"); + } + if (directives.queueReset || typeof directives.cap === "number") { + fields.add("queueCap"); + } + if (directives.queueReset || directives.dropPolicy) { + fields.add("queueDrop"); + } + } + return [...fields]; +} + const formatElevatedEvent = (level: ElevatedLevel) => { if (level === "full") { return "Elevated FULL - exec runs on host with auto-approval."; diff --git a/src/auto-reply/reply/dispatch-from-config.acp-abort.test.ts b/src/auto-reply/reply/dispatch-from-config.acp-abort.test.ts index 4bc68f9028df..23edfb843430 100644 --- a/src/auto-reply/reply/dispatch-from-config.acp-abort.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.acp-abort.test.ts @@ -196,6 +196,9 @@ describe("dispatchReplyFromConfig ACP abort", () => { internalHookMocks.createInternalHookEvent.mockImplementation(createInternalHookEventPayload); internalHookMocks.triggerInternalHook.mockReset(); sessionStoreMocks.currentEntry = undefined; + sessionStoreMocks.loadSessionEntry + .mockReset() + .mockImplementation(() => sessionStoreMocks.currentEntry); sessionStoreMocks.loadSessionStore.mockReset().mockReturnValue({}); sessionStoreMocks.readSessionEntry.mockReset().mockReturnValue(undefined); sessionStoreMocks.resolveStorePath.mockReset().mockReturnValue("/tmp/mock-sessions.json"); @@ -514,6 +517,7 @@ describe("dispatchReplyFromConfig ACP abort", () => { }, }; sessionBindingMocks.resolveByConversation.mockReturnValue(boundConversation); + sessionStoreMocks.currentEntry = sessionStore[sourceSessionKey]; sessionStoreMocks.loadSessionStore.mockReturnValue(sessionStore); sessionStoreMocks.resolveSessionStoreEntry.mockImplementation((...args: unknown[]) => { const params = args[0] as { store?: Record; sessionKey?: string }; @@ -883,8 +887,12 @@ describe("dispatchReplyFromConfig ACP abort", () => { }); await hookStartedPromise; - expect(hookAbortSignal).toBe(existingOperation.abortSignal); + // The hook signal composes the operation signal with lifecycle/upstream + // signals, so assert propagation instead of instance identity. + expect(hookAbortSignal?.aborted).toBe(false); expect(replyRunRegistry.abort("agent:already-active-reply-dispatch")).toBe(true); + expect(existingOperation.abortSignal.aborted).toBe(true); + expect(hookAbortSignal?.aborted).toBe(true); await expect(dispatchPromise).resolves.toMatchObject({ queuedFinal: false, diff --git a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts index 3d27a70aa702..3c25ec5f403d 100644 --- a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts +++ b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts @@ -104,6 +104,7 @@ const pluginConversationBindingMocks = vi.hoisted(() => ({ })); const sessionStoreMocks = vi.hoisted(() => ({ currentEntry: undefined as Record | undefined, + loadSessionEntry: vi.fn((..._args: unknown[]) => sessionStoreMocks.currentEntry), loadSessionStore: vi.fn(() => ({})), readSessionEntry: vi.fn(() => sessionStoreMocks.currentEntry), resolveStorePath: vi.fn(() => "/tmp/mock-sessions.json"), @@ -233,6 +234,13 @@ vi.mock("../../config/sessions/thread-info.js", () => ({ parseSessionThreadInfoFast: (sessionKey: string | undefined) => threadInfoMocks.parseSessionThreadInfo(sessionKey), })); +vi.mock("../../config/sessions/session-accessor.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadSessionEntry: (...args: unknown[]) => sessionStoreMocks.loadSessionEntry(...args), + }; +}); vi.mock("./dispatch-from-config.runtime.js", () => ({ createInternalHookEvent: internalHookMocks.createInternalHookEvent, loadSessionStore: sessionStoreMocks.loadSessionStore, diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index 341aeda3a24d..826ad8a4b8d8 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -1,4 +1,5 @@ // Tests dispatch-from-config runtime selection, hooks, and provider handoff. +import { AsyncResource } from "node:async_hooks"; import { beforeAll, beforeEach, describe, expect, it, vi, type Mock } from "vitest"; import { clearAgentHarnesses, registerAgentHarness } from "../../agents/harness/registry.js"; import type { ChannelMessagingAdapter } from "../../channels/plugins/types.core.js"; @@ -28,6 +29,11 @@ import type { PluginTargetedInboundClaimOutcome, } from "../../plugins/hooks.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { + interruptSessionWorkAdmissions, + isSessionWorkAdmissionActive, + runExclusiveSessionLifecycleMutation, +} from "../../sessions/session-lifecycle-admission.js"; import { createChannelTestPluginBase, createTestRegistry, @@ -150,15 +156,21 @@ const pluginConversationBindingMocks = vi.hoisted(() => ({ const sessionStoreMocks = vi.hoisted(() => ({ currentEntry: undefined as Record | undefined, entriesBySessionKey: new Map>(), + loadSessionEntry: vi.fn((..._args: unknown[]) => sessionStoreMocks.currentEntry), loadSessionStore: vi.fn(() => ({})), readSessionEntry: vi.fn(() => sessionStoreMocks.currentEntry), resolveStorePath: vi.fn(() => "/tmp/mock-sessions.json"), - resolveSessionStoreEntry: vi.fn((params?: { sessionKey?: string }) => ({ - existing: - (params?.sessionKey - ? sessionStoreMocks.entriesBySessionKey.get(params.sessionKey) - : undefined) ?? sessionStoreMocks.currentEntry, - })), + resolveSessionStoreEntry: vi.fn( + (params: { + store: Record>; + sessionKey: string; + }): { existing: Record | undefined } => ({ + existing: + params.store[params.sessionKey] ?? + sessionStoreMocks.entriesBySessionKey.get(params.sessionKey) ?? + sessionStoreMocks.currentEntry, + }), + ), updateSessionStoreEntry: vi.fn( async (params: { update: (entry: Record) => Promise | null>; @@ -466,6 +478,13 @@ vi.mock("./dispatch-from-config.runtime.js", () => ({ triggerInternalHook: internalHookMocks.triggerInternalHook, updateSessionStoreEntry: sessionStoreMocks.updateSessionStoreEntry, })); +vi.mock("../../config/sessions/session-accessor.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadSessionEntry: (...args: unknown[]) => sessionStoreMocks.loadSessionEntry(...args), + }; +}); vi.mock("../../plugins/hook-runner-global.js", () => ({ initializeGlobalHookRunner: vi.fn(), @@ -647,6 +666,8 @@ let tryDispatchAcpReplyHook: typeof import("../../plugin-sdk/acp-runtime.js").tr let createReplyOperation: typeof import("./reply-run-registry.js").createReplyOperation; let replyRunRegistry: typeof import("./reply-run-registry.js").replyRunRegistry; let replyRunTesting: typeof import("./reply-run-registry.js").__testing; +let admitReplyTurn: typeof import("./reply-turn-admission.js").admitReplyTurn; +let runWithReplyOperationLifecycleAdmission: typeof import("./reply-turn-admission.js").runWithReplyOperationLifecycleAdmission; type DispatchReplyArgs = Parameters< typeof import("./dispatch-from-config.js").dispatchReplyFromConfig >[0]; @@ -665,6 +686,8 @@ beforeAll(async () => { replyRunRegistry, __testing: replyRunTesting, } = await import("./reply-run-registry.js")); + ({ admitReplyTurn, runWithReplyOperationLifecycleAdmission } = + await import("./reply-turn-admission.js")); }); function createDispatcher(): ReplyDispatcher { @@ -1112,11 +1135,23 @@ describe("dispatchReplyFromConfig", () => { sessionBindingMocks.touch.mockReset(); sessionStoreMocks.currentEntry = undefined; sessionStoreMocks.entriesBySessionKey.clear(); - sessionStoreMocks.loadSessionStore.mockClear(); + sessionStoreMocks.loadSessionEntry.mockReset(); + sessionStoreMocks.loadSessionEntry.mockImplementation(() => sessionStoreMocks.currentEntry); + sessionStoreMocks.loadSessionStore.mockReset(); + sessionStoreMocks.loadSessionStore.mockReturnValue({}); sessionStoreMocks.readSessionEntry.mockReset(); sessionStoreMocks.readSessionEntry.mockImplementation(() => sessionStoreMocks.currentEntry); - sessionStoreMocks.resolveStorePath.mockClear(); - sessionStoreMocks.resolveSessionStoreEntry.mockClear(); + sessionStoreMocks.resolveStorePath.mockReset(); + sessionStoreMocks.resolveStorePath.mockReturnValue("/tmp/mock-sessions.json"); + sessionStoreMocks.resolveSessionStoreEntry.mockReset(); + sessionStoreMocks.resolveSessionStoreEntry.mockImplementation( + (params: { store: Record>; sessionKey: string }) => ({ + existing: + params.store[params.sessionKey] ?? + sessionStoreMocks.entriesBySessionKey.get(params.sessionKey) ?? + sessionStoreMocks.currentEntry, + }), + ); threadInfoMocks.parseSessionThreadInfo.mockReset(); threadInfoMocks.parseSessionThreadInfo.mockImplementation(parseGenericThreadSessionInfo); ttsMocks.state.synthesizeFinalAudio = false; @@ -1642,15 +1677,50 @@ describe("dispatchReplyFromConfig", () => { it("lets a different Slack DM routed thread reach reply resolution while another thread is active", async () => { setNoAbort(); const sessionKey = "agent:main:slack:direct:U1"; + const sessionId = "active-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; const activeOperation = createReplyOperation({ sessionKey, - sessionId: "active-session", + sessionId, resetTriggered: false, routeThreadId: "500.000", }); activeOperation.setPhase("running"); const dispatcher = createDispatcher(); - const replyResolver = vi.fn(async () => ({ text: "thread B reply" }) satisfies ReplyPayload); + let inBandMutationRan = false; + const rotatedSessionId = "rotated-session"; + const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => { + expect(isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId])).toBe( + true, + ); + await runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + run: async () => { + inBandMutationRan = true; + sessionStoreMocks.currentEntry = { + sessionId: rotatedSessionId, + updatedAt: Date.now(), + }; + ( + opts as + | (GetReplyOptions & { + onSessionPrepared?: (binding: { + sessionKey: string; + sessionId: string; + storePath: string; + }) => void; + }) + | undefined + )?.onSessionPrepared?.({ + sessionKey, + sessionId: rotatedSessionId, + storePath: "/tmp/mock-sessions.json", + }); + }, + }); + return { text: "thread B reply" } satisfies ReplyPayload; + }); try { const resultPromise = dispatchReplyFromConfig({ @@ -1686,12 +1756,402 @@ describe("dispatchReplyFromConfig", () => { counts: { tool: 0, block: 0, final: 0 }, }); expect(replyResolver).toHaveBeenCalledTimes(1); + expect(inBandMutationRan).toBe(true); expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1); } finally { activeOperation.complete(); } }); + it("releases a Slack bypass lease when the competing routed thread changes during admission", async () => { + setNoAbort(); + const sessionKey = "agent:main:slack:direct:U2"; + const sessionId = "active-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + const originalOperation = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + routeThreadId: "500.000", + }); + originalOperation.setPhase("running"); + let replacementOperation: ReturnType | undefined; + let releaseMutation: () => void = () => {}; + const mutationGate = new Promise((resolve) => { + releaseMutation = resolve; + }); + let signalMutationEntered: () => void = () => {}; + const mutationEntered = new Promise((resolve) => { + signalMutationEntered = resolve; + }); + let lifecycleMutation: Promise | undefined; + hookMocks.runner.hasHooks.mockImplementation( + ((hookName?: string) => hookName === "before_dispatch") as () => boolean, + ); + hookMocks.runner.runBeforeDispatch.mockImplementationOnce(async () => { + lifecycleMutation = runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + run: async () => { + signalMutationEntered(); + await mutationGate; + }, + }); + await mutationEntered; + setTimeout(() => { + originalOperation.complete(); + replacementOperation = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + routeThreadId: "501.000", + }); + replacementOperation.setPhase("running"); + releaseMutation(); + }, 0); + return undefined; + }); + const replyResolver = vi.fn(async () => ({ text: "must not run" }) satisfies ReplyPayload); + + try { + const result = await dispatchReplyFromConfig({ + ctx: buildTestCtx({ + Provider: "slack", + Surface: "slack", + OriginatingChannel: "slack", + OriginatingTo: "user:U2", + ChatType: "direct", + SessionKey: sessionKey, + MessageThreadId: "501.000", + BodyForAgent: "same routed thread after replacement", + }), + cfg: emptyConfig, + dispatcher: createDispatcher(), + replyResolver, + }); + await lifecycleMutation; + + expect(result).toMatchObject({ queuedFinal: false }); + expect(replyResolver).not.toHaveBeenCalled(); + expect(isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId])).toBe( + false, + ); + } finally { + releaseMutation(); + originalOperation.complete(); + replacementOperation?.complete(); + await lifecycleMutation; + } + }); + + it("holds a Slack bypass lease until an abort-insensitive resolver settles", async () => { + setNoAbort(); + const sessionKey = "agent:main:slack:direct:U3"; + const sessionId = "active-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + const activeOperation = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + routeThreadId: "500.000", + }); + activeOperation.setPhase("running"); + let releaseResolver: () => void = () => {}; + const resolverGate = new Promise((resolve) => { + releaseResolver = resolve; + }); + let signalResolverEntered: () => void = () => {}; + const resolverEntered = new Promise((resolve) => { + signalResolverEntered = resolve; + }); + const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => { + signalResolverEntered(); + await resolverGate; + await requireBlockReplyHandler(opts?.onBlockReply)({ text: "stale late block" }); + return { text: "late reply" } satisfies ReplyPayload; + }); + const dispatcher = createDispatcher(); + const dispatch = dispatchReplyFromConfig({ + ctx: buildTestCtx({ + Provider: "slack", + Surface: "slack", + OriginatingChannel: "slack", + OriginatingTo: "user:U3", + ChatType: "direct", + SessionKey: sessionKey, + MessageThreadId: "501.000", + BodyForAgent: "abort-insensitive routed thread", + }), + cfg: emptyConfig, + dispatcher, + replyResolver, + }); + await resolverEntered; + activeOperation.complete(); + + let mutationRan = false; + const externalLifecycleRequest = new AsyncResource("slack-bypass-settle-race"); + const mutation = externalLifecycleRequest.runInAsyncScope( + async () => + await runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }), + ); + const result = await dispatch; + + expect(result.queuedFinal).toBe(false); + expect(mutationRan).toBe(false); + expect(isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId])).toBe( + true, + ); + + releaseResolver(); + await mutation; + + expect(mutationRan).toBe(true); + expect(isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId])).toBe( + false, + ); + expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); + expect(dispatcher.sendBlockReply).not.toHaveBeenCalled(); + externalLifecycleRequest.emitDestroy(); + }); + + it("bounds Slack bypass lease cleanup when dispatcher idle never settles", async () => { + setNoAbort(); + const sessionKey = "agent:main:slack:direct:U4"; + const sessionId = "active-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + const activeOperation = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + routeThreadId: "500.000", + }); + activeOperation.setPhase("running"); + const dispatcher = createDispatcher(); + dispatcher.waitForIdle = vi.fn(async () => await new Promise(() => {})); + dispatcher.resolveFollowupAdmissionBarrierTimeoutPolicy = () => ({ + maxTimeoutMs: 25, + shouldExtend: () => false, + }); + + try { + const result = await dispatchReplyFromConfig({ + ctx: buildTestCtx({ + Provider: "slack", + Surface: "slack", + OriginatingChannel: "slack", + OriginatingTo: "user:U4", + ChatType: "direct", + SessionKey: sessionKey, + MessageThreadId: "501.000", + BodyForAgent: "hung delivery barrier", + }), + cfg: emptyConfig, + dispatcher, + replyResolver: async () => undefined, + }); + + expect(result.queuedFinal).toBe(false); + await vi.waitFor( + () => { + expect( + isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId]), + ).toBe(false); + }, + { timeout: 500 }, + ); + } finally { + activeOperation.complete(); + } + }); + + it("holds a Slack bypass lease until queued delivery settles before revalidation", async () => { + setNoAbort(); + const sessionKey = "agent:main:slack:direct:U5"; + const sessionId = "active-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + const activeOperation = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + routeThreadId: "500.000", + }); + activeOperation.setPhase("running"); + let releaseDelivery: () => void = () => {}; + const deliveryGate = new Promise((resolve) => { + releaseDelivery = resolve; + }); + const dispatcher = createDispatcher(); + let holdDelivery = false; + dispatcher.waitForIdle = vi.fn(async () => { + if (holdDelivery) { + await deliveryGate; + } + }); + const externalLifecycleRequest = new AsyncResource("slack-bypass-delivery-race"); + let allowLifecycleInterrupt: () => void = () => {}; + const lifecycleInterruptGate = new Promise((resolve) => { + allowLifecycleInterrupt = resolve; + }); + let signalMutationPrepared: () => void = () => {}; + const mutationPrepared = new Promise((resolve) => { + signalMutationPrepared = resolve; + }); + let signalResolverReturning: () => void = () => {}; + const resolverReturning = new Promise((resolve) => { + signalResolverReturning = resolve; + }); + let mutationRan = false; + let mutation: Promise | undefined; + const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => { + holdDelivery = true; + await requireBlockReplyHandler(opts?.onBlockReply)({ text: "queued block" }); + mutation = externalLifecycleRequest.runInAsyncScope( + async () => + await runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + prepare: async () => { + signalMutationPrepared(); + await lifecycleInterruptGate; + await interruptSessionWorkAdmissions({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }), + ); + signalResolverReturning(); + return undefined; + }); + + try { + const dispatch = dispatchReplyFromConfig({ + ctx: buildTestCtx({ + Provider: "slack", + Surface: "slack", + OriginatingChannel: "slack", + OriginatingTo: "user:U5", + ChatType: "direct", + SessionKey: sessionKey, + MessageThreadId: "501.000", + BodyForAgent: "hold queued delivery", + }), + cfg: emptyConfig, + dispatcher, + replyResolver, + }); + await Promise.all([mutationPrepared, resolverReturning]); + await new Promise((resolve) => { + setImmediate(resolve); + }); + allowLifecycleInterrupt(); + await new Promise((resolve) => { + setImmediate(resolve); + }); + expect(dispatcher.sendBlockReply).toHaveBeenCalledOnce(); + expect(mutationRan).toBe(false); + expect(isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId])).toBe( + true, + ); + + releaseDelivery(); + await dispatch; + await mutation; + + expect(mutationRan).toBe(true); + } finally { + allowLifecycleInterrupt(); + releaseDelivery(); + activeOperation.complete(); + await mutation; + externalLifecycleRequest.emitDestroy(); + } + }); + + it("runs ACP tail dispatch inside a borrowed Slack lifecycle admission", async () => { + setNoAbort(); + const sessionKey = "agent:main:slack:direct:U6"; + const sessionId = "active-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + const activeOperation = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + routeThreadId: "500.000", + }); + activeOperation.setPhase("running"); + let initiatingAdmissionExcluded = false; + let mutationRan = false; + hookMocks.runner.hasHooks.mockImplementation( + ((hookName?: string) => hookName === "reply_dispatch") as () => boolean, + ); + hookMocks.runner.runReplyDispatch.mockImplementation(async (event: unknown) => { + if (!(event as { isTailDispatch?: boolean }).isTailDispatch) { + return undefined; + } + await runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + prepare: async () => { + initiatingAdmissionExcluded = await interruptSessionWorkAdmissions({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + timeoutMs: 25, + }); + }, + run: async () => { + mutationRan = true; + }, + }); + return { + handled: true, + queuedFinal: false, + counts: { tool: 0, block: 0, final: 0 }, + }; + }); + + try { + const result = await dispatchReplyFromConfig({ + ctx: buildTestCtx({ + Provider: "slack", + Surface: "slack", + OriginatingChannel: "slack", + OriginatingTo: "user:U6", + ChatType: "direct", + SessionKey: sessionKey, + MessageThreadId: "501.000", + BodyForAgent: "run tail after reset", + AcpDispatchTailAfterReset: true, + }), + cfg: emptyConfig, + dispatcher: createDispatcher(), + replyResolver: async () => undefined, + }); + + expect(result.queuedFinal).toBe(false); + expect(initiatingAdmissionExcluded).toBe(true); + expect(mutationRan).toBe(true); + } finally { + activeOperation.complete(); + } + }); + it("keeps non-Slack routed direct turns behind the active reply operation", async () => { setNoAbort(); installThreadingTestPlugin({ id: "telegram" }); @@ -2031,7 +2491,7 @@ describe("dispatchReplyFromConfig", () => { recoveryOperation?.complete(); }); - it("does not force-clear an active operation whose session id differs from the terminal snapshot", async () => { + it("rejects a stale turn without clearing the operation for the rotated session", async () => { setNoAbort(); const sessionKey = "agent:main:telegram:group:-1003774691297"; // Terminal store snapshot still reports the failed lifecycle's session id. @@ -2092,20 +2552,16 @@ describe("dispatchReplyFromConfig", () => { setTimeout(resolve, 100); }); - // The session-id guard kept the rotated op untouched: it still owns the - // session key and was never force-failed, so the visible turn simply parks - // behind it instead of dropping it. + // The session-id guard keeps the rotated op untouched while the stale turn + // is invalidated instead of later crossing the reset boundary. expect(freshOperation).toBeDefined(); expect(freshOperation?.result).toBeNull(); expect(replyRunRegistry.get(sessionKey)).toBe(freshOperation); expect(replyResolver).not.toHaveBeenCalled(); - // Releasing the rotated op lets the parked visible turn admit and deliver, - // proving the message survived the rotation rather than being silently lost. freshOperation?.complete(); - const result = await turn; - expect(replyResolver).toHaveBeenCalledTimes(1); - expect(result).toMatchObject({ queuedFinal: true }); + await expect(turn).rejects.toThrow(/changed while starting work/i); + expect(replyResolver).not.toHaveBeenCalled(); expect(replyRunRegistry.isActive(sessionKey)).toBe(false); }); @@ -5067,15 +5523,36 @@ describe("dispatchReplyFromConfig", () => { }); it("fast-aborts without calling the reply resolver", async () => { + hookMocks.runner.hasHooks.mockImplementation( + ((hookName?: string) => hookName === "message_received") as () => boolean, + ); mocks.tryFastAbortFromMessage.mockResolvedValue({ handled: true, aborted: true, }); + sessionBindingMocks.resolveByConversation.mockReturnValue({ + bindingId: "binding-fast-abort", + targetSessionKey: "plugin-binding:test:fast-abort", + targetKind: "session", + conversation: { + channel: "telegram", + accountId: "default", + conversationId: "direct:stop-hook", + }, + status: "active", + boundAt: 1710000000000, + metadata: { + pluginBindingOwner: "plugin", + pluginId: "test-plugin", + pluginRoot: "/tmp/test-plugin", + }, + } satisfies SessionBindingRecord); const cfg = emptyConfig; const dispatcher = createDispatcher(); const ctx = buildTestCtx({ Provider: "telegram", Body: "/stop", + SessionKey: "agent:main:telegram:direct:stop-hook", }); const replyResolver = vi.fn(async () => ({ text: "hi" }) as ReplyPayload); @@ -5085,6 +5562,9 @@ describe("dispatchReplyFromConfig", () => { expect(dispatcher.sendFinalReply).toHaveBeenCalledWith({ text: "⚙️ Agent was aborted.", }); + expect(hookMocks.runner.runMessageReceived).toHaveBeenCalledOnce(); + expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledOnce(); + expect(sessionBindingMocks.touch).toHaveBeenCalledWith("binding-fast-abort"); }); it("reports when a fast abort is rejected during finalization", async () => { @@ -5561,7 +6041,32 @@ describe("dispatchReplyFromConfig", () => { it("retargets reply_dispatch to a bound generic ACP session before model fallback", async () => { setNoAbort(); + const sourceSessionKey = "agent:main:discord:C123"; const boundSessionKey = "agent:opencode:acp:bound-session"; + const sourceStorePath = "/tmp/main-sessions.json"; + const targetStorePath = "/tmp/opencode-sessions.json"; + const sourceEntry = { sessionId: "source-session-id", updatedAt: Date.now() }; + const targetEntry = { sessionId: "target-session-id", updatedAt: Date.now() }; + const stores: Record>> = { + [sourceStorePath]: { [sourceSessionKey]: sourceEntry }, + [targetStorePath]: { [boundSessionKey]: targetEntry }, + }; + sessionStoreMocks.resolveStorePath.mockImplementation( + (_configuredPath?: unknown, options?: { agentId?: string }) => + options?.agentId === "opencode" ? targetStorePath : sourceStorePath, + ); + sessionStoreMocks.loadSessionStore.mockImplementation( + (storePath?: string) => (storePath ? stores[storePath] : undefined) ?? {}, + ); + sessionStoreMocks.resolveSessionStoreEntry.mockImplementation( + (params?: { store: Record>; sessionKey: string }) => ({ + existing: params?.store[params.sessionKey], + }), + ); + sessionStoreMocks.loadSessionEntry.mockImplementation((paramsUnknown: unknown) => { + const params = paramsUnknown as { sessionKey: string; storePath: string }; + return stores[params.storePath]?.[params.sessionKey]; + }); const runtime = createAcpRuntime([ { type: "text_delta", text: "Bound ACP reply" }, { type: "done" }, @@ -5623,7 +6128,7 @@ describe("dispatchReplyFromConfig", () => { OriginatingTo: "discord:C123", To: "discord:C123", AccountId: "default", - SessionKey: "agent:main:discord:C123", + SessionKey: sourceSessionKey, BodyForAgent: "continue", }); @@ -5636,6 +6141,17 @@ describe("dispatchReplyFromConfig", () => { conversationId: "C123", }); expect(sessionBindingMocks.touch).toHaveBeenCalledWith("binding-acp-current"); + expect(sessionStoreMocks.loadSessionEntry).toHaveBeenCalledWith({ + storePath: sourceStorePath, + sessionKey: sourceSessionKey, + readConsistency: "latest", + }); + expect(sessionStoreMocks.loadSessionEntry).not.toHaveBeenCalledWith( + expect.objectContaining({ + storePath: targetStorePath, + sessionKey: sourceSessionKey, + }), + ); const ensureSessionOptions = firstMockArg(runtime.ensureSession, "ensure session") as | { agent?: unknown; sessionKey?: unknown } | undefined; @@ -6826,6 +7342,466 @@ describe("dispatchReplyFromConfig", () => { expect(replyResolver).not.toHaveBeenCalled(); }); + it("holds session lifecycle mutation until an interrupted plugin claim exits", async () => { + setNoAbort(); + hookMocks.runner.hasHooks.mockImplementation( + ((hookName?: string) => hookName === "inbound_claim") as () => boolean, + ); + hookMocks.registry.plugins = [{ id: "test-plugin", status: "loaded" }]; + let resolveClaim: ((outcome: PluginTargetedInboundClaimOutcome) => void) | undefined; + hookMocks.runner.runInboundClaimForPluginOutcome.mockImplementationOnce( + async () => + await new Promise((resolve) => { + resolveClaim = resolve; + }), + ); + sessionBindingMocks.resolveByConversation.mockReturnValue({ + bindingId: "binding-lifecycle-race", + targetSessionKey: "plugin-binding:test:race", + targetKind: "session", + conversation: { + channel: "discord", + accountId: "default", + conversationId: "channel:lifecycle-race", + }, + status: "active", + boundAt: 1710000000000, + metadata: { + pluginBindingOwner: "plugin", + pluginId: "test-plugin", + pluginRoot: "/tmp/test-plugin", + }, + } satisfies SessionBindingRecord); + const sessionKey = "agent:main:discord:channel:lifecycle-race"; + const sessionId = "plugin-lifecycle-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + const dispatcher = createDispatcher(); + const externalLifecycleRequest = new AsyncResource("external-lifecycle-request"); + const ctx = buildTestCtx({ + Provider: "discord", + Surface: "discord", + To: "discord:channel:lifecycle-race", + AccountId: "default", + SessionKey: sessionKey, + Body: "hold this claim", + }); + const replyResolver = vi.fn(async () => ({ text: "must not run" }) satisfies ReplyPayload); + const dispatch = dispatchReplyFromConfig({ ctx, cfg: emptyConfig, dispatcher, replyResolver }); + await vi.waitFor(() => { + expect(hookMocks.runner.runInboundClaimForPluginOutcome).toHaveBeenCalledOnce(); + }); + expect(replyRunRegistry.get(sessionKey)).toBeDefined(); + expect(isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId])).toBe( + true, + ); + + let mutationRan = false; + const mutation = externalLifecycleRequest.runInAsyncScope( + async () => + await runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }), + ); + await vi.waitFor(() => { + expect(replyRunRegistry.get(sessionKey)?.abortSignal.aborted).toBe(true); + }); + expect(mutationRan).toBe(false); + + resolveClaim?.({ + status: "handled", + result: { handled: true, reply: { text: "stale plugin reply" } }, + }); + const result = await dispatch; + await mutation; + + expect(mutationRan).toBe(true); + expect(result.queuedFinal).toBe(false); + expect(mocks.routeReply).not.toHaveBeenCalled(); + expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); + expect(replyResolver).not.toHaveBeenCalled(); + externalLifecycleRequest.emitDestroy(); + }); + + it("holds an owned lifecycle lease until abort-insensitive resolver work settles", async () => { + setNoAbort(); + const sessionKey = "agent:main:discord:channel:owned-resolver-race"; + const sessionId = "owned-resolver-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + let releaseResolver: () => void = () => {}; + const resolverGate = new Promise((resolve) => { + releaseResolver = resolve; + }); + let signalResolverEntered: () => void = () => {}; + const resolverEntered = new Promise((resolve) => { + signalResolverEntered = resolve; + }); + const dispatcher = createDispatcher(); + const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => { + signalResolverEntered(); + await resolverGate; + await requireBlockReplyHandler(opts?.onBlockReply)({ text: "stale late block" }); + return { text: "stale late final" } satisfies ReplyPayload; + }); + const dispatch = dispatchReplyFromConfig({ + ctx: buildTestCtx({ + Provider: "discord", + Surface: "discord", + To: "discord:channel:owned-resolver-race", + AccountId: "default", + SessionKey: sessionKey, + Body: "hold this resolver", + }), + cfg: emptyConfig, + dispatcher, + replyResolver, + }); + await resolverEntered; + + const externalLifecycleRequest = new AsyncResource("external-owned-resolver-lifecycle"); + let mutationRan = false; + const mutation = externalLifecycleRequest.runInAsyncScope( + async () => + await runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }), + ); + + try { + const result = await dispatch; + expect(result.queuedFinal).toBe(false); + expect(mutationRan).toBe(false); + expect(isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId])).toBe( + true, + ); + + releaseResolver(); + await mutation; + + expect(mutationRan).toBe(true); + expect(dispatcher.sendBlockReply).not.toHaveBeenCalled(); + expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); + } finally { + releaseResolver(); + await mutation; + externalLifecycleRequest.emitDestroy(); + } + }); + + it("holds a lifecycle lease for plugin claims behind an active reply operation", async () => { + setNoAbort(); + hookMocks.runner.hasHooks.mockImplementation( + ((hookName?: string) => hookName === "inbound_claim") as () => boolean, + ); + hookMocks.registry.plugins = [{ id: "test-plugin", status: "loaded" }]; + let resolveClaim: ((outcome: PluginTargetedInboundClaimOutcome) => void) | undefined; + hookMocks.runner.runInboundClaimForPluginOutcome.mockImplementationOnce( + async () => + await new Promise((resolve) => { + resolveClaim = resolve; + }), + ); + sessionBindingMocks.resolveByConversation.mockReturnValue({ + bindingId: "binding-active-lifecycle-race", + targetSessionKey: "plugin-binding:test:active-race", + targetKind: "session", + conversation: { + channel: "discord", + accountId: "default", + conversationId: "channel:active-lifecycle-race", + }, + status: "active", + boundAt: 1710000000000, + metadata: { + pluginBindingOwner: "plugin", + pluginId: "test-plugin", + pluginRoot: "/tmp/test-plugin", + }, + } satisfies SessionBindingRecord); + const sessionKey = "agent:main:discord:channel:active-lifecycle-race"; + const sessionId = "plugin-active-lifecycle-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + const existingOperation = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + }); + const dispatcher = createDispatcher(); + const externalLifecycleRequest = new AsyncResource("external-active-lifecycle-request"); + const ctx = buildTestCtx({ + Provider: "discord", + Surface: "discord", + To: "discord:channel:active-lifecycle-race", + AccountId: "default", + SessionKey: sessionKey, + Body: "hold this overlapping claim", + }); + const replyResolver = vi.fn(async () => ({ text: "must not run" }) satisfies ReplyPayload); + const dispatch = dispatchReplyFromConfig({ ctx, cfg: emptyConfig, dispatcher, replyResolver }); + await vi.waitFor(() => { + expect(hookMocks.runner.runInboundClaimForPluginOutcome).toHaveBeenCalledOnce(); + }); + expect(isSessionWorkAdmissionActive("/tmp/mock-sessions.json", [sessionKey, sessionId])).toBe( + true, + ); + + existingOperation.complete(); + expect(replyRunRegistry.get(sessionKey)).toBeUndefined(); + + let mutationPrepared = false; + let mutationRan = false; + const mutation = externalLifecycleRequest.runInAsyncScope( + async () => + await runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + prepare: async () => { + mutationPrepared = true; + await interruptSessionWorkAdmissions({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }), + ); + await vi.waitFor(() => { + expect(mutationPrepared).toBe(true); + }); + expect(mutationRan).toBe(false); + + resolveClaim?.({ + status: "handled", + result: { handled: true, reply: { text: "stale plugin reply" } }, + }); + const result = await dispatch; + await mutation; + + expect(mutationRan).toBe(true); + expect(result.queuedFinal).toBe(false); + expect(mocks.routeReply).not.toHaveBeenCalled(); + expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); + expect(replyResolver).not.toHaveBeenCalled(); + externalLifecycleRequest.emitDestroy(); + }); + + it("does not abort the active owner when its in-band mutation interrupts borrowed work", async () => { + setNoAbort(); + hookMocks.runner.hasHooks.mockImplementation( + ((hookName?: string) => hookName === "inbound_claim") as () => boolean, + ); + hookMocks.registry.plugins = [{ id: "test-plugin", status: "loaded" }]; + let resolveClaim: ((outcome: PluginTargetedInboundClaimOutcome) => void) | undefined; + hookMocks.runner.runInboundClaimForPluginOutcome.mockImplementationOnce( + async () => + await new Promise((resolve) => { + resolveClaim = resolve; + }), + ); + sessionBindingMocks.resolveByConversation.mockReturnValue({ + bindingId: "binding-owner-mutation-race", + targetSessionKey: "plugin-binding:test:owner-mutation-race", + targetKind: "session", + conversation: { + channel: "discord", + accountId: "default", + conversationId: "channel:owner-mutation-race", + }, + status: "active", + boundAt: 1710000000000, + metadata: { + pluginBindingOwner: "plugin", + pluginId: "test-plugin", + pluginRoot: "/tmp/test-plugin", + }, + } satisfies SessionBindingRecord); + const sessionKey = "agent:main:discord:channel:owner-mutation-race"; + const sessionId = "owner-mutation-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + const ownerAdmission = await admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath: "/tmp/mock-sessions.json", + kind: "visible", + resetTriggered: false, + }); + expect(ownerAdmission.status).toBe("owned"); + if (ownerAdmission.status !== "owned") { + throw new Error("expected active owner admission"); + } + const ownerOperation = ownerAdmission.operation; + const dispatcher = createDispatcher(); + const replyResolver = vi.fn(async () => ({ text: "must not run" }) satisfies ReplyPayload); + const dispatch = dispatchReplyFromConfig({ + ctx: buildTestCtx({ + Provider: "discord", + Surface: "discord", + To: "discord:channel:owner-mutation-race", + AccountId: "default", + SessionKey: sessionKey, + Body: "borrow the active lane", + }), + cfg: emptyConfig, + dispatcher, + replyResolver, + }); + await vi.waitFor(() => { + expect(hookMocks.runner.runInboundClaimForPluginOutcome).toHaveBeenCalledOnce(); + }); + + let mutationRan = false; + const mutation = runWithReplyOperationLifecycleAdmission(ownerOperation, async () => + runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }), + ); + await new Promise((resolve) => { + setImmediate(resolve); + }); + expect(ownerOperation.abortSignal.aborted).toBe(false); + expect(mutationRan).toBe(false); + + resolveClaim?.({ status: "handled", result: { handled: true } }); + const result = await dispatch; + await mutation; + + expect(result.queuedFinal).toBe(false); + expect(ownerOperation.abortSignal.aborted).toBe(false); + expect(ownerOperation.result).toBeNull(); + expect(replyRunRegistry.get(sessionKey)).toBe(ownerOperation); + expect(mutationRan).toBe(true); + expect(replyResolver).not.toHaveBeenCalled(); + ownerOperation.complete(); + }); + + it("completes an owned reply operation interrupted during a plugin fallback notice", async () => { + setNoAbort(); + hookMocks.runner.hasHooks.mockImplementation( + ((hookName?: string) => hookName === "inbound_claim") as () => boolean, + ); + hookMocks.runner.runInboundClaimForPluginOutcome.mockResolvedValue({ + status: "missing_plugin", + }); + sessionBindingMocks.resolveByConversation.mockReturnValue({ + bindingId: "binding-interrupted-fallback", + targetSessionKey: "plugin-binding:test:interrupted-fallback", + targetKind: "session", + conversation: { + channel: "discord", + accountId: "default", + conversationId: "channel:interrupted-fallback", + }, + status: "active", + boundAt: 1710000000000, + metadata: { + pluginBindingOwner: "plugin", + pluginId: "missing-plugin", + pluginRoot: "/tmp/missing-plugin", + }, + } satisfies SessionBindingRecord); + const sessionKey = "agent:main:discord:channel:interrupted-fallback"; + const sessionId = "interrupted-fallback-session"; + sessionStoreMocks.currentEntry = { sessionId, updatedAt: Date.now() }; + let resolveNotice: ((result: { ok: true; messageId: string }) => void) | undefined; + mocks.routeReply.mockImplementationOnce( + async () => + await new Promise<{ ok: true; messageId: string }>((resolve) => { + resolveNotice = resolve; + }), + ); + const dispatcher = createDispatcher(); + const replyResolver = vi.fn(async () => ({ text: "must not run" }) satisfies ReplyPayload); + const dispatch = dispatchReplyFromConfig({ + ctx: buildTestCtx({ + Provider: "discord", + Surface: "discord", + OriginatingChannel: "telegram", + OriginatingTo: "telegram:interrupted-fallback", + To: "discord:channel:interrupted-fallback", + AccountId: "default", + SessionKey: sessionKey, + Body: "fall back", + }), + cfg: emptyConfig, + dispatcher, + replyResolver, + }); + await vi.waitFor(() => { + expect(mocks.routeReply).toHaveBeenCalledOnce(); + }); + const operation = replyRunRegistry.get(sessionKey); + expect(operation).toBeDefined(); + + let mutationRan = false; + const externalLifecycleRequest = new AsyncResource("interrupted-fallback-lifecycle"); + const mutation = externalLifecycleRequest.runInAsyncScope( + async () => + await runExclusiveSessionLifecycleMutation({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: "/tmp/mock-sessions.json", + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }), + ); + await vi.waitFor(() => { + expect(operation?.abortSignal.aborted).toBe(true); + }); + expect(mutationRan).toBe(false); + + resolveNotice?.({ ok: true, messageId: "fallback-notice" }); + const result = await dispatch; + await mutation; + + expect(result.queuedFinal).toBe(false); + expect(operation?.result).toMatchObject({ + kind: "aborted", + code: "aborted_for_restart", + }); + expect(replyRunRegistry.isActive(sessionKey)).toBe(false); + expect(mutationRan).toBe(true); + expect(replyResolver).not.toHaveBeenCalled(); + externalLifecycleRequest.emitDestroy(); + }); + it("stages remote iMessage media before plugin-bound inbound claim metadata reaches Codex", async () => { setNoAbort(); const order: string[] = []; @@ -8952,6 +9928,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => sessionBindingMocks.resolveByConversation.mockReturnValue(null); sessionBindingMocks.touch.mockReset(); hookMocks.registry.plugins = []; + hookMocks.runner.runInboundClaimForPluginOutcome.mockClear(); hookMocks.runner.runInboundClaimForPluginOutcome.mockResolvedValue({ status: "no_handler", }); @@ -9337,6 +10314,59 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => }); }); + it("rejects archived plugin-bound work before the plugin handler runs", async () => { + setNoAbort(); + hookMocks.runner.hasHooks.mockImplementation( + ((hookName?: string) => hookName === "inbound_claim") as () => boolean, + ); + hookMocks.registry.plugins = [{ id: "openclaw-codex-app-server", status: "loaded" }]; + hookMocks.runner.runInboundClaimForPluginOutcome.mockResolvedValue({ + status: "handled", + result: { handled: true, reply: { text: "must not send" } }, + }); + sessionBindingMocks.resolveByConversation.mockReturnValue({ + bindingId: "binding-archived", + targetSessionKey: "plugin-binding:codex:archived", + targetKind: "session", + conversation: { + channel: "discord", + accountId: "default", + conversationId: "channel:archived-test", + }, + status: "active", + boundAt: 1710000000000, + metadata: { + pluginBindingOwner: "plugin", + pluginId: "openclaw-codex-app-server", + pluginRoot: "/tmp/plugin", + }, + } satisfies SessionBindingRecord); + sessionStoreMocks.currentEntry = { + sessionId: "s1", + updatedAt: Date.now(), + archivedAt: Date.now(), + }; + const dispatcher = createDispatcher(); + const replyResolver = vi.fn(async () => ({ text: "must not run" }) satisfies ReplyPayload); + const ctx = buildTestCtx({ + Provider: "discord", + Surface: "discord", + To: "discord:channel:archived-test", + AccountId: "default", + SessionKey: "agent:main:discord:channel:archived-test", + Body: "start work", + }); + + await expect( + dispatchReplyFromConfig({ ctx, cfg: emptyConfig, dispatcher, replyResolver }), + ).rejects.toThrow(/is archived/i); + + expect(sessionBindingMocks.touch).not.toHaveBeenCalled(); + expect(hookMocks.runner.runInboundClaimForPluginOutcome).not.toHaveBeenCalled(); + expect(replyResolver).not.toHaveBeenCalled(); + expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); + }); + it("skips plugin-bound claim hook under deny and falls through to suppressed agent dispatch", async () => { // Plugin-bound inbound handlers can emit outbound replies we cannot // rewind. Under deny, skip the plugin claim entirely and let the agent diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 30261b51374f..d5e8773da2fb 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -93,6 +93,7 @@ import { getGlobalHookRunner, getGlobalPluginRegistry } from "../../plugins/hook import type { PluginHookReplyDispatchEvent } from "../../plugins/hook-types.js"; import { isAcpSessionKey } from "../../routing/session-key.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; +import type { SessionWorkAdmissionLease } from "../../sessions/session-lifecycle-admission.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; import { resolveSilentReplyPolicyFromPolicies } from "../../shared/silent-reply-policy.js"; import { truncateUtf16Safe } from "../../shared/utf16-slice.js"; @@ -162,6 +163,7 @@ import { forceClearReplyRunBySessionId, replyRunRegistry, type ReplyOperation, + waitForReplyBarrierSettlement, } from "./reply-run-registry.js"; import { createReplyDeliveryContext, @@ -169,7 +171,11 @@ import { resolveReplyToMode, } from "./reply-threading.js"; import { isReplyProfilerEnabled } from "./reply-timing-tracker.js"; -import { admitReplyTurn, resolveReplyTurnKind } from "./reply-turn-admission.js"; +import { + admitReplyTurn, + resolveReplyTurnKind, + runWithReplyOperationLifecycleAdmission, +} from "./reply-turn-admission.js"; import { resolveRoutedDeliveryThreadId } from "./routed-delivery-thread.js"; import { resolveReplyRoutingDecision } from "./routing-policy.js"; import { @@ -921,24 +927,14 @@ async function mirrorTranscriptAfterDispatcherSettled(params: { function runWithDispatchAbortSignal( signal: AbortSignal | undefined, run: () => Promise | T, + onWorkStarted?: (work: Promise) => void, ): Promise { - if (!signal) { - return Promise.resolve().then(run); - } - const shouldStopForAbort = () => signal.aborted; - if (signal.aborted && shouldStopForAbort()) { + if (signal?.aborted) { return Promise.reject(new DispatchReplyOperationAbortedError()); } + const shouldStopForAbort = () => signal?.aborted === true; let settled = false; let abortHandler: (() => void) | undefined; - const aborted = new Promise((_, reject) => { - abortHandler = () => { - if (!settled && shouldStopForAbort()) { - reject(new DispatchReplyOperationAbortedError()); - } - }; - signal.addEventListener("abort", abortHandler, { once: true }); - }); const work = Promise.resolve() .then(run) .then( @@ -954,6 +950,18 @@ function runWithDispatchAbortSignal( throw error; }, ); + onWorkStarted?.(work); + if (!signal) { + return work; + } + const aborted = new Promise((_, reject) => { + abortHandler = () => { + if (!settled && shouldStopForAbort()) { + reject(new DispatchReplyOperationAbortedError()); + } + }; + signal.addEventListener("abort", abortHandler, { once: true }); + }); return Promise.race([work, aborted]).finally(() => { settled = true; if (abortHandler) { @@ -1225,10 +1233,16 @@ export async function dispatchReplyFromConfig( // ACP routing uses acpDispatchSessionKey. const dispatchOperationSessionKey = initialSessionStoreEntry.sessionKey ?? sessionKey ?? acpDispatchSessionKey; + // Reply-run ownership stays on the inbound/source session. Bound ACP routing + // may use another agent's store, but must not move source lifecycle admission. + const operationSessionStoreEntry = initialSessionStoreEntry; + const initialDispatchReplyOperation = dispatchOperationSessionKey + ? replyRunRegistry.get(dispatchOperationSessionKey) + : undefined; if ( params.replyOptions?.isHeartbeat === true && dispatchOperationSessionKey && - replyRunRegistry.get(dispatchOperationSessionKey) + initialDispatchReplyOperation ) { return { queuedFinal: false, @@ -1259,6 +1273,14 @@ export async function dispatchReplyFromConfig( storePath: sessionStoreEntry.storePath, } : undefined; + let preparedOperationSessionBinding: ReplySessionBinding | undefined = + operationSessionStoreEntry.sessionKey && operationSessionStoreEntry.entry?.sessionId + ? { + sessionKey: operationSessionStoreEntry.sessionKey, + sessionId: operationSessionStoreEntry.entry.sessionId, + storePath: operationSessionStoreEntry.storePath, + } + : undefined; const sessionKeysMatch = (left?: string, right?: string) => Boolean( left && @@ -1269,7 +1291,12 @@ export async function dispatchReplyFromConfig( if (sessionKeysMatch(binding.sessionKey, sessionStoreEntry.sessionKey)) { preparedSessionBinding = binding; } + if (sessionKeysMatch(binding.sessionKey, operationSessionStoreEntry.sessionKey)) { + preparedOperationSessionBinding = binding; + } }; + const resolveOperationExpectedSessionId = () => + preparedOperationSessionBinding?.sessionId ?? operationSessionStoreEntry.entry?.sessionId; const resolvePreparedTranscriptBinding = (mirrorSessionKey?: string) => { if ( !preparedSessionBinding || @@ -1317,10 +1344,89 @@ export async function dispatchReplyFromConfig( let dispatchReplyOperation: ReplyOperation | undefined; let dispatchAbortOperation: ReplyOperation | undefined; let preDispatchAbortOperation: ReplyOperation | undefined; - type DispatchReplyOperationAcquisition = { status: "ready" } | { status: "busy" }; + let preDispatchLifecycleAdmission: SessionWorkAdmissionLease | undefined; + let preDispatchLifecycleAbortController: AbortController | undefined; + let dispatchLifecycleAbortController: AbortController | undefined; + let preDispatchLifecycleInterrupted = false; + const dispatchLifecycleWork = new Set>(); + const trackDispatchLifecycleWork = (work: Promise) => { + if (!dispatchReplyOperation && !preDispatchLifecycleAdmission) { + return; + } + const settled = work.then( + () => {}, + () => {}, + ); + dispatchLifecycleWork.add(settled); + void settled.then(() => { + dispatchLifecycleWork.delete(settled); + }); + }; + const waitForDispatchLifecycleWorkAndDelivery = async (): Promise => { + await Promise.allSettled(Array.from(dispatchLifecycleWork)); + await waitForReplyDispatcherIdle(dispatcher); + }; + const releasePreDispatchLifecycleAdmission = async ( + afterWorkBarrier?: () => PromiseLike, + ): Promise => { + const admission = preDispatchLifecycleAdmission; + const preDispatchAbortController = preDispatchLifecycleAbortController; + const dispatchAbortController = dispatchLifecycleAbortController; + preDispatchLifecycleAdmission = undefined; + if (!admission) { + return; + } + const pendingWork = Array.from(dispatchLifecycleWork); + const clearAbortControllers = () => { + if (preDispatchLifecycleAbortController === preDispatchAbortController) { + preDispatchLifecycleAbortController = undefined; + } + if (dispatchLifecycleAbortController === dispatchAbortController) { + dispatchLifecycleAbortController = undefined; + } + }; + if (!afterWorkBarrier && pendingWork.length === 0) { + clearAbortControllers(); + admission.release(); + return; + } + try { + await Promise.allSettled(pendingWork); + if (afterWorkBarrier) { + await waitForReplyBarrierSettlement( + afterWorkBarrier(), + dispatcher.resolveFollowupAdmissionBarrierTimeoutPolicy?.(), + ); + } + } finally { + clearAbortControllers(); + admission.release(); + } + }; + const runWithDispatchLifecycleAdmission = async (run: () => Promise): Promise => { + if (dispatchReplyOperation) { + return await runWithReplyOperationLifecycleAdmission(dispatchReplyOperation, run); + } + return preDispatchLifecycleAdmission + ? await preDispatchLifecycleAdmission.run(run) + : await run(); + }; + type DispatchReplyOperationAcquisition = + | { status: "ready" } + | { status: "busy" } + | { status: "aborted" }; const ensureDispatchReplyOperation = async ( phase: "pre_dispatch" | "dispatch", ): Promise => { + if (phase === "dispatch") { + // The next full reply operation revalidates the persisted session. Drop + // the hook-only lease after its queued delivery settles so a waiting + // lifecycle mutation cannot commit while that delivery is still active. + await releasePreDispatchLifecycleAdmission(() => waitForReplyDispatcherIdle(dispatcher)); + if (preDispatchLifecycleInterrupted) { + return { status: dispatchReplyOperation ? "aborted" : "busy" }; + } + } if (dispatchReplyOperation) { return { status: "ready" }; } @@ -1353,6 +1459,12 @@ export async function dispatchReplyFromConfig( ctx, routeThreadId, }); + const lifecycleOnlyAbortController = + allowActivePreDispatch || allowSlackRoutedThreadBypass ? new AbortController() : undefined; + const onLifecycleInterrupt = () => { + preDispatchLifecycleInterrupted = true; + lifecycleOnlyAbortController?.abort(); + }; const shouldRecoverStaleVisibleOperation = phase === "dispatch" && replyTurnKind === "visible" && @@ -1372,11 +1484,16 @@ export async function dispatchReplyFromConfig( let admission = await admitReplyTurn({ sessionKey: dispatchOperationSessionKey, sessionId: operationSessionId, + expectedSessionId: resolveOperationExpectedSessionId(), + expectedActiveOperation: initialDispatchReplyOperation, + storePath: operationSessionStoreEntry.storePath, kind: replyTurnKind, resetTriggered: false, routeThreadId, upstreamAbortSignal: params.replyOptions?.abortSignal, waitForActive: !allowActivePreDispatch && !allowSlackRoutedThreadBypass, + retainLifecycleAdmissionOnActive: allowActivePreDispatch || allowSlackRoutedThreadBypass, + onLifecycleInterrupt, ...(shouldRecoverStaleVisibleOperation ? { waitTimeoutMs: visibleReplyRecoveryWaitMs } : {}), }); if (shouldRecoverStaleVisibleOperation) { @@ -1417,6 +1534,9 @@ export async function dispatchReplyFromConfig( admission = await admitReplyTurn({ sessionKey: dispatchOperationSessionKey, sessionId: operationSessionId, + expectedSessionId: resolveOperationExpectedSessionId(), + expectedActiveOperation: initialDispatchReplyOperation, + storePath: operationSessionStoreEntry.storePath, kind: replyTurnKind, resetTriggered: false, routeThreadId, @@ -1435,13 +1555,13 @@ export async function dispatchReplyFromConfig( // admitted (before that op is marked `terminalRecovery`); let it fall // through to normal busy/skip handling instead. replyTurnKind === "visible" && - isRecoverableTerminalSessionStatus(sessionStoreEntry.entry?.status) && + isRecoverableTerminalSessionStatus(operationSessionStoreEntry.entry?.status) && // Only clear the leftover op that belongs to the SAME terminal session. // A concurrent reset/rotation can admit a fresh op (new sessionId) under // this session key while we still hold the stale terminal snapshot; // force-clearing by the active op's id would drop that valid in-flight // reply and recreate the message loss this fix exists to prevent (#86827). - admission.activeOperation?.sessionId === sessionStoreEntry.entry?.sessionId && + admission.activeOperation?.sessionId === operationSessionStoreEntry.entry?.sessionId && // Only clear the proven stale leftover from the failed lifecycle. A // freshly-admitted visible recovery op is marked `terminalRecovery` at the // admission choke point below; force-failing that op would drop the very @@ -1454,23 +1574,31 @@ export async function dispatchReplyFromConfig( new Error("clearing stale terminal reply operation"), ); if (cleared) { + admission.lifecycleAdmission?.release(); logVerbose( `dispatch-from-config: cleared stale active reply operation for terminal session ${dispatchOperationSessionKey}`, ); admission = await admitReplyTurn({ sessionKey: dispatchOperationSessionKey, sessionId: operationSessionId, + expectedSessionId: resolveOperationExpectedSessionId(), + expectedActiveOperation: initialDispatchReplyOperation, + storePath: operationSessionStoreEntry.storePath, kind: replyTurnKind, resetTriggered: false, routeThreadId, upstreamAbortSignal: params.replyOptions?.abortSignal, waitForActive: !allowActivePreDispatch && !allowSlackRoutedThreadBypass, + retainLifecycleAdmissionOnActive: allowActivePreDispatch || allowSlackRoutedThreadBypass, + onLifecycleInterrupt, }); } } if (admission.status === "skipped") { if (allowActivePreDispatch && admission.reason === "active-run") { preDispatchAbortOperation = admission.activeOperation; + preDispatchLifecycleAdmission = admission.lifecycleAdmission; + preDispatchLifecycleAbortController = lifecycleOnlyAbortController; return { status: "ready" }; } if ( @@ -1481,11 +1609,14 @@ export async function dispatchReplyFromConfig( routeThreadId, }) ) { + preDispatchLifecycleAdmission = admission.lifecycleAdmission; + dispatchLifecycleAbortController = lifecycleOnlyAbortController; logVerbose( `dispatch-from-config: allowing Slack routed thread ${routeThreadId} while ${dispatchOperationSessionKey} has an active reply operation in another Slack thread`, ); return { status: "ready" }; } + admission.lifecycleAdmission?.release(); dispatchAbortOperation = admission.activeOperation; logVerbose( `dispatch-from-config: skipped reply operation admission for ${dispatchOperationSessionKey}; reason=${admission.reason}`, @@ -1501,8 +1632,8 @@ export async function dispatchReplyFromConfig( // so they stay unmarked and remain force-clearable. if ( replyTurnKind === "visible" && - isRecoverableTerminalSessionStatus(sessionStoreEntry.entry?.status) && - operationSessionId === sessionStoreEntry.entry?.sessionId + isRecoverableTerminalSessionStatus(operationSessionStoreEntry.entry?.status) && + operationSessionId === operationSessionStoreEntry.entry?.sessionId ) { admission.operation.markTerminalRecovery(); } @@ -1515,6 +1646,7 @@ export async function dispatchReplyFromConfig( let cachedPreDispatchAbortSignal: | { operationSignal: AbortSignal | undefined; + lifecycleSignal: AbortSignal | undefined; upstreamSignal: AbortSignal | undefined; signal: AbortSignal | undefined; } @@ -1528,20 +1660,23 @@ export async function dispatchReplyFromConfig( | undefined; const getPreDispatchAbortSignal = () => { const operationSignal = getPreDispatchAbortOperation()?.abortSignal; + const lifecycleSignal = preDispatchLifecycleAbortController?.signal; const upstreamSignal = params.replyOptions?.abortSignal; if ( cachedPreDispatchAbortSignal && cachedPreDispatchAbortSignal.operationSignal === operationSignal && + cachedPreDispatchAbortSignal.lifecycleSignal === lifecycleSignal && cachedPreDispatchAbortSignal.upstreamSignal === upstreamSignal ) { return cachedPreDispatchAbortSignal.signal; } - const signal = composeAbortSignals(operationSignal, upstreamSignal); - cachedPreDispatchAbortSignal = { operationSignal, upstreamSignal, signal }; + const signal = composeAbortSignals(operationSignal, lifecycleSignal, upstreamSignal); + cachedPreDispatchAbortSignal = { operationSignal, lifecycleSignal, upstreamSignal, signal }; return signal; }; const getDispatchAbortSignal = () => { - const operationSignal = dispatchReplyOperation?.abortSignal; + const operationSignal = + dispatchReplyOperation?.abortSignal ?? dispatchLifecycleAbortController?.signal; // The operation mirrors upstream aborts until the backend commits its // terminal outcome, then keeps delivery alive after freezeAbort(). const upstreamSignal = operationSignal ? undefined : params.replyOptions?.abortSignal; @@ -1579,14 +1714,18 @@ export async function dispatchReplyFromConfig( }; }; const completeDispatchReplyOperation = () => { + const completionBarrier = waitForDispatchLifecycleWorkAndDelivery(); + void releasePreDispatchLifecycleAdmission(() => waitForReplyDispatcherIdle(dispatcher)); if (dispatchReplyOperation) { dispatchReplyOperation.completeWithAfterClearBarrier( - waitForReplyDispatcherIdle(dispatcher), + completionBarrier, dispatcher.resolveFollowupAdmissionBarrierTimeoutPolicy?.(), ); } }; const failDispatchReplyOperation = (error: unknown) => { + const completionBarrier = waitForDispatchLifecycleWorkAndDelivery(); + void releasePreDispatchLifecycleAdmission(() => waitForReplyDispatcherIdle(dispatcher)); if (!dispatchReplyOperation) { return; } @@ -1595,7 +1734,7 @@ export async function dispatchReplyFromConfig( dispatchReplyOperation.fail("run_failed", error); } dispatchReplyOperation.completeWithAfterClearBarrier( - waitForReplyDispatcherIdle(dispatcher), + completionBarrier, dispatcher.resolveFollowupAdmissionBarrierTimeoutPolicy?.(), ); }; @@ -2098,6 +2237,7 @@ export async function dispatchReplyFromConfig( recordAgentDispatchCompleted?: boolean; sessionMetadataChanges?: DispatchFromConfigResult["sessionMetadataChanges"]; }): DispatchFromConfigResult => { + void releasePreDispatchLifecycleAdmission(() => waitForReplyDispatcherIdle(dispatcher)); if (opts?.recordAgentDispatchCompleted) { recordAgentDispatchCompleted("completed", { reason: "reply-operation-active" }); } @@ -2131,169 +2271,33 @@ export async function dispatchReplyFromConfig( | "plugin-bound-fallback-missing-plugin" | "plugin-bound-fallback-no-handler" | undefined; - - if (pluginOwnedBinding) { - if (isPreDispatchOperationAborted()) { - return finishReplyOperationAbortedDispatch(); + const emitMessageReceivedHooks = () => { + if ( + ctx.SuppressMessageReceivedHooks !== true && + hookRunner?.hasHooks("message_received") === true + ) { + const messageReceivedHookContext = buildMessageReceivedHookContext(); + fireAndForgetHook( + hookRunner.runMessageReceived( + toPluginMessageReceivedEvent(messageReceivedHookContext), + toPluginMessageContext(messageReceivedHookContext), + ), + "dispatch-from-config: message_received plugin hook failed", + ); } - touchConversationBindingRecord(pluginOwnedBinding.bindingId); - if (shouldBypassPluginOwnedBindingForCommand(ctx, cfg)) { - logVerbose( - `plugin-bound inbound command escaped plugin binding (plugin=${pluginOwnedBinding.pluginId} session=${sessionKey ?? "unknown"}); falling through to command processing`, + if (ctx.SuppressMessageReceivedHooks !== true && sessionKey) { + const messageReceivedHookContext = buildMessageReceivedHookContext(); + fireAndForgetHook( + triggerInternalHook( + createInternalHookEvent("message", "received", sessionKey, { + ...toInternalMessageReceivedContext(messageReceivedHookContext), + timestamp, + }), + ), + "dispatch-from-config: message_received internal hook failed", ); - } else if (sendPolicyDenied || (suppressDelivery && !suppressAutomaticSourceDelivery)) { - // Plugin-bound inbound handlers typically emit outbound replies we - // cannot rewind. When automatic delivery is explicitly denied, skip the - // plugin claim and fall through to normal suppressed agent processing. - // message_tool_only is the normal visible-reply mode for group chats and - // must still let the bound plugin own the turn unless sendPolicy denied it. - logVerbose( - `plugin-bound inbound skipped under ${deliverySuppressionReason} (plugin=${pluginOwnedBinding.pluginId} session=${sessionKey ?? "unknown"}); falling through to suppressed agent processing`, - ); - } else { - logVerbose( - `plugin-bound inbound routed to ${pluginOwnedBinding.pluginId} conversation=${pluginOwnedBinding.conversationId}`, - ); - // Bound native runtimes need the current owner decision, not stale bind-time identity. - // The resolver folds internal operator.admin authority into this owner decision. - const bindingAuthorization = resolveCommandAuthorization({ - ctx, - cfg, - commandAuthorized: ctx.CommandAuthorized, - }); - const targetedClaimOutcome = hookRunner?.runInboundClaimForPluginOutcome - ? await (async () => { - await prepareHookMediaMetadata(); - const authorizedInboundClaimEvent = { - ...inboundClaimEvent, - senderIsOwner: bindingAuthorization.senderIsOwner, - }; - return hookRunner.runInboundClaimForPluginOutcome( - pluginOwnedBinding.pluginId, - authorizedInboundClaimEvent, - { ...inboundClaimContext, pluginBinding: pluginOwnedBinding }, - ); - })() - : (() => { - const pluginLoaded = - getGlobalPluginRegistry()?.plugins.some( - (plugin) => plugin.id === pluginOwnedBinding.pluginId && plugin.status === "loaded", - ) ?? false; - return pluginLoaded - ? ({ status: "no_handler" } as const) - : ({ status: "missing_plugin" } as const); - })(); - - switch (targetedClaimOutcome.status) { - case "handled": { - if (targetedClaimOutcome.result.reply && shouldDeliverPluginBindingReply) { - // A bound plugin's reply is the explicit output for this claimed turn, - // not an automatic agent final; message-tool-only suppression must not - // turn normal user-request bindings into silent channel responses. - // Ambient room events keep the same privacy guard as final replies. - await deliverBindingPayload(targetedClaimOutcome.result.reply, "terminal"); - } - markIdle("plugin_binding_dispatch"); - recordProcessed("completed", { reason: "plugin-bound-handled" }); - commitInboundDedupeIfClaimed(); - return attachSourceReplyDeliveryMode({ - queuedFinal: false, - counts: dispatcher.getQueuedCounts(), - }); - } - case "missing_plugin": - case "no_handler": { - pluginFallbackReason = - targetedClaimOutcome.status === "missing_plugin" - ? "plugin-bound-fallback-missing-plugin" - : "plugin-bound-fallback-no-handler"; - const isUnmentionedGroupFallback = - (chatType === "group" || chatType === "channel") && - ctx.WasMentioned === false && - !explicitCommandTurnCtx; - const shouldSuppressUnmentionedFallback = - isUnmentionedGroupFallback && ctx.GroupRequireMention !== false; - if (shouldSuppressUnmentionedFallback) { - markIdle("plugin_binding_fallback_unmentioned"); - recordProcessed("completed", { reason: pluginFallbackReason }); - commitInboundDedupeIfClaimed(); - return attachSourceReplyDeliveryMode({ - queuedFinal: false, - counts: dispatcher.getQueuedCounts(), - }); - } - if (!hasShownPluginBindingFallbackNotice(pluginOwnedBinding.bindingId)) { - const didSendNotice = await sendBindingNotice( - { text: buildPluginBindingUnavailableText(pluginOwnedBinding) }, - "additive", - ); - if (didSendNotice) { - markPluginBindingFallbackNoticeShown(pluginOwnedBinding.bindingId); - } - } - break; - } - case "declined": { - await sendBindingNotice( - { text: buildPluginBindingDeclinedText(pluginOwnedBinding) }, - "terminal", - ); - markIdle("plugin_binding_declined"); - recordProcessed("completed", { reason: "plugin-bound-declined" }); - commitInboundDedupeIfClaimed(); - return attachSourceReplyDeliveryMode({ - queuedFinal: false, - counts: dispatcher.getQueuedCounts(), - }); - } - case "error": { - logVerbose( - `plugin-bound inbound claim failed for ${pluginOwnedBinding.pluginId}: ${targetedClaimOutcome.error}`, - ); - await sendBindingNotice( - { text: buildPluginBindingErrorText(pluginOwnedBinding) }, - "terminal", - ); - markIdle("plugin_binding_error"); - recordProcessed("completed", { reason: "plugin-bound-error" }); - commitInboundDedupeIfClaimed(); - return attachSourceReplyDeliveryMode({ - queuedFinal: false, - counts: dispatcher.getQueuedCounts(), - }); - } - } } - } - - // Trigger plugin hooks (fire-and-forget) - const hasMessageReceivedHookConsumer = - ctx.SuppressMessageReceivedHooks !== true && hookRunner?.hasHooks("message_received") === true; - if (hasMessageReceivedHookConsumer) { - const messageReceivedHookContext = buildMessageReceivedHookContext(); - fireAndForgetHook( - hookRunner.runMessageReceived( - toPluginMessageReceivedEvent(messageReceivedHookContext), - toPluginMessageContext(messageReceivedHookContext), - ), - "dispatch-from-config: message_received plugin hook failed", - ); - } - - // Bridge to internal hooks (HOOK.md discovery system) - refs #8807 - if (ctx.SuppressMessageReceivedHooks !== true && sessionKey) { - const messageReceivedHookContext = buildMessageReceivedHookContext(); - fireAndForgetHook( - triggerInternalHook( - createInternalHookEvent("message", "received", sessionKey, { - ...toInternalMessageReceivedContext(messageReceivedHookContext), - timestamp, - }), - ), - "dispatch-from-config: message_received internal hook failed", - ); - } - + }; markProcessing(); try { @@ -2306,6 +2310,10 @@ export async function dispatchReplyFromConfig( } const fastAbort = await fastAbortResolver({ ctx, cfg }); if (fastAbort.handled) { + if (pluginOwnedBinding) { + touchConversationBindingRecord(pluginOwnedBinding.bindingId); + } + emitMessageReceivedHooks(); let queuedFinal = false; let routedFinalCount = 0; if (!suppressDelivery) { @@ -2340,12 +2348,167 @@ export async function dispatchReplyFromConfig( completeDispatchReplyOperation(); return attachSourceReplyDeliveryMode({ queuedFinal, counts }); } - // Register the dispatch-owned operation before any plugin hook or model work - // so /stop can abort pre-run and in-run stalls through the same session lane. - if ((await ensureDispatchReplyOperation("pre_dispatch")).status === "busy") { + // Own the session before plugin-bound handlers or message hooks can perform + // work. Fast abort and inbound dedupe intentionally remain ahead of this gate. + const preDispatchAcquisition = await ensureDispatchReplyOperation("pre_dispatch"); + if (preDispatchAcquisition.status === "aborted") { + return finishReplyOperationAbortedDispatch(); + } + if (preDispatchAcquisition.status === "busy") { return finishReplyOperationBusyDispatch({ dedupeDisposition: "release" }); } + if (pluginOwnedBinding) { + if (isPreDispatchOperationAborted()) { + return finishReplyOperationAbortedDispatch(); + } + touchConversationBindingRecord(pluginOwnedBinding.bindingId); + if (shouldBypassPluginOwnedBindingForCommand(ctx, cfg)) { + logVerbose( + `plugin-bound inbound command escaped plugin binding (plugin=${pluginOwnedBinding.pluginId} session=${sessionKey ?? "unknown"}); falling through to command processing`, + ); + } else if (sendPolicyDenied || (suppressDelivery && !suppressAutomaticSourceDelivery)) { + // Plugin-bound inbound handlers typically emit outbound replies we + // cannot rewind. When automatic delivery is explicitly denied, skip the + // plugin claim and fall through to normal suppressed agent processing. + // message_tool_only is the normal visible-reply mode for group chats and + // must still let the bound plugin own the turn unless sendPolicy denied it. + logVerbose( + `plugin-bound inbound skipped under ${deliverySuppressionReason} (plugin=${pluginOwnedBinding.pluginId} session=${sessionKey ?? "unknown"}); falling through to suppressed agent processing`, + ); + } else { + logVerbose( + `plugin-bound inbound routed to ${pluginOwnedBinding.pluginId} conversation=${pluginOwnedBinding.conversationId}`, + ); + // Bound native runtimes need the current owner decision, not stale bind-time identity. + // The resolver folds internal operator.admin authority into this owner decision. + const bindingAuthorization = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: ctx.CommandAuthorized, + }); + const targetedClaimOutcome = hookRunner?.runInboundClaimForPluginOutcome + ? await (async () => { + await prepareHookMediaMetadata(); + if (isPreDispatchOperationAborted()) { + throw new DispatchReplyOperationAbortedError(); + } + const authorizedInboundClaimEvent = { + ...inboundClaimEvent, + senderIsOwner: bindingAuthorization.senderIsOwner, + }; + return await runWithDispatchLifecycleAdmission( + async () => + await hookRunner.runInboundClaimForPluginOutcome( + pluginOwnedBinding.pluginId, + authorizedInboundClaimEvent, + { ...inboundClaimContext, pluginBinding: pluginOwnedBinding }, + ), + ); + })() + : (() => { + const pluginLoaded = + getGlobalPluginRegistry()?.plugins.some( + (plugin) => + plugin.id === pluginOwnedBinding.pluginId && plugin.status === "loaded", + ) ?? false; + return pluginLoaded + ? ({ status: "no_handler" } as const) + : ({ status: "missing_plugin" } as const); + })(); + + if (isPreDispatchOperationAborted()) { + return finishReplyOperationAbortedDispatch(); + } + + switch (targetedClaimOutcome.status) { + case "handled": { + if (targetedClaimOutcome.result.reply && shouldDeliverPluginBindingReply) { + // A bound plugin's reply is the explicit output for this claimed turn, + // not an automatic agent final; message-tool-only suppression must not + // turn normal user-request bindings into silent channel responses. + // Ambient room events keep the same privacy guard as final replies. + await deliverBindingPayload(targetedClaimOutcome.result.reply, "terminal"); + } + markIdle("plugin_binding_dispatch"); + recordProcessed("completed", { reason: "plugin-bound-handled" }); + commitInboundDedupeIfClaimed(); + completeDispatchReplyOperation(); + return attachSourceReplyDeliveryMode({ + queuedFinal: false, + counts: dispatcher.getQueuedCounts(), + }); + } + case "missing_plugin": + case "no_handler": { + pluginFallbackReason = + targetedClaimOutcome.status === "missing_plugin" + ? "plugin-bound-fallback-missing-plugin" + : "plugin-bound-fallback-no-handler"; + const isUnmentionedGroupFallback = + (chatType === "group" || chatType === "channel") && + ctx.WasMentioned === false && + !explicitCommandTurnCtx; + const shouldSuppressUnmentionedFallback = + isUnmentionedGroupFallback && ctx.GroupRequireMention !== false; + if (shouldSuppressUnmentionedFallback) { + markIdle("plugin_binding_fallback_unmentioned"); + recordProcessed("completed", { reason: pluginFallbackReason }); + commitInboundDedupeIfClaimed(); + completeDispatchReplyOperation(); + return attachSourceReplyDeliveryMode({ + queuedFinal: false, + counts: dispatcher.getQueuedCounts(), + }); + } + if (!hasShownPluginBindingFallbackNotice(pluginOwnedBinding.bindingId)) { + const didSendNotice = await sendBindingNotice( + { text: buildPluginBindingUnavailableText(pluginOwnedBinding) }, + "additive", + ); + if (didSendNotice) { + markPluginBindingFallbackNoticeShown(pluginOwnedBinding.bindingId); + } + } + break; + } + case "declined": { + await sendBindingNotice( + { text: buildPluginBindingDeclinedText(pluginOwnedBinding) }, + "terminal", + ); + markIdle("plugin_binding_declined"); + recordProcessed("completed", { reason: "plugin-bound-declined" }); + commitInboundDedupeIfClaimed(); + completeDispatchReplyOperation(); + return attachSourceReplyDeliveryMode({ + queuedFinal: false, + counts: dispatcher.getQueuedCounts(), + }); + } + case "error": { + logVerbose( + `plugin-bound inbound claim failed for ${pluginOwnedBinding.pluginId}: ${targetedClaimOutcome.error}`, + ); + await sendBindingNotice( + { text: buildPluginBindingErrorText(pluginOwnedBinding) }, + "terminal", + ); + markIdle("plugin_binding_error"); + recordProcessed("completed", { reason: "plugin-bound-error" }); + commitInboundDedupeIfClaimed(); + completeDispatchReplyOperation(); + return attachSourceReplyDeliveryMode({ + queuedFinal: false, + counts: dispatcher.getQueuedCounts(), + }); + } + } + } + } + + emitMessageReceivedHooks(); + const shouldSuppressDefaultToolProgressMessages = () => !shouldEmitVerboseProgress(); const shouldSendVerboseProgressMessages = () => !shouldSuppressDefaultToolProgressMessages(); const shouldSendToolSummaries = () => shouldSendVerboseProgressMessages(); @@ -2611,35 +2774,41 @@ export async function dispatchReplyFromConfig( // Run before_dispatch hook — let plugins inspect or handle before model dispatch. if (hookRunner?.hasHooks("before_dispatch")) { const beforeDispatchResult = await traceReplyPhase("reply.before_dispatch_hooks", () => - runWithDispatchAbortSignal(getPreDispatchAbortSignal(), () => - hookRunner.runBeforeDispatch( - { - content: hookContext.content, - body: hookContext.bodyForAgent ?? hookContext.body, - channel: hookContext.channelId, - sessionKey: sessionStoreEntry.sessionKey ?? sessionKey, - senderId: hookContext.senderId, - replyToId: hookContext.replyToId, - replyToIdFull: hookContext.replyToIdFull, - replyToBody: hookContext.replyToBody, - replyToSender: hookContext.replyToSender, - replyToIsQuote: hookContext.replyToIsQuote, - isGroup: hookContext.isGroup, - timestamp: hookContext.timestamp, - }, - { - channelId: hookContext.channelId, - accountId: hookContext.accountId, - conversationId: inboundClaimContext.conversationId, - sessionKey: sessionStoreEntry.sessionKey ?? sessionKey, - senderId: hookContext.senderId, - replyToId: hookContext.replyToId, - replyToIdFull: hookContext.replyToIdFull, - replyToBody: hookContext.replyToBody, - replyToSender: hookContext.replyToSender, - replyToIsQuote: hookContext.replyToIsQuote, - }, - ), + runWithDispatchLifecycleAdmission( + async () => + await runWithDispatchAbortSignal( + getPreDispatchAbortSignal(), + () => + hookRunner.runBeforeDispatch( + { + content: hookContext.content, + body: hookContext.bodyForAgent ?? hookContext.body, + channel: hookContext.channelId, + sessionKey: sessionStoreEntry.sessionKey ?? sessionKey, + senderId: hookContext.senderId, + replyToId: hookContext.replyToId, + replyToIdFull: hookContext.replyToIdFull, + replyToBody: hookContext.replyToBody, + replyToSender: hookContext.replyToSender, + replyToIsQuote: hookContext.replyToIsQuote, + isGroup: hookContext.isGroup, + timestamp: hookContext.timestamp, + }, + { + channelId: hookContext.channelId, + accountId: hookContext.accountId, + conversationId: inboundClaimContext.conversationId, + sessionKey: sessionStoreEntry.sessionKey ?? sessionKey, + senderId: hookContext.senderId, + replyToId: hookContext.replyToId, + replyToIdFull: hookContext.replyToIdFull, + replyToBody: hookContext.replyToBody, + replyToSender: hookContext.replyToSender, + replyToIsQuote: hookContext.replyToIsQuote, + }, + ), + trackDispatchLifecycleWork, + ), ), ); if (beforeDispatchResult?.handled) { @@ -2669,38 +2838,44 @@ export async function dispatchReplyFromConfig( if (hookRunner?.hasHooks("reply_dispatch")) { const replyDispatchResult = await traceReplyPhase("reply.reply_dispatch_hooks", () => - runWithDispatchAbortSignal(getPreDispatchAbortSignal(), () => - hookRunner.runReplyDispatch( - createReplyDispatchEvent({ - ctx, - runId: params.replyOptions?.runId, - sessionKey: acpDispatchSessionKey, - toolsAllow: params.replyOptions?.toolsAllow, - images: params.replyOptions?.images, - inboundAudio, - sessionTtsAuto, - ttsChannel: deliveryChannel, - suppressUserDelivery: suppressHookUserDelivery, - suppressReplyLifecycle: suppressHookReplyLifecycle, - sourceReplyDeliveryMode, - shouldRouteToOriginating, - originatingChannel: routeReplyChannel, - originatingTo: routeReplyTo, - originatingAccountId: replyContextAccountId, - originatingThreadId: routeReplyThreadId, - originatingChatType: replyRoute.chatType, - shouldSendToolSummaries, - sendPolicy, - }), - { - cfg, - dispatcher: dispatchHookDispatcher, - abortSignal: getPreDispatchAbortSignal() ?? params.replyOptions?.abortSignal, - onReplyStart: params.replyOptions?.onReplyStart, - recordProcessed, - markIdle, - }, - ), + runWithDispatchLifecycleAdmission( + async () => + await runWithDispatchAbortSignal( + getPreDispatchAbortSignal(), + () => + hookRunner.runReplyDispatch( + createReplyDispatchEvent({ + ctx, + runId: params.replyOptions?.runId, + sessionKey: acpDispatchSessionKey, + toolsAllow: params.replyOptions?.toolsAllow, + images: params.replyOptions?.images, + inboundAudio, + sessionTtsAuto, + ttsChannel: deliveryChannel, + suppressUserDelivery: suppressHookUserDelivery, + suppressReplyLifecycle: suppressHookReplyLifecycle, + sourceReplyDeliveryMode, + shouldRouteToOriginating, + originatingChannel: routeReplyChannel, + originatingTo: routeReplyTo, + originatingAccountId: replyContextAccountId, + originatingThreadId: routeReplyThreadId, + originatingChatType: replyRoute.chatType, + shouldSendToolSummaries, + sendPolicy, + }), + { + cfg, + dispatcher: dispatchHookDispatcher, + abortSignal: getPreDispatchAbortSignal() ?? params.replyOptions?.abortSignal, + onReplyStart: params.replyOptions?.onReplyStart, + recordProcessed, + markIdle, + }, + ), + trackDispatchLifecycleWork, + ), ), ); if (replyDispatchResult?.handled) { @@ -2713,7 +2888,11 @@ export async function dispatchReplyFromConfig( } } - if ((await ensureDispatchReplyOperation("dispatch")).status === "busy") { + const dispatchAcquisition = await ensureDispatchReplyOperation("dispatch"); + if (dispatchAcquisition.status === "aborted") { + return finishReplyOperationAbortedDispatch(); + } + if (dispatchAcquisition.status === "busy") { return finishReplyOperationBusyDispatch({ dedupeDisposition: "release" }); } @@ -3073,396 +3252,436 @@ export async function dispatchReplyFromConfig( params.configOverride ? (applyMergePatch(cfg, params.configOverride) as OpenClawConfig) : cfg, ); recordAgentDispatchStarted(); - const replyResult = await runWithDispatchAbortSignal(getDispatchAbortSignal(), () => - traceReplyPhase("reply.run_reply_resolver", () => - replyResolver( - ctx, - { - ...getReplyOptions(), - sourceReplyDeliveryMode, - sessionPromptSourceReplyDeliveryMode: sessionStableSourceReplyDeliveryMode, - ...({ - onSessionMetadataChanges: notifySessionMetadataChanges, - onSessionPrepared: notePreparedSession, - } satisfies InternalReplyResolverOptions), - onObservedReplyDelivery: markObservedReplyDelivery, - suppressToolErrorWarnings, - shouldSuppressToolErrorWarnings, - typingPolicy: typing.typingPolicy, - suppressTyping: typing.suppressTyping, - onPartialReply: wrapProgressCallback(params.replyOptions?.onPartialReply), - onReasoningStream: wrapProgressCallback(params.replyOptions?.onReasoningStream), - streamReasoningInNonStreamModes: params.replyOptions?.streamReasoningInNonStreamModes, - onReasoningEnd: wrapProgressCallback(params.replyOptions?.onReasoningEnd), - onAssistantMessageStart: wrapProgressCallback( - params.replyOptions?.onAssistantMessageStart, - ), - onBlockReplyQueued: wrapProgressCallback(params.replyOptions?.onBlockReplyQueued), - onToolStart: wrapProgressCallback(params.replyOptions?.onToolStart, { - allowWhenToolSummariesHidden: - params.replyOptions?.allowToolLifecycleWhenProgressHidden === true, - forwardWhenSourceDeliverySuppressed: true, - requiresToolSummaryVisibility: true, - waitForDirectBlockReplyDelivery: true, - onForward: async () => { - // Commentary precedes the tool that follows it. - await flushPendingCommentaryProgress(); - }, - }), - onItemEvent, - commentaryProgressEnabled: - deliverStandaloneCommentaryProgress || - canForwardSuppressedSourceItemEvents || - params.replyOptions?.commentaryProgressEnabled, - reasoningPayloadsEnabled, - commentaryPayloadsEnabled, - onCommandOutput: wrapProgressCallback(params.replyOptions?.onCommandOutput, { - forwardWhenSourceDeliverySuppressed: true, - requiresToolSummaryVisibility: true, - waitForDirectBlockReplyDelivery: true, - onForward: (payload) => { - if (hasFailedProgressStatus(payload)) { - markVisibleToolErrorProgress(); - } - }, - }), - onCompactionStart: wrapProgressCallback(params.replyOptions?.onCompactionStart, { - allowWhenToolSummariesHidden: - params.replyOptions?.allowToolLifecycleWhenProgressHidden === true, - forwardWhenSourceDeliverySuppressed: true, - requiresToolSummaryVisibility: true, - waitForDirectBlockReplyDelivery: true, - }), - onCompactionEnd: wrapProgressCallback(params.replyOptions?.onCompactionEnd, { - allowWhenToolSummariesHidden: - params.replyOptions?.allowToolLifecycleWhenProgressHidden === true, - forwardWhenSourceDeliverySuppressed: true, - requiresToolSummaryVisibility: true, - waitForDirectBlockReplyDelivery: true, - }), - onToolResult: (payload: ReplyPayload) => { - markProgress(); - const run = async () => { - if (isDispatchOperationAborted()) { - return; - } - await waitForPendingDirectBlockReplyDelivery(dispatchAbortOperation?.abortSignal); - if (isDispatchOperationAborted()) { - return; - } - markInboundDedupeReplayUnsafe(); - // Buffered commentary preceded this tool; land it before the summary. - await flushPendingCommentaryProgress(); - // When the operator opts into messages.suppressToolErrors, never - // surface tool-error tool-result payloads as channel progress, - // regardless of source delivery mode. payloads.ts already drops - // the warning text; this drops the visible progress delivery too. - if (payload.isError === true && replyConfig.messages?.suppressToolErrors === true) { - return; - } - const isFastModeAutoProgress = isFastModeAutoProgressPayload(payload); - const isFastModeAutoProgressDelivery = - isFastModeAutoProgress && - shouldDeliverFastModeAutoProgressDespiteSourceSuppression(); - const isForcedToolProgress = - shouldDeliverForcedToolProgressDespiteSourceSuppression(); - const progressCallbackForwarded = shouldForwardToolResultProgressCallback( - payload, - isFastModeAutoProgress, - ); - if (progressCallbackForwarded) { - await onToolResultFromReplyOptions?.(payload); - } - if (isDispatchOperationAborted()) { - return; - } - if ( - isFastModeAutoProgress && - progressCallbackForwarded && - onToolResultFromReplyOptions - ) { - return; - } - if (sendPolicyDenied) { - return; - } - if ( - shouldSuppressProgressDelivery() && - !isFastModeAutoProgressDelivery && - !isForcedToolProgress - ) { - return; - } - const visibleToolPayload = isForcedToolProgress - ? payload - : resolveToolDeliveryPayload(payload); - if (!visibleToolPayload) { - return; - } - const ttsPayload = await maybeApplyTtsToReplyPayload({ - payload: visibleToolPayload, - cfg, - channel: deliveryChannel, - kind: "tool", - inboundAudio, - ttsAuto: sessionTtsAuto, - agentId: sessionAgentId, - accountId: replyRoute.accountId, - }); - const normalizedPayload = await normalizeReplyMediaPayload(ttsPayload); - const deliveryPayload = isForcedToolProgress - ? normalizedPayload - : resolveToolDeliveryPayload(normalizedPayload); - if (!deliveryPayload) { - return; - } - if (isDispatchOperationAborted()) { - return; - } - if ( - shouldSuppressLateTextOnlyToolProgress(deliveryPayload) && - !isFastModeAutoProgressPayload(deliveryPayload) && - !isForcedToolProgress - ) { - return; - } - if (shouldSuppressMessageToolOnlyTextErrorProgress(deliveryPayload)) { - return; - } - if ( - shouldSuppressDefaultToolProgressMessages() && - !isFastModeAutoProgressPayload(deliveryPayload) && - !isForcedToolProgress - ) { - const hasMedia = resolveSendableOutboundReplyParts(deliveryPayload).hasMedia; - if (!hasMedia && !hasExecApprovalPayload(deliveryPayload)) { - return; - } - } - if (deliveryPayload.isError === true) { - markVisibleToolErrorProgress(); - } - if (shouldRouteToOriginating) { - await sendPayloadAsync(deliveryPayload, undefined, false); - } else { - markInboundDedupeReplayUnsafe(); - dispatcher.sendToolResult(deliveryPayload); - } - }; - return run(); - }, - onPlanUpdate: async (payload) => { - if (isDispatchOperationAborted()) { - return; - } - markProgress(); - await waitForPendingDirectBlockReplyDelivery(dispatchAbortOperation?.abortSignal); - if (isDispatchOperationAborted()) { - return; - } - markInboundDedupeReplayUnsafe(); - if ( - shouldForwardProgressCallback({ - forwardWhenSourceDeliverySuppressed: true, - requiresToolSummaryVisibility: true, - }) - ) { - await onPlanUpdateFromReplyOptions?.(payload); - } - if (isDispatchOperationAborted()) { - return; - } - if (payload.phase !== "update" || shouldSuppressDefaultToolProgressMessages()) { - return; - } - await sendPlanUpdate({ explanation: payload.explanation, steps: payload.steps }); - }, - onApprovalEvent: async (payload) => { - if (isDispatchOperationAborted()) { - return; - } - markProgress(); - await waitForPendingDirectBlockReplyDelivery(dispatchAbortOperation?.abortSignal); - if (isDispatchOperationAborted()) { - return; - } - markInboundDedupeReplayUnsafe(); - if ( - shouldForwardProgressCallback({ - forwardWhenSourceDeliverySuppressed: true, - requiresToolSummaryVisibility: true, - }) - ) { - await onApprovalEventFromReplyOptions?.(payload); - } - if (isDispatchOperationAborted()) { - return; - } - if (payload.phase !== "requested" || shouldSuppressDefaultToolProgressMessages()) { - return; - } - const label = summarizeApprovalLabel({ - status: payload.status, - command: payload.command, - message: payload.message, - }); - if (!label) { - return; - } - await maybeSendWorkingStatus(label); - }, - onPatchSummary: async (payload) => { - if (isDispatchOperationAborted()) { - return; - } - markProgress(); - await waitForPendingDirectBlockReplyDelivery(dispatchAbortOperation?.abortSignal); - if (isDispatchOperationAborted()) { - return; - } - markInboundDedupeReplayUnsafe(); - if ( - shouldForwardProgressCallback({ - forwardWhenSourceDeliverySuppressed: true, - requiresToolSummaryVisibility: true, - }) - ) { - await onPatchSummaryFromReplyOptions?.(payload); - } - if (isDispatchOperationAborted()) { - return; - } - if (payload.phase !== "end" || shouldSuppressDefaultToolProgressMessages()) { - return; - } - const label = summarizePatchLabel({ summary: payload.summary, title: payload.title }); - if (!label) { - return; - } - await maybeSendWorkingStatus(label); - }, - onBlockReply: (payload: ReplyPayload, context?: BlockReplyContext) => { - markProgress(); - const run = async () => { - if (isDispatchOperationAborted()) { - return; - } - if ( - payload.isReasoning !== true && - payload.isCommentary !== true && - hasOutboundReplyContent(payload, { trimText: true }) - ) { - markInboundDedupeReplayUnsafe(); - } - // Buffered commentary preceded this block; deliver it first. - await flushPendingCommentaryProgress(); - if (suppressDelivery) { - return; - } - // Durable reasoning is a channel-owned lane; generic channels - // keep the historical suppression unless they explicitly opt in. - if (payload.isReasoning === true && !reasoningPayloadsEnabled) { - return; - } - // Durable commentary is a channel-owned lane; generic channels keep the - // historical suppression unless they explicitly opt in. - if (payload.isCommentary === true && !commentaryPayloadsEnabled) { - return; - } - // Accumulate block text for TTS generation after streaming. - // Exclude status notices — they are informational UI signals - // and must not be synthesised into the spoken reply. Display - // lanes stay out too: they are presentation, never final text. - const isStatusNotice = isReplyPayloadStatusNotice(payload); - if ( - payload.text && - !isStatusNotice && - payload.isReasoning !== true && - payload.isCommentary !== true - ) { - const joinsBufferedTtsDirective = - cleanBlockTtsDirectiveText?.hasBufferedDirectiveText() === true; - if (accumulatedBlockText.length > 0) { - accumulatedBlockText += "\n"; - } - accumulatedBlockText += payload.text; - if (accumulatedBlockTtsText.length > 0 && !joinsBufferedTtsDirective) { - accumulatedBlockTtsText += "\n"; - } - accumulatedBlockTtsText += payload.text; - blockCount++; - } - const visiblePayload = - payload.text && - cleanBlockTtsDirectiveText && - !isStatusNotice && - payload.isReasoning !== true && - payload.isCommentary !== true - ? (() => { - const text = cleanBlockTtsDirectiveText.push(payload.text); - return copyReplyPayloadMetadata(payload, { - ...payload, - text: text.trim() ? text : undefined, - }); - })() - : payload; - if (!hasOutboundReplyContent(visiblePayload, { trimText: true })) { - return; - } - // Channels that keep a live draft preview may need to rotate their - // preview state at the logical block boundary before queued block - // delivery drains asynchronously through the dispatcher. - const payloadMetadata = getReplyPayloadMetadata(payload); - const queuedContext = - payloadMetadata?.assistantMessageIndex !== undefined - ? { - ...context, - assistantMessageIndex: payloadMetadata.assistantMessageIndex, + const replyResult = await runWithDispatchLifecycleAdmission( + async () => + await runWithDispatchAbortSignal( + getDispatchAbortSignal(), + () => + traceReplyPhase("reply.run_reply_resolver", () => + replyResolver( + ctx, + { + ...getReplyOptions(), + sourceReplyDeliveryMode, + sessionPromptSourceReplyDeliveryMode: sessionStableSourceReplyDeliveryMode, + ...({ + onSessionMetadataChanges: notifySessionMetadataChanges, + onSessionPrepared: notePreparedSession, + } satisfies InternalReplyResolverOptions), + onObservedReplyDelivery: markObservedReplyDelivery, + suppressToolErrorWarnings, + shouldSuppressToolErrorWarnings, + typingPolicy: typing.typingPolicy, + suppressTyping: typing.suppressTyping, + onPartialReply: wrapProgressCallback(params.replyOptions?.onPartialReply), + onReasoningStream: wrapProgressCallback(params.replyOptions?.onReasoningStream), + streamReasoningInNonStreamModes: + params.replyOptions?.streamReasoningInNonStreamModes, + onReasoningEnd: wrapProgressCallback(params.replyOptions?.onReasoningEnd), + onAssistantMessageStart: wrapProgressCallback( + params.replyOptions?.onAssistantMessageStart, + ), + onBlockReplyQueued: wrapProgressCallback(params.replyOptions?.onBlockReplyQueued), + onToolStart: wrapProgressCallback(params.replyOptions?.onToolStart, { + allowWhenToolSummariesHidden: + params.replyOptions?.allowToolLifecycleWhenProgressHidden === true, + forwardWhenSourceDeliverySuppressed: true, + requiresToolSummaryVisibility: true, + waitForDirectBlockReplyDelivery: true, + onForward: async () => { + // Commentary precedes the tool that follows it. + await flushPendingCommentaryProgress(); + }, + }), + onItemEvent, + commentaryProgressEnabled: + deliverStandaloneCommentaryProgress || + canForwardSuppressedSourceItemEvents || + params.replyOptions?.commentaryProgressEnabled, + reasoningPayloadsEnabled, + commentaryPayloadsEnabled, + onCommandOutput: wrapProgressCallback(params.replyOptions?.onCommandOutput, { + forwardWhenSourceDeliverySuppressed: true, + requiresToolSummaryVisibility: true, + waitForDirectBlockReplyDelivery: true, + onForward: (payload) => { + if (hasFailedProgressStatus(payload)) { + markVisibleToolErrorProgress(); } - : context; - if (!suppressAutomaticSourceDelivery) { - await params.replyOptions?.onBlockReplyQueued?.(visiblePayload, queuedContext); - } - if (isDispatchOperationAborted()) { - return; - } - const ttsPayload = - payload.isReasoning === true || payload.isCommentary === true - ? visiblePayload - : await maybeApplyTtsToReplyPayload({ - payload: visiblePayload, + }, + }), + onCompactionStart: wrapProgressCallback(params.replyOptions?.onCompactionStart, { + allowWhenToolSummariesHidden: + params.replyOptions?.allowToolLifecycleWhenProgressHidden === true, + forwardWhenSourceDeliverySuppressed: true, + requiresToolSummaryVisibility: true, + waitForDirectBlockReplyDelivery: true, + }), + onCompactionEnd: wrapProgressCallback(params.replyOptions?.onCompactionEnd, { + allowWhenToolSummariesHidden: + params.replyOptions?.allowToolLifecycleWhenProgressHidden === true, + forwardWhenSourceDeliverySuppressed: true, + requiresToolSummaryVisibility: true, + waitForDirectBlockReplyDelivery: true, + }), + onToolResult: (payload: ReplyPayload) => { + markProgress(); + const run = async () => { + if (isDispatchOperationAborted()) { + return; + } + await waitForPendingDirectBlockReplyDelivery( + dispatchAbortOperation?.abortSignal, + ); + if (isDispatchOperationAborted()) { + return; + } + markInboundDedupeReplayUnsafe(); + // Buffered commentary preceded this tool; land it before the summary. + await flushPendingCommentaryProgress(); + // When the operator opts into messages.suppressToolErrors, never + // surface tool-error tool-result payloads as channel progress, + // regardless of source delivery mode. payloads.ts already drops + // the warning text; this drops the visible progress delivery too. + if ( + payload.isError === true && + replyConfig.messages?.suppressToolErrors === true + ) { + return; + } + const isFastModeAutoProgress = isFastModeAutoProgressPayload(payload); + const isFastModeAutoProgressDelivery = + isFastModeAutoProgress && + shouldDeliverFastModeAutoProgressDespiteSourceSuppression(); + const isForcedToolProgress = + shouldDeliverForcedToolProgressDespiteSourceSuppression(); + const progressCallbackForwarded = shouldForwardToolResultProgressCallback( + payload, + isFastModeAutoProgress, + ); + if (progressCallbackForwarded) { + await onToolResultFromReplyOptions?.(payload); + } + if (isDispatchOperationAborted()) { + return; + } + if ( + isFastModeAutoProgress && + progressCallbackForwarded && + onToolResultFromReplyOptions + ) { + return; + } + if (sendPolicyDenied) { + return; + } + if ( + shouldSuppressProgressDelivery() && + !isFastModeAutoProgressDelivery && + !isForcedToolProgress + ) { + return; + } + const visibleToolPayload = isForcedToolProgress + ? payload + : resolveToolDeliveryPayload(payload); + if (!visibleToolPayload) { + return; + } + const ttsPayload = await maybeApplyTtsToReplyPayload({ + payload: visibleToolPayload, cfg, channel: deliveryChannel, - kind: "block", + kind: "tool", inboundAudio, ttsAuto: sessionTtsAuto, agentId: sessionAgentId, accountId: replyRoute.accountId, }); - const normalizedPayload = await normalizeReplyMediaPayload(ttsPayload); - if (isDispatchOperationAborted()) { - return; - } - if (shouldRouteToOriginating) { - await sendPayloadAsync(normalizedPayload, context?.abortSignal, false, "block"); - } else { - markInboundDedupeReplayUnsafe(); - const delivered = dispatcher.sendBlockReply(normalizedPayload); - if (delivered) { - hasPendingDirectBlockReplyDelivery = true; - } - } - }; - return run(); - }, - }, - replyConfig, + const normalizedPayload = await normalizeReplyMediaPayload(ttsPayload); + const deliveryPayload = isForcedToolProgress + ? normalizedPayload + : resolveToolDeliveryPayload(normalizedPayload); + if (!deliveryPayload) { + return; + } + if (isDispatchOperationAborted()) { + return; + } + if ( + shouldSuppressLateTextOnlyToolProgress(deliveryPayload) && + !isFastModeAutoProgressPayload(deliveryPayload) && + !isForcedToolProgress + ) { + return; + } + if (shouldSuppressMessageToolOnlyTextErrorProgress(deliveryPayload)) { + return; + } + if ( + shouldSuppressDefaultToolProgressMessages() && + !isFastModeAutoProgressPayload(deliveryPayload) && + !isForcedToolProgress + ) { + const hasMedia = + resolveSendableOutboundReplyParts(deliveryPayload).hasMedia; + if (!hasMedia && !hasExecApprovalPayload(deliveryPayload)) { + return; + } + } + if (deliveryPayload.isError === true) { + markVisibleToolErrorProgress(); + } + if (shouldRouteToOriginating) { + await sendPayloadAsync(deliveryPayload, undefined, false); + } else { + markInboundDedupeReplayUnsafe(); + dispatcher.sendToolResult(deliveryPayload); + } + }; + return run(); + }, + onPlanUpdate: async (payload) => { + if (isDispatchOperationAborted()) { + return; + } + markProgress(); + await waitForPendingDirectBlockReplyDelivery( + dispatchAbortOperation?.abortSignal, + ); + if (isDispatchOperationAborted()) { + return; + } + markInboundDedupeReplayUnsafe(); + if ( + shouldForwardProgressCallback({ + forwardWhenSourceDeliverySuppressed: true, + requiresToolSummaryVisibility: true, + }) + ) { + await onPlanUpdateFromReplyOptions?.(payload); + } + if (isDispatchOperationAborted()) { + return; + } + if (payload.phase !== "update" || shouldSuppressDefaultToolProgressMessages()) { + return; + } + await sendPlanUpdate({ + explanation: payload.explanation, + steps: payload.steps, + }); + }, + onApprovalEvent: async (payload) => { + if (isDispatchOperationAborted()) { + return; + } + markProgress(); + await waitForPendingDirectBlockReplyDelivery( + dispatchAbortOperation?.abortSignal, + ); + if (isDispatchOperationAborted()) { + return; + } + markInboundDedupeReplayUnsafe(); + if ( + shouldForwardProgressCallback({ + forwardWhenSourceDeliverySuppressed: true, + requiresToolSummaryVisibility: true, + }) + ) { + await onApprovalEventFromReplyOptions?.(payload); + } + if (isDispatchOperationAborted()) { + return; + } + if ( + payload.phase !== "requested" || + shouldSuppressDefaultToolProgressMessages() + ) { + return; + } + const label = summarizeApprovalLabel({ + status: payload.status, + command: payload.command, + message: payload.message, + }); + if (!label) { + return; + } + await maybeSendWorkingStatus(label); + }, + onPatchSummary: async (payload) => { + if (isDispatchOperationAborted()) { + return; + } + markProgress(); + await waitForPendingDirectBlockReplyDelivery( + dispatchAbortOperation?.abortSignal, + ); + if (isDispatchOperationAborted()) { + return; + } + markInboundDedupeReplayUnsafe(); + if ( + shouldForwardProgressCallback({ + forwardWhenSourceDeliverySuppressed: true, + requiresToolSummaryVisibility: true, + }) + ) { + await onPatchSummaryFromReplyOptions?.(payload); + } + if (isDispatchOperationAborted()) { + return; + } + if (payload.phase !== "end" || shouldSuppressDefaultToolProgressMessages()) { + return; + } + const label = summarizePatchLabel({ + summary: payload.summary, + title: payload.title, + }); + if (!label) { + return; + } + await maybeSendWorkingStatus(label); + }, + onBlockReply: (payload: ReplyPayload, context?: BlockReplyContext) => { + markProgress(); + const run = async () => { + if (isDispatchOperationAborted()) { + return; + } + if ( + payload.isReasoning !== true && + payload.isCommentary !== true && + hasOutboundReplyContent(payload, { trimText: true }) + ) { + markInboundDedupeReplayUnsafe(); + } + // Buffered commentary preceded this block; deliver it first. + await flushPendingCommentaryProgress(); + if (suppressDelivery) { + return; + } + // Durable reasoning is a channel-owned lane; generic channels + // keep the historical suppression unless they explicitly opt in. + if (payload.isReasoning === true && !reasoningPayloadsEnabled) { + return; + } + // Durable commentary is a channel-owned lane; generic channels keep the + // historical suppression unless they explicitly opt in. + if (payload.isCommentary === true && !commentaryPayloadsEnabled) { + return; + } + // Accumulate block text for TTS generation after streaming. + // Exclude status notices — they are informational UI signals + // and must not be synthesised into the spoken reply. Display + // lanes stay out too: they are presentation, never final text. + const isStatusNotice = isReplyPayloadStatusNotice(payload); + if ( + payload.text && + !isStatusNotice && + payload.isReasoning !== true && + payload.isCommentary !== true + ) { + const joinsBufferedTtsDirective = + cleanBlockTtsDirectiveText?.hasBufferedDirectiveText() === true; + if (accumulatedBlockText.length > 0) { + accumulatedBlockText += "\n"; + } + accumulatedBlockText += payload.text; + if (accumulatedBlockTtsText.length > 0 && !joinsBufferedTtsDirective) { + accumulatedBlockTtsText += "\n"; + } + accumulatedBlockTtsText += payload.text; + blockCount++; + } + const visiblePayload = + payload.text && + cleanBlockTtsDirectiveText && + !isStatusNotice && + payload.isReasoning !== true && + payload.isCommentary !== true + ? (() => { + const text = cleanBlockTtsDirectiveText.push(payload.text); + return copyReplyPayloadMetadata(payload, { + ...payload, + text: text.trim() ? text : undefined, + }); + })() + : payload; + if (!hasOutboundReplyContent(visiblePayload, { trimText: true })) { + return; + } + // Channels that keep a live draft preview may need to rotate their + // preview state at the logical block boundary before queued block + // delivery drains asynchronously through the dispatcher. + const payloadMetadata = getReplyPayloadMetadata(payload); + const queuedContext = + payloadMetadata?.assistantMessageIndex !== undefined + ? { + ...context, + assistantMessageIndex: payloadMetadata.assistantMessageIndex, + } + : context; + if (!suppressAutomaticSourceDelivery) { + await params.replyOptions?.onBlockReplyQueued?.( + visiblePayload, + queuedContext, + ); + } + if (isDispatchOperationAborted()) { + return; + } + const ttsPayload = + payload.isReasoning === true || payload.isCommentary === true + ? visiblePayload + : await maybeApplyTtsToReplyPayload({ + payload: visiblePayload, + cfg, + channel: deliveryChannel, + kind: "block", + inboundAudio, + ttsAuto: sessionTtsAuto, + agentId: sessionAgentId, + accountId: replyRoute.accountId, + }); + const normalizedPayload = await normalizeReplyMediaPayload(ttsPayload); + if (isDispatchOperationAborted()) { + return; + } + if (shouldRouteToOriginating) { + await sendPayloadAsync( + normalizedPayload, + context?.abortSignal, + false, + "block", + ); + } else { + markInboundDedupeReplayUnsafe(); + const delivered = dispatcher.sendBlockReply(normalizedPayload); + if (delivered) { + hasPendingDirectBlockReplyDelivery = true; + } + } + }; + return run(); + }, + }, + replyConfig, + ), + ), + trackDispatchLifecycleWork, ), - ), ); const sessionMetadataChanges = takeCommandSessionMetadataChanges(ctx); notifySessionMetadataChanges(sessionMetadataChanges); - if ((await ensureDispatchReplyOperation("dispatch")).status === "busy") { + const finalDispatchAcquisition = await ensureDispatchReplyOperation("dispatch"); + if (finalDispatchAcquisition.status === "aborted") { + return finishReplyOperationAbortedDispatch(); + } + if (finalDispatchAcquisition.status === "busy") { return finishReplyOperationBusyDispatch({ recordAgentDispatchCompleted: true, ...(sessionMetadataChangesForResult @@ -3476,39 +3695,45 @@ export async function dispatchReplyFromConfig( // Route that tail through ACP now (same turn) instead of embedded dispatch. ctx.AcpDispatchTailAfterReset = false; if (hookRunner?.hasHooks("reply_dispatch")) { - const tailDispatchResult = await runWithDispatchAbortSignal(getDispatchAbortSignal(), () => - hookRunner.runReplyDispatch( - createReplyDispatchEvent({ - ctx, - runId: params.replyOptions?.runId, - sessionKey: acpDispatchSessionKey, - toolsAllow: params.replyOptions?.toolsAllow, - images: params.replyOptions?.images, - inboundAudio, - sessionTtsAuto, - ttsChannel: deliveryChannel, - suppressUserDelivery: suppressHookUserDelivery, - suppressReplyLifecycle: suppressHookReplyLifecycle, - sourceReplyDeliveryMode, - shouldRouteToOriginating, - originatingChannel: routeReplyChannel, - originatingTo: routeReplyTo, - originatingAccountId: replyContextAccountId, - originatingThreadId: routeReplyThreadId, - originatingChatType: replyRoute.chatType, - shouldSendToolSummaries, - sendPolicy, - isTailDispatch: true, - }), - { - cfg, - dispatcher: dispatchHookDispatcher, - abortSignal: getPreDispatchAbortSignal() ?? params.replyOptions?.abortSignal, - onReplyStart: params.replyOptions?.onReplyStart, - recordProcessed, - markIdle, - }, - ), + const tailDispatchResult = await runWithDispatchLifecycleAdmission( + async () => + await runWithDispatchAbortSignal( + getDispatchAbortSignal(), + () => + hookRunner.runReplyDispatch( + createReplyDispatchEvent({ + ctx, + runId: params.replyOptions?.runId, + sessionKey: acpDispatchSessionKey, + toolsAllow: params.replyOptions?.toolsAllow, + images: params.replyOptions?.images, + inboundAudio, + sessionTtsAuto, + ttsChannel: deliveryChannel, + suppressUserDelivery: suppressHookUserDelivery, + suppressReplyLifecycle: suppressHookReplyLifecycle, + sourceReplyDeliveryMode, + shouldRouteToOriginating, + originatingChannel: routeReplyChannel, + originatingTo: routeReplyTo, + originatingAccountId: replyContextAccountId, + originatingThreadId: routeReplyThreadId, + originatingChatType: replyRoute.chatType, + shouldSendToolSummaries, + sendPolicy, + isTailDispatch: true, + }), + { + cfg, + dispatcher: dispatchHookDispatcher, + abortSignal: getPreDispatchAbortSignal() ?? params.replyOptions?.abortSignal, + onReplyStart: params.replyOptions?.onReplyStart, + recordProcessed, + markIdle, + }, + ), + trackDispatchLifecycleWork, + ), ); if (tailDispatchResult?.handled) { recordAgentDispatchCompleted("completed"); diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts index 015d1568e82a..b926e45e9ec1 100644 --- a/src/auto-reply/reply/followup-runner.test.ts +++ b/src/auto-reply/reply/followup-runner.test.ts @@ -1,4 +1,5 @@ // Tests follow-up runner delivery, transcript persistence, and no-reply contracts. +import fsSync from "node:fs"; import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -53,6 +54,7 @@ const FOLLOWUP_TEST_QUEUES = new Map< } >(); const FOLLOWUP_TEST_SESSION_STORES = new Map>(); +const FOLLOWUP_TEST_SESSION_STORE_PATHS = new Set(); function debugFollowupTest(message: string): void { if (!FOLLOWUP_DEBUG) { @@ -143,7 +145,10 @@ function registerFollowupTestSessionStore( storePath: string, sessionStore: Record, ): void { + fsSync.mkdirSync(path.dirname(storePath), { recursive: true }); + fsSync.writeFileSync(storePath, JSON.stringify(sessionStore)); FOLLOWUP_TEST_SESSION_STORES.set(storePath, sessionStore); + FOLLOWUP_TEST_SESSION_STORE_PATHS.add(storePath); } async function incrementRunCompactionCountForFollowupTest( @@ -381,8 +386,8 @@ async function loadFreshFollowupRunnerModuleForTest() { completeFollowupRunLifecycle: (run: Pick) => run.queuedLifecycle?.onComplete?.(), enqueueFollowupRun: enqueueFollowupRunForFollowupTest, - isFollowupRunAborted: (run: Pick) => - run.abortSignal?.aborted === true, + isFollowupRunAborted: (run: Pick) => + run.abortSignal?.aborted === true || run.queueAbortSignal?.aborted === true, refreshQueuedFollowupSession: refreshQueuedFollowupSessionForFollowupTest, })); vi.doMock("./session-run-accounting.js", () => ({ @@ -579,6 +584,10 @@ afterEach(() => { clearFollowupQueue("main"); FOLLOWUP_TEST_QUEUES.clear(); FOLLOWUP_TEST_SESSION_STORES.clear(); + for (const storePath of FOLLOWUP_TEST_SESSION_STORE_PATHS) { + fsSync.rmSync(storePath, { force: true }); + } + FOLLOWUP_TEST_SESSION_STORE_PATHS.clear(); vi.clearAllTimers(); vi.useRealTimers(); clearSessionStoreCacheForTest(); @@ -4115,6 +4124,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { const sessionKey = "main"; const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() }; const sessionStore: Record = { [sessionKey]: sessionEntry }; + registerFollowupTestSessionStore(storePath, sessionStore); const persistSpy = vi.spyOn(sessionRunAccounting, "persistRunSessionUsage"); persistSpy.mockImplementationOnce(async (params) => { const nextEntry: SessionEntry = { @@ -4172,6 +4182,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { const sessionKey = "main"; const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() }; const sessionStore: Record = { [sessionKey]: sessionEntry }; + registerFollowupTestSessionStore(storePath, sessionStore); const cfg = { messages: { @@ -4354,6 +4365,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { const sessionKey = "main"; const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() }; const sessionStore: Record = { [sessionKey]: sessionEntry }; + registerFollowupTestSessionStore(storePath, sessionStore); const persistSpy = vi.spyOn(sessionRunAccounting, "persistRunSessionUsage"); runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], @@ -4419,7 +4431,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { totalTokensFresh: true, }; const sessionStore: Record = { [sessionKey]: sessionEntry }; - FOLLOWUP_TEST_SESSION_STORES.set(storePath, sessionStore); + registerFollowupTestSessionStore(storePath, sessionStore); const persistSpy = vi.spyOn(sessionRunAccounting, "persistRunSessionUsage"); runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "internal announce complete" }], diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index 1559a803b23b..3ea50b049340 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -99,6 +99,15 @@ import type { TypingController } from "./typing.js"; type EmbeddedAgentRunResult = Awaited>; +function resolveFollowupAbortSignal( + run: Pick, +): AbortSignal | undefined { + const signals = [run.abortSignal, run.queueAbortSignal].filter( + (signal): signal is AbortSignal => signal !== undefined, + ); + return signals.length > 1 ? AbortSignal.any(signals) : signals[0]; +} + type FollowupAgentEvent = { stream: string; data: Record }; function readApprovalScopeValue(value: unknown): "turn" | "session" | undefined { @@ -602,10 +611,12 @@ export function createFollowupRunner(params: { const admission = await admitReplyTurn({ sessionId: effectiveQueued.admissionSessionId ?? run.sessionId, sessionKey: replySessionKey ?? "", + expectedSessionId: activeSessionEntry?.sessionId, + storePath, kind: "queued_followup", resetTriggered: false, routeThreadId: queued.originatingThreadId, - upstreamAbortSignal: queued.abortSignal, + upstreamAbortSignal: resolveFollowupAbortSignal(queued), }); if (admission.status === "skipped") { if (admission.reason === "active-run") { diff --git a/src/auto-reply/reply/get-reply-directives-apply.test.ts b/src/auto-reply/reply/get-reply-directives-apply.test.ts index 0ecc553ae296..38741bc36818 100644 --- a/src/auto-reply/reply/get-reply-directives-apply.test.ts +++ b/src/auto-reply/reply/get-reply-directives-apply.test.ts @@ -1,6 +1,30 @@ // Tests applying parsed directives to get-reply execution options. -import { describe, expect, it } from "vitest"; -import { formatModelOverrideResetEvent } from "./get-reply-directives-apply.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { parseInlineDirectives } from "./directive-handling.parse.js"; +import { + applyInlineDirectiveOverrides, + formatModelOverrideResetEvent, +} from "./get-reply-directives-apply.js"; +import { createFastTestModelSelectionState } from "./model-selection.js"; +import { buildTestCtx } from "./test-ctx.js"; + +const mocks = vi.hoisted(() => ({ + fastLane: vi.fn(), + persist: vi.fn(), +})); + +vi.mock("./directive-handling.fast-lane.js", () => ({ + applyInlineDirectivesFastLane: (...args: unknown[]) => mocks.fastLane(...args), +})); + +vi.mock("./directive-handling.persist.runtime.js", () => ({ + persistInlineDirectives: (...args: unknown[]) => mocks.persist(...args), +})); + +beforeEach(() => { + mocks.fastLane.mockReset(); + mocks.persist.mockReset(); +}); describe("formatModelOverrideResetEvent", () => { it("names the rejected model override and allowlist recovery path", () => { @@ -34,3 +58,83 @@ describe("formatModelOverrideResetEvent", () => { ); }); }); + +describe("applyInlineDirectiveOverrides", () => { + it("stops a mixed inline turn when final directive persistence loses", async () => { + const directives = parseInlineDirectives("hello /elevated full"); + mocks.fastLane.mockResolvedValue({ + directiveAck: { text: "Elevated FULL enabled." }, + provider: "openai", + model: "gpt-5.5", + sessionChangesApplied: true, + }); + mocks.persist.mockResolvedValue({ + provider: "openai", + model: "gpt-5.5", + contextTokens: 8192, + sessionChangesApplied: false, + }); + const typing = { + onReplyStart: async () => {}, + startTypingLoop: async () => {}, + startTypingOnText: async () => {}, + refreshTypingTtl: () => {}, + isActive: () => false, + markRunComplete: () => {}, + markDispatchIdle: () => {}, + cleanup: vi.fn(), + }; + const sessionEntry = { sessionId: "session-1", updatedAt: 1 }; + + const result = await applyInlineDirectiveOverrides({ + ctx: buildTestCtx({ Body: "hello /elevated full", CommandAuthorized: true }), + cfg: {}, + agentId: "main", + agentDir: "/tmp/agent", + workspaceDir: "/tmp/workspace", + agentCfg: {}, + sessionEntry, + sessionStore: { "agent:main:main": sessionEntry }, + sessionKey: "agent:main:main", + sessionScope: undefined, + isGroup: false, + allowTextCommands: true, + command: { + surface: "webchat", + channel: "webchat", + ownerList: [], + senderIsOwner: true, + isAuthorizedSender: true, + rawBodyNormalized: "hello /elevated full", + commandBodyNormalized: "hello /elevated full", + }, + directives, + messageProviderKey: "webchat", + elevatedEnabled: true, + elevatedAllowed: true, + elevatedFailures: [], + defaultProvider: "openai", + defaultModel: "gpt-5.5", + aliasIndex: { byAlias: new Map(), byKey: new Map() }, + provider: "openai", + model: "gpt-5.5", + modelState: createFastTestModelSelectionState({ + agentCfg: {}, + provider: "openai", + model: "gpt-5.5", + }), + initialModelLabel: "openai/gpt-5.5", + formatModelSwitchEvent: (label) => label, + resolvedElevatedLevel: "full", + defaultActivation: () => "always", + contextTokens: 8192, + typing, + }); + + expect(result).toEqual({ + kind: "reply", + reply: { text: "Session settings were not applied because the session changed. Retry." }, + }); + expect(typing.cleanup).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/auto-reply/reply/get-reply-directives-apply.ts b/src/auto-reply/reply/get-reply-directives-apply.ts index 239cd0665b3c..c8a937476b24 100644 --- a/src/auto-reply/reply/get-reply-directives-apply.ts +++ b/src/auto-reply/reply/get-reply-directives-apply.ts @@ -306,6 +306,13 @@ export async function applyInlineDirectiveOverrides(params: { model, markLiveSwitchPending: true, }); + if (!persisted.sessionChangesApplied) { + typing.cleanup(); + return { + kind: "reply", + reply: { text: "Model change was not applied because the session changed. Retry." }, + }; + } const label = `${modelSelection.provider}/${modelSelection.model}`; const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label; const parts = [ @@ -431,6 +438,17 @@ export async function applyInlineDirectiveOverrides(params: { directiveAck = fastLane.directiveAck; provider = fastLane.provider; model = fastLane.model; + if (!fastLane.sessionChangesApplied) { + typing.cleanup(); + return { + kind: "reply", + reply: + directiveAck ?? + ({ + text: "Session settings were not applied because the session changed. Retry.", + } satisfies ReplyPayload), + }; + } } const persisted = await ( @@ -443,6 +461,15 @@ export async function applyInlineDirectiveOverrides(params: { provider = persisted.provider; model = persisted.model; contextTokens = persisted.contextTokens; + if (!persisted.sessionChangesApplied) { + typing.cleanup(); + return { + kind: "reply", + reply: { + text: "Session settings were not applied because the session changed. Retry.", + }, + }; + } const perMessageQueueMode = directives.hasQueueDirective && !directives.queueReset ? directives.queueMode : undefined; diff --git a/src/auto-reply/reply/get-reply-directives.target-session.test.ts b/src/auto-reply/reply/get-reply-directives.target-session.test.ts index 7a0cb6e6ee26..b31d9dfc0081 100644 --- a/src/auto-reply/reply/get-reply-directives.target-session.test.ts +++ b/src/auto-reply/reply/get-reply-directives.target-session.test.ts @@ -1,6 +1,7 @@ /** Tests directive handling for target-session command turns. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions.js"; +import { SessionWorkStartInvalidatedError } from "../../config/sessions/lifecycle.js"; import type { TemplateContext } from "../templating.js"; import { resolveReplyDirectives } from "./get-reply-directives.js"; import { buildTestCtx } from "./test-ctx.js"; @@ -156,20 +157,26 @@ async function resolveHelloWithModelDefaults(params: { model?: string; ctx?: Parameters[0]; opts?: Parameters[0]["opts"]; + modelError?: unknown; }) { const resolveDefaultThinkingLevel = vi.fn(async () => params.defaultThinking); const resolveDefaultReasoningLevel = vi.fn(async () => params.defaultReasoning); mocks.listAgentEntries.mockReturnValue(params.agentEntries ?? []); - mocks.createModelSelectionState.mockResolvedValueOnce({ - provider: params.selectedProvider ?? "openai", - model: params.selectedModel ?? "gpt-4o-mini", - allowedModelKeys: new Set(), - allowedModelCatalog: [], - resetModelOverride: false, - resolveDefaultThinkingLevel, - hasConfiguredThinkingDefault: params.hasConfiguredThinkingDefault, - resolveDefaultReasoningLevel, - }); + if (params.modelError) { + mocks.createModelSelectionState.mockRejectedValueOnce(params.modelError); + } else { + mocks.createModelSelectionState.mockResolvedValueOnce({ + provider: params.selectedProvider ?? "openai", + model: params.selectedModel ?? "gpt-4o-mini", + allowedModelKeys: new Set(), + allowedModelCatalog: [], + resetModelOverride: false, + resolveDefaultThinkingLevel, + hasConfiguredThinkingDefault: params.hasConfiguredThinkingDefault, + resolveDefaultReasoningLevel, + }); + } + const typing = makeTypingController(); const result = await resolveReplyDirectives({ ctx: buildTestCtx({ @@ -206,12 +213,12 @@ async function resolveHelloWithModelDefaults(params: { model: params.model ?? "gpt-4o-mini", hasOneTurnModelOverride: params.hasOneTurnModelOverride, hasResolvedHeartbeatModelOverride: false, - typing: makeTypingController(), + typing, opts: params.opts, skillFilter: undefined, }); - return { result, resolveDefaultReasoningLevel }; + return { result, resolveDefaultReasoningLevel, typing }; } vi.mock("../../agents/agent-scope.js", () => ({ @@ -347,6 +354,21 @@ describe("resolveReplyDirectives", () => { expect(modelSelectionInput.hasOneTurnModelOverride).toBe(true); }); + it("returns a terminal retry when model preparation sees a rotated session", async () => { + const error = new SessionWorkStartInvalidatedError( + 'Session "agent:main:whatsapp:+2000" changed while starting work. Retry.', + ); + const { result, typing } = await resolveHelloWithModelDefaults({ + defaultThinking: "off", + defaultReasoning: "on", + modelError: error, + }); + + expect(result).toEqual({ kind: "reply", reply: { text: error.message } }); + expect(typing.cleanup).toHaveBeenCalledOnce(); + expect(mocks.applyInlineDirectiveOverrides).not.toHaveBeenCalled(); + }); + it("keeps one-turn fast mode with the resolved fast mode", async () => { const { result } = await resolveHelloWithModelDefaults({ defaultThinking: "off", diff --git a/src/auto-reply/reply/get-reply-directives.ts b/src/auto-reply/reply/get-reply-directives.ts index 7ebaad6df294..45b1e229af7c 100644 --- a/src/auto-reply/reply/get-reply-directives.ts +++ b/src/auto-reply/reply/get-reply-directives.ts @@ -9,6 +9,7 @@ import { resolveFastModeState } from "../../agents/fast-mode.js"; import { type ModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox/runtime-status.js"; import type { SessionEntry } from "../../config/sessions.js"; +import { isSessionWorkStartInvalidatedError } from "../../config/sessions/lifecycle.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { normalizeAgentId } from "../../routing/session-key.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; @@ -516,34 +517,45 @@ export async function resolveReplyDirectives(params: { aliasIndex: params.aliasIndex, })); - const modelState = useFastModelSelection - ? createFastTestModelSelectionState({ - agentCfg, - provider, - model, - }) - : await createModelSelectionState({ - cfg, - agentId, - agentCfg, - sessionEntry: targetSessionEntry, - sessionStore, - sessionKey, - parentSessionKey: - targetSessionEntry?.parentSessionKey ?? ctx.ModelParentSessionKey ?? ctx.ParentSessionKey, - storePath, - defaultProvider, - defaultModel, - primaryProvider, - primaryModel, - provider, - model, - hasModelDirective: directives.hasModelDirective, - hasOneTurnModelOverride, - skipStoredModelOverride, - hasResolvedHeartbeatModelOverride, - isHeartbeat: opts?.isHeartbeat === true, - }); + let modelState: Awaited>; + try { + modelState = useFastModelSelection + ? createFastTestModelSelectionState({ + agentCfg, + provider, + model, + }) + : await createModelSelectionState({ + cfg, + agentId, + agentCfg, + sessionEntry: targetSessionEntry, + sessionStore, + sessionKey, + parentSessionKey: + targetSessionEntry?.parentSessionKey ?? + ctx.ModelParentSessionKey ?? + ctx.ParentSessionKey, + storePath, + defaultProvider, + defaultModel, + primaryProvider, + primaryModel, + provider, + model, + hasModelDirective: directives.hasModelDirective, + hasOneTurnModelOverride, + skipStoredModelOverride, + hasResolvedHeartbeatModelOverride, + isHeartbeat: opts?.isHeartbeat === true, + }); + } catch (error) { + if (!isSessionWorkStartInvalidatedError(error)) { + throw error; + } + typing.cleanup(); + return { kind: "reply", reply: { text: error.message } }; + } provider = modelState.provider; model = modelState.model; const resolvedThinkLevelWithDefault = diff --git a/src/auto-reply/reply/get-reply-fast-path.ts b/src/auto-reply/reply/get-reply-fast-path.ts index a42ed1b0dbf1..b573a4a017fa 100644 --- a/src/auto-reply/reply/get-reply-fast-path.ts +++ b/src/auto-reply/reply/get-reply-fast-path.ts @@ -9,7 +9,7 @@ import { normalizeAnyChannelId } from "../../channels/registry.js"; import { applyMergePatch } from "../../config/merge-patch.js"; import { resolveSessionTranscriptPath, resolveStorePath } from "../../config/sessions/paths.js"; import { resolveSessionKey } from "../../config/sessions/session-key.js"; -import { loadSessionStore } from "../../config/sessions/store.js"; +import { loadSessionStore, resolveSessionStoreEntry } from "../../config/sessions/store.js"; import type { SessionEntry, SessionScope } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { resolveCommandTurnTargetSessionKey } from "../command-turn-context.js"; @@ -221,7 +221,10 @@ export function initFastReplySessionState(params: { skipCache: true, clone: false, }); - const existingEntry = sessionStore[sessionKey]; + const existingEntry = resolveSessionStoreEntry({ + store: sessionStore, + sessionKey, + }).existing; const commandSource = ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? ""; const triggerBodyNormalized = isFormattedGoalContinuationPrompt(commandSource) ? commandSource.trim() @@ -300,6 +303,7 @@ export function initFastReplySessionState(params: { return { sessionCtx, sessionEntry, + initialSessionEntry: existingEntry ? { ...existingEntry } : undefined, sessionEntryHandle, sessionStore, sessionKey, diff --git a/src/auto-reply/reply/get-reply-native-slash-fast-path.test.ts b/src/auto-reply/reply/get-reply-native-slash-fast-path.test.ts index 0dcb193a19b1..f4d17c354dac 100644 --- a/src/auto-reply/reply/get-reply-native-slash-fast-path.test.ts +++ b/src/auto-reply/reply/get-reply-native-slash-fast-path.test.ts @@ -1,7 +1,11 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; import type { OpenClawConfig } from "../../config/config.js"; +import { loadSessionStore, saveSessionStore } from "../../config/sessions/store.js"; import { getReplyPayloadMetadata } from "../reply-payload.js"; import { markCompleteReplyConfig } from "./get-reply-fast-path.js"; +import * as sessionPersistence from "./session-entry-persistence.js"; import { buildTestCtx } from "./test-ctx.js"; import type { TypingController } from "./typing.js"; @@ -16,6 +20,8 @@ vi.mock("./commands.runtime.js", () => ({ const { maybeResolveNativeSlashCommandFastReply } = await import("./get-reply-native-slash-fast-path.js"); +const tempDirs = useAutoCleanupTempDirTracker(afterEach); + const createTypingController = (): TypingController => ({ onReplyStart: async () => {}, startTypingLoop: async () => {}, @@ -58,7 +64,9 @@ describe("maybeResolveNativeSlashCommandFastReply", () => { const result = await maybeResolveNativeSlashCommandFastReply({ ctx, cfg: markCompleteReplyConfig({ - session: { store: "/tmp/openclaw-native-slash-sessions.json" }, + session: { + store: path.join(tempDirs.make("openclaw-native-slash-"), "sessions.json"), + }, } as OpenClawConfig), agentId: "main", agentDir: "/tmp/agent", @@ -120,7 +128,9 @@ describe("maybeResolveNativeSlashCommandFastReply", () => { const result = await maybeResolveNativeSlashCommandFastReply({ ctx, cfg: markCompleteReplyConfig({ - session: { store: "/tmp/openclaw-text-slash-sessions.json" }, + session: { + store: path.join(tempDirs.make("openclaw-text-slash-"), "sessions.json"), + }, } as OpenClawConfig), agentId: "dev", agentDir: "/tmp/agent", @@ -173,7 +183,9 @@ describe("maybeResolveNativeSlashCommandFastReply", () => { const result = await maybeResolveNativeSlashCommandFastReply({ ctx, cfg: markCompleteReplyConfig({ - session: { store: "/tmp/openclaw-external-text-slash-sessions.json" }, + session: { + store: path.join(tempDirs.make("openclaw-external-text-slash-"), "sessions.json"), + }, } as OpenClawConfig), agentId: "dev", agentDir: "/tmp/agent", @@ -192,4 +204,317 @@ describe("maybeResolveNativeSlashCommandFastReply", () => { expect(handleCommandsMock).not.toHaveBeenCalled(); expect(typing.cleanup).not.toHaveBeenCalled(); }); + + it("does not create a session for an unauthorized native command", async () => { + const storePath = path.join( + tempDirs.make("openclaw-native-slash-unauthorized-"), + "sessions.json", + ); + const sessionKey = "agent:main:telegram:slash:unauthorized"; + handleCommandsMock.mockResolvedValueOnce({ + shouldContinue: false, + reply: { text: "You are not authorized to use this command." }, + }); + + const result = await maybeResolveNativeSlashCommandFastReply({ + ctx: buildTestCtx({ + Body: "/config show", + CommandBody: "/config show", + CommandSource: "native", + CommandAuthorized: false, + Provider: "telegram", + CommandTargetSessionKey: sessionKey, + CommandTurn: { + kind: "native", + source: "native", + authorized: false, + commandName: "config", + body: "/config show", + }, + }), + cfg: markCompleteReplyConfig({ session: { store: storePath } } as OpenClawConfig), + agentId: "main", + agentDir: "/tmp/agent", + agentCfg: undefined, + commandAuthorized: false, + defaultProvider: "openai", + defaultModel: "gpt-5.5", + aliasIndex: { byKey: new Map(), byAlias: new Map() }, + provider: "openai", + model: "gpt-5.5", + workspaceDir: "/tmp/workspace", + typing: createTypingController(), + }); + + expect(result).toEqual({ + handled: true, + reply: expect.objectContaining({ text: "You are not authorized to use this command." }), + }); + expect(handleCommandsMock).toHaveBeenCalledOnce(); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toBeUndefined(); + }); + + it("marks deleted-session initialization conflicts for delivery", async () => { + vi.spyOn(sessionPersistence, "persistReplySessionEntry").mockResolvedValueOnce({ + status: "lifecycle-invalidated", + error: 'Session "agent:main:main" was deleted while starting work. Retry.', + }); + const result = await maybeResolveNativeSlashCommandFastReply({ + ctx: buildTestCtx({ + Body: "/compact", + CommandBody: "/compact", + CommandSource: "native", + CommandAuthorized: true, + CommandTargetSessionKey: "agent:main:main", + CommandTurn: { + kind: "native", + source: "native", + authorized: true, + commandName: "compact", + body: "/compact", + }, + }), + cfg: markCompleteReplyConfig({ + session: { + store: path.join(tempDirs.make("openclaw-native-slash-conflict-"), "sessions.json"), + }, + } as OpenClawConfig), + agentId: "main", + agentDir: "/tmp/agent", + agentCfg: undefined, + commandAuthorized: true, + defaultProvider: "openai", + defaultModel: "gpt-5.5", + aliasIndex: { byKey: new Map(), byAlias: new Map() }, + provider: "openai", + model: "gpt-5.5", + workspaceDir: "/tmp/workspace", + typing: createTypingController(), + }); + + expect(result.handled).toBe(true); + if (!result.handled || !result.reply || Array.isArray(result.reply)) { + throw new Error("expected single handled reply"); + } + expect(result.reply.text).toContain("was deleted"); + expect(getReplyPayloadMetadata(result.reply)?.deliverDespiteSourceReplySuppression).toBe(true); + expect(handleCommandsMock).not.toHaveBeenCalled(); + }); + + it("rejects initialization when the session rotates during persistence", async () => { + vi.spyOn(sessionPersistence, "persistReplySessionEntry").mockResolvedValueOnce({ + status: "lifecycle-invalidated", + error: 'Session "agent:main:main" changed while starting work. Retry.', + }); + const result = await maybeResolveNativeSlashCommandFastReply({ + ctx: buildTestCtx({ + Body: "/compact", + CommandBody: "/compact", + CommandSource: "native", + CommandAuthorized: true, + CommandTargetSessionKey: "agent:main:main", + CommandTurn: { + kind: "native", + source: "native", + authorized: true, + commandName: "compact", + body: "/compact", + }, + }), + cfg: markCompleteReplyConfig({ + session: { + store: path.join(tempDirs.make("openclaw-native-slash-rotation-"), "sessions.json"), + }, + } as OpenClawConfig), + agentId: "main", + agentDir: "/tmp/agent", + agentCfg: undefined, + commandAuthorized: true, + defaultProvider: "openai", + defaultModel: "gpt-5.5", + aliasIndex: { byKey: new Map(), byAlias: new Map() }, + provider: "openai", + model: "gpt-5.5", + workspaceDir: "/tmp/workspace", + typing: createTypingController(), + }); + + expect(result).toEqual({ + handled: true, + reply: expect.objectContaining({ text: expect.stringContaining("changed while") }), + }); + expect(handleCommandsMock).not.toHaveBeenCalled(); + }); + + it("adopts a supported legacy alias before native command initialization", async () => { + const storePath = path.join(tempDirs.make("openclaw-native-slash-alias-"), "sessions.json"); + const sessionKey = "agent:main:main"; + await saveSessionStore( + storePath, + { + "Agent:main:main": { + sessionId: "legacy-session", + updatedAt: 1, + }, + }, + { skipMaintenance: true }, + ); + handleCommandsMock.mockImplementationOnce(async (params: { sessionEntry?: unknown }) => { + expect(params.sessionEntry).toMatchObject({ sessionId: "legacy-session" }); + return { shouldContinue: false, reply: { text: "ok" } }; + }); + + const result = await maybeResolveNativeSlashCommandFastReply({ + ctx: buildTestCtx({ + Body: "/compact", + CommandBody: "/compact", + CommandSource: "native", + CommandAuthorized: true, + CommandTargetSessionKey: sessionKey, + CommandTurn: { + kind: "native", + source: "native", + authorized: true, + commandName: "compact", + body: "/compact", + }, + }), + cfg: markCompleteReplyConfig({ session: { store: storePath } } as OpenClawConfig), + agentId: "main", + agentDir: "/tmp/agent", + agentCfg: undefined, + commandAuthorized: true, + defaultProvider: "openai", + defaultModel: "gpt-5.5", + aliasIndex: { byKey: new Map(), byAlias: new Map() }, + provider: "openai", + model: "gpt-5.5", + workspaceDir: "/tmp/workspace", + typing: createTypingController(), + }); + + expect(result).toEqual({ + handled: true, + reply: expect.objectContaining({ text: "ok" }), + }); + expect(handleCommandsMock).toHaveBeenCalledOnce(); + }); + + it("does not mutate an archived session during native command initialization", async () => { + const storePath = path.join(tempDirs.make("openclaw-native-slash-archived-"), "sessions.json"); + const sessionKey = "agent:main:main"; + const archivedEntry = { + sessionId: "archived-session", + updatedAt: 1, + lastInteractionAt: 1, + archivedAt: 2, + channel: "telegram", + }; + await saveSessionStore(storePath, { [sessionKey]: archivedEntry }, { skipMaintenance: true }); + const persistedArchivedEntry = loadSessionStore(storePath, { skipCache: true })[sessionKey]; + + const result = await maybeResolveNativeSlashCommandFastReply({ + ctx: buildTestCtx({ + Body: "/compact", + CommandBody: "/compact", + CommandSource: "native", + CommandAuthorized: true, + Provider: "telegram", + CommandTargetSessionKey: sessionKey, + CommandTurn: { + kind: "native", + source: "native", + authorized: true, + commandName: "compact", + body: "/compact", + }, + }), + cfg: markCompleteReplyConfig({ session: { store: storePath } } as OpenClawConfig), + agentId: "main", + agentDir: "/tmp/agent", + agentCfg: undefined, + commandAuthorized: true, + defaultProvider: "openai", + defaultModel: "gpt-5.5", + aliasIndex: { byKey: new Map(), byAlias: new Map() }, + provider: "openai", + model: "gpt-5.5", + workspaceDir: "/tmp/workspace", + typing: createTypingController(), + }); + + expect(result).toEqual({ + handled: true, + reply: expect.objectContaining({ text: expect.stringContaining("is archived") }), + }); + expect(handleCommandsMock).not.toHaveBeenCalled(); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toEqual( + persistedArchivedEntry, + ); + }); + + it("persists fast-path session initialization before command mutation", async () => { + const storePath = path.join(tempDirs.make("openclaw-native-slash-init-"), "sessions.json"); + const sessionKey = "agent:main:main"; + await saveSessionStore( + storePath, + { + [sessionKey]: { + sessionId: "session-1", + updatedAt: 1, + lastInteractionAt: 1, + channel: "old-channel", + }, + }, + { skipMaintenance: true }, + ); + handleCommandsMock.mockImplementationOnce(async (params: { sessionEntry?: unknown }) => { + const persisted = loadSessionStore(storePath, { skipCache: true })[sessionKey]; + expect(params.sessionEntry).toMatchObject({ + sessionId: "session-1", + updatedAt: 100, + lastInteractionAt: 100, + channel: "telegram", + }); + expect(persisted).toMatchObject(params.sessionEntry as object); + return { shouldContinue: false, reply: { text: "ok" } }; + }); + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(100); + + try { + await maybeResolveNativeSlashCommandFastReply({ + ctx: buildTestCtx({ + Body: "/compact", + CommandBody: "/compact", + CommandSource: "native", + CommandAuthorized: true, + Provider: "telegram", + CommandTargetSessionKey: sessionKey, + CommandTurn: { + kind: "native", + source: "native", + authorized: true, + commandName: "compact", + body: "/compact", + }, + }), + cfg: markCompleteReplyConfig({ session: { store: storePath } } as OpenClawConfig), + agentId: "main", + agentDir: "/tmp/agent", + agentCfg: undefined, + commandAuthorized: true, + defaultProvider: "openai", + defaultModel: "gpt-5.5", + aliasIndex: { byKey: new Map(), byAlias: new Map() }, + provider: "openai", + model: "gpt-5.5", + workspaceDir: "/tmp/workspace", + typing: createTypingController(), + }); + } finally { + nowSpy.mockRestore(); + } + + expect(handleCommandsMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/auto-reply/reply/get-reply-native-slash-fast-path.ts b/src/auto-reply/reply/get-reply-native-slash-fast-path.ts index 411a09734fee..dd77ceeb0242 100644 --- a/src/auto-reply/reply/get-reply-native-slash-fast-path.ts +++ b/src/auto-reply/reply/get-reply-native-slash-fast-path.ts @@ -28,6 +28,7 @@ import { resolveReplyDirectives } from "./get-reply-directives.js"; import { initFastReplySessionState } from "./get-reply-fast-path.js"; import { handleInlineActions } from "./get-reply-inline-actions.js"; import { stripStructuralPrefixes } from "./mentions.js"; +import { persistReplySessionEntry } from "./session-entry-persistence.js"; import type { createTypingController } from "./typing.js"; type AgentDefaults = NonNullable["defaults"]> | undefined; @@ -137,6 +138,34 @@ export async function maybeResolveNativeSlashCommandFastReply(params: { commandAuthorized: params.commandAuthorized, workspaceDir: params.workspaceDir, }); + if (params.commandAuthorized) { + const creatingSession = sessionState.initialSessionEntry === undefined; + const initializationEntry = sessionState.initialSessionEntry ?? sessionState.sessionEntry; + const persistence = await persistReplySessionEntry({ + storePath: sessionState.storePath, + sessionKey: sessionState.sessionKey, + allowCreate: creatingSession, + initialEntry: initializationEntry, + entry: sessionState.sessionEntry, + skipMaintenance: !creatingSession, + }); + if (persistence.status === "lifecycle-invalidated") { + params.typing.cleanup(); + return { + handled: true, + reply: markCommandReplyForDelivery({ + text: persistence.error, + }), + }; + } + const persistedInitialEntry = persistence.entry; + // Commit the synthesized activity/channel touch before commands or directives + // capture their own mutation baseline. + sessionState.sessionEntry = persistedInitialEntry; + sessionState.sessionEntryHandle.replaceCurrent(persistedInitialEntry); + sessionState.sessionStore[sessionState.sessionKey] = persistedInitialEntry; + sessionState.sessionId = persistedInitialEntry.sessionId; + } const command = buildCommandContext({ ctx: params.ctx, cfg: params.cfg, diff --git a/src/auto-reply/reply/get-reply.auto-fallback.test.ts b/src/auto-reply/reply/get-reply.auto-fallback.test.ts index daf4aa1fde9c..045a7bcf13b9 100644 --- a/src/auto-reply/reply/get-reply.auto-fallback.test.ts +++ b/src/auto-reply/reply/get-reply.auto-fallback.test.ts @@ -1,5 +1,8 @@ // Tests get-reply behavior while probing an auto-fallback primary model. -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; import type { ModelDefinitionConfig, OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; import type { ThinkLevel } from "../thinking.js"; @@ -130,6 +133,8 @@ function makePerModelThinkingConfig( } satisfies OpenClawConfig); } +const tempDirs = useAutoCleanupTempDirTracker(afterEach); + function mockAutoFallbackSession() { const sessionKey = "agent:main:telegram:123"; const sessionEntry: SessionEntry = { @@ -141,11 +146,17 @@ function mockAutoFallbackSession() { modelOverrideFallbackOriginProvider: "openai", modelOverrideFallbackOriginModel: "gpt-5.5", }; + // Reply-turn admission re-reads the store from disk before starting work; + // seed a real per-test store so the guard sees the same session the mocks + // describe instead of depending on leftover host files. + const storePath = path.join(tempDirs.make("auto-fallback-store"), "sessions.json"); + fs.writeFileSync(storePath, JSON.stringify({ [sessionKey]: sessionEntry })); mocks.initSessionState.mockResolvedValue( createGetReplySessionState({ sessionKey, sessionEntry, sessionStore: { [sessionKey]: sessionEntry }, + storePath, triggerBodyNormalized: "hello", bodyStripped: "hello", }), diff --git a/src/auto-reply/reply/get-reply.ts b/src/auto-reply/reply/get-reply.ts index d769d4571556..f003480ce2dd 100644 --- a/src/auto-reply/reply/get-reply.ts +++ b/src/auto-reply/reply/get-reply.ts @@ -16,6 +16,7 @@ import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../../agents/workspace.js"; import { resolveChannelModelOverride } from "../../channels/model-overrides.js"; import { type OpenClawConfig, getRuntimeConfig } from "../../config/config.js"; +import { isSessionWorkStartInvalidatedError } from "../../config/sessions/lifecycle.js"; import { logVerbose } from "../../globals.js"; import { measureDiagnosticsTimelineSpan } from "../../infra/diagnostics-timeline.js"; import { formatErrorMessage } from "../../infra/errors.js"; @@ -484,6 +485,7 @@ export async function getReplyFromConfig( commandAuthorized, requestedSessionId: internalResolvedOpts?.requestedSessionId, resumeRequestedSession: internalResolvedOpts?.resumeRequestedSession, + signal: internalResolvedOpts?.abortSignal, }), ); const { @@ -559,22 +561,30 @@ export async function getReplyFromConfig( if (resetTriggered && normalizeOptionalString(bodyStripped)) { const { applyResetModelOverride } = await loadSessionResetModelRuntime(); - await applyResetModelOverride({ - cfg, - agentId, - resetTriggered, - bodyStripped, - sessionCtx, - ctx: finalized, - sessionEntry, - sessionEntryHandle, - sessionStore, - sessionKey, - storePath, - defaultProvider, - defaultModel, - aliasIndex, - }); + try { + await applyResetModelOverride({ + cfg, + agentId, + resetTriggered, + bodyStripped, + sessionCtx, + ctx: finalized, + sessionEntry, + sessionEntryHandle, + sessionStore, + sessionKey, + storePath, + defaultProvider, + defaultModel, + aliasIndex, + }); + } catch (error) { + if (!isSessionWorkStartInvalidatedError(error)) { + throw error; + } + typing.cleanup(); + return { text: error.message }; + } } const channelModelOverride = cfg.channels?.modelByChannel @@ -902,29 +912,37 @@ export async function getReplyFromConfig( const runModel = runAutoFallbackPrimaryProbe?.model ?? model; let runModelState = modelState; if (runAutoFallbackPrimaryProbe) { - runModelState = await createModelSelectionState({ - cfg, - agentId, - agentCfg, - sessionEntry, - sessionStore, - sessionKey, - parentSessionKey: - sessionEntry.parentSessionKey ?? - sessionCtx.ModelParentSessionKey ?? - sessionCtx.ParentSessionKey, - storePath, - defaultProvider, - defaultModel, - primaryProvider, - primaryModel, - provider: runProvider, - model: runModel, - hasModelDirective: false, - skipStoredModelOverride: true, - hasResolvedHeartbeatModelOverride, - isHeartbeat: opts?.isHeartbeat === true, - }); + try { + runModelState = await createModelSelectionState({ + cfg, + agentId, + agentCfg, + sessionEntry, + sessionStore, + sessionKey, + parentSessionKey: + sessionEntry.parentSessionKey ?? + sessionCtx.ModelParentSessionKey ?? + sessionCtx.ParentSessionKey, + storePath, + defaultProvider, + defaultModel, + primaryProvider, + primaryModel, + provider: runProvider, + model: runModel, + hasModelDirective: false, + skipStoredModelOverride: true, + hasResolvedHeartbeatModelOverride, + isHeartbeat: opts?.isHeartbeat === true, + }); + } catch (error) { + if (!isSessionWorkStartInvalidatedError(error)) { + throw error; + } + typing.cleanup(); + return { text: error.message }; + } const thinkingLevelOverride = normalizeThinkLevel(resolvedOpts?.thinkingLevelOverride); const hasTurnOrSessionThinkLevel = thinkingLevelOverride !== undefined || diff --git a/src/auto-reply/reply/model-selection.test.ts b/src/auto-reply/reply/model-selection.test.ts index 32a6fca8a282..9f496ad9703f 100644 --- a/src/auto-reply/reply/model-selection.test.ts +++ b/src/auto-reply/reply/model-selection.test.ts @@ -1,4 +1,7 @@ // Tests model selection resolution from directives, config, and session state. +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { MODEL_CONTEXT_TOKEN_CACHE, @@ -10,6 +13,7 @@ import { } from "../../agents/model-catalog.runtime.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; +import { loadSessionStore, saveSessionStore } from "../../config/sessions/store.js"; import { createModelSelectionState, resolveContextTokens } from "./model-selection.js"; vi.mock("../../agents/model-catalog.runtime.js", () => ({ @@ -1145,6 +1149,124 @@ describe("createModelSelectionState respects session model override", () => { expect(sessionStore[sessionKey]?.providerOverride).toBeUndefined(); }); + it("adopts a concurrent valid model while repairing a stale override", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-repair-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const cfg = { + agents: { + defaults: { + model: { primary: "openai/gpt-4o" }, + models: { + "openai/gpt-4o": {}, + "openai/gpt-5.5": {}, + }, + }, + }, + } as OpenClawConfig; + const sessionKey = "agent:main:telegram:direct:1"; + const sessionEntry = makeEntry({ + providerOverride: "openai", + modelOverride: "gpt-4o-mini", + }); + const concurrentEntry = makeEntry({ + updatedAt: sessionEntry.updatedAt + 1, + providerOverride: "openai", + modelOverride: "gpt-5.5", + modelOverrideSource: "user", + }); + await saveSessionStore(storePath, { [sessionKey]: concurrentEntry }, { skipMaintenance: true }); + const sessionStore = { [sessionKey]: sessionEntry }; + + try { + const state = await createModelSelectionState({ + cfg, + agentCfg: cfg.agents?.defaults, + sessionEntry, + sessionStore, + sessionKey, + storePath, + defaultProvider: "openai", + defaultModel: "gpt-4o", + provider: "openai", + model: "gpt-4o-mini", + hasModelDirective: false, + }); + + expect(state).toMatchObject({ + provider: "openai", + model: "gpt-5.5", + resetModelOverride: false, + }); + expect(sessionEntry).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-5.5", + modelOverrideSource: "user", + }); + expect(sessionStore[sessionKey]).toEqual(sessionEntry); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toEqual(sessionEntry); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("rejects stale-model repair when the session rotates during persistence", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-repair-rotation-")); + const storePath = path.join(tempRoot, "sessions.json"); + const cfg = { + agents: { + defaults: { + model: { primary: "openai/gpt-4o" }, + models: { + "openai/gpt-4o": {}, + }, + }, + }, + } as OpenClawConfig; + const sessionKey = "agent:main:telegram:direct:1"; + const sessionEntry = makeEntry({ + sessionId: "s1", + providerOverride: "openai", + modelOverride: "gpt-4o-mini", + }); + const rotatedEntry = makeEntry({ + sessionId: "s2", + updatedAt: sessionEntry.updatedAt + 1, + providerOverride: "openai", + modelOverride: "gpt-4o", + modelOverrideSource: "user", + }); + await saveSessionStore(storePath, { [sessionKey]: rotatedEntry }, { skipMaintenance: true }); + const sessionStore = { [sessionKey]: sessionEntry }; + + try { + await expect( + createModelSelectionState({ + cfg, + agentCfg: cfg.agents?.defaults, + sessionEntry, + sessionStore, + sessionKey, + storePath, + defaultProvider: "openai", + defaultModel: "gpt-4o", + provider: "openai", + model: "gpt-4o-mini", + hasModelDirective: false, + }), + ).rejects.toThrow(/changed while starting work/i); + + expect(sessionEntry).toMatchObject({ + sessionId: "s1", + providerOverride: "openai", + modelOverride: "gpt-4o-mini", + }); + expect(sessionStore[sessionKey]).toBe(sessionEntry); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toEqual(rotatedEntry); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + it("keeps wildcard-provider overrides when configured catalog rows are unavailable", async () => { const cfg = { agents: { diff --git a/src/auto-reply/reply/model-selection.ts b/src/auto-reply/reply/model-selection.ts index 2647bd44689b..766874b2d108 100644 --- a/src/auto-reply/reply/model-selection.ts +++ b/src/auto-reply/reply/model-selection.ts @@ -31,6 +31,11 @@ import { OPENAI_PROVIDER_ID, listOpenAIAuthProfileProvidersForAgentRuntime, } from "../../agents/openai-routing.js"; +import { SessionWorkStartInvalidatedError } from "../../config/sessions/lifecycle.js"; +import { + adoptPersistedSessionSnapshot, + sessionModelOverrideChangesApplied, +} from "../../config/sessions/session-snapshot-merge.js"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js"; @@ -112,8 +117,8 @@ function shouldLogModelSelectionTiming(): boolean { const modelCatalogRuntimeLoader = createLazyImportLoader( () => import("../../agents/model-catalog.runtime.js"), ); -const sessionAccessorRuntimeLoader = createLazyImportLoader( - () => import("../../config/sessions/session-accessor.js"), +const sessionPersistenceRuntimeLoader = createLazyImportLoader( + () => import("./session-entry-persistence.js"), ); function normalizeRuntimeModelRef(provider: string, model: string) { return normalizeModelRef(provider, model, RUNTIME_MODEL_VISIBILITY_NORMALIZATION); @@ -123,8 +128,8 @@ function loadModelCatalogRuntime() { return modelCatalogRuntimeLoader.load(); } -function loadSessionAccessorRuntime() { - return sessionAccessorRuntimeLoader.load(); +function loadSessionPersistenceRuntime() { + return sessionPersistenceRuntimeLoader.load(); } function findSelectedCatalogEntry(params: { @@ -321,20 +326,40 @@ export async function createModelSelectionState(params: { ); const key = modelKey(normalizedOverride.provider, normalizedOverride.model); if (staleDirectStoredOverride || !visibilityPolicy.allowsKey(key)) { + const initialSessionEntry = { ...sessionEntry }; + const nextSessionEntry = { ...sessionEntry }; const { updated } = applyModelOverrideToSessionEntry({ - entry: sessionEntry, + entry: nextSessionEntry, selection: { provider: primaryProvider, model: primaryModel, isDefault: true }, preserveAuthProfileOverride: staleDirectStoredOverride, }); + let resetApplied = updated; if (updated) { - sessionStore[sessionKey] = sessionEntry; if (storePath) { - const { replaceSessionEntry } = await loadSessionAccessorRuntime(); - await replaceSessionEntry({ storePath, sessionKey }, sessionEntry); + const { persistReplySessionEntry } = await loadSessionPersistenceRuntime(); + const persistence = await persistReplySessionEntry({ + storePath, + sessionKey, + initialEntry: initialSessionEntry, + entry: nextSessionEntry, + }); + if (persistence.status === "lifecycle-invalidated") { + throw new SessionWorkStartInvalidatedError(persistence.error); + } + const persistedEntry = persistence.entry; + resetApplied = sessionModelOverrideChangesApplied({ + initial: initialSessionEntry, + next: nextSessionEntry, + current: persistedEntry, + }); + adoptPersistedSessionSnapshot(sessionEntry, persistedEntry); + } else { + adoptPersistedSessionSnapshot(sessionEntry, nextSessionEntry); } + sessionStore[sessionKey] = sessionEntry; } - resetModelOverride = updated; - if (updated) { + resetModelOverride = resetApplied; + if (resetApplied) { resetModelOverrideRef = key; resetModelOverrideReason = staleDirectStoredOverride ? "stale" : "disallowed"; } @@ -369,7 +394,7 @@ export async function createModelSelectionState(params: { params.skipStoredModelOverride === true || hasOneTurnModelOverride || params.hasResolvedHeartbeatModelOverride === true || - (staleDirectStoredOverride && storedOverride?.source === "session"); + (resetModelOverride && staleDirectStoredOverride && storedOverride?.source === "session"); if (storedOverride?.model && !skipStoredOverride) { const normalizedStoredOverride = normalizeRuntimeModelRef( diff --git a/src/auto-reply/reply/queue/drain.ts b/src/auto-reply/reply/queue/drain.ts index 4ad639aa22ef..3ebecfdf562e 100644 --- a/src/auto-reply/reply/queue/drain.ts +++ b/src/auto-reply/reply/queue/drain.ts @@ -231,6 +231,7 @@ type FollowupRuntimeMetadata = Pick< | "currentInboundAudio" | "currentInboundContext" | "abortSignal" + | "queueAbortSignal" | "deliveryCorrelations" | "queuedLifecycle" >; @@ -253,7 +254,11 @@ function hasRuntimeOnlyFollowupMetadata(item: FollowupRun): boolean { } function combineAbortSignals(items: readonly FollowupRun[]): AbortSignal | undefined { - const signals = items.flatMap((item) => (item.abortSignal ? [item.abortSignal] : [])); + const signals = items.flatMap((item) => + [item.abortSignal, item.queueAbortSignal].filter( + (signal): signal is AbortSignal => signal !== undefined, + ), + ); if (signals.length === 0) { return undefined; } @@ -290,6 +295,7 @@ function collectRuntimeMetadata( ? singletonOwner : items.find(hasCurrentTurnRuntimeMetadata); const abortSignal = singletonOwner?.abortSignal ?? combineAbortSignals(candidates); + const queueAbortSignal = singletonOwner?.queueAbortSignal; const deliveryCorrelations = items.flatMap((item) => item.deliveryCorrelations ?? []); const lifecycleSource = singletonOwner ?? items.find((item) => item.queuedLifecycle); return { @@ -297,6 +303,7 @@ function collectRuntimeMetadata( currentInboundAudio: currentTurnSource?.currentInboundAudio, currentInboundContext: currentTurnSource?.currentInboundContext, abortSignal, + queueAbortSignal, deliveryCorrelations: deliveryCorrelations.length > 0 ? deliveryCorrelations : undefined, queuedLifecycle: singletonOwner?.queuedLifecycle ?? @@ -502,6 +509,7 @@ function resolveOverflowSummarySourceGroup(queue: { export function createOverflowSummaryRetrySource(source: FollowupRun): FollowupRun { return { prompt: source.prompt, + queueAbortSignal: source.queueAbortSignal, transcriptPrompt: source.transcriptPrompt, messageId: source.messageId, summaryLine: source.summaryLine, @@ -594,6 +602,7 @@ async function runSyntheticOverflowSummary(params: { const currentInboundEventKind = resolveOverflowSummaryInboundEventKind(params.sources); await params.runFollowup({ prompt: params.prompt, + queueAbortSignal: params.source.queueAbortSignal, transcriptPrompt: params.prompt, messageId: params.source.messageId, userTurnTranscriptRecorder, diff --git a/src/auto-reply/reply/queue/enqueue.ts b/src/auto-reply/reply/queue/enqueue.ts index 9814ff39f085..33588d6fe757 100644 --- a/src/auto-reply/reply/queue/enqueue.ts +++ b/src/auto-reply/reply/queue/enqueue.ts @@ -171,6 +171,7 @@ export function enqueueFollowupRun( return false; } + run.queueAbortSignal = queue.abortController.signal; queue.items.push(run); markFollowupRunEnqueued(run); if (recentMessageIdKey) { diff --git a/src/auto-reply/reply/queue/state.test.ts b/src/auto-reply/reply/queue/state.test.ts index 715028843f27..d255c31c82a0 100644 --- a/src/auto-reply/reply/queue/state.test.ts +++ b/src/auto-reply/reply/queue/state.test.ts @@ -1,5 +1,6 @@ // Tests queue state storage, dedupe, and cleanup primitives. import { afterEach, describe, expect, it } from "vitest"; +import { enqueueFollowupRun } from "./enqueue.js"; import { clearFollowupQueue, getFollowupQueue, refreshQueuedFollowupSession } from "./state.js"; import type { FollowupRun } from "./types.js"; @@ -121,6 +122,19 @@ describe("refreshQueuedFollowupSession", () => { }); describe("getFollowupQueue", () => { + it("aborts work owned by a cleared queue", () => { + const queuedRun: FollowupRun = { + prompt: "queued message", + enqueuedAt: Date.now(), + run: makeRun(), + }; + enqueueFollowupRun(QUEUE_KEY, queuedRun, { mode: "followup" }); + + expect(queuedRun.queueAbortSignal?.aborted).toBe(false); + clearFollowupQueue(QUEUE_KEY); + expect(queuedRun.queueAbortSignal?.aborted).toBe(true); + }); + it("trims overflow metadata when a live queue cap shrinks", () => { const queue = getFollowupQueue(QUEUE_KEY, { mode: "followup", cap: 3 }); for (const [contextKey, count] of [ diff --git a/src/auto-reply/reply/queue/state.ts b/src/auto-reply/reply/queue/state.ts index d81c81e45ba9..c747035200d6 100644 --- a/src/auto-reply/reply/queue/state.ts +++ b/src/auto-reply/reply/queue/state.ts @@ -11,6 +11,7 @@ import { } from "./types.js"; export type FollowupQueueState = { + abortController: AbortController; items: FollowupRun[]; draining: boolean; lastEnqueuedAt: number; @@ -75,6 +76,7 @@ export function getFollowupQueue(key: string, settings: QueueSettings): Followup } const created: FollowupQueueState = { + abortController: new AbortController(), items: [], draining: false, lastEnqueuedAt: 0, @@ -108,6 +110,7 @@ export function clearFollowupQueue(key: string): number { if (!queue) { return 0; } + queue.abortController.abort(); const cleared = queue.items.length + queue.droppedCount; for (const item of queue.items) { completeFollowupRunLifecycle(item); diff --git a/src/auto-reply/reply/queue/types.ts b/src/auto-reply/reply/queue/types.ts index 642c17fe00dc..8b44c247bdb6 100644 --- a/src/auto-reply/reply/queue/types.ts +++ b/src/auto-reply/reply/queue/types.ts @@ -62,6 +62,8 @@ export type FollowupRun = { currentInboundContext?: CurrentInboundPromptContext; /** Abort signal for turns that are canceled by their source-channel admission fence. */ abortSignal?: AbortSignal; + /** Queue-owned cancellation fence used when lifecycle cleanup invalidates pending work. */ + queueAbortSignal?: AbortSignal; deliveryCorrelations?: QueuedReplyDeliveryCorrelation[]; queuedLifecycle?: QueuedReplyLifecycle; /** Provider message ID, when available (for deduplication). */ @@ -160,8 +162,10 @@ export type FollowupRun = { }; }; -export function isFollowupRunAborted(run: Pick): boolean { - return run.abortSignal?.aborted === true; +export function isFollowupRunAborted( + run: Pick, +): boolean { + return run.abortSignal?.aborted === true || run.queueAbortSignal?.aborted === true; } const enqueuedFollowupLifecycles = new WeakSet(); diff --git a/src/auto-reply/reply/reply-operation-run-state.ts b/src/auto-reply/reply/reply-operation-run-state.ts index 3a978166d66f..e2ed5fcaa297 100644 --- a/src/auto-reply/reply/reply-operation-run-state.ts +++ b/src/auto-reply/reply/reply-operation-run-state.ts @@ -1,6 +1,6 @@ export type ReplyOperationAdmissionSnapshot = | { status: "owned" } - | { status: "skipped"; reason: "active-run" | "aborted" }; + | { status: "skipped"; reason: "active-run" | "aborted" | "lifecycle-invalidated" }; export type ReplyOperationRunState = { admission?: ReplyOperationAdmissionSnapshot; diff --git a/src/auto-reply/reply/reply-run-registry.ts b/src/auto-reply/reply/reply-run-registry.ts index 074e20305c83..b13b5199aff9 100644 --- a/src/auto-reply/reply/reply-run-registry.ts +++ b/src/auto-reply/reply/reply-run-registry.ts @@ -71,6 +71,8 @@ export type ReplyOperation = { readonly terminalRecovery: boolean; readonly phase: ReplyOperationPhase; readonly result: ReplyOperationResult | null; + /** True when this operation has owned the supplied session ID. */ + hasOwnedSessionId(sessionId: string): boolean; setPhase(next: "queued" | "preflight_compacting" | "memory_flushing" | "running"): void; /** Mark this operation as an in-flight terminal-session recovery. */ markTerminalRecovery(): void; @@ -234,6 +236,7 @@ function isReplyRunCompacting(operation: ReplyOperation): boolean { const attachedBackendByOperation = new WeakMap(); const abortFrozenOperations = new WeakSet(); const operationsByUpstreamAbortSignal = new WeakMap(); +const retainStateUntilCompleteOperations = new WeakSet(); const afterClearCallbacksByOperation = new WeakMap< ReplyOperation, Set<(sessionId: string) => void> @@ -263,6 +266,11 @@ export function isReplyRunAbortableForSignal(signal: AbortSignal): boolean { return operation ? isReplyOperationAbortable(operation) : true; } +/** Keep terminal state registered until the operation owner exits via complete(). */ +export function retainReplyOperationUntilComplete(operation: ReplyOperation): void { + retainStateUntilCompleteOperations.add(operation); +} + function isReplyBackendMessageInjectable(backend: ReplyBackendHandle): boolean { try { return backend.isStopped === undefined ? backend.isStreaming() : !backend.isStopped(); @@ -297,16 +305,12 @@ function flushReplyOperationAfterClear(operation: ReplyOperation, sessionId: str } } -function registerFollowupAdmissionBarrier( - sessionKey: string, - sessionId: string, +export function waitForReplyBarrierSettlement( barrier: PromiseLike, timeout: number | ReplyFollowupAdmissionBarrierTimeoutPolicy = REPLY_RUN_IDLE_SETTLE_TIMEOUT_MS, -): ReplyRunFollowupAdmissionBarrier { - const barriersByKey = replyRunState.followupAdmissionBarriersByKey; - const previous = barriersByKey.get(sessionKey)?.settled; +): Promise { // Owners may extend this for bounded retry envelopes; all barriers retain a failsafe. - const current = new Promise((resolve) => { + return new Promise((resolve) => { let settled = false; let timer: ReturnType; const finish = () => { @@ -352,6 +356,17 @@ function registerFollowupAdmissionBarrier( } void Promise.resolve(barrier).then(finish, finish); }); +} + +function registerFollowupAdmissionBarrier( + sessionKey: string, + sessionId: string, + barrier: PromiseLike, + timeout: number | ReplyFollowupAdmissionBarrierTimeoutPolicy = REPLY_RUN_IDLE_SETTLE_TIMEOUT_MS, +): ReplyRunFollowupAdmissionBarrier { + const barriersByKey = replyRunState.followupAdmissionBarriersByKey; + const previous = barriersByKey.get(sessionKey)?.settled; + const current = waitForReplyBarrierSettlement(barrier, timeout); const settled = previous ? Promise.all([previous, current]).then(() => undefined) : current; const entry = { settled, sessionId }; barriersByKey.set(sessionKey, entry); @@ -446,6 +461,7 @@ export function createReplyOperation(params: { upstreamAbortSignal?.removeEventListener("abort", upstreamAbortHandler); upstreamAbortHandler = undefined; }; + const ownedSessionIds = new Set([sessionId]); const clearState = ( afterClearBarrier?: PromiseLike, @@ -524,6 +540,10 @@ export function createReplyOperation(params: { get result() { return result; }, + hasOwnedSessionId(candidateSessionId) { + const normalizedSessionId = normalizeOptionalString(candidateSessionId); + return normalizedSessionId ? ownedSessionIds.has(normalizedSessionId) : false; + }, setPhase(next) { if (result) { return; @@ -552,6 +572,7 @@ export function createReplyOperation(params: { replyRunState.activeKeysBySessionId.delete(currentSessionId); registerWaitSessionId(sessionKey, currentSessionId); currentSessionId = normalizedNextSessionId; + ownedSessionIds.add(currentSessionId); updateFollowupAdmissionSessionId(sessionKey, currentSessionId); replyRunState.activeSessionIdsByKey.set(sessionKey, currentSessionId); replyRunState.activeKeysBySessionId.set(currentSessionId, sessionKey); @@ -611,7 +632,7 @@ export function createReplyOperation(params: { result = { kind: "failed", code, cause }; phase = "failed"; } - if (!retainFailureUntilComplete) { + if (!retainFailureUntilComplete && !retainStateUntilCompleteOperations.has(operation)) { clearState(); } }, @@ -623,7 +644,7 @@ export function createReplyOperation(params: { abortWithReason("user_abort", createUserAbortError(), { abortedCode: "aborted_by_user", }); - if (phaseBeforeAbort === "queued") { + if (phaseBeforeAbort === "queued" && !retainStateUntilCompleteOperations.has(operation)) { clearState(); } return true; @@ -636,7 +657,7 @@ export function createReplyOperation(params: { abortWithReason("restart", createAgentRunRestartAbortError(), { abortedCode: "aborted_for_restart", }); - if (phaseBeforeAbort === "queued") { + if (phaseBeforeAbort === "queued" && !retainStateUntilCompleteOperations.has(operation)) { clearState(); } return true; @@ -659,7 +680,7 @@ export function createReplyOperation(params: { abortWithReason(restart ? "restart" : "user_abort", upstreamAbortSignal.reason, { abortedCode: restart ? "aborted_for_restart" : "aborted_by_user", }); - if (phaseBeforeAbort === "queued") { + if (phaseBeforeAbort === "queued" && !retainStateUntilCompleteOperations.has(operation)) { clearState(); } }; diff --git a/src/auto-reply/reply/reply-turn-admission.test.ts b/src/auto-reply/reply/reply-turn-admission.test.ts index 8b148456ca74..9cd292ed0bb7 100644 --- a/src/auto-reply/reply/reply-turn-admission.test.ts +++ b/src/auto-reply/reply/reply-turn-admission.test.ts @@ -1,17 +1,435 @@ // Tests reply turn admission decisions for active, queued, and aborted runs. +import fs from "node:fs"; +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; +import { + interruptSessionWorkAdmissions, + runExclusiveSessionLifecycleMutation, +} from "../../sessions/session-lifecycle-admission.js"; import { createReplyOperation, + replyRunRegistry, runAfterReplyOperationClear, testing, + type ReplyOperation, } from "./reply-run-registry.js"; -import { admitReplyTurn } from "./reply-turn-admission.js"; +import { admitReplyTurn, runWithReplyOperationLifecycleAdmission } from "./reply-turn-admission.js"; + +const tempDirs = useAutoCleanupTempDirTracker(afterEach); + +function createDeferred() { + let resolve = () => {}; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +function createSessionStore(entries: Record): string { + const root = tempDirs.make("openclaw-reply-admission-"); + const storePath = path.join(root, "sessions.json"); + fs.writeFileSync(storePath, JSON.stringify(entries)); + return storePath; +} describe("reply turn admission", () => { afterEach(() => { testing.resetReplyRunRegistry(); }); + it("rejects a reply when an archive commits before admission", async () => { + const sessionKey = "agent:main:telegram:topic:archived"; + const sessionId = "session-before-archive"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + fs.writeFileSync( + storePath, + JSON.stringify({ + [sessionKey]: { sessionId, updatedAt: Date.now(), archivedAt: Date.now() }, + }), + ); + }, + }); + await mutationStarted.promise; + + const admission = admitReplyTurn({ + sessionKey, + sessionId, + storePath, + kind: "visible", + resetTriggered: false, + }); + releaseMutation.resolve(); + await mutation; + + await expect(admission).rejects.toThrow( + `Session "${sessionKey}" is archived. Restore it before starting new work.`, + ); + }); + + it("rejects a reply when deletion commits before admission", async () => { + const sessionKey = "agent:main:telegram:topic:deleted"; + const sessionId = "session-before-delete"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + fs.writeFileSync(storePath, JSON.stringify({})); + }, + }); + await mutationStarted.promise; + + const admission = admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "visible", + resetTriggered: false, + }); + releaseMutation.resolve(); + await mutation; + + await expect(admission).rejects.toThrow(/deleted while starting work/i); + }); + + it("uses the persisted session id when reset commits before admission", async () => { + const sessionKey = "agent:main:telegram:topic:reset"; + const sessionId = "session-before-reset"; + const nextSessionId = "session-after-reset"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + fs.writeFileSync( + storePath, + JSON.stringify({ + [sessionKey]: { sessionId: nextSessionId, updatedAt: Date.now() }, + }), + ); + }, + }); + await mutationStarted.promise; + + const admission = admitReplyTurn({ + sessionKey, + sessionId, + storePath, + kind: "visible", + resetTriggered: false, + }); + releaseMutation.resolve(); + await mutation; + const result = await admission; + + expect(result.status).toBe("owned"); + if (result.status === "owned") { + expect(result.operation.sessionId).toBe(nextSessionId); + result.operation.complete(); + } + }); + + it("rejects expected-session work when reset commits before admission", async () => { + const sessionKey = "agent:main:telegram:topic:reset-expected"; + const sessionId = "session-before-reset"; + const nextSessionId = "session-after-reset"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + fs.writeFileSync( + storePath, + JSON.stringify({ + [sessionKey]: { sessionId: nextSessionId, updatedAt: Date.now() }, + }), + ); + }, + }); + await mutationStarted.promise; + + const admission = admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "visible", + resetTriggered: false, + }); + releaseMutation.resolve(); + await mutation; + + await expect(admission).rejects.toThrow(/changed while starting work/i); + }); + + it("drops queued work when reset cleanup cancels admission", async () => { + const sessionKey = "agent:main:telegram:topic:queued-reset"; + const sessionId = "session-before-reset"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const abortController = new AbortController(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + abortController.abort(); + fs.writeFileSync( + storePath, + JSON.stringify({ + [sessionKey]: { sessionId: "session-after-reset", updatedAt: Date.now() }, + }), + ); + }, + }); + await mutationStarted.promise; + + const admission = admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "queued_followup", + resetTriggered: false, + upstreamAbortSignal: abortController.signal, + }); + releaseMutation.resolve(); + await mutation; + + await expect(admission).resolves.toEqual({ + status: "skipped", + reason: "aborted", + }); + }); + + it("drops queued work when the session is archived", async () => { + const sessionKey = "agent:main:telegram:topic:queued-archive"; + const sessionId = "session-before-archive"; + const storePath = createSessionStore({ + [sessionKey]: { + sessionId, + updatedAt: Date.now(), + archivedAt: Date.now(), + }, + }); + + await expect( + admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "queued_followup", + resetTriggered: false, + }), + ).resolves.toEqual({ + status: "skipped", + reason: "lifecycle-invalidated", + }); + }); + + it("holds lifecycle admission until a running reply operation clears", async () => { + const sessionKey = "agent:main:telegram:topic:running-reset"; + const sessionId = "session-before-reset"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const admission = await admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "visible", + resetTriggered: false, + }); + expect(admission.status).toBe("owned"); + if (admission.status !== "owned") { + return; + } + admission.operation.setPhase("running"); + let mutationRan = false; + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: storePath, + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }); + + await vi.waitFor(() => { + expect(admission.operation.abortSignal.aborted).toBe(true); + }); + expect(admission.operation.result).toEqual({ + kind: "aborted", + code: "aborted_for_restart", + }); + expect(mutationRan).toBe(false); + + admission.operation.complete(); + await mutation; + expect(mutationRan).toBe(true); + }); + + it("holds interrupted queued reply work until its owner exits", async () => { + const sessionKey = "agent:main:telegram:topic:queued-delete"; + const sessionId = "session-before-delete"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const admission = await admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "visible", + resetTriggered: false, + }); + expect(admission.status).toBe("owned"); + if (admission.status !== "owned") { + return; + } + + let mutationRan = false; + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: storePath, + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationRan = true; + }, + }); + + await vi.waitFor(() => { + expect(admission.operation.abortSignal.aborted).toBe(true); + }); + expect(admission.operation.result).toEqual({ + kind: "aborted", + code: "aborted_for_restart", + }); + expect(mutationRan).toBe(false); + expect(replyRunRegistry.get(sessionKey)).toBe(admission.operation); + + admission.operation.complete(); + await mutation; + expect(mutationRan).toBe(true); + expect(replyRunRegistry.get(sessionKey)).toBeUndefined(); + }); + + it("excludes the initiating reply admission from an in-band lifecycle mutation", async () => { + const sessionKey = "agent:main:telegram:topic:in-band-reset"; + const sessionId = "session-before-reset"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const admission = await admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "visible", + resetTriggered: false, + }); + expect(admission.status).toBe("owned"); + if (admission.status !== "owned") { + return; + } + + await runWithReplyOperationLifecycleAdmission(admission.operation, async () => { + await runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: storePath, + identities: [sessionKey, sessionId], + }); + }, + run: async () => undefined, + }); + }); + + expect(admission.operation.abortSignal.aborted).toBe(false); + admission.operation.complete(); + }); + + it("skips an aborted reply waiting behind a lifecycle mutation", async () => { + const sessionKey = "agent:main:telegram:topic:aborted"; + const sessionId = "session-before-abort"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + const controller = new AbortController(); + const admission = admitReplyTurn({ + sessionKey, + sessionId, + storePath, + kind: "visible", + resetTriggered: false, + upstreamAbortSignal: controller.signal, + }); + controller.abort(); + releaseMutation.resolve(); + await mutation; + + await expect(admission).resolves.toEqual({ status: "skipped", reason: "aborted" }); + }); + it("waits for visible turns and reuses the active session id", async () => { const active = createReplyOperation({ sessionKey: "agent:main:telegram:topic:42", @@ -31,7 +449,9 @@ describe("reply turn admission", () => { void admitted.then(() => { settled = true; }); - await Promise.resolve(); + await new Promise((resolve) => { + setImmediate(resolve); + }); expect(settled).toBe(false); active.complete(); @@ -271,6 +691,206 @@ describe("reply turn admission", () => { } }); + it("accepts an expected session id rotated by the active run", async () => { + const sessionKey = "agent:main:telegram:topic:compaction"; + const sessionId = "pre-compact-session"; + const nextSessionId = "post-compact-session"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const active = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + }); + active.setPhase("preflight_compacting"); + + const admitted = admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "visible", + resetTriggered: false, + }); + + await new Promise((resolve) => { + setImmediate(resolve); + }); + fs.writeFileSync( + storePath, + JSON.stringify({ + [sessionKey]: { sessionId: nextSessionId, updatedAt: Date.now() }, + }), + ); + active.updateSessionId(nextSessionId); + active.complete(); + const result = await admitted; + + expect(result.status).toBe("owned"); + if (result.status === "owned") { + expect(result.operation.sessionId).toBe(nextSessionId); + result.operation.complete(); + } + }); + + it("accepts a rotation already published by the expected active run", async () => { + const sessionKey = "agent:main:telegram:topic:compaction-before-admission"; + const sessionId = "pre-compact-session"; + const nextSessionId = "post-compact-session"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const active = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + }); + active.setPhase("preflight_compacting"); + active.updateSessionId(nextSessionId); + fs.writeFileSync( + storePath, + JSON.stringify({ + [sessionKey]: { sessionId: nextSessionId, updatedAt: Date.now() }, + }), + ); + active.complete(); + + const result = await admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + expectedActiveOperation: active, + storePath, + kind: "visible", + resetTriggered: false, + }); + + expect(result.status).toBe("owned"); + if (result.status === "owned") { + expect(result.operation.sessionId).toBe(nextSessionId); + result.operation.complete(); + } + }); + + it("accepts a rotation published by the live owner after the caller snapshot", async () => { + const sessionKey = "agent:main:telegram:topic:late-compaction-owner"; + const sessionId = "pre-compact-session"; + const nextSessionId = "post-compact-session"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const active = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + }); + active.setPhase("preflight_compacting"); + active.updateSessionId(nextSessionId); + fs.writeFileSync( + storePath, + JSON.stringify({ + [sessionKey]: { sessionId: nextSessionId, updatedAt: Date.now() }, + }), + ); + + const admitted = admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "visible", + resetTriggered: false, + waitForActive: true, + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + active.complete(); + const result = await admitted; + + expect(result.status).toBe("owned"); + if (result.status === "owned") { + expect(result.operation.sessionId).toBe(nextSessionId); + result.operation.complete(); + } + }); + + it("rejects a fresh post-reset owner as rotation proof", async () => { + const sessionKey = "agent:main:telegram:topic:fresh-post-reset-owner"; + const sessionId = "session-before-reset"; + const nextSessionId = "session-after-reset"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId: nextSessionId, updatedAt: Date.now() }, + }); + const freshOwner = createReplyOperation({ + sessionKey, + sessionId: nextSessionId, + resetTriggered: false, + }); + + const admitted = admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + storePath, + kind: "visible", + resetTriggered: false, + waitForActive: true, + }); + + await expect(admitted).rejects.toThrow(/changed while starting work/i); + freshOwner.complete(); + }); + + it.each([ + ["failed", (operation: ReplyOperation) => operation.fail("run_failed")], + [ + "user-aborted", + (operation: ReplyOperation) => { + operation.abortByUser(); + operation.complete(); + }, + ], + ])("accepts a rotation published before the expected run %s", async (_outcome, finish) => { + const sessionKey = "agent:main:telegram:topic:compaction-terminal-outcome"; + const sessionId = "pre-compact-session"; + const nextSessionId = "post-compact-session"; + const storePath = createSessionStore({ + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + const active = createReplyOperation({ + sessionKey, + sessionId, + resetTriggered: false, + }); + active.setPhase("preflight_compacting"); + active.updateSessionId(nextSessionId); + fs.writeFileSync( + storePath, + JSON.stringify({ + [sessionKey]: { sessionId: nextSessionId, updatedAt: Date.now() }, + }), + ); + finish(active); + + const result = await admitReplyTurn({ + sessionKey, + sessionId, + expectedSessionId: sessionId, + expectedActiveOperation: active, + storePath, + kind: "visible", + resetTriggered: false, + }); + + expect(result.status).toBe("owned"); + if (result.status === "owned") { + expect(result.operation.sessionId).toBe(nextSessionId); + result.operation.complete(); + } + }); + it("skips heartbeat turns while a visible turn owns the lane", async () => { const active = createReplyOperation({ sessionKey: "agent:main:telegram:topic:42", diff --git a/src/auto-reply/reply/reply-turn-admission.ts b/src/auto-reply/reply/reply-turn-admission.ts index e4bcf8be94c5..c68307d848ed 100644 --- a/src/auto-reply/reply/reply-turn-admission.ts +++ b/src/auto-reply/reply/reply-turn-admission.ts @@ -1,10 +1,18 @@ // Decides whether an inbound turn may start, queue, or abort a reply run. +import { resolveSessionWorkStartError } from "../../config/sessions/lifecycle.js"; +import { loadSessionEntry } from "../../config/sessions/session-accessor.js"; +import { + beginSessionWorkAdmission, + type SessionWorkAdmissionLease, +} from "../../sessions/session-lifecycle-admission.js"; import { createReplyOperation, REPLY_RUN_IDLE_SETTLE_TIMEOUT_MS, replyRunRegistry, ReplyRunAlreadyActiveError, ReplyRunFollowupAdmissionBlockedError, + retainReplyOperationUntilComplete, + runAfterReplyOperationClear, type ReplyOperation, waitForReplyRunFollowupAdmission, } from "./reply-run-registry.js"; @@ -17,10 +25,31 @@ export type ReplyTurnAdmission = | { status: "owned"; operation: ReplyOperation } | { status: "skipped"; - reason: "active-run" | "aborted"; + reason: "active-run" | "aborted" | "lifecycle-invalidated"; activeOperation?: ReplyOperation; + lifecycleAdmission?: SessionWorkAdmissionLease; }; +class QueuedFollowupLifecycleInvalidatedError extends Error {} + +const lifecycleAdmissionByOperation = new WeakMap(); + +/** Runs owner work with its admission marked as the initiating lifecycle context. */ +export async function runWithReplyOperationLifecycleAdmission( + operation: ReplyOperation | undefined, + run: () => Promise, +): Promise { + const admission = operation ? lifecycleAdmissionByOperation.get(operation) : undefined; + return admission ? await admission.run(run) : await run(); +} + +function rejectLifecycleInvalidatedWork(params: { kind: ReplyTurnKind; message: string }): never { + if (params.kind === "queued_followup") { + throw new QueuedFollowupLifecycleInvalidatedError(params.message); + } + throw new Error(params.message); +} + function isAbortSignalAborted(signal: AbortSignal | undefined): boolean { return signal?.aborted === true; } @@ -29,14 +58,20 @@ function isAbortSignalAborted(signal: AbortSignal | undefined): boolean { export async function admitReplyTurn(params: { sessionKey: string; sessionId: string; + expectedSessionId?: string; + expectedActiveOperation?: ReplyOperation; + storePath?: string; kind: ReplyTurnKind; resetTriggered: boolean; routeThreadId?: string | number; upstreamAbortSignal?: AbortSignal; waitTimeoutMs?: number; waitForActive?: boolean; + retainLifecycleAdmissionOnActive?: boolean; + onLifecycleInterrupt?: () => void; }): Promise { let sessionId = params.sessionId; + let expectedSessionId = params.expectedSessionId; const waitTimeoutMs = params.waitTimeoutMs ?? (params.kind === "queued_followup" ? REPLY_RUN_IDLE_SETTLE_TIMEOUT_MS : undefined); @@ -45,9 +80,90 @@ export async function admitReplyTurn(params: { return { status: "skipped", reason: "aborted" }; } try { - return { - status: "owned", - operation: createReplyOperation({ + const storePath = params.storePath; + let operation: ReplyOperation | undefined; + let interruptedBeforeOperation = false; + const admission = storePath + ? await beginSessionWorkAdmission({ + scope: storePath, + identities: [params.sessionKey], + signal: params.upstreamAbortSignal, + onInterrupt: () => { + interruptedBeforeOperation = true; + operation?.abortForRestart(); + params.onLifecycleInterrupt?.(); + }, + assertAllowed: () => { + const currentEntry = loadSessionEntry({ + storePath, + sessionKey: params.sessionKey, + readConsistency: "latest", + }); + if (expectedSessionId && !currentEntry) { + rejectLifecycleInvalidatedWork({ + kind: params.kind, + message: `Session "${params.sessionKey}" was deleted while starting work. Retry.`, + }); + } + const registeredOperation = replyRunRegistry.get(params.sessionKey); + const rotationOperation = [registeredOperation, params.expectedActiveOperation].find( + (candidate) => { + if ( + !candidate || + !expectedSessionId || + currentEntry?.sessionId !== candidate.sessionId || + !candidate.hasOwnedSessionId(expectedSessionId) + ) { + return false; + } + if ( + candidate.result?.kind === "aborted" && + candidate.result.code === "aborted_for_restart" + ) { + return false; + } + return candidate === registeredOperation || candidate.result !== null; + }, + ); + const activeOperationRotatedExpectedSession = Boolean( + rotationOperation && currentEntry?.sessionId === rotationOperation.sessionId, + ); + if ( + expectedSessionId && + currentEntry?.sessionId !== expectedSessionId && + !activeOperationRotatedExpectedSession + ) { + rejectLifecycleInvalidatedWork({ + kind: params.kind, + message: `Session "${params.sessionKey}" changed while starting work. Retry.`, + }); + } + if (activeOperationRotatedExpectedSession) { + expectedSessionId = currentEntry?.sessionId; + } + const archivedSessionError = resolveSessionWorkStartError( + params.sessionKey || sessionId, + currentEntry, + ); + if (archivedSessionError) { + rejectLifecycleInvalidatedWork({ + kind: params.kind, + message: archivedSessionError, + }); + } + sessionId = currentEntry?.sessionId ?? sessionId; + }, + }) + : undefined; + if (interruptedBeforeOperation) { + admission?.release(); + rejectLifecycleInvalidatedWork({ + kind: params.kind, + message: `Session "${params.sessionKey}" changed while starting work. Retry.`, + }); + } + try { + operation = createReplyOperation({ sessionKey: params.sessionKey, sessionId, resetTriggered: params.resetTriggered, @@ -55,9 +171,45 @@ export async function admitReplyTurn(params: { upstreamAbortSignal: params.upstreamAbortSignal, respectFollowupAdmissionBarrier: params.kind === "queued_followup" || params.kind === "heartbeat", - }), + }); + } catch (error) { + if ( + error instanceof ReplyRunAlreadyActiveError && + admission && + params.retainLifecycleAdmissionOnActive + ) { + return { + status: "skipped", + reason: "active-run", + activeOperation: replyRunRegistry.get(params.sessionKey), + lifecycleAdmission: admission, + }; + } + admission?.release(); + throw error; + } + if (admission) { + // The lifecycle fence follows hooks, media work, agent execution, and + // final delivery. Reset/delete interrupts the operation and waits until + // its actual owner clears it before mutating the persisted session. + retainReplyOperationUntilComplete(operation); + lifecycleAdmissionByOperation.set(operation, admission); + runAfterReplyOperationClear(operation, () => { + lifecycleAdmissionByOperation.delete(operation); + admission.release(); + }); + } + return { + status: "owned", + operation, }; } catch (error) { + if (isAbortSignalAborted(params.upstreamAbortSignal)) { + return { status: "skipped", reason: "aborted" }; + } + if (error instanceof QueuedFollowupLifecycleInvalidatedError) { + return { status: "skipped", reason: "lifecycle-invalidated" }; + } if (error instanceof ReplyRunFollowupAdmissionBlockedError) { if (params.kind === "heartbeat") { return { status: "skipped", reason: "active-run" }; @@ -74,6 +226,9 @@ export async function admitReplyTurn(params: { }; } sessionId = followupAdmission.sessionId ?? sessionId; + if (expectedSessionId && followupAdmission.sessionId) { + expectedSessionId = followupAdmission.sessionId; + } continue; } if (!(error instanceof ReplyRunAlreadyActiveError)) { @@ -99,6 +254,17 @@ export async function admitReplyTurn(params: { } if (activeOperation) { sessionId = activeOperation.sessionId; + // In-lane compaction may rotate the active operation's persisted ID. + // Lifecycle reset aborts use a distinct result and must stay invalidated. + if ( + expectedSessionId && + !( + activeOperation.result?.kind === "aborted" && + activeOperation.result.code === "aborted_for_restart" + ) + ) { + expectedSessionId = activeOperation.sessionId; + } } } } diff --git a/src/auto-reply/reply/session-entry-persistence.test.ts b/src/auto-reply/reply/session-entry-persistence.test.ts new file mode 100644 index 000000000000..9ba7ecab5ce4 --- /dev/null +++ b/src/auto-reply/reply/session-entry-persistence.test.ts @@ -0,0 +1,159 @@ +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; +import { + clearSessionStoreCacheForTest, + loadSessionStore, + saveSessionStore, +} from "../../config/sessions/store.js"; +import type { SessionEntry } from "../../config/sessions/types.js"; +import { persistReplySessionEntry } from "./session-entry-persistence.js"; + +const tempDirs = useAutoCleanupTempDirTracker(afterEach); + +describe("persistReplySessionEntry", () => { + it("does not restore policy fields revoked during reply processing", async () => { + const dir = tempDirs.make("openclaw-reply-session-store-"); + try { + const storePath = path.join(dir, "sessions.json"); + const initialEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 100, + thinkingLevel: "low", + elevatedLevel: "full", + inheritedToolAllow: ["exec"], + sendPolicy: "allow", + }; + const currentEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 400, + thinkingLevel: "low", + sendPolicy: "deny", + }; + await saveSessionStore(storePath, { main: currentEntry }, { skipMaintenance: true }); + + const result = await persistReplySessionEntry({ + storePath, + sessionKey: "main", + initialEntry, + entry: { + ...initialEntry, + thinkingLevel: "high", + updatedAt: 250, + }, + }); + + expect(result.status).toBe("current"); + if (result.status !== "current") { + throw new Error("expected current persisted session"); + } + expect(result.entry).toMatchObject({ + sessionId: "session-1", + thinkingLevel: "high", + sendPolicy: "deny", + updatedAt: 400, + }); + expect(result.entry.elevatedLevel).toBeUndefined(); + expect(result.entry.inheritedToolAllow).toBeUndefined(); + expect(loadSessionStore(storePath, { skipCache: true }).main).toEqual(result.entry); + } finally { + clearSessionStoreCacheForTest(); + } + }); + + it("rejects persistence when the session rotated", async () => { + const dir = tempDirs.make("openclaw-reply-session-store-"); + try { + const storePath = path.join(dir, "sessions.json"); + const initialEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 100, + thinkingLevel: "low", + }; + const currentEntry: SessionEntry = { + sessionId: "session-2", + updatedAt: 400, + thinkingLevel: "medium", + }; + await saveSessionStore(storePath, { main: currentEntry }, { skipMaintenance: true }); + + const result = await persistReplySessionEntry({ + storePath, + sessionKey: "main", + initialEntry, + entry: { ...initialEntry, thinkingLevel: "high", updatedAt: 250 }, + }); + + expect(result).toEqual({ + status: "lifecycle-invalidated", + error: 'Session "main" changed while starting work. Retry.', + entry: currentEntry, + }); + expect(loadSessionStore(storePath, { skipCache: true }).main).toEqual(currentEntry); + } finally { + clearSessionStoreCacheForTest(); + } + }); + + it("does not recreate a row deleted after reply initialization by default", async () => { + const dir = tempDirs.make("openclaw-reply-session-store-"); + try { + const storePath = path.join(dir, "sessions.json"); + const initialEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 100, + }; + await saveSessionStore(storePath, {}, { skipMaintenance: true }); + + const result = await persistReplySessionEntry({ + storePath, + sessionKey: "main", + initialEntry, + entry: { ...initialEntry, updatedAt: 250 }, + }); + + expect(result).toEqual({ + status: "lifecycle-invalidated", + error: 'Session "main" was deleted while starting work. Retry.', + }); + expect(loadSessionStore(storePath, { skipCache: true }).main).toBeUndefined(); + } finally { + clearSessionStoreCacheForTest(); + } + }); + + it("rejects same-value persistence after the session is archived", async () => { + const dir = tempDirs.make("openclaw-reply-session-store-"); + try { + const storePath = path.join(dir, "sessions.json"); + const initialEntry: SessionEntry = { + sessionId: "session-1", + updatedAt: 100, + modelOverride: "gpt-5.5", + }; + const archivedEntry: SessionEntry = { + ...initialEntry, + updatedAt: 400, + archivedAt: 300, + }; + await saveSessionStore(storePath, { main: archivedEntry }, { skipMaintenance: true }); + + const result = await persistReplySessionEntry({ + storePath, + sessionKey: "main", + initialEntry, + entry: { ...initialEntry, updatedAt: 250 }, + touchedFields: ["modelOverride"], + }); + + expect(result).toEqual({ + status: "lifecycle-invalidated", + error: 'Session "main" is archived. Restore it before starting new work.', + entry: archivedEntry, + }); + expect(loadSessionStore(storePath, { skipCache: true }).main).toEqual(archivedEntry); + } finally { + clearSessionStoreCacheForTest(); + } + }); +}); diff --git a/src/auto-reply/reply/session-entry-persistence.ts b/src/auto-reply/reply/session-entry-persistence.ts new file mode 100644 index 000000000000..c5019256ec87 --- /dev/null +++ b/src/auto-reply/reply/session-entry-persistence.ts @@ -0,0 +1,89 @@ +// Atomic persistence for broad auto-reply session snapshots. +import type { SessionEntry } from "../../config/sessions.js"; +import { resolveSessionWorkStartError } from "../../config/sessions/lifecycle.js"; +import { patchSessionEntry } from "../../config/sessions/session-accessor.js"; +import { + mergeSessionSnapshotChanges, + sessionSnapshotTouchedFieldsConflict, +} from "../../config/sessions/session-snapshot-merge.js"; + +type PersistReplySessionEntryParams = { + allowCreate?: boolean; + entry: SessionEntry; + initialEntry: SessionEntry; + reassertLiveModelSwitchPending?: boolean; + sessionKey: string; + skipMaintenance?: boolean; + storePath: string; + touchedFields?: ReadonlyArray; +}; + +export type PersistReplySessionEntryResult = + | { status: "current"; entry: SessionEntry } + | { status: "lifecycle-invalidated"; error: string; entry?: SessionEntry }; + +/** Persists reply-owned state without reverting concurrent session management. */ +export async function persistReplySessionEntry( + params: PersistReplySessionEntryParams, +): Promise { + let lifecycleError: string | undefined; + let lifecycleEntry: SessionEntry | undefined; + const persisted = await patchSessionEntry( + { sessionKey: params.sessionKey, storePath: params.storePath }, + (_entry, context) => { + if (!context.existingEntry) { + if (params.allowCreate !== true) { + lifecycleError = resolveSessionWorkStartError(params.sessionKey, undefined, { + expectedSessionId: params.initialEntry.sessionId, + }); + return null; + } + return params.entry; + } + lifecycleError = resolveSessionWorkStartError(params.sessionKey, context.existingEntry, { + expectedSessionId: params.initialEntry.sessionId, + }); + if (lifecycleError) { + lifecycleEntry = context.existingEntry; + return null; + } + if ( + sessionSnapshotTouchedFieldsConflict({ + initial: params.initialEntry, + next: params.entry, + current: context.existingEntry, + touchedFields: params.touchedFields, + }) + ) { + return null; + } + // Reply flows persist broad snapshots. Project only reply-owned changes + // so concurrent lifecycle, policy, and privacy updates remain authoritative. + return mergeSessionSnapshotChanges({ + initial: params.initialEntry, + next: params.entry, + current: context.existingEntry, + reassertLiveModelSwitchPending: params.reassertLiveModelSwitchPending, + }); + }, + { + fallbackEntry: params.entry, + replaceEntry: true, + skipMaintenance: params.skipMaintenance, + }, + ); + if (lifecycleError) { + return { + status: "lifecycle-invalidated", + error: lifecycleError, + ...(lifecycleEntry ? { entry: lifecycleEntry } : {}), + }; + } + if (!persisted) { + return { + status: "lifecycle-invalidated", + error: `Session "${params.sessionKey}" changed while starting work. Retry.`, + }; + } + return { status: "current", entry: persisted }; +} diff --git a/src/auto-reply/reply/session-reset-model.test.ts b/src/auto-reply/reply/session-reset-model.test.ts index 791598279c39..06b1efe1b74e 100644 --- a/src/auto-reply/reply/session-reset-model.test.ts +++ b/src/auto-reply/reply/session-reset-model.test.ts @@ -1,8 +1,16 @@ // Tests reset model selection and persisted model override cleanup. +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import type { ModelCatalogEntry } from "../../agents/model-catalog.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; +import { + clearSessionStoreCacheForTest, + loadSessionStore, + saveSessionStore, +} from "../../config/sessions/store.js"; import type { ModelAliasIndex } from "./model-selection-directive.js"; import { applyResetModelOverride } from "./session-reset-model.js"; @@ -77,6 +85,155 @@ describe("applyResetModelOverride", () => { expect(sessionEntry.authProfileOverrideCompactionCount).toBeUndefined(); }); + it("adopts a concurrent model winner instead of acknowledging the reset hint", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-reset-model-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const fixture = createResetFixture(); + const concurrentEntry: SessionEntry = { + ...fixture.sessionEntry, + updatedAt: fixture.sessionEntry.updatedAt + 1, + providerOverride: "openai", + modelOverride: "gpt-4o-mini", + modelOverrideSource: "user", + }; + await saveSessionStore( + storePath, + { "agent:main:dm:1": concurrentEntry }, + { skipMaintenance: true }, + ); + + try { + const result = await applyResetModelOverride({ + cfg: fixture.cfg, + resetTriggered: true, + bodyStripped: "minimax summarize", + sessionCtx: fixture.sessionCtx, + ctx: fixture.ctx, + sessionEntry: fixture.sessionEntry, + sessionStore: fixture.sessionStore, + sessionKey: "agent:main:dm:1", + storePath, + defaultProvider: "openai", + defaultModel: "gpt-4o-mini", + aliasIndex: fixture.aliasIndex, + modelCatalog, + }); + + expect(result.selection).toBeUndefined(); + expect(result.cleanedBody).toBe("summarize"); + expect(fixture.sessionEntry).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-4o-mini", + modelOverrideSource: "user", + }); + expect(fixture.sessionEntry.updatedAt).toBeGreaterThanOrEqual(concurrentEntry.updatedAt); + expect(fixture.sessionStore["agent:main:dm:1"]).toEqual(fixture.sessionEntry); + expect(loadSessionStore(storePath, { skipCache: true })["agent:main:dm:1"]).toEqual( + fixture.sessionEntry, + ); + } finally { + clearSessionStoreCacheForTest(); + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("checks the persisted winner for an explicit same-value reset hint", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-reset-model-race-")); + const storePath = path.join(tempRoot, "sessions.json"); + const fixture = createResetFixture({ + providerOverride: "minimax", + modelOverride: "m2.7", + modelOverrideSource: "user", + }); + const concurrentEntry: SessionEntry = { + ...fixture.sessionEntry, + updatedAt: fixture.sessionEntry.updatedAt + 1, + providerOverride: "openai", + modelOverride: "gpt-4o-mini", + }; + await saveSessionStore( + storePath, + { "agent:main:dm:1": concurrentEntry }, + { skipMaintenance: true }, + ); + + try { + const result = await applyResetModelOverride({ + cfg: fixture.cfg, + resetTriggered: true, + bodyStripped: "minimax summarize", + sessionCtx: fixture.sessionCtx, + ctx: fixture.ctx, + sessionEntry: fixture.sessionEntry, + sessionStore: fixture.sessionStore, + sessionKey: "agent:main:dm:1", + storePath, + defaultProvider: "openai", + defaultModel: "gpt-4o-mini", + aliasIndex: fixture.aliasIndex, + modelCatalog, + }); + + expect(result.selection).toBeUndefined(); + expect(fixture.sessionEntry).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-4o-mini", + }); + expect(fixture.sessionStore["agent:main:dm:1"]).toEqual(fixture.sessionEntry); + } finally { + clearSessionStoreCacheForTest(); + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("rejects a reset-model hint when the session rotates during persistence", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-reset-model-rotation-")); + const storePath = path.join(tempRoot, "sessions.json"); + const fixture = createResetFixture(); + const rotatedEntry: SessionEntry = { + sessionId: "s2", + updatedAt: fixture.sessionEntry.updatedAt + 1, + providerOverride: "openai", + modelOverride: "gpt-4o-mini", + modelOverrideSource: "user", + }; + await saveSessionStore( + storePath, + { "agent:main:dm:1": rotatedEntry }, + { skipMaintenance: true }, + ); + + try { + await expect( + applyResetModelOverride({ + cfg: fixture.cfg, + resetTriggered: true, + bodyStripped: "minimax summarize", + sessionCtx: fixture.sessionCtx, + ctx: fixture.ctx, + sessionEntry: fixture.sessionEntry, + sessionStore: fixture.sessionStore, + sessionKey: "agent:main:dm:1", + storePath, + defaultProvider: "openai", + defaultModel: "gpt-4o-mini", + aliasIndex: fixture.aliasIndex, + modelCatalog, + }), + ).rejects.toThrow(/changed while starting work/i); + + expect(fixture.sessionEntry.sessionId).toBe("s1"); + expect(fixture.sessionEntry.modelOverride).toBeUndefined(); + expect(fixture.sessionStore["agent:main:dm:1"]).toBe(fixture.sessionEntry); + expect(loadSessionStore(storePath, { skipCache: true })["agent:main:dm:1"]).toEqual( + rotatedEntry, + ); + } finally { + clearSessionStoreCacheForTest(); + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + it("skips when resetTriggered is false", async () => { const { sessionEntry, sessionCtx } = await applyResetFixture({ resetTriggered: false, diff --git a/src/auto-reply/reply/session-reset-model.ts b/src/auto-reply/reply/session-reset-model.ts index f1b8812765a8..c3b599c26bb0 100644 --- a/src/auto-reply/reply/session-reset-model.ts +++ b/src/auto-reply/reply/session-reset-model.ts @@ -8,6 +8,12 @@ import { } from "../../agents/model-selection-shared.js"; import { resolveAgentModelFallbackValues } from "../../config/model-input.js"; import type { SessionEntry } from "../../config/sessions.js"; +import { SessionWorkStartInvalidatedError } from "../../config/sessions/lifecycle.js"; +import { + adoptPersistedSessionSnapshot, + SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS, + sessionModelOverrideChangesApplied, +} from "../../config/sessions/session-snapshot-merge.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js"; import type { MsgContext, TemplateContext } from "../templating.js"; @@ -107,40 +113,54 @@ function buildSelectionFromExplicit(params: { }; } -function applySelectionToSession(params: { +async function applySelectionToSession(params: { selection: ModelDirectiveSelection; sessionEntry?: SessionEntry; sessionEntryHandle?: ReplySessionEntryHandle; sessionStore?: Record; sessionKey?: string; storePath?: string; -}) { +}): Promise { const { selection, sessionEntryHandle, sessionStore, sessionKey, storePath } = params; const sessionEntry = sessionEntryHandle?.getCurrent() ?? params.sessionEntry; if (!sessionEntry || !sessionKey) { - return; + return true; } - const { updated } = applyModelOverrideToSessionEntry({ - entry: sessionEntry, + const initialSessionEntry = { ...sessionEntry }; + const nextSessionEntry = { ...sessionEntry }; + applyModelOverrideToSessionEntry({ + entry: nextSessionEntry, selection, }); - if (!updated) { - return; + let appliedEntry = nextSessionEntry; + let selectionApplied = true; + if (storePath) { + const { persistReplySessionEntry } = await import("./session-entry-persistence.js"); + const persistence = await persistReplySessionEntry({ + storePath, + sessionKey, + initialEntry: initialSessionEntry, + entry: nextSessionEntry, + touchedFields: SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS, + }); + if (persistence.status === "lifecycle-invalidated") { + throw new SessionWorkStartInvalidatedError(persistence.error); + } + const persistedEntry = persistence.entry; + appliedEntry = persistedEntry; + selectionApplied = sessionModelOverrideChangesApplied({ + initial: initialSessionEntry, + next: nextSessionEntry, + current: persistedEntry, + }); } + adoptPersistedSessionSnapshot(sessionEntry, appliedEntry); if (sessionEntryHandle) { sessionEntryHandle.replaceCurrent(sessionEntry); } else if (sessionStore) { sessionStore[sessionKey] = sessionEntry; } - if (storePath) { - void import("../../config/sessions/session-accessor.js") - .then(({ replaceSessionEntry }) => - replaceSessionEntry({ storePath, sessionKey }, sessionEntry), - ) - .catch(() => { - // Ignore persistence errors; session still proceeds. - }); - } + return selectionApplied; } /** Applies a model override embedded in a reset command body. */ @@ -253,7 +273,7 @@ export async function applyResetModelOverride(params: { params.sessionCtx.BodyStripped = cleanedBody; params.sessionCtx.BodyForCommands = cleanedBody; - applySelectionToSession({ + const selectionApplied = await applySelectionToSession({ selection, sessionEntry: params.sessionEntry, sessionEntryHandle: params.sessionEntryHandle, @@ -262,5 +282,5 @@ export async function applyResetModelOverride(params: { storePath: params.storePath, }); - return { selection, cleanedBody }; + return { selection: selectionApplied ? selection : undefined, cleanedBody }; } diff --git a/src/auto-reply/reply/session-updates.test.ts b/src/auto-reply/reply/session-updates.test.ts index 69fd6e207a08..88bf675f6791 100644 --- a/src/auto-reply/reply/session-updates.test.ts +++ b/src/auto-reply/reply/session-updates.test.ts @@ -1,5 +1,9 @@ // Tests session update fanout and persisted lifecycle records. +import fs from "node:fs/promises"; +import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; +import type { SessionEntry } from "../../config/sessions/types.js"; const TEST_WORKSPACE_DIR = "/tmp/workspace"; @@ -66,6 +70,7 @@ vi.mock("../../routing/session-key.js", () => ({ })); const { ensureSkillSnapshot } = await import("./session-updates.js"); +const tempDirs = useAutoCleanupTempDirTracker(afterEach); describe("ensureSkillSnapshot", () => { beforeEach(() => { @@ -116,4 +121,57 @@ describe("ensureSkillSnapshot", () => { expect(snapshotParams.agentId).toBe("writer"); expect(resolveAgentIdFromSessionKeyMock).not.toHaveBeenCalled(); }); + + it("keeps a concurrent rename and unpin while persisting a skill snapshot", async () => { + vi.stubEnv("OPENCLAW_TEST_FAST", "0"); + const root = tempDirs.make("openclaw-session-updates-"); + const storePath = path.join(root, "sessions.json"); + const sessionKey = "agent:main:reply"; + const staleEntry: SessionEntry = { + sessionId: "reply-session", + updatedAt: 1, + label: "Before rename", + pinnedAt: 100, + }; + await fs.writeFile( + storePath, + JSON.stringify({ + [sessionKey]: { + sessionId: staleEntry.sessionId, + updatedAt: 2, + label: "After rename", + sendPolicy: "deny", + }, + }), + "utf8", + ); + const sessionStore = { [sessionKey]: staleEntry }; + + const result = await ensureSkillSnapshot({ + sessionEntry: staleEntry, + sessionStore, + sessionKey, + storePath, + isFirstTurnInSession: true, + workspaceDir: TEST_WORKSPACE_DIR, + cfg: {}, + }); + + expect(result.sessionEntry).toMatchObject({ + sessionId: "reply-session", + label: "After rename", + sendPolicy: "deny", + systemSent: true, + }); + expect(result.sessionEntry?.pinnedAt).toBeUndefined(); + expect(sessionStore[sessionKey]).toBe(result.sessionEntry); + const persisted = JSON.parse(await fs.readFile(storePath, "utf8")) as Record< + string, + SessionEntry + >; + expect(persisted[sessionKey]?.label).toBe("After rename"); + expect(persisted[sessionKey]?.pinnedAt).toBeUndefined(); + expect(persisted[sessionKey]?.sendPolicy).toBe("deny"); + expect(persisted[sessionKey]?.skillsSnapshot).toBeDefined(); + }); }); diff --git a/src/auto-reply/reply/session-updates.ts b/src/auto-reply/reply/session-updates.ts index 662d313366e5..45b59869dbb2 100644 --- a/src/auto-reply/reply/session-updates.ts +++ b/src/auto-reply/reply/session-updates.ts @@ -4,7 +4,7 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { canExecRequestNode } from "../../agents/exec-defaults.js"; import { resolveCompactionSessionFile, type SessionEntry } from "../../config/sessions.js"; -import { patchSessionEntry, upsertSessionEntry } from "../../config/sessions/session-accessor.js"; +import { patchSessionEntry } from "../../config/sessions/session-accessor.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { forgetActiveSessionForShutdown, @@ -27,27 +27,32 @@ async function persistSessionEntryUpdate(params: { sessionKey?: string; storePath?: string; nextEntry: SessionEntry; -}) { - if (params.sessionEntryHandle) { - params.sessionEntryHandle.replaceCurrent(params.nextEntry); - } else if (params.sessionStore && params.sessionKey) { - params.sessionStore[params.sessionKey] = { - ...params.sessionStore[params.sessionKey], - ...params.nextEntry, - }; - } else { - return; + updates: Partial; +}): Promise { + if (!params.sessionEntryHandle && (!params.sessionStore || !params.sessionKey)) { + return params.nextEntry; } + let persistedEntry = params.nextEntry; if (!params.storePath || !params.sessionKey) { - return; + if (params.sessionEntryHandle) { + params.sessionEntryHandle.replaceCurrent(persistedEntry); + } else if (params.sessionStore && params.sessionKey) { + params.sessionStore[params.sessionKey] = persistedEntry; + } + return persistedEntry; } - await upsertSessionEntry( - { - storePath: params.storePath, - sessionKey: params.sessionKey, - }, - params.nextEntry, - ); + persistedEntry = + (await patchSessionEntry( + { storePath: params.storePath, sessionKey: params.sessionKey }, + () => params.updates, + { fallbackEntry: params.nextEntry }, + )) ?? persistedEntry; + if (params.sessionEntryHandle) { + params.sessionEntryHandle.replaceCurrent(persistedEntry); + } else if (params.sessionStore) { + params.sessionStore[params.sessionKey] = persistedEntry; + } + return persistedEntry; } function emitCompactionSessionLifecycleHooks(params: { @@ -198,12 +203,18 @@ export async function ensureSkillSnapshot(params: { systemSent: true, skillsSnapshot: skillSnapshot, }; - await persistSessionEntryUpdate({ + nextEntry = await persistSessionEntryUpdate({ sessionEntryHandle, sessionStore, sessionKey, storePath, nextEntry, + updates: { + sessionId: nextEntry.sessionId, + updatedAt: nextEntry.updatedAt, + systemSent: nextEntry.systemSent, + skillsSnapshot: nextEntry.skillsSnapshot, + }, }); systemSent = true; } @@ -234,12 +245,17 @@ export async function ensureSkillSnapshot(params: { updatedAt: Date.now(), skillsSnapshot, }; - await persistSessionEntryUpdate({ + nextEntry = await persistSessionEntryUpdate({ sessionEntryHandle, sessionStore, sessionKey, storePath, nextEntry, + updates: { + sessionId: nextEntry.sessionId, + updatedAt: nextEntry.updatedAt, + skillsSnapshot: nextEntry.skillsSnapshot, + }, }); } diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index ccdbb5c3ba68..521836046ff1 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -24,6 +24,11 @@ import { resetSystemEventsForTest, } from "../../infra/system-events.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js"; +import { + beginSessionWorkAdmission, + isSessionLifecycleMutationActive, + runExclusiveSessionLifecycleMutation, +} from "../../sessions/session-lifecycle-admission.js"; import { createChannelTestPluginBase, createTestRegistry, @@ -472,6 +477,31 @@ afterEach(async () => { await sessionMcpTesting.resetSessionMcpRuntimeManager(); }); describe("initSessionState guarded initialization", () => { + it("rejects inbound work for an archived session", async () => { + const storePath = await createStorePath("openclaw-session-init-archived-"); + const sessionKey = "agent:main:telegram:chat:archived"; + await writeSessionStoreFast(storePath, { + [sessionKey]: { + sessionId: "archived-session", + archivedAt: Date.now(), + updatedAt: 100, + }, + }); + + await expect( + initSessionState({ + ctx: { + Body: "blocked while archived", + SessionKey: sessionKey, + }, + cfg: { session: { store: storePath } } as OpenClawConfig, + commandAuthorized: true, + }), + ).rejects.toThrow( + 'Session "agent:main:telegram:chat:archived" is archived. Restore it before starting new work.', + ); + }); + it("serializes concurrent initializers before reading the guarded snapshot", async () => { const storePath = await createStorePath("openclaw-session-init-race-"); const sessionKey = "agent:main:telegram:chat:42"; @@ -1124,7 +1154,7 @@ describe("initSessionState RawBody", () => { expect(store[sessionKey]?.modelOverrideSource).toBe("user"); }); - it("preserves user-set behavior overrides across an implicit daily stale rollover (#92562)", async () => { + it("preserves user-set behavior and pinned state across an implicit daily stale rollover (#92562)", async () => { // Regression: session-level behavior overrides (/think, /verbose, /reasoning, // /trace, ttsAuto) survive an explicit /new but were dropped after the // automatic daily/idle reset, because the carryover was gated on @@ -1151,6 +1181,7 @@ describe("initSessionState RawBody", () => { traceLevel: "high", reasoningLevel: "low", ttsAuto: "always", + pinnedAt: 123, }, }); @@ -1179,6 +1210,7 @@ describe("initSessionState RawBody", () => { expect(result.sessionEntry.traceLevel).toBe("high"); expect(result.sessionEntry.reasoningLevel).toBe("low"); expect(result.sessionEntry.ttsAuto).toBe("always"); + expect(result.sessionEntry.pinnedAt).toBe(123); const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< string, @@ -1188,6 +1220,7 @@ describe("initSessionState RawBody", () => { traceLevel?: string; reasoningLevel?: string; ttsAuto?: string; + pinnedAt?: number; } >; expect(store[sessionKey]?.thinkingLevel).toBe("medium"); @@ -1195,6 +1228,7 @@ describe("initSessionState RawBody", () => { expect(store[sessionKey]?.traceLevel).toBe("high"); expect(store[sessionKey]?.reasoningLevel).toBe("low"); expect(store[sessionKey]?.ttsAuto).toBe("always"); + expect(store[sessionKey]?.pinnedAt).toBe(123); }); it("preserves usage footer mode across daily rollover", async () => { @@ -3717,6 +3751,332 @@ describe("initSessionState preserves behavior overrides across /new and /reset", expect(archived).toHaveLength(1); }); + it("drains foreign work before replacing a reply session", async () => { + const storePath = await createStorePath("openclaw-rollover-admission-"); + const sessionKey = "agent:main:telegram:dm:rollover-admission"; + const existingSessionId = "session-before-admitted-rollover"; + const transcriptPath = path.join(path.dirname(storePath), `${existingSessionId}.jsonl`); + await writeSessionStoreFast(storePath, { + [sessionKey]: { sessionId: existingSessionId, updatedAt: Date.now() }, + }); + await fs.writeFile(transcriptPath, '{"type":"message"}\n', "utf8"); + + let signalInterrupted = () => {}; + const interrupted = new Promise((resolve) => { + signalInterrupted = resolve; + }); + const admission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, existingSessionId], + assertAllowed: () => {}, + onInterrupt: signalInterrupted, + }); + const initialization = initSessionState({ + ctx: { + Body: "/new", + RawBody: "/new", + CommandBody: "/new", + From: "user-rollover-admission", + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg: { session: { store: storePath, idleMinutes: 999 } } as OpenClawConfig, + commandAuthorized: true, + }); + + try { + await interrupted; + // Foreign owners may need the writer lane to finalize before releasing. + // The rollover must not hold that lane while it drains them. + await runExclusiveSessionStoreWrite(storePath, async () => {}); + expect(readSessionStoreForTest(storePath)[sessionKey]?.sessionId).toBe( + existingSessionId, + ); + expect(await fs.stat(transcriptPath).catch(() => null)).not.toBeNull(); + + admission.release(); + const result = await initialization; + expect(result.sessionId).not.toBe(existingSessionId); + expect(await fs.stat(transcriptPath).catch(() => null)).toBeNull(); + } finally { + admission.release(); + await initialization.catch(() => {}); + } + }); + + it("keeps the initiating reply admission during an in-band rollover", async () => { + const storePath = await createStorePath("openclaw-rollover-initiator-"); + const sessionKey = "agent:main:telegram:dm:rollover-initiator"; + const existingSessionId = "session-before-initiating-rollover"; + await writeSessionStoreFast(storePath, { + [sessionKey]: { sessionId: existingSessionId, updatedAt: Date.now() }, + }); + const onInterrupt = vi.fn(); + const admission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, existingSessionId], + assertAllowed: () => {}, + onInterrupt, + }); + + try { + const result = await admission.run( + async () => + await initSessionState({ + ctx: { + Body: "/reset", + RawBody: "/reset", + CommandBody: "/reset", + From: "user-rollover-initiator", + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg: { session: { store: storePath, idleMinutes: 999 } } as OpenClawConfig, + commandAuthorized: true, + }), + ); + expect(result.sessionId).not.toBe(existingSessionId); + expect(onInterrupt).not.toHaveBeenCalled(); + } finally { + admission.release(); + } + }); + + it("cancels a competing admitted rollover without deadlocking the session", async () => { + const storePath = await createStorePath("openclaw-rollover-contenders-"); + const sessionKey = "agent:main:telegram:dm:rollover-contenders"; + const existingSessionId = "session-before-competing-rollovers"; + await writeSessionStoreFast(storePath, { + [sessionKey]: { sessionId: existingSessionId, updatedAt: Date.now() }, + }); + const controllers = [new AbortController(), new AbortController()]; + const admissions = await Promise.all( + controllers.map( + async (controller) => + await beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, existingSessionId], + assertAllowed: () => {}, + onInterrupt: () => controller.abort(new Error("competing rollover interrupted")), + }), + ), + ); + const runRollover = async (index: number) => { + const admission = admissions[index]; + const controller = controllers[index]; + try { + return await admission.run( + async () => + await initSessionState({ + ctx: { + Body: "/new", + RawBody: "/new", + CommandBody: "/new", + From: `user-rollover-contender-${index}`, + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg: { session: { store: storePath, idleMinutes: 999 } } as OpenClawConfig, + commandAuthorized: true, + signal: controller.signal, + }), + ); + } finally { + admission.release(); + } + }; + + const outcomes = await Promise.allSettled([runRollover(0), runRollover(1)]); + expect(outcomes.filter((outcome) => outcome.status === "fulfilled")).toHaveLength(1); + expect(outcomes.filter((outcome) => outcome.status === "rejected")).toHaveLength(1); + expect(readSessionStoreForTest(storePath)[sessionKey]?.sessionId).not.toBe( + existingSessionId, + ); + }); + + it.each([ + { + name: "reuses a fresh replacement without interrupting its work", + body: "continue", + expectedInterruption: false, + }, + { + name: "reacquires a changed identity before an explicit reset", + body: "/new", + expectedInterruption: true, + }, + ])("$name", async ({ body, expectedInterruption }) => { + const storePath = await createStorePath("openclaw-rollover-revalidation-"); + const sessionKey = "agent:main:telegram:dm:rollover-revalidation"; + const staleSessionId = "stale-session-before-revalidation"; + const replacementSessionId = "fresh-replacement-before-revalidation"; + const postDrainSessionId = "replacement-created-during-drain"; + const finalGapSessionId = "replacement-created-before-destructive-write"; + await writeSessionStoreFast(storePath, { + [sessionKey]: { + sessionId: staleSessionId, + updatedAt: Date.now() - 10 * 60_000, + }, + }); + + let replacementInterrupted = false; + let replacementIdentityFenced = false; + let postDrainIdentityFenced = false; + let finalGapIdentityFenced = false; + let postDrainAdmission: Awaited> | undefined; + let finalGapAdmission: Awaited> | undefined; + let postDrainFinalization = Promise.resolve(); + let finalGapRebind = Promise.resolve(); + let releaseReplacementAdmission = () => {}; + const replacementAdmission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [replacementSessionId], + assertAllowed: () => {}, + onInterrupt: () => { + replacementInterrupted = true; + replacementIdentityFenced = isSessionLifecycleMutationActive(storePath, [ + replacementSessionId, + ]); + if (!expectedInterruption) { + releaseReplacementAdmission(); + return; + } + postDrainFinalization = runExclusiveSessionStoreWrite(storePath, async () => { + await writeSessionStoreFast(storePath, { + [sessionKey]: { sessionId: postDrainSessionId, updatedAt: Date.now() }, + }); + }) + .then(async () => { + let releasePostDrainAdmission = () => {}; + postDrainAdmission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [postDrainSessionId], + assertAllowed: () => {}, + onInterrupt: () => { + postDrainIdentityFenced = isSessionLifecycleMutationActive(storePath, [ + postDrainSessionId, + ]); + releasePostDrainAdmission(); + finalGapRebind = runExclusiveSessionStoreWrite(storePath, async () => { + await writeSessionStoreFast(storePath, { + [sessionKey]: { sessionId: finalGapSessionId, updatedAt: Date.now() }, + }); + let releaseFinalGapAdmission = () => {}; + finalGapAdmission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [finalGapSessionId], + assertAllowed: () => {}, + onInterrupt: () => { + finalGapIdentityFenced = isSessionLifecycleMutationActive(storePath, [ + finalGapSessionId, + ]); + releaseFinalGapAdmission(); + }, + }); + releaseFinalGapAdmission = finalGapAdmission.release; + }); + }, + }); + releasePostDrainAdmission = postDrainAdmission.release; + }) + .finally(releaseReplacementAdmission); + }, + }); + releaseReplacementAdmission = replacementAdmission.release; + + let signalMutationStarted = () => {}; + const mutationStarted = new Promise((resolve) => { + signalMutationStarted = resolve; + }); + let releaseMutation = () => {}; + const mutationGate = new Promise((resolve) => { + releaseMutation = resolve; + }); + const blockingMutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, staleSessionId], + run: async () => { + signalMutationStarted(); + await mutationGate; + }, + }); + await mutationStarted; + + let signalWriterStarted = () => {}; + const writerStarted = new Promise((resolve) => { + signalWriterStarted = resolve; + }); + let releaseWriter = () => {}; + const writerGate = new Promise((resolve) => { + releaseWriter = resolve; + }); + const blockingWriter = runExclusiveSessionStoreWrite(storePath, async () => { + signalWriterStarted(); + await writerGate; + }); + await writerStarted; + + const initialization = initSessionState({ + ctx: { + Body: body, + RawBody: body, + CommandBody: body, + From: "user-rollover-revalidation", + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg: { session: { store: storePath, idleMinutes: 1 } } as OpenClawConfig, + commandAuthorized: true, + }); + const replaceSession = runExclusiveSessionStoreWrite(storePath, async () => { + await writeSessionStoreFast(storePath, { + [sessionKey]: { sessionId: replacementSessionId, updatedAt: Date.now() }, + }); + }); + + try { + releaseWriter(); + await blockingWriter; + await replaceSession; + releaseMutation(); + await blockingMutation; + + const result = await initialization; + await postDrainFinalization; + await finalGapRebind; + expect(replacementInterrupted).toBe(expectedInterruption); + if (expectedInterruption) { + expect(replacementIdentityFenced).toBe(true); + expect(postDrainIdentityFenced).toBe(true); + expect(finalGapIdentityFenced).toBe(true); + expect(result.sessionId).not.toBe(finalGapSessionId); + } else { + expect(result.sessionId).toBe(replacementSessionId); + } + } finally { + releaseWriter(); + releaseMutation(); + replacementAdmission.release(); + postDrainAdmission?.release(); + finalGapAdmission?.release(); + await Promise.allSettled([blockingWriter, replaceSession, blockingMutation, initialization]); + await postDrainFinalization.catch(() => {}); + await finalGapRebind.catch(() => {}); + } + }); + it("archives the old session transcript on daily/scheduled reset (stale session)", async () => { // Daily resets occur when the session becomes stale (not via /new or /reset command). // Previously, previousSessionEntry was only set when resetTriggered=true, leaving diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 27ab5a3de5a5..6776793af97a 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -16,6 +16,7 @@ import { resolveGroupSessionKey } from "../../config/sessions/group.js"; import { hasTerminalMainSessionTranscriptNewerThanRegistry, resolveSessionLifecycleTimestamps, + resolveSessionWorkStartError, } from "../../config/sessions/lifecycle.js"; import { canonicalizeMainSessionAlias } from "../../config/sessions/main-session.js"; import { deriveSessionMetaPatch } from "../../config/sessions/metadata.js"; @@ -60,6 +61,11 @@ import { normalizeMainKey, } from "../../routing/session-key.js"; import { isInterSessionInputProvenance } from "../../sessions/input-provenance.js"; +import { + SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS, + interruptSessionWorkAdmissions, + runExclusiveSessionLifecycleMutation, +} from "../../sessions/session-lifecycle-admission.js"; import { normalizeDeliveryChannelRoute, normalizeSessionDeliveryFields, @@ -178,6 +184,7 @@ function recoverTerminalSessionEntryForVisibleTurn(entry: SessionEntry): Session export type SessionInitResult = { sessionCtx: TemplateContext; sessionEntry: SessionEntry; + initialSessionEntry?: SessionEntry; previousSessionEntry?: SessionEntry; sessionEntryHandle: ReplySessionEntryHandle; sessionStore: Record; @@ -201,6 +208,7 @@ export type InitSessionStateParams = { ctx: MsgContext; requestedSessionId?: string; resumeRequestedSession?: boolean; + signal?: AbortSignal; }; type InitSessionStateAttemptContext = { @@ -211,6 +219,10 @@ type InitSessionStateAttemptContext = { storePath: string; }; +type InitSessionStateAttemptOutcome = + | { kind: "complete"; result: SessionInitResult } + | { kind: "lifecycle-mutation"; sessionId: string; sessionKey: string }; + function resolveSessionConversationBindingContext( cfg: OpenClawConfig, ctx: MsgContext, @@ -319,17 +331,77 @@ async function initSessionStateAttempt( const attemptContext = resolveInitSessionStateAttemptContext(params); // Guarded revision checks only serialize correctly when the snapshot and // commit share the same writer lane. - return await runExclusiveSessionStoreWrite( + const attempt = await runExclusiveSessionStoreWrite( attemptContext.storePath, - async () => await initSessionStateAttemptLocked(params, attemptContext, staleSnapshotRetried), + async () => + await initSessionStateAttemptLocked(params, attemptContext, staleSnapshotRetried, undefined), ); + if (attempt.kind === "complete") { + return attempt.result; + } + + let rollover = attempt; + while (true) { + const candidate = rollover; + const identities = [candidate.sessionKey, candidate.sessionId]; + let preparedOutcome: InitSessionStateAttemptOutcome | undefined; + // Drain foreign owners before the rollover takes the writer lane. Holding + // that lane while waiting would deadlock owners that release after a write. + const outcome = await runExclusiveSessionLifecycleMutation({ + scope: attemptContext.storePath, + identities, + signal: params.signal, + prepare: async () => { + // A queued rollover may change identity or become obsolete. Recheck + // before interrupting, then reacquire any refreshed identity first. + const revalidated = await runExclusiveSessionStoreWrite( + attemptContext.storePath, + async () => await initSessionStateAttemptLocked(params, attemptContext, false, undefined), + ); + if ( + revalidated.kind === "complete" || + revalidated.sessionKey !== candidate.sessionKey || + revalidated.sessionId !== candidate.sessionId + ) { + preparedOutcome = revalidated; + return; + } + const drained = await interruptSessionWorkAdmissions({ + scope: attemptContext.storePath, + identities, + timeoutMs: SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS, + }); + if (!drained) { + throw new Error( + `timed out draining work before reply session rollover: ${candidate.sessionKey}`, + ); + } + }, + run: async () => { + if (preparedOutcome) { + return preparedOutcome; + } + // Interrupted owners can rebind while draining. The locked attempt + // must match this exact fenced identity before any rollover side effect. + return await runExclusiveSessionStoreWrite( + attemptContext.storePath, + async () => await initSessionStateAttemptLocked(params, attemptContext, false, candidate), + ); + }, + }); + if (outcome.kind === "complete") { + return outcome.result; + } + rollover = outcome; + } } async function initSessionStateAttemptLocked( params: InitSessionStateParams, attemptContext: InitSessionStateAttemptContext, staleSnapshotRetried: boolean, -): Promise { + lifecycleMutationIdentity: { sessionId: string; sessionKey: string } | undefined, +): Promise { const { ctx, cfg, commandAuthorized } = params; const { agentId, conversationBindingContext, isSystemEvent, sessionCtxForState, storePath } = attemptContext; @@ -477,6 +549,10 @@ async function initSessionStateAttemptLocked( ctx, }); const entry = initializationSnapshot.currentEntry; + const archivedSessionError = resolveSessionWorkStartError(sessionKey, entry); + if (archivedSessionError) { + throw new Error(archivedSessionError); + } const now = Date.now(); const isThread = resolveThreadFlag({ sessionKey, @@ -591,6 +667,18 @@ async function initSessionStateAttemptLocked( entry, freshness: entryFreshness, }); + const lifecycleMutationMatches = Boolean( + previousSessionEntry && + lifecycleMutationIdentity?.sessionKey === sessionKey && + lifecycleMutationIdentity.sessionId === previousSessionEntry.sessionId, + ); + if (previousSessionEntry && !lifecycleMutationMatches) { + return { + kind: "lifecycle-mutation", + sessionId: previousSessionEntry.sessionId, + sessionKey, + }; + } clearBootstrapSnapshotOnSessionRollover({ sessionKey, previousSessionId: previousSessionEntry?.sessionId, @@ -774,6 +862,7 @@ async function initSessionStateAttemptLocked( reasoningLevel: persistedReasoning ?? baseEntry?.reasoningLevel, ttsAuto: persistedTtsAuto ?? baseEntry?.ttsAuto, responseUsage: persistedResponseUsage ?? baseEntry?.responseUsage, + pinnedAt: entry?.pinnedAt, usageFamilyKey, usageFamilySessionIds, modelOverride: persistedModelOverride ?? baseEntry?.modelOverride, @@ -932,7 +1021,7 @@ async function initSessionStateAttemptLocked( }); if (!committed.ok) { if (!staleSnapshotRetried) { - return await initSessionStateAttemptLocked(params, attemptContext, true); + return await initSessionStateAttemptLocked(params, attemptContext, true, undefined); } throw new Error(`reply session initialization conflicted for ${sessionKey}`); } @@ -1038,22 +1127,25 @@ async function initSessionStateAttemptLocked( } return { - sessionCtx, - sessionEntry, - sessionEntryHandle, - previousSessionEntry, - sessionStore, - sessionKey, - sessionId: sessionId ?? crypto.randomUUID(), - isNewSession, - resetTriggered, - systemSent, - abortedLastRun, - storePath, - sessionScope, - groupResolution, - isGroup, - bodyStripped, - triggerBodyNormalized, + kind: "complete", + result: { + sessionCtx, + sessionEntry, + sessionEntryHandle, + previousSessionEntry, + sessionStore, + sessionKey, + sessionId: sessionId ?? crypto.randomUUID(), + isNewSession, + resetTriggered, + systemSent, + abortedLastRun, + storePath, + sessionScope, + groupResolution, + isGroup, + bodyStripped, + triggerBodyNormalized, + }, }; } diff --git a/src/cli/gateway-cli/call.ts b/src/cli/gateway-cli/call.ts index a6c9d5a8d59f..17587f19ccaa 100644 --- a/src/cli/gateway-cli/call.ts +++ b/src/cli/gateway-cli/call.ts @@ -14,7 +14,7 @@ export type GatewayRpcOpts = { url?: string; token?: string; password?: string; - timeout?: string; + timeout?: string | null; expectFinal?: boolean; json?: boolean; localPortOverride?: number; @@ -32,9 +32,12 @@ export const gatewayCallOpts = (cmd: Command) => .option("--json", "Output JSON", false); export const callGatewayCli = async (method: string, opts: GatewayRpcOpts, params?: unknown) => { - const timeoutMs = parseTimeoutMsWithFallback(opts.timeout, DEFAULT_GATEWAY_RPC_TIMEOUT_MS, { - invalidType: "error", - }); + const timeoutMs = + opts.timeout === null + ? null + : parseTimeoutMsWithFallback(opts.timeout, DEFAULT_GATEWAY_RPC_TIMEOUT_MS, { + invalidType: "error", + }); return await withProgress( { label: `Gateway ${method}`, diff --git a/src/cli/gateway-cli/register.ts b/src/cli/gateway-cli/register.ts index e3d7d0ea6797..6e1fc3789149 100644 --- a/src/cli/gateway-cli/register.ts +++ b/src/cli/gateway-cli/register.ts @@ -440,7 +440,7 @@ function renderSupportExportResult( function resolveSupportExportRpcOptions( rpc?: Pick, -): GatewayRpcOpts { +): GatewayRpcOpts & { timeout: string } { return { url: rpc?.url, token: rpc?.token, diff --git a/src/cli/program/register.status-health-sessions.ts b/src/cli/program/register.status-health-sessions.ts index fca9cdc7b807..ae84757c93b7 100644 --- a/src/cli/program/register.status-health-sessions.ts +++ b/src/cli/program/register.status-health-sessions.ts @@ -367,7 +367,7 @@ export function registerStatusHealthSessionsCommands(program: Command) { .option("--url ", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)") .option("--token ", "Gateway token (if required)") .option("--password ", "Gateway password (password auth)") - .option("--timeout ", "RPC timeout in milliseconds (summarization can be slow)", "180000") + .option("--timeout ", "RPC timeout in milliseconds (defaults to no client deadline)") .option("--json", "Output JSON", false) .addHelpText( "after", diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 44e82ecf024f..823f5ff8cddf 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -11,6 +11,8 @@ import { deliverAgentCommandResult } from "../agents/command/delivery.runtime.js import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { loadManifestModelCatalog, loadModelCatalog } from "../agents/model-catalog.js"; import * as modelSelectionModule from "../agents/model-selection.js"; +import { isAgentRunRestartAbortReason } from "../agents/run-termination.js"; +import { ensureAgentWorkspace } from "../agents/workspace.js"; import { BASE_THINKING_LEVELS } from "../auto-reply/thinking.shared.js"; import * as runtimeSnapshotModule from "../config/runtime-snapshot.js"; import { loadSessionStore } from "../config/sessions/store-load.js"; @@ -25,6 +27,7 @@ import { import type { PluginProviderRegistration } from "../plugins/registry.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; import type { RuntimeEnv } from "../runtime.js"; +import { interruptSessionWorkAdmissions } from "../sessions/session-lifecycle-admission.js"; import { createDirectOutboundTestAdapter, createOutboundTestPlugin, @@ -73,6 +76,8 @@ vi.mock("../agents/auth-profiles/source-check.js", () => ({ vi.mock("../agents/command/session-store.runtime.js", () => { return { + loadSessionEntry: ({ storePath, sessionKey }: { storePath: string; sessionKey: string }) => + loadSessionStore(storePath, { skipCache: true })[sessionKey], updateSessionStoreAfterAgentRun: vi.fn(async () => undefined), }; }); @@ -455,6 +460,211 @@ describe("agentCommand", () => { ).rejects.toThrow("allowModelOverride must be explicitly set for ingress agent runs."); }); + it("rejects archived sessions selected by session id", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + mockConfig(home, store); + writeSessionStoreSeed(store, { + "agent:main:subagent:archived": { + sessionId: "archived-session-id", + archivedAt: Date.now(), + updatedAt: Date.now(), + }, + }); + vi.mocked(runEmbeddedAgent).mockClear(); + + await expect( + agentCommandFromIngress( + { + message: "blocked while archived", + sessionId: "archived-session-id", + allowModelOverride: false, + }, + runtime, + ), + ).rejects.toThrow( + 'Session "agent:main:subagent:archived" is archived. Restore it before starting new work.', + ); + expect(runEmbeddedAgent).not.toHaveBeenCalled(); + }); + }); + + it("reloads archive state after asynchronous command preparation", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + const sessionKey = "agent:main:subagent:archive-race"; + const sessionId = "archive-race-session-id"; + mockConfig(home, store); + writeSessionStoreSeed(store, { + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + vi.mocked(ensureAgentWorkspace).mockImplementationOnce(async (params) => { + writeSessionStoreSeed(store, { + [sessionKey]: { + sessionId, + archivedAt: Date.now(), + updatedAt: Date.now(), + }, + }); + return { dir: params?.dir ?? "/tmp/openclaw-workspace" }; + }); + + await expect( + agentCommandFromIngress( + { + message: "blocked after preparation", + sessionId, + allowModelOverride: false, + }, + runtime, + ), + ).rejects.toThrow( + `Session "${sessionKey}" is archived. Restore it before starting new work.`, + ); + expect(runEmbeddedAgent).not.toHaveBeenCalled(); + }); + }); + + it("keeps a restored session restored after asynchronous command preparation", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + const sessionKey = "agent:main:subagent:restore-race"; + const sessionId = "restore-race-session-id"; + mockConfig(home, store); + writeSessionStoreSeed(store, { + [sessionKey]: { + sessionId, + archivedAt: Date.now(), + updatedAt: Date.now(), + }, + }); + vi.mocked(ensureAgentWorkspace).mockImplementationOnce(async (params) => { + writeSessionStoreSeed(store, { + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + return { dir: params?.dir ?? "/tmp/openclaw-workspace" }; + }); + + await agentCommandFromIngress( + { + message: "run after restore", + sessionId, + allowModelOverride: false, + }, + runtime, + ); + + expect(runEmbeddedAgent).toHaveBeenCalled(); + expect( + readSessionStore<{ archivedAt?: number }>(store)[sessionKey]?.archivedAt, + ).toBeUndefined(); + }); + }); + + it("excludes an initiating agent turn from its own lifecycle interruption", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + const sessionKey = "agent:main:subagent:in-band-lifecycle"; + const sessionId = "in-band-lifecycle-session-id"; + mockConfig(home, store); + writeSessionStoreSeed(store, { + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + vi.mocked(runEmbeddedAgent).mockImplementationOnce(async () => { + await expect( + interruptSessionWorkAdmissions({ + scope: store, + identities: [sessionKey, sessionId], + timeoutMs: 5, + }), + ).resolves.toBe(true); + return createDefaultAgentResult(); + }); + + await agentCommandFromIngress( + { + message: "run an in-band lifecycle command", + sessionId, + allowModelOverride: false, + }, + runtime, + ); + + expect(runEmbeddedAgent).toHaveBeenCalledOnce(); + }); + }); + + it("classifies lifecycle interruption as a restart abort", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + const sessionKey = "agent:main:subagent:lifecycle-restart"; + const sessionId = "lifecycle-restart-session-id"; + mockConfig(home, store); + writeSessionStoreSeed(store, { + [sessionKey]: { sessionId, updatedAt: Date.now() }, + }); + let observedAbortReason: unknown; + vi.mocked(runEmbeddedAgent).mockImplementationOnce( + async (opts) => + await new Promise((resolve) => { + const finish = () => { + observedAbortReason = opts.abortSignal?.reason; + resolve(createDefaultAgentResult()); + }; + if (opts.abortSignal?.aborted) { + finish(); + return; + } + opts.abortSignal?.addEventListener("abort", finish, { once: true }); + }), + ); + + const command = agentCommandFromIngress( + { + message: "interrupt this lifecycle run", + sessionId, + allowModelOverride: false, + }, + runtime, + ).catch((error: unknown) => error); + await vi.waitFor(() => { + expect(runEmbeddedAgent).toHaveBeenCalledOnce(); + }); + await interruptSessionWorkAdmissions({ + scope: store, + identities: [sessionKey, sessionId], + }); + const commandError = await command; + + expect(isAgentRunRestartAbortReason(observedAbortReason)).toBe(true); + expect(isAgentRunRestartAbortReason(commandError)).toBe(true); + }); + }); + + it("rejects a stale requested session id after command preparation", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + const sessionKey = "agent:main:subagent:stale-request"; + mockConfig(home, store); + writeSessionStoreSeed(store, { + [sessionKey]: { sessionId: "current-session-id", updatedAt: Date.now() }, + }); + + await expect( + agentCommandFromIngress( + { + message: "do not enter the replacement session", + sessionKey, + sessionId: "stale-session-id", + allowModelOverride: false, + }, + runtime, + ), + ).rejects.toThrow(`Session "${sessionKey}" changed while starting work. Retry.`); + expect(runEmbeddedAgent).not.toHaveBeenCalled(); + }); + }); + it("uses the selected agent thinkingDefault for fresh ingress runs", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); diff --git a/src/commands/sessions-compact.test.ts b/src/commands/sessions-compact.test.ts index 57f184fe812b..f3de9ae0d163 100644 --- a/src/commands/sessions-compact.test.ts +++ b/src/commands/sessions-compact.test.ts @@ -37,15 +37,26 @@ describe("sessionsCompactCommand", () => { await sessionsCompactCommand({ key: "agent:main:main" }, runtime); expect(runtime.exit).not.toHaveBeenCalled(); + expect(callGatewayCli.mock.calls[0]?.[1]).toMatchObject({ timeout: null }); const logged = joinedArgs(runtime.log); expect(logged).toContain("243868"); expect(logged).toContain("34941"); }); + it("preserves an explicit client timeout override", async () => { + callGatewayCli.mockResolvedValue({ + ok: true, + key: "agent:main:main", + compacted: true, + }); + const runtime = createRuntime(); + + await sessionsCompactCommand({ key: "agent:main:main", timeout: "120000" }, runtime); + + expect(callGatewayCli.mock.calls[0]?.[1]).toMatchObject({ timeout: "120000" }); + }); + it("reports an asynchronously started Codex compaction as pending, not a no-op", async () => { - // Codex app-server `thread/compact/start` returns ok:true / compacted:false - // with a pending marker; completion is delivered later, so this is a started - // compaction, NOT "no compaction needed". callGatewayCli.mockResolvedValue({ ok: true, key: "agent:main:main", @@ -65,6 +76,31 @@ describe("sessionsCompactCommand", () => { expect(logged).not.toContain("No compaction needed"); }); + it("reports a terminal Codex compaction as completed", async () => { + callGatewayCli.mockResolvedValue({ + ok: true, + key: "agent:main:main", + compacted: true, + result: { + tokensBefore: 1200, + details: { + backend: "codex-app-server", + signal: "thread/compact/start", + pending: false, + completed: true, + }, + }, + }); + const runtime = createRuntime(); + + await sessionsCompactCommand({ key: "agent:main:main" }, runtime); + + expect(runtime.exit).not.toHaveBeenCalled(); + const logged = joinedArgs(runtime.log); + expect(logged).toContain("Compacted session"); + expect(logged).not.toContain("pending"); + }); + it("exits non-zero when the gateway reports ok:false (no silent no-op)", async () => { callGatewayCli.mockResolvedValue({ ok: false, diff --git a/src/commands/sessions-compact.ts b/src/commands/sessions-compact.ts index 44f9d8375bf3..ce2ab618dd5e 100644 --- a/src/commands/sessions-compact.ts +++ b/src/commands/sessions-compact.ts @@ -34,14 +34,13 @@ type SessionsCompactResult = { tokensAfter?: number; sessionId?: string; sessionFile?: string; - // Codex app-server `thread/compact/start` reports ok:true / compacted:false - // with this pending marker; the compaction was *started* and completion is - // delivered asynchronously, so it must not be rendered as "no work needed". + // A backend can explicitly report an accepted asynchronous operation. details?: { backend?: string; threadId?: string; signal?: string; pending?: boolean; + completed?: boolean; }; }; }; @@ -50,7 +49,7 @@ function describeCompaction(result: SessionsCompactResult, fallbackKey: string): const sessionKey = result.key ?? fallbackKey; if (!result.compacted) { const details = result.result?.details; - if (details?.pending === true || details?.signal === "thread/compact/start") { + if (details?.pending === true) { return `Compaction started for session ${sessionKey} (pending; completion is reported asynchronously by the backend).`; } const reason = result.reason ? ` (${result.reason})` : ""; @@ -76,7 +75,9 @@ export async function sessionsCompactCommand( url: opts.url, token: opts.token, password: opts.password, - timeout: opts.timeout, + // Compaction owns a configurable multi-stage server deadline. Keep an + // explicit CLI override, but otherwise avoid an ambiguous client timeout. + timeout: opts.timeout ?? null, json: opts.json, }; const params = { diff --git a/src/config/sessions/lifecycle.ts b/src/config/sessions/lifecycle.ts index cf7fb07a7b94..d370f5cde191 100644 --- a/src/config/sessions/lifecycle.ts +++ b/src/config/sessions/lifecycle.ts @@ -15,6 +15,54 @@ type SessionLifecycleEntry = Pick< "sessionId" | "sessionFile" | "sessionStartedAt" | "lastInteractionAt" | "updatedAt" >; +type SessionWorkStartEntry = Pick; + +type SessionWorkStartOptions = { + expectedSessionId?: string; +}; + +/** Stable Gateway error detail for stale session lifecycle requests. */ +export const SESSION_LIFECYCLE_CHANGED_ERROR_REASON = "session-changed"; +export const SESSION_WORK_START_INVALIDATED_ERROR_CODE = "SESSION_WORK_START_INVALIDATED"; + +export class SessionWorkStartInvalidatedError extends Error { + readonly code = SESSION_WORK_START_INVALIDATED_ERROR_CODE; + + constructor(message: string) { + super(message); + this.name = "SessionWorkStartInvalidatedError"; + } +} + +export function isSessionWorkStartInvalidatedError( + error: unknown, +): error is SessionWorkStartInvalidatedError { + return ( + error instanceof SessionWorkStartInvalidatedError || + (typeof error === "object" && + error !== null && + "code" in error && + error.code === SESSION_WORK_START_INVALIDATED_ERROR_CODE) + ); +} + +/** Archived sessions are read-only until the lifecycle owner restores them. */ +export function resolveSessionWorkStartError( + sessionKey: string, + entry: SessionWorkStartEntry | null | undefined, + options?: SessionWorkStartOptions, +): string | undefined { + if (options?.expectedSessionId && !entry) { + return `Session "${sessionKey}" was deleted while starting work. Retry.`; + } + if (options?.expectedSessionId && entry?.sessionId !== options.expectedSessionId) { + return `Session "${sessionKey}" changed while starting work. Retry.`; + } + return entry?.archivedAt === undefined + ? undefined + : `Session "${sessionKey}" is archived. Restore it before starting new work.`; +} + // Transcript headers are read lazily to recover startedAt without parsing full files. type TerminalMainSessionTranscriptRegistryParams = { diff --git a/src/config/sessions/session-accessor.ts b/src/config/sessions/session-accessor.ts index b0f76c1f5955..f02f210ca043 100644 --- a/src/config/sessions/session-accessor.ts +++ b/src/config/sessions/session-accessor.ts @@ -318,6 +318,8 @@ export type SessionTranscriptTurnPersistOptions = { * the same write transaction as the transcript append and metadata touch. */ expectedSessionId?: string; + /** Rejects the turn when lifecycle ownership changed without rotating the session id. */ + expectedLifecycleRevision?: string; /** Message rows to append under one transcript write lock. */ messages: readonly SessionTranscriptTurnMessageAppend[]; /** Controls whether the update event includes the last appended message. */ @@ -676,6 +678,14 @@ export type DeleteSessionEntryLifecycleParams = { agentId?: string; /** Whether transcript artifacts should be archived/deleted with the entry. */ archiveTranscript: boolean; + /** Optional exact row guard checked under the storage writer lock. */ + expectedEntry?: SessionEntry; + /** Optional provider-run identity guard checked under the storage writer lock. */ + expectedSessionId?: string; + /** Optional owner revision guard checked under the storage writer lock. */ + expectedLifecycleRevision?: string; + /** Optional persisted revision guard checked under the storage writer lock. */ + expectedUpdatedAt?: number; /** Explicit store target for file-backed stores and SQLite migration adapters. */ storePath: string; /** Canonical key plus aliases that identify the logical entry. */ @@ -2502,7 +2512,11 @@ async function persistExpectedSessionTranscriptTurn( storePath: scope.storePath, }, async (currentEntry) => { - if (currentEntry.sessionId !== expectedSessionId) { + if ( + currentEntry.sessionId !== expectedSessionId || + (options.expectedLifecycleRevision !== undefined && + currentEntry.lifecycleRevision !== options.expectedLifecycleRevision) + ) { rejectedEntry = currentEntry; return null; } @@ -2538,7 +2552,12 @@ async function persistExpectedSessionTranscriptTurn( { skipMaintenance: true }, ); - if (rejectedEntry || updated?.sessionId !== expectedSessionId) { + if ( + rejectedEntry || + updated?.sessionId !== expectedSessionId || + (options.expectedLifecycleRevision !== undefined && + updated.lifecycleRevision !== options.expectedLifecycleRevision) + ) { return { appendedCount: 0, messages: [], diff --git a/src/config/sessions/session-snapshot-merge.test.ts b/src/config/sessions/session-snapshot-merge.test.ts new file mode 100644 index 000000000000..cc3f3fbc3b2f --- /dev/null +++ b/src/config/sessions/session-snapshot-merge.test.ts @@ -0,0 +1,343 @@ +import { describe, expect, it } from "vitest"; +import { + mergeSessionSnapshotChanges, + projectSessionSnapshotChanges, + sessionSnapshotTouchedFieldsConflict, +} from "./session-snapshot-merge.js"; +import { mergeSessionEntry, type SessionEntry } from "./types.js"; + +const initial: SessionEntry = { + sessionId: "session-1", + updatedAt: 1, + modelProvider: "anthropic", + model: "claude-opus-4-6", +}; + +describe("session snapshot model merge", () => { + it("projects same-provider model changes as an atomic pair", () => { + const next = { ...initial, model: "claude-sonnet-4-6", updatedAt: 2 }; + const patch = projectSessionSnapshotChanges({ initial, next, current: initial }); + + expect(patch).toMatchObject({ + modelProvider: "anthropic", + model: "claude-sonnet-4-6", + }); + expect(mergeSessionEntry(initial, patch)).toMatchObject({ + modelProvider: "anthropic", + model: "claude-sonnet-4-6", + }); + }); + + it("keeps a concurrently changed model pair", () => { + const next = { ...initial, model: "claude-sonnet-4-6", updatedAt: 2 }; + const current = { + ...initial, + modelProvider: "openai", + model: "gpt-5.5", + updatedAt: 3, + }; + + expect(mergeSessionSnapshotChanges({ initial, next, current })).toMatchObject({ + modelProvider: "openai", + model: "gpt-5.5", + }); + }); + + it("keeps a concurrent model and auth override family atomically", () => { + const initialOverride: SessionEntry = { + ...initial, + providerOverride: "openai", + modelOverride: "gpt-old", + modelOverrideSource: "auto", + modelOverrideFallbackOriginProvider: "anthropic", + modelOverrideFallbackOriginModel: "claude-opus-4-6", + authProfileOverride: "openai:fallback", + authProfileOverrideSource: "auto", + fallbackNoticeSelectedModel: "openai/gpt-old", + }; + const next = { + ...initialOverride, + updatedAt: 2, + providerOverride: undefined, + modelOverride: undefined, + modelOverrideSource: undefined, + modelOverrideFallbackOriginProvider: undefined, + modelOverrideFallbackOriginModel: undefined, + authProfileOverride: undefined, + authProfileOverrideSource: undefined, + fallbackNoticeSelectedModel: undefined, + liveModelSwitchPending: true, + }; + const current: SessionEntry = { + ...initialOverride, + updatedAt: 3, + modelOverride: "gpt-new", + modelOverrideSource: "user", + authProfileOverride: "openai:user", + authProfileOverrideSource: "user", + fallbackNoticeSelectedModel: "openai/gpt-new", + }; + + expect(mergeSessionSnapshotChanges({ initial: initialOverride, next, current })).toEqual( + current, + ); + }); + + it("re-arms a winning explicit model switch after the prior signal clears", () => { + const initialOverride: SessionEntry = { + ...initial, + providerOverride: "openai", + modelOverride: "gpt-5.4", + modelOverrideSource: "user", + liveModelSwitchPending: true, + }; + const next: SessionEntry = { + ...initialOverride, + updatedAt: 2, + modelOverride: "gpt-5.5", + }; + const current = { ...initialOverride, updatedAt: 3 }; + delete current.liveModelSwitchPending; + + expect( + mergeSessionSnapshotChanges({ + initial: initialOverride, + next, + current, + reassertLiveModelSwitchPending: true, + }), + ).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-5.5", + modelOverrideSource: "user", + liveModelSwitchPending: true, + }); + }); + + it("preserves concurrently consumed unchanged model-dependent state", () => { + const initialOverride: SessionEntry = { + ...initial, + providerOverride: "openai", + modelOverride: "gpt-5.4", + modelOverrideSource: "user", + liveModelSwitchPending: true, + thinkingLevel: "high", + }; + const next: SessionEntry = { + ...initialOverride, + updatedAt: 2, + modelOverride: "gpt-5.5", + }; + const current: SessionEntry = { + ...initialOverride, + updatedAt: 3, + thinkingLevel: "low", + }; + delete current.liveModelSwitchPending; + + const merged = mergeSessionSnapshotChanges({ initial: initialOverride, next, current }); + + expect(merged).toMatchObject({ + modelOverride: "gpt-5.5", + thinkingLevel: "low", + }); + expect(merged.liveModelSwitchPending).toBeUndefined(); + }); + + it("clears stale model-dependent state with the winning model selection", () => { + const initialOverride: SessionEntry = { + ...initial, + providerOverride: "openai", + modelOverride: "gpt-5.4", + modelOverrideSource: "user", + modelProvider: "openai", + model: "gpt-5.4", + fallbackNoticeSelectedModel: "openai/gpt-5.4", + fallbackNoticeActiveModel: "openai/gpt-5.4-mini", + fallbackNoticeReason: "rate_limit", + contextTokens: 100_000, + contextBudgetStatus: { + schemaVersion: 1, + source: "pre-prompt-estimate", + updatedAt: 1, + provider: "openai", + model: "gpt-5.4", + route: "fits", + shouldCompact: false, + estimatedPromptTokens: 10, + contextTokenBudget: 100, + promptBudgetBeforeReserve: 80, + reserveTokens: 20, + effectiveReserveTokens: 20, + remainingPromptBudgetTokens: 70, + overflowTokens: 0, + toolResultReducibleChars: 0, + messageCount: 1, + unwindowedMessageCount: 1, + }, + }; + const next: SessionEntry = { + ...initialOverride, + updatedAt: 2, + modelOverride: "gpt-5.5", + modelProvider: undefined, + model: undefined, + fallbackNoticeSelectedModel: undefined, + fallbackNoticeActiveModel: undefined, + fallbackNoticeReason: undefined, + contextTokens: undefined, + contextBudgetStatus: undefined, + }; + const current: SessionEntry = { + ...initialOverride, + updatedAt: 3, + modelProvider: "openai", + model: "gpt-5.4-mini", + fallbackNoticeActiveModel: "openai/gpt-5.4-nano", + contextTokens: 80_000, + }; + + const merged = mergeSessionSnapshotChanges({ initial: initialOverride, next, current }); + + expect(merged).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-5.5", + modelOverrideSource: "user", + }); + expect(merged.modelProvider).toBeUndefined(); + expect(merged.model).toBeUndefined(); + expect(merged.fallbackNoticeSelectedModel).toBeUndefined(); + expect(merged.fallbackNoticeActiveModel).toBeUndefined(); + expect(merged.fallbackNoticeReason).toBeUndefined(); + expect(merged.contextTokens).toBeUndefined(); + expect(merged.contextBudgetStatus).toBeUndefined(); + }); + + it("clears runtime metadata added concurrently for the previous model", () => { + const initialOverride: SessionEntry = { + sessionId: "session-1", + updatedAt: 1, + providerOverride: "openai", + modelOverride: "gpt-5.4", + modelOverrideSource: "user", + }; + const next: SessionEntry = { + ...initialOverride, + updatedAt: 2, + modelOverride: "gpt-5.5", + }; + const current: SessionEntry = { + ...initialOverride, + updatedAt: 3, + modelProvider: "openai", + model: "gpt-5.4", + fallbackNoticeActiveModel: "openai/gpt-5.4-mini", + contextTokens: 80_000, + }; + + const merged = mergeSessionSnapshotChanges({ initial: initialOverride, next, current }); + + expect(merged).toMatchObject({ + providerOverride: "openai", + modelOverride: "gpt-5.5", + modelOverrideSource: "user", + }); + expect(merged.modelProvider).toBeUndefined(); + expect(merged.model).toBeUndefined(); + expect(merged.fallbackNoticeActiveModel).toBeUndefined(); + expect(merged.contextTokens).toBeUndefined(); + }); + + it("does not reject a model switch after concurrent runtime metadata refresh", () => { + const initialOverride: SessionEntry = { + ...initial, + providerOverride: "openai", + modelOverride: "gpt-5.4", + modelOverrideSource: "user", + contextTokens: 100_000, + }; + const next: SessionEntry = { + ...initialOverride, + modelOverride: "gpt-5.5", + modelProvider: undefined, + model: undefined, + contextTokens: undefined, + }; + const current: SessionEntry = { + ...initialOverride, + updatedAt: 3, + model: "gpt-5.4-mini", + contextTokens: 80_000, + }; + + expect( + sessionSnapshotTouchedFieldsConflict({ + initial: initialOverride, + next, + current, + touchedFields: ["providerOverride", "modelOverride", "modelOverrideSource"], + }), + ).toBe(false); + }); + + it("rejects a model switch after a concurrent thinking override", () => { + const initialOverride: SessionEntry = { + ...initial, + providerOverride: "openai", + modelOverride: "gpt-5.4", + modelOverrideSource: "user", + thinkingLevel: "xhigh", + }; + const next: SessionEntry = { + ...initialOverride, + modelOverride: "gpt-5.5", + thinkingLevel: "high", + }; + const current: SessionEntry = { + ...initialOverride, + updatedAt: 3, + thinkingLevel: "low", + }; + + expect( + sessionSnapshotTouchedFieldsConflict({ + initial: initialOverride, + next, + current, + touchedFields: ["providerOverride", "modelOverride", "modelOverrideSource"], + }), + ).toBe(true); + }); + + it("blocks stale model-dependent state after a concurrent selection", () => { + const next: SessionEntry = { + ...initial, + updatedAt: 2, + modelProvider: "openai", + model: "gpt-5.4", + fallbackNoticeSelectedModel: "openai/gpt-5.4", + contextTokens: 100_000, + thinkingLevel: "medium", + }; + const current: SessionEntry = { + ...initial, + updatedAt: 3, + providerOverride: "openai", + modelOverride: "gpt-5.5", + modelOverrideSource: "user", + }; + + expect(mergeSessionSnapshotChanges({ initial, next, current })).toEqual(current); + }); + + it("does not project a stale snapshot across a session rotation", () => { + const next = { ...initial, thinkingLevel: "high", updatedAt: 2 }; + const current: SessionEntry = { + sessionId: "session-2", + updatedAt: 3, + thinkingLevel: "low", + }; + + expect(mergeSessionSnapshotChanges({ initial, next, current })).toEqual(current); + }); +}); diff --git a/src/config/sessions/session-snapshot-merge.ts b/src/config/sessions/session-snapshot-merge.ts new file mode 100644 index 000000000000..db8fc10d61d7 --- /dev/null +++ b/src/config/sessions/session-snapshot-merge.ts @@ -0,0 +1,290 @@ +import { isDeepStrictEqual } from "node:util"; +import type { SessionEntry } from "./types.js"; + +type SessionEntryRecord = Partial>; + +export const SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS = [ + "providerOverride", + "modelOverride", + "agentRuntimeOverride", + "modelOverrideSource", + "modelOverrideFallbackOriginProvider", + "modelOverrideFallbackOriginModel", + "authProfileOverride", + "authProfileOverrideSource", + "authProfileOverrideCompactionCount", +] as const satisfies ReadonlyArray; + +const MODEL_ROUTE_OVERRIDE_FIELDS = [ + "providerOverride", + "modelOverride", + "agentRuntimeOverride", +] as const satisfies ReadonlyArray; + +const MODEL_OVERRIDE_RUNTIME_FIELDS = [ + "modelProvider", + "model", + "fallbackNoticeSelectedModel", + "fallbackNoticeActiveModel", + "fallbackNoticeReason", + "contextTokens", + "contextBudgetStatus", +] as const satisfies ReadonlyArray; +const MODEL_OVERRIDE_RUNTIME_FIELD_SET = new Set(MODEL_OVERRIDE_RUNTIME_FIELDS); + +const MODEL_OVERRIDE_DEPENDENT_FIELDS = new Set([ + ...MODEL_OVERRIDE_RUNTIME_FIELDS, + "liveModelSwitchPending", + "thinkingLevel", +]); + +const MODEL_OVERRIDE_CONFLICT_DEPENDENT_FIELDS = ["thinkingLevel"] as const satisfies ReadonlyArray< + keyof SessionEntry +>; + +function anySessionFieldChanged( + before: SessionEntryRecord, + after: SessionEntryRecord, + fields: ReadonlyArray, +): boolean { + return fields.some((field) => !isDeepStrictEqual(before[field], after[field])); +} + +/** Projects run-local snapshot changes without restoring concurrently changed fields. */ +export function projectSessionSnapshotChanges(params: { + initial: SessionEntry; + next: SessionEntry; + current: SessionEntry; + reassertLiveModelSwitchPending?: boolean; +}): Partial { + if (params.current.sessionId !== params.initial.sessionId) { + return {}; + } + const initial = params.initial as SessionEntryRecord; + const next = params.next as SessionEntryRecord; + const current = params.current as SessionEntryRecord; + const patch: Partial = {}; + const patchRecord = patch as SessionEntryRecord; + const fields = new Set([ + ...(Object.keys(params.initial) as Array), + ...(Object.keys(params.next) as Array), + ]); + + const modelOverrideChanged = anySessionFieldChanged( + initial, + next, + SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS, + ); + const modelRouteOverrideChanged = anySessionFieldChanged( + initial, + next, + MODEL_ROUTE_OVERRIDE_FIELDS, + ); + const modelOverrideChangedConcurrently = anySessionFieldChanged( + initial, + current, + SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS, + ); + if (modelOverrideChanged && !modelOverrideChangedConcurrently) { + // Model, provenance, and auth overrides form one selection transaction. + // Project the whole family or none of it so concurrent switches cannot hybridize rows. + for (const field of SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS) { + patchRecord[field] = Object.hasOwn(params.next, field) ? next[field] : undefined; + } + if (modelRouteOverrideChanged) { + // A winning route switch invalidates any runtime facts written for the + // old model, including facts that appeared after this snapshot began. + for (const field of MODEL_OVERRIDE_RUNTIME_FIELDS) { + patchRecord[field] = Object.hasOwn(params.next, field) ? next[field] : undefined; + } + } + for (const field of MODEL_OVERRIDE_DEPENDENT_FIELDS) { + if (modelRouteOverrideChanged && MODEL_OVERRIDE_RUNTIME_FIELD_SET.has(field)) { + continue; + } + if (!isDeepStrictEqual(initial[field], next[field])) { + patchRecord[field] = Object.hasOwn(params.next, field) ? next[field] : undefined; + } + } + if (params.reassertLiveModelSwitchPending) { + // A second explicit switch can inherit true from the first snapshot while + // the active runner concurrently clears it. Re-arm the winning transaction. + patch.liveModelSwitchPending = true; + } + } + + const runtimeModelChanged = + !isDeepStrictEqual(initial.model, next.model) || + !isDeepStrictEqual(initial.modelProvider, next.modelProvider); + const runtimeModelChangedConcurrently = + !isDeepStrictEqual(current.model, initial.model) || + !isDeepStrictEqual(current.modelProvider, initial.modelProvider); + if ( + runtimeModelChanged && + !runtimeModelChangedConcurrently && + !modelOverrideChanged && + !modelOverrideChangedConcurrently + ) { + // Runtime model metadata is one pair. A model-only patch intentionally + // clears provider state, so snapshot projection must emit both fields. + patch.model = params.next.model; + patch.modelProvider = params.next.modelProvider; + } + + for (const field of fields) { + if ( + field === "model" || + field === "modelProvider" || + SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS.includes( + field as (typeof SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS)[number], + ) + ) { + continue; + } + if (isDeepStrictEqual(initial[field], next[field])) { + continue; + } + if (field === "updatedAt") { + patch.updatedAt = Math.max(params.current.updatedAt, params.next.updatedAt); + continue; + } + if ( + (modelOverrideChanged || modelOverrideChangedConcurrently) && + MODEL_OVERRIDE_DEPENDENT_FIELDS.has(field) + ) { + continue; + } + // The latest row wins conflicts. This prevents a stale run snapshot from + // reviving policy or privacy fields cleared while the run was active. + if (!isDeepStrictEqual(current[field], initial[field])) { + continue; + } + patchRecord[field] = Object.hasOwn(params.next, field) ? next[field] : undefined; + } + return patch; +} + +/** Reports whether every caller-owned snapshot delta is present in the current row. */ +export function sessionSnapshotChangesApplied(params: { + initial: SessionEntry; + next: SessionEntry; + current: SessionEntry; + touchedFields?: ReadonlyArray; +}): boolean { + if (params.current.sessionId !== params.initial.sessionId) { + return false; + } + const initial = params.initial as SessionEntryRecord; + const next = params.next as SessionEntryRecord; + const current = params.current as SessionEntryRecord; + const fields = new Set([ + ...(Object.keys(params.initial) as Array), + ...(Object.keys(params.next) as Array), + ...(params.touchedFields ?? []), + ]); + fields.delete("updatedAt"); + for (const field of fields) { + const explicitlyTouched = params.touchedFields?.includes(field) === true; + if ( + (explicitlyTouched || !isDeepStrictEqual(initial[field], next[field])) && + !isDeepStrictEqual(current[field], next[field]) + ) { + return false; + } + } + return true; +} + +/** Reports an explicit grouped-write conflict before any snapshot fields are committed. */ +export function sessionSnapshotTouchedFieldsConflict(params: { + initial: SessionEntry; + next: SessionEntry; + current: SessionEntry; + touchedFields?: ReadonlyArray; +}): boolean { + if (params.current.sessionId !== params.initial.sessionId) { + return true; + } + const initial = params.initial as SessionEntryRecord; + const next = params.next as SessionEntryRecord; + const current = params.current as SessionEntryRecord; + const fields = new Set(params.touchedFields ?? []); + if (SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS.some((field) => fields.has(field))) { + for (const field of MODEL_OVERRIDE_CONFLICT_DEPENDENT_FIELDS) { + if (!isDeepStrictEqual(initial[field], next[field])) { + fields.add(field); + } + } + } + return [...fields].some( + (field) => + !isDeepStrictEqual(current[field], initial[field]) && + !isDeepStrictEqual(current[field], next[field]), + ); +} + +/** Replaces a caller-held snapshot with the latest persisted row in place. */ +export function adoptPersistedSessionSnapshot(target: SessionEntry, current: SessionEntry): void { + const targetRecord = target as SessionEntryRecord; + const currentRecord = current as SessionEntryRecord; + for (const field of Object.keys(target) as Array) { + if (!Object.hasOwn(current, field)) { + delete targetRecord[field]; + } + } + for (const field of Object.keys(current) as Array) { + targetRecord[field] = currentRecord[field]; + } +} + +/** Reports whether a model/auth selection transaction is the persisted winner. */ +export function sessionModelOverrideChangesApplied(params: { + initial: SessionEntry; + next: SessionEntry; + current: SessionEntry; + reassertLiveModelSwitchPending?: boolean; +}): boolean { + if (params.current.sessionId !== params.initial.sessionId) { + return false; + } + const next = params.next as SessionEntryRecord; + const current = params.current as SessionEntryRecord; + const initial = params.initial as SessionEntryRecord; + const changedDependentFields = [...MODEL_OVERRIDE_DEPENDENT_FIELDS].filter( + (field) => !isDeepStrictEqual(initial[field], next[field]), + ); + const modelRouteOverrideChanged = anySessionFieldChanged( + initial, + next, + MODEL_ROUTE_OVERRIDE_FIELDS, + ); + const fields = [ + ...SESSION_MODEL_OVERRIDE_TRANSACTION_FIELDS, + ...(modelRouteOverrideChanged ? MODEL_OVERRIDE_RUNTIME_FIELDS : []), + ...changedDependentFields, + ]; + if (fields.some((field) => !isDeepStrictEqual(current[field], next[field]))) { + return false; + } + return !params.reassertLiveModelSwitchPending || params.current.liveModelSwitchPending === true; +} + +/** Merges run-local snapshot changes into the latest persisted session row. */ +export function mergeSessionSnapshotChanges(params: { + initial: SessionEntry; + next: SessionEntry; + current: SessionEntry; + reassertLiveModelSwitchPending?: boolean; +}): SessionEntry { + const merged = { ...params.current }; + const mergedRecord = merged as SessionEntryRecord; + const patch = projectSessionSnapshotChanges(params) as SessionEntryRecord; + for (const field of Object.keys(patch) as Array) { + if (patch[field] === undefined) { + delete mergedRecord[field]; + } else { + mergedRecord[field] = patch[field]; + } + } + return merged; +} diff --git a/src/config/sessions/store.session-lifecycle-mutation.test.ts b/src/config/sessions/store.session-lifecycle-mutation.test.ts index 0018fdb585c9..1f78c27a8edd 100644 --- a/src/config/sessions/store.session-lifecycle-mutation.test.ts +++ b/src/config/sessions/store.session-lifecycle-mutation.test.ts @@ -108,4 +108,100 @@ describe("session store lifecycle mutations", () => { expect(store["agent:main:keep"]?.sessionId).toBe("keep-session"); expect(fs.existsSync(transcriptPath)).toBe(false); }); + + it("keeps a row that changed before guarded deletion acquired the writer lock", async () => { + const sessionKey = "agent:main:delete"; + await saveSessionStore( + storePath, + { + [sessionKey]: { + label: "new revision", + sessionId: "delete-session", + updatedAt: 2, + }, + }, + { skipMaintenance: true }, + ); + + const result = await deleteSessionEntryLifecycle({ + archiveTranscript: false, + expectedSessionId: "delete-session", + expectedUpdatedAt: 1, + storePath, + target: { + canonicalKey: sessionKey, + storeKeys: [sessionKey], + }, + }); + + expect(result).toMatchObject({ deleted: false, expectedEntryMismatch: true }); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toMatchObject({ + label: "new revision", + updatedAt: 2, + }); + }); + + it("does not cache alias promotion when guarded deletion is rejected", async () => { + const canonicalKey = "agent:main:room"; + const aliasKey = "Agent:Main:Room"; + await saveSessionStore( + storePath, + { + [canonicalKey]: { + label: "canonical", + sessionId: "canonical-session", + updatedAt: 1, + }, + [aliasKey]: { + label: "fresh alias", + sessionId: "alias-session", + updatedAt: 2, + }, + }, + { skipMaintenance: true }, + ); + + const result = await deleteSessionEntryLifecycle({ + archiveTranscript: false, + expectedSessionId: "stale-session", + storePath, + target: { + canonicalKey, + storeKeys: [canonicalKey, aliasKey], + }, + }); + + expect(result).toMatchObject({ deleted: false, expectedEntryMismatch: true }); + expect(loadSessionStore(storePath)).toMatchObject({ + [canonicalKey]: { label: "canonical", sessionId: "canonical-session" }, + [aliasKey]: { label: "fresh alias", sessionId: "alias-session" }, + }); + }); + + it("deletes only the exact row snapshot supplied after lifecycle cleanup", async () => { + const sessionKey = "agent:main:delete"; + const expectedEntry = { + label: "cleanup-owned revision", + lifecycleRevision: "run-revision", + sessionId: "delete-session", + updatedAt: 2, + } satisfies SessionEntry; + await saveSessionStore(storePath, { [sessionKey]: expectedEntry }, { skipMaintenance: true }); + + const result = await deleteSessionEntryLifecycle({ + archiveTranscript: false, + expectedEntry, + expectedLifecycleRevision: expectedEntry.lifecycleRevision, + expectedSessionId: expectedEntry.sessionId, + expectedUpdatedAt: expectedEntry.updatedAt, + storePath, + target: { + canonicalKey: sessionKey, + storeKeys: [sessionKey], + }, + }); + + expect(result.deleted).toBe(true); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toBeUndefined(); + }); }); diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index bbf1c7863b0c..34a57e8c069b 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -268,6 +268,7 @@ export type ResetSessionEntryLifecycleMutation = Omit< export type DeleteSessionEntryLifecycleResult = { archivedTranscripts: SessionLifecycleArchivedTranscript[]; deleted: boolean; + expectedEntryMismatch?: true; deletedEntry?: SessionEntry; deletedSessionFile?: string; deletedSessionId?: string; @@ -651,15 +652,9 @@ function resolveLifecyclePrimaryEntry(params: { store: Record; target: SessionLifecycleStoreTarget; }): SessionEntry | undefined { - const freshestMatch = resolveFreshestLifecycleStoreMatch({ - store: params.store, - storeKeys: params.target.storeKeys, - }); - if (freshestMatch) { - const currentPrimary = params.store[params.target.canonicalKey]; - if (!currentPrimary || (freshestMatch.entry.updatedAt ?? 0) > (currentPrimary.updatedAt ?? 0)) { - params.store[params.target.canonicalKey] = freshestMatch.entry; - } + const primaryEntry = resolveLifecyclePrimaryEntrySnapshot(params); + if (primaryEntry) { + params.store[params.target.canonicalKey] = primaryEntry; } pruneLifecycleLegacyStoreKeys({ store: params.store, @@ -668,6 +663,24 @@ function resolveLifecyclePrimaryEntry(params: { return params.store[params.target.canonicalKey]; } +function resolveLifecyclePrimaryEntrySnapshot(params: { + store: Record; + target: SessionLifecycleStoreTarget; +}): SessionEntry | undefined { + const currentPrimary = params.store[params.target.canonicalKey]; + const freshestMatch = resolveFreshestLifecycleStoreMatch({ + store: params.store, + storeKeys: params.target.storeKeys, + }); + if ( + freshestMatch && + (!currentPrimary || (freshestMatch.entry.updatedAt ?? 0) > (currentPrimary.updatedAt ?? 0)) + ) { + return freshestMatch.entry; + } + return currentPrimary; +} + function resolveFreshestLifecycleStoreMatch(params: { store: Record; storeKeys: string[]; @@ -1201,12 +1214,18 @@ export async function resetSessionEntryLifecycle(params: { export async function deleteSessionEntryLifecycle(params: { agentId?: string; archiveTranscript: boolean; + expectedEntry?: SessionEntry; + expectedLifecycleRevision?: string; + expectedSessionId?: string; + expectedUpdatedAt?: number; storePath: string; target: SessionLifecycleStoreTarget; }): Promise { return await runExclusiveSessionStoreWrite(params.storePath, async () => { const store = loadMutableSessionStoreForWriter(params.storePath); - const deletedEntry = resolveLifecyclePrimaryEntry({ + // Compare against an unmodified snapshot. Alias promotion is itself a + // mutation and must not enter the cache when a guarded delete is rejected. + const deletedEntry = resolveLifecyclePrimaryEntrySnapshot({ store, target: params.target, }); @@ -1217,6 +1236,34 @@ export async function deleteSessionEntryLifecycle(params: { deleted: false, }; } + const expectedEntryMatches = + params.expectedEntry === undefined || + JSON.stringify(deletedEntry) === JSON.stringify(params.expectedEntry); + const expectedLifecycleRevisionMatches = + params.expectedLifecycleRevision === undefined || + deletedEntry.lifecycleRevision === params.expectedLifecycleRevision; + const expectedSessionIdMatches = + !params.expectedSessionId || + deletedEntry.sessionId === params.expectedSessionId || + (deletedEntry.sessionId === undefined && + params.expectedLifecycleRevision !== undefined && + expectedLifecycleRevisionMatches); + const expectedUpdatedAtMatches = + params.expectedUpdatedAt === undefined || deletedEntry.updatedAt === params.expectedUpdatedAt; + if ( + !expectedEntryMatches || + !expectedLifecycleRevisionMatches || + !expectedSessionIdMatches || + !expectedUpdatedAtMatches + ) { + restoreUnchangedSessionStoreCache(params.storePath, store); + return { + archivedTranscripts: [], + deleted: false, + expectedEntryMismatch: true, + }; + } + pruneLifecycleLegacyStoreKeys({ store, target: params.target }); const deletedSessionId = deletedEntry.sessionId; const deletedSessionFile = deletedEntry.sessionFile; delete store[params.target.canonicalKey]; diff --git a/src/config/sessions/transcript.test.ts b/src/config/sessions/transcript.test.ts index d5c0c74954ff..67ac8a01bdd0 100644 --- a/src/config/sessions/transcript.test.ts +++ b/src/config/sessions/transcript.test.ts @@ -1359,6 +1359,57 @@ describe("appendAssistantMessageToSessionTranscript", () => { expect(fs.existsSync(replacementSessionFile)).toBe(false); }); + it("rejects a concurrent lifecycle owner change without a session id rotation", async () => { + fs.writeFileSync( + fixture.storePath(), + JSON.stringify({ + [sessionKey]: { + sessionId, + lifecycleRevision: "original-revision", + chatType: "direct", + channel: "discord", + }, + }), + "utf-8", + ); + let releaseOwnerChange = () => {}; + const ownerChangeGate = new Promise((resolve) => { + releaseOwnerChange = resolve; + }); + let markOwnerChangeStarted = () => {}; + const ownerChangeStarted = new Promise((resolve) => { + markOwnerChangeStarted = resolve; + }); + const ownerChange = updateSessionStoreEntry({ + storePath: fixture.storePath(), + sessionKey, + update: async () => { + markOwnerChangeStarted(); + await ownerChangeGate; + return { lifecycleRevision: "replacement-revision" }; + }, + }); + await ownerChangeStarted; + + const append = appendExactAssistantMessageToSessionTranscript({ + sessionKey, + expectedSessionId: sessionId, + expectedLifecycleRevision: "original-revision", + storePath: fixture.storePath(), + message: createExactAssistantMessage({ text: "late output" }), + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + releaseOwnerChange(); + + await ownerChange; + await expect(append).resolves.toMatchObject({ + ok: false, + code: "session-rebound", + }); + }); + it("dedupes concurrent exact assistant appends by idempotency key", async () => { writeTranscriptStore(); const idempotencyKey = "mirror:concurrent-assistant"; diff --git a/src/config/sessions/transcript.ts b/src/config/sessions/transcript.ts index d4af9d49e211..acd008929075 100644 --- a/src/config/sessions/transcript.ts +++ b/src/config/sessions/transcript.ts @@ -328,6 +328,7 @@ export async function appendAssistantMessageToSessionTranscript(params: { agentId?: string; sessionKey: string; expectedSessionId?: string; + expectedLifecycleRevision?: string; text?: string; mediaUrls?: string[]; idempotencyKey?: string; @@ -355,6 +356,9 @@ export async function appendAssistantMessageToSessionTranscript(params: { agentId: params.agentId, sessionKey, ...(params.expectedSessionId ? { expectedSessionId: params.expectedSessionId } : {}), + ...(params.expectedLifecycleRevision + ? { expectedLifecycleRevision: params.expectedLifecycleRevision } + : {}), storePath: params.storePath, idempotencyKey: params.idempotencyKey, updateMode: params.updateMode, @@ -391,6 +395,7 @@ export async function appendExactAssistantMessageToSessionTranscript(params: { agentId?: string; sessionKey: string; expectedSessionId?: string; + expectedLifecycleRevision?: string; message: SessionTranscriptAssistantMessage; idempotencyKey?: string; storePath?: string; @@ -422,6 +427,16 @@ export async function appendExactAssistantMessageToSessionTranscript(params: { reason: `session rebound for sessionKey: ${sessionKey}`, }; } + if ( + params.expectedLifecycleRevision && + entry?.lifecycleRevision !== params.expectedLifecycleRevision + ) { + return { + ok: false, + code: "session-rebound", + reason: `session rebound for sessionKey: ${sessionKey}`, + }; + } if (!entry?.sessionId) { return { ok: false, reason: `unknown sessionKey: ${sessionKey}` }; } @@ -469,6 +484,9 @@ export async function appendExactAssistantMessageToSessionTranscript(params: { { cwd: currentEntry.spawnedCwd, ...(params.expectedSessionId ? { expectedSessionId: params.expectedSessionId } : {}), + ...(params.expectedLifecycleRevision + ? { expectedLifecycleRevision: params.expectedLifecycleRevision } + : {}), ...(params.config ? { config: params.config } : {}), updateMode: params.updateMode ?? "inline", touchSessionEntry: true, diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index 3da2228bba30..e8513f393566 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -247,6 +247,17 @@ export type SessionEntry = { pluginNextTurnInjections?: Record; sessionId: string; updatedAt: number; + /** Opaque owner revision used to reject stale lifecycle mutations. */ + lifecycleRevision?: string; + // archivedAt/pinnedAt mirror the Codex thread-management shape (state DB + // threads.archived_at: the boolean is always derived from the timestamp and + // stamped server-side). Codex serializes camelCase but in epoch SECONDS; + // these are epoch MS like every other session timestamp — convert at the + // codex plugin seam when exchanging thread metadata. + /** Timestamp (ms) when the session was archived from active session lists. */ + archivedAt?: number; + /** Timestamp (ms) when the session was pinned for quick access. */ + pinnedAt?: number; sessionFile?: string; /** Parent session key that spawned this session (used for sandbox session-tool scoping). */ spawnedBy?: string; diff --git a/src/cron/isolated-agent.session-identity.test.ts b/src/cron/isolated-agent.session-identity.test.ts index 5f2ed942afe3..d42734978f32 100644 --- a/src/cron/isolated-agent.session-identity.test.ts +++ b/src/cron/isolated-agent.session-identity.test.ts @@ -214,7 +214,12 @@ describe("runCronIsolatedAgentTurn session identity", () => { const finalPersist = updateSessionStoreMock.mock.calls.at(-1); expect(finalPersist?.[0]).toBe(storePath); - const persistedStore: Record = {}; + // Replay the final persist against the real post-run store: the claim + // guard rejects writes into a store that never held the session. + const persistedStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< + string, + { [key: string]: unknown } + >; (finalPersist![1] as (store: typeof persistedStore) => void)(persistedStore); expect(persistedStore[boundSessionKey]).toEqual( expect.objectContaining({ diff --git a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts index 88ad469c0d59..9f99d511ec6a 100644 --- a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts +++ b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts @@ -20,6 +20,7 @@ const { countActiveDescendantRunsMock, deliverOutboundPayloadsMock, ensureOutboundSessionEntryMock, + loadCronSessionEntryLatestMock, maybeApplyTtsToPayloadMock, retireSessionMcpRuntimeMock, resolveOutboundSessionRouteMock, @@ -32,6 +33,7 @@ const { countActiveDescendantRunsMock: vi.fn().mockReturnValue(0), deliverOutboundPayloadsMock: vi.fn().mockResolvedValue([{ ok: true }]), ensureOutboundSessionEntryMock: vi.fn().mockResolvedValue(undefined), + loadCronSessionEntryLatestMock: vi.fn(), maybeApplyTtsToPayloadMock: vi.fn(async (params: { payload: unknown }) => params.payload), retireSessionMcpRuntimeMock: vi.fn().mockResolvedValue(true), resolveOutboundSessionRouteMock: vi.fn().mockResolvedValue(null), @@ -106,12 +108,16 @@ vi.mock("../../config/sessions/transcript.runtime.js", () => ({ appendAssistantMessageToSessionTranscript: appendAssistantMessageToSessionTranscriptMock, })); +vi.mock("./session.js", () => ({ + loadCronSessionEntryLatest: loadCronSessionEntryLatestMock, +})); + vi.mock("../../cli/outbound-send-deps.js", () => ({ createOutboundSendDeps: vi.fn().mockReturnValue({}), })); vi.mock("../../gateway/call.runtime.js", () => ({ - callGateway: vi.fn().mockResolvedValue({ status: "ok" }), + callGateway: vi.fn().mockResolvedValue({ ok: true, deleted: true }), })); vi.mock("../../logger.js", () => ({ @@ -225,6 +231,8 @@ function makeBaseParams(overrides: { agentSessionKey: "agent:main", runSessionKey: overrides.runSessionKey ?? "agent:main", sessionId: "test-session-id", + lifecycleRevision: "test-lifecycle-revision", + sessionUpdatedAt: 1_000, runStartedAt, runEndedAt: runStartedAt, timeoutMs: 30_000, @@ -318,6 +326,10 @@ describe("dispatchCronDelivery — double-announce guard", () => { sessionFile: "session.jsonl", messageId: "mirror-message", }); + loadCronSessionEntryLatestMock.mockReturnValue({ + sessionId: "test-session-id", + lifecycleRevision: "test-lifecycle-revision", + }); maybeApplyTtsToPayloadMock.mockReset().mockImplementation(async (params) => params.payload); }); @@ -1424,6 +1436,9 @@ describe("dispatchCronDelivery — double-announce guard", () => { key: "agent:main:cron:test-job", deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 1_000, }, timeoutMs: 10_000, }); @@ -1446,6 +1461,35 @@ describe("dispatchCronDelivery — double-announce guard", () => { key: "agent:main:cron:test-job", deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 1_000, + }, + timeoutMs: 10_000, + }); + }); + + it("does not mirror into a self-deleting run session before guarded cleanup", async () => { + mockResolvedOutboundRoute({ + sessionKey: "agent:main:cron:test-job", + baseSessionKey: "agent:main:cron:test-job", + }); + const params = makeBaseParams({ synthesizedText: "Delivered report" }); + params.agentSessionKey = "agent:main:cron:test-job"; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + await dispatchCronDelivery(params); + + expect(appendAssistantMessageToSessionTranscript).not.toHaveBeenCalled(); + expect(callGateway).toHaveBeenCalledWith({ + method: "sessions.delete", + params: { + key: "agent:main:cron:test-job", + deleteTranscript: true, + emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 1_000, }, timeoutMs: 10_000, }); @@ -1471,6 +1515,151 @@ describe("dispatchCronDelivery — double-announce guard", () => { }); }); + it("guards the deferred mirror when isolated cleanup only retires the runtime", async () => { + mockResolvedOutboundRoute({ + sessionKey: "agent:main:cron:test-job", + baseSessionKey: "agent:main:cron:test-job", + }); + vi.mocked(callGateway).mockRejectedValueOnce(new Error("gateway down")); + + const params = makeBaseParams({ synthesizedText: "Delivered report" }); + params.agentSessionKey = "agent:main:cron:test-job"; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + await dispatchCronDelivery(params); + + expect(retireSessionMcpRuntime).toHaveBeenCalledWith({ + sessionId: "test-session-id", + reason: "cron-delete-after-run-fallback", + }); + expect(appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith( + expect.objectContaining({ + sessionKey: "agent:main:cron:test-job", + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + text: "Delivered report", + }), + ); + }); + + it("cancels deferred mirror admission when the cron run aborts during cleanup", async () => { + mockResolvedOutboundRoute({ + sessionKey: "agent:main:cron:test-job", + baseSessionKey: "agent:main:cron:test-job", + }); + const abortController = new AbortController(); + vi.mocked(callGateway).mockImplementationOnce(async () => { + abortController.abort(new Error("cron run aborted")); + throw new Error("gateway down"); + }); + + const params = makeBaseParams({ synthesizedText: "Delivered report" }); + params.agentSessionKey = "agent:main:cron:test-job"; + params.abortSignal = abortController.signal; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + await dispatchCronDelivery(params); + + expect(appendAssistantMessageToSessionTranscript).not.toHaveBeenCalled(); + }); + + it("does not retire a replacement runtime when guarded cleanup finds a changed session", async () => { + const changedError = new Error("session changed") as Error & { + gatewayCode: string; + details: { reason: string }; + }; + changedError.name = "GatewayClientRequestError"; + changedError.gatewayCode = "INVALID_REQUEST"; + changedError.details = { reason: "session-changed" }; + vi.mocked(callGateway).mockRejectedValueOnce(changedError); + + const params = makeBaseParams({ synthesizedText: SILENT_REPLY_TOKEN }); + params.agentSessionKey = "agent:main:cron:test-job"; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + const state = await dispatchCronDelivery(params); + + expect(state.cronRunSessionCleanupAttempted).toBe(true); + expect(retireSessionMcpRuntime).not.toHaveBeenCalled(); + }); + + it("does not retire a persistent session runtime when gateway cleanup fails", async () => { + vi.mocked(callGateway).mockRejectedValueOnce(new Error("gateway down")); + + const params = makeBaseParams({ + synthesizedText: SILENT_REPLY_TOKEN, + sessionTarget: "session:agent:main:cron:test-job", + }); + params.agentSessionKey = "agent:main:cron:test-job"; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + const state = await dispatchCronDelivery(params); + + expect(state.cronRunSessionCleanupAttempted).toBe(true); + expect(retireSessionMcpRuntime).not.toHaveBeenCalled(); + }); + + it("restores the guarded delivery mirror when a persistent session survives cleanup failure", async () => { + mockResolvedOutboundRoute({ + sessionKey: "agent:main:cron:test-job", + baseSessionKey: "agent:main:cron:test-job", + }); + vi.mocked(callGateway).mockRejectedValueOnce(new Error("gateway down")); + + const params = makeBaseParams({ + synthesizedText: "Delivered report", + sessionTarget: "session:agent:main:cron:test-job", + }); + params.agentSessionKey = "agent:main:cron:test-job"; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + await dispatchCronDelivery(params); + + expect(appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith( + expect.objectContaining({ + sessionKey: "agent:main:cron:test-job", + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + text: "Delivered report", + }), + ); + }); + + it("does not append the deferred mirror after archive wins the cleanup gap", async () => { + mockResolvedOutboundRoute({ + sessionKey: "agent:main:cron:test-job", + baseSessionKey: "agent:main:cron:test-job", + }); + const changedError = new Error("session changed") as Error & { + gatewayCode: string; + details: { reason: string }; + }; + changedError.name = "GatewayClientRequestError"; + changedError.gatewayCode = "INVALID_REQUEST"; + changedError.details = { reason: "session-changed" }; + vi.mocked(callGateway).mockRejectedValueOnce(changedError); + loadCronSessionEntryLatestMock.mockReturnValue({ + sessionId: "test-session-id", + lifecycleRevision: "test-lifecycle-revision", + archivedAt: Date.now(), + }); + + const params = makeBaseParams({ + synthesizedText: "Delivered report", + sessionTarget: "session:agent:main:cron:test-job", + }); + params.agentSessionKey = "agent:main:cron:test-job"; + (params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true; + + await dispatchCronDelivery(params); + + expect(loadCronSessionEntryLatestMock).toHaveBeenCalledWith( + expect.any(String), + "agent:main:cron:test-job", + ); + expect(appendAssistantMessageToSessionTranscript).not.toHaveBeenCalled(); + }); + it("skips deleteAfterRun cleanup for non-cron sessions", async () => { const params = makeBaseParams({ synthesizedText: SILENT_REPLY_TOKEN }); params.agentSessionKey = "agent:main:whatsapp:direct:+15551234567"; @@ -1522,6 +1711,9 @@ describe("dispatchCronDelivery — double-announce guard", () => { key: "agent:main:cron:test-job", deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 1_000, }, timeoutMs: 10_000, }); @@ -1559,6 +1751,9 @@ describe("dispatchCronDelivery — double-announce guard", () => { key: "agent:main:cron:test-job", deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 1_000, }, timeoutMs: 10_000, }); @@ -2000,6 +2195,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { channel: "whatsapp", to: "+15551234567", }); + loadCronSessionEntryLatestMock.mockReturnValue({ sessionId: "test-session-id" }); const state = await dispatchCronDelivery(params); @@ -2030,6 +2226,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { expect(appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith({ sessionKey: "agent:main:whatsapp:direct:+15551234567", agentId: "main", + expectedSessionId: "test-session-id", text: "REPRO_TOKEN_K7M3X9", mediaUrls: undefined, storePath: expect.stringContaining("cron-mirror-sessions.json"), @@ -2038,6 +2235,31 @@ describe("dispatchCronDelivery — double-announce guard", () => { }); }); + it("does not mirror a direct delivery into an archived destination session", async () => { + mockResolvedOutboundRoute({ + sessionKey: "agent:main:whatsapp:direct:+15551234567", + baseSessionKey: "agent:main:whatsapp:direct:+15551234567", + peer: { kind: "direct", id: "+15551234567" }, + from: "whatsapp:+15551234567", + to: "+15551234567", + }); + loadCronSessionEntryLatestMock.mockReturnValue({ + sessionId: "archived-session-id", + archivedAt: Date.now(), + }); + + const params = makeBaseParams({ synthesizedText: "Delivered outside OpenClaw" }); + params.resolvedDelivery = makeResolvedDelivery({ + channel: "whatsapp", + to: "+15551234567", + }); + + const state = await dispatchCronDelivery(params); + + expect(state.delivered).toBe(true); + expect(appendAssistantMessageToSessionTranscript).not.toHaveBeenCalled(); + }); + it("keeps successful direct delivery delivered when the transcript mirror append fails", async () => { mockResolvedOutboundRoute({ sessionKey: "agent:main:whatsapp:direct:+15551234567", @@ -2095,6 +2317,8 @@ describe("dispatchCronDelivery — double-announce guard", () => { expect(appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith({ sessionKey: "agent:main:session:daily-report", agentId: "main", + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", text: "custom-session report", mediaUrls: undefined, storePath: expect.stringContaining("cron-custom-session-mirror.json"), @@ -2142,6 +2366,9 @@ describe("dispatchCronDelivery — double-announce guard", () => { key: "agent:main:cron:test-job", deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 1_000, }, timeoutMs: 10_000, }); @@ -2199,6 +2426,9 @@ describe("dispatchCronDelivery — double-announce guard", () => { key: "agent:main:cron:test-job", deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 1_000, }, timeoutMs: 10_000, }); @@ -2248,6 +2478,9 @@ describe("dispatchCronDelivery — double-announce guard", () => { key: "agent:main:cron:test-job", deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 1_000, }, timeoutMs: 10_000, }); diff --git a/src/cron/isolated-agent/delivery-dispatch.ts b/src/cron/isolated-agent/delivery-dispatch.ts index cda0bba3cdce..3d8da241c641 100644 --- a/src/cron/isolated-agent/delivery-dispatch.ts +++ b/src/cron/isolated-agent/delivery-dispatch.ts @@ -11,6 +11,7 @@ import { } from "../../auto-reply/tokens.js"; import type { CliDeps } from "../../cli/outbound-send-deps.js"; import { resolveStorePath } from "../../config/sessions/inbound.runtime.js"; +import { resolveSessionWorkStartError } from "../../config/sessions/lifecycle.js"; import { canonicalizeMainSessionAlias, resolveAgentMainSessionKey, @@ -38,9 +39,11 @@ import { normalizeTargetForProvider } from "../../infra/outbound/target-normaliz import { hasReplyPayloadContent } from "../../interactive/payload.js"; import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js"; import { + isCronSessionKey, parseThreadSessionSuffix, resolveAgentIdFromSessionKey, } from "../../routing/session-key.js"; +import { beginSessionWorkAdmission } from "../../sessions/session-lifecycle-admission.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; import { shouldAttemptTtsPayload } from "../../tts/tts-config.js"; import { createCronExecutionId } from "../run-id.js"; @@ -48,8 +51,13 @@ import { hasScheduledNextRunAtMs } from "../service/jobs.js"; import type { CronJob, CronRunTelemetry } from "../types.js"; import type { DeliveryTargetResolution } from "./delivery-target.js"; import { pickLastNonEmptyTextFromPayloads, pickSummaryFromOutput } from "./helpers.js"; +import { resolveCronLifecycleRevisionIdentity } from "./run-session-state.js"; import type { RunCronAgentTurnResult } from "./run.types.js"; -import { cleanupCronRunSessionAfterRun } from "./session-cleanup.js"; +import { + cleanupCronRunSessionAfterRun, + type CronRunSessionCleanupOutcome, +} from "./session-cleanup.js"; +import { loadCronSessionEntryLatest } from "./session.js"; import { expectsSubagentFollowup, isLikelyInterimCronMessage } from "./subagent-followup-hints.js"; function normalizeDeliveryTarget(channel: string, to: string): string { @@ -107,6 +115,9 @@ type DispatchCronDeliveryParams = { agentSessionKey: string; runSessionKey: string; sessionId: string; + lifecycleRevision: string; + sessionUpdatedAt: number; + beforeSessionDelete?: () => void; runStartedAt: number; runEndedAt: number; timeoutMs: number; @@ -130,6 +141,18 @@ type DispatchCronDeliveryParams = { ) => RunCronAgentTurnResult; }; +type DirectCronTranscriptMirror = { + sessionKey: string; + agentId: string; + expectedSessionId?: string; + expectedLifecycleRevision?: string; + text?: string; + mediaUrls?: string[]; + storePath?: string; + idempotencyKey: string; + config: OpenClawConfig; +}; + /** Mutable delivery-dispatch accumulator returned to the isolated cron runner. */ export type DispatchCronDeliveryState = { result?: RunCronAgentTurnResult; @@ -247,12 +270,18 @@ export async function cleanupDirectCronSession(params: { job: CronJob; agentSessionKey: string; sessionId: string; + lifecycleRevision: string; + sessionUpdatedAt: number; + beforeSessionDelete?: () => void; retireReason: string; }): Promise { await cleanupCronRunSessionAfterRun({ job: params.job, agentSessionKey: params.agentSessionKey, sessionId: params.sessionId, + lifecycleRevision: params.lifecycleRevision, + sessionUpdatedAt: params.sessionUpdatedAt, + beforeDelete: params.beforeSessionDelete, reason: params.retireReason, }); } @@ -804,15 +833,7 @@ export async function queueCronMessageToolDeliveryAwareness(params: { async function appendDirectCronDeliveryTranscriptMirror(params: { job: CronJob; - mirror: { - sessionKey: string; - agentId: string; - text?: string; - mediaUrls?: string[]; - storePath?: string; - idempotencyKey: string; - config: OpenClawConfig; - }; + mirror: DirectCronTranscriptMirror; }): Promise { if (!params.mirror.text && !params.mirror.mediaUrls?.length) { return; @@ -832,6 +853,75 @@ async function appendDirectCronDeliveryTranscriptMirror(params: { } } +async function appendAdmittedDirectCronDeliveryTranscriptMirror(params: { + job: CronJob; + mirror: DirectCronTranscriptMirror; + abortSignal?: AbortSignal; +}): Promise { + const storePath = params.mirror.storePath; + const initial = storePath + ? loadCronSessionEntryLatest(storePath, params.mirror.sessionKey) + : undefined; + const expectedSessionId = params.mirror.expectedSessionId ?? initial?.sessionId; + const expectedLifecycleRevision = + params.mirror.expectedLifecycleRevision ?? initial?.lifecycleRevision; + if (!storePath || !expectedSessionId) { + await logCronDeliveryWarn( + `[cron:${params.job.id}] skipped transcript mirror without an exact session identity`, + ); + return; + } + const admittedMirror = { + ...params.mirror, + expectedSessionId, + ...(expectedLifecycleRevision ? { expectedLifecycleRevision } : {}), + }; + + try { + const admission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [ + params.mirror.sessionKey, + expectedSessionId, + expectedLifecycleRevision + ? resolveCronLifecycleRevisionIdentity(expectedLifecycleRevision) + : undefined, + ], + signal: params.abortSignal, + assertAllowed: () => { + const latest = loadCronSessionEntryLatest(storePath, params.mirror.sessionKey); + if ( + latest?.sessionId !== expectedSessionId || + (expectedLifecycleRevision !== undefined && + latest.lifecycleRevision !== expectedLifecycleRevision) + ) { + throw new Error( + `Session "${params.mirror.sessionKey}" changed before transcript mirror.`, + ); + } + const archivedError = resolveSessionWorkStartError(params.mirror.sessionKey, latest); + if (archivedError) { + throw new Error(archivedError); + } + }, + }); + try { + await admission.run(() => + appendDirectCronDeliveryTranscriptMirror({ + job: params.job, + mirror: admittedMirror, + }), + ); + } finally { + admission.release(); + } + } catch (err) { + await logCronDeliveryWarn( + `[cron:${params.job.id}] skipped transcript mirror: ${formatErrorMessage(err)}`, + ); + } +} + /** Clears the direct-delivery idempotency cache for deterministic tests. */ export function resetCompletedDirectCronDeliveriesForTests() { COMPLETED_DIRECT_CRON_DELIVERIES.clear(); @@ -917,6 +1007,7 @@ export async function dispatchCronDelivery( let delivered = verifiedMessageToolDelivery; let deliveryAttempted = verifiedMessageToolDelivery; let directCronSessionCleanupAttempted = false; + let deferredDeletingSessionMirror: DirectCronTranscriptMirror | undefined; const buildDeliveryState = (result?: RunCronAgentTurnResult): DispatchCronDeliveryState => ({ ...(result ? { result } : {}), delivered, @@ -941,19 +1032,32 @@ export async function dispatchCronDelivery( deliveryAttempted, ...params.telemetry, }); - const cleanupDirectCronSessionIfNeeded = async (): Promise => { + const cleanupDirectCronSessionIfNeeded = async (): Promise => { if (directCronSessionCleanupAttempted) { - return; + return "not-requested"; } - const cleanupAttempted = await cleanupCronRunSessionAfterRun({ + const cleanupOutcome = await cleanupCronRunSessionAfterRun({ job: params.job, agentSessionKey: params.agentSessionKey, sessionId: params.sessionId, + lifecycleRevision: params.lifecycleRevision, + sessionUpdatedAt: params.sessionUpdatedAt, + beforeDelete: params.beforeSessionDelete, reason: "cron-delete-after-run-fallback", }); - if (cleanupAttempted) { + if (cleanupOutcome !== "not-requested") { directCronSessionCleanupAttempted = true; } + const survivingMirror = deferredDeletingSessionMirror; + deferredDeletingSessionMirror = undefined; + if (cleanupOutcome !== "not-requested" && cleanupOutcome !== "deleted" && survivingMirror) { + await appendAdmittedDirectCronDeliveryTranscriptMirror({ + job: params.job, + mirror: survivingMirror, + abortSignal: params.abortSignal, + }); + } + return cleanupOutcome; }; const finishSilentReplyDelivery = async (): Promise => { deliveryAttempted = true; @@ -1081,6 +1185,10 @@ export async function dispatchCronDelivery( deliverySessionKey, awarenessMainSessionKey, ); + const mirrorTargetsDeletingRunSession = + params.job.deleteAfterRun === true && + isCronSessionKey(params.agentSessionKey) && + isSameSessionKey(deliverySessionKey, params.agentSessionKey); // Track bestEffort partial failures so we can log them and avoid // marking the job as delivered when payloads were silently dropped. @@ -1217,6 +1325,12 @@ export async function dispatchCronDelivery( const transcriptMirror = { sessionKey: deliverySessionKey, agentId: params.agentId, + ...(mirrorTargetsDeletingRunSession + ? { + expectedSessionId: params.sessionId, + expectedLifecycleRevision: params.lifecycleRevision, + } + : {}), text: mirrorText, // Keep cron delivery mirrors text-first: non-audio attachment names // are folded into mirrorText so media does not replace delivered text. @@ -1227,10 +1341,15 @@ export async function dispatchCronDelivery( idempotencyKey: deliveryIdempotencyKey, config: params.cfgWithAgentDefaults, }; - await appendDirectCronDeliveryTranscriptMirror({ - job: params.job, - mirror: transcriptMirror, - }); + if (mirrorTargetsDeletingRunSession) { + deferredDeletingSessionMirror = transcriptMirror; + } else { + await appendAdmittedDirectCronDeliveryTranscriptMirror({ + job: params.job, + mirror: transcriptMirror, + abortSignal: params.abortSignal, + }); + } } if ( delivered && diff --git a/src/cron/isolated-agent/run-session-state.test.ts b/src/cron/isolated-agent/run-session-state.test.ts index 30c0ef68ec74..196e000d87e6 100644 --- a/src/cron/isolated-agent/run-session-state.test.ts +++ b/src/cron/isolated-agent/run-session-state.test.ts @@ -6,9 +6,11 @@ import path from "node:path"; import { afterAll, describe, expect, it, vi } from "vitest"; import { cleanupTempDirs, makeTempDir } from "../../../test/helpers/temp-dir.js"; import type { SessionEntry } from "../../config/sessions.js"; +import { beginSessionWorkAdmission } from "../../sessions/session-lifecycle-admission.js"; import { adoptCronRunSessionMetadata, createPersistCronSessionEntry, + resolveCronLifecycleRevisionIdentity, type MutableCronSession, } from "./run-session-state.js"; @@ -74,6 +76,7 @@ describe("createPersistCronSessionEntry", () => { ); const cronSession = makeCronSession( makeSessionEntry({ + lifecycleRevision: "run-revision", sessionFile: missingTranscriptPath, label: "Cron: shell-only", status: "running", @@ -85,6 +88,7 @@ describe("createPersistCronSessionEntry", () => { update(store); expect(store["agent:main:cron:shell-only"]).toEqual({ label: "Cron: shell-only", + lifecycleRevision: "run-revision", status: "running", updatedAt: 1000, systemSent: true, @@ -103,6 +107,9 @@ describe("createPersistCronSessionEntry", () => { expect(cronSession.store["agent:main:cron:shell-only"]?.sessionId).toBeUndefined(); expect(cronSession.store["agent:main:cron:shell-only"]?.sessionFile).toBeUndefined(); + expect(cronSession.store["agent:main:cron:shell-only"]?.lifecycleRevision).toBe("run-revision"); + expect(cronSession.sessionEntry.sessionId).toBe("run-session-id"); + expect(cronSession.sessionEntry.sessionFile).toBe(missingTranscriptPath); }); it("restores resumable cron fields once the transcript exists", async () => { @@ -166,6 +173,236 @@ describe("createPersistCronSessionEntry", () => { expect(cronSession.store["agent:main:session"]).toBe(cronSession.sessionEntry); }); + it("does not let an older concurrent run reclaim a persisted lifecycle revision", async () => { + const sessionKey = "agent:main:session"; + const initialSessionEntry = makeSessionEntry({ lifecycleRevision: "initial-revision" }); + const persistedStore: Record = { + [sessionKey]: initialSessionEntry, + }; + const makeConcurrentSession = (lifecycleRevision: string): MutableCronSession => + ({ + ...makeCronSession( + makeSessionEntry({ + lifecycleRevision, + label: lifecycleRevision, + }), + ), + initialSessionEntry, + lifecycleRevision, + }) as MutableCronSession; + const updateSessionStore = vi.fn( + async (_storePath, update: (store: Record) => void) => { + update(persistedStore); + }, + ); + const olderSession = makeConcurrentSession("older-revision"); + const newerSession = makeConcurrentSession("newer-revision"); + const persistOlder = createPersistCronSessionEntry({ + isFastTestEnv: false, + cronSession: olderSession, + agentSessionKey: sessionKey, + updateSessionStore, + }); + const persistNewer = createPersistCronSessionEntry({ + isFastTestEnv: false, + cronSession: newerSession, + agentSessionKey: sessionKey, + updateSessionStore, + }); + + await persistNewer(); + await expect(persistOlder()).rejects.toThrow( + `Session "${sessionKey}" changed while starting work. Retry.`, + ); + + expect(persistedStore[sessionKey]).toStrictEqual(newerSession.sessionEntry); + expect(olderSession.store[sessionKey]).toBeUndefined(); + }); + + it("does not replace a lifecycle revision while its owner is admitted", async () => { + const sessionKey = "agent:main:session"; + const storePath = "/tmp/sessions-active-lifecycle.json"; + const activeRevision = crypto.randomUUID(); + const nextRevision = crypto.randomUUID(); + const activeEntry = makeSessionEntry({ lifecycleRevision: activeRevision }); + const persistedStore: Record = { [sessionKey]: activeEntry }; + const nextSession = { + ...makeCronSession(makeSessionEntry({ lifecycleRevision: nextRevision })), + initialSessionEntry: activeEntry, + lifecycleRevision: nextRevision, + storePath, + } as MutableCronSession; + const persistNext = createPersistCronSessionEntry({ + isFastTestEnv: false, + cronSession: nextSession, + agentSessionKey: sessionKey, + updateSessionStore: async (_storePath, update) => { + update(persistedStore); + }, + }); + const activeLease = await beginSessionWorkAdmission({ + scope: storePath, + identities: [resolveCronLifecycleRevisionIdentity(activeRevision)], + assertAllowed: () => {}, + }); + + try { + await expect(persistNext()).rejects.toThrow( + `Session "${sessionKey}" changed while starting work. Retry.`, + ); + expect(persistedStore[sessionKey]).toBe(activeEntry); + } finally { + activeLease.release(); + } + await expect(persistNext()).resolves.toBeUndefined(); + expect(persistedStore[sessionKey]).toStrictEqual(nextSession.sessionEntry); + }); + + it("claims an initial row after a concurrent pin and rename", async () => { + const sessionKey = "agent:main:session"; + const lifecycleRevision = crypto.randomUUID(); + const initialSessionEntry = makeSessionEntry({ lifecycleRevision: "initial-revision" }); + const cronSession = { + ...makeCronSession( + makeSessionEntry({ + lifecycleRevision, + status: "running", + }), + ), + initialSessionEntry, + lifecycleRevision, + } as MutableCronSession; + const persistedStore: Record = { + [sessionKey]: { + ...initialSessionEntry, + label: "Renamed before claim", + pinnedAt: 2000, + updatedAt: 2000, + }, + }; + const persist = createPersistCronSessionEntry({ + isFastTestEnv: false, + cronSession, + agentSessionKey: sessionKey, + updateSessionStore: async (_storePath, update) => { + update(persistedStore); + }, + }); + + await expect(persist()).resolves.toBeUndefined(); + expect(persistedStore[sessionKey]).toMatchObject({ + label: "Renamed before claim", + lifecycleRevision, + pinnedAt: 2000, + status: "running", + updatedAt: 2000, + }); + }); + + it.each([ + { + name: "pin and rename", + current: { label: "Renamed", pinnedAt: 2000, updatedAt: 2000 }, + expected: { label: "Renamed", pinnedAt: 2000, updatedAt: 2000 }, + }, + { + name: "unpin and clear the label", + current: { label: undefined, pinnedAt: undefined, updatedAt: 2000 }, + expected: { label: undefined, pinnedAt: undefined, updatedAt: 2000 }, + }, + ])("preserves a concurrent $name during cron persistence", async ({ current, expected }) => { + const sessionKey = "agent:main:session"; + const lifecycleRevision = crypto.randomUUID(); + const runEntry = makeSessionEntry({ + lifecycleRevision, + label: "Original", + pinnedAt: 1000, + status: "done", + }); + const cronSession = { + ...makeCronSession(runEntry), + initialSessionEntry: { ...runEntry }, + lifecycleRevision, + } as MutableCronSession; + const persistedStore: Record = { + [sessionKey]: { + ...cronSession.sessionEntry, + ...current, + }, + }; + const persist = createPersistCronSessionEntry({ + isFastTestEnv: false, + cronSession, + agentSessionKey: sessionKey, + updateSessionStore: async (_storePath, update) => { + update(persistedStore); + }, + }); + + await persist(); + + expect(persistedStore[sessionKey]).toMatchObject({ + lifecycleRevision, + status: "done", + updatedAt: expected.updatedAt, + }); + expect(persistedStore[sessionKey]?.label).toBe(expected.label); + expect(persistedStore[sessionKey]?.pinnedAt).toBe(expected.pinnedAt); + expect(cronSession.sessionEntry.label).toBe(expected.label); + expect(cronSession.sessionEntry.pinnedAt).toBe(expected.pinnedAt); + expect(cronSession.sessionEntry.updatedAt).toBe(expected.updatedAt); + }); + + it("does not restore session policy cleared while a cron run is active", async () => { + const sessionKey = "agent:main:session"; + const lifecycleRevision = crypto.randomUUID(); + const initialSessionEntry = makeSessionEntry({ + lifecycleRevision, + chatType: "direct", + elevatedLevel: "full", + inheritedToolAllow: ["exec"], + sendPolicy: "allow", + }); + const cronSession = { + ...makeCronSession({ + ...initialSessionEntry, + status: "done", + totalTokens: 42, + }), + initialSessionEntry, + lifecycleRevision, + } as MutableCronSession; + const currentEntry: SessionEntry = { + ...initialSessionEntry, + chatType: "group", + sendPolicy: "deny", + updatedAt: 2000, + }; + delete currentEntry.elevatedLevel; + delete currentEntry.inheritedToolAllow; + const persistedStore: Record = { [sessionKey]: currentEntry }; + const persist = createPersistCronSessionEntry({ + isFastTestEnv: false, + cronSession, + agentSessionKey: sessionKey, + updateSessionStore: async (_storePath, update) => { + update(persistedStore); + }, + }); + + await persist(); + + expect(persistedStore[sessionKey]).toMatchObject({ + chatType: "group", + sendPolicy: "deny", + status: "done", + totalTokens: 42, + updatedAt: 2000, + }); + expect(persistedStore[sessionKey]?.elevatedLevel).toBeUndefined(); + expect(persistedStore[sessionKey]?.inheritedToolAllow).toBeUndefined(); + }); + it("adopts rotated run transcript metadata before persisting session-bound cron state", async () => { const cronSession = makeCronSession( makeSessionEntry({ diff --git a/src/cron/isolated-agent/run-session-state.ts b/src/cron/isolated-agent/run-session-state.ts index 1807e7003a82..3bb73459d9cb 100644 --- a/src/cron/isolated-agent/run-session-state.ts +++ b/src/cron/isolated-agent/run-session-state.ts @@ -1,8 +1,11 @@ /** Mutates and persists isolated cron session state around one run. */ import fs from "node:fs"; +import { isDeepStrictEqual } from "node:util"; import type { LiveSessionModelSelection } from "../../agents/live-model-switch.js"; import type { SessionEntry } from "../../config/sessions.js"; +import { mergeSessionSnapshotChanges } from "../../config/sessions/session-snapshot-merge.js"; import { isCronSessionKey } from "../../sessions/session-key-utils.js"; +import { isSessionWorkAdmissionActive } from "../../sessions/session-lifecycle-admission.js"; import type { SkillSnapshot } from "../../skills/types.js"; import type { resolveCronSession } from "./session.js"; @@ -26,6 +29,17 @@ type UpdateSessionStore = ( /** Persists the currently selected mutable cron session entry to the session store. */ export type PersistCronSessionEntry = () => Promise; +export class CronSessionLifecycleClaimError extends Error { + constructor(sessionKey: string) { + super(`Session "${sessionKey}" changed while starting work. Retry.`); + this.name = "CronSessionLifecycleClaimError"; + } +} + +export function resolveCronLifecycleRevisionIdentity(lifecycleRevision: string): string { + return `cron-lifecycle-revision:${lifecycleRevision}`; +} + function cronTranscriptExists(entry: SessionEntry): boolean { const sessionFile = entry.sessionFile?.trim(); return Boolean(sessionFile && fs.existsSync(sessionFile)); @@ -36,6 +50,14 @@ function normalizeSessionField(value: string | undefined): string | undefined { return trimmed ? trimmed : undefined; } +export function projectCronOwnershipFields(entry: SessionEntry): Partial { + const projected: Partial = { ...entry }; + delete projected.label; + delete projected.pinnedAt; + delete projected.updatedAt; + return projected; +} + function toNonResumableCronSessionEntry(entry: SessionEntry): SessionEntry { const next = { ...entry } as Partial; // If the transcript never materialized, do not persist stale resume handles @@ -61,18 +83,63 @@ export function createPersistCronSessionEntry(params: { if (params.isFastTestEnv) { return; } + const liveEntry = params.cronSession.sessionEntry; const persistedEntry = isCronSessionKey(params.agentSessionKey) && - params.cronSession.sessionEntry.sessionId && - !cronTranscriptExists(params.cronSession.sessionEntry) - ? toNonResumableCronSessionEntry(params.cronSession.sessionEntry) - : params.cronSession.sessionEntry; - // Update both the in-memory store and persisted JSON so later operations in - // this process observe the same session entry that hit disk. - params.cronSession.store[params.agentSessionKey] = persistedEntry; + liveEntry.sessionId && + !cronTranscriptExists(liveEntry) + ? toNonResumableCronSessionEntry(liveEntry) + : liveEntry; + let committedEntry = persistedEntry; + let mergedLiveEntry = liveEntry; await params.updateSessionStore(params.cronSession.storePath, (store) => { - store[params.agentSessionKey] = persistedEntry; + const currentEntry = store[params.agentSessionKey]; + const ownsCurrentRevision = + currentEntry?.lifecycleRevision === params.cronSession.lifecycleRevision; + const currentRevisionActive = Boolean( + currentEntry?.lifecycleRevision && + isSessionWorkAdmissionActive(params.cronSession.storePath, [ + resolveCronLifecycleRevisionIdentity(currentEntry.lifecycleRevision), + ]), + ); + const initialEntryMatchesOwnershipFields = + currentEntry !== undefined && + params.cronSession.initialSessionEntry !== undefined && + isDeepStrictEqual( + projectCronOwnershipFields(currentEntry), + projectCronOwnershipFields(params.cronSession.initialSessionEntry), + ); + const canClaimInitialRevision = params.cronSession.initialSessionEntry + ? !currentRevisionActive && initialEntryMatchesOwnershipFields + : currentEntry === undefined; + // Concurrent persistent runs can resolve the same initial row. Once one + // revision claims it, older owners must not reclaim it and delete newer state. + if (!ownsCurrentRevision && !canClaimInitialRevision) { + throw new CronSessionLifecycleClaimError(params.agentSessionKey); + } + if ( + (ownsCurrentRevision || canClaimInitialRevision) && + currentEntry && + params.cronSession.initialSessionEntry + ) { + committedEntry = mergeSessionSnapshotChanges({ + initial: params.cronSession.initialSessionEntry, + next: persistedEntry, + current: currentEntry, + }); + mergedLiveEntry = mergeSessionSnapshotChanges({ + initial: params.cronSession.initialSessionEntry, + next: liveEntry, + current: currentEntry, + }); + } + store[params.agentSessionKey] = committedEntry; }); + // The storage projection may intentionally omit resume identity until its + // transcript exists. Keep that projection out of the active run object. + params.cronSession.sessionEntry = mergedLiveEntry; + params.cronSession.initialSessionEntry = structuredClone(committedEntry); + params.cronSession.store[params.agentSessionKey] = committedEntry; }; } diff --git a/src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts b/src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts index a4098b99cdf4..85249729b589 100644 --- a/src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts +++ b/src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts @@ -735,20 +735,14 @@ describe("runCronIsolatedAgentTurn — cron model override forwarding (#58065)", expect(capturedFallbacksOverride).toEqual(["openai/gpt-4o"]); }); - it("forwards the cron abort signal into runWithModelFallback", async () => { + it("rejects a pre-aborted cron turn before model fallback starts", async () => { const controller = new AbortController(); - let capturedAbortSignal: AbortSignal | undefined; - runWithModelFallbackMock.mockImplementation( - async (params: { provider: string; model: string; abortSignal?: AbortSignal }) => { - capturedAbortSignal = params.abortSignal; - return makeSuccessfulRunResult(); - }, - ); controller.abort(new Error("cron: job execution timed out")); - await runCronIsolatedAgentTurn(makeParams({ abortSignal: controller.signal })); - - expect(capturedAbortSignal).toBe(controller.signal); + await expect( + runCronIsolatedAgentTurn(makeParams({ abortSignal: controller.signal })), + ).rejects.toThrow("cron: job execution timed out"); + expect(runWithModelFallbackMock).not.toHaveBeenCalled(); }); }); diff --git a/src/cron/isolated-agent/run.cron-model-override.test.ts b/src/cron/isolated-agent/run.cron-model-override.test.ts index fa4bab1094f1..1acb5c2d0182 100644 --- a/src/cron/isolated-agent/run.cron-model-override.test.ts +++ b/src/cron/isolated-agent/run.cron-model-override.test.ts @@ -140,11 +140,13 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => { modelProvider?: string; systemSent?: boolean; }> = []; + // One persistent store across persist calls: the lifecycle claim guard + // treats a store that lost the entry between calls as a foreign owner. + const persistentStore: Record = {}; updateSessionStoreMock.mockImplementation( async (_path: string, cb: (s: Record) => void) => { - const store: Record = {}; - cb(store); - const entry = Object.values(store)[0] as + cb(persistentStore); + const entry = Object.values(persistentStore)[0] as | { model?: string; modelProvider?: string; systemSent?: boolean } | undefined; if (entry) { diff --git a/src/cron/isolated-agent/run.fast-mode.test.ts b/src/cron/isolated-agent/run.fast-mode.test.ts index 255ad4b0073f..50b22001f226 100644 --- a/src/cron/isolated-agent/run.fast-mode.test.ts +++ b/src/cron/isolated-agent/run.fast-mode.test.ts @@ -173,6 +173,9 @@ describe("runCronIsolatedAgentTurn — fast mode", () => { key: "agent:default:cron:test", deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: "test-session-id", + expectedLifecycleRevision: "test-lifecycle-revision", + expectedSessionUpdatedAt: 0, }, timeoutMs: 10_000, }); diff --git a/src/cron/isolated-agent/run.message-tool-policy.test.ts b/src/cron/isolated-agent/run.message-tool-policy.test.ts index f36084264f37..0607eee664e6 100644 --- a/src/cron/isolated-agent/run.message-tool-policy.test.ts +++ b/src/cron/isolated-agent/run.message-tool-policy.test.ts @@ -1176,6 +1176,9 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { job: expect.objectContaining({ id: "fatal-error-payload" }), agentSessionKey: "agent:default:cron:message-tool-policy", sessionId: "test-session-id", + lifecycleRevision: "test-lifecycle-revision", + sessionUpdatedAt: expect.any(Number), + beforeSessionDelete: expect.any(Function), retireReason: "cron-delete-after-run-fatal-error", }); expectDeliveryFields(result.delivery, { @@ -1212,6 +1215,9 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { }), agentSessionKey: "agent:default:cron:message-tool-policy", sessionId: "test-session-id", + lifecycleRevision: "test-lifecycle-revision", + sessionUpdatedAt: expect.any(Number), + beforeSessionDelete: expect.any(Function), retireReason: "cron-delete-after-run-fatal-error", }); }); diff --git a/src/cron/isolated-agent/run.meta-error-status.test.ts b/src/cron/isolated-agent/run.meta-error-status.test.ts index 3ec47bc52d71..a8991a1ba4b7 100644 --- a/src/cron/isolated-agent/run.meta-error-status.test.ts +++ b/src/cron/isolated-agent/run.meta-error-status.test.ts @@ -82,6 +82,9 @@ describe("runCronIsolatedAgentTurn - meta.error status propagation", () => { job: expect.objectContaining({ deleteAfterRun: true }), agentSessionKey: "agent:default:cron:test", sessionId: "test-session-id", + lifecycleRevision: "test-lifecycle-revision", + sessionUpdatedAt: expect.any(Number), + beforeSessionDelete: expect.any(Function), retireReason: "cron-delete-after-run-aborted", }); }); @@ -111,19 +114,11 @@ describe("runCronIsolatedAgentTurn - meta.error status propagation", () => { ); timeoutError.name = "TimeoutError"; abortController.abort(timeoutError); - runWithModelFallbackMock.mockRejectedValueOnce( - new Error( - 'All models failed (2): openai/gpt-5.5: Command lane "cron-nested" task timed out after 330000ms (timeout)', + await expect( + runCronIsolatedAgentTurn( + makeIsolatedAgentParamsFixture({ abortSignal: abortController.signal }), ), - ); - - const result = await runCronIsolatedAgentTurn( - makeIsolatedAgentParamsFixture({ abortSignal: abortController.signal }), - ); - - expect(result.status).toBe("error"); - expect(result.error).toBe("cron: job execution timed out (last phase: model_call_started)"); - expect(result.error).not.toContain("All models failed"); - expect(result.error).not.toContain("cron-nested"); + ).rejects.toBe(timeoutError); + expect(runWithModelFallbackMock).not.toHaveBeenCalled(); }); }); diff --git a/src/cron/isolated-agent/run.session-lifecycle.test.ts b/src/cron/isolated-agent/run.session-lifecycle.test.ts new file mode 100644 index 000000000000..a8ad9cefab5e --- /dev/null +++ b/src/cron/isolated-agent/run.session-lifecycle.test.ts @@ -0,0 +1,302 @@ +// Persistent cron session tests cover lifecycle admission and mutation races. +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + interruptSessionWorkAdmissions, + isSessionWorkAdmissionActive, + runExclusiveSessionLifecycleMutation, +} from "../../sessions/session-lifecycle-admission.js"; +import { makeIsolatedAgentJobFixture, makeIsolatedAgentParamsFixture } from "./job-fixtures.js"; +import { + dispatchCronDeliveryMock, + loadRunCronIsolatedAgentTurn, + loadSessionEntryMock, + callGatewayMock, + makeCronSession, + makeCronSessionEntry, + mockRunCronFallbackPassthrough, + preflightCronModelProviderMock, + resetRunCronIsolatedAgentTurnHarness, + resolveCronSessionMock, + runEmbeddedAgentMock, +} from "./run.test-harness.js"; + +const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); + +function createDeferred() { + let resolve!: () => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +function makePersistentCronParams(sessionKey: string) { + return makeIsolatedAgentParamsFixture({ + agentId: "main", + sessionKey, + job: makeIsolatedAgentJobFixture({ + sessionTarget: "current", + delivery: { mode: "none" }, + }), + }); +} + +describe("runCronIsolatedAgentTurn session lifecycle", () => { + beforeEach(() => { + resetRunCronIsolatedAgentTurnHarness(); + mockRunCronFallbackPassthrough(); + }); + + it("rejects a session that rotates during async setup", async () => { + const sessionKey = "agent:main:main"; + const initialSessionEntry = makeCronSessionEntry({ sessionId: "session-before-setup" }); + resolveCronSessionMock.mockReturnValue( + makeCronSession({ + storePath: "/tmp/cron-lifecycle-rotation.json", + initialSessionEntry, + isNewSession: false, + sessionEntry: { ...initialSessionEntry }, + }), + ); + loadSessionEntryMock.mockReturnValue({ + ...initialSessionEntry, + sessionId: "session-after-setup", + }); + const releasePreflight = createDeferred(); + preflightCronModelProviderMock.mockImplementationOnce(async () => { + await releasePreflight.promise; + return { status: "available" }; + }); + + const run = runCronIsolatedAgentTurn(makePersistentCronParams(sessionKey)); + await vi.waitFor(() => expect(preflightCronModelProviderMock).toHaveBeenCalledTimes(1)); + releasePreflight.resolve(); + + await expect(run).rejects.toThrow( + `Session "${sessionKey}" changed while starting work. Retry.`, + ); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); + }); + + it("allows a rename and unpin during async setup", async () => { + const sessionKey = "agent:main:main"; + const initialSessionEntry = makeCronSessionEntry({ + label: "before setup", + pinnedAt: 1, + sessionId: "same-session", + updatedAt: 1, + }); + resolveCronSessionMock.mockReturnValue( + makeCronSession({ + storePath: "/tmp/cron-lifecycle-revision.json", + initialSessionEntry, + isNewSession: false, + sessionEntry: { ...initialSessionEntry }, + }), + ); + loadSessionEntryMock.mockReturnValue({ + ...initialSessionEntry, + label: "patched during setup", + pinnedAt: undefined, + updatedAt: 2, + }); + const releasePreflight = createDeferred(); + preflightCronModelProviderMock.mockImplementationOnce(async () => { + await releasePreflight.promise; + return { status: "available" }; + }); + + const run = runCronIsolatedAgentTurn(makePersistentCronParams(sessionKey)); + await vi.waitFor(() => expect(preflightCronModelProviderMock).toHaveBeenCalledTimes(1)); + releasePreflight.resolve(); + + await expect(run).resolves.toMatchObject({ status: "ok" }); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + }); + + it("interrupts persistent cron work and waits for its lifecycle lease to release", async () => { + const sessionKey = "agent:main:telegram:direct:42"; + const sessionId = "shared-session"; + const storePath = "/tmp/cron-lifecycle-interrupt.json"; + const initialSessionEntry = makeCronSessionEntry({ sessionId }); + resolveCronSessionMock.mockReturnValue( + makeCronSession({ + storePath, + initialSessionEntry, + isNewSession: false, + sessionEntry: { ...initialSessionEntry }, + }), + ); + loadSessionEntryMock.mockReturnValue({ ...initialSessionEntry }); + const runnerStarted = createDeferred(); + const lifecycleInterrupted = createDeferred(); + const releaseRunner = createDeferred(); + runEmbeddedAgentMock.mockImplementationOnce( + async ({ abortSignal }: { abortSignal?: AbortSignal }) => { + runnerStarted.resolve(); + if (abortSignal?.aborted) { + lifecycleInterrupted.resolve(); + } else { + abortSignal?.addEventListener("abort", lifecycleInterrupted.resolve, { once: true }); + } + await releaseRunner.promise; + return { + payloads: [], + meta: { aborted: true, agentMeta: {} }, + }; + }, + ); + + const run = runCronIsolatedAgentTurn(makePersistentCronParams(sessionKey)); + await runnerStarted.promise; + let mutationCommitted = false; + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + prepare: async () => { + await interruptSessionWorkAdmissions({ + scope: storePath, + identities: [sessionKey, sessionId], + }); + }, + run: async () => { + mutationCommitted = true; + }, + }); + + await lifecycleInterrupted.promise; + expect(mutationCommitted).toBe(false); + releaseRunner.resolve(); + + const [result] = await Promise.all([run, mutation]); + expect(result).toEqual( + expect.objectContaining({ + status: "error", + error: "agent run aborted for restart", + }), + ); + expect(mutationCommitted).toBe(true); + }); + + it("releases an isolated run lease before delete-after-run cleanup", async () => { + const sessionKey = "agent:main:cron:test-job"; + const sessionId = "isolated-session"; + const storePath = "/tmp/cron-lifecycle-self-delete.json"; + resolveCronSessionMock.mockReturnValue( + makeCronSession({ + storePath, + initialSessionEntry: undefined, + isNewSession: true, + sessionEntry: makeCronSessionEntry({ sessionId }), + }), + ); + loadSessionEntryMock.mockReturnValue(undefined); + let admissionActiveDuringDelete = true; + callGatewayMock.mockImplementationOnce(async () => { + admissionActiveDuringDelete = isSessionWorkAdmissionActive(storePath, [ + sessionKey, + sessionId, + ]); + return { ok: true, deleted: true }; + }); + + const result = await runCronIsolatedAgentTurn( + makeIsolatedAgentParamsFixture({ + agentId: "main", + sessionKey: "cron:test-job", + job: makeIsolatedAgentJobFixture({ + sessionTarget: "isolated", + deleteAfterRun: true, + delivery: { mode: "none" }, + }), + }), + ); + + expect(result.status).toBe("ok"); + expect(callGatewayMock).toHaveBeenCalledTimes(1); + expect(admissionActiveDuringDelete).toBe(false); + }); + + it("keeps a non-deleting isolated run admitted through delivery", async () => { + const sessionKey = "agent:main:cron:test-job"; + const sessionId = "isolated-session"; + const storePath = "/tmp/cron-lifecycle-isolated-delivery.json"; + resolveCronSessionMock.mockReturnValue( + makeCronSession({ + storePath, + initialSessionEntry: undefined, + isNewSession: true, + sessionEntry: makeCronSessionEntry({ sessionId }), + }), + ); + loadSessionEntryMock.mockReturnValue(undefined); + const deliveryStarted = createDeferred(); + const releaseDelivery = createDeferred(); + dispatchCronDeliveryMock.mockImplementationOnce(async ({ deliveryPayloads }) => { + deliveryStarted.resolve(); + await releaseDelivery.promise; + return { + delivered: false, + deliveryAttempted: false, + deliveryPayloads, + }; + }); + + const run = runCronIsolatedAgentTurn( + makeIsolatedAgentParamsFixture({ + agentId: "main", + sessionKey: "cron:test-job", + job: makeIsolatedAgentJobFixture({ + sessionTarget: "isolated", + deleteAfterRun: false, + delivery: { mode: "none" }, + }), + }), + ); + await deliveryStarted.promise; + expect(isSessionWorkAdmissionActive(storePath, [sessionKey, sessionId])).toBe(true); + releaseDelivery.resolve(); + + await expect(run).resolves.toMatchObject({ status: "ok" }); + expect(isSessionWorkAdmissionActive(storePath, [sessionKey, sessionId])).toBe(false); + }); + + it("releases a custom cron session lease before delete-after-run cleanup", async () => { + const sessionKey = "agent:main:cron:cleanup"; + const sessionId = "custom-cron-session"; + const storePath = "/tmp/cron-lifecycle-custom-self-delete.json"; + resolveCronSessionMock.mockReturnValue( + makeCronSession({ + storePath, + initialSessionEntry: makeCronSessionEntry({ sessionId }), + isNewSession: false, + sessionEntry: makeCronSessionEntry({ sessionId }), + }), + ); + loadSessionEntryMock.mockReturnValue(makeCronSessionEntry({ sessionId })); + let admissionActiveDuringDelete = true; + callGatewayMock.mockImplementationOnce(async () => { + admissionActiveDuringDelete = isSessionWorkAdmissionActive(storePath, [ + sessionKey, + sessionId, + ]); + return { ok: true, deleted: true }; + }); + + const result = await runCronIsolatedAgentTurn( + makeIsolatedAgentParamsFixture({ + agentId: "main", + sessionKey, + job: makeIsolatedAgentJobFixture({ + sessionTarget: `session:${sessionKey}`, + deleteAfterRun: true, + delivery: { mode: "none" }, + }), + }), + ); + + expect(result.status).toBe("ok"); + expect(callGatewayMock).toHaveBeenCalledTimes(1); + expect(admissionActiveDuringDelete).toBe(false); + }); +}); diff --git a/src/cron/isolated-agent/run.skill-filter.test.ts b/src/cron/isolated-agent/run.skill-filter.test.ts index 874c12d9230f..12aa106ac60c 100644 --- a/src/cron/isolated-agent/run.skill-filter.test.ts +++ b/src/cron/isolated-agent/run.skill-filter.test.ts @@ -306,7 +306,8 @@ describe("runCronIsolatedAgentTurn — skill filter", () => { isCliProviderMock.mockReturnValue(true); runCliAgentMock.mockImplementationOnce(async (params: { abortSignal?: AbortSignal }) => { - expect(params.abortSignal).toBe(abortController.signal); + expect(params.abortSignal).not.toBe(abortController.signal); + expect(params.abortSignal?.aborted).toBe(false); if (!markCliStarted) { throw new Error("Expected CLI start marker callback to be initialized"); } diff --git a/src/cron/isolated-agent/run.test-harness.ts b/src/cron/isolated-agent/run.test-harness.ts index fcc59d9d40ed..0e030eb87b80 100644 --- a/src/cron/isolated-agent/run.test-harness.ts +++ b/src/cron/isolated-agent/run.test-harness.ts @@ -20,6 +20,7 @@ type CronSession = { storePath: string; store: Record; sessionEntry: CronSessionEntry; + lifecycleRevision: string; systemSent: boolean; isNewSession: boolean; [key: string]: unknown; @@ -62,6 +63,7 @@ export const getCliSessionIdMock = createMock(); export const clearCliSessionMock = createMock(); export const setCliSessionBindingMock = createMock(); export const updateSessionStoreMock = createMock(); +export const loadSessionEntryMock = createMock(); export const resolveCronSessionMock = createMock(); export const logWarnMock = createMock(); export const countActiveDescendantRunsMock = createMock(); @@ -352,6 +354,7 @@ vi.mock("../../channels/plugins/index.js", () => ({ })); vi.mock("./session.js", () => ({ + loadCronSessionEntryLatest: loadSessionEntryMock, resolveCronSession: resolveCronSessionMock, })); @@ -370,6 +373,8 @@ export function makeCronSession(overrides?: Record): CronSessio storePath: "/tmp/store.json", store: {}, sessionEntry: makeCronSessionEntry(), + lifecycleRevision: "test-lifecycle-revision", + initialSessionEntry: undefined, systemSent: false, isNewSession: true, ...overrides, @@ -667,6 +672,8 @@ function resetRunOutcomeMocks(): void { } function resetRunSessionMocks(): void { + loadSessionEntryMock.mockReset(); + loadSessionEntryMock.mockReturnValue(undefined); updateSessionStoreMock.mockReset(); updateSessionStoreMock.mockResolvedValue(undefined); resolveCronSessionMock.mockReset(); diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 18ec7e6ba1b7..053acbd940fc 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -1,10 +1,12 @@ /** Orchestrates isolated cron agent turn setup, execution, delivery, and cleanup. */ +import { isDeepStrictEqual } from "node:util"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { retireSessionMcpRuntime } from "../../agents/agent-bundle-mcp-tools.js"; import { hasAnyAuthProfileStoreSource } from "../../agents/auth-profiles/source-check.js"; import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js"; import { findModelInCatalog } from "../../agents/model-catalog-lookup.js"; import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-routing.js"; +import { createAgentRunRestartAbortError } from "../../agents/run-termination.js"; import { expandToolGroups, normalizeToolName } from "../../agents/tool-policy.js"; import { deriveContextPromptTokens } from "../../agents/usage.js"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; @@ -15,6 +17,7 @@ import { selectApplicableRuntimeConfig, } from "../../config/config.js"; import { resolveAgentModelPrimaryValue } from "../../config/model-input.js"; +import { resolveSessionWorkStartError } from "../../config/sessions/lifecycle.js"; import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { @@ -38,6 +41,10 @@ import { import { createDiagnosticMessageLifecycle } from "../../logging/message-lifecycle.js"; import { isCommandLaneTaskTimeoutError } from "../../process/command-queue.js"; import { CommandLane } from "../../process/lanes.js"; +import { + beginSessionWorkAdmission, + type SessionWorkAdmissionLease, +} from "../../sessions/session-lifecycle-admission.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; import { resolveNonNegativeNumber } from "../../shared/number-coercion.js"; import { resolveCronSkillsSnapshot } from "../../skills/runtime/cron-snapshot.js"; @@ -77,9 +84,12 @@ import { buildCronAgentDefaultsConfig } from "./run-config.js"; import { resolveCronPreflightCandidates } from "./run-fallback-policy.js"; import { adoptCronRunSessionMetadata, + CronSessionLifecycleClaimError, createPersistCronSessionEntry, markCronSessionPreRun, persistCronSkillsSnapshotIfChanged, + projectCronOwnershipFields, + resolveCronLifecycleRevisionIdentity, type CronLiveSelection, type MutableCronSession, type PersistCronSessionEntry, @@ -112,7 +122,7 @@ import { import type { RunCronAgentTurnResult } from "./run.types.js"; import { cleanupCronRunSessionAfterRun } from "./session-cleanup.js"; import { resolveCronAgentSessionKey } from "./session-key.js"; -import { resolveCronSession } from "./session.js"; +import { loadCronSessionEntryLatest, resolveCronSession } from "./session.js"; import { resolveCronSourceDeliveryPlan } from "./source-delivery-fallback.js"; const sessionStoreRuntimeLoader = createLazyImportLoader( @@ -529,6 +539,7 @@ type PreparedCronRunContext = { workspaceDir: string; commandBody: string; cronSession: MutableCronSession; + sessionWorkAdmission: SessionWorkAdmissionLease; persistSessionEntry: PersistCronSessionEntry; withRunSession: WithRunSession; agentPayload: Extract | null; @@ -577,6 +588,7 @@ function resolveCronActiveRuntimeConfig(cfg: OpenClawConfig): OpenClawConfig { async function prepareCronRunContext(params: { input: RunCronAgentTurnParams; isFastTestEnv: boolean; + onLifecycleInterrupt: () => void; }): Promise { const { input } = params; const runtimeCfg = resolveCronActiveRuntimeConfig(input.cfg); @@ -895,108 +907,148 @@ async function prepareCronRunContext(params: { requireExplicitMessageTarget: sourceDelivery.messageTool.requireExplicitTarget, }); - const skillsSnapshot = await resolveCronSkillsSnapshot({ - workspaceDir, - config: cfgWithAgentDefaults, - agentId, - existingSnapshot: cronSession.sessionEntry.skillsSnapshot, - isFastTestEnv: params.isFastTestEnv, - }); - await persistCronSkillsSnapshotIfChanged({ - isFastTestEnv: params.isFastTestEnv, - cronSession, - skillsSnapshot, - nowMs: Date.now(), - persistSessionEntry, - }); - - markCronSessionPreRun({ entry: cronSession.sessionEntry, provider, model }); - try { - await persistSessionEntry(); - } catch (err) { - logWarn(`[cron:${input.job.id}] Failed to persist pre-run session entry: ${String(err)}`); - } - await retireRolledCronSessionMcpRuntime({ - job: input.job, - cronSession, - }); - const hasSessionAuthProfileOverride = Boolean( - cronSession.sessionEntry.authProfileOverride?.trim(), - ); - const authProfileId = - !hasSessionAuthProfileOverride && - !hasConfiguredAuthProfiles(cfgWithAgentDefaults) && - !hasAnyAuthProfileStoreSource(agentDir) - ? undefined - : await ( - await loadCronAuthProfileRuntime() - ).resolveSessionAuthProfileOverride({ - // Auth profile resolution can mutate session state; pass the same - // store and key that persistence will later write. - cfg: cfgWithAgentDefaults, - provider, - acceptedProviderIds: listOpenAIAuthProfileProvidersForAgentRuntime({ - provider, - harnessRuntime: resolveAgentHarnessPolicy({ - provider, - modelId: model, - config: cfgWithAgentDefaults, - agentId, - sessionKey: agentSessionKey, - }).runtime, - config: cfgWithAgentDefaults, - }), - agentDir, - sessionEntry: cronSession.sessionEntry, - sessionStore: cronSession.store, - sessionKey: agentSessionKey, - storePath: cronSession.storePath, - isNewSession: cronSession.isNewSession && input.job.sessionTarget !== "isolated", - }); - const liveSelection: CronLiveSelection = { - provider, - model, - authProfileId, - authProfileIdSource: authProfileId - ? cronSession.sessionEntry.authProfileOverrideSource - : undefined, - }; - - return { - ok: true, - context: { - input, - cfgWithAgentDefaults, - agentId, - agentCfg, - agentDir, + const initialSessionEntry = cronSession.initialSessionEntry; + const sessionWorkAdmission = await beginSessionWorkAdmission({ + scope: cronSession.storePath, + identities: [ agentSessionKey, - runSessionId, - currentRunSessionId, + initialSessionEntry?.sessionId, + cronSession.sessionEntry.sessionId, + resolveCronLifecycleRevisionIdentity(cronSession.lifecycleRevision), runSessionKey, - workspaceDir, - commandBody, - cronSession, - persistSessionEntry, - withRunSession, - agentPayload, - deliveryPlan, - resolvedDelivery, - deliveryRequested, - sourceDelivery, - messageToolPromptEnabled, - suppressExecNotifyOnExit: deliveryPlan.mode === "none", - skillsSnapshot, - liveSelection, - useSubagentFallbacks, - inheritDefaultFallbacksForAgentStringModel, - modelFallbacksOverride, - thinkLevel, - timeoutMs, - preflightDiagnostics, - runTimeoutOverrideMs, + ], + signal: input.abortSignal ?? input.signal, + onInterrupt: params.onLifecycleInterrupt, + assertAllowed: () => { + const currentEntry = loadCronSessionEntryLatest(cronSession.storePath, agentSessionKey); + const changed = initialSessionEntry + ? !currentEntry || + !isDeepStrictEqual( + projectCronOwnershipFields(currentEntry), + projectCronOwnershipFields(initialSessionEntry), + ) + : Boolean(currentEntry); + if (changed) { + throw new Error(`Session "${agentSessionKey}" changed while starting work. Retry.`); + } + const archivedSessionError = resolveSessionWorkStartError(agentSessionKey, currentEntry); + if (archivedSessionError) { + throw new Error(archivedSessionError); + } }, - }; + }); + + try { + const skillsSnapshot = await resolveCronSkillsSnapshot({ + workspaceDir, + config: cfgWithAgentDefaults, + agentId, + existingSnapshot: cronSession.sessionEntry.skillsSnapshot, + isFastTestEnv: params.isFastTestEnv, + }); + await persistCronSkillsSnapshotIfChanged({ + isFastTestEnv: params.isFastTestEnv, + cronSession, + skillsSnapshot, + nowMs: Date.now(), + persistSessionEntry, + }); + + markCronSessionPreRun({ entry: cronSession.sessionEntry, provider, model }); + try { + await persistSessionEntry(); + } catch (err) { + if (err instanceof CronSessionLifecycleClaimError) { + throw err; + } + logWarn(`[cron:${input.job.id}] Failed to persist pre-run session entry: ${String(err)}`); + } + await retireRolledCronSessionMcpRuntime({ + job: input.job, + cronSession, + }); + const hasSessionAuthProfileOverride = Boolean( + cronSession.sessionEntry.authProfileOverride?.trim(), + ); + const authProfileId = + !hasSessionAuthProfileOverride && + !hasConfiguredAuthProfiles(cfgWithAgentDefaults) && + !hasAnyAuthProfileStoreSource(agentDir) + ? undefined + : await ( + await loadCronAuthProfileRuntime() + ).resolveSessionAuthProfileOverride({ + // Auth profile resolution can mutate session state; pass the same + // store and key that persistence will later write. + cfg: cfgWithAgentDefaults, + provider, + acceptedProviderIds: listOpenAIAuthProfileProvidersForAgentRuntime({ + provider, + harnessRuntime: resolveAgentHarnessPolicy({ + provider, + modelId: model, + config: cfgWithAgentDefaults, + agentId, + sessionKey: agentSessionKey, + }).runtime, + config: cfgWithAgentDefaults, + }), + agentDir, + sessionEntry: cronSession.sessionEntry, + sessionStore: cronSession.store, + sessionKey: agentSessionKey, + storePath: cronSession.storePath, + isNewSession: cronSession.isNewSession && input.job.sessionTarget !== "isolated", + }); + const liveSelection: CronLiveSelection = { + provider, + model, + authProfileId, + authProfileIdSource: authProfileId + ? cronSession.sessionEntry.authProfileOverrideSource + : undefined, + }; + + return { + ok: true, + context: { + input, + cfgWithAgentDefaults, + agentId, + agentCfg, + agentDir, + agentSessionKey, + runSessionId, + currentRunSessionId, + runSessionKey, + workspaceDir, + commandBody, + cronSession, + sessionWorkAdmission, + persistSessionEntry, + withRunSession, + agentPayload, + deliveryPlan, + resolvedDelivery, + deliveryRequested, + sourceDelivery, + messageToolPromptEnabled, + suppressExecNotifyOnExit: deliveryPlan.mode === "none", + skillsSnapshot, + liveSelection, + useSubagentFallbacks, + inheritDefaultFallbacksForAgentStringModel, + modelFallbacksOverride, + thinkLevel, + timeoutMs, + preflightDiagnostics, + runTimeoutOverrideMs, + }, + }; + } catch (error) { + sessionWorkAdmission.release(); + throw error; + } } async function finalizeCronRun(params: { @@ -1005,6 +1057,7 @@ async function finalizeCronRun(params: { abortReason: () => string; isAborted: () => boolean; markCronRunSessionCleanupAttempted: () => void; + beforeSessionDelete: () => void; }): Promise { const { prepared, execution } = params; const finalRunResult = execution.runResult; @@ -1206,6 +1259,9 @@ async function finalizeCronRun(params: { job: prepared.input.job, agentSessionKey: prepared.agentSessionKey, sessionId: prepared.currentRunSessionId(), + lifecycleRevision: prepared.cronSession.lifecycleRevision, + sessionUpdatedAt: prepared.cronSession.sessionEntry.updatedAt, + beforeSessionDelete: params.beforeSessionDelete, retireReason: "cron-delete-after-run-aborted", }); params.markCronRunSessionCleanupAttempted(); @@ -1293,6 +1349,9 @@ async function finalizeCronRun(params: { job: prepared.input.job, agentSessionKey: prepared.agentSessionKey, sessionId: prepared.currentRunSessionId(), + lifecycleRevision: prepared.cronSession.lifecycleRevision, + sessionUpdatedAt: prepared.cronSession.sessionEntry.updatedAt, + beforeSessionDelete: params.beforeSessionDelete, retireReason: "cron-delete-after-run-fatal-error", }); params.markCronRunSessionCleanupAttempted(); @@ -1319,6 +1378,9 @@ async function finalizeCronRun(params: { agentSessionKey: prepared.agentSessionKey, runSessionKey: prepared.runSessionKey, sessionId: prepared.currentRunSessionId(), + lifecycleRevision: prepared.cronSession.lifecycleRevision, + sessionUpdatedAt: prepared.cronSession.sessionEntry.updatedAt, + beforeSessionDelete: params.beforeSessionDelete, runStartedAt: execution.runStartedAt, runEndedAt: execution.runEndedAt, timeoutMs: prepared.timeoutMs, @@ -1436,12 +1498,20 @@ export async function runCronIsolatedAgentTurn(params: { lane?: string; }): Promise { const admittedLifecycleGeneration = getAgentEventLifecycleGeneration(); - const abortSignal = params.abortSignal ?? params.signal; - const isAborted = () => abortSignal?.aborted === true; + const upstreamAbortSignal = params.abortSignal ?? params.signal; + const lifecycleAbortController = new AbortController(); + const abortSignal = upstreamAbortSignal + ? AbortSignal.any([upstreamAbortSignal, lifecycleAbortController.signal]) + : lifecycleAbortController.signal; + const isAborted = () => abortSignal?.aborted ?? false; const abortReason = () => resolveCronAbortReasonText(abortSignal?.reason) ?? "cron: job execution timed out"; const isFastTestEnv = process.env.OPENCLAW_TEST_FAST === "1"; - const prepared = await prepareCronRunContext({ input: params, isFastTestEnv }); + const prepared = await prepareCronRunContext({ + input: { ...params, abortSignal }, + isFastTestEnv, + onLifecycleInterrupt: () => lifecycleAbortController.abort(createAgentRunRestartAbortError()), + }); if (!prepared.ok) { return prepared.result; } @@ -1480,17 +1550,24 @@ export async function runCronIsolatedAgentTurn(params: { }; const turnStartedAtMs = Date.now(); - const diagnosticsEnabled = isDiagnosticsEnabled(params.cfg); - const messageLifecycle = createDiagnosticMessageLifecycle({ - enabled: diagnosticsEnabled, - sessionId: prepared.context.runSessionId, - sessionKey: prepared.context.runSessionKey, - channel: "cron", - source: "cron-isolated", - startedAtMs: turnStartedAtMs, - trackSessionState: true, - }); - messageLifecycle.markProcessing(); + const messageLifecycle = (() => { + try { + const lifecycle = createDiagnosticMessageLifecycle({ + enabled: isDiagnosticsEnabled(params.cfg), + sessionId: prepared.context.runSessionId, + sessionKey: prepared.context.runSessionKey, + channel: "cron", + source: "cron-isolated", + startedAtMs: turnStartedAtMs, + trackSessionState: true, + }); + lifecycle.markProcessing(); + return lifecycle; + } catch (error) { + prepared.context.sessionWorkAdmission.release(); + throw error; + } + })(); let outcome: "completed" | "error" = "completed"; let outcomeError: string | undefined; @@ -1514,7 +1591,7 @@ export async function runCronIsolatedAgentTurn(params: { }, ); const { executeCronRun } = await loadCronExecutorRuntime(); - const execution = await executeCronRun({ + const executionParams: Parameters[0] = { cfg: params.cfg, cfgWithAgentDefaults: prepared.context.cfgWithAgentDefaults, job: params.job, @@ -1555,7 +1632,10 @@ export async function runCronIsolatedAgentTurn(params: { timeoutMs: prepared.context.timeoutMs, runTimeoutOverrideMs: prepared.context.runTimeoutOverrideMs, suppressExecNotifyOnExit: prepared.context.suppressExecNotifyOnExit, - }); + }; + const execution = await prepared.context.sessionWorkAdmission.run(async () => + executeCronRun(executionParams), + ); const finalized = await finalizeCronRun({ prepared: prepared.context, execution, @@ -1564,6 +1644,9 @@ export async function runCronIsolatedAgentTurn(params: { markCronRunSessionCleanupAttempted: () => { cronRunSessionCleanupAttempted = true; }, + // Self-deleting sessions must release before their own lifecycle mutation. + // Other runs retain admission through delivery and release in finally. + beforeSessionDelete: prepared.context.sessionWorkAdmission.release, }); if (finalized.status === "error") { outcome = "error"; @@ -1605,23 +1688,31 @@ export async function runCronIsolatedAgentTurn(params: { }); try { if (!cronRunSessionCleanupAttempted) { - cronRunSessionCleanupAttempted = await cleanupCronRunSessionAfterRun({ + const cleanupOutcome = await cleanupCronRunSessionAfterRun({ job: params.job, agentSessionKey: prepared.context.agentSessionKey, sessionId: prepared.context.currentRunSessionId(), + lifecycleRevision: prepared.context.cronSession.lifecycleRevision, + sessionUpdatedAt: prepared.context.cronSession.sessionEntry.updatedAt, + beforeDelete: prepared.context.sessionWorkAdmission.release, reason: "cron-delete-after-run-finally", }); + cronRunSessionCleanupAttempted = cleanupOutcome !== "not-requested"; } } finally { // Release runtime references after the run completes (success or failure). // The session entry has already been persisted to disk by this point, // so the in-memory store and run context can be safely dropped. - await disposeCronRunContext({ - sessionId: initialSessionId, - cronSession: prepared.context.cronSession, - ownsRunContext, - runContextOwnerToken, - }); + try { + await disposeCronRunContext({ + sessionId: initialSessionId, + cronSession: prepared.context.cronSession, + ownsRunContext, + runContextOwnerToken, + }); + } finally { + prepared.context.sessionWorkAdmission.release(); + } } } } diff --git a/src/cron/isolated-agent/session-cleanup.ts b/src/cron/isolated-agent/session-cleanup.ts index 39f9ec8b126d..61a37834389d 100644 --- a/src/cron/isolated-agent/session-cleanup.ts +++ b/src/cron/isolated-agent/session-cleanup.ts @@ -1,4 +1,5 @@ import { retireSessionMcpRuntime } from "../../agents/agent-bundle-mcp-tools.js"; +import { SESSION_LIFECYCLE_CHANGED_ERROR_REASON } from "../../config/sessions/lifecycle.js"; import { isCronSessionKey } from "../../routing/session-key.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; import type { CronJob } from "../types.js"; @@ -11,34 +12,75 @@ async function loadGatewayCallRuntime(): Promise; + job: Pick; agentSessionKey: string; sessionId: string; + lifecycleRevision: string; + sessionUpdatedAt: number; + beforeDelete?: () => void; reason: string; -}): Promise { - if (!params.job.deleteAfterRun) { - return false; - } - if (!isCronSessionKey(params.agentSessionKey)) { - return false; +}): Promise { + if (!shouldDeleteCronRunSessionAfterRun(params)) { + return "not-requested"; } + params.beforeDelete?.(); try { const { callGateway } = await loadGatewayCallRuntime(); - await callGateway({ + const result = await callGateway<{ deleted?: boolean }>({ method: "sessions.delete", params: { key: params.agentSessionKey, deleteTranscript: true, emitLifecycleHooks: false, + expectedSessionId: params.sessionId, + expectedLifecycleRevision: params.lifecycleRevision, + expectedSessionUpdatedAt: params.sessionUpdatedAt, }, timeoutMs: 10_000, }); - } catch { - await retireSessionMcpRuntime({ - sessionId: params.sessionId, - reason: params.reason, - }); + return result.deleted === true ? "deleted" : "changed"; + } catch (error) { + if (isSessionChangedGatewayError(error)) { + return "changed"; + } + if (params.job.sessionTarget === "isolated") { + await retireSessionMcpRuntime({ + sessionId: params.sessionId, + reason: params.reason, + }); + return "retired"; + } + // Persistent custom targets survive transport failures. The caller may + // restore delivery state with the same lifecycle revision as an atomic guard. + return "survived"; } - return true; +} + +function shouldDeleteCronRunSessionAfterRun(params: { + job: Pick; + agentSessionKey: string; +}): boolean { + return params.job.deleteAfterRun === true && isCronSessionKey(params.agentSessionKey); +} + +function isSessionChangedGatewayError(error: unknown): boolean { + if (!(error instanceof Error) || error.name !== "GatewayClientRequestError") { + return false; + } + const requestError = error as Error & { gatewayCode?: unknown; details?: unknown }; + const details = requestError.details; + return ( + requestError.gatewayCode === "INVALID_REQUEST" && + typeof details === "object" && + details !== null && + (details as { reason?: unknown }).reason === SESSION_LIFECYCLE_CHANGED_ERROR_REASON + ); } diff --git a/src/cron/isolated-agent/session.test.ts b/src/cron/isolated-agent/session.test.ts index 1bcbdf2c6ee2..c88061b41a9e 100644 --- a/src/cron/isolated-agent/session.test.ts +++ b/src/cron/isolated-agent/session.test.ts @@ -109,6 +109,30 @@ describe("resolveCronSession", () => { expect(result.isNewSession).toBe(true); }); + it("assigns a collision-proof lifecycle revision to each resolved run", () => { + const first = resolveWithStoredEntry({ sessionKey: "agent:main:cron:new-job" }); + const second = resolveWithStoredEntry({ sessionKey: "agent:main:cron:new-job" }); + + expect(first.lifecycleRevision).toBe(first.sessionEntry.lifecycleRevision); + expect(second.lifecycleRevision).toBe(second.sessionEntry.lifecycleRevision); + expect(first.lifecycleRevision).not.toBe(second.lifecycleRevision); + }); + + it("rejects archived persistent sessions before rollover", () => { + expect(() => + resolveWithStoredEntry({ + sessionKey: "agent:main:main", + entry: { + sessionId: "archived-session-id", + updatedAt: NOW_MS - 1000, + archivedAt: NOW_MS, + }, + forceNew: true, + }), + ).toThrow('Session "agent:main:main" is archived. Restore it before starting new work.'); + expect(clearBootstrapSnapshot).not.toHaveBeenCalled(); + }); + // New tests for session reuse behavior (#18027) describe("session reuse for webhooks/cron", () => { it("reuses existing sessionId when session is fresh", () => { @@ -176,6 +200,22 @@ describe("resolveCronSession", () => { expect(clearBootstrapSnapshot).toHaveBeenCalledWith("webhook:stable-key"); }); + it("preserves pin state when rolling to a fresh session", () => { + const pinnedAt = NOW_MS - 500; + const result = resolveWithStoredEntry({ + entry: { + sessionId: "existing-session-id-pinned", + updatedAt: NOW_MS - 1000, + pinnedAt, + }, + fresh: true, + forceNew: true, + }); + + expect(result.isNewSession).toBe(true); + expect(result.sessionEntry.pinnedAt).toBe(pinnedAt); + }); + it("clears stale sessionFile when forceNew rolls to a fresh session", () => { const result = resolveWithStoredEntry({ entry: { diff --git a/src/cron/isolated-agent/session.ts b/src/cron/isolated-agent/session.ts index 0bc1847473d0..fb3d9fb5c750 100644 --- a/src/cron/isolated-agent/session.ts +++ b/src/cron/isolated-agent/session.ts @@ -2,7 +2,10 @@ import crypto from "node:crypto"; import { clearBootstrapSnapshotOnSessionRollover } from "../../agents/bootstrap-cache.js"; import { hasProviderOwnedSession } from "../../config/sessions/entry-freshness.js"; -import { resolveSessionLifecycleTimestamps } from "../../config/sessions/lifecycle.js"; +import { + resolveSessionLifecycleTimestamps, + resolveSessionWorkStartError, +} from "../../config/sessions/lifecycle.js"; import { hasSessionAutoModelFallbackProvenance } from "../../config/sessions/model-override-provenance.js"; import { resolveStorePath } from "../../config/sessions/paths.js"; import { @@ -24,6 +27,7 @@ const FRESH_CRON_CARRIED_PREFERENCE_FIELDS = [ "reasoningLevel", "ttsAuto", "responseUsage", + "pinnedAt", "label", "displayName", ] as const satisfies readonly (keyof SessionEntry)[]; @@ -106,6 +110,14 @@ function sanitizeFreshCronSessionEntry( return next; } +/** Reads the exact cron session row without an in-process cache snapshot. */ +export function loadCronSessionEntryLatest( + storePath: string, + sessionKey: string, +): SessionEntry | undefined { + return loadSessionStore(storePath, { skipCache: true })[sessionKey]; +} + /** Resolves or rolls over the cron session entry for one isolated-agent run. */ export function resolveCronSession(params: { cfg: OpenClawConfig; @@ -121,6 +133,10 @@ export function resolveCronSession(params: { }); const store = params.store ?? loadSessionStore(storePath); const entry = store[params.sessionKey]; + const archivedSessionError = resolveSessionWorkStartError(params.sessionKey, entry); + if (archivedSessionError) { + throw new Error(archivedSessionError); + } let sessionId: string; let isNewSession: boolean; @@ -174,11 +190,13 @@ export function resolveCronSession(params: { : entry : undefined; + const lifecycleRevision = crypto.randomUUID(); const sessionEntry: SessionEntry = { // Fresh cron sessions keep user preference/auth overrides but drop resume // handles and auto-fallback model overrides that belong to the old run. ...baseEntry, sessionId, + lifecycleRevision, updatedAt: params.nowMs, sessionStartedAt: isNewSession ? params.nowMs @@ -191,5 +209,14 @@ export function resolveCronSession(params: { lastInteractionAt: isNewSession ? params.nowMs : baseEntry?.lastInteractionAt, systemSent, }; - return { storePath, store, sessionEntry, systemSent, isNewSession, previousSessionId }; + return { + storePath, + store, + sessionEntry, + lifecycleRevision, + systemSent, + isNewSession, + previousSessionId, + initialSessionEntry: entry, + }; } diff --git a/src/gateway/call.test.ts b/src/gateway/call.test.ts index 3998d17d008f..fe4eed6993e0 100644 --- a/src/gateway/call.test.ts +++ b/src/gateway/call.test.ts @@ -1793,6 +1793,82 @@ describe("callGateway error details", () => { expect(lastRequestOptions?.opts?.timeoutMs).toBe(45_000); }); + it("keeps the startup deadline when the request timeout is disabled", async () => { + startMode = "silent"; + setLocalLoopbackGatewayConfig(); + vi.useFakeTimers(); + let err: unknown; + + const promise = callGateway({ method: "health", timeoutMs: null }).catch((caught: unknown) => { + err = caught; + }); + + await vi.advanceTimersByTimeAsync(10_000); + await promise; + + expect(isGatewayTransportError(err)).toBe(true); + expect(err).toMatchObject({ kind: "timeout", timeoutMs: 10_000 }); + expect(lastRequestOptions).toBeNull(); + }); + + it("disables the request and wrapper deadline when timeout is null", async () => { + setLocalLoopbackGatewayConfig(); + vi.useFakeTimers(); + let releaseRequest: (() => void) | undefined; + + testing.setDepsForTests({ + createGatewayClient: (opts) => + ({ + async request( + method: string, + params: unknown, + requestOpts?: { expectFinal?: boolean; timeoutMs?: number | null }, + ) { + lastRequestOptions = { method, params, opts: requestOpts }; + await new Promise((resolve) => { + releaseRequest = resolve; + }); + return { ok: true }; + }, + start() { + opts.onHelloOk?.({ + features: { + methods: helloMethods ?? [], + events: [], + }, + } as unknown as Parameters>[0]); + }, + stop() {}, + async stopAndWait() {}, + }) as never, + getRuntimeConfig: getRuntimeConfig as unknown as () => OpenClawConfig, + loadOrCreateDeviceIdentity: () => deviceIdentityState.value, + loadDeviceAuthToken: loadDeviceAuthTokenMock, + resolveGatewayPort: resolveGatewayPort as unknown as ( + cfg?: OpenClawConfig, + env?: NodeJS.ProcessEnv, + ) => number, + }); + + let settled = false; + const promise = callGateway({ method: "health", timeoutMs: null }).then((result) => { + settled = true; + return result; + }); + + await vi.waitFor(() => { + expect(lastRequestOptions?.opts?.timeoutMs).toBeNull(); + }); + await vi.advanceTimersByTimeAsync(60_000); + expect(settled).toBe(false); + + if (!releaseRequest) { + throw new Error("Expected request release callback to be initialized"); + } + releaseRequest(); + await expect(promise).resolves.toEqual({ ok: true }); + }); + it("forwards caller abort signal and accepted callback to client requests", async () => { setLocalLoopbackGatewayConfig(); const controller = new AbortController(); diff --git a/src/gateway/call.ts b/src/gateway/call.ts index 4d0ee519acc5..10b907e00e66 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -76,7 +76,7 @@ type CallGatewayBaseOptions = { method: string; params?: unknown; expectFinal?: boolean; - timeoutMs?: number; + timeoutMs?: number | null; signal?: AbortSignal; onAccepted?: GatewayClientRequestOptions["onAccepted"]; onSignalAbort?: (request: GatewayRequestFunction) => Promise | void; @@ -653,7 +653,8 @@ function resolveGatewayCallTimeout( timeoutValue: unknown, configuredHandshakeTimeoutMs?: number | null, ): { - timeoutMs: number; + timeoutMs: number | null; + startupTimeoutMs: number; safeTimerTimeoutMs: number; } { const hasConfiguredHandshakeTimeout = @@ -667,14 +668,16 @@ function resolveGatewayCallTimeout( hasConfiguredHandshakeTimeout || hasEnvHandshakeTimeout ? resolvePreauthHandshakeTimeoutMs({ configuredTimeoutMs: configuredHandshakeTimeoutMs }) : undefined; - const timeoutMs = - typeof timeoutValue === "number" && Number.isFinite(timeoutValue) - ? timeoutValue - : typeof resolvedHandshakeTimeoutMs === "number" && resolvedHandshakeTimeoutMs > 10_000 - ? resolvedHandshakeTimeoutMs - : 10_000; - const safeTimerTimeoutMs = resolveSafeTimeoutDelayMs(timeoutMs); - return { timeoutMs, safeTimerTimeoutMs }; + const defaultTimeoutMs = + typeof resolvedHandshakeTimeoutMs === "number" && resolvedHandshakeTimeoutMs > 10_000 + ? resolvedHandshakeTimeoutMs + : 10_000; + const explicitTimeoutMs = + typeof timeoutValue === "number" && Number.isFinite(timeoutValue) ? timeoutValue : undefined; + const startupTimeoutMs = explicitTimeoutMs ?? defaultTimeoutMs; + const timeoutMs = timeoutValue === null ? null : (explicitTimeoutMs ?? defaultTimeoutMs); + const safeTimerTimeoutMs = resolveSafeTimeoutDelayMs(timeoutMs ?? startupTimeoutMs); + return { timeoutMs, startupTimeoutMs, safeTimerTimeoutMs }; } async function resolveGatewayCallContext( @@ -896,7 +899,8 @@ async function executeGatewayRequestWithScopes(params: { password?: string; tlsFingerprint?: string; preauthHandshakeTimeoutMs?: number; - timeoutMs: number; + timeoutMs: number | null; + startupTimeoutMs: number; safeTimerTimeoutMs: number; connectionDetails: GatewayConnectionDetails; deviceIdentity: DeviceIdentity | null; @@ -911,6 +915,7 @@ async function executeGatewayRequestWithScopes(params: { tlsFingerprint, preauthHandshakeTimeoutMs, timeoutMs, + startupTimeoutMs, safeTimerTimeoutMs, deviceIdentity, surfaceGatewayClientRequestErrors, @@ -922,6 +927,7 @@ async function executeGatewayRequestWithScopes(params: { } let settled = false; let ignoreClose = false; + let timer: NodeJS.Timeout | undefined; const startAbort = new AbortController(); let primaryRequestStarted = false; let suppressedPreHelloCleanCloses = 0; @@ -1004,6 +1010,10 @@ async function executeGatewayRequestWithScopes(params: { minProtocol: opts.minProtocol ?? MIN_CLIENT_PROTOCOL_VERSION, maxProtocol: opts.maxProtocol ?? PROTOCOL_VERSION, onHelloOk: (hello) => { + if (timeoutMs === null && timer) { + clearTimeout(timer); + timer = undefined; + } void (async () => { try { ensureGatewaySupportsRequiredMethods({ @@ -1068,11 +1078,12 @@ async function executeGatewayRequestWithScopes(params: { }, }); - const timer: NodeJS.Timeout | undefined = setTimeout(() => { + const wrapperTimeoutMs = timeoutMs ?? startupTimeoutMs; + timer = setTimeout(() => { ignoreClose = true; stop( createGatewayTimeoutTransportError({ - timeoutMs, + timeoutMs: wrapperTimeoutMs, connectionDetails: params.connectionDetails, }), ); @@ -1089,7 +1100,7 @@ async function executeGatewayRequestWithScopes(params: { ignoreClose = true; stop( createGatewayTimeoutTransportError({ - timeoutMs, + timeoutMs: startupTimeoutMs, connectionDetails: params.connectionDetails, }), ); @@ -1109,7 +1120,7 @@ async function callGatewayWithScopes>( scopes: OperatorScope[] | undefined, ): Promise { const context = await resolveGatewayCallContext(opts); - const { timeoutMs, safeTimerTimeoutMs } = resolveGatewayCallTimeout( + const { timeoutMs, startupTimeoutMs, safeTimerTimeoutMs } = resolveGatewayCallTimeout( opts.timeoutMs, context.config.gateway?.handshakeTimeoutMs, ); @@ -1210,6 +1221,7 @@ async function callGatewayWithScopes>( tlsFingerprint, preauthHandshakeTimeoutMs: context.config.gateway?.handshakeTimeoutMs, timeoutMs, + startupTimeoutMs, safeTimerTimeoutMs, connectionDetails, deviceIdentity, diff --git a/src/gateway/gateway-codex-harness.live.test.ts b/src/gateway/gateway-codex-harness.live.test.ts index d9252217ae0d..e53e72592438 100644 --- a/src/gateway/gateway-codex-harness.live.test.ts +++ b/src/gateway/gateway-codex-harness.live.test.ts @@ -903,7 +903,7 @@ async function verifyCodexSubagentProbe(params: { error?: string; method: string; status: "error" | "ok"; - timeoutMs?: number; + timeoutMs?: number | null; }> = []; subagentSpawnTesting.setDepsForTest({ resolveContextEngine: async () => noOpContextEngine, diff --git a/src/gateway/server-maintenance.test.ts b/src/gateway/server-maintenance.test.ts index 8bc9c055c6d0..9e08b3210b2d 100644 --- a/src/gateway/server-maintenance.test.ts +++ b/src/gateway/server-maintenance.test.ts @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { HealthSummary } from "../commands/health.js"; import type { ChatAbortControllerEntry } from "./chat-abort.js"; import { DEDUPE_MAX, DEDUPE_TTL_MS } from "./server-constants.js"; +import { pendingChatSendDedupeKey } from "./server-shared.js"; import { createGatewayMaintenanceStateForTest } from "./test-helpers.maintenance-state.js"; const cleanOldMediaMock = vi.fn(async () => {}); @@ -384,6 +385,40 @@ describe("startGatewayMaintenanceTimers", () => { stopMaintenanceTimers(timers); }); + it("keeps pending chat sends through ttl and overflow until their run expiry", async () => { + const { startGatewayMaintenanceTimers, deps, now } = await createTimedMaintenanceScenario(); + seedStableDedupeEntries(deps, now); + deps.dedupe.set(pendingChatSendDedupeKey("pending-chat"), { + ts: now - DEDUPE_TTL_MS - 1, + ok: true, + payload: { + runId: "pending-chat", + sessionKey: "agent:main:main", + status: "accepted", + expiresAtMs: now + 120_000, + }, + }); + deps.dedupe.set(pendingChatSendDedupeKey("expired-chat"), { + ts: now - DEDUPE_TTL_MS - 1, + ok: true, + payload: { + runId: "expired-chat", + sessionKey: "agent:main:main", + status: "accepted", + expiresAtMs: now - 1, + }, + }); + const timers = startGatewayMaintenanceTimers(deps); + + await vi.advanceTimersByTimeAsync(60_000); + + expect(deps.dedupe.has(pendingChatSendDedupeKey("pending-chat"))).toBe(true); + expect(deps.dedupe.has(pendingChatSendDedupeKey("expired-chat"))).toBe(false); + expect(deps.dedupe.size).toBe(DEDUPE_MAX); + + stopMaintenanceTimers(timers); + }); + it("evicts pending accepted agent dedupe entries with invalid run expiry", async () => { const { startGatewayMaintenanceTimers, deps, now } = await createTimedMaintenanceScenario(); deps.dedupe.set("agent:invalid-expiry-pending-agent", { diff --git a/src/gateway/server-maintenance.ts b/src/gateway/server-maintenance.ts index a93a0563a594..51697be27d09 100644 --- a/src/gateway/server-maintenance.ts +++ b/src/gateway/server-maintenance.ts @@ -20,7 +20,7 @@ import { HEALTH_REFRESH_INTERVAL_MS, TICK_INTERVAL_MS, } from "./server-constants.js"; -import type { DedupeEntry } from "./server-shared.js"; +import { PENDING_CHAT_SEND_DEDUPE_PREFIX, type DedupeEntry } from "./server-shared.js"; import { formatError } from "./server-utils.js"; import { setBroadcastHealthUpdate } from "./server/health-state.js"; @@ -122,8 +122,8 @@ export function startGatewayMaintenanceTimers(params: { : undefined : undefined; }; - const isPendingAcceptedAgentDedupeKey = (key: string, dedupeEntry: DedupeEntry) => { - if (!key.startsWith("agent:")) { + const isPendingAcceptedRunDedupeKey = (key: string, dedupeEntry: DedupeEntry) => { + if (!key.startsWith("agent:") && !key.startsWith(PENDING_CHAT_SEND_DEDUPE_PREFIX)) { return false; } const payload = dedupeEntry.payload; @@ -139,7 +139,9 @@ export function startGatewayMaintenanceTimers(params: { const isActiveRunDedupeKey = (key: string, dedupeEntry: DedupeEntry) => { // Keep idempotency records for active runs so retries cannot create // duplicate chat/agent work while a command is still draining. - if (!key.startsWith("agent:") && !key.startsWith("chat:")) { + const isAgentKey = key.startsWith("agent:"); + const isChatKey = key.startsWith("chat:"); + if (!isAgentKey && !isChatKey) { return false; } const runId = resolveDedupeRunId(key, dedupeEntry); @@ -147,10 +149,10 @@ export function startGatewayMaintenanceTimers(params: { if (!entry) { return false; } - return key.startsWith("agent:") ? entry.kind === "agent" : entry.kind !== "agent"; + return isAgentKey ? entry.kind === "agent" : entry.kind !== "agent"; }; for (const [k, v] of params.dedupe) { - if (isActiveRunDedupeKey(k, v) || isPendingAcceptedAgentDedupeKey(k, v)) { + if (isActiveRunDedupeKey(k, v) || isPendingAcceptedRunDedupeKey(k, v)) { continue; } if (now - v.ts > DEDUPE_TTL_MS) { @@ -162,7 +164,7 @@ export function startGatewayMaintenanceTimers(params: { const oldestKeys = [...params.dedupe.entries()] .filter( ([key, entry]) => - !isActiveRunDedupeKey(key, entry) && !isPendingAcceptedAgentDedupeKey(key, entry), + !isActiveRunDedupeKey(key, entry) && !isPendingAcceptedRunDedupeKey(key, entry), ) .toSorted(([, left], [, right]) => left.ts - right.ts) .slice(0, excess) diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 2f13eb9c197e..1c92730ca9a0 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -8,7 +8,10 @@ import { registerExecApprovalFollowupRuntimeHandoff, resetExecApprovalFollowupRuntimeHandoffsForTests, } from "../../agents/bash-tools.exec-approval-followup-state.js"; -import { createAgentRunRestartAbortError } from "../../agents/run-termination.js"; +import { + createAgentRunRestartAbortError, + isAgentRunRestartAbortReason, +} from "../../agents/run-termination.js"; import { getSubagentRunByChildSessionKey, resetSubagentRegistryForTests, @@ -20,6 +23,10 @@ import { waitForDiagnosticEventsDrained, type DiagnosticEventPayload, } from "../../infra/diagnostic-events.js"; +import { + interruptSessionWorkAdmissions, + runExclusiveSessionLifecycleMutation, +} from "../../sessions/session-lifecycle-admission.js"; import { getDetachedTaskLifecycleRuntime, resetDetachedTaskLifecycleRuntimeForTests, @@ -686,6 +693,460 @@ describe("gateway agent handler", () => { }); }); + it("dispatches with the session id reloaded during lifecycle admission", async () => { + const sessionKey = "agent:main:main"; + const initialSessionId = "session-before-reset"; + const admittedSessionId = "session-after-reset"; + let currentSessionId = initialSessionId; + mocks.loadSessionEntry.mockImplementation(() => ({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: { sessionId: currentSessionId, updatedAt: Date.now() }, + canonicalKey: sessionKey, + })); + mocks.updateSessionStore.mockImplementation(async (_path, updater) => { + const result = await updater({ + [sessionKey]: { sessionId: initialSessionId, updatedAt: Date.now() }, + }); + currentSessionId = admittedSessionId; + return result; + }); + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + await runMainAgent("hi", "idem-reset-before-admission"); + + const call = await waitForAgentCommandCall<{ sessionId?: string }>(); + expect(call.sessionId).toBe(admittedSessionId); + }); + + it("does not recreate a session deleted before lifecycle admission", async () => { + const sessionKey = "agent:main:main"; + let deleted = false; + mocks.loadSessionEntry.mockImplementation(() => ({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: deleted ? undefined : { sessionId: "session-before-delete", updatedAt: Date.now() }, + canonicalKey: sessionKey, + })); + mocks.updateSessionStore.mockImplementation(async (_path, updater) => { + const result = await updater({ + [sessionKey]: { sessionId: "session-before-delete", updatedAt: Date.now() }, + }); + deleted = true; + return result; + }); + mocks.agentCommand.mockClear(); + const respond = vi.fn(); + + await invokeAgent( + { + message: "do not recreate the deleted session", + agentId: "main", + sessionKey, + idempotencyKey: "idem-deleted-before-admission", + }, + { respond, reqId: "idem-deleted-before-admission" }, + ); + + expectRespondError(respond, { + message: `Error: Session "${sessionKey}" was deleted while starting work. Retry.`, + }); + expect(mocks.agentCommand).not.toHaveBeenCalled(); + }); + + it("does not recreate a session deleted before its initial store touch", async () => { + const sessionKey = "agent:main:main"; + mocks.loadSessionEntry.mockImplementation(() => ({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: { sessionId: "session-before-delete", updatedAt: Date.now() }, + canonicalKey: sessionKey, + })); + mocks.updateSessionStore.mockImplementation(async (_path, updater) => await updater({})); + mocks.agentCommand.mockClear(); + const respond = vi.fn(); + + await invokeAgent( + { + message: "do not recreate the deleted session", + agentId: "main", + sessionKey, + idempotencyKey: "idem-deleted-before-touch", + }, + { respond, reqId: "idem-deleted-before-touch" }, + ); + + expectRespondError(respond, { + message: `Error: Session "${sessionKey}" was deleted while starting work. Retry.`, + }); + expect(mocks.agentCommand).not.toHaveBeenCalled(); + }); + + it("does not recreate a newly touched session deleted before lifecycle admission", async () => { + const sessionKey = "agent:main:slack:group:new-session"; + let storedEntry: Record | undefined; + mocks.loadSessionEntry.mockImplementation(() => ({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: storedEntry, + canonicalKey: sessionKey, + })); + mocks.updateSessionStore.mockImplementation(async (_path, updater) => { + const result = await updater({}); + storedEntry = undefined; + return result; + }); + mocks.agentCommand.mockClear(); + const respond = vi.fn(); + + await invokeAgent( + { + message: "do not revive the newly deleted session", + agentId: "main", + sessionKey, + idempotencyKey: "idem-new-session-deleted-before-admission", + }, + { respond, reqId: "idem-new-session-deleted-before-admission" }, + ); + + expectRespondError(respond, { + message: `Error: Session "${sessionKey}" was deleted while starting work. Retry.`, + }); + expect(mocks.agentCommand).not.toHaveBeenCalled(); + }); + + it("blocks the initial store touch and preserves an abort while admission waits", async () => { + primeMainAgentRun(); + mocks.agentCommand.mockClear(); + mocks.updateSessionStore.mockClear(); + const sessionKey = "agent:main:main"; + const runId = "idem-abort-during-admission"; + let releaseMutation = () => {}; + let markMutationStarted = () => {}; + const mutationStarted = new Promise((resolve) => { + markMutationStarted = resolve; + }); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: "/tmp/sessions.json", + identities: [sessionKey, "existing-session-id"], + run: async () => { + markMutationStarted(); + await new Promise((release) => { + releaseMutation = release; + }); + }, + }); + await mutationStarted; + const context = makeContext(); + const respond = vi.fn(); + const request = invokeAgent( + { + message: "do not run after abort", + agentId: "main", + sessionKey, + idempotencyKey: runId, + }, + { context, respond, reqId: runId }, + ); + await waitForAssertion(() => { + expect(context.dedupe.has(`agent:${runId}`)).toBe(true); + }); + expect(mocks.updateSessionStore).not.toHaveBeenCalled(); + + const abortRespond = vi.fn(); + await chatHandlers["chat.abort"]({ + params: { sessionKey, runId }, + respond: abortRespond as never, + context, + req: { type: "req", id: "abort-req", method: "chat.abort" }, + client: null, + isWebchatConnect: () => false, + }); + releaseMutation(); + await mutation; + await request; + + expect(mockCallArg(abortRespond)).toBe(true); + expect(mocks.agentCommand).not.toHaveBeenCalled(); + expect(context.chatAbortControllers.has(runId)).toBe(false); + expect( + respond.mock.calls.some( + ([ok, payload]) => ok === true && (payload as { status?: string })?.status === "timeout", + ), + ).toBe(true); + }); + + it("does not revive an expired reservation after lifecycle admission waits", async () => { + primeMainAgentRun(); + mocks.agentCommand.mockClear(); + const sessionKey = "agent:main:main"; + const runId = "idem-expired-during-admission"; + let releaseMutation = () => {}; + let markMutationStarted = () => {}; + const mutationStarted = new Promise((resolve) => { + markMutationStarted = resolve; + }); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: "/tmp/sessions.json", + identities: [sessionKey, "existing-session-id"], + run: async () => { + markMutationStarted(); + await new Promise((release) => { + releaseMutation = release; + }); + }, + }); + await mutationStarted; + const context = makeContext(); + const respond = vi.fn(); + const request = invokeAgent( + { + message: "do not run after queue expiry", + agentId: "main", + sessionKey, + idempotencyKey: runId, + }, + { context, respond, reqId: runId }, + ); + const dedupeKey = `agent:${runId}`; + await waitForAssertion(() => { + expect(context.dedupe.has(dedupeKey)).toBe(true); + }); + const reserved = context.dedupe.get(dedupeKey); + context.dedupe.set(dedupeKey, { + ts: reserved?.ts ?? Date.now(), + ok: true, + payload: { + ...(reserved?.payload as Record), + expiresAtMs: Date.now() - 1, + }, + }); + + releaseMutation(); + await mutation; + await request; + + expect(mocks.agentCommand).not.toHaveBeenCalled(); + expect(context.chatAbortControllers.has(runId)).toBe(false); + expect( + respond.mock.calls.some( + ([ok, payload]) => + ok === true && + (payload as { status?: string; stopReason?: string })?.status === "timeout" && + (payload as { stopReason?: string }).stopReason === "timeout", + ), + ).toBe(true); + }); + + it("preserves a newer terminal reservation result after lifecycle admission waits", async () => { + primeMainAgentRun(); + mocks.agentCommand.mockClear(); + const sessionKey = "agent:main:main"; + const runId = "idem-terminal-during-admission"; + let releaseMutation = () => {}; + let markMutationStarted = () => {}; + const mutationStarted = new Promise((resolve) => { + markMutationStarted = resolve; + }); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: "/tmp/sessions.json", + identities: [sessionKey, "existing-session-id"], + run: async () => { + markMutationStarted(); + await new Promise((release) => { + releaseMutation = release; + }); + }, + }); + await mutationStarted; + const context = makeContext(); + const respond = vi.fn(); + const request = invokeAgent( + { + message: "do not overwrite the replacement result", + agentId: "main", + sessionKey, + idempotencyKey: runId, + }, + { context, respond, reqId: runId }, + ); + const dedupeKey = `agent:${runId}`; + await waitForAssertion(() => { + expect(context.dedupe.has(dedupeKey)).toBe(true); + }); + const replacement = { + ts: Date.now(), + ok: true, + payload: { runId, status: "ok", summary: "replacement completed" }, + }; + context.dedupe.set(dedupeKey, replacement); + + releaseMutation(); + await mutation; + await request; + + expect(context.dedupe.get(dedupeKey)).toBe(replacement); + expect(mocks.agentCommand).not.toHaveBeenCalled(); + expect(respond).toHaveBeenLastCalledWith( + true, + replacement.payload, + undefined, + expect.objectContaining({ cached: true, runId }), + ); + }); + + it("runs gateway agent work inside its lifecycle admission context", async () => { + primeMainAgentRun(); + const sessionKey = "agent:main:main"; + const sessionId = "existing-session-id"; + mocks.agentCommand.mockImplementationOnce(async () => { + await expect( + interruptSessionWorkAdmissions({ + scope: "/tmp/sessions.json", + identities: [sessionKey, sessionId], + timeoutMs: 5, + }), + ).resolves.toBe(true); + return { + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }; + }); + + await invokeAgent({ + message: "run in-band lifecycle work", + agentId: "main", + sessionKey, + idempotencyKey: "idem-agent-admission-context", + }); + + expect(mocks.agentCommand).toHaveBeenCalledOnce(); + }); + + it("classifies gateway lifecycle interruption as restart", async () => { + primeMainAgentRun(); + const sessionKey = "agent:main:main"; + const sessionId = "existing-session-id"; + let observedAbortReason: unknown; + mocks.agentCommand.mockImplementationOnce( + async (opts: { abortSignal?: AbortSignal }) => + await new Promise((_resolve, reject) => { + const finish = () => { + observedAbortReason = opts.abortSignal?.reason; + reject( + observedAbortReason instanceof Error + ? observedAbortReason + : new Error("agent lifecycle interrupted"), + ); + }; + if (opts.abortSignal?.aborted) { + finish(); + return; + } + opts.abortSignal?.addEventListener("abort", finish, { once: true }); + }), + ); + const context = makeContext(); + const runId = "idem-agent-lifecycle-restart"; + await invokeAgent( + { + message: "interrupt as restart", + agentId: "main", + sessionKey, + idempotencyKey: runId, + }, + { context, reqId: runId }, + ); + await waitForAgentCommandCall(); + + await interruptSessionWorkAdmissions({ + scope: "/tmp/sessions.json", + identities: [sessionKey, sessionId], + }); + await flushScheduledDispatchStep(); + + expect(isAgentRunRestartAbortReason(observedAbortReason)).toBe(true); + expectRecordFields(context.dedupe.get(`agent:${runId}`)?.payload, { + runId, + status: "timeout", + stopReason: "restart", + }); + }); + + it("does not mutate a session archived before the initial store update", async () => { + primeMainAgentRun(); + mocks.agentCommand.mockClear(); + const sessionKey = "agent:main:main"; + const archivedEntry = { + sessionId: "existing-session-id", + updatedAt: Date.now(), + archivedAt: Date.now(), + }; + const store = { [sessionKey]: structuredClone(archivedEntry) }; + mocks.updateSessionStore.mockImplementation(async (_path, updater) => await updater(store)); + const respond = vi.fn(); + + await invokeAgent( + { + message: "do not touch the archive", + agentId: "main", + sessionKey, + idempotencyKey: "idem-archive-before-store-update", + }, + { respond, reqId: "idem-archive-before-store-update" }, + ); + + expectRespondError(respond, { + message: `Session "${sessionKey}" is archived. Restore it before starting new work.`, + }); + expect(store).toEqual({ [sessionKey]: archivedEntry }); + expect(mocks.agentCommand).not.toHaveBeenCalled(); + }); + + it("uses the freshest alias when checking archive state before migration", async () => { + const cfg = { + session: { mainKey: "work" }, + agents: { list: [{ id: "main", default: true }] }, + }; + mocks.loadConfigReturn = cfg; + mocks.loadSessionEntry.mockReturnValue({ + cfg, + storePath: "/tmp/sessions.json", + entry: { sessionId: "restored-session", updatedAt: 2 }, + canonicalKey: "agent:main:work", + }); + const store = { + "agent:main:work": { + sessionId: "archived-session", + updatedAt: 1, + archivedAt: 1, + }, + "agent:main:main": { + sessionId: "restored-session", + updatedAt: 2, + }, + }; + mocks.updateSessionStore.mockImplementation(async (_path, updater) => await updater(store)); + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + await invokeAgent({ + message: "continue restored session", + agentId: "main", + sessionKey: "agent:main:main", + idempotencyKey: "idem-restored-alias", + }); + + expect(mocks.agentCommand).toHaveBeenCalled(); + expect(store["agent:main:work"]?.archivedAt).toBeUndefined(); + expect(store["agent:main:main"]).toBeUndefined(); + }); + it("disables single-entry persistence when admission prunes legacy store keys", async () => { mocks.loadConfigReturn = { session: { mainKey: "work" }, @@ -1274,14 +1735,14 @@ describe("gateway agent handler", () => { it("persists first-turn group selectors for a trusted new group session", async () => { const sessionKey = "agent:main:slack:group:C123"; - mocks.loadSessionEntry.mockReturnValue({ + let capturedEntry: Record | undefined; + mocks.loadSessionEntry.mockImplementation(() => ({ cfg: {}, storePath: "/tmp/sessions.json", - entry: undefined, + entry: capturedEntry, canonicalKey: sessionKey, - }); + })); - let capturedEntry: Record | undefined; mocks.updateSessionStore.mockImplementation(async (_path, updater) => { const store: Record = {}; const result = await updater(store); @@ -1325,14 +1786,14 @@ describe("gateway agent handler", () => { it("tags newly-created plugin runtime sessions with the plugin owner", async () => { const sessionKey = "agent:main:dreaming-narrative-light-workspace-1"; - mocks.loadSessionEntry.mockReturnValue({ + let capturedEntry: Record | undefined; + mocks.loadSessionEntry.mockImplementation(() => ({ cfg: {}, storePath: "/tmp/sessions.json", - entry: undefined, + entry: capturedEntry, canonicalKey: sessionKey, - }); + })); - let capturedEntry: Record | undefined; mocks.updateSessionStore.mockImplementation(async (_path, updater) => { const store: Record = {}; const result = await updater(store); @@ -2336,6 +2797,41 @@ describe("gateway agent handler", () => { }); }); + it("allows backend internal runs without a persisted session row", async () => { + const sessionKey = "agent:main:internal:ephemeral"; + mocks.loadSessionEntry.mockReturnValue({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: undefined, + canonicalKey: sessionKey, + }); + mocks.updateSessionStore.mockClear(); + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + await invokeAgent( + { + message: "internal ephemeral work", + agentId: "main", + sessionKey, + sessionEffects: "internal", + suppressPromptPersistence: true, + idempotencyKey: "test-backend-internal-ephemeral", + }, + { + reqId: "backend-internal-ephemeral", + client: backendGatewayClient(), + }, + ); + + const callArgs = await waitForAgentCommandCall<{ sessionId?: string; sessionKey?: string }>(); + expect(callArgs.sessionKey).toBe(sessionKey); + expect(callArgs.sessionId).toEqual(expect.any(String)); + expect(mocks.updateSessionStore).not.toHaveBeenCalled(); + }); + it("forwards admin caller ownership to ingress agent runs", async () => { primeMainAgentRun({ cfg: mocks.loadConfigReturn }); mocks.agentCommand.mockClear(); @@ -6250,7 +6746,10 @@ describe("gateway agent handler chat.abort integration", () => { it("yields after the accepted ack before dispatching heavy agent work", async () => { prime(); - mocks.agentCommand.mockReturnValueOnce(new Promise(() => {})); + mocks.agentCommand.mockResolvedValueOnce({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); const context = makeContext(); const respond = vi.fn(); @@ -6264,9 +6763,7 @@ describe("gateway agent handler chat.abort integration", () => { }, { context, respond, reqId: runId, flushDispatch: false }, ); - - await Promise.resolve(); - await Promise.resolve(); + await pending; expect(mockCallArg(respond)).toBe(true); const acceptedPayload = expectRecordFields(mockCallArg(respond, 0, 1), { @@ -6290,14 +6787,16 @@ describe("gateway agent handler chat.abort integration", () => { }); expect(mocks.agentCommand).not.toHaveBeenCalled(); await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalledTimes(1)); - await pending; expect(mocks.agentCommand).toHaveBeenCalledTimes(1); }); it("does not dispatch when chat.abort lands during the accepted ack yield", async () => { prime(); - mocks.agentCommand.mockReturnValueOnce(new Promise(() => {})); + mocks.agentCommand.mockResolvedValueOnce({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); const context = makeContext(); const respond = vi.fn(); @@ -6839,6 +7338,142 @@ describe("gateway agent handler chat.abort integration", () => { }); }); + it("does not recreate a session deleted during slow attachment setup", async () => { + const sessionKey = "agent:main:main"; + const persistedEntry = { + sessionId: "existing-session-id", + updatedAt: Date.now(), + model: "vision-model", + modelProvider: "test", + }; + let deleted = false; + mocks.loadSessionEntry.mockImplementation(() => ({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: deleted ? undefined : persistedEntry, + canonicalKey: sessionKey, + })); + mocks.updateSessionStore.mockClear(); + mocks.agentCommand.mockClear(); + + let releaseCatalog: (() => void) | undefined; + const context = { + ...makeContext(), + loadGatewayModelCatalog: vi.fn( + async () => + await new Promise((resolve) => { + releaseCatalog = () => + resolve([ + { + id: "vision-model", + name: "vision-model", + provider: "test", + input: ["image"], + }, + ]); + }), + ), + } as unknown as GatewayRequestContext; + const respond = vi.fn(); + const runId = "idem-delete-during-attachment-setup"; + const pending = invokeAgent( + { + message: "inspect this", + agentId: "main", + sessionKey, + idempotencyKey: runId, + attachments: [ + { + type: "file", + mimeType: "image/png", + fileName: "pixel.png", + content: Buffer.from("not really a png").toString("base64"), + }, + ], + }, + { context, respond, reqId: runId, flushDispatch: false }, + ); + + await waitForAssertion(() => expect(context.loadGatewayModelCatalog).toHaveBeenCalled()); + deleted = true; + releaseCatalog?.(); + await pending; + + expectRespondError(respond, { + message: `Session "${sessionKey}" was deleted while starting work. Retry.`, + }); + expect(mocks.updateSessionStore).not.toHaveBeenCalled(); + expect(mocks.agentCommand).not.toHaveBeenCalled(); + }); + + it("does not dispatch into a session reset during slow attachment setup", async () => { + const sessionKey = "agent:main:main"; + const persistedEntry = { + sessionId: "existing-session-id", + updatedAt: Date.now(), + model: "vision-model", + modelProvider: "test", + }; + let currentEntry = persistedEntry; + mocks.loadSessionEntry.mockImplementation(() => ({ + cfg: {}, + storePath: "/tmp/sessions.json", + entry: currentEntry, + canonicalKey: sessionKey, + })); + mocks.updateSessionStore.mockClear(); + mocks.agentCommand.mockClear(); + + let releaseCatalog: (() => void) | undefined; + const context = { + ...makeContext(), + loadGatewayModelCatalog: vi.fn( + async () => + await new Promise((resolve) => { + releaseCatalog = () => + resolve([ + { + id: "vision-model", + name: "vision-model", + provider: "test", + input: ["image"], + }, + ]); + }), + ), + } as unknown as GatewayRequestContext; + const respond = vi.fn(); + const runId = "idem-reset-during-attachment-setup"; + const pending = invokeAgent( + { + message: "inspect this", + agentId: "main", + sessionKey, + idempotencyKey: runId, + attachments: [ + { + type: "file", + mimeType: "image/png", + fileName: "pixel.png", + content: Buffer.from("not really a png").toString("base64"), + }, + ], + }, + { context, respond, reqId: runId, flushDispatch: false }, + ); + + await waitForAssertion(() => expect(context.loadGatewayModelCatalog).toHaveBeenCalled()); + currentEntry = { ...persistedEntry, sessionId: "reset-session-id" }; + releaseCatalog?.(); + await pending; + + expectRespondError(respond, { + message: `Session "${sessionKey}" changed while starting work. Retry.`, + }); + expect(mocks.updateSessionStore).not.toHaveBeenCalled(); + expect(mocks.agentCommand).not.toHaveBeenCalled(); + }); + it("keeps selected-global agent scope while aborting during attachment setup", async () => { mocks.listAgentIds.mockReturnValue(["main", "work"]); mocks.loadSessionEntry.mockReturnValue({ diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 73b12d3bc0cd..19e2c6bc9a73 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -2,6 +2,7 @@ // and related session-aware RPC handlers used by UI and operator clients. import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; +import { isFutureDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -49,6 +50,7 @@ import type { AgentInternalEvent } from "../../agents/internal-events.js"; import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js"; import { AGENT_RUN_RESTART_ABORT_STOP_REASON, + createAgentRunRestartAbortError, isAgentRunRestartAbortReason, } from "../../agents/run-termination.js"; import { @@ -73,6 +75,7 @@ import { resolveSessionFilePath, resolveSessionFilePathOptions, resolveSessionLifecycleTimestamps, + resolveSessionWorkStartError, resolveSessionResetPolicy, resolveSessionResetType, type SessionEntry, @@ -82,6 +85,7 @@ import { import { hasProviderOwnedSession } from "../../config/sessions/entry-freshness.js"; import { resolveMaintenanceConfigFromInput } from "../../config/sessions/store-maintenance.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { isAbortError } from "../../infra/abort-signal.js"; import { assertAgentRunLifecycleGenerationCurrent, claimAgentRunContext, @@ -96,7 +100,6 @@ import { } from "../../infra/outbound/agent-delivery.js"; import { shouldDowngradeDeliveryToSessionOnly } from "../../infra/outbound/best-effort-delivery.js"; import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js"; -import { isAbortError } from "../../infra/abort-signal.js"; import { loadVoiceWakeRoutingConfig, resolveVoiceWakeRouteByTrigger, @@ -121,6 +124,10 @@ import { parseRawSessionConversationRef, parseThreadSessionSuffix, } from "../../sessions/session-key-utils.js"; +import { + beginSessionWorkAdmission, + type SessionWorkAdmissionLease, +} from "../../sessions/session-lifecycle-admission.js"; import { createRunningTaskRun, finalizeTaskRunByRunId } from "../../tasks/detached-task-runtime.js"; import type { TaskStatus } from "../../tasks/task-registry.types.js"; import { @@ -161,6 +168,7 @@ import { loadSessionEntry, migrateAndPruneGatewaySessionStoreKey, resolveDeletedAgentIdFromSessionKey, + resolveFreshestSessionEntryFromStoreKeys, resolveGatewaySessionStoreTarget, resolveGatewayModelSupportsImages, resolveSessionStoreKey, @@ -258,7 +266,7 @@ function respondDeletedAgentSession(params: { return true; } -function respondDeletedAgentSessionForKey(params: { +function respondUnavailableAgentSessionForKey(params: { sessionKey: string; agentId?: string; respond: GatewayRequestHandlerOptions["respond"]; @@ -267,13 +275,23 @@ function respondDeletedAgentSessionForKey(params: { ...(params.agentId ? { agentId: params.agentId } : {}), clone: false, }); - return respondDeletedAgentSession({ - cfg, - canonicalKey, - entry, - acpMetadataSessionKey: legacyKey, - respond: params.respond, - }); + if ( + respondDeletedAgentSession({ + cfg, + canonicalKey, + entry, + acpMetadataSessionKey: legacyKey, + respond: params.respond, + }) + ) { + return true; + } + const archivedSessionError = resolveSessionWorkStartError(canonicalKey, entry); + if (!archivedSessionError) { + return false; + } + params.respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, archivedSessionError)); + return true; } function resolveAllowModelOverrideFromClient( @@ -792,6 +810,7 @@ function isAcceptedAgentDedupePayload(payload: unknown): payload is { expiresAtMs?: unknown; ownerConnId?: unknown; ownerDeviceId?: unknown; + reservationId?: unknown; runId?: unknown; sessionKey?: unknown; status: "accepted"; @@ -1265,6 +1284,7 @@ export const agentHandlers: GatewayRequestHandlers = { } let agentDedupeReserved = false; let agentRunAccepted = false; + const agentReservationId = randomUUID(); let committedResetCompletion: | { reason: "new" | "reset"; @@ -1295,6 +1315,7 @@ export const agentHandlers: GatewayRequestHandlers = { ok: true, payload: { runId, + reservationId: agentReservationId, status: "accepted" as const, sessionKey, ...(dedupeSessionResolvesGlobal && dedupeAgentId ? { agentId: dedupeAgentId } : {}), @@ -1328,6 +1349,13 @@ export const agentHandlers: GatewayRequestHandlers = { ) { return; } + if ( + reservedEntry?.ok && + isAcceptedAgentDedupePayload(reservedEntry.payload) && + reservedEntry.payload.reservationId !== agentReservationId + ) { + return; + } deleteGatewayDedupeEntries({ dedupe: context.dedupe, keys: agentDedupeKeys, @@ -1493,11 +1521,11 @@ export const agentHandlers: GatewayRequestHandlers = { return; } } - // Keep deleted-agent rejection ahead of dedupe, media offload, reset, and - // dispatch so agent RPC shares the chat.send / sessions.send boundary. + // Keep unavailable-session rejection ahead of dedupe, media offload, reset, + // and dispatch so agent RPC shares the chat.send / sessions.send boundary. if ( requestedSessionKey && - respondDeletedAgentSessionForKey({ sessionKey: requestedSessionKey, agentId, respond }) + respondUnavailableAgentSessionForKey({ sessionKey: requestedSessionKey, agentId, respond }) ) { return; } @@ -1553,6 +1581,22 @@ export const agentHandlers: GatewayRequestHandlers = { ? "global" : requestedSessionKey; reservePreAcceptedAgentDedupe(preAcceptedReservedSessionKey, agentId); + const preAttachmentSession = requestedSessionKey + ? (() => { + const loaded = loadSessionEntry(requestedSessionKey, { + ...(agentId ? { agentId } : {}), + clone: false, + }); + return loaded.entry + ? { + canonicalKey: loaded.canonicalKey, + sessionId: loaded.entry.sessionId, + } + : undefined; + })() + : undefined; + let gatewayWorkAdmission: SessionWorkAdmissionLease | undefined; + let gatewayAdmissionTransferred = false; try { let message = (request.message ?? "").trim(); @@ -1723,13 +1767,204 @@ export const agentHandlers: GatewayRequestHandlers = { } let resolvedSessionId = requestedSessionId; let sessionEntry: SessionEntry | undefined; + let sessionPersistedBeforeGatewayAdmission = false; let bestEffortDeliver = requestedBestEffortDeliver ?? false; let cfgForAgent: OpenClawConfig | undefined; let resolvedSessionKey = requestedSessionKey; let resolvedSessionAgentId: string | undefined; let isNewSession = false; + let supersededSessionId: string | undefined; let skipAgentInitialSessionTouch = false; let pendingChatRun: { sessionKey: string; agentId?: string } | undefined; + let admittedSessionId = resolvedSessionId ?? runId; + let admittedRunAbort: ReturnType | undefined; + let postAdmissionAbort: ReturnType; + let postAdmissionTimeout: + | { + runId: string; + status: "timeout"; + summary: "aborted"; + stopReason: "timeout"; + timeoutPhase: "queue"; + providerStarted: false; + } + | undefined; + let postAdmissionSuperseded = false; + let lifecycleRotatedDuringAdmission = false; + const admissionAgentId = () => + resolvedSessionKey === "global" + ? (resolvedSessionAgentId ?? agentId ?? resolveDefaultAgentId(cfgForAgent ?? cfg)) + : undefined; + const assertGatewayWorkAdmissionAllowed = () => { + const latestPreRegistrationAbort = readGatewayDedupeEntry({ + dedupe: context.dedupe, + keys: agentDedupeKeys, + }); + if ( + isPreRegistrationAbortedAgentDedupeEntryForSession({ + entry: latestPreRegistrationAbort, + runId, + sessionKey: resolvedSessionKey, + alternateSessionKeys: [preAcceptedReservedSessionKey, requestedSessionKey], + }) + ) { + postAdmissionAbort = latestPreRegistrationAbort; + return; + } + if (agentDedupeReserved) { + if (!latestPreRegistrationAbort) { + postAdmissionTimeout = { + runId, + status: "timeout", + summary: "aborted", + stopReason: "timeout", + timeoutPhase: "queue", + providerStarted: false, + }; + setAbortedAgentDedupeEntries({ + dedupe: context.dedupe, + keys: agentDedupeKeys, + agentId: admissionAgentId(), + sessionKey: resolvedSessionKey, + runId, + stopReason: "timeout", + }); + return; + } + if ( + !latestPreRegistrationAbort.ok || + !isAcceptedAgentDedupePayload(latestPreRegistrationAbort.payload) + ) { + postAdmissionAbort = latestPreRegistrationAbort; + return; + } + if (latestPreRegistrationAbort.payload.reservationId !== agentReservationId) { + postAdmissionSuperseded = true; + return; + } + if ( + !isFutureDateTimestampMs(latestPreRegistrationAbort.payload.expiresAtMs, { + nowMs: Date.now(), + }) + ) { + postAdmissionTimeout = { + runId, + status: "timeout", + summary: "aborted", + stopReason: "timeout", + timeoutPhase: "queue", + providerStarted: false, + }; + setAbortedAgentDedupeEntries({ + dedupe: context.dedupe, + keys: agentDedupeKeys, + agentId: admissionAgentId(), + sessionKey: resolvedSessionKey, + runId, + stopReason: "timeout", + }); + return; + } + } + lifecycleRotatedDuringAdmission = abortForLifecycleRotation({ + sessionKey: resolvedSessionKey, + agentId: admissionAgentId(), + }); + if (lifecycleRotatedDuringAdmission || !resolvedSessionKey) { + return; + } + let latestEntry = loadSessionEntry(resolvedSessionKey, { + ...(resolvedSessionKey === "global" ? { agentId: admissionAgentId() } : {}), + clone: false, + }).entry; + // Legacy stores may only carry the requested spelling (e.g. bare + // "main"); a canonical-only re-read would misreport those sessions + // as deleted mid-start. + if (!latestEntry && requestedSessionKey && requestedSessionKey !== resolvedSessionKey) { + latestEntry = loadSessionEntry(requestedSessionKey, { clone: false }).entry; + } + if (sessionPersistedBeforeGatewayAdmission && !latestEntry) { + throw new Error( + `Session "${resolvedSessionKey}" was deleted while starting work. Retry.`, + ); + } + const archivedError = resolveSessionWorkStartError(resolvedSessionKey, latestEntry); + if (archivedError) { + throw new Error(archivedError); + } + if (latestEntry?.sessionId && latestEntry.sessionId !== supersededSessionId) { + admittedSessionId = latestEntry.sessionId; + } + }; + const interruptGatewayWorkAdmission = () => { + if (admittedRunAbort?.entry) { + admittedRunAbort.entry.abortStopReason = AGENT_RUN_RESTART_ABORT_STOP_REASON; + } + if (admittedRunAbort) { + admittedRunAbort.controller.abort(createAgentRunRestartAbortError()); + return; + } + const reservedEntry = readGatewayDedupeEntry({ + dedupe: context.dedupe, + keys: agentDedupeKeys, + }); + if ( + reservedEntry?.ok && + isAcceptedAgentDedupePayload(reservedEntry.payload) && + reservedEntry.payload.reservationId === agentReservationId + ) { + setAbortedAgentDedupeEntries({ + dedupe: context.dedupe, + keys: agentDedupeKeys, + agentId: admissionAgentId(), + sessionKey: resolvedSessionKey, + runId, + stopReason: AGENT_RUN_RESTART_ABORT_STOP_REASON, + }); + } + }; + const acquireGatewayWorkAdmission = async (scope: string) => { + if (gatewayWorkAdmission) { + return; + } + gatewayWorkAdmission = await beginSessionWorkAdmission({ + scope, + identities: [resolvedSessionKey, resolvedSessionId], + assertAllowed: assertGatewayWorkAdmissionAllowed, + onInterrupt: interruptGatewayWorkAdmission, + }); + }; + const respondToGatewayAdmissionOutcome = (): boolean => { + if (postAdmissionAbort) { + gatewayWorkAdmission?.release(); + agentRunAccepted = true; + respond(postAdmissionAbort.ok, postAdmissionAbort.payload, postAdmissionAbort.error, { + cached: true, + runId, + }); + return true; + } + if (postAdmissionTimeout) { + gatewayWorkAdmission?.release(); + agentRunAccepted = true; + respond(true, postAdmissionTimeout, undefined, { cached: true, runId }); + return true; + } + if (postAdmissionSuperseded) { + gatewayWorkAdmission?.release(); + agentRunAccepted = true; + respond(true, { runId, status: "in_flight" as const }, undefined, { + cached: true, + runId, + }); + return true; + } + if (lifecycleRotatedDuringAdmission) { + gatewayWorkAdmission?.release(); + return true; + } + return false; + }; const resetCommandMatch = message.match(RESET_COMMAND_RE); if (resetCommandMatch && requestedSessionKey) { @@ -1863,6 +2098,34 @@ export const agentHandlers: GatewayRequestHandlers = { legacyKey, } = loadSessionEntry(requestedSessionKey, sessionLoadOptions); cfgForAgent = cfgLocal; + const sessionExistedBeforeAttachmentSetup = + preAttachmentSession?.canonicalKey === canonicalKey ? preAttachmentSession : undefined; + if (sessionExistedBeforeAttachmentSetup && !entry) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `Session "${canonicalKey}" was deleted while starting work. Retry.`, + ), + ); + return; + } + if ( + sessionExistedBeforeAttachmentSetup && + entry?.sessionId !== sessionExistedBeforeAttachmentSetup.sessionId + ) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `Session "${canonicalKey}" changed while starting work. Retry.`, + ), + ); + return; + } + sessionPersistedBeforeGatewayAdmission = entry !== undefined; if ( respondDeletedAgentSession({ cfg: cfgLocal, @@ -1874,6 +2137,11 @@ export const agentHandlers: GatewayRequestHandlers = { ) { return; } + const archivedSessionError = resolveSessionWorkStartError(canonicalKey, entry); + if (archivedSessionError) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, archivedSessionError)); + return; + } const sessionMaintenanceConfig = resolveMaintenanceConfigFromInput( cfgLocal.session?.maintenance, ); @@ -2216,11 +2484,21 @@ export const agentHandlers: GatewayRequestHandlers = { freshness = patchBuild.freshness; sessionEntry = mergeSessionEntry(entry, patchBuild.patch); resolvedSessionId = sessionEntry?.sessionId ?? sessionId; + admittedSessionId = resolvedSessionId ?? runId; const canonicalSessionKey = canonicalKey; resolvedSessionKey = canonicalSessionKey; const sessionAgentId = canonicalSessionAgentId; resolvedSessionAgentId = sessionAgentId; const mainSessionKey = mainSessionKeyForRequest; + try { + await acquireGatewayWorkAdmission(storePath ?? `agent:${sessionAgentId}`); + } catch (err) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, formatForLog(err))); + return; + } + if (respondToGatewayAdmissionOutcome()) { + return; + } // Legacy stores may lack sessionStartedAt entirely. Pre-compute a // JSONL-transcript-derived candidate outside the store lock; the // updater below only writes it when the freshly-loaded store still @@ -2247,6 +2525,8 @@ export const agentHandlers: GatewayRequestHandlers = { } | undefined; let persisted: SessionEntry | undefined; + let archivedDuringStoreUpdateError: string | undefined; + let deletedDuringStoreUpdateError: string | undefined; try { persisted = await updateSessionStore( storePath, @@ -2261,6 +2541,38 @@ export const agentHandlers: GatewayRequestHandlers = { store, ...(sessionAgentId ? { agentId: sessionAgentId } : {}), }); + const preMigrationEntry = resolveFreshestSessionEntryFromStoreKeys( + store, + preMigrationTarget.storeKeys, + ); + const initialTarget = resolveGatewaySessionStoreTarget({ + cfg: cfgLocal, + key: canonicalSessionKey, + store, + ...(sessionAgentId ? { agentId: sessionAgentId } : {}), + }); + const initialEntry = resolveFreshestSessionEntryFromStoreKeys( + store, + initialTarget.storeKeys, + ); + // A completed delete must win over this request's earlier read; + // otherwise the initial touch would recreate the removed row. + // The row counts as deleted only when both the requested and + // canonical alias sets miss it: exec-approval followups read + // under the canonical key while legacy stores may only carry + // the requested spelling (e.g. bare "main"). + if (entry && !preMigrationEntry && !initialEntry) { + deletedDuringStoreUpdateError = `Session "${canonicalSessionKey}" was deleted while starting work. Retry.`; + throw new Error(deletedDuringStoreUpdateError); + } + const archivedError = resolveSessionWorkStartError( + preMigrationTarget.canonicalKey, + preMigrationEntry, + ); + if (archivedError) { + archivedDuringStoreUpdateError = archivedError; + throw new Error(archivedError); + } const hadLegacyStoreKey = preMigrationTarget.storeKeys.some( (storeKey) => storeKey !== preMigrationTarget.canonicalKey && Object.hasOwn(store, storeKey), @@ -2317,14 +2629,44 @@ export const agentHandlers: GatewayRequestHandlers = { if (abortForLifecycleRotation({ sessionKey: canonicalSessionKey, agentId })) { return; } + if (archivedDuringStoreUpdateError) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, archivedDuringStoreUpdateError), + ); + return; + } + if (deletedDuringStoreUpdateError) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, formatForLog(err))); + return; + } throw err; } - if (abortForLifecycleRotation({ sessionKey: canonicalSessionKey, agentId })) { - return; - } if (persisted) { sessionEntry = persisted; resolvedSessionId = sessionEntry.sessionId; + sessionPersistedBeforeGatewayAdmission = true; + } + if ( + patchBuild.isNewSession && + entry?.sessionId && + resolvedSessionId !== entry.sessionId + ) { + supersededSessionId = entry.sessionId; + } + admittedSessionId = resolvedSessionId ?? runId; + try { + assertGatewayWorkAdmissionAllowed(); + } catch (err) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, formatForLog(err))); + return; + } + if (respondToGatewayAdmissionOutcome()) { + return; + } + if (abortForLifecycleRotation({ sessionKey: canonicalSessionKey, agentId })) { + return; } skipAgentInitialSessionTouch = touchInteraction; if (deniedBySendPolicy) { @@ -2344,6 +2686,9 @@ export const agentHandlers: GatewayRequestHandlers = { resolvedGroupId = patchBuild.groupId; resolvedGroupChannel = patchBuild.groupChannel; resolvedGroupSpace = patchBuild.groupSpace; + if (isNewSession && entry?.sessionId && resolvedSessionId !== entry.sessionId) { + supersededSessionId = entry.sessionId; + } if ( !suppressVisibleSessionEffects && isNewSession && @@ -2622,27 +2967,64 @@ export const agentHandlers: GatewayRequestHandlers = { const activeAuthProvider = resolveProviderIdForAuth(activeModelProvider, { config: cfgForAgent ?? cfg, }); - const activeRunAbort = registerChatAbortController({ - chatAbortControllers: context.chatAbortControllers, - runId, - sessionId: resolvedSessionId ?? runId, - sessionKey: resolvedSessionKey, - agentId: resolvedSessionKey === "global" ? activeSessionAgentId : undefined, - timeoutMs, - now, - expiresAtMs: resolveAgentRunExpiresAtMs({ now, timeoutMs }), - ownerConnId, - ownerDeviceId, - providerId: activeModelProvider, - authProviderId: activeAuthProvider, - isAbortable: () => isEmbeddedAgentRunAbortableForRunId(runId), - onRemoved: () => clearEmbeddedAgentRunAbortabilityForRunId(runId), - controlUiVisible: !suppressVisibleSessionEffects, - kind: "agent", - lifecycleGeneration, - }); + const lifecycleStorePath = resolvedSessionKey + ? loadSessionEntry(resolvedSessionKey, { + ...(resolvedSessionKey === "global" ? { agentId: activeSessionAgentId } : {}), + clone: false, + }).storePath + : `agent:${activeSessionAgentId}`; + try { + await acquireGatewayWorkAdmission(lifecycleStorePath); + assertGatewayWorkAdmissionAllowed(); + const hasAdmissionOutcome = Boolean( + postAdmissionAbort || + postAdmissionTimeout || + postAdmissionSuperseded || + lifecycleRotatedDuringAdmission, + ); + if (!hasAdmissionOutcome) { + admittedRunAbort = registerChatAbortController({ + chatAbortControllers: context.chatAbortControllers, + runId, + sessionId: admittedSessionId, + sessionKey: resolvedSessionKey, + agentId: admissionAgentId(), + timeoutMs, + now, + expiresAtMs: resolveAgentRunExpiresAtMs({ now, timeoutMs }), + ownerConnId, + ownerDeviceId, + providerId: activeModelProvider, + authProviderId: activeAuthProvider, + isAbortable: () => isEmbeddedAgentRunAbortableForRunId(runId), + onRemoved: () => clearEmbeddedAgentRunAbortabilityForRunId(runId), + controlUiVisible: !suppressVisibleSessionEffects, + kind: "agent", + lifecycleGeneration, + }); + } + } catch (err) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, formatForLog(err))); + return; + } + if (respondToGatewayAdmissionOutcome()) { + return; + } + const activeGatewayWorkAdmission = gatewayWorkAdmission; + if (!activeGatewayWorkAdmission) { + respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "agent run admission failed")); + return; + } + const activeRunAbort = admittedRunAbort; + if (!activeRunAbort) { + activeGatewayWorkAdmission.release(); + respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "agent run admission failed")); + return; + } + resolvedSessionId = admittedSessionId; const existingRunAbort = context.chatAbortControllers.get(runId); if (!activeRunAbort.registered && existingRunAbort) { + activeGatewayWorkAdmission.release(); agentRunAccepted = existingRunAbort.kind === "agent"; respond(true, { runId, status: "in_flight" as const }, undefined, { cached: true, @@ -2650,6 +3032,13 @@ export const agentHandlers: GatewayRequestHandlers = { }); return; } + if (!activeRunAbort.registered) { + activeGatewayWorkAdmission.release(); + } + const cleanupAdmittedRun: typeof activeRunAbort.cleanup = (options) => { + activeRunAbort.cleanup(options); + activeGatewayWorkAdmission.release(); + }; if (activeRunAbort.registered) { retainEmbeddedAgentRunAbortabilityForRunId(runId); if (pendingChatRun) { @@ -2663,13 +3052,13 @@ export const agentHandlers: GatewayRequestHandlers = { runId, suppressVisibleSessionEffects ? { isControlUiVisible: false, lifecycleGeneration } - : { sessionKey: resolvedSessionKey, lifecycleGeneration }, + : { + sessionKey: resolvedSessionKey, + lifecycleGeneration, + }, ); } } - const cleanupActiveRunAbort = (opts?: { force?: boolean }) => { - activeRunAbort.cleanup(opts); - }; const resolvedThreadId = explicitThreadId ?? deliveryPlan.resolvedThreadId; // Confirmed only when the caller is the trusted in-process backend ACP @@ -2745,7 +3134,8 @@ export const agentHandlers: GatewayRequestHandlers = { // starts potentially heavy synchronous prompt/context setup. The dispatch // is scheduled out of this request handler so immediate agent.wait calls // can reach the gateway before the pre-turn runner monopolizes the loop. - void (async () => { + gatewayAdmissionTransferred = true; + void activeGatewayWorkAdmission.run(async () => { await yieldAfterAgentAcceptedAck(); let dispatched = false; @@ -2927,7 +3317,7 @@ export const agentHandlers: GatewayRequestHandlers = { runId, dedupeKeys: agentDedupeKeys, abortController: activeRunAbort.controller, - cleanupAbortController: cleanupActiveRunAbort, + cleanupAbortController: cleanupAdmittedRun, respond, context, taskTrackingMode: dispatchTaskTrackingMode, @@ -2956,11 +3346,14 @@ export const agentHandlers: GatewayRequestHandlers = { }); } finally { if (!dispatched) { - cleanupActiveRunAbort({ force: true }); + cleanupAdmittedRun({ force: true }); } } - })(); + }); } finally { + if (!gatewayAdmissionTransferred) { + gatewayWorkAdmission?.release(); + } clearUnacceptedExecApprovalFollowupDedupe(); } }, diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index 0ab368638c74..0872712fd3c2 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -22,6 +22,8 @@ import type { MsgContext } from "../../auto-reply/templating.js"; import { appendSessionTranscriptMessage } from "../../config/sessions/transcript-append.js"; import { resolveMirroredTranscriptText } from "../../config/sessions/transcript-mirror.js"; import { getAgentRunContext } from "../../infra/agent-events.js"; +import { runExclusiveSessionLifecycleMutation } from "../../sessions/session-lifecycle-admission.js"; +import { createDeferred } from "../../test-utils/deferred.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { readSessionTranscriptIndex } from "../session-transcript-index.fs.js"; import type { GatewayRequestContext } from "./types.js"; @@ -3287,6 +3289,71 @@ describe("chat directive tag stripping for non-streaming final payloads", () => expect(extractFirstTextBlock(broadcastPayload)).toBe(""); }); + it("chat.inject rejects archived sessions without appending", async () => { + createTranscriptFixture("openclaw-chat-inject-archived-"); + mockState.sessionEntry = { archivedAt: Date.now() }; + const respond = vi.fn(); + const context = createChatContext(); + + await chatHandlers["chat.inject"]({ + params: { sessionKey: "main", message: "must stay read-only" }, + respond, + req: {} as never, + client: null as never, + isWebchatConnect: () => false, + context: context as GatewayRequestContext, + }); + + const response = lastRespondCall(respond); + expect(response?.[0]).toBe(false); + expect(response?.[2]?.message).toMatch(/archived/i); + expect(context.broadcast).not.toHaveBeenCalled(); + expect(readTranscriptJsonLines(mockState.transcriptPath)).toHaveLength(1); + }); + + it("chat.inject rechecks archive state after lifecycle admission waits", async () => { + createTranscriptFixture("openclaw-chat-inject-archive-race-"); + const storePath = path.join(path.dirname(mockState.transcriptPath), "sessions.json"); + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: ["main", mockState.sessionId], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + const respond = vi.fn(); + const context = createChatContext(); + + try { + const inject = chatHandlers["chat.inject"]({ + params: { sessionKey: "main", message: "must lose the archive race" }, + respond, + req: {} as never, + client: null as never, + isWebchatConnect: () => false, + context: context as GatewayRequestContext, + }); + await waitForAssertion(() => expect(mockState.loadSessionEntryCalls).toHaveLength(1)); + mockState.sessionEntry = { archivedAt: Date.now() }; + releaseMutation.resolve(); + await mutation; + await inject; + + const response = lastRespondCall(respond); + expect(response?.[0]).toBe(false); + expect(response?.[2]?.message).toMatch(/archived/i); + expect(context.broadcast).not.toHaveBeenCalled(); + expect(readTranscriptJsonLines(mockState.transcriptPath)).toHaveLength(1); + } finally { + releaseMutation.resolve(); + await mutation; + } + }); + it("chat.send non-streaming final keeps message defined for directive-only assistant text", async () => { createTranscriptFixture("openclaw-chat-send-directive-only-"); mockState.finalText = "[[reply_to_current]]"; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index da8c28ab83b0..3a7745606fd5 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -1,11 +1,12 @@ // Chat gateway methods implement chat.send/history/abort/inject/metadata and // bridge UI RPCs to agent dispatch, transcripts, media, and streaming state. -import { createHash } from "node:crypto"; +import { createHash, randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { performance } from "node:perf_hooks"; import { fileURLToPath } from "node:url"; import { isAudioFileName } from "@openclaw/media-core/mime"; +import { isFutureDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import type { FastMode } from "@openclaw/normalization-core/string-coerce"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; @@ -44,6 +45,7 @@ import { runAgentHarnessBeforeMessageWriteHook } from "../../agents/harness/hook import { modelCatalogBrowseRequiresFullDiscovery } from "../../agents/model-catalog-browse.js"; import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js"; import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js"; +import { createAgentRunRestartAbortError } from "../../agents/run-termination.js"; import type { AgentMessage } from "../../agents/runtime/index.js"; import { ensureSandboxWorkspaceForSession } from "../../agents/sandbox/context.js"; import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; @@ -61,7 +63,11 @@ import { type StageSandboxMediaResult, } from "../../auto-reply/reply/stage-sandbox-media.js"; import type { MsgContext, TemplateContext } from "../../auto-reply/templating.js"; -import { resolveSessionFilePath, updateSessionStoreEntry } from "../../config/sessions.js"; +import { + resolveSessionFilePath, + resolveSessionWorkStartError, + updateSessionStoreEntry, +} from "../../config/sessions.js"; import { resolveMirroredTranscriptText } from "../../config/sessions/transcript-mirror.js"; import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -101,6 +107,7 @@ import { normalizeAgentId, scopeLegacySessionKeyToAgent } from "../../routing/se import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; import { parseAgentSessionKey } from "../../sessions/session-key-utils.js"; +import { beginSessionWorkAdmission } from "../../sessions/session-lifecycle-admission.js"; import { createUserTurnTranscriptRecorder, type UserTurnInput, @@ -127,6 +134,7 @@ import { type ChatAbortOps, isChatStopCommandText, registerChatAbortController, + resolveChatRunExpiresAtMs, resolveInFlightRunSnapshot, updateChatRunProvider, } from "../chat-abort.js"; @@ -156,8 +164,17 @@ import { createManagedOutgoingImageBlocks, } from "../managed-image-attachments.js"; import { ADMIN_SCOPE } from "../method-scopes.js"; -import { chatAbortMarkerTimestampMs, type ChatRunTiming } from "../server-chat-state.js"; +import { + chatAbortMarkerTimestampMs, + createChatAbortMarker, + type ChatRunTiming, +} from "../server-chat-state.js"; import { getMaxChatHistoryMessagesBytes, MAX_PAYLOAD_BYTES } from "../server-constants.js"; +import { + PENDING_CHAT_SEND_DEDUPE_PREFIX, + pendingChatSendDedupeKey, + type DedupeEntry, +} from "../server-shared.js"; import { resolveSessionHistoryTailReadOptions } from "../session-history-state.js"; import { persistGatewaySessionLifecycleEvent } from "../session-lifecycle-state.js"; import { readSessionTranscriptIndex } from "../session-transcript-index.fs.js"; @@ -229,12 +246,15 @@ type ChatAbortRequester = { type PreRegisteredAgentDedupePayload = { agentId?: unknown; + attemptId?: unknown; controlUiVisible?: unknown; dedupeKeys?: unknown; + expiresAtMs?: unknown; ownerConnId?: unknown; ownerDeviceId?: unknown; runId?: unknown; sessionKey?: unknown; + sessionKeyAliases?: unknown; status?: unknown; }; @@ -634,27 +654,6 @@ type ChatSendOriginatingRoute = { explicitDeliverRoute: boolean; }; -const ACTIVE_CHAT_SEND_DEDUPE_PREFIX = "chat:active-send"; - -function resolveActiveChatSendRunId(value: unknown): string | null { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return null; - } - const runId = (value as { runId?: unknown }).runId; - return typeof runId === "string" && runId.trim() ? runId : null; -} - -function clearActiveChatSendDedupeRun( - dedupe: GatewayRequestContext["dedupe"], - key: string | null, - runId: string, -) { - if (!key || resolveActiveChatSendRunId(dedupe.get(key)?.payload) !== runId) { - return; - } - dedupe.delete(key); -} - function buildAbortedChatSendPayload(params: { runId: string; endedAt: number; @@ -669,34 +668,6 @@ function buildAbortedChatSendPayload(params: { }; } -function buildActiveChatSendDedupeKey(params: { - attachmentCount: number; - explicitDeliverRoute: boolean; - message: string; - originatingChannel: string; - sessionKey: string; - systemScope?: string; -}): string | null { - const message = params.message.trim(); - if ( - !message || - message.startsWith("/") || - params.attachmentCount > 0 || - params.explicitDeliverRoute || - normalizeMessageChannel(params.originatingChannel) !== INTERNAL_MESSAGE_CHANNEL - ) { - return null; - } - const dedupeParts = params.systemScope?.trim() - ? [params.sessionKey, message, params.systemScope.trim()] - : [params.sessionKey, message]; - const digest = createHash("sha256") - .update(JSON.stringify(dedupeParts)) - .digest("hex") - .slice(0, 32); - return `${ACTIVE_CHAT_SEND_DEDUPE_PREFIX}:${digest}`; -} - function validateChatSelectedAgent(params: { cfg: OpenClawConfig; requestedSessionKey: string; @@ -2214,7 +2185,13 @@ function readPreRegisteredAgentDedupePayloadForSession(params: { if (payloadRunId && payloadRunId !== params.runId) { return undefined; } - if (normalizeUnknownText(payload.sessionKey) !== params.sessionKey) { + const payloadSessionKeys = new Set([ + normalizeUnknownText(payload.sessionKey), + ...(Array.isArray(payload.sessionKeyAliases) + ? payload.sessionKeyAliases.map(normalizeUnknownText) + : []), + ]); + if (!payloadSessionKeys.has(params.sessionKey)) { return undefined; } const agentId = normalizeOptionalText(params.agentId)?.toLowerCase(); @@ -2236,11 +2213,12 @@ function readPreRegisteredAgentDedupePayloadForSession(params: { return payload; } -function readPreRegisteredAgentRun(params: { +function readPreRegisteredRun(params: { key: string; entry: GatewayRequestContext["dedupe"] extends Map ? T | undefined : never; + keyPrefix: string; }): PreRegisteredAgentRun | undefined { - if (!params.key.startsWith("agent:") || !params.entry?.ok) { + if (!params.key.startsWith(params.keyPrefix) || !params.entry?.ok) { return undefined; } const payload = params.entry.payload as PreRegisteredAgentDedupePayload | undefined; @@ -2250,7 +2228,9 @@ function readPreRegisteredAgentRun(params: { if (payload.controlUiVisible === false) { return undefined; } - const runId = normalizeUnknownText(payload.runId) ?? normalizeOptionalText(params.key.slice(6)); + const runId = + normalizeUnknownText(payload.runId) ?? + normalizeOptionalText(params.key.slice(params.keyPrefix.length)); const sessionKey = normalizeUnknownText(payload.sessionKey); if (!runId || !sessionKey) { return undefined; @@ -2258,7 +2238,7 @@ function readPreRegisteredAgentRun(params: { return { runId, sessionKey, payload }; } -function canRequesterAbortPreRegisteredAgentRun( +function canRequesterAbortPreRegisteredRun( payload: PreRegisteredAgentDedupePayload, requester: ChatAbortRequester, ): boolean { @@ -2332,12 +2312,42 @@ function writePreRegisteredAgentAbort(params: { } } -function resolveAuthorizedPreRegisteredAgentRunsForSessionKeys(params: { +function writePreRegisteredChatAbort(params: { + context: GatewayRequestContext; + runId: string; + stopReason: string; + endedAt?: number; + attemptId?: string; +}) { + const endedAt = params.endedAt ?? Date.now(); + const payload = buildAbortedChatSendPayload({ + runId: params.runId, + stopReason: params.stopReason, + endedAt, + }); + params.context.chatAbortedRuns.set(params.runId, createChatAbortMarker(endedAt)); + const pendingKey = pendingChatSendDedupeKey(params.runId); + const pendingAttemptId = normalizeUnknownText( + (params.context.dedupe.get(pendingKey)?.payload as PreRegisteredAgentDedupePayload | undefined) + ?.attemptId, + ); + if (!params.attemptId || pendingAttemptId === params.attemptId) { + params.context.dedupe.delete(pendingKey); + } + setGatewayDedupeEntry({ + dedupe: params.context.dedupe, + key: `chat:${params.runId}`, + entry: { ts: endedAt, ok: true, payload }, + }); +} + +function resolveAuthorizedPreRegisteredRunsForSessionKeys(params: { context: GatewayRequestContext; sessionKeys: Iterable; agentId?: string; defaultAgentId: string; requester: ChatAbortRequester; + keyPrefix: string; }) { const sessionKeys = new Set( Array.from(params.sessionKeys, (sessionKey) => normalizeOptionalText(sessionKey)).filter( @@ -2347,8 +2357,17 @@ function resolveAuthorizedPreRegisteredAgentRunsForSessionKeys(params: { const authorizedByRunId = new Map(); let matchedSessionRuns = 0; for (const [key, entry] of params.context.dedupe) { - const run = readPreRegisteredAgentRun({ key, entry }); - if (!run || !sessionKeys.has(run.sessionKey)) { + const run = readPreRegisteredRun({ key, entry, keyPrefix: params.keyPrefix }); + if (!run) { + continue; + } + const runSessionKeys = [ + run.sessionKey, + ...(Array.isArray(run.payload.sessionKeyAliases) + ? run.payload.sessionKeyAliases.map(normalizeUnknownText) + : []), + ]; + if (!runSessionKeys.some((sessionKey) => Boolean(sessionKey && sessionKeys.has(sessionKey)))) { continue; } if (params.context.chatAbortControllers.has(run.runId)) { @@ -2366,7 +2385,7 @@ function resolveAuthorizedPreRegisteredAgentRunsForSessionKeys(params: { continue; } matchedSessionRuns += 1; - if (canRequesterAbortPreRegisteredAgentRun(run.payload, params.requester)) { + if (canRequesterAbortPreRegisteredRun(run.payload, params.requester)) { authorizedByRunId.set(run.runId, run); } } @@ -2447,18 +2466,33 @@ async function abortChatRunsForSessionKeyWithPartials(params: { const { matchedSessionRuns: matchedPendingAgentRuns, authorizedRuns: authorizedPendingAgentRuns, - } = resolveAuthorizedPreRegisteredAgentRunsForSessionKeys({ + } = resolveAuthorizedPreRegisteredRunsForSessionKeys({ context: params.context, sessionKeys, agentId: params.agentId, defaultAgentId: params.defaultAgentId, requester: params.requester, + keyPrefix: "agent:", }); - if (authorizedRuns.length === 0 && authorizedPendingAgentRuns.length === 0) { + const { matchedSessionRuns: matchedPendingChatRuns, authorizedRuns: authorizedPendingChatRuns } = + resolveAuthorizedPreRegisteredRunsForSessionKeys({ + context: params.context, + sessionKeys, + agentId: params.agentId, + defaultAgentId: params.defaultAgentId, + requester: params.requester, + keyPrefix: PENDING_CHAT_SEND_DEDUPE_PREFIX, + }); + if ( + authorizedRuns.length === 0 && + authorizedPendingAgentRuns.length === 0 && + authorizedPendingChatRuns.length === 0 + ) { return { aborted: false, runIds: [], - unauthorized: matchedSessionRuns > 0 || matchedPendingAgentRuns > 0, + unauthorized: + matchedSessionRuns > 0 || matchedPendingAgentRuns > 0 || matchedPendingChatRuns > 0, }; } const authorizedRunIdSet = new Set(authorizedRuns.map((run) => run.runId)); @@ -2492,6 +2526,16 @@ async function abortChatRunsForSessionKeyWithPartials(params: { }); runIds.push(runId); } + for (const { runId, payload } of authorizedPendingChatRuns) { + writePreRegisteredChatAbort({ + context: params.context, + runId, + stopReason, + endedAt, + attemptId: normalizeUnknownText(payload.attemptId), + }); + runIds.push(runId); + } const res = { aborted: runIds.length > 0, runIds, unauthorized: false }; if (res.aborted) { const abortedRunIds = new Set(runIds); @@ -3283,10 +3327,11 @@ export const chatHandlers: GatewayRequestHandlers = { const active = context.chatAbortControllers.get(runId); if (!active) { - const pendingAgentEntry = context.dedupe.get(`agent:${runId}`); - const pendingAgentMatch = (() => { + const readPendingRunForAbort = ( + entry: GatewayRequestContext["dedupe"] extends Map ? T | undefined : never, + ) => { const canonicalMatch = readPreRegisteredAgentDedupePayloadForSession({ - entry: pendingAgentEntry, + entry, runId, sessionKey: canonicalAbortSessionKey, agentId: abortAgentId, @@ -3300,7 +3345,7 @@ export const chatHandlers: GatewayRequestHandlers = { return undefined; } const aliasMatch = readPreRegisteredAgentDedupePayloadForSession({ - entry: pendingAgentEntry, + entry, runId, sessionKey: rawSessionKey, agentId: abortAgentId, @@ -3308,10 +3353,29 @@ export const chatHandlers: GatewayRequestHandlers = { includeHidden: true, }); return aliasMatch ? { sessionKey: rawSessionKey, payload: aliasMatch } : undefined; - })(); + }; + const pendingChatMatch = readPendingRunForAbort( + context.dedupe.get(pendingChatSendDedupeKey(runId)), + ); + if (pendingChatMatch) { + if (!canRequesterAbortPreRegisteredRun(pendingChatMatch.payload, requester)) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized")); + return; + } + writePreRegisteredChatAbort({ + context, + runId, + stopReason: "rpc", + attemptId: normalizeUnknownText(pendingChatMatch.payload.attemptId), + }); + respond(true, { ok: true, aborted: true, runIds: [runId] }); + return; + } + const pendingAgentEntry = context.dedupe.get(`agent:${runId}`); + const pendingAgentMatch = readPendingRunForAbort(pendingAgentEntry); if (pendingAgentMatch) { const pendingAgentPayload = pendingAgentMatch.payload; - if (!canRequesterAbortPreRegisteredAgentRun(pendingAgentPayload, requester)) { + if (!canRequesterAbortPreRegisteredRun(pendingAgentPayload, requester)) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized")); return; } @@ -3471,10 +3535,6 @@ export const chatHandlers: GatewayRequestHandlers = { const inboundMessage = sanitizedMessageResult.message; const systemInputProvenance = normalizeInputProvenance(p.systemInputProvenance); const systemProvenanceReceipt = systemReceiptResult.receipt; - const systemDedupeScope = - systemInputProvenance || systemProvenanceReceipt - ? JSON.stringify([systemProvenanceReceipt ?? null, systemInputProvenance ?? null]) - : undefined; const stopCommand = !suppressCommandInterpretation && isChatStopCommandText(inboundMessage); const normalizedAttachments = normalizeRpcAttachmentsToChatAttachments(p.attachments); const rawMessage = inboundMessage.trim(); @@ -3489,6 +3549,7 @@ export const chatHandlers: GatewayRequestHandlers = { const rawSessionKey = p.sessionKey; const agentIdOverride = normalizeOptionalText(p.agentId); const clientRunId = p.idempotencyKey; + const pendingChatSendKey = pendingChatSendDedupeKey(clientRunId); const requestedAgentId = resolveRequestedChatAgentId({ cfg: (context as { getRuntimeConfig?: () => OpenClawConfig }).getRuntimeConfig?.(), requestedSessionKey: rawSessionKey, @@ -3509,7 +3570,7 @@ export const chatHandlers: GatewayRequestHandlers = { }, ); const sessionLoadMs = roundedChatSendTimingMs(performance.now() - sessionLoadStartedAtMs); - const { cfg, entry, canonicalKey: sessionKey, legacyKey } = sessionLoadResult; + const { cfg, storePath, entry, canonicalKey: sessionKey, legacyKey } = sessionLoadResult; const selectedAgent = validateChatSelectedAgent({ cfg, requestedSessionKey: rawSessionKey, @@ -3634,6 +3695,19 @@ export const chatHandlers: GatewayRequestHandlers = { return; } + const pendingChatSend = readPreRegisteredRun({ + key: pendingChatSendKey, + entry: context.dedupe.get(pendingChatSendKey), + keyPrefix: PENDING_CHAT_SEND_DEDUPE_PREFIX, + }); + if (pendingChatSend) { + respond(true, { runId: clientRunId, status: "in_flight" as const }, undefined, { + cached: true, + runId: clientRunId, + }); + return; + } + const activeExisting = context.chatAbortControllers.get(clientRunId); if (activeExisting) { respond(true, { runId: clientRunId, status: "in_flight" as const }, undefined, { @@ -3642,6 +3716,11 @@ export const chatHandlers: GatewayRequestHandlers = { }); return; } + const archivedSessionError = resolveSessionWorkStartError(sessionKey, entry); + if (archivedSessionError) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, archivedSessionError)); + return; + } const chatSendTraceAttributes = { runId: clientRunId, sessionKey, @@ -3661,62 +3740,216 @@ export const chatHandlers: GatewayRequestHandlers = { mainKey: cfg.session?.mainKey, sessionKey, }); - const activeChatSendDedupeKey = buildActiveChatSendDedupeKey({ - attachmentCount: normalizedAttachments.length, - explicitDeliverRoute: originatingRoute.explicitDeliverRoute, - message: rawMessage, - originatingChannel: originatingRoute.originatingChannel, - sessionKey: activeRunScopeKey, - systemScope: systemDedupeScope, + const lifecycleGeneration = getAgentEventLifecycleGeneration(); + const pendingAttemptId = randomUUID(); + const pendingExpiresAtMs = resolveChatRunExpiresAtMs({ now, timeoutMs }); + // Keep the run abortable while lifecycle mutation owns the session. Admission + // must reject an expired/missing reservation instead of reviving evicted work. + context.dedupe.set(pendingChatSendKey, { + ts: now, + ok: true, + payload: { + runId: clientRunId, + attemptId: pendingAttemptId, + status: "accepted" as const, + sessionKey, + ...(rawSessionKey === sessionKey ? {} : { sessionKeyAliases: [rawSessionKey] }), + ...(sessionKey === "global" && selectedAgent.agentId + ? { agentId: selectedAgent.agentId } + : {}), + ownerConnId: normalizeOptionalText(client?.connId), + ownerDeviceId: normalizeOptionalText(client?.connect?.device?.id), + expiresAtMs: pendingExpiresAtMs, + }, }); - if (activeChatSendDedupeKey) { - const activeRunId = resolveActiveChatSendRunId( - context.dedupe.get(activeChatSendDedupeKey)?.payload, - ); - if (activeRunId && context.chatAbortControllers.has(activeRunId)) { - respond(true, { runId: activeRunId, status: "in_flight" as const }, undefined, { + const clearPendingChatSendReservation = () => { + const pending = readPreRegisteredRun({ + key: pendingChatSendKey, + entry: context.dedupe.get(pendingChatSendKey), + keyPrefix: PENDING_CHAT_SEND_DEDUPE_PREFIX, + }); + if ( + pending?.runId === clientRunId && + normalizeUnknownText(pending.payload.attemptId) === pendingAttemptId + ) { + context.dedupe.delete(pendingChatSendKey); + } + }; + let admittedSessionId = backingSessionId ?? clientRunId; + let gatewayWorkAdmission: Awaited> | undefined; + let admittedRunAbort: ReturnType | undefined; + let reservationSuperseded = false; + let supersedingResult: DedupeEntry | undefined; + try { + gatewayWorkAdmission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, backingSessionId], + assertAllowed: () => { + if (context.chatAbortedRuns.has(clientRunId)) { + return; + } + const pendingReservation = readPreRegisteredRun({ + key: pendingChatSendKey, + entry: context.dedupe.get(pendingChatSendKey), + keyPrefix: PENDING_CHAT_SEND_DEDUPE_PREFIX, + }); + if ( + pendingReservation && + normalizeUnknownText(pendingReservation.payload.attemptId) !== pendingAttemptId + ) { + reservationSuperseded = true; + return; + } + if (!pendingReservation) { + const terminalResult = context.dedupe.get(`chat:${clientRunId}`); + if (terminalResult || context.chatAbortControllers.has(clientRunId)) { + reservationSuperseded = true; + supersedingResult = terminalResult; + return; + } + } + if (lifecycleGeneration !== getAgentEventLifecycleGeneration()) { + writePreRegisteredChatAbort({ + context, + runId: clientRunId, + stopReason: "restart", + attemptId: pendingAttemptId, + }); + return; + } + if ( + !pendingReservation || + !isFutureDateTimestampMs(pendingReservation.payload.expiresAtMs, { + nowMs: Date.now(), + }) + ) { + writePreRegisteredChatAbort({ + context, + runId: clientRunId, + stopReason: "timeout", + attemptId: pendingAttemptId, + }); + return; + } + const latestEntry = loadSessionEntry(rawSessionKey, sessionLoadOptions).entry; + if (entry && !latestEntry) { + throw new Error(`Session "${sessionKey}" was deleted while starting work. Retry.`); + } + // Admission can queue behind reset. Never route a request captured + // against the old session into the replacement transcript. + if ( + backingSessionId && + latestEntry?.sessionId && + latestEntry.sessionId !== backingSessionId + ) { + throw new Error(`Session "${sessionKey}" changed while starting work. Retry.`); + } + const archivedError = resolveSessionWorkStartError(sessionKey, latestEntry); + if (archivedError) { + throw new Error(archivedError); + } + admittedSessionId = latestEntry?.sessionId ?? backingSessionId ?? clientRunId; + admittedRunAbort = registerChatAbortController({ + chatAbortControllers: context.chatAbortControllers, + runId: clientRunId, + sessionId: admittedSessionId, + sessionKey, + agentId: selectedAgent.agentId, + timeoutMs, + now, + ownerConnId: normalizeOptionalText(client?.connId), + ownerDeviceId: normalizeOptionalText(client?.connect?.device?.id), + providerId: resolvedSessionModel.provider, + authProviderId: resolvedSessionAuthProvider, + isAbortable: (active) => isReplyRunAbortableForSignal(active.controller.signal), + kind: "chat-send", + lifecycleGeneration, + }); + }, + onInterrupt: () => { + if (admittedRunAbort?.entry) { + admittedRunAbort.entry.abortStopReason = "restart"; + } + admittedRunAbort?.controller.abort(createAgentRunRestartAbortError()); + }, + }); + } catch (err) { + clearPendingChatSendReservation(); + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, formatForLog(err))); + return; + } + clearPendingChatSendReservation(); + const activeRunAbort = admittedRunAbort; + if (reservationSuperseded) { + gatewayWorkAdmission.release(); + const supersedingCached = supersedingResult ?? context.dedupe.get(`chat:${clientRunId}`); + if (supersedingCached) { + respond(supersedingCached.ok, supersedingCached.payload, supersedingCached.error, { cached: true, - runId: activeRunId, + runId: clientRunId, }); return; } - } - const lifecycleGeneration = getAgentEventLifecycleGeneration(); - const activeRunAbort = registerChatAbortController({ - chatAbortControllers: context.chatAbortControllers, - runId: clientRunId, - sessionId: backingSessionId ?? clientRunId, - sessionKey, - agentId: selectedAgent.agentId, - timeoutMs, - now, - ownerConnId: normalizeOptionalText(client?.connId), - ownerDeviceId: normalizeOptionalText(client?.connect?.device?.id), - providerId: resolvedSessionModel.provider, - authProviderId: resolvedSessionAuthProvider, - isAbortable: (active) => isReplyRunAbortableForSignal(active.controller.signal), - kind: "chat-send", - lifecycleGeneration, - }); - if (!activeRunAbort.registered) { respond(true, { runId: clientRunId, status: "in_flight" as const }, undefined, { cached: true, runId: clientRunId, }); return; } + if (lifecycleGeneration !== getAgentEventLifecycleGeneration()) { + if (activeRunAbort) { + if (activeRunAbort.entry) { + activeRunAbort.entry.abortStopReason = "restart"; + } + activeRunAbort.controller.abort(); + activeRunAbort.cleanup({ force: true }); + } + gatewayWorkAdmission.release(); + if (!context.dedupe.has(`chat:${clientRunId}`)) { + writePreRegisteredChatAbort({ + context, + runId: clientRunId, + stopReason: activeRunAbort?.entry?.abortStopReason ?? "restart", + attemptId: pendingAttemptId, + }); + } + const aborted = context.dedupe.get(`chat:${clientRunId}`); + respond(aborted?.ok ?? true, aborted?.payload, aborted?.error, { + cached: true, + runId: clientRunId, + }); + return; + } + if (!activeRunAbort) { + gatewayWorkAdmission.release(); + const aborted = context.dedupe.get(`chat:${clientRunId}`); + if (aborted) { + respond(aborted.ok, aborted.payload, aborted.error, { + cached: true, + runId: clientRunId, + }); + return; + } + respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "chat run admission failed")); + return; + } + if (!activeRunAbort.registered) { + gatewayWorkAdmission.release(); + respond(true, { runId: clientRunId, status: "in_flight" as const }, undefined, { + cached: true, + runId: clientRunId, + }); + return; + } + const cleanupAdmittedRun: typeof activeRunAbort.cleanup = (options) => { + activeRunAbort.cleanup(options); + gatewayWorkAdmission?.release(); + }; claimAgentRunContext(clientRunId, { sessionKey, - sessionId: backingSessionId ?? clientRunId, + sessionId: admittedSessionId, lifecycleGeneration, }); - if (activeChatSendDedupeKey) { - context.dedupe.set(activeChatSendDedupeKey, { - ts: now, - ok: true, - payload: { runId: clientRunId }, - }); - } const explicitOriginTargetsPlugin = explicitOriginTargetsPluginBinding( explicitOriginResult.value, ); @@ -3788,9 +4021,8 @@ export const chatHandlers: GatewayRequestHandlers = { performance.now() - prepareAttachmentsStartedAtMs, ); } catch (err) { - activeRunAbort.cleanup({ force: true }); + cleanupAdmittedRun({ force: true }); clearAgentRunContext(clientRunId, lifecycleGeneration); - clearActiveChatSendDedupeRun(context.dedupe, activeChatSendDedupeKey, clientRunId); logAttachmentFailure(context.logGateway, "chat.send attachment parse/stage failed", err); respond( false, @@ -3811,7 +4043,6 @@ export const chatHandlers: GatewayRequestHandlers = { stopReason, endedAt, }); - clearActiveChatSendDedupeRun(context.dedupe, activeChatSendDedupeKey, clientRunId); setGatewayDedupeEntry({ dedupe: context.dedupe, key: `chat:${clientRunId}`, @@ -3821,6 +4052,8 @@ export const chatHandlers: GatewayRequestHandlers = { payload, }, }); + cleanupAdmittedRun({ force: true }); + clearAgentRunContext(clientRunId, lifecycleGeneration); respond(true, payload, undefined, { runId: clientRunId }); return; } @@ -4216,109 +4449,114 @@ export const chatHandlers: GatewayRequestHandlers = { } emitServerTiming("first-assistant-event", undefined, dispatchStartedAtMs); }; - void measureDiagnosticsTimelineSpan( - "gateway.chat_send.dispatch_inbound", - async () => { - applyChatSendManagedMediaFields(ctx, await pluginBoundMediaFieldsPromise); - const dispatchResult = await dispatchInboundMessage({ - ctx, - cfg, - dispatcher, - onSessionMetadataChanges: (changes) => { - for (const change of changes) { - emitSessionsChanged(context, change); - } - }, - replyOptions: { - runId: clientRunId, - ...(isOperatorUiClient(clientInfo) - ? { - promptCacheKey: resolveWebchatPromptCacheKey({ - agentId, - provider: resolvedSessionModel.provider, - model: resolvedSessionModel.model, - sessionKey: activeRunScopeKey, - }), + void gatewayWorkAdmission + .run(() => + measureDiagnosticsTimelineSpan( + "gateway.chat_send.dispatch_inbound", + async () => { + applyChatSendManagedMediaFields(ctx, await pluginBoundMediaFieldsPromise); + const dispatchResult = await dispatchInboundMessage({ + ctx, + cfg, + dispatcher, + onSessionMetadataChanges: (changes) => { + for (const change of changes) { + emitSessionsChanged(context, change); } - : {}), - requestedSessionId, - resumeRequestedSession: controlUiReconnectResume.resumeRequested, - abortSignal: activeRunAbort.controller.signal, - images: replyOptionImages, - imageOrder: imageOrder.length > 0 ? imageOrder : undefined, - thinkingLevelOverride: p.thinking, - fastModeOverride: p.fastMode, - userTurnTranscriptRecorder: userTurnRecorder, - fastModeAutoOnSecondsOverride: p.fastAutoOnSeconds, - onAgentRunStart: (runId) => { - agentRunStarted = true; - emitServerTiming( - "agent-run-started", - runId !== clientRunId ? { agentRunId: runId } : undefined, - dispatchStartedAtMs, - ); - const connId = typeof client?.connId === "string" ? client.connId : undefined; - const wantsToolEvents = hasGatewayClientCap( - client?.connect?.caps, - GATEWAY_CLIENT_CAPS.TOOL_EVENTS, - ); - if (connId && wantsToolEvents) { - context.registerToolEventRecipient(runId, connId); - // Register for any other active runs *in the same session* so - // late-joining clients (e.g. page refresh mid-response) receive - // in-progress tool events without leaking cross-session data. - const defaultAgentId = resolveDefaultAgentId(cfg); - const selectedGlobalAgentId = - sessionKey === "global" ? (selectedAgent.agentId ?? defaultAgentId) : undefined; - for (const [activeRunId, active] of context.chatAbortControllers) { - const activeGlobalAgentId = - active.sessionKey === "global" - ? (active.agentId ?? defaultAgentId) - : undefined; - const sameSelectedGlobalAgent = - sessionKey === "global" && - selectedGlobalAgentId !== undefined && - activeGlobalAgentId === selectedGlobalAgentId; - const sameSession = - active.sessionKey === sessionKey && - (sessionKey !== "global" || sameSelectedGlobalAgent); - if (activeRunId !== runId && sameSession) { - context.registerToolEventRecipient(activeRunId, connId); - } - } - } - }, - onModelSelected: (modelSelection) => { - updateChatRunProvider(context.chatAbortControllers, { + }, + replyOptions: { runId: clientRunId, - providerId: modelSelection.provider, - authProviderId: resolveProviderIdForAuth(modelSelection.provider, { - config: cfg, - }), - }); - onModelSelected(modelSelection); - emitServerTiming( - "model-selected", - { - provider: modelSelection.provider, - model: modelSelection.model, + ...(isOperatorUiClient(clientInfo) + ? { + promptCacheKey: resolveWebchatPromptCacheKey({ + agentId, + provider: resolvedSessionModel.provider, + model: resolvedSessionModel.model, + sessionKey: activeRunScopeKey, + }), + } + : {}), + requestedSessionId, + resumeRequestedSession: controlUiReconnectResume.resumeRequested, + abortSignal: activeRunAbort.controller.signal, + images: replyOptionImages, + imageOrder: imageOrder.length > 0 ? imageOrder : undefined, + thinkingLevelOverride: p.thinking, + fastModeOverride: p.fastMode, + userTurnTranscriptRecorder: userTurnRecorder, + fastModeAutoOnSecondsOverride: p.fastAutoOnSeconds, + onAgentRunStart: (runId) => { + agentRunStarted = true; + emitServerTiming( + "agent-run-started", + runId !== clientRunId ? { agentRunId: runId } : undefined, + dispatchStartedAtMs, + ); + const connId = typeof client?.connId === "string" ? client.connId : undefined; + const wantsToolEvents = hasGatewayClientCap( + client?.connect?.caps, + GATEWAY_CLIENT_CAPS.TOOL_EVENTS, + ); + if (connId && wantsToolEvents) { + context.registerToolEventRecipient(runId, connId); + // Register for any other active runs *in the same session* so + // late-joining clients (e.g. page refresh mid-response) receive + // in-progress tool events without leaking cross-session data. + const defaultAgentId = resolveDefaultAgentId(cfg); + const selectedGlobalAgentId = + sessionKey === "global" + ? (selectedAgent.agentId ?? defaultAgentId) + : undefined; + for (const [activeRunId, active] of context.chatAbortControllers) { + const activeGlobalAgentId = + active.sessionKey === "global" + ? (active.agentId ?? defaultAgentId) + : undefined; + const sameSelectedGlobalAgent = + sessionKey === "global" && + selectedGlobalAgentId !== undefined && + activeGlobalAgentId === selectedGlobalAgentId; + const sameSession = + active.sessionKey === sessionKey && + (sessionKey !== "global" || sameSelectedGlobalAgent); + if (activeRunId !== runId && sameSession) { + context.registerToolEventRecipient(activeRunId, connId); + } + } + } }, - dispatchStartedAtMs, - ); - }, + onModelSelected: (modelSelection) => { + updateChatRunProvider(context.chatAbortControllers, { + runId: clientRunId, + providerId: modelSelection.provider, + authProviderId: resolveProviderIdForAuth(modelSelection.provider, { + config: cfg, + }), + }); + onModelSelected(modelSelection); + emitServerTiming( + "model-selected", + { + provider: modelSelection.provider, + model: modelSelection.model, + }, + dispatchStartedAtMs, + ); + }, + }, + }); + if (dispatchResult.beforeAgentRunBlocked === true) { + userTurnRecorder.markBlocked(); + } + return dispatchResult; }, - }); - if (dispatchResult.beforeAgentRunBlocked === true) { - userTurnRecorder.markBlocked(); - } - return dispatchResult; - }, - { - phase: "agent-turn", - config: cfg, - attributes: chatSendTraceAttributes, - }, - ) + { + phase: "agent-turn", + config: cfg, + attributes: chatSendTraceAttributes, + }, + ), + ) .then(async () => { emitServerTiming("dispatch-completed", undefined, dispatchStartedAtMs); const postDispatchStartedAtMs = performance.now(); @@ -5272,9 +5510,8 @@ export const chatHandlers: GatewayRequestHandlers = { }); }) .finally(() => { - activeRunAbort.cleanup(); + cleanupAdmittedRun(); clearAgentRunContext(clientRunId, lifecycleGeneration); - clearActiveChatSendDedupeRun(context.dedupe, activeChatSendDedupeKey, clientRunId); context.removeChatRun(clientRunId, clientRunId, sessionKey); if (!pendingDispatchLifecycleError) { return; @@ -5325,9 +5562,8 @@ export const chatHandlers: GatewayRequestHandlers = { void persistDispatchLifecycleError(); }); } catch (err) { - activeRunAbort.cleanup({ force: true }); + cleanupAdmittedRun({ force: true }); clearAgentRunContext(clientRunId, lifecycleGeneration); - clearActiveChatSendDedupeRun(context.dedupe, activeChatSendDedupeKey, clientRunId); context.removeChatRun(clientRunId, clientRunId, sessionKey); const error = errorShape(ErrorCodes.UNAVAILABLE, String(err)); const payload = { @@ -5411,17 +5647,47 @@ export const chatHandlers: GatewayRequestHandlers = { agentId: selectedAgent.agentId, }); - const appended = await appendAssistantTranscriptMessage({ - sessionKey, - message: p.message, - label: p.label, - sessionId, - storePath, - sessionFile: entry?.sessionFile, - agentId, - createIfMissing: true, - cfg, - }); + let appended: Awaited>; + try { + const admission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, sessionId], + assertAllowed: () => { + const latestEntry = loadSessionEntry(rawSessionKey, sessionLoadOptions).entry; + if (!latestEntry) { + throw new Error(`Session "${sessionKey}" was deleted while starting work. Retry.`); + } + if (latestEntry.sessionId !== sessionId) { + throw new Error(`Session "${sessionKey}" changed while starting work. Retry.`); + } + const archivedError = resolveSessionWorkStartError(sessionKey, latestEntry); + if (archivedError) { + throw new Error(archivedError); + } + }, + }); + try { + appended = await admission.run( + async () => + await appendAssistantTranscriptMessage({ + sessionKey, + message: p.message, + label: p.label, + sessionId, + storePath, + sessionFile: entry.sessionFile, + agentId, + createIfMissing: true, + cfg, + }), + ); + } finally { + admission.release(); + } + } catch (err) { + respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, formatForLog(err))); + return; + } if (!appended.ok || !appended.messageId || !appended.message) { respond( false, diff --git a/src/gateway/server-methods/session-active-runs.test.ts b/src/gateway/server-methods/session-active-runs.test.ts new file mode 100644 index 000000000000..ee381bdd5663 --- /dev/null +++ b/src/gateway/server-methods/session-active-runs.test.ts @@ -0,0 +1,27 @@ +// Tests gateway active-run matching by logical session key and backing id. +import { expect, it } from "vitest"; +import { hasVisibleActiveSessionRun } from "./session-active-runs.js"; + +it("matches session-id-only gateway runs during archive admission", () => { + const context = { + chatAbortControllers: new Map([ + [ + "run-1", + { + sessionId: "session-1", + controlUiVisible: true, + projectSessionActive: true, + }, + ], + ]), + } as never; + + expect( + hasVisibleActiveSessionRun({ + context, + requestedKey: "agent:main:child", + canonicalKey: "agent:main:child", + sessionId: "session-1", + }), + ).toBe(true); +}); diff --git a/src/gateway/server-methods/session-active-runs.ts b/src/gateway/server-methods/session-active-runs.ts index 9c6646569655..a37fad464139 100644 --- a/src/gateway/server-methods/session-active-runs.ts +++ b/src/gateway/server-methods/session-active-runs.ts @@ -11,7 +11,8 @@ import type { GatewayRequestContext } from "./types.js"; * do not make a session look busy to user-facing session operations. */ type TrackedActiveSessionRun = { - sessionKey: string; + sessionKey?: string; + sessionId?: string; agentId?: string; }; @@ -23,14 +24,15 @@ function collectTrackedActiveSessionRuns( return runs; } for (const active of context.chatAbortControllers.values()) { - if ( - active.projectSessionActive !== false && - active.controlUiVisible !== false && - typeof active.sessionKey === "string" && - active.sessionKey.trim() - ) { + if (active.projectSessionActive !== false && active.controlUiVisible !== false) { + const sessionKey = active.sessionKey?.trim(); + const sessionId = active.sessionId?.trim(); + if (!sessionKey && !sessionId) { + continue; + } runs.push({ - sessionKey: active.sessionKey, + ...(sessionKey ? { sessionKey } : {}), + ...(sessionId ? { sessionId } : {}), agentId: typeof active.agentId === "string" ? normalizeAgentId(active.agentId) : undefined, }); } @@ -44,7 +46,7 @@ function isTrackedActiveSessionRunForKey( agentId?: string, defaultAgentId?: string, ): boolean { - if (active.sessionKey !== key) { + if (!active.sessionKey || active.sessionKey !== key) { return false; } if (key !== "global") { @@ -98,5 +100,13 @@ export function hasVisibleActiveSessionRun(params: { return true; } const sessionId = params.sessionId?.trim(); - return sessionId ? isEmbeddedAgentRunActive(sessionId) : false; + if (!sessionId) { + return false; + } + if ( + collectTrackedActiveSessionRuns(params.context).some((active) => active.sessionId === sessionId) + ) { + return true; + } + return isEmbeddedAgentRunActive(sessionId); } diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 4e6190340a3a..12e783c93329 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -46,11 +46,14 @@ import { } from "../../agents/embedded-agent-runner/runs.js"; import { compactEmbeddedAgentSession } from "../../agents/embedded-agent.js"; import { clearSessionQueues } from "../../auto-reply/reply/queue/cleanup.js"; +import { replyRunRegistry } from "../../auto-reply/reply/reply-run-registry.js"; import { normalizeReasoningLevel, normalizeThinkLevel } from "../../auto-reply/thinking.js"; import { runSessionsCleanup, serializeSessionCleanupResult, resolveMainSessionKey, + resolveSessionWorkStartError, + SESSION_LIFECYCLE_CHANGED_ERROR_REASON, listConfiguredSessionStoreAgentIds, deleteSessionEntryLifecycle, type SessionEntry, @@ -82,6 +85,13 @@ import { resolveAgentIdFromSessionKey, toAgentStoreSessionKey, } from "../../routing/session-key.js"; +import { + interruptSessionWorkAdmissions, + isSessionLifecycleMutationActive, + isSessionWorkAdmissionActive, + runExclusiveSessionLifecycleMutation, + SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS, +} from "../../sessions/session-lifecycle-admission.js"; import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { ADMIN_SCOPE } from "../operator-scopes.js"; import { resolveSessionKeyForRun } from "../server-session-key.js"; @@ -707,6 +717,41 @@ async function handleSessionSend(params: { ); return; } + const rawIdempotencyKey = (p as { idempotencyKey?: string }).idempotencyKey; + const explicitIdempotencyKey = + typeof rawIdempotencyKey === "string" && rawIdempotencyKey.trim() + ? rawIdempotencyKey.trim() + : undefined; + const idempotencyKey = explicitIdempotencyKey ?? randomUUID(); + const dispatchChatSend = async (respond: RespondFn) => { + await chatHandlers["chat.send"]({ + req: params.req, + params: { + sessionKey: canonicalKey, + ...(canonicalKey === "global" && requestedAgentId ? { agentId: requestedAgentId } : {}), + message: (p as { message: string }).message, + thinking: (p as { thinking?: string }).thinking, + attachments: (p as { attachments?: unknown[] }).attachments, + timeoutMs: (p as { timeoutMs?: number }).timeoutMs, + idempotencyKey, + }, + respond, + context: params.context, + client: params.client, + isWebchatConnect: params.isWebchatConnect, + }); + }; + const archivedSessionError = resolveSessionWorkStartError(canonicalKey, entry); + if (archivedSessionError) { + // An explicit retry may already have a terminal chat.send result. Let the + // owning handler replay that result before it applies the archive guard. + if (explicitIdempotencyKey) { + await dispatchChatSend(params.respond); + return; + } + params.respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, archivedSessionError)); + return; + } if (!entry?.sessionId && !params.interruptIfActive && isAgentMainSessionKey(cfg, canonicalKey)) { // Sending to an empty agent main session should create it; steering still requires an active row. const created = await createAgentMainSessionForSend({ @@ -764,60 +809,40 @@ async function handleSessionSend(params: { let sendPayload: unknown; let sendCached = false; let startedRunId: string | undefined; - const rawIdempotencyKey = (p as { idempotencyKey?: string }).idempotencyKey; - const idempotencyKey = - typeof rawIdempotencyKey === "string" && rawIdempotencyKey.trim() - ? rawIdempotencyKey.trim() - : randomUUID(); - await chatHandlers["chat.send"]({ - req: params.req, - params: { - sessionKey: canonicalKey, - ...(canonicalKey === "global" && requestedAgentId ? { agentId: requestedAgentId } : {}), - message: (p as { message: string }).message, - thinking: (p as { thinking?: string }).thinking, - attachments: (p as { attachments?: unknown[] }).attachments, - timeoutMs: (p as { timeoutMs?: number }).timeoutMs, - idempotencyKey, - }, - respond: (ok, payload, error, meta) => { - sendAcked = ok; - sendPayload = payload; - sendCached = meta?.cached === true; - startedRunId = - payload && - typeof payload === "object" && - typeof (payload as { runId?: unknown }).runId === "string" - ? (payload as { runId: string }).runId - : undefined; - if (ok && shouldAttachPendingMessageSeq({ payload, cached: meta?.cached === true })) { - params.respond( - true, - { - ...(payload && typeof payload === "object" ? payload : {}), - messageSeq, - ...(interruptedActiveRun ? { interruptedActiveRun: true } : {}), - }, - undefined, - meta, - ); - return; - } + await dispatchChatSend((ok, payload, error, meta) => { + sendAcked = ok; + sendPayload = payload; + sendCached = meta?.cached === true; + startedRunId = + payload && + typeof payload === "object" && + typeof (payload as { runId?: unknown }).runId === "string" + ? (payload as { runId: string }).runId + : undefined; + if (ok && shouldAttachPendingMessageSeq({ payload, cached: meta?.cached === true })) { params.respond( - ok, - ok && payload && typeof payload === "object" - ? { - ...payload, - ...(interruptedActiveRun ? { interruptedActiveRun: true } : {}), - } - : payload, - error, + true, + { + ...(payload && typeof payload === "object" ? payload : {}), + messageSeq, + ...(interruptedActiveRun ? { interruptedActiveRun: true } : {}), + }, + undefined, meta, ); - }, - context: params.context, - client: params.client, - isWebchatConnect: params.isWebchatConnect, + return; + } + params.respond( + ok, + ok && payload && typeof payload === "object" + ? { + ...payload, + ...(interruptedActiveRun ? { interruptedActiveRun: true } : {}), + } + : payload, + error, + meta, + ); }); if (sendAcked) { if (shouldAttachPendingMessageSeq({ payload: sendPayload, cached: sendCached })) { @@ -1728,72 +1753,148 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } - const interruptResult = await interruptSessionRunIfActive({ - req, - context, - client, - isWebchatConnect, - requestedKey: key, + const lifecycleIdentities = [ + key, canonicalKey, - agentId: requestedAgent.agentId, - sessionId: entry.sessionId, - }); - if (interruptResult.error) { - respond(false, undefined, interruptResult.error); - return; - } - - const restoredSession = await compactionCheckpointStore.restoreCheckpointSession({ - storePath, - sessionKey: canonicalKey, - sessionStoreKey: legacyKey, - checkpointId, - }); - if ( - restoredSession.status === "missing-checkpoint" || - restoredSession.status === "missing-boundary" - ) { - respond( - false, - undefined, - errorShape(ErrorCodes.INVALID_REQUEST, `checkpoint not found: ${checkpointId}`), - ); - return; - } - if (restoredSession.status === "missing-session") { - respond( - false, - undefined, - errorShape(ErrorCodes.INVALID_REQUEST, `session not found: ${key}`), - ); - return; - } - if (restoredSession.status === "failed") { - respond( - false, - undefined, - errorShape(ErrorCodes.UNAVAILABLE, "failed to restore checkpoint transcript"), - ); - return; - } - - respond( - true, - { - ok: true, - key: restoredSession.key, - sessionId: restoredSession.entry.sessionId, - checkpoint: restoredSession.checkpoint, - entry: restoredSession.entry, + legacyKey, + entry.sessionId, + entry.lifecycleRevision, + ]; + let admittedWorkReleased = true; + let restoreTargetStillCurrent = true; + // Restore replaces the active transcript identity. Hold the same lifecycle fence as + // compaction so neither operation can publish state from the other's obsolete session. + await runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: lifecycleIdentities, + prepare: async () => { + const current = loadSessionEntry(key, { agentId: requestedAgent.agentId }); + restoreTargetStillCurrent = Boolean( + current.entry?.sessionId === entry.sessionId && + current.entry.lifecycleRevision === entry.lifecycleRevision && + getSessionCompactionCheckpoint({ entry: current.entry, checkpointId }), + ); + if (!restoreTargetStillCurrent) { + return; + } + clearSessionQueues([ + key, + current.canonicalKey, + current.legacyKey, + current.entry?.sessionId, + ]); + admittedWorkReleased = await interruptSessionWorkAdmissions({ + scope: storePath, + identities: lifecycleIdentities, + timeoutMs: SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS, + }); + }, + run: async () => { + if (!restoreTargetStillCurrent) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `Session ${key} changed before checkpoint restore. Retry.`, + { details: { reason: SESSION_LIFECYCLE_CHANGED_ERROR_REASON } }, + ), + ); + return; + } + if (!admittedWorkReleased) { + respond( + false, + undefined, + errorShape(ErrorCodes.UNAVAILABLE, `Session ${key} is still active; try again.`), + ); + return; + } + const current = loadSessionEntry(key, { agentId: requestedAgent.agentId }); + if (!current.entry?.sessionId) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, `session not found: ${key}`), + ); + return; + } + if (!getSessionCompactionCheckpoint({ entry: current.entry, checkpointId })) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, `checkpoint not found: ${checkpointId}`), + ); + return; + } + const interruptResult = await interruptSessionRunIfActive({ + req, + context, + client, + isWebchatConnect, + requestedKey: key, + canonicalKey: current.canonicalKey, + agentId: requestedAgent.agentId, + sessionId: current.entry.sessionId, + }); + if (interruptResult.error) { + respond(false, undefined, interruptResult.error); + return; + } + + const restoredSession = await compactionCheckpointStore.restoreCheckpointSession({ + storePath, + sessionKey: current.canonicalKey, + sessionStoreKey: current.legacyKey, + checkpointId, + }); + if ( + restoredSession.status === "missing-checkpoint" || + restoredSession.status === "missing-boundary" + ) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, `checkpoint not found: ${checkpointId}`), + ); + return; + } + if (restoredSession.status === "missing-session") { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, `session not found: ${key}`), + ); + return; + } + if (restoredSession.status === "failed") { + respond( + false, + undefined, + errorShape(ErrorCodes.UNAVAILABLE, "failed to restore checkpoint transcript"), + ); + return; + } + + respond( + true, + { + ok: true, + key: restoredSession.key, + sessionId: restoredSession.entry.sessionId, + checkpoint: restoredSession.checkpoint, + entry: restoredSession.entry, + }, + undefined, + ); + emitSessionsChanged(context, { + sessionKey: current.canonicalKey, + ...(current.canonicalKey === "global" && requestedAgent.agentId + ? { agentId: requestedAgent.agentId } + : {}), + reason: "checkpoint-restore", + }); }, - undefined, - ); - emitSessionsChanged(context, { - sessionKey: canonicalKey, - ...(canonicalKey === "global" && requestedAgent.agentId - ? { agentId: requestedAgent.agentId } - : {}), - reason: "checkpoint-restore", }); }, "sessions.send": async ({ req, params, respond, context, client, isWebchatConnect }) => { @@ -2013,31 +2114,103 @@ export const sessionsHandlers: GatewayRequestHandlers = { const { target, storePath } = resolveGatewaySessionTargetFromKey(key, cfg, { agentId: requestedAgentId, }); - const applied = await applySessionPatchProjection({ - storePath, - resolveTarget: ({ entries }) => { - const store = Object.fromEntries( - entries.map(({ sessionKey, entry }) => [sessionKey, entry]), + const canonicalKey = target.canonicalKey ?? key; + const lifecycleEntry = loadSessionEntry(key, { agentId: requestedAgentId }).entry; + const lifecycleIdentities = [canonicalKey, key, lifecycleEntry?.sessionId]; + if (p.archived === true && isSessionLifecycleMutationActive(storePath, lifecycleIdentities)) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, "Cannot archive a session with an active run."), + ); + return; + } + const applyPatch = async () => { + const currentLifecycleEntry = loadSessionEntry(key, { agentId: requestedAgentId }).entry; + // A reset queued ahead of archive can rotate the row before this mutation starts. + // Never apply stale destructive intent to the replacement session identity. + const lifecycleEntryRemoved = + lifecycleEntry !== undefined && currentLifecycleEntry === undefined; + const archiveTargetChanged = + p.archived === true && + (lifecycleEntry === undefined + ? currentLifecycleEntry !== undefined + : currentLifecycleEntry !== undefined && + (currentLifecycleEntry.sessionId !== lifecycleEntry.sessionId || + currentLifecycleEntry.lifecycleRevision !== lifecycleEntry.lifecycleRevision)); + if (lifecycleEntryRemoved || archiveTargetChanged) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, `Session ${key} changed before patch. Retry.`), ); - const { target: migratedTarget, primaryKey } = migrateAndPruneGatewaySessionStoreKey({ - cfg, - key, - store, - agentId: requestedAgentId, - }); - return { primaryKey, candidateKeys: migratedTarget.storeKeys }; - }, - project: async ({ primaryKey, existingEntry, entries }) => - await projectSessionsPatchEntry({ - cfg, - entries, - existingEntry, - storeKey: primaryKey, - agentId: requestedAgentId, - patch: p, - loadGatewayModelCatalog: context.loadGatewayModelCatalog, - }), + return null; + } + if (p.archived === true) { + if (canonicalKey === "global" || isAgentMainSessionKey(cfg, canonicalKey)) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, "Cannot archive an agent's main session."), + ); + return null; + } + const { entry } = loadSessionEntry(key, { agentId: requestedAgentId }); + const activeIdentities = [canonicalKey, key, entry?.sessionId]; + if ( + isSessionWorkAdmissionActive(storePath, activeIdentities) || + replyRunRegistry.isActive(canonicalKey) || + replyRunRegistry.isActive(key) || + hasVisibleActiveSessionRun({ + context, + requestedKey: key, + canonicalKey, + sessionId: entry?.sessionId, + defaultAgentId: resolveDefaultAgentId(cfg), + }) + ) { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, "Cannot archive a session with an active run."), + ); + return null; + } + } + return await applySessionPatchProjection({ + storePath, + resolveTarget: ({ entries }) => { + const store = Object.fromEntries( + entries.map(({ sessionKey, entry }) => [sessionKey, entry]), + ); + const { target: migratedTarget, primaryKey } = migrateAndPruneGatewaySessionStoreKey({ + cfg, + key, + store, + agentId: requestedAgentId, + }); + return { primaryKey, candidateKeys: migratedTarget.storeKeys }; + }, + project: async ({ primaryKey, existingEntry, entries }) => + await projectSessionsPatchEntry({ + cfg, + entries, + existingEntry, + storeKey: primaryKey, + agentId: requestedAgentId, + patch: p, + loadGatewayModelCatalog: context.loadGatewayModelCatalog, + }), + }); + }; + const applied = await runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: lifecycleIdentities, + run: applyPatch, }); + if (!applied) { + return; + } if (!applied.ok) { respond(false, undefined, applied.error); return; @@ -2241,57 +2414,177 @@ export const sessionsHandlers: GatewayRequestHandlers = { emitSessionUnboundLifecycleEvent, } = await loadSessionsRuntimeModule(); - const { entry, legacyKey, canonicalKey } = loadSessionEntry(key, { + const initialDeleteEntry = loadSessionEntry(key, { agentId: requestedAgentId, - }); - if (rejectPluginRuntimeDeleteMismatch({ client, key: canonicalKey ?? key, entry, respond })) { + }).entry; + const expectedSessionId = p.expectedSessionId?.trim(); + const expectedLifecycleRevision = p.expectedLifecycleRevision?.trim(); + const expectedSessionUpdatedAt = p.expectedSessionUpdatedAt; + const expectedLifecycleRevisionMatches = (entry: SessionEntry | undefined): boolean => + !expectedLifecycleRevision || entry?.lifecycleRevision === expectedLifecycleRevision; + const expectedSessionIdMatches = (entry: SessionEntry | undefined): boolean => { + if (!expectedSessionId || entry?.sessionId === expectedSessionId) { + return true; + } + return ( + entry?.sessionId === undefined && + expectedLifecycleRevision !== undefined && + expectedLifecycleRevisionMatches(entry) + ); + }; + const respondSessionChanged = () => { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, `Session ${key} changed before deletion. Retry.`, { + details: { reason: SESSION_LIFECYCLE_CHANGED_ERROR_REASON }, + }), + ); + }; + const rejectExpectedSessionMismatch = (entry: SessionEntry | undefined): boolean => { + const updatedAtMatches = + expectedSessionUpdatedAt === undefined || entry?.updatedAt === expectedSessionUpdatedAt; + if ( + expectedLifecycleRevisionMatches(entry) && + expectedSessionIdMatches(entry) && + updatedAtMatches + ) { + return false; + } + respondSessionChanged(); + return true; + }; + if (rejectExpectedSessionMismatch(initialDeleteEntry)) { return; } - const mutationCleanupError = await cleanupSessionBeforeMutation({ - cfg, + if ( + rejectPluginRuntimeDeleteMismatch({ + client, + key: target.canonicalKey ?? key, + entry: initialDeleteEntry, + respond, + }) + ) { + return; + } + const deleteLifecycleIdentities = [ + target.canonicalKey, key, - target, - entry, - legacyKey, - canonicalKey, - reason: "session-delete", - }); - if (mutationCleanupError) { - respond(false, undefined, mutationCleanupError); - return; - } - const deletion = await deleteSessionEntryLifecycle({ - agentId: target.agentId, - archiveTranscript: deleteTranscript, - storePath, - target: { - canonicalKey: target.canonicalKey, - storeKeys: target.storeKeys, + initialDeleteEntry?.sessionId, + expectedSessionId, + ]; + let admittedWorkReleased = true; + let expectedSessionStillCurrent = true; + const deletion = await runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: deleteLifecycleIdentities, + prepare: async () => { + expectedSessionStillCurrent = !rejectExpectedSessionMismatch( + loadSessionEntry(key, { agentId: requestedAgentId }).entry, + ); + if (!expectedSessionStillCurrent) { + return; + } + admittedWorkReleased = await interruptSessionWorkAdmissions({ + scope: storePath, + identities: deleteLifecycleIdentities, + timeoutMs: SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS, + }); + }, + run: async () => { + if (!expectedSessionStillCurrent) { + return undefined; + } + if (!admittedWorkReleased) { + respond( + false, + undefined, + errorShape(ErrorCodes.UNAVAILABLE, `Session ${key} is still active; try again.`), + ); + return undefined; + } + const { entry, legacyKey, canonicalKey } = loadSessionEntry(key, { + agentId: requestedAgentId, + }); + if (rejectExpectedSessionMismatch(entry)) { + return undefined; + } + if ( + rejectPluginRuntimeDeleteMismatch({ + client, + key: canonicalKey ?? key, + entry, + respond, + }) + ) { + return undefined; + } + const mutationCleanupError = await cleanupSessionBeforeMutation({ + cfg, + key, + target, + entry, + legacyKey, + canonicalKey, + reason: "session-delete", + }); + if (mutationCleanupError) { + respond(false, undefined, mutationCleanupError); + return undefined; + } + const postCleanupEntry = loadSessionEntry(key, { + agentId: requestedAgentId, + }).entry; + if ( + !expectedLifecycleRevisionMatches(postCleanupEntry) || + !expectedSessionIdMatches(postCleanupEntry) + ) { + respondSessionChanged(); + return undefined; + } + const result = await deleteSessionEntryLifecycle({ + agentId: target.agentId, + archiveTranscript: deleteTranscript, + expectedEntry: postCleanupEntry, + expectedLifecycleRevision, + expectedSessionId, + expectedUpdatedAt: postCleanupEntry?.updatedAt, + storePath, + target: { + canonicalKey: target.canonicalKey, + storeKeys: target.storeKeys, + }, + }); + if (result.expectedEntryMismatch) { + respondSessionChanged(); + return undefined; + } + if (result.deleted) { + emitGatewaySessionEndPluginHook({ + cfg, + sessionKey: target.canonicalKey ?? key, + sessionId: result.deletedSessionId, + storePath, + sessionFile: result.deletedSessionFile, + agentId: target.agentId, + reason: "deleted", + archivedTranscripts: result.archivedTranscripts, + }); + await emitSessionUnboundLifecycleEvent({ + targetSessionKey: target.canonicalKey ?? key, + reason: "session-delete", + emitHooks: p.emitLifecycleHooks !== false, + }); + } + return result; }, }); + if (!deletion) { + return; + } const deleted = deletion.deleted; - const sessionId = deletion.deletedSessionId; - const sessionFile = deletion.deletedSessionFile; const archivedTranscripts = deletion.archivedTranscripts; const archived = archivedTranscripts.map((entryLocal) => entryLocal.archivedPath); - if (deleted) { - emitGatewaySessionEndPluginHook({ - cfg, - sessionKey: target.canonicalKey ?? key, - sessionId, - storePath, - sessionFile, - agentId: target.agentId, - reason: "deleted", - archivedTranscripts, - }); - const emitLifecycleHooks = p.emitLifecycleHooks !== false; - await emitSessionUnboundLifecycleEvent({ - targetSessionKey: target.canonicalKey ?? key, - reason: "session-delete", - emitHooks: emitLifecycleHooks, - }); - } respond(true, { ok: true, key: target.canonicalKey, deleted, archived }, undefined); if (deleted) { @@ -2409,11 +2702,36 @@ export const sessionsHandlers: GatewayRequestHandlers = { return; } - if (maxLines === undefined) { + if (maxLines !== undefined) { + const trimPreflight = await preflightSessionTranscriptForManualCompact( + { + sessionId, + storePath, + sessionKey: compactTarget.primaryKey, + agentId: target.agentId, + }, + { maxLines, sessionFile: entry.sessionFile }, + ); + if (!trimPreflight.compacted) { + respond( + true, + { + ok: true, + key: target.canonicalKey, + compacted: false, + ...("kept" in trimPreflight + ? { kept: trimPreflight.kept } + : { reason: "no transcript" }), + }, + undefined, + ); + return; + } + } else { const filePath = resolveSessionTranscriptCandidates( sessionId, storePath, - entry?.sessionFile, + entry.sessionFile, target.agentId, ).find((candidate) => fs.existsSync(candidate)); if (!filePath) { @@ -2429,250 +2747,293 @@ export const sessionsHandlers: GatewayRequestHandlers = { ); return; } + } - const interruptResult = await interruptSessionRunIfActive({ - req, - context, - client, - isWebchatConnect, - requestedKey: key, - canonicalKey: target.canonicalKey, - agentId: requestedAgentId, - sessionId, - }); - if (interruptResult.error) { - respond(false, undefined, interruptResult.error); - return; - } - - const resolvedModel = resolveSessionModelRef(cfg, entry, target.agentId); - const workspaceDir = - normalizeOptionalString(entry?.spawnedWorkspaceDir) || - resolveAgentWorkspaceDir(cfg, target.agentId); - const cwd = normalizeOptionalString(entry?.spawnedCwd); - const operationId = randomUUID(); - emitSessionOperation(context, { - operationId, - operation: "compact", - phase: "start", - sessionKey: target.canonicalKey, - ...(target.canonicalKey === "global" && target.agentId ? { agentId: target.agentId } : {}), - }); - let result: Awaited>; - try { - result = await compactEmbeddedAgentSession({ - sessionId, - sessionKey: target.canonicalKey, - agentId: target.agentId, - allowGatewaySubagentBinding: true, - sessionFile: filePath, - workspaceDir, - cwd, - config: cfg, - provider: resolvedModel.provider, - model: resolvedModel.model, - authProfileId: entry?.authProfileOverride, - agentHarnessId: entry?.sessionId === sessionId ? entry.agentHarnessId : undefined, - thinkLevel: normalizeThinkLevel(entry?.thinkingLevel), - reasoningLevel: normalizeReasoningLevel(entry?.reasoningLevel), - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - trigger: "manual", - }); - } catch (err) { - emitSessionOperation(context, { - operationId, - operation: "compact", - phase: "end", - sessionKey: target.canonicalKey, - ...(target.canonicalKey === "global" && target.agentId - ? { agentId: target.agentId } - : {}), - completed: false, - reason: formatErrorMessage(err), - }); - throw err; - } - emitSessionOperation(context, { - operationId, - operation: "compact", - phase: "end", - sessionKey: target.canonicalKey, - ...(target.canonicalKey === "global" && target.agentId ? { agentId: target.agentId } : {}), - completed: result.ok && result.compacted, - reason: result.reason, - }); - - if (result.ok && result.compacted) { - await updateSessionStore(storePath, (store) => { - const entryKey = compactTarget.primaryKey; - const entryToUpdate = store[entryKey]; - if (!entryToUpdate) { + const lifecycleRevision = entry.lifecycleRevision; + const lifecycleIdentities = [ + key, + target.canonicalKey, + compactTarget.primaryKey, + sessionId, + lifecycleRevision, + ]; + let sessionStillCurrent = true; + let admittedWorkReleased = true; + try { + await runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: lifecycleIdentities, + kind: "compaction", + prepare: async () => { + const latestEntry = loadSessionEntry(key, { agentId: requestedAgentId }).entry; + sessionStillCurrent = Boolean( + latestEntry && + latestEntry.sessionId === sessionId && + latestEntry.lifecycleRevision === lifecycleRevision && + !resolveSessionWorkStartError(target.canonicalKey, latestEntry), + ); + if (!sessionStillCurrent) { return; } - entryToUpdate.updatedAt = Date.now(); - entryToUpdate.compactionCount = Math.max(0, entryToUpdate.compactionCount ?? 0) + 1; - if (result.result?.sessionId && result.result.sessionId !== entryToUpdate.sessionId) { - entryToUpdate.sessionId = result.result.sessionId; - } - if (result.result?.sessionFile) { - entryToUpdate.sessionFile = result.result.sessionFile; - } - delete entryToUpdate.inputTokens; - delete entryToUpdate.outputTokens; - delete entryToUpdate.contextBudgetStatus; - if ( - typeof result.result?.tokensAfter === "number" && - Number.isFinite(result.result.tokensAfter) - ) { - entryToUpdate.totalTokens = result.result.tokensAfter; - entryToUpdate.totalTokensFresh = true; - } else { - delete entryToUpdate.totalTokens; - delete entryToUpdate.totalTokensFresh; - } - }); - } - - respond( - true, - { - ok: result.ok, - key: target.canonicalKey, - compacted: result.compacted, - reason: result.reason, - result: result.result, + // Drop work queued against the pre-compaction transcript before its + // active admission drains and no longer exposes queue cleanup. + clearSessionQueues([key, target.canonicalKey, compactTarget.primaryKey, sessionId]); + admittedWorkReleased = await interruptSessionWorkAdmissions({ + scope: storePath, + identities: lifecycleIdentities, + timeoutMs: SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS, + }); }, - undefined, - ); - if (result.ok) { - emitSessionsChanged(context, { - sessionKey: target.canonicalKey, - ...(target.canonicalKey === "global" && target.agentId - ? { agentId: target.agentId } - : {}), - reason: "compact", - compacted: result.compacted, - }); - } - return; - } + run: async () => { + if (!sessionStillCurrent) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `Session ${key} changed before compaction. Retry.`, + { details: { reason: SESSION_LIFECYCLE_CHANGED_ERROR_REASON } }, + ), + ); + return; + } + if (!admittedWorkReleased) { + respond( + false, + undefined, + errorShape(ErrorCodes.UNAVAILABLE, `Session ${key} is still active; try again.`), + ); + return; + } - const trimPreflight = await preflightSessionTranscriptForManualCompact( - { - sessionId, - storePath, - sessionKey: compactTarget.primaryKey, - agentId: target.agentId, - }, - { - maxLines, - sessionFile: entry?.sessionFile, - }, - ); - if (!trimPreflight.compacted) { - if ("kept" in trimPreflight) { - respond( - true, - { - ok: true, - key: target.canonicalKey, - compacted: false, - kept: trimPreflight.kept, - }, - undefined, - ); - } else { - respond( - true, - { - ok: true, - key: target.canonicalKey, - compacted: false, - reason: "no transcript", - }, - undefined, - ); - } - return; - } + const latestEntry = loadSessionEntry(key, { agentId: requestedAgentId }).entry; + if ( + !latestEntry || + latestEntry.sessionId !== sessionId || + latestEntry.lifecycleRevision !== lifecycleRevision || + resolveSessionWorkStartError(target.canonicalKey, latestEntry) + ) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + `Session ${key} changed before compaction. Retry.`, + { details: { reason: SESSION_LIFECYCLE_CHANGED_ERROR_REASON } }, + ), + ); + return; + } - // Active-run safety parity with the LLM-summarize branch above. The maxLines - // truncate path archives and overwrites the transcript, so once preflight - // proves a destructive trim is needed, interrupt before rereading and writing. - const truncateInterrupt = await interruptSessionRunIfActive({ - req, - context, - client, - isWebchatConnect, - requestedKey: key, - canonicalKey: target.canonicalKey, - agentId: requestedAgentId, - sessionId, - }); - if (truncateInterrupt.error) { - respond(false, undefined, truncateInterrupt.error); - return; - } + const interruptResult = await interruptSessionRunIfActive({ + req, + context, + client, + isWebchatConnect, + requestedKey: key, + canonicalKey: target.canonicalKey, + agentId: requestedAgentId, + sessionId, + }); + if (interruptResult.error) { + respond(false, undefined, interruptResult.error); + return; + } - const trimResult = await trimSessionTranscriptForManualCompact( - { - sessionId, - storePath, - sessionKey: compactTarget.primaryKey, - agentId: target.agentId, - }, - { - maxLines, - sessionFile: entry?.sessionFile, - }, - ); - if (!trimResult.compacted) { - if ("kept" in trimResult) { - respond( - true, - { - ok: true, - key: target.canonicalKey, - compacted: false, - kept: trimResult.kept, - }, - undefined, - ); - } else { - respond( - true, - { - ok: true, - key: target.canonicalKey, - compacted: false, - reason: "no transcript", - }, - undefined, - ); - } - return; - } + if (maxLines !== undefined) { + const trimResult = await trimSessionTranscriptForManualCompact( + { + sessionId, + storePath, + sessionKey: compactTarget.primaryKey, + agentId: target.agentId, + }, + { maxLines, sessionFile: latestEntry.sessionFile }, + ); + respond( + true, + { + ok: true, + key: target.canonicalKey, + compacted: trimResult.compacted, + ...(trimResult.compacted + ? { archived: trimResult.archived, kept: trimResult.kept } + : "kept" in trimResult + ? { kept: trimResult.kept } + : { reason: "no transcript" }), + }, + undefined, + ); + if (trimResult.compacted) { + emitSessionsChanged(context, { + sessionKey: target.canonicalKey, + ...(target.canonicalKey === "global" && target.agentId + ? { agentId: target.agentId } + : {}), + reason: "compact", + compacted: true, + }); + } + return; + } - respond( - true, - { - ok: true, - key: target.canonicalKey, - compacted: true, - archived: trimResult.archived, - kept: trimResult.kept, - }, - undefined, - ); - emitSessionsChanged(context, { - sessionKey: target.canonicalKey, - ...(target.canonicalKey === "global" && target.agentId ? { agentId: target.agentId } : {}), - reason: "compact", - compacted: true, - }); + const filePath = resolveSessionTranscriptCandidates( + sessionId, + storePath, + latestEntry.sessionFile, + target.agentId, + ).find((candidate) => fs.existsSync(candidate)); + if (!filePath) { + respond( + true, + { + ok: true, + key: target.canonicalKey, + compacted: false, + reason: "no transcript", + }, + undefined, + ); + return; + } + + const resolvedModel = resolveSessionModelRef(cfg, latestEntry, target.agentId); + const workspaceDir = + normalizeOptionalString(latestEntry.spawnedWorkspaceDir) || + resolveAgentWorkspaceDir(cfg, target.agentId); + const operationId = randomUUID(); + emitSessionOperation(context, { + operationId, + operation: "compact", + phase: "start", + sessionKey: target.canonicalKey, + ...(target.canonicalKey === "global" && target.agentId + ? { agentId: target.agentId } + : {}), + }); + const emitCompactionEnd = (completed: boolean, reason?: string) => + emitSessionOperation(context, { + operationId, + operation: "compact", + phase: "end", + sessionKey: target.canonicalKey, + ...(target.canonicalKey === "global" && target.agentId + ? { agentId: target.agentId } + : {}), + completed, + reason, + }); + let result: Awaited>; + try { + result = await compactEmbeddedAgentSession({ + sessionId, + sessionKey: target.canonicalKey, + agentId: target.agentId, + allowGatewaySubagentBinding: true, + sessionFile: filePath, + workspaceDir, + cwd: normalizeOptionalString(latestEntry.spawnedCwd), + config: cfg, + provider: resolvedModel.provider, + model: resolvedModel.model, + authProfileId: latestEntry.authProfileOverride, + agentHarnessId: latestEntry.agentHarnessId, + thinkLevel: normalizeThinkLevel(latestEntry.thinkingLevel), + reasoningLevel: normalizeReasoningLevel(latestEntry.reasoningLevel), + bashElevated: { + enabled: false, + allowed: false, + defaultLevel: "off", + }, + trigger: "manual", + }); + } catch (err) { + emitCompactionEnd(false, formatErrorMessage(err)); + throw err; + } + if (result.ok && result.compacted) { + let persisted: boolean; + try { + persisted = await updateSessionStore(storePath, (store) => { + const entryToUpdate = store[compactTarget.primaryKey]; + if ( + !entryToUpdate || + entryToUpdate.sessionId !== sessionId || + entryToUpdate.lifecycleRevision !== lifecycleRevision || + resolveSessionWorkStartError(target.canonicalKey, entryToUpdate) + ) { + return false; + } + entryToUpdate.updatedAt = Date.now(); + entryToUpdate.compactionCount = Math.max(0, entryToUpdate.compactionCount ?? 0) + 1; + if ( + result.result?.sessionId && + result.result.sessionId !== entryToUpdate.sessionId + ) { + entryToUpdate.sessionId = result.result.sessionId; + } + if (result.result?.sessionFile) { + entryToUpdate.sessionFile = result.result.sessionFile; + } + delete entryToUpdate.inputTokens; + delete entryToUpdate.outputTokens; + delete entryToUpdate.contextBudgetStatus; + if ( + typeof result.result?.tokensAfter === "number" && + Number.isFinite(result.result.tokensAfter) + ) { + entryToUpdate.totalTokens = result.result.tokensAfter; + entryToUpdate.totalTokensFresh = true; + } else { + delete entryToUpdate.totalTokens; + delete entryToUpdate.totalTokensFresh; + } + return true; + }); + } catch (err) { + emitCompactionEnd(false, formatErrorMessage(err)); + throw err; + } + if (!persisted) { + const reason = `Session ${key} changed before compaction completed. Retry.`; + emitCompactionEnd(false, reason); + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, reason, { + details: { reason: SESSION_LIFECYCLE_CHANGED_ERROR_REASON }, + }), + ); + return; + } + } + + emitCompactionEnd(result.ok && result.compacted, result.reason); + respond( + true, + { + ok: result.ok, + key: target.canonicalKey, + compacted: result.compacted, + reason: result.reason, + result: result.result, + }, + undefined, + ); + if (result.ok) { + emitSessionsChanged(context, { + sessionKey: target.canonicalKey, + ...(target.canonicalKey === "global" && target.agentId + ? { agentId: target.agentId } + : {}), + reason: "compact", + compacted: result.compacted, + }); + } + }, + }); + } catch (err) { + respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(err))); + } }, }; diff --git a/src/gateway/server-shared.ts b/src/gateway/server-shared.ts index 1a0b36ac1c43..ea915108c99f 100644 --- a/src/gateway/server-shared.ts +++ b/src/gateway/server-shared.ts @@ -2,6 +2,12 @@ // Defines cached dedupe entries for idempotent Gateway method calls. import type { ErrorShape } from "../../packages/gateway-protocol/src/index.js"; +export const PENDING_CHAT_SEND_DEDUPE_PREFIX = "pending-chat:"; + +export function pendingChatSendDedupeKey(runId: string): string { + return `${PENDING_CHAT_SEND_DEDUPE_PREFIX}${runId}`; +} + // Dedupe entries cache recent request results so repeated gateway calls can // replay the same success/error payload without re-running the method. export type DedupeEntry = { diff --git a/src/gateway/server.agent.deleted-agent-gateway.test.ts b/src/gateway/server.agent.deleted-agent-gateway.test.ts index 422eeb0473cb..e5022edf89b6 100644 --- a/src/gateway/server.agent.deleted-agent-gateway.test.ts +++ b/src/gateway/server.agent.deleted-agent-gateway.test.ts @@ -61,6 +61,46 @@ test("agent RPC rejects deleted-agent session keys before dispatch", async () => } }); +test("agent RPC rejects archived session keys before dispatch", async () => { + const { dir } = await createSessionStoreDir(); + const storeTemplate = await configurePerAgentSessionStore(dir); + const mainStorePath = storeTemplate.replace("{agentId}", "main"); + const archivedKey = "agent:main:subagent:archived"; + + await fs.mkdir(path.dirname(mainStorePath), { recursive: true }); + await fs.writeFile( + mainStorePath, + JSON.stringify( + { + [archivedKey]: sessionStoreEntry("sess-archived", { archivedAt: Date.now() }), + }, + null, + 2, + ), + "utf-8", + ); + + vi.mocked(agentCommand).mockClear(); + const { ws } = await openClient(); + try { + const blocked = await rpcReq(ws, "agent", { + sessionKey: archivedKey, + message: "hi", + idempotencyKey: "proof-archived-session", + }); + expect(blocked.ok).toBe(false); + expect(blocked.error).toEqual({ + code: ErrorCodes.INVALID_REQUEST, + message: + 'Session "agent:main:subagent:archived" is archived. Restore it before starting new work.', + }); + expect(agentCommand).not.toHaveBeenCalled(); + } finally { + ws.close(); + resetSessionStoreFixture(); + } +}); + test("agent RPC still dispatches for configured-agent session keys", async () => { const { dir } = await createSessionStoreDir(); const storeTemplate = await configurePerAgentSessionStore(dir); diff --git a/src/gateway/server.chat.gateway-server-chat-b.test.ts b/src/gateway/server.chat.gateway-server-chat-b.test.ts index e5798e634529..1ef4ec6f20ed 100644 --- a/src/gateway/server.chat.gateway-server-chat-b.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-b.test.ts @@ -3,16 +3,21 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js"; import type { GetReplyOptions } from "../auto-reply/get-reply-options.types.js"; import type { InternalGetReplyOptions } from "../auto-reply/reply/get-reply.types.js"; import { clearConfigCache } from "../config/config.js"; +import { invalidateSessionStoreCache } from "../config/sessions/store-cache.js"; import type { AgentModelConfig } from "../config/types.agents-shared.js"; +import { rotateAgentEventLifecycleGeneration } from "../infra/agent-events.js"; +import { runExclusiveSessionLifecycleMutation } from "../sessions/session-lifecycle-admission.js"; import { createDeferred } from "../test-utils/deferred.js"; import { captureEnv, setTestEnvValue } from "../test-utils/env.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { setMaxChatHistoryMessagesBytesForTest } from "./server-constants.js"; import type { GatewayRequestContext, RespondFn } from "./server-methods/shared-types.js"; +import { pendingChatSendDedupeKey } from "./server-shared.js"; import { connectOk, createGatewaySuiteHarness, @@ -31,6 +36,7 @@ const FAST_WAIT_OPTS = { timeout: 2_000, interval: 5 } as const; type GatewayHarness = Awaited>; type GatewaySocket = Awaited>; let harness: GatewayHarness; +const autoCleanupTempDirs = useAutoCleanupTempDirTracker(afterEach); beforeAll(async () => { harness = await createGatewaySuiteHarness(); @@ -141,6 +147,93 @@ async function removeTempDir(dir: string): Promise { await fs.rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); } +function createDirectChatContext(): GatewayRequestContext { + return { + loadGatewayModelCatalog: vi.fn().mockResolvedValue([]), + logGateway: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + agentRunSeq: new Map(), + chatAbortControllers: new Map(), + chatAbortedRuns: new Map(), + chatRunBuffers: new Map(), + chatDeltaSentAt: new Map(), + chatDeltaLastBroadcastLen: new Map(), + chatDeltaLastBroadcastText: new Map(), + agentDeltaSentAt: new Map(), + bufferedAgentEvents: new Map(), + clearChatRunState: vi.fn(), + addChatRun: vi.fn(), + removeChatRun: vi.fn(), + broadcast: vi.fn(), + nodeSendToSession: vi.fn(), + registerToolEventRecipient: vi.fn(), + getRuntimeConfig: () => ({}), + dedupe: new Map(), + } as unknown as GatewayRequestContext; +} + +test("chat.send replays a cached result after the session is archived", async () => { + const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + try { + dispatchInboundMessageMock.mockClear(); + testState.sessionStorePath = path.join(sessionDir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + archivedAt: Date.now(), + }, + }, + }); + const context = createDirectChatContext(); + const runId = "idem-archived-cached-result"; + const cachedPayload = { runId, status: "ok", summary: "already completed" }; + context.dedupe.set(`chat:${runId}`, { + ts: Date.now(), + ok: true, + payload: cachedPayload, + }); + const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown; meta?: unknown }> = + []; + const { chatHandlers } = await import("./server-methods/chat.js"); + + await chatHandlers["chat.send"]({ + req: { type: "req", id: "cached", method: "chat.send" }, + params: { + sessionKey: "main", + message: "retry completed send", + idempotencyKey: runId, + }, + client: null, + isWebchatConnect: () => false, + respond: ((ok, payload, error, meta) => { + responses.push({ ok, payload, error, meta }); + }) as RespondFn, + context, + }); + + expect(responses).toEqual([ + { + ok: true, + payload: cachedPayload, + error: undefined, + meta: { cached: true }, + }, + ]); + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + } finally { + dispatchInboundMessageMock.mockReset(); + testState.sessionStorePath = undefined; + clearConfigCache(); + await removeTempDir(sessionDir); + } +}); + async function readTimelineEvents(filePath: string): Promise>> { const raw = await fs.readFile(filePath, "utf-8"); return raw @@ -1109,6 +1202,508 @@ describe("gateway server chat", () => { } }); + test("chat.abort cancels chat.send while lifecycle admission waits", async () => { + const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const releaseMutation = createDeferred(); + try { + testState.sessionStorePath = path.join(sessionDir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + }, + }); + const mutationStarted = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: testState.sessionStorePath, + identities: ["sess-main"], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + + const sendResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; + const abortResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; + const context = createDirectChatContext(); + const runId = "idem-lifecycle-wait-abort"; + const collidingFinalKey = `chat:pending:${runId}`; + const collidingFinalEntry = { + ts: Date.now(), + ok: true, + payload: { runId: `pending:${runId}`, status: "ok" }, + }; + context.dedupe.set(collidingFinalKey, collidingFinalEntry); + const params = { + sessionKey: "main", + message: "do not dispatch", + idempotencyKey: runId, + }; + const client = { + connId: "conn-owner", + connect: { + device: { id: "dev-owner" }, + scopes: ["operator.write"], + }, + } as never; + const { chatHandlers } = await import("./server-methods/chat.js"); + const send = Promise.resolve( + chatHandlers["chat.send"]({ + req: { type: "req", id: "send", method: "chat.send", params }, + params, + client, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + sendResponses.push({ ok, payload, error }); + }) as RespondFn, + context, + }), + ); + await vi.waitFor(() => { + expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(true); + }, FAST_WAIT_OPTS); + expect(context.dedupe.get(collidingFinalKey)).toBe(collidingFinalEntry); + expect(context.chatAbortControllers.has(runId)).toBe(false); + + const retryResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; + await chatHandlers["chat.send"]({ + req: { type: "req", id: "retry", method: "chat.send", params }, + params, + client, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + retryResponses.push({ ok, payload, error }); + }) as RespondFn, + context, + }); + expect(retryResponses).toEqual([ + { + ok: true, + payload: { runId, status: "in_flight" }, + error: undefined, + }, + ]); + expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(true); + + await chatHandlers["chat.abort"]({ + req: { + type: "req", + id: "abort", + method: "chat.abort", + params: { sessionKey: "main", runId }, + }, + params: { sessionKey: "main", runId }, + client, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + abortResponses.push({ ok, payload, error }); + }) as RespondFn, + context, + }); + releaseMutation.resolve(); + await mutation; + await send; + + expect(abortResponses).toEqual([ + { + ok: true, + payload: { ok: true, aborted: true, runIds: [runId] }, + error: undefined, + }, + ]); + expect(sendResponses).toEqual([ + { + ok: true, + payload: { + runId, + status: "timeout", + summary: "aborted", + stopReason: "rpc", + endedAt: expect.any(Number), + }, + error: undefined, + }, + ]); + expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(false); + expect(context.dedupe.get(collidingFinalKey)).toBe(collidingFinalEntry); + expect(context.chatAbortControllers.has(runId)).toBe(false); + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + } finally { + releaseMutation.resolve(); + dispatchInboundMessageMock.mockReset(); + testState.sessionStorePath = undefined; + clearConfigCache(); + await removeTempDir(sessionDir); + } + }); + + test("chat.send rejects stale lifecycle work after admission waits", async () => { + const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const releaseMutation = createDeferred(); + try { + testState.sessionStorePath = path.join(sessionDir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + }, + }); + const mutationStarted = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: testState.sessionStorePath, + identities: ["sess-main"], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + + const sendResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; + const context = createDirectChatContext(); + const runId = "idem-stale-lifecycle"; + const params = { + sessionKey: "main", + message: "do not resume after restart", + idempotencyKey: runId, + }; + const { chatHandlers } = await import("./server-methods/chat.js"); + const send = Promise.resolve( + chatHandlers["chat.send"]({ + req: { type: "req", id: "send", method: "chat.send", params }, + params, + client: null, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + sendResponses.push({ ok, payload, error }); + }) as RespondFn, + context, + }), + ); + await vi.waitFor(() => { + expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(true); + }, FAST_WAIT_OPTS); + + rotateAgentEventLifecycleGeneration(); + releaseMutation.resolve(); + await mutation; + await send; + + expect(sendResponses).toEqual([ + { + ok: true, + payload: { + runId, + status: "timeout", + summary: "aborted", + stopReason: "restart", + endedAt: expect.any(Number), + }, + error: undefined, + }, + ]); + expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(false); + expect(context.chatAbortControllers.has(runId)).toBe(false); + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + } finally { + releaseMutation.resolve(); + dispatchInboundMessageMock.mockReset(); + testState.sessionStorePath = undefined; + clearConfigCache(); + await removeTempDir(sessionDir); + } + }); + + test("chat.send does not recreate a session deleted while admission waits", async () => { + const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const releaseMutation = createDeferred(); + try { + testState.sessionStorePath = path.join(sessionDir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + }, + }); + const mutationStarted = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: testState.sessionStorePath, + identities: ["agent:main:main", "sess-main"], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + + const sendResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; + const context = createDirectChatContext(); + const runId = "idem-deleted-during-admission"; + const params = { + sessionKey: "main", + message: "do not recreate the deleted session", + idempotencyKey: runId, + }; + const { chatHandlers } = await import("./server-methods/chat.js"); + const send = Promise.resolve( + chatHandlers["chat.send"]({ + req: { type: "req", id: "send", method: "chat.send", params }, + params, + client: null, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + sendResponses.push({ ok, payload, error }); + }) as RespondFn, + context, + }), + ); + await vi.waitFor(() => { + expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(true); + }, FAST_WAIT_OPTS); + + await writeSessionStore({ entries: {} }); + releaseMutation.resolve(); + await mutation; + await send; + + expect(sendResponses).toHaveLength(1); + expect(sendResponses[0]?.ok).toBe(false); + expect(sendResponses[0]?.error).toMatchObject({ + message: expect.stringMatching(/deleted while starting work/i), + }); + expect(context.chatAbortControllers.has(runId)).toBe(false); + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + } finally { + releaseMutation.resolve(); + dispatchInboundMessageMock.mockReset(); + testState.sessionStorePath = undefined; + clearConfigCache(); + await removeTempDir(sessionDir); + } + }); + + test("chat.send does not enter a replacement session after reset while admission waits", async () => { + const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const releaseMutation = createDeferred(); + try { + testState.sessionStorePath = path.join(sessionDir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-before-reset", + updatedAt: Date.now(), + }, + }, + }); + const mutationStarted = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: testState.sessionStorePath, + identities: ["agent:main:main", "sess-before-reset"], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + + const sendResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; + const context = createDirectChatContext(); + const runId = "idem-reset-during-admission"; + const params = { + sessionKey: "main", + message: "do not enter the replacement session", + idempotencyKey: runId, + }; + const { chatHandlers } = await import("./server-methods/chat.js"); + const send = Promise.resolve( + chatHandlers["chat.send"]({ + req: { type: "req", id: "send", method: "chat.send", params }, + params, + client: null, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + sendResponses.push({ ok, payload, error }); + }) as RespondFn, + context, + }), + ); + await vi.waitFor(() => { + expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(true); + }, FAST_WAIT_OPTS); + + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-after-reset", + updatedAt: Date.now(), + }, + }, + }); + releaseMutation.resolve(); + await mutation; + await send; + + expect(sendResponses).toHaveLength(1); + expect(sendResponses[0]?.ok).toBe(false); + expect(sendResponses[0]?.error).toMatchObject({ + message: expect.stringMatching(/changed while starting work/i), + }); + expect(context.chatAbortControllers.has(runId)).toBe(false); + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + } finally { + releaseMutation.resolve(); + dispatchInboundMessageMock.mockReset(); + testState.sessionStorePath = undefined; + clearConfigCache(); + await removeTempDir(sessionDir); + } + }); + + test("chat.send does not consume a replacement pending reservation", async () => { + const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const releaseMutation = createDeferred(); + const releaseTerminalMutation = createDeferred(); + try { + testState.sessionStorePath = path.join(sessionDir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + }, + }); + const mutationStarted = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: testState.sessionStorePath, + identities: ["sess-main"], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + + const sendResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; + const context = createDirectChatContext(); + const runId = "idem-replaced-reservation"; + const pendingKey = pendingChatSendDedupeKey(runId); + const params = { + sessionKey: "main", + message: "only the replacement may run", + idempotencyKey: runId, + }; + const { chatHandlers } = await import("./server-methods/chat.js"); + const send = Promise.resolve( + chatHandlers["chat.send"]({ + req: { type: "req", id: "send", method: "chat.send", params }, + params, + client: null, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + sendResponses.push({ ok, payload, error }); + }) as RespondFn, + context, + }), + ); + await vi.waitFor(() => { + expect(context.dedupe.has(pendingKey)).toBe(true); + }, FAST_WAIT_OPTS); + const original = context.dedupe.get(pendingKey); + const originalPayload = original?.payload as Record; + const replacement = { + ts: Date.now(), + ok: true, + payload: { + ...originalPayload, + attemptId: "replacement-attempt", + expiresAtMs: Date.now() + 120_000, + }, + }; + context.dedupe.set(pendingKey, replacement); + + releaseMutation.resolve(); + await mutation; + await send; + + expect(sendResponses).toEqual([ + { + ok: true, + payload: { runId, status: "in_flight" }, + error: undefined, + }, + ]); + expect(context.dedupe.get(pendingKey)).toBe(replacement); + expect(context.chatAbortControllers.has(runId)).toBe(false); + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + + const terminalMutationStarted = createDeferred(); + const terminalMutation = runExclusiveSessionLifecycleMutation({ + scope: testState.sessionStorePath, + identities: ["sess-main"], + run: async () => { + terminalMutationStarted.resolve(); + await releaseTerminalMutation.promise; + }, + }); + await terminalMutationStarted.promise; + const terminalRunId = "idem-terminal-replacement"; + const terminalPendingKey = pendingChatSendDedupeKey(terminalRunId); + const terminalParams = { + sessionKey: "main", + message: "preserve the replacement result", + idempotencyKey: terminalRunId, + }; + const terminalResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; + const terminalSend = Promise.resolve( + chatHandlers["chat.send"]({ + req: { type: "req", id: "terminal-send", method: "chat.send", params: terminalParams }, + params: terminalParams, + client: null, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + terminalResponses.push({ ok, payload, error }); + }) as RespondFn, + context, + }), + ); + await vi.waitFor(() => { + expect(context.dedupe.has(terminalPendingKey)).toBe(true); + }, FAST_WAIT_OPTS); + const terminalResult = { + ts: Date.now(), + ok: true, + payload: { runId: terminalRunId, status: "ok", summary: "replacement completed" }, + }; + context.dedupe.delete(terminalPendingKey); + context.dedupe.set(`chat:${terminalRunId}`, terminalResult); + + releaseTerminalMutation.resolve(); + await terminalMutation; + await terminalSend; + + expect(terminalResponses).toEqual([ + { ok: true, payload: terminalResult.payload, error: undefined }, + ]); + expect(context.dedupe.get(`chat:${terminalRunId}`)).toBe(terminalResult); + expect(context.chatAbortedRuns.has(terminalRunId)).toBe(false); + } finally { + releaseMutation.resolve(); + releaseTerminalMutation.resolve(); + dispatchInboundMessageMock.mockReset(); + testState.sessionStorePath = undefined; + clearConfigCache(); + await removeTempDir(sessionDir); + } + }); + test.each(configuredImageModelCases)( "chat.send preserves text-only image uploads as MediaPaths even with configured imageModel: $id", async ({ id, imageModel }) => { @@ -1262,7 +1857,7 @@ describe("gateway server chat", () => { }, ); - test("chat.send reuses only active WebChat text sends with the same system context", async () => { + test("chat.send keeps matching WebChat text sends distinct by idempotency key", async () => { const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); const dispatchRelease = createDeferred(); try { @@ -1305,7 +1900,12 @@ describe("gateway server chat", () => { dispatchInboundMessageMock.mockImplementation(async () => dispatchRelease.promise); const { chatHandlers } = await import("./server-methods/chat.js"); - const callSend = (id: string, idempotencyKey: string, systemProvenanceReceipt?: string) => + const callSend = ( + id: string, + idempotencyKey: string, + systemProvenanceReceipt?: string, + thinking = "low", + ) => chatHandlers["chat.send"]({ req: { type: "req", @@ -1315,6 +1915,7 @@ describe("gateway server chat", () => { sessionKey: "main", message: "?", idempotencyKey, + thinking, ...(systemProvenanceReceipt ? { systemProvenanceReceipt } : {}), }, }, @@ -1322,6 +1923,7 @@ describe("gateway server chat", () => { sessionKey: "main", message: "?", idempotencyKey, + thinking, ...(systemProvenanceReceipt ? { systemProvenanceReceipt } : {}), }, client: { @@ -1362,31 +1964,40 @@ describe("gateway server chat", () => { { timeout: 2_000, interval: 5 }, ); - await callSend("duplicate", "idem-active-b"); + const duplicate = Promise.resolve(callSend("duplicate", "idem-active-b")); - expect(responses).toEqual([ - { - id: "first", - ok: true, - payload: expect.objectContaining({ - runId: "idem-active-a", - status: "started", - serverTiming: { - receivedToAckMs: expect.any(Number), - loadSessionMs: expect.any(Number), - }, - }), - error: undefined, - }, - { - id: "duplicate", - ok: true, - payload: { runId: "idem-active-a", status: "in_flight" }, - error: undefined, - }, - ]); - expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1); - expect(context.addChatRun).toHaveBeenCalledTimes(1); + await vi.waitFor(() => { + expect(responses).toEqual([ + { + id: "first", + ok: true, + payload: expect.objectContaining({ + runId: "idem-active-a", + status: "started", + serverTiming: { + receivedToAckMs: expect.any(Number), + loadSessionMs: expect.any(Number), + }, + }), + error: undefined, + }, + { + id: "duplicate", + ok: true, + payload: expect.objectContaining({ + runId: "idem-active-b", + status: "started", + serverTiming: { + receivedToAckMs: expect.any(Number), + loadSessionMs: expect.any(Number), + }, + }), + error: undefined, + }, + ]); + }, FAST_WAIT_OPTS); + expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(2); + expect(context.addChatRun).toHaveBeenCalledTimes(2); const withSystemContext = Promise.resolve( callSend("system-context", "idem-active-c", "proposal=support-file-sampler-b"), @@ -1411,7 +2022,14 @@ describe("gateway server chat", () => { { id: "duplicate", ok: true, - payload: { runId: "idem-active-a", status: "in_flight" }, + payload: expect.objectContaining({ + runId: "idem-active-b", + status: "started", + serverTiming: { + receivedToAckMs: expect.any(Number), + loadSessionMs: expect.any(Number), + }, + }), error: undefined, }, { @@ -1431,11 +2049,113 @@ describe("gateway server chat", () => { }, { timeout: 2_000, interval: 5 }, ); - expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(2); - expect(context.addChatRun).toHaveBeenCalledTimes(2); + expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(3); + expect(context.addChatRun).toHaveBeenCalledTimes(3); + + const withDifferentThinking = Promise.resolve( + callSend("different-thinking", "idem-active-d", undefined, "high"), + ); + await vi.waitFor( + () => { + expect(responses.at(-1)).toEqual({ + id: "different-thinking", + ok: true, + payload: expect.objectContaining({ + runId: "idem-active-d", + status: "started", + serverTiming: { + receivedToAckMs: expect.any(Number), + loadSessionMs: expect.any(Number), + }, + }), + error: undefined, + }); + }, + { timeout: 2_000, interval: 5 }, + ); + expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(4); + expect(context.addChatRun).toHaveBeenCalledTimes(4); dispatchRelease.resolve(); - await Promise.all([first, withSystemContext]); + await Promise.all([first, duplicate, withSystemContext, withDifferentThinking]); + await vi.waitFor(() => { + expect(context.removeChatRun).toHaveBeenCalledTimes(4); + }, FAST_WAIT_OPTS); + } finally { + dispatchRelease.resolve(); + dispatchInboundMessageMock.mockReset(); + testState.sessionStorePath = undefined; + clearConfigCache(); + await removeTempDir(sessionDir); + } + }); + + test("chat.send keeps distinct sends independent when a session ID appears during the first turn", async () => { + const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const dispatchRelease = createDeferred(); + try { + const storePath = path.join(sessionDir, "sessions.json"); + testState.sessionStorePath = storePath; + await writeSessionStore({ entries: {} }); + const responses: Array<{ id: string; ok: boolean; payload?: unknown; error?: unknown }> = []; + const context = createDirectChatContext(); + dispatchInboundMessageMock.mockImplementation(async () => dispatchRelease.promise); + const { chatHandlers } = await import("./server-methods/chat.js"); + const callSend = (id: string, idempotencyKey: string) => { + const params = { + sessionKey: "main", + message: "create this session once", + idempotencyKey, + }; + return chatHandlers["chat.send"]({ + req: { type: "req", id, method: "chat.send", params }, + params, + client: null, + isWebchatConnect: () => false, + respond: ((ok, payload, error) => { + responses.push({ id, ok, payload, error }); + }) as RespondFn, + context, + }); + }; + + const first = Promise.resolve(callSend("first", "idem-new-session-a")); + await vi.waitFor(() => { + expect(responses[0]).toEqual({ + id: "first", + ok: true, + payload: expect.objectContaining({ + runId: "idem-new-session-a", + status: "started", + }), + error: undefined, + }); + }, FAST_WAIT_OPTS); + await fs.writeFile( + storePath, + JSON.stringify({ + main: { + sessionId: "sess-created-during-run", + updatedAt: Date.now(), + }, + }), + "utf8", + ); + invalidateSessionStoreCache(storePath); + + const duplicate = Promise.resolve(callSend("duplicate", "idem-new-session-b")); + await vi.waitFor(() => { + expect(responses.at(-1)).toEqual({ + id: "duplicate", + ok: true, + payload: expect.objectContaining({ runId: "idem-new-session-b", status: "started" }), + error: undefined, + }); + }, FAST_WAIT_OPTS); + expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(2); + + dispatchRelease.resolve(); + await Promise.all([first, duplicate]); await vi.waitFor(() => { expect(context.removeChatRun).toHaveBeenCalledTimes(2); }, FAST_WAIT_OPTS); @@ -1444,7 +2164,6 @@ describe("gateway server chat", () => { dispatchInboundMessageMock.mockReset(); testState.sessionStorePath = undefined; clearConfigCache(); - await removeTempDir(sessionDir); } }); diff --git a/src/gateway/server.chat.gateway-server-chat.test.ts b/src/gateway/server.chat.gateway-server-chat.test.ts index 49a34fc4f9b0..365d7208b579 100644 --- a/src/gateway/server.chat.gateway-server-chat.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.test.ts @@ -102,7 +102,7 @@ describe("gateway server chat", () => { const withMainSessionStore = async ( run: (dir: string) => Promise, - options?: { sessionId?: string }, + options?: { archivedAt?: number; sessionId?: string }, ): Promise => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); try { @@ -114,6 +114,7 @@ describe("gateway server chat", () => { sessionId, sessionFile: path.join(dir, `${sessionId}.jsonl`), updatedAt: Date.now(), + ...(options?.archivedAt !== undefined ? { archivedAt: options.archivedAt } : {}), }, }, }); @@ -180,6 +181,26 @@ describe("gateway server chat", () => { return res; }; + test("chat.send rejects archived sessions before dispatch", async () => { + await withMainSessionStore( + async () => { + dispatchInboundMessageMock.mockClear(); + const res = await rpcReq(ws, "chat.send", { + sessionKey: "main", + message: "blocked while archived", + idempotencyKey: "proof-chat-archived-session", + }); + expect(res.ok).toBe(false); + expect(res.error).toMatchObject({ + code: "INVALID_REQUEST", + message: 'Session "agent:main:main" is archived. Restore it before starting new work.', + }); + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + }, + { archivedAt: Date.now() }, + ); + }); + const waitForAgentRunOk = async (runId: string, timeoutMs = 1_000) => { const res = await rpcReq(ws, "agent.wait", { runId, diff --git a/src/gateway/server.sessions.compaction.test.ts b/src/gateway/server.sessions.compaction.test.ts index c9ab424385e2..faa2abe464b6 100644 --- a/src/gateway/server.sessions.compaction.test.ts +++ b/src/gateway/server.sessions.compaction.test.ts @@ -6,6 +6,10 @@ import os from "node:os"; import path from "node:path"; import { expect, test, vi } from "vitest"; import type { SessionCompactionCheckpoint } from "../config/sessions.js"; +import { + beginSessionWorkAdmission, + runExclusiveSessionLifecycleMutation, +} from "../sessions/session-lifecycle-admission.js"; import { withTempDir } from "../test-helpers/temp-dir.js"; import { withEnvAsync } from "../test-utils/env.js"; import { @@ -19,11 +23,13 @@ import { } from "./test-helpers.js"; import { setupGatewaySessionsTestHarness, + createDeferred, getSessionManagerModule, getGatewayConfigModule, sessionStoreEntry, createCheckpointFixture, directSessionReq, + expectSessionQueueCleanup, } from "./test/server-sessions.test-helpers.js"; const { createSessionStoreDir, createSelectedGlobalSessionStore, openClient } = @@ -551,7 +557,7 @@ test("sessions.compact without maxLines runs embedded manual compaction for chec ws.close(); }); -test("sessions.compact treats Codex native compaction start as pending, not completed", async () => { +test("sessions.compact records terminal Codex native compaction", async () => { const { dir, storePath } = await createSessionStoreDir(); await fs.writeFile( path.join(dir, "sess-codex.jsonl"), @@ -570,7 +576,7 @@ test("sessions.compact treats Codex native compaction start as pending, not comp }); embeddedRunMock.compactEmbeddedAgentSession.mockResolvedValueOnce({ ok: true, - compacted: false, + compacted: true, result: { summary: "", firstKeptEntryId: "", @@ -579,7 +585,8 @@ test("sessions.compact treats Codex native compaction start as pending, not comp backend: "codex-app-server", threadId: "thread-1", signal: "thread/compact/start", - pending: true, + pending: false, + completed: true, }, }, }); @@ -597,19 +604,20 @@ test("sessions.compact treats Codex native compaction start as pending, not comp key: "main", }); - expectMainCompactionResult(compacted, false); + expectMainCompactionResult(compacted, true); expect(compacted.payload?.result?.details).toMatchObject({ backend: "codex-app-server", threadId: "thread-1", signal: "thread/compact/start", - pending: true, + pending: false, + completed: true, }); const endEvent = await endEventPromise; expect(endEvent.payload).toMatchObject({ operation: "compact", phase: "end", sessionKey: "agent:main:main", - completed: false, + completed: true, }); const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< @@ -620,13 +628,466 @@ test("sessions.compact treats Codex native compaction start as pending, not comp totalTokensFresh?: boolean; } >; - expect(store["agent:main:main"]?.compactionCount).toBe(2); - expect(store["agent:main:main"]?.totalTokens).toBe(54_321); - expect(store["agent:main:main"]?.totalTokensFresh).toBe(true); + expect(store["agent:main:main"]?.compactionCount).toBe(3); + expect(store["agent:main:main"]?.totalTokens).toBeUndefined(); + expect(store["agent:main:main"]?.totalTokensFresh).toBeUndefined(); ws.close(); }); +test("sessions.compact emits a terminal operation event when persistence fails", async () => { + const { dir } = await createSessionStoreDir(); + const sessionId = "sess-compact-write-failure"; + await fs.writeFile( + path.join(dir, `${sessionId}.jsonl`), + `${JSON.stringify({ role: "user", content: "hello" })}\n`, + "utf-8", + ); + await writeSessionStore({ entries: { main: sessionStoreEntry(sessionId) } }); + const compaction = createDeferred<{ + ok: true; + compacted: true; + result: { + summary: string; + firstKeptEntryId: string; + tokensBefore: number; + tokensAfter: number; + }; + }>(); + embeddedRunMock.compactEmbeddedAgentSession.mockReturnValueOnce(compaction.promise); + + const { ws } = await openClient(); + await rpcReq(ws, "sessions.subscribe", {}); + const startEventPromise = onceMessage(ws, (message) => isCompactOperationEvent(message, "start")); + const endEventPromise = onceMessage(ws, (message) => isCompactOperationEvent(message, "end")); + const compactResult = rpcReq(ws, "sessions.compact", { key: "main" }); + await startEventPromise; + const terminalResult = { + ok: true as const, + compacted: true as const, + result: { + summary: "summary", + firstKeptEntryId: "entry-1", + tokensBefore: 120, + tokensAfter: 80, + }, + }; + Object.defineProperty(terminalResult.result, "tokensAfter", { + get: () => { + throw new Error("forced persistence projection failure"); + }, + }); + compaction.resolve(terminalResult); + + const response = await compactResult; + expect(response.ok).toBe(false); + expect(response.error?.code).toBe("UNAVAILABLE"); + expect((await endEventPromise).payload).toMatchObject({ + operation: "compact", + phase: "end", + sessionKey: "agent:main:main", + completed: false, + }); + ws.close(); +}); + +test("sessions.compact rejects stale terminal persistence after the session changes", async () => { + const { dir, storePath } = await createSessionStoreDir(); + await fs.writeFile( + path.join(dir, "sess-compact-old.jsonl"), + `${JSON.stringify({ role: "user", content: "hello" })}\n`, + "utf-8", + ); + await writeSessionStore({ entries: { main: sessionStoreEntry("sess-compact-old") } }); + const compaction = createDeferred<{ + ok: true; + compacted: true; + result: { + summary: string; + firstKeptEntryId: string; + tokensBefore: number; + tokensAfter: number; + sessionId: string; + }; + }>(); + embeddedRunMock.compactEmbeddedAgentSession.mockReturnValueOnce(compaction.promise); + + const { ws } = await openClient(); + const compactResult = rpcReq(ws, "sessions.compact", { key: "main" }); + await vi.waitFor(() => { + expect(embeddedRunMock.compactEmbeddedAgentSession).toHaveBeenCalledTimes(1); + }); + await writeSessionStore({ entries: { main: sessionStoreEntry("sess-replacement") } }); + compaction.resolve({ + ok: true, + compacted: true, + result: { + summary: "summary", + firstKeptEntryId: "entry-1", + tokensBefore: 120, + tokensAfter: 80, + sessionId: "sess-compacted-successor", + }, + }); + + const response = await compactResult; + expect(response.ok).toBe(false); + expect(response.error).toMatchObject({ + details: { reason: "session-changed" }, + }); + const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< + string, + { sessionId?: string; compactionCount?: number } + >; + expect(store["agent:main:main"]?.sessionId).toBe("sess-replacement"); + expect(store["agent:main:main"]?.compactionCount).toBeUndefined(); + ws.close(); +}); + +test("sessions.reset waits for terminal compaction before replacing the session", async () => { + const { dir, storePath } = await createSessionStoreDir(); + await fs.writeFile( + path.join(dir, "sess-compact-reset.jsonl"), + `${JSON.stringify({ role: "user", content: "hello" })}\n`, + "utf-8", + ); + await writeSessionStore({ entries: { main: sessionStoreEntry("sess-compact-reset") } }); + const compaction = createDeferred<{ + ok: true; + compacted: true; + result: { + summary: string; + firstKeptEntryId: string; + tokensBefore: number; + tokensAfter: number; + }; + }>(); + embeddedRunMock.compactEmbeddedAgentSession.mockReturnValueOnce(compaction.promise); + + const { ws } = await openClient(); + const compactResult = rpcReq(ws, "sessions.compact", { key: "main" }); + await vi.waitFor(() => { + expect(embeddedRunMock.compactEmbeddedAgentSession).toHaveBeenCalledTimes(1); + }); + let resetSettled = false; + const resetResult = rpcReq<{ entry: { sessionId: string } }>(ws, "sessions.reset", { + key: "main", + }).finally(() => { + resetSettled = true; + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + expect(resetSettled).toBe(false); + + compaction.resolve({ + ok: true, + compacted: true, + result: { + summary: "summary", + firstKeptEntryId: "entry-1", + tokensBefore: 120, + tokensAfter: 80, + }, + }); + expect((await compactResult).ok).toBe(true); + const reset = await resetResult; + expect(reset.ok).toBe(true); + const resetSessionId = reset.payload?.entry.sessionId; + expect(resetSessionId).toBeTruthy(); + expect(resetSessionId).not.toBe("sess-compact-reset"); + const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< + string, + { sessionId?: string } + >; + expect(store["agent:main:main"]?.sessionId).toBe(resetSessionId); + ws.close(); +}); + +test("sessions.compaction.restore waits for terminal compaction before replacing the session", async () => { + const { dir } = await createSessionStoreDir(); + const fixture = await createCheckpointFixture(dir, { legacyPreCompactionSnapshot: false }); + const checkpointEntry = compactionCheckpointEntry(fixture, { + checkpointId: "checkpoint-race", + sessionKey: "agent:main:main", + createdAt: Date.now(), + reason: "manual", + summary: "checkpoint summary", + tokensBefore: 123, + tokensAfter: 45, + }); + await writeSessionStore({ + entries: { + main: sessionStoreEntry(fixture.sessionId, { + sessionFile: fixture.sessionFile, + compactionCheckpoints: [checkpointEntry], + }), + }, + }); + const compaction = createDeferred<{ + ok: true; + compacted: true; + result: { + summary: string; + firstKeptEntryId: string; + tokensBefore: number; + tokensAfter: number; + }; + }>(); + embeddedRunMock.compactEmbeddedAgentSession.mockReturnValueOnce(compaction.promise); + + const { ws } = await openClient(); + const compactResult = rpcReq(ws, "sessions.compact", { key: "main" }); + await vi.waitFor(() => { + expect(embeddedRunMock.compactEmbeddedAgentSession).toHaveBeenCalledTimes(1); + }); + let restoreSettled = false; + const restoreResult = rpcReq<{ sessionId: string }>(ws, "sessions.compaction.restore", { + key: "main", + checkpointId: "checkpoint-race", + }).finally(() => { + restoreSettled = true; + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + expect(restoreSettled).toBe(false); + + compaction.resolve({ + ok: true, + compacted: true, + result: { + summary: "summary", + firstKeptEntryId: "entry-1", + tokensBefore: 123, + tokensAfter: 45, + }, + }); + expect((await compactResult).ok).toBe(true); + const restored = await restoreResult; + expect(restored.ok).toBe(true); + expect(restored.payload?.sessionId).toBeTruthy(); + expect(restored.payload?.sessionId).not.toBe(fixture.sessionId); + ws.close(); +}); + +test("sessions.compaction.restore leaves replacement-session work untouched when queued state is stale", async () => { + const { dir, storePath } = await createSessionStoreDir(); + const fixture = await createCheckpointFixture(dir, { legacyPreCompactionSnapshot: false }); + const checkpointEntry = compactionCheckpointEntry(fixture, { + checkpointId: "checkpoint-stale-restore", + sessionKey: "agent:main:main", + createdAt: Date.now(), + reason: "manual", + summary: "checkpoint summary", + }); + await writeSessionStore({ + entries: { + main: sessionStoreEntry(fixture.sessionId, { + sessionFile: fixture.sessionFile, + compactionCheckpoints: [checkpointEntry], + }), + }, + }); + const replacementSessionId = "sess-replacement-after-restore-queued"; + let replacementInterrupted = false; + const replacementAdmission = await beginSessionWorkAdmission({ + scope: storePath, + identities: ["agent:main:main", replacementSessionId], + assertAllowed: () => {}, + onInterrupt: () => { + replacementInterrupted = true; + }, + }); + const blockerStarted = createDeferred(); + const releaseBlocker = createDeferred(); + const blocker = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: ["main", "agent:main:main", fixture.sessionId], + run: async () => { + blockerStarted.resolve(); + await releaseBlocker.promise; + }, + }); + await blockerStarted.promise; + + const { ws } = await openClient(); + const restore = rpcReq(ws, "sessions.compaction.restore", { + key: "main", + checkpointId: "checkpoint-stale-restore", + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + await writeSessionStore({ entries: { main: sessionStoreEntry(replacementSessionId) } }); + + try { + releaseBlocker.resolve(); + await blocker; + await expect(restore).resolves.toMatchObject({ + ok: false, + error: { + code: "INVALID_REQUEST", + details: { reason: "session-changed" }, + }, + }); + expect(replacementInterrupted).toBe(false); + } finally { + releaseBlocker.resolve(); + replacementAdmission.release(); + await blocker; + ws.close(); + } +}); + +test("sessions.compact blocks new work admission through terminal persistence", async () => { + const { dir, storePath } = await createSessionStoreDir(); + const sessionId = "sess-compact-admission"; + await fs.writeFile( + path.join(dir, `${sessionId}.jsonl`), + `${JSON.stringify({ role: "user", content: "hello" })}\n`, + "utf-8", + ); + await writeSessionStore({ entries: { main: sessionStoreEntry(sessionId) } }); + const compaction = createDeferred<{ + ok: true; + compacted: true; + result: { + summary: string; + firstKeptEntryId: string; + tokensBefore: number; + tokensAfter: number; + }; + }>(); + embeddedRunMock.compactEmbeddedAgentSession.mockReturnValueOnce(compaction.promise); + + const { ws } = await openClient(); + const compactResult = rpcReq(ws, "sessions.compact", { key: "main" }); + await vi.waitFor(() => { + expect(embeddedRunMock.compactEmbeddedAgentSession).toHaveBeenCalledTimes(1); + }); + + let admitted = false; + const pendingAdmission = beginSessionWorkAdmission({ + scope: storePath, + identities: ["agent:main:main", sessionId], + assertAllowed: () => {}, + }).then((lease) => { + admitted = true; + return lease; + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + expect(admitted).toBe(false); + + compaction.resolve({ + ok: true, + compacted: true, + result: { + summary: "summary", + firstKeptEntryId: "entry-1", + tokensBefore: 120, + tokensAfter: 80, + }, + }); + expect((await compactResult).ok).toBe(true); + const admission = await pendingAdmission; + try { + expect(admitted).toBe(true); + } finally { + admission.release(); + } + ws.close(); +}); + +test("sessions.compact clears queued work before draining an active admission", async () => { + const { dir, storePath } = await createSessionStoreDir(); + const sessionId = "sess-compact-queued-work"; + await fs.writeFile( + path.join(dir, `${sessionId}.jsonl`), + `${JSON.stringify({ role: "user", content: "hello" })}\n`, + "utf-8", + ); + await writeSessionStore({ entries: { main: sessionStoreEntry(sessionId) } }); + embeddedRunMock.compactEmbeddedAgentSession.mockResolvedValueOnce({ + ok: true, + compacted: true, + result: { + summary: "summary", + firstKeptEntryId: "entry-1", + tokensBefore: 120, + tokensAfter: 80, + }, + }); + + let releaseAdmission = () => {}; + const admission = await beginSessionWorkAdmission({ + scope: storePath, + identities: ["main", "agent:main:main", sessionId], + assertAllowed: () => {}, + onInterrupt: () => releaseAdmission(), + }); + releaseAdmission = admission.release; + + const { ws } = await openClient(); + try { + const compacted = await rpcReq(ws, "sessions.compact", { key: "main" }); + + expect(compacted.ok).toBe(true); + expectSessionQueueCleanup(["main", "agent:main:main", sessionId]); + } finally { + admission.release(); + ws.close(); + } +}); + +test("sessions.patch rejects archive while terminal compaction owns the session", async () => { + const { dir } = await createSessionStoreDir(); + const sessionKey = "agent:main:dashboard:compact-race"; + await fs.writeFile( + path.join(dir, "sess-compact-archive.jsonl"), + `${JSON.stringify({ role: "user", content: "hello" })}\n`, + "utf-8", + ); + await writeSessionStore({ + entries: { [sessionKey]: sessionStoreEntry("sess-compact-archive") }, + }); + const compaction = createDeferred<{ + ok: true; + compacted: true; + result: { + summary: string; + firstKeptEntryId: string; + tokensBefore: number; + tokensAfter: number; + }; + }>(); + embeddedRunMock.compactEmbeddedAgentSession.mockReturnValueOnce(compaction.promise); + + const { ws } = await openClient(); + const compactResult = rpcReq(ws, "sessions.compact", { key: sessionKey }); + await vi.waitFor(() => { + expect(embeddedRunMock.compactEmbeddedAgentSession).toHaveBeenCalledTimes(1); + }); + const archived = await rpcReq(ws, "sessions.patch", { key: sessionKey, archived: true }); + expect(archived.ok).toBe(false); + expect(archived.error?.message).toContain("active run"); + + compaction.resolve({ + ok: true, + compacted: true, + result: { + summary: "summary", + firstKeptEntryId: "entry-1", + tokensBefore: 120, + tokensAfter: 80, + }, + }); + expect((await compactResult).ok).toBe(true); + ws.close(); +}); + test("sessions.compact maxLines truncates the transcript on disk and archives the original to .bak", async () => { const { dir } = await createSessionStoreDir(); const transcriptPath = path.join(dir, "sess-main.jsonl"); diff --git a/src/gateway/server.sessions.delete-lifecycle.test.ts b/src/gateway/server.sessions.delete-lifecycle.test.ts index f8f8207974eb..8287b19df6a9 100644 --- a/src/gateway/server.sessions.delete-lifecycle.test.ts +++ b/src/gateway/server.sessions.delete-lifecycle.test.ts @@ -7,6 +7,11 @@ import { readAcpSessionMeta, writeAcpSessionMetaForMigration, } from "../acp/runtime/session-meta.js"; +import { loadSessionStore } from "../config/sessions/store.js"; +import { + beginSessionWorkAdmission, + runExclusiveSessionLifecycleMutation, +} from "../sessions/session-lifecycle-admission.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import { embeddedRunMock, rpcReq, writeSessionStore } from "./test-helpers.js"; import { @@ -46,6 +51,9 @@ type SessionDeleteRequest = { agentId?: string; deleteTranscript?: boolean; emitLifecycleHooks?: boolean; + expectedSessionId?: string; + expectedLifecycleRevision?: string; + expectedSessionUpdatedAt?: number; }; async function expectSessionDeleteSucceeds(request: SessionDeleteRequest) { @@ -134,8 +142,385 @@ test("sessions.delete rejects main and aborts active runs", async () => { }); }); +test("sessions.delete interrupts work admitted before runtime registration", async () => { + const { storePath } = await createSessionStoreDir(); + await writeSessionStore({ + entries: { + "agent:main:subagent:worker": sessionStoreEntry("sess-subagent"), + }, + }); + let interrupted = false; + let releaseAdmission = () => {}; + const admissionLease = await beginSessionWorkAdmission({ + scope: storePath, + identities: ["agent:main:subagent:worker", "sess-subagent"], + assertAllowed: () => {}, + onInterrupt: () => { + interrupted = true; + releaseAdmission(); + }, + }); + releaseAdmission = admissionLease.release; + + const deleted = await expectSessionDeleteSucceeds({ + key: "agent:main:subagent:worker", + }); + + expect(deleted.payload?.deleted).toBe(true); + expect(interrupted).toBe(true); +}); + +test("sessions.delete rejects a stale expected session id without interrupting its replacement", async () => { + const { storePath } = await createSessionStoreDir(); + const sessionKey = "agent:main:subagent:worker"; + const replacementSessionId = "sess-replacement"; + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(replacementSessionId), + }, + }); + let interrupted = false; + const admission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, replacementSessionId], + assertAllowed: () => {}, + onInterrupt: () => { + interrupted = true; + }, + }); + + try { + const deleted = await directSessionReq("sessions.delete", { + key: sessionKey, + expectedSessionId: "sess-stale", + }); + expect(deleted.ok).toBe(false); + expect(deleted.error?.message).toBe(`Session ${sessionKey} changed before deletion. Retry.`); + expect((deleted.error as { details?: unknown } | undefined)?.details).toEqual({ + reason: "session-changed", + }); + expect(interrupted).toBe(false); + } finally { + admission.release(); + } +}); + +test("sessions.delete rechecks its expected id before interrupting replacement work", async () => { + const { storePath } = await createSessionStoreDir(); + const sessionKey = "agent:main:subagent:worker"; + const originalSessionId = "sess-original"; + const replacementSessionId = "sess-replacement"; + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(originalSessionId), + }, + }); + let replacementInterrupted = false; + const replacementAdmission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, replacementSessionId], + assertAllowed: () => {}, + onInterrupt: () => { + replacementInterrupted = true; + }, + }); + let releaseBlockingMutation = () => {}; + let markBlockingMutationStarted = () => {}; + const blockingMutationStarted = new Promise((resolve) => { + markBlockingMutationStarted = resolve; + }); + const blockingMutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey], + run: async () => { + markBlockingMutationStarted(); + await new Promise((release) => { + releaseBlockingMutation = release; + }); + }, + }); + await blockingMutationStarted; + + const deletion = directSessionReq("sessions.delete", { + key: sessionKey, + expectedSessionId: originalSessionId, + }); + await Promise.resolve(); + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(replacementSessionId), + }, + }); + releaseBlockingMutation(); + + try { + const [deleted] = await Promise.all([deletion, blockingMutation]); + expect(deleted.ok).toBe(false); + expect(replacementInterrupted).toBe(false); + } finally { + replacementAdmission.release(); + } +}); + +test("sessions.delete accepts a matching lifecycle revision for a non-resumable stub", async () => { + const sessionKey = "agent:main:cron:cleanup"; + const lifecycleRevision = "run-revision"; + const updatedAt = 1_737_600_000_000; + await createSessionStoreDir(); + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry("discarded-before-persist", { + sessionId: undefined, + lifecycleRevision, + updatedAt, + }), + }, + }); + + const deleted = await expectSessionDeleteSucceeds({ + key: sessionKey, + expectedSessionId: "in-memory-run-id", + expectedLifecycleRevision: lifecycleRevision, + expectedSessionUpdatedAt: updatedAt, + }); + + expect(deleted.payload?.deleted).toBe(true); +}); + +test("sessions.delete rejects an ID-less replacement with the same updated-at timestamp", async () => { + const sessionKey = "agent:main:cron:cleanup"; + const updatedAt = 1_737_600_000_000; + const { storePath } = await createSessionStoreDir(); + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry("discarded-before-persist", { + sessionId: undefined, + lifecycleRevision: "replacement-revision", + updatedAt, + }), + }, + }); + let interrupted = false; + const admission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, "replacement-run"], + assertAllowed: () => {}, + onInterrupt: () => { + interrupted = true; + }, + }); + + try { + const deleted = await directSessionReq("sessions.delete", { + key: sessionKey, + expectedSessionId: "stale-run", + expectedLifecycleRevision: "stale-revision", + expectedSessionUpdatedAt: updatedAt, + }); + + expect(deleted.ok).toBe(false); + expect(interrupted).toBe(false); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toMatchObject({ + lifecycleRevision: "replacement-revision", + updatedAt, + }); + } finally { + admission.release(); + } +}); + +test("sessions.delete includes cleanup-owned row changes in its guarded deletion", async () => { + const sessionKey = "agent:main:cron:cleanup"; + const sessionId = "sess-cleanup"; + const lifecycleRevision = "cleanup-revision"; + const updatedAt = 1_737_600_000_000; + const { storePath } = await createSessionStoreDir(); + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(sessionId, { lifecycleRevision, updatedAt }), + }, + }); + bundleMcpRuntimeMocks.disposeSessionMcpRuntime.mockImplementationOnce(async () => { + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(sessionId, { + label: "cleanup-owned revision", + lifecycleRevision, + updatedAt: updatedAt + 1, + }), + }, + }); + }); + + const deleted = await expectSessionDeleteSucceeds({ + key: sessionKey, + expectedSessionId: sessionId, + expectedLifecycleRevision: lifecycleRevision, + expectedSessionUpdatedAt: updatedAt, + }); + + expect(deleted.payload?.deleted).toBe(true); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toBeUndefined(); +}); + +test("sessions.delete serializes a patch behind asynchronous runtime cleanup", async () => { + const sessionKey = "agent:main:subagent:worker"; + const sessionId = "sess-subagent"; + const updatedAt = 1_737_600_000_000; + const { storePath } = await createSessionStoreDir(); + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(sessionId, { updatedAt }), + }, + }); + let releaseRuntimeCleanup = () => {}; + const runtimeCleanupStarted = new Promise((resolve) => { + bundleMcpRuntimeMocks.disposeSessionMcpRuntime.mockImplementationOnce(async () => { + resolve(); + await new Promise((release) => { + releaseRuntimeCleanup = release; + }); + }); + }); + + const deletion = directSessionReq("sessions.delete", { + key: sessionKey, + expectedSessionId: sessionId, + expectedSessionUpdatedAt: updatedAt, + }); + await runtimeCleanupStarted; + let patchSettled = false; + const patch = directSessionReq("sessions.patch", { + key: sessionKey, + label: "updated during cleanup", + }).then((result) => { + patchSettled = true; + return result; + }); + await Promise.resolve(); + expect(patchSettled).toBe(false); + releaseRuntimeCleanup(); + + const [deleted, patched] = await Promise.all([deletion, patch]); + expect(deleted.ok).toBe(true); + expect(patched.ok).toBe(false); + expect(patched.error?.message).toBe(`Session ${sessionKey} changed before patch. Retry.`); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]).toBeUndefined(); +}); + +test("sessions.patch waits for an in-flight session lifecycle mutation", async () => { + const { storePath } = await createSessionStoreDir(); + const sessionKey = "agent:main:subagent:worker"; + const sessionId = "sess-subagent"; + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(sessionId), + }, + }); + let releaseMutation = () => {}; + let markMutationStarted = () => {}; + const mutationStarted = new Promise((resolve) => { + markMutationStarted = resolve; + }); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + run: async () => { + markMutationStarted(); + await new Promise((release) => { + releaseMutation = release; + }); + }, + }); + await mutationStarted; + let patchSettled = false; + const patch = directSessionReq("sessions.patch", { + key: sessionKey, + label: "after lifecycle mutation", + }).then((result) => { + patchSettled = true; + return result; + }); + await Promise.resolve(); + expect(patchSettled).toBe(false); + releaseMutation(); + + const [patched] = await Promise.all([patch, mutation]); + expect(patched.ok).toBe(true); + expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]?.label).toBe( + "after lifecycle mutation", + ); +}); + +test("sessions.delete keeps lifecycle admission blocked through session unbinding", async () => { + const { storePath } = await createSessionStoreDir(); + const sessionKey = "agent:main:subagent:worker"; + const sessionId = "sess-subagent"; + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(sessionId), + }, + }); + let releaseUnbind = () => {}; + const unbindStarted = new Promise((resolve) => { + threadBindingMocks.unbindThreadBindingsBySessionKey.mockImplementationOnce(async () => { + resolve(); + await new Promise((release) => { + releaseUnbind = release; + }); + return []; + }); + }); + + const deletion = directSessionReq<{ ok: true; deleted: boolean }>("sessions.delete", { + key: sessionKey, + }); + await unbindStarted; + let replacementAdmitted = false; + const replacement = beginSessionWorkAdmission({ + scope: storePath, + identities: [sessionKey, sessionId], + assertAllowed: () => {}, + }).then((lease) => { + replacementAdmitted = true; + return lease; + }); + await Promise.resolve(); + expect(replacementAdmitted).toBe(false); + + releaseUnbind(); + const [deleted, replacementAdmission] = await Promise.all([deletion, replacement]); + try { + expect(deleted.ok).toBe(true); + expect(deleted.payload?.deleted).toBe(true); + expect(replacementAdmitted).toBe(true); + } finally { + replacementAdmission.release(); + } +}); + +test("sessions.patch rejects archiving active runs", async () => { + await createSessionStoreDir(); + await writeSessionStore({ + entries: { + "discord:group:dev": sessionStoreEntry("sess-active"), + }, + }); + embeddedRunMock.activeIds.add("sess-active"); + + const archived = await directSessionReq("sessions.patch", { + key: "discord:group:dev", + archived: true, + }); + + expect(archived.ok).toBe(false); + expect(archived.error).toMatchObject({ + message: "Cannot archive a session with an active run.", + }); +}); + test("sessions.delete limits plugin-runtime cleanup to sessions owned by that plugin", async () => { - const { dir } = await createSessionStoreDir(); + const { dir, storePath } = await createSessionStoreDir(); await writeSingleLineSession(dir, "sess-owned", "owned"); await writeSingleLineSession(dir, "sess-foreign", "foreign"); @@ -158,18 +543,32 @@ test("sessions.delete limits plugin-runtime cleanup to sessions owned by that pl pluginRuntimeOwnerId: "memory-core", }, } as never; + let foreignWorkInterrupted = false; + const foreignAdmission = await beginSessionWorkAdmission({ + scope: storePath, + identities: ["agent:main:dreaming-narrative-foreign", "sess-foreign"], + assertAllowed: () => {}, + onInterrupt: () => { + foreignWorkInterrupted = true; + }, + }); - const denied = await directSessionReq( - "sessions.delete", - { - key: "agent:main:dreaming-narrative-foreign", - }, - { - client: pluginClient, - }, - ); - expect(denied.ok).toBe(false); - expect(denied.error?.message).toContain("did not create it"); + try { + const denied = await directSessionReq( + "sessions.delete", + { + key: "agent:main:dreaming-narrative-foreign", + }, + { + client: pluginClient, + }, + ); + expect(denied.ok).toBe(false); + expect(denied.error?.message).toContain("did not create it"); + expect(foreignWorkInterrupted).toBe(false); + } finally { + foreignAdmission.release(); + } const deleted = await directSessionReq<{ ok: true; deleted: boolean }>( "sessions.delete", diff --git a/src/gateway/server.sessions.list-changed.test.ts b/src/gateway/server.sessions.list-changed.test.ts index 0b195c0bd8b4..4de4acaed63e 100644 --- a/src/gateway/server.sessions.list-changed.test.ts +++ b/src/gateway/server.sessions.list-changed.test.ts @@ -727,6 +727,61 @@ test("sessions.changed mutation events include sendPolicy metadata", async () => }); }); +test("sessions.changed mutation events include session management metadata", async () => { + await createSessionStoreDir(); + await writeSessionStore({ + entries: { + "discord:group:dev": sessionStoreEntry("sess-dev", { pinnedAt: 10 }), + }, + }); + + const archived = await invokeSessionsPatch({ + key: "discord:group:dev", + archived: true, + }); + expectChangedBroadcast(archived.broadcastToConnIds, { + sessionKey: "agent:main:discord:group:dev", + reason: "patch", + archived: true, + archivedAt: expect.any(Number), + pinned: false, + pinnedAt: null, + }); + + const restored = await invokeSessionsPatch({ + key: "discord:group:dev", + archived: false, + }); + expectChangedBroadcast(restored.broadcastToConnIds, { + sessionKey: "agent:main:discord:group:dev", + reason: "patch", + archived: false, + archivedAt: null, + }); + + const pinned = await invokeSessionsPatch({ + key: "discord:group:dev", + pinned: true, + }); + expectChangedBroadcast(pinned.broadcastToConnIds, { + sessionKey: "agent:main:discord:group:dev", + reason: "patch", + pinned: true, + pinnedAt: expect.any(Number), + }); + + const unpinned = await invokeSessionsPatch({ + key: "discord:group:dev", + pinned: false, + }); + expectChangedBroadcast(unpinned.broadcastToConnIds, { + sessionKey: "agent:main:discord:group:dev", + reason: "patch", + pinned: false, + pinnedAt: null, + }); +}); + test("sessions.patch scopes selected global mutations and events to the requested agent", async () => { const globalStores = await createConfiguredGlobalAgentSessionStore({ writePrimeStore: true }); diff --git a/src/gateway/server.sessions.reset-cleanup.test.ts b/src/gateway/server.sessions.reset-cleanup.test.ts index 69761bea1753..bf7b486f9a3e 100644 --- a/src/gateway/server.sessions.reset-cleanup.test.ts +++ b/src/gateway/server.sessions.reset-cleanup.test.ts @@ -9,6 +9,11 @@ import { } from "../acp/runtime/session-meta.js"; import type { SessionAcpMeta } from "../config/sessions/types.js"; import { enqueueSystemEvent, peekSystemEvents } from "../infra/system-events.js"; +import { + beginSessionWorkAdmission, + runExclusiveSessionLifecycle, + runExclusiveSessionLifecycleMutation, +} from "../sessions/session-lifecycle-admission.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import { embeddedRunMock, testState, writeSessionStore } from "./test-helpers.js"; import { @@ -25,6 +30,9 @@ import { sessionStoreEntry, expectActiveRunCleanup, directSessionReq, + getGatewayConfigModule, + getSessionsHandlers, + sessionHookMocks, } from "./test/server-sessions.test-helpers.js"; const { createSessionStoreDir, seedActiveMainSession } = setupGatewaySessionsTestHarness(); @@ -181,6 +189,102 @@ test("sessions.reset aborts active runs and clears queues", async () => { }); }); +test("sessions.reset interrupts work admitted before runtime registration", async () => { + const { storePath } = await seedActiveMainSession(); + let interrupted = false; + let releaseAdmission = () => {}; + const admissionLease = await beginSessionWorkAdmission({ + scope: storePath, + identities: ["agent:main:main", "sess-main"], + assertAllowed: () => {}, + onInterrupt: () => { + interrupted = true; + releaseAdmission(); + }, + }); + releaseAdmission = admissionLease.release; + + const reset = await resetMainSession(); + expect(reset.ok).toBe(true); + expect(interrupted).toBe(true); +}); + +test("sessions.reset does not interrupt the admission that initiates it", async () => { + const { storePath } = await seedActiveMainSession(); + let interrupted = false; + const admissionLease = await beginSessionWorkAdmission({ + scope: storePath, + identities: ["agent:main:main", "sess-main"], + assertAllowed: () => {}, + onInterrupt: () => { + interrupted = true; + }, + }); + + try { + const reset = await admissionLease.run(resetMainSession); + expect(reset.ok).toBe(true); + expect(interrupted).toBe(false); + } finally { + admissionLease.release(); + } +}); + +test("sessions.reset rejects an active lifecycle mutation without interrupting admitted work", async () => { + const { storePath } = await seedActiveMainSession(); + let interrupted = false; + const admissionLease = await beginSessionWorkAdmission({ + scope: storePath, + identities: ["agent:main:main", "sess-main"], + assertAllowed: () => {}, + onInterrupt: () => { + interrupted = true; + }, + }); + let releaseMutation = () => {}; + let markMutationStarted = () => {}; + const mutationStarted = new Promise((resolve) => { + markMutationStarted = resolve; + }); + const blocker = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: ["agent:main:main", "sess-main"], + run: async () => { + markMutationStarted(); + await new Promise((resolve) => { + releaseMutation = resolve; + }); + }, + }); + await mutationStarted; + const { performGatewaySessionReset } = await import("./session-reset-service.js"); + const assertCurrent = vi.fn(() => { + throw new Error("stale lifecycle"); + }); + const reset = await performGatewaySessionReset({ + key: "main", + reason: "reset", + commandSource: "gateway:agent", + assertCurrent, + }); + releaseMutation(); + + try { + await blocker; + expect(reset).toMatchObject({ + ok: false, + error: { + code: "UNAVAILABLE", + message: expect.stringContaining("lifecycle mutation in progress"), + }, + }); + expect(assertCurrent).not.toHaveBeenCalled(); + expect(interrupted).toBe(false); + } finally { + admissionLease.release(); + } +}); + test("sessions.reset skips browser cleanup when root browser support is disabled", async () => { await expectResetWithConfigSkipsBrowserCleanup({ browser: { enabled: false } }); }); @@ -307,6 +411,121 @@ test("sessions.reset finishes after lifecycle rotation during destructive cleanu expect(prepareFreshSession).not.toHaveBeenCalled(); }); +test("sessions.reset rejects a concurrent archive during lifecycle rotation", async () => { + const { dir, storePath } = await createSessionStoreDir(); + const sessionKey = "agent:main:subagent:archive-race"; + await writeSingleLineSession(dir, "sess-archive-race", "hello"); + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry("sess-archive-race"), + }, + }); + let releaseHook = () => {}; + const hookReleased = new Promise((resolve) => { + releaseHook = resolve; + }); + let markHookStarted = () => {}; + const hookStarted = new Promise((resolve) => { + markHookStarted = resolve; + }); + sessionHookMocks.triggerInternalHook.mockImplementationOnce(async () => { + markHookStarted(); + await hookReleased; + }); + const { performGatewaySessionReset } = await import("./session-reset-service.js"); + + const resetPromise = performGatewaySessionReset({ + key: sessionKey, + reason: "new", + commandSource: "gateway:sessions.reset", + }); + await hookStarted; + const archivePromise = directSessionReq("sessions.patch", { + key: sessionKey, + archived: true, + }); + releaseHook(); + + const [reset, archived] = await Promise.all([resetPromise, archivePromise]); + expect(reset.ok).toBe(true); + expect(archived).toMatchObject({ + ok: false, + error: { message: "Cannot archive a session with an active run." }, + }); + const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< + string, + { archivedAt?: number; sessionId?: string } + >; + expect(store[sessionKey]?.archivedAt).toBeUndefined(); + expect(store[sessionKey]?.sessionId).not.toBe("sess-archive-race"); +}); + +test.each([ + { initialSessionId: "sess-queued-archive-race", transition: "rotated" }, + { initialSessionId: undefined, transition: "created" }, +])("sessions.patch rejects an archive queued behind a $transition session", async (fixture) => { + const { storePath } = await createSessionStoreDir(); + const sessionKey = "agent:main:subagent:queued-archive-race"; + const replacementSessionId = "sess-after-queued-reset"; + await writeSessionStore({ + entries: fixture.initialSessionId + ? { [sessionKey]: sessionStoreEntry(fixture.initialSessionId) } + : {}, + }); + // Resolve the lazy handler/config imports before queue ordering begins. + await Promise.all([getSessionsHandlers(), getGatewayConfigModule()]); + let releaseBlocker = () => {}; + let markBlockerStarted = () => {}; + const blockerStarted = new Promise((resolve) => { + markBlockerStarted = resolve; + }); + const blocker = runExclusiveSessionLifecycle({ + scope: storePath, + identities: [sessionKey, fixture.initialSessionId], + run: async () => { + markBlockerStarted(); + await new Promise((resolve) => { + releaseBlocker = resolve; + }); + }, + }); + await blockerStarted; + const queuedReset = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, fixture.initialSessionId], + run: async () => { + await writeSessionStore({ + entries: { + [sessionKey]: sessionStoreEntry(replacementSessionId), + }, + }); + }, + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + const archivePromise = directSessionReq("sessions.patch", { + key: sessionKey, + archived: true, + }); + await new Promise((resolve) => { + setImmediate(resolve); + }); + releaseBlocker(); + + const archived = (await Promise.all([blocker, queuedReset, archivePromise]))[2]; + expect(archived).toMatchObject({ + ok: false, + error: { message: `Session ${sessionKey} changed before patch. Retry.` }, + }); + const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< + string, + { archivedAt?: number; sessionId?: string } + >; + expect(store[sessionKey]?.archivedAt).toBeUndefined(); + expect(store[sessionKey]?.sessionId).toBe(replacementSessionId); +}); + test("sessions.reset preserves a newer session after lifecycle rotation", async () => { const { dir, storePath } = await createSessionStoreDir(); await writeSingleLineSession(dir, "sess-main", "hello"); @@ -662,16 +881,17 @@ test("sessions.reset preserves explicit responseUsage preference across session await writeSingleLineSession(dir, "sess-main", "hello"); await writeSessionStore({ entries: { - main: sessionStoreEntry("sess-main", { responseUsage: "tokens" }), + main: sessionStoreEntry("sess-main", { responseUsage: "tokens", pinnedAt: 123 }), }, }); const reset = await directSessionReq<{ ok: true; key: string; - entry: { sessionId: string; responseUsage?: string }; + entry: { sessionId: string; responseUsage?: string; pinnedAt?: number }; }>("sessions.reset", { key: "main" }); expect(reset.ok).toBe(true); expect(reset.payload?.entry.responseUsage).toBe("tokens"); + expect(reset.payload?.entry.pinnedAt).toBe(123); }); diff --git a/src/gateway/server.sessions.store-rpc.test.ts b/src/gateway/server.sessions.store-rpc.test.ts index c6f41a574afa..d216282863a3 100644 --- a/src/gateway/server.sessions.store-rpc.test.ts +++ b/src/gateway/server.sessions.store-rpc.test.ts @@ -102,11 +102,12 @@ test("lists and patches session store via sessions.* RPC", async () => { const { getRuntimeConfig } = await getGatewayConfigModule(); const directContext = { broadcastToConnIds: vi.fn(), + dedupe: new Map(), getSessionEventSubscriberConnIds: () => new Set(), logGateway: { debug: vi.fn() }, loadGatewayModelCatalog: async () => agentDiscoveryMock.models, getRuntimeConfig, - } as never; + }; async function directSessionReq( method: keyof typeof sessionsHandlers, params: Record, @@ -134,7 +135,7 @@ test("lists and patches session store via sessions.* RPC", async () => { error, }; }, - context: directContext, + context: directContext as never, client: null, isWebchatConnect: () => false, }); @@ -241,6 +242,100 @@ test("lists and patches session store via sessions.* RPC", async () => { }); expect(labelPatchedDuplicate.ok).toBe(false); + const mainArchive = await directSessionReq("sessions.patch", { + key: "agent:main:main", + archived: true, + }); + expect(mainArchive.ok).toBe(false); + + const pinned = await directSessionReq<{ + entry: { pinnedAt?: number }; + }>("sessions.patch", { + key: "agent:main:subagent:one", + pinned: true, + }); + expect(pinned.ok).toBe(true); + expect(pinned.payload?.entry.pinnedAt).toEqual(expect.any(Number)); + + const pinnedList = await directSessionReq<{ + sessions: Array<{ key: string; pinned?: boolean }>; + }>("sessions.list", {}); + expect(pinnedList.payload?.sessions[0]).toMatchObject({ + key: "agent:main:subagent:one", + pinned: true, + }); + + const archived = await directSessionReq<{ + entry: { archivedAt?: number; pinnedAt?: number }; + }>("sessions.patch", { + key: "agent:main:subagent:one", + archived: true, + }); + expect(archived.ok).toBe(true); + expect(archived.payload?.entry.archivedAt).toEqual(expect.any(Number)); + expect(archived.payload?.entry.pinnedAt).toBeUndefined(); + + const activeAfterArchive = await directSessionReq<{ + sessions: Array<{ key: string }>; + }>("sessions.list", {}); + expect(activeAfterArchive.payload?.sessions.map((session) => session.key)).not.toContain( + "agent:main:subagent:one", + ); + const archivedList = await directSessionReq<{ + sessions: Array<{ key: string; archived?: boolean }>; + }>("sessions.list", { archived: true }); + expect(archivedList.payload?.sessions).toMatchObject([ + { key: "agent:main:subagent:one", archived: true }, + ]); + + const archivedSend = await directSessionReq("sessions.send", { + key: "agent:main:subagent:one", + message: "blocked while archived", + }); + expect(archivedSend).toMatchObject({ + ok: false, + error: { + message: + 'Session "agent:main:subagent:one" is archived. Restore it before starting new work.', + }, + }); + + const cachedArchivedRunId = "cached-before-archive"; + directContext.dedupe.set(`chat:${cachedArchivedRunId}`, { + ts: Date.now(), + ok: true, + payload: { runId: cachedArchivedRunId, status: "ok" }, + }); + const cachedArchivedSend = await directSessionReq("sessions.send", { + key: "agent:main:subagent:one", + message: "already completed before archive", + idempotencyKey: cachedArchivedRunId, + }); + expect(cachedArchivedSend).toMatchObject({ + ok: true, + payload: { runId: cachedArchivedRunId, status: "ok" }, + }); + + const archivedReset = await directSessionReq("sessions.reset", { + key: "agent:main:subagent:one", + }); + expect(archivedReset).toMatchObject({ + ok: false, + error: { + message: + 'Session "agent:main:subagent:one" is archived. Restore it before starting new work.', + }, + }); + + const restored = await directSessionReq<{ + entry: { archivedAt?: number }; + }>("sessions.patch", { + key: "agent:main:subagent:one", + archived: false, + }); + expect(restored.ok).toBe(true); + expect(restored.payload?.entry.archivedAt).toBeUndefined(); + const list2 = await directSessionReq<{ sessions: Array<{ key: string; diff --git a/src/gateway/session-compaction-checkpoints.test.ts b/src/gateway/session-compaction-checkpoints.test.ts index 57eb79924bbf..1d61ccc80efa 100644 --- a/src/gateway/session-compaction-checkpoints.test.ts +++ b/src/gateway/session-compaction-checkpoints.test.ts @@ -633,7 +633,7 @@ describe("session-compaction-checkpoints", () => { ]); }); - test("file-backed checkpoint store restores from the stored transcript boundary", async () => { + test("file-backed checkpoint store branches active state and restores source management state", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-checkpoint-store-")); tempDirs.push(dir); @@ -658,10 +658,13 @@ describe("session-compaction-checkpoints", () => { const sessionFile = requireNonEmptyString(session.getSessionFile(), "session file missing"); const storePath = path.join(dir, "sessions.json"); + const managedAt = Date.now() - 2; await writeSessionStore(storePath, MAIN_SESSION_KEY, { sessionId: "current-session", sessionFile, updatedAt: Date.now() - 1, + archivedAt: managedAt, + pinnedAt: managedAt, totalTokens: 200, compactionCheckpoints: [ { @@ -681,6 +684,19 @@ describe("session-compaction-checkpoints", () => { ], }); const store = createFileBackedCompactionCheckpointStore(); + const branched = await store.branchCheckpointSession({ + storePath, + sourceKey: MAIN_SESSION_KEY, + nextKey: "agent:main:dashboard:checkpoint-branch", + checkpointId: "checkpoint-1", + }); + + if (branched.status !== "created") { + throw new Error("expected branched checkpoint transcript"); + } + expect(branched.entry.archivedAt).toBeUndefined(); + expect(branched.entry.pinnedAt).toBeUndefined(); + const restored = await store.restoreCheckpointSession({ storePath, sessionKey: MAIN_SESSION_KEY, @@ -691,6 +707,8 @@ describe("session-compaction-checkpoints", () => { throw new Error("expected restored checkpoint transcript"); } expect(restored.entry.totalTokens).toBe(45); + expect(restored.entry.archivedAt).toBe(managedAt); + expect(restored.entry.pinnedAt).toBe(managedAt); const restoredSessionFile = requireNonEmptyString( restored.entry.sessionFile, "restored session file missing", diff --git a/src/gateway/session-compaction-checkpoints.ts b/src/gateway/session-compaction-checkpoints.ts index 90898ea01910..3c8b74925460 100644 --- a/src/gateway/session-compaction-checkpoints.ts +++ b/src/gateway/session-compaction-checkpoints.ts @@ -542,6 +542,7 @@ function cloneCheckpointSessionEntry(params: { parentSessionKey?: string; totalTokens?: number; preserveCompactionCheckpoints?: boolean; + preserveManagementState?: boolean; }): SessionEntry { return { ...params.currentEntry, @@ -569,6 +570,8 @@ function cloneCheckpointSessionEntry(params: { : undefined, label: params.label ?? params.currentEntry.label, parentSessionKey: params.parentSessionKey ?? params.currentEntry.parentSessionKey, + archivedAt: params.preserveManagementState ? params.currentEntry.archivedAt : undefined, + pinnedAt: params.preserveManagementState ? params.currentEntry.pinnedAt : undefined, compactionCheckpoints: params.preserveCompactionCheckpoints ? params.currentEntry.compactionCheckpoints : undefined, @@ -619,6 +622,7 @@ async function restoreCheckpointSessionFromStoredBoundary( nextSessionFile: forkedTranscript.sessionFile, totalTokens: forkedTranscript.totalTokens, preserveCompactionCheckpoints: true, + preserveManagementState: true, }), }); } diff --git a/src/gateway/session-event-payload.ts b/src/gateway/session-event-payload.ts index 9c227df01a86..331a570111a7 100644 --- a/src/gateway/session-event-payload.ts +++ b/src/gateway/session-event-payload.ts @@ -20,6 +20,10 @@ export function buildGatewaySessionEventFields(params: { space: sessionRow.space, chatType: sessionRow.chatType, origin: sessionRow.origin, + archived: sessionRow.archived ?? false, + archivedAt: sessionRow.archivedAt ?? null, + pinned: sessionRow.pinned ?? false, + pinnedAt: sessionRow.pinnedAt ?? null, spawnedBy: sessionRow.spawnedBy, spawnedWorkspaceDir: sessionRow.spawnedWorkspaceDir, spawnedCwd: sessionRow.spawnedCwd, diff --git a/src/gateway/session-reset-service.ts b/src/gateway/session-reset-service.ts index e52d1baac94c..8e8f4100c974 100644 --- a/src/gateway/session-reset-service.ts +++ b/src/gateway/session-reset-service.ts @@ -28,6 +28,7 @@ import { clearSessionResetRuntimeState } from "../auto-reply/reply/session-reset import { cleanupBrowserSessionsForLifecycleEnd } from "../browser-lifecycle-cleanup.js"; import { getRuntimeConfig } from "../config/io.js"; import { + resolveSessionWorkStartError, snapshotSessionOrigin, type SessionEntry, resetSessionEntryLifecycle, @@ -52,6 +53,13 @@ import { normalizeAgentId, parseAgentSessionKey, } from "../routing/session-key.js"; +import { + hasOnlySessionLifecycleMutationKindActive, + interruptSessionWorkAdmissions, + isSessionLifecycleMutationActive, + runExclusiveSessionLifecycleMutation, + SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS, +} from "../sessions/session-lifecycle-admission.js"; import { forgetActiveSessionForShutdown, listActiveSessionsForShutdown, @@ -885,287 +893,351 @@ export async function performGatewaySessionReset(params: { if (!resetTarget.ok) { return resetTarget; } - const { cfg, target, storePath, requestedAgentId } = resetTarget; - const { entry, legacyKey, canonicalKey } = loadSessionEntry( + const initialResetEntry = loadSessionEntry( params.key, - requestedAgentId ? { agentId: requestedAgentId } : undefined, + resetTarget.requestedAgentId ? { agentId: resetTarget.requestedAgentId } : undefined, + ).entry; + const resetLifecycleIdentities = [ + resetTarget.target.canonicalKey, + params.key, + initialResetEntry?.sessionId, + ]; + const activeLifecycleMutation = isSessionLifecycleMutationActive( + resetTarget.storePath, + resetLifecycleIdentities, ); - const hadExistingEntry = Boolean(entry); - const agentId = normalizeAgentId(target.agentId ?? resolveDefaultAgentId(cfg)); - const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); - const resetPluginRegistry = getActivePluginRegistry(); - const isResetLifecycleCurrent = () => { - try { + const activeCompaction = hasOnlySessionLifecycleMutationKindActive( + resetTarget.storePath, + resetLifecycleIdentities, + "compaction", + ); + if (activeLifecycleMutation && !activeCompaction) { + return { + ok: false, + error: errorShape( + ErrorCodes.UNAVAILABLE, + `Session ${params.key} has another lifecycle mutation in progress; try again.`, + ), + }; + } + let admittedWorkReleased = true; + return await runExclusiveSessionLifecycleMutation({ + scope: resetTarget.storePath, + identities: resetLifecycleIdentities, + // Mark the mutation first, then interrupt outside the identity lock. This + // lets aborted runs finish admission cleanup without deadlocking reset. + prepare: async () => { params.assertCurrent?.(); - return true; - } catch { - return false; - } - }; - let deferredAcpResetState: { sessionKey: string; meta: SessionAcpMeta } | undefined; - const hookEvent = createInternalHookEvent( - "command", - params.reason, - target.canonicalKey ?? params.key, - { - sessionEntry: entry, - previousSessionEntry: entry, - commandSource: params.commandSource, - cfg, - workspaceDir, + admittedWorkReleased = await interruptSessionWorkAdmissions({ + scope: resetTarget.storePath, + identities: resetLifecycleIdentities, + timeoutMs: SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS, + }); }, - ); - params.assertCurrent?.(); - await triggerInternalHook(hookEvent); - params.assertCurrent?.(); - // Cleanup below is destructive. Once it starts, finish rotating the same - // session even if gateway ownership changes; otherwise runtime state can be - // reset while the persisted session still points at the old conversation. - const runtimeCleanupError = await ensureSessionRuntimeCleanup({ - cfg, - key: params.key, - target, - sessionId: entry?.sessionId, - }); - if (runtimeCleanupError) { - return { ok: false, error: runtimeCleanupError }; - } - const parentSessionKey = target.canonicalKey ?? canonicalKey ?? params.key; - const parentAcpError = await closeAcpRuntimeForSession({ - cfg, - sessionKey: parentSessionKey, - fallbackSessionKeys: [canonicalKey, legacyKey, params.key], - reason: "session-reset", - deferResetState: true, - onDeferredResetState: (state) => { - deferredAcpResetState = state; - }, - }); - if (parentAcpError) { - return { ok: false, error: parentAcpError }; - } - const pluginCleanup = await runPluginHostCleanup({ - cfg, - registry: resetPluginRegistry, - reason: "reset", - sessionKey: target.canonicalKey ?? params.key, - skipPersistentSessionState: true, - }); - for (const failure of pluginCleanup.failures) { - logVerbose( - `plugin host cleanup failed for ${failure.pluginId}/${failure.hookId}: ${String(failure.error)}`, - ); - } - await closeChildAcpRuntimesForParent({ - cfg, - parentKey: target.canonicalKey ?? canonicalKey ?? params.key, - reason: "session-reset", - }); - - const lifecycle = await resetSessionEntryLifecycle({ - agentId: target.agentId, - storePath, - target: { - canonicalKey: target.canonicalKey, - storeKeys: target.storeKeys, - }, - buildNextEntry: ({ currentEntry, primaryKey }) => { - if (!isResetLifecycleCurrent() && currentEntry?.sessionId !== entry?.sessionId) { - // A newer owner already replaced or removed the session while cleanup - // targeted the old id. Preserve that newer state instead of resetting it. - params.assertCurrent?.(); + run: async () => { + const { cfg, target, storePath, requestedAgentId } = resetTarget; + if (!admittedWorkReleased) { + return { + ok: false, + error: errorShape( + ErrorCodes.UNAVAILABLE, + `Session ${params.key} is still active; try again in a moment.`, + ), + }; } - const parsed = parseAgentSessionKey(primaryKey); - const sessionAgentId = normalizeAgentId( - parsed?.agentId ?? target.agentId ?? requestedAgentId ?? resolveDefaultAgentId(cfg), + const { entry, legacyKey, canonicalKey } = loadSessionEntry( + params.key, + requestedAgentId ? { agentId: requestedAgentId } : undefined, ); - const resetPreservedSelection = resolveResetPreservedSelection({ - entry: currentEntry, - }); - const resetEntry = { - ...stripRuntimeModelState(currentEntry), - providerOverride: undefined, - modelOverride: undefined, - modelOverrideSource: undefined, - authProfileOverride: undefined, - authProfileOverrideSource: undefined, - authProfileOverrideCompactionCount: undefined, - ...resetPreservedSelection, - }; - const resolvedModel = resolveSessionModelRef(cfg, resetEntry, sessionAgentId); - const now = Date.now(); - const nextSessionId = randomUUID(); - const sessionFile = resolveResetSessionFile({ - nextSessionId, - currentEntry, - storePath, - agentId: sessionAgentId, - }); - const nextEntry: SessionEntry = { - sessionId: nextSessionId, - sessionFile, - updatedAt: now, - systemSent: false, - abortedLastRun: false, - thinkingLevel: currentEntry?.thinkingLevel, - fastMode: currentEntry?.fastMode, - verboseLevel: currentEntry?.verboseLevel, - traceLevel: currentEntry?.traceLevel, - reasoningLevel: currentEntry?.reasoningLevel, - elevatedLevel: currentEntry?.elevatedLevel, - ttsAuto: currentEntry?.ttsAuto, - execHost: currentEntry?.execHost, - execSecurity: currentEntry?.execSecurity, - execAsk: currentEntry?.execAsk, - execNode: currentEntry?.execNode, - responseUsage: currentEntry?.responseUsage, - // Resets should keep the user's explicit selection, but clear any - // temporary fallback model that was pinned during the previous run. - ...resetPreservedSelection, - groupActivation: currentEntry?.groupActivation, - groupActivationNeedsSystemIntro: currentEntry?.groupActivationNeedsSystemIntro, - chatType: currentEntry?.chatType, - model: resolvedModel.model, - modelProvider: resolvedModel.provider, - contextTokens: resetEntry?.contextTokens, - compactionCount: currentEntry?.compactionCount, - compactionCheckpoints: currentEntry?.compactionCheckpoints, - sendPolicy: currentEntry?.sendPolicy, - queueMode: currentEntry?.queueMode, - queueDebounceMs: currentEntry?.queueDebounceMs, - queueCap: currentEntry?.queueCap, - queueDrop: currentEntry?.queueDrop, - spawnedBy: currentEntry?.spawnedBy, - spawnedWorkspaceDir: currentEntry?.spawnedWorkspaceDir, - spawnedCwd: currentEntry?.spawnedCwd, - parentSessionKey: currentEntry?.parentSessionKey, - forkedFromParent: currentEntry?.forkedFromParent, - spawnDepth: currentEntry?.spawnDepth, - subagentRole: currentEntry?.subagentRole, - subagentControlScope: currentEntry?.subagentControlScope, - label: currentEntry?.label, - displayName: currentEntry?.displayName, - channel: currentEntry?.channel, - groupId: currentEntry?.groupId, - subject: currentEntry?.subject, - groupChannel: currentEntry?.groupChannel, - space: currentEntry?.space, - origin: snapshotSessionOrigin(currentEntry), - deliveryContext: currentEntry?.deliveryContext, - cliSessionBindings: currentEntry?.cliSessionBindings, - cliSessionIds: currentEntry?.cliSessionIds, - claudeCliSessionId: currentEntry?.claudeCliSessionId, - lastChannel: currentEntry?.lastChannel, - lastTo: currentEntry?.lastTo, - lastAccountId: currentEntry?.lastAccountId, - lastThreadId: currentEntry?.lastThreadId, - // Do not carry the cached skills catalog across /new. Long-lived channel - // sessions (Signal DMs/groups in particular) otherwise keep advertising a - // stale block even after reset/restart, because the - // skills snapshot version is runtime-local and may reset to 0. - inputTokens: 0, - outputTokens: 0, - totalTokens: 0, - totalTokensFresh: true, - }; - // Drop CLI provider bindings so the next turn after reset starts a fresh - // CLI conversation on the provider side. Preserved only for spawned - // subagents (canonical `:subagent:` keys), where Tak Hoffman's fa56682b3ced - // regression fix intentionally protects CLI continuity for - // orchestration-driven resets. Non-subagent sessions that happen to set - // `parentSessionKey` (e.g. dashboard children) are not exempt. - if (!isSubagentSessionKey(primaryKey)) { - clearAllCliSessions(nextEntry); - } else { - nextEntry.cliSessionBindings = rebindCliSessionReseedReceiptsForReset( - nextEntry.cliSessionBindings, - nextSessionId, - ); + const archivedSessionError = resolveSessionWorkStartError(canonicalKey, entry); + if (archivedSessionError) { + return { + ok: false, + error: errorShape(ErrorCodes.INVALID_REQUEST, archivedSessionError), + }; } - return nextEntry; - }, - afterEntryMutation: async (mutation) => { - let committedAcpResetState: { sessionKey: string; meta: SessionAcpMeta } | undefined; - if (deferredAcpResetState) { - const identity = deferredAcpResetState.meta.identity; - if (identity?.state === "resolved" && (identity.acpxSessionId || identity.agentSessionId)) { - committedAcpResetState = { - sessionKey: deferredAcpResetState.sessionKey, - meta: buildPendingAcpMeta(deferredAcpResetState.meta, Date.now()), - }; - // The JSON session rotation and SQLite metadata cannot share a transaction. - // Bind captured ACP state before acknowledging the committed reset so the - // new session never observes an unreadable old-session row. - writeAcpSessionMetaForMigration({ - sessionKey: committedAcpResetState.sessionKey, - sessionId: mutation.nextEntry.sessionId, - meta: committedAcpResetState.meta, - }); - } - } - params.onCommitted?.({ - key: target.canonicalKey, - sessionId: mutation.nextEntry.sessionId, - }); - if (committedAcpResetState && isResetLifecycleCurrent()) { + const hadExistingEntry = Boolean(entry); + const agentId = normalizeAgentId(target.agentId ?? resolveDefaultAgentId(cfg)); + const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); + const resetPluginRegistry = getActivePluginRegistry(); + const isResetLifecycleCurrent = () => { try { - await getAcpRuntimeBackend( - (committedAcpResetState.meta.backend || cfg.acp?.backend || "").trim() || undefined, - )?.runtime.prepareFreshSession?.({ - sessionKey: committedAcpResetState.sessionKey, - }); - } catch (error) { - logVerbose( - `sessions.session-reset: ACP prepareFreshSession failed for ${committedAcpResetState.sessionKey}: ${String(error)}`, - ); + params.assertCurrent?.(); + return true; + } catch { + return false; } - } - await emitGatewayBeforeResetPluginHook({ + }; + let deferredAcpResetState: { sessionKey: string; meta: SessionAcpMeta } | undefined; + const hookEvent = createInternalHookEvent( + "command", + params.reason, + target.canonicalKey ?? params.key, + { + sessionEntry: entry, + previousSessionEntry: entry, + commandSource: params.commandSource, + cfg, + workspaceDir, + }, + ); + params.assertCurrent?.(); + await triggerInternalHook(hookEvent); + params.assertCurrent?.(); + // Cleanup below is destructive. Once it starts, finish rotating the same + // session even if gateway ownership changes; otherwise runtime state can be + // reset while the persisted session still points at the old conversation. + const runtimeCleanupError = await ensureSessionRuntimeCleanup({ cfg, key: params.key, target, - storePath, - entry: mutation.previousEntry, - reason: params.reason, + sessionId: entry?.sessionId, }); + if (runtimeCleanupError) { + return { ok: false, error: runtimeCleanupError }; + } + const parentSessionKey = target.canonicalKey ?? canonicalKey ?? params.key; + const parentAcpError = await closeAcpRuntimeForSession({ + cfg, + sessionKey: parentSessionKey, + fallbackSessionKeys: [canonicalKey, legacyKey, params.key], + reason: "session-reset", + deferResetState: true, + onDeferredResetState: (state) => { + deferredAcpResetState = state; + }, + }); + if (parentAcpError) { + return { ok: false, error: parentAcpError }; + } + const pluginCleanup = await runPluginHostCleanup({ + cfg, + registry: resetPluginRegistry, + reason: "reset", + sessionKey: target.canonicalKey ?? params.key, + skipPersistentSessionState: true, + }); + for (const failure of pluginCleanup.failures) { + logVerbose( + `plugin host cleanup failed for ${failure.pluginId}/${failure.hookId}: ${String(failure.error)}`, + ); + } + await closeChildAcpRuntimesForParent({ + cfg, + parentKey: target.canonicalKey ?? canonicalKey ?? params.key, + reason: "session-reset", + }); + + const lifecycle = await resetSessionEntryLifecycle({ + agentId: target.agentId, + storePath, + target: { + canonicalKey: target.canonicalKey, + storeKeys: target.storeKeys, + }, + buildNextEntry: ({ currentEntry, primaryKey }) => { + if (!isResetLifecycleCurrent() && currentEntry?.sessionId !== entry?.sessionId) { + // A newer owner already replaced or removed the session while cleanup + // targeted the old id. Preserve that newer state instead of resetting it. + params.assertCurrent?.(); + } + const parsed = parseAgentSessionKey(primaryKey); + const sessionAgentId = normalizeAgentId( + parsed?.agentId ?? target.agentId ?? requestedAgentId ?? resolveDefaultAgentId(cfg), + ); + const resetPreservedSelection = resolveResetPreservedSelection({ + entry: currentEntry, + }); + const resetEntry = { + ...stripRuntimeModelState(currentEntry), + providerOverride: undefined, + modelOverride: undefined, + modelOverrideSource: undefined, + authProfileOverride: undefined, + authProfileOverrideSource: undefined, + authProfileOverrideCompactionCount: undefined, + ...resetPreservedSelection, + }; + const resolvedModel = resolveSessionModelRef(cfg, resetEntry, sessionAgentId); + const now = Date.now(); + const nextSessionId = randomUUID(); + const sessionFile = resolveResetSessionFile({ + nextSessionId, + currentEntry, + storePath, + agentId: sessionAgentId, + }); + const nextEntry: SessionEntry = { + sessionId: nextSessionId, + sessionFile, + updatedAt: now, + systemSent: false, + abortedLastRun: false, + thinkingLevel: currentEntry?.thinkingLevel, + fastMode: currentEntry?.fastMode, + verboseLevel: currentEntry?.verboseLevel, + traceLevel: currentEntry?.traceLevel, + reasoningLevel: currentEntry?.reasoningLevel, + elevatedLevel: currentEntry?.elevatedLevel, + ttsAuto: currentEntry?.ttsAuto, + execHost: currentEntry?.execHost, + execSecurity: currentEntry?.execSecurity, + execAsk: currentEntry?.execAsk, + execNode: currentEntry?.execNode, + responseUsage: currentEntry?.responseUsage, + pinnedAt: currentEntry?.pinnedAt, + // Resets should keep the user's explicit selection, but clear any + // temporary fallback model that was pinned during the previous run. + ...resetPreservedSelection, + groupActivation: currentEntry?.groupActivation, + groupActivationNeedsSystemIntro: currentEntry?.groupActivationNeedsSystemIntro, + chatType: currentEntry?.chatType, + model: resolvedModel.model, + modelProvider: resolvedModel.provider, + contextTokens: resetEntry?.contextTokens, + compactionCount: currentEntry?.compactionCount, + compactionCheckpoints: currentEntry?.compactionCheckpoints, + sendPolicy: currentEntry?.sendPolicy, + queueMode: currentEntry?.queueMode, + queueDebounceMs: currentEntry?.queueDebounceMs, + queueCap: currentEntry?.queueCap, + queueDrop: currentEntry?.queueDrop, + spawnedBy: currentEntry?.spawnedBy, + spawnedWorkspaceDir: currentEntry?.spawnedWorkspaceDir, + spawnedCwd: currentEntry?.spawnedCwd, + parentSessionKey: currentEntry?.parentSessionKey, + forkedFromParent: currentEntry?.forkedFromParent, + spawnDepth: currentEntry?.spawnDepth, + subagentRole: currentEntry?.subagentRole, + subagentControlScope: currentEntry?.subagentControlScope, + label: currentEntry?.label, + displayName: currentEntry?.displayName, + channel: currentEntry?.channel, + groupId: currentEntry?.groupId, + subject: currentEntry?.subject, + groupChannel: currentEntry?.groupChannel, + space: currentEntry?.space, + origin: snapshotSessionOrigin(currentEntry), + deliveryContext: currentEntry?.deliveryContext, + cliSessionBindings: currentEntry?.cliSessionBindings, + cliSessionIds: currentEntry?.cliSessionIds, + claudeCliSessionId: currentEntry?.claudeCliSessionId, + lastChannel: currentEntry?.lastChannel, + lastTo: currentEntry?.lastTo, + lastAccountId: currentEntry?.lastAccountId, + lastThreadId: currentEntry?.lastThreadId, + // Do not carry the cached skills catalog across /new. Long-lived channel + // sessions (Signal DMs/groups in particular) otherwise keep advertising a + // stale block even after reset/restart, because the + // skills snapshot version is runtime-local and may reset to 0. + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + totalTokensFresh: true, + }; + // Drop CLI provider bindings so the next turn after reset starts a fresh + // CLI conversation on the provider side. Preserved only for spawned + // subagents (canonical `:subagent:` keys), where Tak Hoffman's fa56682b3ced + // regression fix intentionally protects CLI continuity for + // orchestration-driven resets. Non-subagent sessions that happen to set + // `parentSessionKey` (e.g. dashboard children) are not exempt. + if (!isSubagentSessionKey(primaryKey)) { + clearAllCliSessions(nextEntry); + } else { + nextEntry.cliSessionBindings = rebindCliSessionReseedReceiptsForReset( + nextEntry.cliSessionBindings, + nextSessionId, + ); + } + return nextEntry; + }, + afterEntryMutation: async (mutation) => { + let committedAcpResetState: { sessionKey: string; meta: SessionAcpMeta } | undefined; + if (deferredAcpResetState) { + const identity = deferredAcpResetState.meta.identity; + if ( + identity?.state === "resolved" && + (identity.acpxSessionId || identity.agentSessionId) + ) { + committedAcpResetState = { + sessionKey: deferredAcpResetState.sessionKey, + meta: buildPendingAcpMeta(deferredAcpResetState.meta, Date.now()), + }; + // The JSON session rotation and SQLite metadata cannot share a transaction. + // Bind captured ACP state before acknowledging the committed reset so the + // new session never observes an unreadable old-session row. + writeAcpSessionMetaForMigration({ + sessionKey: committedAcpResetState.sessionKey, + sessionId: mutation.nextEntry.sessionId, + meta: committedAcpResetState.meta, + }); + } + } + params.onCommitted?.({ + key: target.canonicalKey, + sessionId: mutation.nextEntry.sessionId, + }); + if (committedAcpResetState && isResetLifecycleCurrent()) { + try { + await getAcpRuntimeBackend( + (committedAcpResetState.meta.backend || cfg.acp?.backend || "").trim() || undefined, + )?.runtime.prepareFreshSession?.({ + sessionKey: committedAcpResetState.sessionKey, + }); + } catch (error) { + logVerbose( + `sessions.session-reset: ACP prepareFreshSession failed for ${committedAcpResetState.sessionKey}: ${String(error)}`, + ); + } + } + await emitGatewayBeforeResetPluginHook({ + cfg, + key: params.key, + target, + storePath, + entry: mutation.previousEntry, + reason: params.reason, + }); + }, + }); + const next = lifecycle.nextEntry; + const oldSessionId = lifecycle.previousSessionId; + const oldSessionFile = lifecycle.previousSessionFile; + + const archivedTranscripts = lifecycle.archivedTranscripts; + emitGatewaySessionEndPluginHook({ + cfg, + sessionKey: target.canonicalKey ?? params.key, + sessionId: oldSessionId, + storePath, + sessionFile: oldSessionFile, + agentId: target.agentId, + reason: params.reason, + archivedTranscripts, + nextSessionId: next.sessionId, + }); + emitGatewaySessionStartPluginHook({ + cfg, + sessionKey: target.canonicalKey ?? params.key, + sessionId: next.sessionId, + resumedFrom: oldSessionId, + storePath, + sessionFile: next.sessionFile, + agentId: target.agentId, + }); + if (hadExistingEntry) { + await emitSessionUnboundLifecycleEvent({ + targetSessionKey: target.canonicalKey ?? params.key, + reason: "session-reset", + }); + } + return { + ok: true, + key: target.canonicalKey, + entry: next, + agentId: target.agentId, + storePath, + }; }, }); - const next = lifecycle.nextEntry; - const oldSessionId = lifecycle.previousSessionId; - const oldSessionFile = lifecycle.previousSessionFile; - - const archivedTranscripts = lifecycle.archivedTranscripts; - emitGatewaySessionEndPluginHook({ - cfg, - sessionKey: target.canonicalKey ?? params.key, - sessionId: oldSessionId, - storePath, - sessionFile: oldSessionFile, - agentId: target.agentId, - reason: params.reason, - archivedTranscripts, - nextSessionId: next.sessionId, - }); - emitGatewaySessionStartPluginHook({ - cfg, - sessionKey: target.canonicalKey ?? params.key, - sessionId: next.sessionId, - resumedFrom: oldSessionId, - storePath, - sessionFile: next.sessionFile, - agentId: target.agentId, - }); - if (hadExistingEntry) { - await emitSessionUnboundLifecycleEvent({ - targetSessionKey: target.canonicalKey ?? params.key, - reason: "session-reset", - }); - } - return { - ok: true, - key: target.canonicalKey, - entry: next, - agentId: target.agentId, - storePath, - }; } diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 785e6df633fd..15763633a379 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -172,6 +172,29 @@ describe("gateway session utils", () => { expect(listed.hasMore).toBe(true); }); + test("session lists separate archived rows and sort pinned sessions first", () => { + const cfg = createModelDefaultsConfig({ primary: "openai/gpt-5.4" }); + const store = { + recent: { sessionId: "recent", updatedAt: 30 }, + pinned: { sessionId: "pinned", updatedAt: 10, pinnedAt: 40 }, + archived: { sessionId: "archived", updatedAt: 20, archivedAt: 50 }, + } satisfies Record; + + const active = listSessionsFromStore({ cfg, storePath: "", store, opts: {} }); + expect(active.sessions.map((session) => session.key)).toEqual(["pinned", "recent"]); + expect(active.sessions[0]).toMatchObject({ pinned: true, pinnedAt: 40, archived: false }); + + const archived = listSessionsFromStore({ + cfg, + storePath: "", + store, + opts: { archived: true }, + }); + expect(archived.sessions).toMatchObject([ + { key: "archived", archived: true, archivedAt: 50, pinned: false }, + ]); + }); + test("session lists page from an offset after filtering and sorting", () => { const cfg = createModelDefaultsConfig({ primary: "openai/gpt-5.4" }); const store = Object.fromEntries( diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index af3d13761450..94daea587f35 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -2167,6 +2167,10 @@ export function buildGatewaySessionRow(params: { chatType: entry?.chatType, origin, updatedAt, + archived: entry?.archivedAt !== undefined, + archivedAt: entry?.archivedAt, + pinned: entry?.pinnedAt !== undefined, + pinnedAt: entry?.pinnedAt, sessionId: entry?.sessionId, systemSent: entry?.systemSent, abortedLastRun: entry?.abortedLastRun, @@ -2425,7 +2429,12 @@ type SessionEntrySelection = { hasMore: boolean; }; -function compareSessionEntryPairsByUpdatedAt(a: SessionEntryPair, b: SessionEntryPair): number { +function compareSessionEntryPairs(a: SessionEntryPair, b: SessionEntryPair): number { + const aPinnedAt = a[1]?.pinnedAt ?? 0; + const bPinnedAt = b[1]?.pinnedAt ?? 0; + if (aPinnedAt !== bPinnedAt) { + return bPinnedAt - aPinnedAt; + } return (b[1]?.updatedAt ?? 0) - (a[1]?.updatedAt ?? 0); } @@ -2461,7 +2470,7 @@ function selectNewestLimitedEntries( const selected: SessionEntryPair[] = []; for (const entry of entries) { const insertAt = selected.findIndex( - (candidate) => compareSessionEntryPairsByUpdatedAt(entry, candidate) < 0, + (candidate) => compareSessionEntryPairs(entry, candidate) < 0, ); if (insertAt >= 0) { selected.splice(insertAt, 0, entry); @@ -2482,7 +2491,7 @@ function sortAndLimitSessionEntries( if (limit !== undefined && limit <= SESSIONS_LIST_TOP_N_LIMIT) { return selectNewestLimitedEntries(entries, limit); } - const sorted = entries.toSorted(compareSessionEntryPairsByUpdatedAt); + const sorted = entries.toSorted(compareSessionEntryPairs); return limit === undefined ? sorted : sorted.slice(0, limit); } @@ -2565,6 +2574,10 @@ function filterSessionEntries(params: { (entry?.spawnedBy === spawnedBy || entry?.parentSessionKey === spawnedBy) ); }) + .filter(([, entry]) => { + const archived = entry?.archivedAt !== undefined; + return opts.archived === true ? archived : !archived; + }) .filter(([, entry]) => { if (!label) { return true; diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts index 75408577f722..ad239167afa7 100644 --- a/src/gateway/session-utils.types.ts +++ b/src/gateway/session-utils.types.ts @@ -60,6 +60,10 @@ export type GatewaySessionRow = { chatType?: ChatType; origin?: SessionEntry["origin"]; updatedAt: number | null; + archived?: boolean; + archivedAt?: number; + pinned?: boolean; + pinnedAt?: number; sessionId?: string; systemSent?: boolean; abortedLastRun?: boolean; diff --git a/src/gateway/sessions-patch.test.ts b/src/gateway/sessions-patch.test.ts index aba5524920b6..6262f5ac7647 100644 --- a/src/gateway/sessions-patch.test.ts +++ b/src/gateway/sessions-patch.test.ts @@ -216,6 +216,40 @@ describe("gateway sessions patch", () => { resetPluginRuntimeStateForTest(); }); + test("archives and restores sessions without retaining a pin", async () => { + const archived = expectPatchOk( + await runPatch({ + store: mainStoreEntry({ pinnedAt: 10 }), + patch: { key: MAIN_SESSION_KEY, archived: true }, + }), + ); + expect(archived.archivedAt).toEqual(expect.any(Number)); + expect(archived.pinnedAt).toBeUndefined(); + + const restored = expectPatchOk( + await runPatch({ + store: mainStoreEntry({ archivedAt: archived.archivedAt }), + patch: { key: MAIN_SESSION_KEY, archived: false }, + }), + ); + expect(restored.archivedAt).toBeUndefined(); + }); + + test("pins active sessions and rejects pinned archived sessions", async () => { + const pinned = expectPatchOk( + await runPatch({ patch: { key: MAIN_SESSION_KEY, pinned: true } }), + ); + expect(pinned.pinnedAt).toEqual(expect.any(Number)); + + expectPatchError( + await runPatch({ + store: mainStoreEntry({ archivedAt: 10 }), + patch: { key: MAIN_SESSION_KEY, pinned: true }, + }), + "restore it first", + ); + }); + test("persists thinkingLevel=off (does not clear)", async () => { const entry = expectPatchOk( await runPatch({ diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index fe14db3e0063..33b6fa1219e8 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -369,6 +369,27 @@ export async function projectSessionsPatchEntry(params: { } } + if ("archived" in patch) { + if (patch.archived === true) { + // Archived sessions leave the active quick-access set in the same write. + next.archivedAt ??= now; + delete next.pinnedAt; + } else { + delete next.archivedAt; + } + } + + if ("pinned" in patch) { + if (patch.pinned === true) { + if (next.archivedAt !== undefined) { + return invalid("cannot pin an archived session; restore it first"); + } + next.pinnedAt ??= now; + } else { + delete next.pinnedAt; + } + } + if ("thinkingLevel" in patch) { const raw = patch.thinkingLevel; if (raw === null) { diff --git a/src/gateway/test/server-sessions.test-helpers.ts b/src/gateway/test/server-sessions.test-helpers.ts index b73489ed8987..1fa183b5ed14 100644 --- a/src/gateway/test/server-sessions.test-helpers.ts +++ b/src/gateway/test/server-sessions.test-helpers.ts @@ -116,7 +116,7 @@ const subagentLifecycleHookState = vi.hoisted(() => ({ })); const threadBindingMocks = vi.hoisted(() => ({ - unbindThreadBindingsBySessionKey: vi.fn((_params?: unknown) => []), + unbindThreadBindingsBySessionKey: vi.fn(async (_params?: unknown) => []), })); const acpRuntimeMocks = vi.hoisted(() => ({ cancel: vi.fn(async () => {}), @@ -586,6 +586,12 @@ export function expectActiveRunCleanup( cfg: expect.any(Object), requesterSessionKey, }); + expectSessionQueueCleanup(expectedQueueKeys); + expect(embeddedRunMock.abortCalls).toEqual([sessionId]); + expect(embeddedRunMock.waitCalls).toEqual([sessionId]); +} + +export function expectSessionQueueCleanup(expectedQueueKeys: string[]) { expect(sessionCleanupMocks.clearSessionQueues).toHaveBeenCalledTimes(1); const clearedKeys = ( sessionCleanupMocks.clearSessionQueues.mock.calls as unknown as Array<[string[]]> @@ -593,8 +599,6 @@ export function expectActiveRunCleanup( for (const key of expectedQueueKeys) { expect(clearedKeys).toContain(key); } - expect(embeddedRunMock.abortCalls).toEqual([sessionId]); - expect(embeddedRunMock.waitCalls).toEqual([sessionId]); } export async function getMainPreviewEntry(ws: import("ws").WebSocket) { diff --git a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts index 5c6a419ecbd8..3f20aa3e2850 100644 --- a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts +++ b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts @@ -455,6 +455,9 @@ export function createPluginRuntimeMock(overrides: DeepPartial = .mockResolvedValue( undefined, ) as unknown as PluginRuntime["agent"]["session"]["upsertSessionEntry"], + runWithWorkAdmission: vi.fn( + async (_params, run) => await run(new AbortController().signal), + ) as PluginRuntime["agent"]["session"]["runWithWorkAdmission"], saveSessionStore: vi .fn() .mockResolvedValue( diff --git a/src/plugins/runtime/index.test.ts b/src/plugins/runtime/index.test.ts index 7856b231c2c0..bf5e78f7ac4f 100644 --- a/src/plugins/runtime/index.test.ts +++ b/src/plugins/runtime/index.test.ts @@ -327,6 +327,7 @@ describe("plugin runtime command execution", () => { "listSessionEntries", "patchSessionEntry", "upsertSessionEntry", + "runWithWorkAdmission", "saveSessionStore", "updateSessionStore", "updateSessionStoreEntry", diff --git a/src/plugins/runtime/runtime-agent.test.ts b/src/plugins/runtime/runtime-agent.test.ts new file mode 100644 index 000000000000..27c8aa5de584 --- /dev/null +++ b/src/plugins/runtime/runtime-agent.test.ts @@ -0,0 +1,123 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + interruptSessionWorkAdmissions, + runExclusiveSessionLifecycleMutation, +} from "../../sessions/session-lifecycle-admission.js"; +import { createRuntimeAgent } from "./runtime-agent.js"; + +function createDeferred(): { promise: Promise; resolve: () => void } { + let resolve = () => {}; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +describe("plugin runtime session work admission", () => { + let tempDir: string; + let storePath: string; + const sessionKey = "agent:main:voice:caller"; + const sessionId = "voice-session-id"; + + beforeEach(async () => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-session-admission-")); + storePath = path.join(tempDir, "sessions.json"); + await createRuntimeAgent().session.upsertSessionEntry({ + storePath, + sessionKey, + entry: { sessionId, updatedAt: Date.now() }, + }); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it("rejects an archived session before running admitted work", async () => { + const runtime = createRuntimeAgent(); + await runtime.session.patchSessionEntry({ + storePath, + sessionKey, + update: () => ({ archivedAt: Date.now() }), + }); + let ran = false; + + await expect( + runtime.session.runWithWorkAdmission({ storePath, sessionKey }, async () => { + ran = true; + }), + ).rejects.toThrow(`Session "${sessionKey}" is archived`); + expect(ran).toBe(false); + }); + + it("waits for a queued archive mutation and rejects the stale start", async () => { + const runtime = createRuntimeAgent(); + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: storePath, + identities: [sessionKey, sessionId], + prepare: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + run: async () => { + await runtime.session.patchSessionEntry({ + storePath, + sessionKey, + update: () => ({ archivedAt: Date.now() }), + }); + }, + }); + await mutationStarted.promise; + + const work = runtime.session.runWithWorkAdmission({ storePath, sessionKey }, async () => {}); + releaseMutation.resolve(); + await mutation; + + await expect(work).rejects.toThrow(`Session "${sessionKey}" is archived`); + }); + + it("admits fresh work and protects session creation inside the callback", async () => { + const runtime = createRuntimeAgent(); + const freshKey = "agent:main:voice:fresh"; + const freshId = "fresh-session-id"; + + await runtime.session.runWithWorkAdmission({ storePath, sessionKey: freshKey }, async () => { + await runtime.session.upsertSessionEntry({ + storePath, + sessionKey: freshKey, + entry: { sessionId: freshId, updatedAt: Date.now() }, + }); + }); + + expect(runtime.session.getSessionEntry({ storePath, sessionKey: freshKey })?.sessionId).toBe( + freshId, + ); + }); + + it("holds admission through the callback and relays lifecycle interruption", async () => { + const runtime = createRuntimeAgent(); + const workStarted = createDeferred(); + let admittedSignal: AbortSignal | undefined; + const work = runtime.session.runWithWorkAdmission({ storePath, sessionKey }, async (signal) => { + admittedSignal = signal; + workStarted.resolve(); + await new Promise((resolve) => { + signal.addEventListener("abort", () => resolve(), { once: true }); + }); + }); + await workStarted.promise; + + await interruptSessionWorkAdmissions({ + scope: storePath, + identities: [sessionKey, sessionId], + }); + await work; + + expect(admittedSignal?.aborted).toBe(true); + }); +}); diff --git a/src/plugins/runtime/runtime-agent.ts b/src/plugins/runtime/runtime-agent.ts index 6419f3c869f9..eff95c94447f 100644 --- a/src/plugins/runtime/runtime-agent.ts +++ b/src/plugins/runtime/runtime-agent.ts @@ -10,6 +10,7 @@ import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; import { ensureAgentWorkspace } from "../../agents/workspace.js"; import { normalizeThinkLevel, resolveThinkingProfile } from "../../auto-reply/thinking.js"; import { getRuntimeConfig } from "../../config/config.js"; +import { resolveSessionWorkStartError } from "../../config/sessions/lifecycle.js"; import { resolveSessionFilePath, resolveStorePath } from "../../config/sessions/paths.js"; import { listSessionEntries as listAccessorSessionEntries, @@ -27,6 +28,7 @@ import { type ResolvedSessionMaintenanceConfigInput, } from "../../config/sessions/store.js"; import type { SessionEntry } from "../../config/sessions/types.js"; +import { beginSessionWorkAdmission } from "../../sessions/session-lifecycle-admission.js"; import { createLazyRuntimeMethod, createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { defineCachedValue } from "./runtime-cache.js"; import type { PluginRuntime } from "./types.js"; @@ -158,6 +160,53 @@ async function upsertSessionEntry(params: RuntimeUpsertSessionEntryParams): Prom await replaceSessionEntry(toSessionAccessScope(params), params.entry); } +async function runWithSessionWorkAdmission( + params: { storePath: string; sessionKey: string; signal?: AbortSignal }, + run: (signal: AbortSignal) => Promise, +): Promise { + const initialEntry = getSessionEntry({ + storePath: params.storePath, + sessionKey: params.sessionKey, + readConsistency: "latest", + }); + const lifecycleAbortController = new AbortController(); + const admission = await beginSessionWorkAdmission({ + scope: params.storePath, + identities: [params.sessionKey, initialEntry?.sessionId], + signal: params.signal, + onInterrupt: () => + lifecycleAbortController.abort( + new Error("Agent work interrupted by a session lifecycle change."), + ), + assertAllowed: () => { + const currentEntry = getSessionEntry({ + storePath: params.storePath, + sessionKey: params.sessionKey, + readConsistency: "latest", + }); + const changed = initialEntry + ? !currentEntry || currentEntry.sessionId !== initialEntry.sessionId + : Boolean(currentEntry); + if (changed) { + throw new Error(`Session "${params.sessionKey}" changed while starting work. Retry.`); + } + const archivedSessionError = resolveSessionWorkStartError(params.sessionKey, currentEntry); + if (archivedSessionError) { + throw new Error(archivedSessionError); + } + }, + }); + + try { + const signal = params.signal + ? AbortSignal.any([params.signal, lifecycleAbortController.signal]) + : lifecycleAbortController.signal; + return await admission.run(async () => await run(signal)); + } finally { + admission.release(); + } +} + /** Creates the plugin runtime agent facade with lazy embedded-agent/session helpers. */ export function createRuntimeAgent(): PluginRuntime["agent"] { const agentRuntime = { @@ -202,6 +251,7 @@ export function createRuntimeAgent(): PluginRuntime["agent"] { listSessionEntries, patchSessionEntry, upsertSessionEntry, + runWithWorkAdmission: runWithSessionWorkAdmission, loadSessionStore, saveSessionStore, updateSessionStore, diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index 540ec623b59c..e51fd2ca7fb4 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -86,6 +86,11 @@ type RuntimeSessionStoreEntryPatchParams = RuntimeSessionStoreReadParams & { type RuntimeUpsertSessionEntryParams = RuntimeSessionStoreReadParams & { entry: RuntimeSessionEntry; }; +type RuntimeSessionWorkAdmissionParams = { + storePath: string; + sessionKey: string; + signal?: AbortSignal; +}; type RuntimeSessionStoreEntryUpdateParams = { storePath: string; sessionKey: string; @@ -250,6 +255,10 @@ export type PluginRuntimeCore = { params: RuntimeSessionStoreEntryPatchParams, ) => Promise; upsertSessionEntry: (params: RuntimeUpsertSessionEntryParams) => Promise; + runWithWorkAdmission: ( + params: RuntimeSessionWorkAdmissionParams, + run: (signal: AbortSignal) => Promise, + ) => Promise; /** * @deprecated Use getSessionEntry/listSessionEntries for reads and * patchSessionEntry/upsertSessionEntry for writes. This whole-store diff --git a/src/plugins/session-entry-slot-keys.ts b/src/plugins/session-entry-slot-keys.ts index f335c5050ee1..42689f8a8e6e 100644 --- a/src/plugins/session-entry-slot-keys.ts +++ b/src/plugins/session-entry-slot-keys.ts @@ -13,7 +13,10 @@ const SESSION_ENTRY_RESERVED_SLOT_KEY_LIST = [ "pluginExtensionSlotKeys", "pluginNextTurnInjections", "sessionId", + "lifecycleRevision", "updatedAt", + "archivedAt", + "pinnedAt", "sessionFile", "spawnedBy", "spawnedWorkspaceDir", diff --git a/src/scripts/test-projects.test.ts b/src/scripts/test-projects.test.ts index 7ac95ea57dec..b8e7c725d8ae 100644 --- a/src/scripts/test-projects.test.ts +++ b/src/scripts/test-projects.test.ts @@ -859,6 +859,8 @@ describe("test-projects args", () => { config: "test/vitest/vitest.unit-fast.config.ts", forwardedArgs: [], includePatterns: [ + "src/agents/command/attempt-execution.shared.test.ts", + "src/auto-reply/reply/session-entry-persistence.test.ts", "src/crestodian/operations.test.ts", "src/install-sh-version.test.ts", "src/proxy-capture/store.sqlite.test.ts", @@ -937,6 +939,7 @@ describe("test-projects args", () => { "src/gateway/hooks-mapping.test.ts", "src/gateway/server-methods/chat.abort-persistence.test.ts", "src/gateway/server.agent.gateway-server-agent-b.test.ts", + "src/gateway/server.chat.gateway-server-chat-b.test.ts", "src/gateway/server.sessions.permissions-hooks.test.ts", ], watchMode: false, @@ -965,6 +968,17 @@ describe("test-projects args", () => { ], watchMode: false, }, + { + config: "test/vitest/vitest.auto-reply.config.ts", + forwardedArgs: [], + includePatterns: [ + "src/auto-reply/reply/get-reply-native-slash-fast-path.test.ts", + "src/auto-reply/reply/get-reply.auto-fallback.test.ts", + "src/auto-reply/reply/reply-turn-admission.test.ts", + "src/auto-reply/reply/session-updates.test.ts", + ], + watchMode: false, + }, { config: "test/vitest/vitest.agents.config.ts", forwardedArgs: [], diff --git a/src/sessions/session-lifecycle-admission.test.ts b/src/sessions/session-lifecycle-admission.test.ts new file mode 100644 index 000000000000..de245bfbd560 --- /dev/null +++ b/src/sessions/session-lifecycle-admission.test.ts @@ -0,0 +1,341 @@ +// Tests lifecycle/work admission ordering across canonical keys and backing ids. +import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; +import { expect, it } from "vitest"; +import { + beginSessionWorkAdmission, + hasOnlySessionLifecycleMutationKindActive, + interruptSessionWorkAdmissions, + isSessionWorkAdmissionActive, + runExclusiveSessionLifecycleMutation, +} from "./session-lifecycle-admission.js"; + +function createDeferred() { + let resolve = () => {}; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +it("serializes lifecycle mutation and work admission across identity aliases", async () => { + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + + let admitted = false; + const admission = beginSessionWorkAdmission({ + scope: "store-a", + identities: ["session-1"], + assertAllowed: () => { + admitted = true; + }, + }); + await Promise.resolve(); + expect(admitted).toBe(false); + + releaseMutation.resolve(); + await mutation; + const admissionLease = await admission; + expect(admitted).toBe(true); + expect(isSessionWorkAdmissionActive("store-a", ["agent:main:child", "session-1"])).toBe(true); + + admissionLease.release(); + expect(isSessionWorkAdmissionActive("store-a", ["session-1"])).toBe(false); +}); + +it("tracks the active lifecycle mutation kind across identity aliases", async () => { + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: "store-kind", + identities: ["agent:main:child", "session-kind"], + kind: "compaction", + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + + expect( + hasOnlySessionLifecycleMutationKindActive("store-kind", ["session-kind"], "compaction"), + ).toBe(true); + expect( + hasOnlySessionLifecycleMutationKindActive("store-other", ["session-kind"], "compaction"), + ).toBe(false); + + releaseMutation.resolve(); + await mutation; + expect( + hasOnlySessionLifecycleMutationKindActive("store-kind", ["session-kind"], "compaction"), + ).toBe(false); +}); + +it("keeps identical session keys isolated by store", async () => { + const admissionLease = await beginSessionWorkAdmission({ + scope: "store-a", + identities: ["global", "session-a"], + assertAllowed: () => {}, + }); + + try { + expect(isSessionWorkAdmissionActive("store-a", ["global"])).toBe(true); + expect(isSessionWorkAdmissionActive("store-b", ["global"])).toBe(false); + let storeBMutationRan = false; + await runExclusiveSessionLifecycleMutation({ + scope: "store-b", + identities: ["global"], + run: async () => { + storeBMutationRan = true; + }, + }); + expect(storeBMutationRan).toBe(true); + } finally { + admissionLease.release(); + } +}); + +it("cancels work admission waiting behind a lifecycle mutation", async () => { + const mutationPrepared = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + prepare: async () => { + mutationPrepared.resolve(); + await releaseMutation.promise; + }, + run: async () => {}, + }); + await mutationPrepared.promise; + + const controller = new AbortController(); + const abortError = new Error("reset interrupted admission"); + const admission = beginSessionWorkAdmission({ + scope: "store-a", + identities: ["session-1"], + signal: controller.signal, + assertAllowed: () => {}, + }); + controller.abort(abortError); + + await expect(admission).rejects.toBe(abortError); + releaseMutation.resolve(); + await mutation; +}); + +it("cancels work admission while a lifecycle mutation holds the identity lock", async () => { + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + }, + }); + await mutationStarted.promise; + + const controller = new AbortController(); + const abortError = new Error("cancel during lifecycle mutation"); + const admission = beginSessionWorkAdmission({ + scope: "store-a", + identities: ["session-1"], + signal: controller.signal, + assertAllowed: () => {}, + }); + controller.abort(abortError); + + await expect(admission).rejects.toBe(abortError); + releaseMutation.resolve(); + await mutation; +}); + +it("cancels a queued lifecycle mutation before it becomes active", async () => { + const firstStarted = createDeferred(); + const releaseFirst = createDeferred(); + const first = runExclusiveSessionLifecycleMutation({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + run: async () => { + firstStarted.resolve(); + await releaseFirst.promise; + }, + }); + await firstStarted.promise; + + const controller = new AbortController(); + const abortError = new Error("cancel queued lifecycle mutation"); + let cancelledMutationRan = false; + const cancelled = runExclusiveSessionLifecycleMutation({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + signal: controller.signal, + run: async () => { + cancelledMutationRan = true; + }, + }); + controller.abort(abortError); + + await expect(cancelled).rejects.toBe(abortError); + releaseFirst.resolve(); + await first; + await runExclusiveSessionLifecycleMutation({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + run: async () => {}, + }); + expect(cancelledMutationRan).toBe(false); +}); + +it("preserves the initiating admission across a queued lifecycle mutation", async () => { + let selfInterrupted = false; + const admission = await beginSessionWorkAdmission({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + assertAllowed: () => {}, + onInterrupt: () => { + selfInterrupted = true; + }, + }); + const firstStarted = createDeferred(); + const releaseFirst = createDeferred(); + const first = runExclusiveSessionLifecycleMutation({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + run: async () => { + firstStarted.resolve(); + await releaseFirst.promise; + }, + }); + await firstStarted.promise; + + let initiatingAdmissionExcluded = false; + const queued = admission.run( + async () => + await runExclusiveSessionLifecycleMutation({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + prepare: async () => { + initiatingAdmissionExcluded = await interruptSessionWorkAdmissions({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + timeoutMs: 1, + }); + }, + run: async () => {}, + }), + ); + + try { + releaseFirst.resolve(); + await first; + await queued; + expect(initiatingAdmissionExcluded).toBe(true); + expect(selfInterrupted).toBe(false); + } finally { + releaseFirst.resolve(); + admission.release(); + await Promise.allSettled([first, queued]); + } +}); + +it("bounds interruption waits for non-cooperative work", async () => { + const admissionLease = await beginSessionWorkAdmission({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + assertAllowed: () => {}, + onInterrupt: () => {}, + }); + + try { + await expect( + interruptSessionWorkAdmissions({ + scope: "store-a", + identities: ["session-1"], + timeoutMs: 1, + }), + ).resolves.toBe(false); + } finally { + admissionLease.release(); + } +}); + +it("excludes the initiating admission from an in-band interruption", async () => { + let interrupted = false; + const admissionLease = await beginSessionWorkAdmission({ + scope: "store-a", + identities: ["agent:main:child", "session-1"], + assertAllowed: () => {}, + onInterrupt: () => { + interrupted = true; + }, + }); + + try { + await expect( + admissionLease.run( + async () => + await interruptSessionWorkAdmissions({ + scope: "store-a", + identities: ["session-1"], + timeoutMs: 1, + }), + ), + ).resolves.toBe(true); + expect(interrupted).toBe(false); + } finally { + admissionLease.release(); + } +}); + +it("shares lifecycle coordination across duplicate module instances", async () => { + const first = await importFreshModule( + import.meta.url, + "./session-lifecycle-admission.js?scope=session-lifecycle-a", + ); + const second = await importFreshModule( + import.meta.url, + "./session-lifecycle-admission.js?scope=session-lifecycle-b", + ); + let releaseLease = () => {}; + let interrupted = false; + const lease = await first.beginSessionWorkAdmission({ + scope: "store-duplicate", + identities: ["agent:main:child", "session-duplicate"], + assertAllowed: () => {}, + onInterrupt: () => { + interrupted = true; + releaseLease(); + }, + }); + releaseLease = lease.release; + + try { + expect(second.isSessionWorkAdmissionActive("store-duplicate", ["session-duplicate"])).toBe( + true, + ); + await expect( + second.interruptSessionWorkAdmissions({ + scope: "store-duplicate", + identities: ["agent:main:child"], + timeoutMs: 50, + }), + ).resolves.toBe(true); + expect(interrupted).toBe(true); + expect(first.isSessionWorkAdmissionActive("store-duplicate", ["session-duplicate"])).toBe( + false, + ); + } finally { + lease.release(); + } +}); diff --git a/src/sessions/session-lifecycle-admission.ts b/src/sessions/session-lifecycle-admission.ts new file mode 100644 index 000000000000..58d2c6f7a8dd --- /dev/null +++ b/src/sessions/session-lifecycle-admission.ts @@ -0,0 +1,429 @@ +// Serializes lifecycle mutations and work admission for logical session identities. +import { AsyncLocalStorage } from "node:async_hooks"; +import { resolveGlobalSingleton } from "../shared/global-singleton.js"; +import { runQueuedStoreWrite, type StoreWriterQueue } from "../shared/store-writer-queue.js"; + +export const SESSION_WORK_ADMISSION_DRAIN_TIMEOUT_MS = 15_000; +type SessionWorkAdmission = { + interrupt?: () => void; + released: Promise; +}; + +type SessionLifecycleAdmissionState = { + lifecycleQueues: Map; + mutationQueues: Map; + activeAdmissions: Map>; + activeMutations: Map; + activeMutationKinds: Map>; + idleWaiters: Map void>>; + currentAdmissions: AsyncLocalStorage>; +}; + +export type SessionLifecycleMutationKind = "compaction"; + +// Runtime chunks can load separate module instances while still coordinating +// the same sessions. One shared state keeps every lock and admission visible. +const SESSION_LIFECYCLE_ADMISSION_STATE = resolveGlobalSingleton( + Symbol.for("openclaw.sessionLifecycleAdmissionState"), + (): SessionLifecycleAdmissionState => ({ + lifecycleQueues: new Map(), + mutationQueues: new Map(), + activeAdmissions: new Map(), + activeMutations: new Map(), + activeMutationKinds: new Map(), + idleWaiters: new Map(), + currentAdmissions: new AsyncLocalStorage(), + }), +); +const { + lifecycleQueues: SESSION_LIFECYCLE_QUEUES, + mutationQueues: SESSION_LIFECYCLE_MUTATION_QUEUES, + activeAdmissions: ACTIVE_SESSION_WORK_ADMISSIONS, + activeMutations: ACTIVE_SESSION_LIFECYCLE_MUTATIONS, + activeMutationKinds: ACTIVE_SESSION_LIFECYCLE_MUTATION_KINDS, + idleWaiters: SESSION_LIFECYCLE_IDLE_WAITERS, + currentAdmissions: CURRENT_SESSION_WORK_ADMISSIONS, +} = SESSION_LIFECYCLE_ADMISSION_STATE; + +export type SessionWorkAdmissionLease = { + release: () => void; + run: (run: () => Promise) => Promise; +}; + +function normalizeSessionIdentities( + scope: string, + identities: Iterable, +): string[] { + const normalizedScope = scope.trim(); + if (!normalizedScope) { + throw new Error("session lifecycle scope is required"); + } + return Array.from( + new Set( + Array.from(identities, (identity) => identity?.trim()).filter( + (identity): identity is string => Boolean(identity), + ), + ), + ) + .map((identity) => JSON.stringify([normalizedScope, identity])) + .toSorted(); +} + +async function runWithSessionIdentityLocks( + identities: readonly string[], + index: number, + run: () => Promise, +): Promise { + const identity = identities[index]; + if (!identity) { + return await run(); + } + return await runQueuedStoreWrite({ + queues: SESSION_LIFECYCLE_QUEUES, + storePath: identity, + label: "runExclusiveSessionLifecycle", + reentrant: true, + fn: async () => await runWithSessionIdentityLocks(identities, index + 1, run), + }); +} + +async function runWithSessionMutationIdentityLocks( + identities: readonly string[], + index: number, + run: () => Promise, +): Promise { + const identity = identities[index]; + if (!identity) { + return await run(); + } + return await runQueuedStoreWrite({ + queues: SESSION_LIFECYCLE_MUTATION_QUEUES, + storePath: identity, + label: "runExclusiveSessionLifecycleMutation", + reentrant: true, + fn: async () => await runWithSessionMutationIdentityLocks(identities, index + 1, run), + }); +} + +function hasActiveSessionLifecycleMutation(identities: readonly string[]): boolean { + return identities.some((identity) => (ACTIVE_SESSION_LIFECYCLE_MUTATIONS.get(identity) ?? 0) > 0); +} + +function hasOnlyActiveSessionLifecycleMutationKind( + identities: readonly string[], + kind: SessionLifecycleMutationKind, +): boolean { + let foundActiveMutation = false; + for (const identity of identities) { + const activeCount = ACTIVE_SESSION_LIFECYCLE_MUTATIONS.get(identity) ?? 0; + if (activeCount === 0) { + continue; + } + foundActiveMutation = true; + if ((ACTIVE_SESSION_LIFECYCLE_MUTATION_KINDS.get(identity)?.get(kind) ?? 0) !== activeCount) { + return false; + } + } + return foundActiveMutation; +} + +async function waitForNormalizedSessionLifecycleMutationIdle( + identities: readonly string[], + signal?: AbortSignal, +): Promise { + const activeIdentities = identities.filter( + (identity) => (ACTIVE_SESSION_LIFECYCLE_MUTATIONS.get(identity) ?? 0) > 0, + ); + if (activeIdentities.length === 0) { + return; + } + signal?.throwIfAborted(); + const idle = Promise.all( + activeIdentities.map( + (identity) => + new Promise((resolve) => { + const waiters = SESSION_LIFECYCLE_IDLE_WAITERS.get(identity) ?? new Set(); + waiters.add(resolve); + SESSION_LIFECYCLE_IDLE_WAITERS.set(identity, waiters); + }), + ), + ); + if (!signal) { + await idle; + return; + } + let rejectAborted = () => {}; + const aborted = new Promise((_, reject) => { + rejectAborted = () => + reject( + signal.reason instanceof Error + ? signal.reason + : new Error("session work admission aborted"), + ); + signal.addEventListener("abort", rejectAborted, { once: true }); + }); + try { + await Promise.race([idle, aborted]); + } finally { + signal.removeEventListener("abort", rejectAborted); + } +} + +export async function runExclusiveSessionLifecycle(params: { + scope: string; + identities: Iterable; + signal?: AbortSignal; + run: () => Promise; +}): Promise { + const identities = normalizeSessionIdentities(params.scope, params.identities); + while (true) { + params.signal?.throwIfAborted(); + if (hasActiveSessionLifecycleMutation(identities)) { + await waitForNormalizedSessionLifecycleMutationIdle(identities, params.signal); + continue; + } + const attempt = await runWithSessionIdentityLocks(identities, 0, async () => { + params.signal?.throwIfAborted(); + if (hasActiveSessionLifecycleMutation(identities)) { + return { blocked: true as const }; + } + return { blocked: false as const, value: await params.run() }; + }); + if (!attempt.blocked) { + return attempt.value; + } + await waitForNormalizedSessionLifecycleMutationIdle(identities, params.signal); + } +} + +export async function runExclusiveSessionLifecycleMutation(params: { + scope: string; + identities: Iterable; + kind?: SessionLifecycleMutationKind; + prepare?: () => Promise; + run: () => Promise; + signal?: AbortSignal; +}): Promise { + const identities = normalizeSessionIdentities(params.scope, params.identities); + const signal = params.signal; + signal?.throwIfAborted(); + const callerAdmissions = new Set(CURRENT_SESSION_WORK_ADMISSIONS.getStore()); + let mutationActivated = false; + let removeAbortListener = () => {}; + const mutation = runWithSessionMutationIdentityLocks( + identities, + 0, + async () => + await CURRENT_SESSION_WORK_ADMISSIONS.run(callerAdmissions, async () => { + await runWithSessionIdentityLocks(identities, 0, async () => { + signal?.throwIfAborted(); + mutationActivated = true; + removeAbortListener(); + for (const identity of identities) { + ACTIVE_SESSION_LIFECYCLE_MUTATIONS.set( + identity, + (ACTIVE_SESSION_LIFECYCLE_MUTATIONS.get(identity) ?? 0) + 1, + ); + if (params.kind) { + const kinds = ACTIVE_SESSION_LIFECYCLE_MUTATION_KINDS.get(identity) ?? new Map(); + kinds.set(params.kind, (kinds.get(params.kind) ?? 0) + 1); + ACTIVE_SESSION_LIFECYCLE_MUTATION_KINDS.set(identity, kinds); + } + } + }); + // Cancellation may abandon a queued contender, but never an active + // mutation whose caller must observe cleanup and completion. + try { + await params.prepare?.(); + return await runWithSessionIdentityLocks(identities, 0, params.run); + } finally { + await runWithSessionIdentityLocks(identities, 0, async () => { + for (const identity of identities) { + if (params.kind) { + const kinds = ACTIVE_SESSION_LIFECYCLE_MUTATION_KINDS.get(identity); + const remainingKindCount = (kinds?.get(params.kind) ?? 1) - 1; + if (remainingKindCount > 0) { + kinds?.set(params.kind, remainingKindCount); + } else { + kinds?.delete(params.kind); + if (kinds?.size === 0) { + ACTIVE_SESSION_LIFECYCLE_MUTATION_KINDS.delete(identity); + } + } + } + const remaining = (ACTIVE_SESSION_LIFECYCLE_MUTATIONS.get(identity) ?? 1) - 1; + if (remaining > 0) { + ACTIVE_SESSION_LIFECYCLE_MUTATIONS.set(identity, remaining); + continue; + } + ACTIVE_SESSION_LIFECYCLE_MUTATIONS.delete(identity); + const waiters = SESSION_LIFECYCLE_IDLE_WAITERS.get(identity); + SESSION_LIFECYCLE_IDLE_WAITERS.delete(identity); + for (const resolve of waiters ?? []) { + resolve(); + } + } + }); + } + }), + ); + if (!signal) { + return await mutation; + } + if (mutationActivated) { + return await mutation; + } + const aborted = new Promise((_, reject) => { + const onAbort = () => { + if (mutationActivated) { + return; + } + try { + signal.throwIfAborted(); + } catch (error) { + reject(error instanceof Error ? error : new Error(String(error))); + } + }; + removeAbortListener = () => signal.removeEventListener("abort", onAbort); + signal.addEventListener("abort", onAbort, { once: true }); + if (signal.aborted) { + onAbort(); + } + }); + try { + return await Promise.race([mutation, aborted]); + } finally { + removeAbortListener(); + } +} + +export function isSessionLifecycleMutationActive( + scope: string, + identities: Iterable, +): boolean { + return hasActiveSessionLifecycleMutation(normalizeSessionIdentities(scope, identities)); +} + +export function hasOnlySessionLifecycleMutationKindActive( + scope: string, + identities: Iterable, + kind: SessionLifecycleMutationKind, +): boolean { + return hasOnlyActiveSessionLifecycleMutationKind( + normalizeSessionIdentities(scope, identities), + kind, + ); +} + +export async function waitForSessionLifecycleMutationIdle( + scope: string, + identities: Iterable, +): Promise { + await waitForNormalizedSessionLifecycleMutationIdle( + normalizeSessionIdentities(scope, identities), + ); +} + +export function isSessionWorkAdmissionActive( + scope: string, + identities: Iterable, +): boolean { + return normalizeSessionIdentities(scope, identities).some( + (identity) => (ACTIVE_SESSION_WORK_ADMISSIONS.get(identity)?.size ?? 0) > 0, + ); +} + +export async function beginSessionWorkAdmission(params: { + scope: string; + identities: Iterable; + assertAllowed: () => Promise | void; + onInterrupt?: () => void; + signal?: AbortSignal; +}): Promise { + const identities = normalizeSessionIdentities(params.scope, params.identities); + return await runExclusiveSessionLifecycle({ + scope: params.scope, + identities: params.identities, + signal: params.signal, + run: async () => { + await params.assertAllowed(); + let resolveReleased = () => {}; + const admission: SessionWorkAdmission = { + interrupt: params.onInterrupt, + released: new Promise((resolve) => { + resolveReleased = resolve; + }), + }; + for (const identity of identities) { + const active = ACTIVE_SESSION_WORK_ADMISSIONS.get(identity) ?? new Set(); + active.add(admission); + ACTIVE_SESSION_WORK_ADMISSIONS.set(identity, active); + } + let released = false; + const release = () => { + if (released) { + return; + } + released = true; + for (const identity of identities) { + const active = ACTIVE_SESSION_WORK_ADMISSIONS.get(identity); + active?.delete(admission); + if (!active?.size) { + ACTIVE_SESSION_WORK_ADMISSIONS.delete(identity); + } + } + resolveReleased(); + }; + return { + release, + run: async (run: () => Promise) => { + const current = new Set(CURRENT_SESSION_WORK_ADMISSIONS.getStore()); + current.add(admission); + return await CURRENT_SESSION_WORK_ADMISSIONS.run(current, run); + }, + }; + }, + }); +} + +export async function interruptSessionWorkAdmissions(params: { + scope: string; + identities: Iterable; + timeoutMs?: number; +}): Promise { + const admissions = new Set(); + const currentAdmissions = CURRENT_SESSION_WORK_ADMISSIONS.getStore(); + for (const identity of normalizeSessionIdentities(params.scope, params.identities)) { + for (const admission of ACTIVE_SESSION_WORK_ADMISSIONS.get(identity) ?? []) { + // In-band lifecycle commands suspend their own admitted turn while the + // mutation runs. Interrupt competing work, not the initiating stack. + if (currentAdmissions?.has(admission)) { + continue; + } + admissions.add(admission); + } + } + for (const admission of admissions) { + admission.interrupt?.(); + } + const released = Promise.all(Array.from(admissions, (admission) => admission.released)); + if (params.timeoutMs === undefined) { + await released; + return true; + } + const timeoutMs = params.timeoutMs; + let timer: ReturnType | undefined; + try { + return await Promise.race([ + released.then(() => true), + new Promise((resolve) => { + timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs)); + timer.unref?.(); + }), + ]); + } finally { + if (timer) { + clearTimeout(timer); + } + } +} diff --git a/src/shared/store-writer-queue.test.ts b/src/shared/store-writer-queue.test.ts new file mode 100644 index 000000000000..81e3fc0e0041 --- /dev/null +++ b/src/shared/store-writer-queue.test.ts @@ -0,0 +1,42 @@ +// Verifies queue ownership and reentrancy across separately loaded runtime chunks. +import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; +import { expect, it } from "vitest"; +import type { StoreWriterQueue } from "./store-writer-queue.js"; + +it("shares reentrant writer context across duplicate module instances", async () => { + const first = await importFreshModule( + import.meta.url, + "./store-writer-queue.js?scope=store-writer-a", + ); + const second = await importFreshModule( + import.meta.url, + "./store-writer-queue.js?scope=store-writer-b", + ); + const queues = new Map(); + const order: string[] = []; + + const result = await first.runQueuedStoreWrite({ + queues, + storePath: "shared-store", + label: "outer", + fn: async () => { + order.push("outer:start"); + const nested = await second.runQueuedStoreWrite({ + queues, + storePath: "shared-store", + label: "inner", + reentrant: true, + fn: async () => { + order.push("inner"); + return "nested-result"; + }, + }); + order.push("outer:end"); + return nested; + }, + }); + + expect(result).toBe("nested-result"); + expect(order).toEqual(["outer:start", "inner", "outer:end"]); + expect(queues.size).toBe(0); +}); diff --git a/src/shared/store-writer-queue.ts b/src/shared/store-writer-queue.ts index 3cf2a675a6dc..00e29f94c167 100644 --- a/src/shared/store-writer-queue.ts +++ b/src/shared/store-writer-queue.ts @@ -1,4 +1,5 @@ import { AsyncLocalStorage } from "node:async_hooks"; +import { resolveGlobalSingleton } from "./global-singleton.js"; /** Pending exclusive store write plus the promise hooks for its caller. */ export type StoreWriterTask = { @@ -30,7 +31,12 @@ type ActiveStoreWriter = { storePath: string; }; -const activeStoreWriters = new AsyncLocalStorage(); +// Queue maps are often global singletons shared by separately bundled runtime +// chunks. Their reentrancy context must cross the same module boundary. +const activeStoreWriters = resolveGlobalSingleton( + Symbol.for("openclaw.activeStoreWriters"), + () => new AsyncLocalStorage(), +); function isActiveStoreWriter(queues: StoreWriterQueues, storePath: string): boolean { let active = activeStoreWriters.getStore(); diff --git a/src/talk/agent-consult-runtime.test.ts b/src/talk/agent-consult-runtime.test.ts index 5a4f3e87e9c1..c6f312ebd013 100644 --- a/src/talk/agent-consult-runtime.test.ts +++ b/src/talk/agent-consult-runtime.test.ts @@ -5,6 +5,7 @@ import type { ForkSessionEntryFromParentResult, } from "../auto-reply/reply/session-fork.js"; import type { SessionEntry } from "../config/sessions/types.js"; +import { runExclusiveSessionLifecycleMutation } from "../sessions/session-lifecycle-admission.js"; import { setRealtimeVoiceAgentConsultDepsForTest, consultRealtimeVoiceAgent, @@ -19,6 +20,7 @@ function createAgentRuntime(payloads: unknown[] = [{ text: "Speak this." }]) { { sessionId?: string; updatedAt?: number; + archivedAt?: number; sessionFile?: string; spawnedBy?: string; forkedFromParent?: boolean; @@ -126,6 +128,14 @@ function expectNonEmptyString(value: unknown) { expect((value as string).trim()).not.toBe(""); } +function createDeferred() { + let resolve = () => {}; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + describe("realtime voice agent consult runtime", () => { afterEach(() => { setRealtimeVoiceAgentConsultDepsForTest(null); @@ -209,6 +219,83 @@ describe("realtime voice agent consult runtime", () => { ); }); + it("rejects an archived consult session before mutating or starting work", async () => { + const { runtime, runEmbeddedAgent, sessionStore } = createAgentRuntime(); + sessionStore["voice:archived"] = { + sessionId: "archived-session", + updatedAt: 1, + archivedAt: 2, + }; + + await expect( + consultRealtimeVoiceAgent({ + cfg: {} as never, + agentRuntime: runtime as never, + logger: { warn: vi.fn() }, + sessionKey: "voice:archived", + messageProvider: "voice", + lane: "voice", + runIdPrefix: "voice-realtime-consult:archived", + args: { question: "What should I say?" }, + transcript: [], + surface: "a live phone call", + userLabel: "Caller", + }), + ).rejects.toThrow('Session "voice:archived" is archived. Restore it before starting new work.'); + expect(runtime.ensureAgentWorkspace).not.toHaveBeenCalled(); + expect(runtime.session.patchSessionEntry).not.toHaveBeenCalled(); + expect(runEmbeddedAgent).not.toHaveBeenCalled(); + }); + + it("fresh-checks archive state after a queued lifecycle mutation", async () => { + const { runtime, runEmbeddedAgent, sessionStore } = createAgentRuntime(); + const sessionKey = "voice:archive-race"; + sessionStore[sessionKey] = { + sessionId: "active-session", + updatedAt: 1, + }; + const mutationStarted = createDeferred(); + const releaseMutation = createDeferred(); + const mutation = runExclusiveSessionLifecycleMutation({ + scope: "/tmp/sessions.json", + identities: [sessionKey, "active-session"], + run: async () => { + mutationStarted.resolve(); + await releaseMutation.promise; + const entry = sessionStore[sessionKey]; + if (entry) { + entry.archivedAt = 2; + } + }, + }); + await mutationStarted.promise; + + const consult = consultRealtimeVoiceAgent({ + cfg: {} as never, + agentRuntime: runtime as never, + logger: { warn: vi.fn() }, + sessionKey, + messageProvider: "voice", + lane: "voice", + runIdPrefix: "voice-realtime-consult:archive-race", + args: { question: "What should I say?" }, + transcript: [], + surface: "a live phone call", + userLabel: "Caller", + }); + await Promise.resolve(); + expect(runEmbeddedAgent).not.toHaveBeenCalled(); + + releaseMutation.resolve(); + await mutation; + await expect(consult).rejects.toThrow( + 'Session "voice:archive-race" is archived. Restore it before starting new work.', + ); + expect(runtime.ensureAgentWorkspace).not.toHaveBeenCalled(); + expect(runtime.session.patchSessionEntry).not.toHaveBeenCalled(); + expect(runEmbeddedAgent).not.toHaveBeenCalled(); + }); + it("scopes sandbox resolution to the configured consult agent", async () => { const { runtime, runEmbeddedAgent } = createAgentRuntime(); diff --git a/src/talk/agent-consult-runtime.ts b/src/talk/agent-consult-runtime.ts index a7619bfbd434..ba1ebf711dae 100644 --- a/src/talk/agent-consult-runtime.ts +++ b/src/talk/agent-consult-runtime.ts @@ -2,11 +2,13 @@ import { randomUUID } from "node:crypto"; import type { RunEmbeddedAgentParams } from "../agents/embedded-agent-runner/run/params.js"; import { forkSessionEntryFromParent } from "../auto-reply/reply/session-fork.js"; +import { resolveSessionWorkStartError } from "../config/sessions/lifecycle.js"; import { parseSessionThreadInfoFast } from "../config/sessions/thread-info.js"; import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { RuntimeLogger, PluginRuntimeCore } from "../plugins/runtime/types-core.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; +import { beginSessionWorkAdmission } from "../sessions/session-lifecycle-admission.js"; import { deliveryContextFromSession, normalizeDeliveryContext, @@ -236,89 +238,131 @@ export async function consultRealtimeVoiceAgent(params: { const agentId = params.agentId ?? "main"; const agentDir = params.agentRuntime.resolveAgentDir(params.cfg, agentId); const workspaceDir = params.agentRuntime.resolveAgentWorkspaceDir(params.cfg, agentId); - await params.agentRuntime.ensureAgentWorkspace({ dir: workspaceDir }); - - // The consult session stores normal session metadata so subsequent voice turns can keep - // routing and, in fork mode, recover useful conversation context from the requester. const storePath = params.agentRuntime.session.resolveStorePath(params.cfg.session?.store, { agentId, }); - const resolvedDeliveryContext = resolveRealtimeVoiceAgentDeliveryContext({ - agentRuntime: params.agentRuntime, + const initialSessionEntry = params.agentRuntime.session.getSessionEntry({ storePath, sessionKey: params.sessionKey, - spawnedBy: params.spawnedBy, + readConsistency: "latest", }); - const sessionEntry = await resolveRealtimeVoiceAgentConsultSessionEntry({ - agentId, - cfg: params.cfg, - sessionKey: params.sessionKey, - spawnedBy: params.spawnedBy, - contextMode: params.contextMode, - deliveryContext: resolvedDeliveryContext, - storePath, - agentRuntime: params.agentRuntime, - logger: params.logger, - }); - const consultDeliveryContext = - resolvedDeliveryContext ?? deliveryContextFromSession(sessionEntry); - const sessionId = sessionEntry.sessionId; - - // Voice consults suppress verbose/reasoning output because the bridge needs a short, - // speakable answer, not agent-run diagnostics or hidden reasoning artifacts. - const result = await params.agentRuntime.runEmbeddedAgent({ - sessionId, - sessionKey: params.sessionKey, - sessionTarget: { - agentId, - sessionId, - sessionKey: params.sessionKey, - storePath, + const lifecycleAbortController = new AbortController(); + const sessionWorkAdmission = await beginSessionWorkAdmission({ + scope: storePath, + identities: [params.sessionKey, initialSessionEntry?.sessionId], + onInterrupt: () => + lifecycleAbortController.abort( + new Error("Realtime voice agent consult interrupted by a session lifecycle change."), + ), + assertAllowed: () => { + const currentEntry = params.agentRuntime.session.getSessionEntry({ + storePath, + sessionKey: params.sessionKey, + readConsistency: "latest", + }); + const changed = initialSessionEntry + ? !currentEntry || currentEntry.sessionId !== initialSessionEntry.sessionId + : Boolean(currentEntry); + if (changed) { + throw new Error(`Session "${params.sessionKey}" changed while starting work. Retry.`); + } + const archivedSessionError = resolveSessionWorkStartError(params.sessionKey, currentEntry); + if (archivedSessionError) { + throw new Error(archivedSessionError); + } }, - sandboxSessionKey: resolveRealtimeVoiceAgentSandboxSessionKey(agentId, params.sessionKey), - agentId, - spawnedBy: params.spawnedBy, - messageProvider: consultDeliveryContext?.channel ?? params.messageProvider, - agentAccountId: consultDeliveryContext?.accountId, - messageTo: consultDeliveryContext?.to, - messageThreadId: consultDeliveryContext?.threadId, - currentChannelId: consultDeliveryContext?.to, - currentThreadTs: - consultDeliveryContext?.threadId != null - ? String(consultDeliveryContext.threadId) - : undefined, - workspaceDir, - config: params.cfg, - prompt: buildRealtimeVoiceAgentConsultPrompt({ - args: params.args, - transcript: params.transcript, - surface: params.surface, - userLabel: params.userLabel, - assistantLabel: params.assistantLabel, - questionSourceLabel: params.questionSourceLabel, - }), - provider: params.provider, - model: params.model, - thinkLevel: params.thinkLevel ?? "high", - fastMode: params.fastMode, - verboseLevel: "off", - reasoningLevel: "off", - toolResultFormat: "plain", - toolsAllow: params.toolsAllow, - timeoutMs: params.timeoutMs ?? params.agentRuntime.resolveAgentTimeoutMs({ cfg: params.cfg }), - runId: `${params.runIdPrefix}:${Date.now()}`, - lane: params.lane, - extraSystemPrompt: - params.extraSystemPrompt ?? - "You are the configured OpenClaw agent receiving delegated requests from a live voice bridge. Act on behalf of the user, use available tools when appropriate, and return a brief speakable result.", - agentDir, }); - const text = collectRealtimeVoiceAgentConsultVisibleText(result.payloads ?? []); - if (!text) { - const reason = result.meta?.aborted ? "agent run aborted" : "agent returned no speakable text"; - params.logger.warn(`[talk] agent consult produced no answer: ${reason}`); - return { text: params.fallbackText ?? "I need a moment to verify that before answering." }; + try { + return await sessionWorkAdmission.run(async () => { + await params.agentRuntime.ensureAgentWorkspace({ dir: workspaceDir }); + + // The consult session stores normal session metadata so subsequent voice turns can keep + // routing and, in fork mode, recover useful conversation context from the requester. + const resolvedDeliveryContext = resolveRealtimeVoiceAgentDeliveryContext({ + agentRuntime: params.agentRuntime, + storePath, + sessionKey: params.sessionKey, + spawnedBy: params.spawnedBy, + }); + const sessionEntry = await resolveRealtimeVoiceAgentConsultSessionEntry({ + agentId, + cfg: params.cfg, + sessionKey: params.sessionKey, + spawnedBy: params.spawnedBy, + contextMode: params.contextMode, + deliveryContext: resolvedDeliveryContext, + storePath, + agentRuntime: params.agentRuntime, + logger: params.logger, + }); + const consultDeliveryContext = + resolvedDeliveryContext ?? deliveryContextFromSession(sessionEntry); + const sessionId = sessionEntry.sessionId; + + // Voice consults suppress verbose/reasoning output because the bridge needs a short, + // speakable answer, not agent-run diagnostics or hidden reasoning artifacts. + const result = await params.agentRuntime.runEmbeddedAgent({ + sessionId, + sessionKey: params.sessionKey, + sessionTarget: { + agentId, + sessionId, + sessionKey: params.sessionKey, + storePath, + }, + sandboxSessionKey: resolveRealtimeVoiceAgentSandboxSessionKey(agentId, params.sessionKey), + agentId, + spawnedBy: params.spawnedBy, + messageProvider: consultDeliveryContext?.channel ?? params.messageProvider, + agentAccountId: consultDeliveryContext?.accountId, + messageTo: consultDeliveryContext?.to, + messageThreadId: consultDeliveryContext?.threadId, + currentChannelId: consultDeliveryContext?.to, + currentThreadTs: + consultDeliveryContext?.threadId != null + ? String(consultDeliveryContext.threadId) + : undefined, + workspaceDir, + config: params.cfg, + prompt: buildRealtimeVoiceAgentConsultPrompt({ + args: params.args, + transcript: params.transcript, + surface: params.surface, + userLabel: params.userLabel, + assistantLabel: params.assistantLabel, + questionSourceLabel: params.questionSourceLabel, + }), + provider: params.provider, + model: params.model, + thinkLevel: params.thinkLevel ?? "high", + fastMode: params.fastMode, + verboseLevel: "off", + reasoningLevel: "off", + toolResultFormat: "plain", + toolsAllow: params.toolsAllow, + timeoutMs: + params.timeoutMs ?? params.agentRuntime.resolveAgentTimeoutMs({ cfg: params.cfg }), + runId: `${params.runIdPrefix}:${Date.now()}`, + lane: params.lane, + extraSystemPrompt: + params.extraSystemPrompt ?? + "You are the configured OpenClaw agent receiving delegated requests from a live voice bridge. Act on behalf of the user, use available tools when appropriate, and return a brief speakable result.", + agentDir, + abortSignal: lifecycleAbortController.signal, + }); + + const text = collectRealtimeVoiceAgentConsultVisibleText(result.payloads ?? []); + if (!text) { + const reason = result.meta?.aborted + ? "agent run aborted" + : "agent returned no speakable text"; + params.logger.warn(`[talk] agent consult produced no answer: ${reason}`); + return { text: params.fallbackText ?? "I need a moment to verify that before answering." }; + } + return { text }; + }); + } finally { + sessionWorkAdmission.release(); } - return { text }; } diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json index 00260b8c5901..3aafafcccaa4 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json @@ -1105,7 +1105,7 @@ }, { "deferLoading": true, - "description": "List visible sessions; filter by kind, label, agentId, search, activity. Use before sessions_history or sessions_send target selection.", + "description": "List visible sessions; filter by kind, label, agentId, search, activity, archive state. Use before sessions_history or sessions_send target selection.", "inputSchema": { "properties": { "activeMinutes": { @@ -1117,6 +1117,9 @@ "minLength": 1, "type": "string" }, + "archived": { + "type": "boolean" + }, "includeDerivedTitles": { "type": "boolean" }, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json index 247f80fb0286..ed31d1a27670 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json @@ -1137,7 +1137,7 @@ }, { "deferLoading": true, - "description": "List visible sessions; filter by kind, label, agentId, search, activity. Use before sessions_history or sessions_send target selection.", + "description": "List visible sessions; filter by kind, label, agentId, search, activity, archive state. Use before sessions_history or sessions_send target selection.", "inputSchema": { "properties": { "activeMinutes": { @@ -1149,6 +1149,9 @@ "minLength": 1, "type": "string" }, + "archived": { + "type": "boolean" + }, "includeDerivedTitles": { "type": "boolean" }, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json index 3894a04d7894..c865ff5dd1c5 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json @@ -1101,7 +1101,7 @@ }, { "deferLoading": true, - "description": "List visible sessions; filter by kind, label, agentId, search, activity. Use before sessions_history or sessions_send target selection.", + "description": "List visible sessions; filter by kind, label, agentId, search, activity, archive state. Use before sessions_history or sessions_send target selection.", "inputSchema": { "properties": { "activeMinutes": { @@ -1113,6 +1113,9 @@ "minLength": 1, "type": "string" }, + "archived": { + "type": "boolean" + }, "includeDerivedTitles": { "type": "boolean" }, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 84ce7914e9b0..8af8c17c7193 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -227,8 +227,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 50395, - "roughTokens": 12599 + "chars": 50483, + "roughTokens": 12621 }, "openClawDeveloperInstructions": { "chars": 3045, @@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6916 }, "totalWithDynamicToolsJson": { - "chars": 78060, - "roughTokens": 19515 + "chars": 78148, + "roughTokens": 19537 }, "userInputText": { "chars": 1535, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index 7a82f23ce45a..f01bf32b1883 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -227,8 +227,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 50084, - "roughTokens": 12521 + "chars": 50172, + "roughTokens": 12543 }, "openClawDeveloperInstructions": { "chars": 1936, @@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6513 }, "totalWithDynamicToolsJson": { - "chars": 76138, - "roughTokens": 19035 + "chars": 76226, + "roughTokens": 19057 }, "userInputText": { "chars": 1033, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 72a9e6275359..7a5e35e91f7c 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -228,8 +228,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 51374, - "roughTokens": 12844 + "chars": 51462, + "roughTokens": 12866 }, "openClawDeveloperInstructions": { "chars": 1955, @@ -240,8 +240,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6749 }, "totalWithDynamicToolsJson": { - "chars": 78371, - "roughTokens": 19593 + "chars": 78459, + "roughTokens": 19615 }, "userInputText": { "chars": 1271, diff --git a/ui/src/i18n/.i18n/ar.meta.json b/ui/src/i18n/.i18n/ar.meta.json index 29c20208b705..fd5040e93662 100644 --- a/ui/src/i18n/.i18n/ar.meta.json +++ b/ui/src/i18n/.i18n/ar.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:22:37.807Z", + "generatedAt": "2026-07-04T18:20:57.318Z", "locale": "ar", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/ar.tm.jsonl b/ui/src/i18n/.i18n/ar.tm.jsonl index 4fd40a7db0c7..eec0538d62a3 100644 --- a/ui/src/i18n/.i18n/ar.tm.jsonl +++ b/ui/src/i18n/.i18n/ar.tm.jsonl @@ -3,31 +3,28 @@ {"cache_key":"07d0a18463d8e9dfe536b077fef07cf767e3ddcd5831c2a79d0fca7d4bf57fe6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"ar","translated":"ملاحظات المشغّل","updated_at":"2026-06-16T14:15:27.040Z"} {"cache_key":"09d7ed4188351ed43023fca4e13106acd55914e6a163313651659cf888056b44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"ar","translated":"مهمة Gateway","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"0b298171ee36f89d998695f0c6072d800a9dd983636c58895331fdc246e70d24","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"ar","translated":"الجلسة","updated_at":"2026-06-16T14:15:34.787Z"} +{"cache_key":"0d3d4eea0f81666381e8fff87731b6efd3d80eb3401a53aad072d567cf3cafe3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"ar","translated":"استعادة الجلسة","updated_at":"2026-07-02T14:30:22.962Z"} {"cache_key":"15180164eeb8c004f2a57f8ea82ed48e83a706e86c87441a5d9987f01018d0f0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"ar","translated":"عرض التفاصيل","updated_at":"2026-06-16T14:15:27.039Z"} -{"cache_key":"17db4e142d43d4cd505390fdf65e2cb0087d35eb4f938ec94b674feea1960ee6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"ar","translated":"نسخ رمز الإعداد","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"1b701a3f34630b2c5cf2026b3ca35588ad3a76f0999436f126779e39c51e420d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"ar","translated":"العناصر","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"1bb67bd03a5259003cb18979044f40ee96d0b50a449e65065f33ddb5ba15a1fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"ar","translated":"البطاقات بدون وكيل محدد.","updated_at":"2026-06-17T14:14:58.728Z"} {"cache_key":"1de1f729171e18967f76a207f15eaca44b44fa087edb9eb78548516b09ff2197","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"ar","translated":"{count} عنصرًا","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"20b2e88b33793df99629daf65b657cb666cb9b4a71ea30e2c05f943939c49565","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"ar","translated":"التعليق","updated_at":"2026-07-01T01:07:19.841Z"} -{"cache_key":"20f1470b053a9a914f21010891606e57b8a0d842568628936c9cfda4fbaaf337","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"ar","translated":"إقران جهاز محمول","updated_at":"2026-07-04T16:47:12.791Z"} {"cache_key":"20f98bac595afb3715bf5acbd1e087d08570280150e8d7e0649e378a98ac0489","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"ar","translated":"لا توجد ملفات مطابقة.","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"222a52155c00d9125472c178e688a19fba317aa89a0fabcd3e6a77c0b930f30c","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"ar","translated":"انتهاك البروتوكول","updated_at":"2026-05-30T15:38:27.116Z"} -{"cache_key":"24a012378ab9085022da6783e6ed189def84f5b9b174548570cd326a535c7520","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"ar","translated":"يلزم الوصول كمسؤول لإنشاء رموز الإعداد.","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"28628884bf42001bb1106e9a421a05631fa1606a126aa1769ff909067eb3dfd8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"ar","translated":"كثافة بطاقات مريحة","updated_at":"2026-06-17T14:14:58.728Z"} +{"cache_key":"2b403b1fcffdd4ea08a6bc5f1dc8c12d59e8e8a293b20ea542cdbb020259a01d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"ar","translated":"المؤرشفة فقط","updated_at":"2026-07-02T14:30:22.961Z"} {"cache_key":"2b786815301e6972a6a9ef192e5fbc61483dc68c1860dd5c75df2d9f3a695e11","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"ar","translated":"{agent} (غير مُهيّأ)","updated_at":"2026-06-17T14:14:58.728Z"} {"cache_key":"2db23dc5db0199a24416b0fe590cac915fa36c9bd4b3586b7c6098575bf73fb3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"ar","translated":"فشل التحديث","updated_at":"2026-06-17T14:15:04.326Z"} {"cache_key":"30e6e7267a04dbe556f45fef9edb3af633ab75546a2d011c75f38f2fbd5317c1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"ar","translated":"منتهي الصلاحية","updated_at":"2026-07-01T10:32:31.521Z"} {"cache_key":"31baf5b9e4f804c6e3a61e02532c991cbb3d27c24b36ad51b9505cdc8bdf9f97","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"ar","translated":"ملاحظة الهدف","updated_at":"2026-05-29T21:01:05.556Z"} {"cache_key":"32bb33e694d9600daf879d9de557ac18f7114621a965d13947acef47bed0f87f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"ar","translated":"Skills: {skills}","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"344eb5fc143a6257bad30043dd1e5276992506d337a40e890cc68c7827de15eb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"ar","translated":"ملخص مساحة عمل الجلسة","updated_at":"2026-06-16T14:15:41.761Z"} -{"cache_key":"394ca3e636ea0e543560fb488db7d0994a3b3376fa6505befcca144c9cb380c7","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"ar","translated":"امسح رمز QR هذا في تطبيق الهاتف المحمول لتوصيل هاتف جديد.","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"3b5a4d5bde77f68a213cc7fe8fa99301db21f7abfec8869f1c0289416583907b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"ar","translated":"لم يتم التعامل مع أي ملفات في هذه الجلسة بعد","updated_at":"2026-06-16T14:15:34.787Z"} {"cache_key":"3d6682af16bb417d5a1877053f7bd3f74da5a314c36c848003e9b306e0c3d41e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"ar","translated":"لا توجد ملفات في هذا المجلد.","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"447c131c2bd2e343b3fb792a75f1fb4409801716aad5201157df74715f1b23a9","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"ar","translated":"العامل {state}","updated_at":"2026-05-30T15:38:27.116Z"} -{"cache_key":"45abe81d8286e514bbc2d599b33ce9bfdb33dec5c8d8d3f37ca6dbfe32feb443","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"ar","translated":"مراجعة","updated_at":"2026-06-17T14:14:58.728Z"} {"cache_key":"4bb28e9afda63bf46e4182639fb9b725c1cba5013470c24ada19c394ab1cbe1b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"ar","translated":"مسار مساحة العمل","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"4be427737e937eebb61b9821fbfec2f031dfc33120e00fe9e9076c9f7c2ba26d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"ar","translated":"تم التغيير","updated_at":"2026-06-16T14:15:41.761Z"} -{"cache_key":"4f01e5361b9b13d948ff1355176f4c300932d035d8d4150e4f1cfc5f429ac772","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"ar","translated":"رمز جديد","updated_at":"2026-07-04T16:47:12.792Z"} +{"cache_key":"4f59bc6df25e2495ede201a5cd831658d42000f5f645df19553afc9be514e0b2","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"ar","translated":"أرشفة الجلسة","updated_at":"2026-07-02T14:30:22.962Z"} {"cache_key":"56d2b9f7ed6b2bcefdfae9336946b14a96da27c781baf9d405b7100ab4930169","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"ar","translated":"آخر تحديث","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"571f1918b7f3976555081141e7c5e24cb588d8f0eca873af458c47400334f664","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"ar","translated":"البحث في الملفات","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"5c8aabd59beb6ef77a79452f2e5ac60a0de8c8eef5a71c273f2825d91ef664fc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"ar","translated":"قديم","updated_at":"2026-06-17T14:15:04.326Z"} @@ -40,12 +37,14 @@ {"cache_key":"67705efc36f4fd59149aaf0e64e0a77c76b6cc38c6fe7a51fcf48bd778d9e80c","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"ar","translated":"الروسية (Russian)","updated_at":"2026-06-26T21:43:32.403Z"} {"cache_key":"685580078c464cffb01fba647ae14db5d572c570d640c230842de054c1cf2585","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"ar","translated":"كل الجلسات","updated_at":"2026-07-03T07:38:39.676Z"} {"cache_key":"69febdd4f0d2bd27da0c3d1b40c234a16264866e1a9047c43bdba454fb1d75d7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"ar","translated":"بروتوكول العامل","updated_at":"2026-06-16T14:15:27.039Z"} +{"cache_key":"6e72b3484a4b260082596e6a561cf0a45364fbe344b4b7ec098cc234ffbf9987","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"ar","translated":"إعادة تسمية الجلسة","updated_at":"2026-07-02T14:30:22.961Z"} {"cache_key":"708f7fa69eda584e6034ed3fd424c90e0c1d8c6f3bc0899754aa0a9685a0b78e","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"ar","translated":"خوادم MCP، والمصادقة، والأدوات، والتشخيصات.","updated_at":"2026-05-31T05:36:42.114Z"} +{"cache_key":"70a218e2172f80df8964fec614441ac780584802ca8d9e02134d666459aaf709","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"ar","translated":"إظهار الجلسات المؤرشفة فقط.","updated_at":"2026-07-02T14:30:22.961Z"} {"cache_key":"7171731fdc606f54bcffcf226ceb013e82b0869f2df180bd9d00f686589697c4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"ar","translated":"طي مساحة عمل الجلسة","updated_at":"2026-06-16T14:15:34.787Z"} {"cache_key":"732ca472029046ca18ebf8398747ced1c60994e3c2a0555b164aa3a52730691b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"ar","translated":"محظور","updated_at":"2026-06-17T14:14:58.728Z"} +{"cache_key":"738bb1b09f84bd01b5a3eacd158f0d8a272957838c6b8ee0b4bad02a9b991cff","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"ar","translated":"استعِد هذه الجلسة لإرسال الرسائل.","updated_at":"2026-07-02T14:30:22.962Z"} {"cache_key":"73a457b2179107dd5d1426e0fec360f1cf9f00ec5ed7b3e77594575ef9660ad0","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"ar","translated":"الهندية (Hindi)","updated_at":"2026-06-26T21:43:32.403Z"} {"cache_key":"74cc623b957f70b35f6fa003d2b2f6cf8343bfa56087b0a673520cc111d226b6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"ar","translated":"جميع البطاقات","updated_at":"2026-06-17T14:14:58.728Z"} -{"cache_key":"77897aeb08e90f035c73942276cdd5ad3a6b71b1ed1e12a70135da716d6188c8","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"ar","translated":"مساعدة الإقران","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"7861b489b6754a2b4e68f72ce2d3c1c3a539bb8e2181026222020d021f025c72","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"ar","translated":"الاحتفاظ بالتعليق بعد الإجابة النهائية","updated_at":"2026-07-01T01:07:19.841Z"} {"cache_key":"7d60d8554dcfb8d41137e8314ecceeffee430377085a746514a22d59c6140240","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"ar","translated":"مساحة العمل: {workspace}","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"7e044004d8cce1e8fd0122556a521aa760fe712e2c33164c9e8ca17cbbcc2d25","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"ar","translated":"{count} بطاقة","updated_at":"2026-06-17T14:14:58.728Z"} @@ -74,15 +73,11 @@ {"cache_key":"a799f78fb8d8cab2a44a19ba1f2614b952c22172817b269f0e8316856ef95dd5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"ar","translated":"{count} جاهزة","updated_at":"2026-06-16T14:15:34.787Z"} {"cache_key":"a87670d94be192fb5a79a15fbd43b3ec4e6f4b5cff08ba0a9e81f309c8221ae8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"ar","translated":"نتائج البحث","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"a8e4f6965e837401e257f7489e4021746c30bc5500b8192b8ff7aa632c562e8d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"ar","translated":"أضف قرارًا أو عائقًا أو ملاحظة إثبات...","updated_at":"2026-06-16T14:15:34.787Z"} -{"cache_key":"a90ae97eb1e58a2b0a731f418ebb5e4c7491f3cc1e477d9875f5a8fd99b59eb7","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"ar","translated":"إدارة الأجهزة","updated_at":"2026-07-04T16:47:12.792Z"} -{"cache_key":"a98e9f542266d3438077541875aaaa28c55ef726864cf5f9bc7017c242b30b6f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"ar","translated":"تتصل تطبيقات OpenClaw الرسمية للأجهزة المحمولة تلقائيًا بعد المسح.","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"b0fe8a7e77db10bf497226f711f15db25b4c0ecc915174c2250999c582a349cf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"ar","translated":"ملفات المشروع","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"b21f9533c2b71b808463481f3809e658ed2af29251c4bf7b3e97b35cf06f8512","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"ar","translated":"الجذر","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"b2e5905a66b6e1d0937b7acbbff5968bd206bdcbdfc24d4b894949ee2bffe0a1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"ar","translated":"غيّر العرض أو البحث أو الأولوية أو الوكيل أو فلتر الأرشيف.","updated_at":"2026-06-17T14:15:04.326Z"} {"cache_key":"b309694f44e2b0afebb62af6aec30b2815ae6588b5dc435836abcdcc67b2b8a4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"ar","translated":"مفقودة","updated_at":"2026-06-16T14:15:34.787Z"} -{"cache_key":"b380a4c4396ad3a00a437e0129dd14f0f906df24bc4286dbbae6bfe4f94ebfe1","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"ar","translated":"إظهار رمز الإعداد","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"b3d797c3884a888f9f6d0cebacc92c89b622e3866c5a0b0d83a58611797f812e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"ar","translated":"عرض لوحة العمل","updated_at":"2026-06-17T14:14:58.728Z"} -{"cache_key":"b53684177b8ce5bfb07a4b9f69801766d96c3d3bf456d8f0883b57bafd3a196b","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"ar","translated":"طلبات الأجهزة بانتظار المراجعة: {count}","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"b5bce556612af8d7432ccf09d806b6bde7777e8e03d88a5403773ef5b5899215","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"ar","translated":"تحديث تلقائي","updated_at":"2026-06-17T14:14:58.728Z"} {"cache_key":"b5f6b03e2a0a61acc557fbdcd0fdca67e663653591011a919bd7a290fff8abce","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"ar","translated":"تم التحديث {time}","updated_at":"2026-06-17T14:15:04.326Z"} {"cache_key":"b6849367e69f4d3ab4f93ec9604b16f6d0db30d967d3007df19015f6e988ca44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"ar","translated":"30 ث","updated_at":"2026-06-17T14:15:04.326Z"} @@ -92,6 +87,7 @@ {"cache_key":"bd91083b860fca9e7b7c865dc565e5d33577aedb51444e69b9c1879431081c65","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"ar","translated":"الأتمتة","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"c005360fbda0d931c7b39fe4c4fe18b3c5d457ac3e23d6a0ac8f4b6cf1bdcda3","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"ar","translated":"منبّه الموزّع","updated_at":"2026-05-30T15:38:27.116Z"} {"cache_key":"c05bb7c1f5bbceeaab7d8e36c991a98e1be5f36df8cb86876c56cbb96f90d1fe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"ar","translated":"إضافة ملاحظة","updated_at":"2026-06-16T14:15:34.787Z"} +{"cache_key":"c0d6d9e62ebcede762e0b8d9b52f4c3b03579bcbd0a4779d7afef36cd13338fe","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"ar","translated":"إعادة تسمية الجلسة","updated_at":"2026-07-02T14:30:22.961Z"} {"cache_key":"c19dc4c22c3fa18ea0719010644e229be1ac61e4a44be75d2376e2cfcc7cc647","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"ar","translated":"قيد التشغيل","updated_at":"2026-06-17T14:15:04.326Z"} {"cache_key":"c233241cc23d7d2177c4d793589327b8469616ba77558d39e5dde23793e6e497","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"ar","translated":"التشخيصات","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"c47db2bbef13e9c6cea5ccfce9edc7d0d4ddd749672c836d0c9460e5b59b04a9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"ar","translated":"انتهت صلاحية رمز الاقتران QR","updated_at":"2026-07-01T10:32:31.521Z"} @@ -105,8 +101,6 @@ {"cache_key":"cdbbd4457bcc9e07450a6d7e0d7e3ad6957776bb7a2b3ba4623d522dd556523e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"ar","translated":"محاولات فاشلة","updated_at":"2026-06-17T14:15:04.326Z"} {"cache_key":"ce4f953ca30562add25d39724fbd5b7265cff565e2f1919602bbcf28c4cb2c7f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"ar","translated":"محظور","updated_at":"2026-06-17T14:15:04.326Z"} {"cache_key":"d1c88b87eb9d246da3b4bd8b72ab2f2c953a8354fd17b57343ca057d076d93d5","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"ar","translated":"خطأ في الأداة","updated_at":"2026-05-31T06:43:57.803Z"} -{"cache_key":"d313e74b7d651a8f5aee2b4e63f1090c2d7923ab8360d37fbf7ea60523279a19","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"ar","translated":"رمز QR غير متاح. انسخ رمز الإعداد بدلًا من ذلك.","updated_at":"2026-07-04T16:47:12.792Z"} -{"cache_key":"d3d6db6fc977b940248fdaeb78ac231448bc3904e4c4feb1615a7169f4b8cefd","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"ar","translated":"تعذر إنشاء رمز إعداد.","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"d3f65bc2f28d2a47a2803589105f2532f2fecd3041a92822a0450147c672c170","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"ar","translated":"في انتظار التبعيات: {parents}.","updated_at":"2026-06-16T14:15:34.787Z"} {"cache_key":"d62f0d36f678aa4b3430a8108f75ad5af9539734cba60b9da0e0739e65b98c54","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"ar","translated":"المجلد الأصلي","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"d6a6fd89a6ca52fb4de8c081a3b21b323ceb980fe5a3f341b5b629b547a491b3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"ar","translated":"جاهز غير مُعيَّن","updated_at":"2026-06-17T14:15:04.326Z"} @@ -116,24 +110,24 @@ {"cache_key":"dd91daeca04bb82d249aa3c4e37d428abea62442b176b84e6e38e38fce450cc4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"ar","translated":"استخدام المحادثة الحالية","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"dd91de2fa327547c02aa43bd3b4c2d75146d09cc6e1abce547b01b1cc9ed618b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"ar","translated":"تمت القراءة","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"ddb169dfbfdcf91ad52b810c9e72d18dae6e2e7444b3f676a3254cd01dde0161","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"ar","translated":"{count} معروضة","updated_at":"2026-06-16T14:15:41.761Z"} -{"cache_key":"e21531bd5eaab5abc96b5f8eb559c5fc5e02355d502fbf5e09f27cd1946ed880","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"ar","translated":"OpenClaw للأجهزة المحمولة","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"e23352876d234513223dd70fcd8f259938027b9a17c307142be450528006f6fe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"ar","translated":"مساحة العمل","updated_at":"2026-06-16T14:15:34.787Z"} +{"cache_key":"e2b798d7bec27d887656b055ab8643d959d1f5201d9aa70ead44f9f0450a442b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"ar","translated":"مثبّتة","updated_at":"2026-07-02T14:30:22.961Z"} {"cache_key":"e6524eced9d1d75aba4b448b6d634ed4c879c1b5a10965d3e81db152b56b14eb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"ar","translated":"5 ث","updated_at":"2026-06-17T14:15:04.326Z"} {"cache_key":"e939a79038d1554b14789344422e8b6793ec9b92e709c1a6a194fb9b1cd5720a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"ar","translated":"الوكيل الافتراضي","updated_at":"2026-06-17T14:14:58.728Z"} {"cache_key":"e9cb068cd1a94066a144d34441487719ad779c59fb6d17a2b85bb8156cf84bfe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"ar","translated":"نبض {age}","updated_at":"2026-06-17T14:15:04.326Z"} {"cache_key":"eba4c95b26b65f610116dc374592e0f392fc07534e00cbbf8619b1b689b979af","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"ar","translated":"إثبات","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"ec6a4e2eede0729a7d1c3f96bf1d640916db3d15365d01c708924fa384bf804f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"ar","translated":"15 ث","updated_at":"2026-06-17T14:15:04.326Z"} +{"cache_key":"ed4cc5cbce34a303630df5de1a46fc57b9b1375e7023f68b0156db0ae9b4cc12","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"ar","translated":"تثبيت الجلسة","updated_at":"2026-07-02T14:30:22.961Z"} {"cache_key":"ed5fe2073ec30c7509c5ecd30a07f651adcc6108c1f7f34a174126e60fe5db80","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"ar","translated":"كثافة بطاقات مدمجة","updated_at":"2026-06-17T14:14:58.728Z"} {"cache_key":"eda32d5a2e43607eebfb01c0b5a8321f7729f5fadae7c6b6ad72b7d700bd58eb","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"ar","translated":"راجع المقترحات وحسّنها وطبّقها قبل أن تصبح مهارات نشطة.","updated_at":"2026-05-31T21:48:26.917Z"} {"cache_key":"ef1a3af954643de28b50947b690c7bb7b308c116f51ef4c645916a1545b8baf0","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"ar","translated":"MCP","updated_at":"2026-05-31T05:36:42.114Z"} -{"cache_key":"efc6af37f49b7844683406efd04fd0b068b149f625e70a23300736fe995f2a66","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"ar","translated":"رمز QR لإقران OpenClaw للأجهزة المحمولة","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"f20dc0bab783ad6666e8debdbcb775f5c5f4151fa75824a7b6e146ad35f3bbe9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"ar","translated":"{count} تم تغييرها","updated_at":"2026-06-16T14:15:41.761Z"} {"cache_key":"f3c58c5476fcc120fbee5bbe697442b9d617065d471bee6b35cffd853d7b4b34","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"ar","translated":"شغّل ‎/pair qr‎ مرة أخرى لإنشاء رمز إعداد جديد.","updated_at":"2026-07-01T10:32:31.521Z"} {"cache_key":"f4ac00d519c812ca6aee67bb3ff6737d6130e02187c343b6efa70733049645cb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"ar","translated":"غير معروف","updated_at":"2026-06-16T14:15:34.787Z"} {"cache_key":"f55994fff11b7bd2579094af9cc82e05db12ad10559ed6879bce8d5e200f81c5","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"ar","translated":"تمت إضافة المرفق","updated_at":"2026-05-30T15:38:27.116Z"} {"cache_key":"f5dd1a3dceb24158d410cf98ac8e3cbaa855655d0588bde767fe2b74f5279abe","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"ar","translated":"التنسيق","updated_at":"2026-05-30T15:38:27.116Z"} +{"cache_key":"f805d3687a1005ce56362ac06a7f825158f5c37bc9c09824ffd13fe53f4ee994","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"ar","translated":"إلغاء تثبيت الجلسة","updated_at":"2026-07-02T14:30:22.961Z"} {"cache_key":"f9eedc106a641d38e6448ae7fa906c55f90fa6e62c9cec090275c7ce8ac9d317","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"ar","translated":"ورشة Skills","updated_at":"2026-05-31T21:48:26.917Z"} -{"cache_key":"fa4694663916de0516134693ba2df927d49e8262c0b3a6c6d177c92318034ed6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"ar","translated":"جارٍ إنشاء رمز إعداد آمن…","updated_at":"2026-07-04T16:47:12.792Z"} {"cache_key":"facb678991931b3ad169c22e96e1e00416ef6280e8822170dc2b75e6b070fa58","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"ar","translated":"{count} مرفقات","updated_at":"2026-05-30T15:38:27.116Z"} {"cache_key":"fbbebeaeb00675160d3e6b43b349ca1b0348f60d6ae9d6af656971a6a55a950e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"ar","translated":"تشغيل","updated_at":"2026-06-16T14:15:27.039Z"} {"cache_key":"fd62fbf939e7fcff810d10222fa51cf501598d19c82bb8028a3923f3a2220557","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"ar","translated":"{count} س","updated_at":"2026-06-17T14:15:04.326Z"} diff --git a/ui/src/i18n/.i18n/de.meta.json b/ui/src/i18n/.i18n/de.meta.json index 484f8374be61..6f714afe81b7 100644 --- a/ui/src/i18n/.i18n/de.meta.json +++ b/ui/src/i18n/.i18n/de.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:21:15.126Z", + "generatedAt": "2026-07-04T18:20:52.190Z", "locale": "de", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/de.tm.jsonl b/ui/src/i18n/.i18n/de.tm.jsonl index 7c3704603b43..869a011824b1 100644 --- a/ui/src/i18n/.i18n/de.tm.jsonl +++ b/ui/src/i18n/.i18n/de.tm.jsonl @@ -15,17 +15,14 @@ {"cache_key":"215bfdcd203a162a4270c5eafabeef308b0a0f52349714b3ea45395cd8f4814f","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"de","translated":"Anhang hinzugefügt","updated_at":"2026-05-30T15:38:11.508Z"} {"cache_key":"222981f3dabff8f006dd110acf52ee80162d3e8d00d35cc3937c86baa8fab05f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"de","translated":"Nicht zugewiesen (verwendet {agent})","updated_at":"2026-06-17T14:13:07.390Z"} {"cache_key":"23a98d848479c9a7a6f4f006588ce8b12b77ba405961be20416a2b9365a8c4a2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"de","translated":"Fehlender Nachweis","updated_at":"2026-06-17T14:13:07.391Z"} +{"cache_key":"29be66e77cb30fd6a98d6766164c9afbe015638d2949c640cab3c2ba2ab31a10","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"de","translated":"Stellen Sie diese Sitzung wieder her, um Nachrichten zu senden.","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"2b9d1af0d015e80d3f6801086c957b49c37e3c6f12955494376638da668d2b96","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"de","translated":"Zusammenfassung: {summary}","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"2c2ece6f95fad589f09f4a93431c584cd31e3df4537b1211ce6f0572f6a34b4f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"de","translated":"Aktuellen Chat für Überarbeitungsanfragen verwenden","updated_at":"2026-06-16T14:12:53.111Z"} -{"cache_key":"2ed8057c49fb0b0bc074c7dff7c5a19074e47d388c6e6c4320432f37e24c44b6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"de","translated":"QR-Code nicht verfügbar. Kopieren Sie stattdessen den Einrichtungscode.","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"2f35e2351b0a1ad96639ba44b49d0b7bb36ab489b419747a0531bb7804568b70","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"de","translated":"Sitzung","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"2f723e14f564dffd937c778a05ea3d3df3c29ba944075100d6cf6f4a15a89873","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"de","translated":"Heartbeat {age}","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"3291b27d87c9b085d83645bf3d093be323f1809505f54b9edf8bd2691601fde6","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"de","translated":"Ziel","updated_at":"2026-05-29T21:00:05.331Z"} -{"cache_key":"32b2d2e848eb9f540af0794eb8a7a632e8b24fa58c6305373dc3808db5850c66","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"de","translated":"Einrichtungscode konnte nicht erstellt werden.","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"3370e1d00f22cfdaceffac5de196a27c9b29c7819554bd695abdf03e239885cc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"de","translated":"Warten auf Abhängigkeiten: {parents}.","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"33e8316ed3f105834e2d783ab819b39a6937433244142b909de5f196b173e223","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"de","translated":"{count} angezeigt","updated_at":"2026-06-16T14:13:06.672Z"} -{"cache_key":"35df08aea2e63104c285793f63282e2f2cd076c5a188c68449b003c972cb5674","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"de","translated":"Einrichtungscode anzeigen","updated_at":"2026-07-04T16:44:25.985Z"} -{"cache_key":"384267d4d45b9f9fe476e3b7c7e8f9427784c766da2e1b6a9d70b95e8faf63de","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"de","translated":"Administratorzugriff ist erforderlich, um Einrichtungscodes zu erstellen.","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"38e90629c5ab653eb98c51b46a96cd32b666c4df22f51e52491b54bf55937b4f","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"de","translated":"हिन्दी (Hindi)","updated_at":"2026-06-26T21:43:23.990Z"} {"cache_key":"3cac7757683c4ca9fcf12c3f70f783d1ae4108c8657514a1261cf62d164a1253","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"de","translated":"{count} geändert","updated_at":"2026-06-16T14:13:06.672Z"} {"cache_key":"3d2cf57b5c53bbfb394245677d806a93358a500f039a740fe47a94b85b83c94a","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"de","translated":"MCP","updated_at":"2026-05-31T05:36:35.020Z"} @@ -35,6 +32,7 @@ {"cache_key":"47545fc886536f8fbf74932dd9c172a99996c2ae64913f57f3521300cac25864","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"de","translated":"{count} blockiert","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"4888f94f0e820114a64974fac6b531eb53210a405325ba1c40436112f6d38259","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"de","translated":"Kompakte Kartendichte","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"48cb3024f0b889ab34973c00042efd0f1fa797b21ea1277b29a8dcbc794faee8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"de","translated":"Alle Karten","updated_at":"2026-06-17T14:13:07.391Z"} +{"cache_key":"4aff5cea726f683a13e23e59ef08da51453f62cdac93115f5cb7e52e10f9d6b3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"de","translated":"Nur archivierte","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"4bd58ebe329a26f286d6d26d93acaaeacf1c7849aa73ae8793a848d7ef27dcae","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"de","translated":"Aus","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"4ebf745cc2315636a9fe6b4aa41b941705089a811d0fb3bca976114c0525102c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"de","translated":"Worker-Protokoll","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"4effebd1aaac8a0588f229497bfeb16f3ac3f6dc34bcd867d0b6771464423b68","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"de","translated":"Bereit","updated_at":"2026-06-17T14:13:07.391Z"} @@ -43,6 +41,7 @@ {"cache_key":"563460816d3948dc9e063429950fd02b7f97ccd41ea95b4f82aef2639ca3c805","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"de","translated":"Skills: {skills}","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"57473c8a09374714eceaf4b6a43a37ce1d388c4b12e5d78f1d569698095651fb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"de","translated":"Es werden die ersten passenden Dateien angezeigt. Verfeinern Sie die Suche, um die Ergebnisse einzugrenzen.","updated_at":"2026-06-16T14:13:06.672Z"} {"cache_key":"5786703813168994fba14e4dff988d5692e5e29a64cf341eb483932fe9d3e06e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"de","translated":"Kommentar nach der endgültigen Antwort behalten","updated_at":"2026-07-01T01:06:02.805Z"} +{"cache_key":"595a2bc0419491adf3174e4a49cb1303005277ea35e7a5eccd1035ff8f55be0d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"de","translated":"Sitzung umbenennen","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"59a3e874dc9b1299daf4a2644ec2659a6e9c50587c91ac357f24215c16b94169","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.cwd","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"CWD","text_hash":"0217f1cb7725737f15a6710df3bcfa3bc10a239f0f7801ec3d7168e675f5ebd6","tgt_lang":"de","translated":"CWD","updated_at":"2026-06-16T14:13:08.672Z"} {"cache_key":"5b0ab59138fd65f7f3a236db3c04458d74c99539c8ab736086c9637395c06d55","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"de","translated":"Sitzungsarbeitsbereich aufklappen","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"5f60d04e50be0e22c14d761f808667c2dfd5623e67247c367c1d280613bcffca","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"de","translated":"{count} Worker-Protokolle","updated_at":"2026-05-30T15:38:11.508Z"} @@ -52,8 +51,9 @@ {"cache_key":"68b6aafe6c75b6205bdeb1b62600b4e2d232b9f0bc1c775f3f42f67bf23913b4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"de","translated":"Führen Sie /pair qr erneut aus, um einen neuen Einrichtungscode zu generieren.","updated_at":"2026-07-01T10:30:56.717Z"} {"cache_key":"6a93f1e03c824aa1b6f5e52390b126fe4253721b69eea2f468ca6e3b8dae2147","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"de","translated":"Projektdateien","updated_at":"2026-06-16T14:13:06.672Z"} {"cache_key":"6b16c393f4aba8aa0c9bca7002325ca7c31ba9ac3e43f5f53caa46dd9dcd0cc1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"de","translated":"Standardagent","updated_at":"2026-06-17T14:13:07.391Z"} -{"cache_key":"6b4d104efa25648b9e18f7336500abf29af7ca1422d5a57320714e397aef50f6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"de","translated":"Scannen Sie diesen QR-Code in der mobilen App, um ein neues Telefon zu verbinden.","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"6cdcd04cff08f31e6968e0874474f397c04a51d8bbb7c59fe7ee520ea7d11851","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"de","translated":"Keine Karten entsprechen dieser Ansicht","updated_at":"2026-06-17T14:13:12.251Z"} +{"cache_key":"6f1dc6a5c7bcdc586c12df1c1909d43a4b4153bd4780c878501d432248a3660f","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"de","translated":"Sitzung archivieren","updated_at":"2026-07-02T14:30:04.036Z"} +{"cache_key":"713e281048dce15dcceabc3d3b71a4d02ad0c78bff98e219681266d035cf88a9","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"de","translated":"Angeheftet","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"7431ce4c8c6fda32a44e65f3881f395281632b9c2af979b87d2326db8b9b7a5c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"de","translated":"Workspace: {workspace}","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"7a8f93ecfc4045545dd27474b1163e402cea16c1f59237fd0da8f9799894c858","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"de","translated":"Sitzungsarbeitsbereich wird geladen…","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"7c487658d1333c0af906ea1e9829964a34be7c7ff142fe79b6de18c62df4def4","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"de","translated":"Skill Workshop","updated_at":"2026-05-31T21:48:18.221Z"} @@ -65,27 +65,23 @@ {"cache_key":"85d619d86696656edaaed6116ea685a722870d484055301d2ae2a434c94fdee8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"de","translated":"Ändern Sie die Ansicht, Suche, Priorität, den Agenten oder den Archivfilter.","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"88064bbd82ec63409db6979375b172b5087d6c5520eac77aba0005f0ef476dbb","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"de","translated":"Vorschläge prüfen, verfeinern und anwenden, bevor sie zu aktiven Skills werden.","updated_at":"2026-05-31T21:48:18.221Z"} {"cache_key":"88cb3c131cf937211dd7de347b8031dac54e11224211b3bae8d8d7cbd42c0197","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"de","translated":"Orchestrierung","updated_at":"2026-05-30T15:38:11.508Z"} -{"cache_key":"895a5cccd9b117b47be37bb5e2cc88ee8bcd1164548697bf86b0fef31abbde67","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"de","translated":"Neuer Code","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"8b1c1eda411ab2416305f6c9ba0cc166e3490f1063510c2d9e5fcd8562da1581","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"de","translated":"Ausführung","updated_at":"2026-06-16T14:12:53.111Z"} -{"cache_key":"8d458992ed3ac9a3434106b57a523be0dadc283bb64625c7e7dcdc666539a96a","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"de","translated":"Sicherer Einrichtungscode wird erstellt…","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"8d8bee4267ec378ae0371bdd8deeb6a54c511cfe21e49e1135ea89d35dc44d41","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"de","translated":"Worker {state}","updated_at":"2026-05-30T15:38:11.508Z"} {"cache_key":"8e6e700ed5967e363855bbcc83f6be2eab783b3d7955961f297e537c380c6dba","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"de","translated":"Unbekannt","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"8f9e6fbe70d4a2bdf14fcf3060c1b346f9c0dc71f6efd2ad99dcaaf4fe117739","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"de","translated":"Vorschau","updated_at":"2026-06-16T14:13:08.672Z"} {"cache_key":"8fac9eb935bc659318a8f2b618675e4bde9b72fd1e3c808af178904190fad6c2","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"de","translated":"{count} Anhänge","updated_at":"2026-05-30T15:38:11.508Z"} {"cache_key":"8fcd339edd96fc44a5551b0ecfc2082c5450aa678ae7c5a8f9fad41954f560c9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"de","translated":"Notiz hinzufügen","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"908b411a083d4600fb56427c4affaa1c360ea61d23315e4ed8691f5ba88cc8d7","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"de","translated":"Dispatcher anstoßen","updated_at":"2026-05-30T15:38:11.508Z"} -{"cache_key":"9120afead2cd75fff0bfb46774c013ef04155ee77e073c53c1b1698a03c00433","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"de","translated":"Überprüfung","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"91f443ba5b7ba09906868e6df706c499e0bf1f8768a2a9b45b2ee2397cc007f0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"de","translated":"5s","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"97b76947e97aaa2a5b8850bc347ee67f11ef4c832165d1c89ade21fae9296e66","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"de","translated":"Gateway-Aufgabe","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"9bcb1fb9dd98cc8afe7e07293615e056bc79e9eda22bbeb4a9831e13530d9ffb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"de","translated":"60s","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"9dd671afcd83f8520477bdaae5c3ac1f1445032301322d61bb5b9de9b9e557ef","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"de","translated":"{count}h","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"9ea088f95993e4e1fd7be6ad73ad533c01bc79aace908b70776bc88e3c470a15","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"de","translated":"Stamm","updated_at":"2026-06-16T14:13:06.672Z"} {"cache_key":"9ed765600e963d86c337ce7df810feb36a184ac6a3314e8fcaaa0cd8316d7d39","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"de","translated":"{count} Karten","updated_at":"2026-06-17T14:13:07.391Z"} -{"cache_key":"a037ccbc96cf7ff6fcfcca43597e4ad32aded3335f9160f2e32ac4c72011af72","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"de","translated":"Mobilgerät koppeln","updated_at":"2026-07-04T16:44:25.984Z"} +{"cache_key":"9efb288d0b3ff5d7cc3780ada172ff057d99f3588a0bb593a67aca20925986c2","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"de","translated":"Sitzung anheften","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"a0cb083257f573efd2bafd0dfd27b6f0fe8486df0f87489c1efc39f360a72318","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"de","translated":"Aktuellen Chat verwenden","updated_at":"2026-06-16T14:12:53.110Z"} {"cache_key":"a1e0376c9cf4b3449312840920e19848bd91d6d6e624aba16fc37bac4dd86c82","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"de","translated":"Protokollverstoß","updated_at":"2026-05-30T15:38:11.508Z"} {"cache_key":"a5443cc44c106268ba917833e9ba2fea70b44c14b885f1cd64ef14a2800803a2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"de","translated":"Komfortable Kartendichte","updated_at":"2026-06-17T14:13:07.391Z"} -{"cache_key":"a8a238f5d072b601980dcfa64169653bef10e2fa9a4c0c89fdcefaa3e281c14f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"de","translated":"Offizielle mobile OpenClaw-Apps verbinden sich nach dem Scannen automatisch.","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"aa402f682e15d55fe0919f4cbf5b8fd99f55c894c1a24509ddb6de981863b421","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"de","translated":"Aktualisiert","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"abb4fa03f4741e5e8793b4053c417b4dcf7b9ea63b64883379d09c425fa959e4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"de","translated":"15s","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"abcbe3ba5df9edae1ccdc1af61773648e0af82001a43d7a3d4e568b76d6b0b27","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"de","translated":"Русский (Russisch)","updated_at":"2026-06-26T21:43:23.990Z"} @@ -102,38 +98,36 @@ {"cache_key":"b6662447466f4ec8f56ff69e891f5a1812ba507ce772ffbe46b1ce1a36899144","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"de","translated":"Keine passenden Dateien.","updated_at":"2026-06-16T14:13:06.672Z"} {"cache_key":"b6dc2c84c8c1c38be632b308e93ba2ac92ecf5426015f11cbcd2700231313586","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"de","translated":"Zusammenfassung des Sitzungs-Workspace","updated_at":"2026-06-16T14:13:06.672Z"} {"cache_key":"b7d1dfb8a82266cd2a531a5586e0827022b2efcfadcf8d6841d129b66b4933f0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"de","translated":"fehlgeschlagene Versuche","updated_at":"2026-06-17T14:13:12.251Z"} -{"cache_key":"b8e045f6eefc5235a85982e95f24d134fefcd88f1da4d7843d3248340b5c09df","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"de","translated":"OpenClaw mobile","updated_at":"2026-07-04T16:44:25.985Z"} +{"cache_key":"bb40657bdb3cca8849d74adfc4aa2a36eff32696a8dc04a78f271a71f9e0ad98","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"de","translated":"Sitzung umbenennen","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"bdd4c50f0099d45b3e313cad2d0b3f7068bd200a3634371c014dcba67f9a1002","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"de","translated":"Kartendetails","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"bfe958245bd105456a4491c5d3b1fda2866dd30dd0563255765cdf8fa0f859c0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"de","translated":"bereit, nicht zugewiesen","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"c2c19da5340707d83522b2fa42c7ca053b5b8dc49211f2107b84e5accdf52b07","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyMissing","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{parent} (missing)","text_hash":"8daa419059727391c01e3b7021e05d8d70b4da67f9c57cd2d80f302af77aac53","tgt_lang":"de","translated":"{parent} (fehlt)","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"c2eb49eb497f366bb3d993de133466265379108d6ff88af6d3c8b0c677c32d58","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"de","translated":"Mandant: {tenant}","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"c4771eae6a73503a6758ad3debd186ec31fe571e991dbc8e8879f5309ab1251e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"de","translated":"Dateien suchen","updated_at":"2026-06-16T14:13:06.672Z"} -{"cache_key":"ccee08936545afa8596ed18930e170577e9ea13f8d262d8f37684ea32de7a973","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"de","translated":"Geräte verwalten","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"ccee1cdfcc7c2115b14c65277a37a4dbd6b49d3ae4cfa37db23c9ed30377df95","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"de","translated":"Automatische Aktualisierung","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"d03fb625bb64bb537044a2169d6c1eafeacfd46687a7acfb3db01f251d541e5e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"de","translated":"Kürzlich erledigt","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"d38ac7e8c801f19e2899898714f2c27397c75565f456202d650e6d2ff750c023","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.copyPath","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"de","translated":"Pfad kopieren","updated_at":"2026-06-16T14:13:08.672Z"} {"cache_key":"d477d53f4724d1b9576a3480d44cdb4b080ef56b7a77ab30effd0b7d0f78f35c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"de","translated":"Entscheidung, Blocker oder Nachweisnotiz hinzufügen...","updated_at":"2026-06-16T14:12:59.821Z"} +{"cache_key":"d7b3d2e3ce70b9d05c82cab44b67236a3bfbee3c59922bddd5e4708f4cb7eb7a","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"de","translated":"Sitzung lösen","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"dbd708750243e3f96f6249e02d5d7b3c0535e0f4889d9d2faf50e61fcaeae879","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"de","translated":"Nachweis fehlt","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"dcafcf41becff258d37e8ce9568c9811842e36b940492a605cfe7b4d83dea864","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"de","translated":"{agent} (Standard)","updated_at":"2026-06-17T14:13:07.391Z"} -{"cache_key":"dd2f9a6dc7281087e13a0be6c0ad578dd8513ec64bd24ce7fa7c264897ea8fb2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"de","translated":"QR-Code für die Kopplung mit OpenClaw mobile","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"ddf636d4bb9bca7e507399b7aa21d5562bdda8140504a38a7778251fe001b5dc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"de","translated":"Abgelaufen","updated_at":"2026-07-01T10:30:56.717Z"} {"cache_key":"de35c4bf5e498ca7fce831ae21e4063f7ae69aede234ebde8be4104a791be765","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"de","translated":"Arbeitsbereich","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"de58d7657c6f03a21d8cc5fead1332613bea7eb89446f7959f3d27cc5e1705cb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"de","translated":"veraltet","updated_at":"2026-06-17T14:13:12.251Z"} {"cache_key":"dfab650ecf90a65280cd1f0d9a8c8e2ad55dc1c68a3993daea42b2f0f7568822","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"de","translated":"Karten, die explizit dem konfigurierten Standardagenten zugewiesen sind.","updated_at":"2026-06-17T14:13:07.391Z"} +{"cache_key":"e2249f40d9070318fba10febd046411287068929f7c013ea8dd08b4626b1483b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"de","translated":"Nur archivierte Sitzungen anzeigen.","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"e351ed5b8875597b29735f9e236faf76729b18c44b297816f6548768a37682fe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewStale","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Stale","text_hash":"40c9e59c5e152b0ae9affc84d8461c29b75a7709e4506307eeedf246b526014e","tgt_lang":"de","translated":"Veraltet","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"e50aa42b44fa61de5322d01701b08a1460b315ebf5beb258a7b68d1c40200a02","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"de","translated":"Zielnotiz","updated_at":"2026-05-29T21:00:05.331Z"} {"cache_key":"e84499843ca01f10623698a6f183340bb57f5a8f47f0088a38331c198eb8649c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"de","translated":"Artefakte","updated_at":"2026-06-16T14:13:06.672Z"} {"cache_key":"e8c9e2c6cfa86e6ce24cfaaefe3b5750cbfcf64fcf696acf46fe6f7732bd2b66","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"de","translated":"Blockiert","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"ea59ba4468e1248a7a92997bdef429bbc8dbe1b81e39147b6f94c2f3065ba98b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"de","translated":"Noch keine Betreibernotizen.","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"ebfcfd028832ad46eca8a94be8ffa3bc5d9c149628466ec7b00128b4d2214393","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"de","translated":"{count} Abhängigkeiten sind erledigt.","updated_at":"2026-06-16T14:12:59.821Z"} -{"cache_key":"ec60cbf9a0d4202c452795539fdd0fa8146c376d9d9aabf8310a3d6470636c08","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"de","translated":"Hilfe zur Kopplung","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"ec64311ae5ab0ea704733d9bf84f28f79d8c7fa641823ed9389c6f7d34accabd","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"de","translated":"Alle Sitzungen","updated_at":"2026-07-03T07:35:53.179Z"} -{"cache_key":"ed97733b28d28873ea0fa56448578d3695733c31d3d2ebb6452c322097691e7f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"de","translated":"Einrichtungscode kopieren","updated_at":"2026-07-04T16:44:25.985Z"} {"cache_key":"edab01c21e26c06015644c52cad9963fe6a8230290cf9280533e117ded8e931e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"de","translated":"Aktualisiert: {time}","updated_at":"2026-06-16T14:12:53.111Z"} {"cache_key":"ef4e64cffd801ff70f07e9687f2287929be680971221fe4020ba16f11e12dc2c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"de","translated":"Karten ohne expliziten Agenten.","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"ef68728e2d9dff3db226689c2a2d7f7006ac757eaf4617f7cf927440cd33d9ca","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"de","translated":"Nachweis","updated_at":"2026-06-16T14:12:53.111Z"} +{"cache_key":"f6a4da531e49ab70c30ea50b25437a9bd8f6bbddad9d6b1e55d30f21b489387b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"de","translated":"Sitzung wiederherstellen","updated_at":"2026-07-02T14:30:04.036Z"} {"cache_key":"f715083d23b1ef97ba627a3be48560a105a6f5d69f2d31ae4723665b31a0bb59","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"de","translated":"{agent} (nicht konfiguriert)","updated_at":"2026-06-17T14:13:07.391Z"} {"cache_key":"f8b9c85418169642438a234a9c66a06d4970c323c27ccdfc5a0fe57329b4bf69","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"de","translated":"{count} bereit","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"fa934f472f72f3e8e1547695730b2ba14bf1caed6478a0b84bc4a749c4070199","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"de","translated":"Fehlt","updated_at":"2026-06-16T14:12:59.821Z"} {"cache_key":"fc9073817f53d0a220d6c587eff69e6c2e3cdab8dca474cc5f8a2e5afc25d9b5","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"de","translated":"Tool-Fehler","updated_at":"2026-05-31T06:43:49.527Z"} -{"cache_key":"ff80ddf4931c02c9da87cbd0cc10ecef943a2fd3f0f5e183794d155594ac78ff","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"de","translated":"Geräteanfragen warten auf Überprüfung: {count}","updated_at":"2026-07-04T16:44:25.985Z"} diff --git a/ui/src/i18n/.i18n/es.meta.json b/ui/src/i18n/.i18n/es.meta.json index f5df661b5a13..1811f6036af8 100644 --- a/ui/src/i18n/.i18n/es.meta.json +++ b/ui/src/i18n/.i18n/es.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:21:49.218Z", + "generatedAt": "2026-07-04T18:20:54.659Z", "locale": "es", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/es.tm.jsonl b/ui/src/i18n/.i18n/es.tm.jsonl index bbe06ed2afbf..5563271d257a 100644 --- a/ui/src/i18n/.i18n/es.tm.jsonl +++ b/ui/src/i18n/.i18n/es.tm.jsonl @@ -1,7 +1,7 @@ -{"cache_key":"02c28921d11337b08a298c4adc370d4ad08b3a2cb2c1fce7fd1a8ffb5fb60d8c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"es","translated":"Revisión","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"02c9e8c6218cf44b860cf6a78ebb6b0f30166cbd55160dbe101edf537379005d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"es","translated":"Espacio de trabajo: {workspace}","updated_at":"2026-06-16T14:14:11.215Z"} {"cache_key":"032120514bb78a15fb3669d3f79dee5836bb77fcc765a8c1f5a9b04b2e3250bb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"es","translated":"Sesión","updated_at":"2026-06-16T14:14:23.912Z"} {"cache_key":"04ba0cf9164ead45bca8c7a21f5faa951ab570b095f7ecab344320fe0899c0c3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"es","translated":"Vista previa","updated_at":"2026-06-16T14:14:26.057Z"} +{"cache_key":"070cb7b6cc19bbf9557f8c4d498fa0c8306694e65be6a89484b6faa72288c8e0","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"es","translated":"Restaura esta sesión para enviar mensajes.","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"08d1fe3b65db13db5d96fef00cff4e80da96363a46511cf1f12459a866cf9677","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"es","translated":"Sesión","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"0b4aab4fa5ea6f55e0de55c8233ac024d83ed8130bb4c8154ca8d16a4e6afe83","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"es","translated":"latido {age}","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"0c0d3c51aa05a97375c8e39dc6e033c18094847ef750649baedff678006d1aaf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"es","translated":"bloqueado","updated_at":"2026-06-17T14:14:13.986Z"} @@ -10,7 +10,6 @@ {"cache_key":"0dcf7bbefa6a698ee1fb323ab743fed5ba5106e64c3e71d9160823b94181c769","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"es","translated":"Sin asignar (usa {agent})","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"13a74e79c18ebf8425496f4784d57dc48a34f9c3b092c8a0143a31c7fe512d1f","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"es","translated":"Error de herramienta","updated_at":"2026-05-31T06:43:51.095Z"} {"cache_key":"1607b905341a2f410b56987930eb556121bddd0fe25ebc52fbebaa708fed9fd9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"es","translated":"Comando","updated_at":"2026-06-16T14:14:26.057Z"} -{"cache_key":"17028c1c301baccb50ba8e0514e60f67724f7a7eaa65d573596df4c77cb3b913","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"es","translated":"QR no disponible. Copia el código de configuración en su lugar.","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"195ea02a71cf205ef7b728b82e45cca30efce24d894c153db80d69b34fd5b083","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"es","translated":"Completadas recientemente","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"19db5b13584b1cb9f7836ee8770c94c3bee22775a8885c5171dd2f69232e62c8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"es","translated":"Caducado","updated_at":"2026-07-01T10:31:44.118Z"} {"cache_key":"19f05bbf3278fdc746db244f7d233bc2cd7bd93af28afcf16ad0e33e8a282451","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"es","translated":"Servidores MCP, autenticación, herramientas y diagnósticos.","updated_at":"2026-05-31T05:36:36.332Z"} @@ -18,6 +17,7 @@ {"cache_key":"1bda1fb6186f43d9a1ffdf309c9da7e9d27ae490c1e4e720b0f43c146adfdd65","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"es","translated":"{agent} (predeterminado)","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"1cb2c42ea023c5e5085afc88a8d602988bfb2570f9f05854b11ee2f0d29711b4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"es","translated":"{count} bloqueadas","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"1dddab56566c4b3500eb371deead8093360c4074b4c22c9fa850b1fbd5805baf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"es","translated":"30s","updated_at":"2026-06-17T14:14:13.986Z"} +{"cache_key":"20ca650bcae7ae2159f713730e43ae104969a0c6608b9de32c1ca16a5f9d629d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"es","translated":"Cambiar nombre de la sesión","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"216ce4d3223dec0c5106670964de1b78d32829c2cf0c4624efc737c27e2d0d52","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"es","translated":"Avisar al despachador","updated_at":"2026-05-30T15:38:14.082Z"} {"cache_key":"21e3730083c4c55059de1624288d99482bb69512b28c26b6837edc7c02bfa718","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewStale","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Stale","text_hash":"40c9e59c5e152b0ae9affc84d8461c29b75a7709e4506307eeedf246b526014e","tgt_lang":"es","translated":"Obsoletas","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"24a6de859b0956a90013999bf3dba224d13e795496ea46fae64faed2a199e17a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"es","translated":"Actualizar espacio de trabajo de la sesión","updated_at":"2026-06-16T14:14:17.778Z"} @@ -25,18 +25,17 @@ {"cache_key":"281e153d782e5cddfb08ab96830e0df2c057cac4b6ed26c0240247b35b39e675","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"es","translated":"Ejecución","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"2a12488d59c8ed57ec0d57bacf354486376a131687975f7866c863a04cfcd6b5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"es","translated":"Mantener los comentarios después de la respuesta final","updated_at":"2026-07-01T01:06:39.912Z"} {"cache_key":"2bc6ac4dce823273f77abe66c28e3e51e6d814f2ee44d288d3f55ef35a4e5656","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"es","translated":"Usar el chat actual","updated_at":"2026-06-16T14:14:11.214Z"} +{"cache_key":"2f4f2a0214786fc1896ef02c6581830f32936e2b4077476e945d6cabff7cc47a","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"es","translated":"Mostrar solo las sesiones archivadas.","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"2f707cdc5fe0e4fc554806b712d898b35b2b1325ca6aebddfde0a0aee28905f8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"es","translated":"{count} listas","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"30934b4ae099ddda1d297ca871fb752e63f1606ac20f87b5a0a68d470aa1204b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"es","translated":"Automatización","updated_at":"2026-06-16T14:14:11.215Z"} {"cache_key":"345358e0962f203fa5c2148974e39697ea65be312eb2bff156c93da1298914b7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"es","translated":"Cargando espacio de trabajo de la sesión…","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"35228e3b8dbb2adea8538a82d3ac77df66528dac552b0ad98d1fac14dea3f7ec","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyMissing","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{parent} (missing)","text_hash":"8daa419059727391c01e3b7021e05d8d70b4da67f9c57cd2d80f302af77aac53","tgt_lang":"es","translated":"{parent} (faltante)","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"35e16ca9765010a0c640e81bfa001ed199028283a4470cf2c9225204d969b0e1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"es","translated":"{count}h","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"363ef018abed6976a78d031b2449f29d082aae2cc338a86617e712f6f0fc7c0f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"es","translated":"Actualizado {time}","updated_at":"2026-06-17T14:14:13.986Z"} -{"cache_key":"36b8dfa3bdd100aea990fdcbeb67959d17b6ce72fb98422a52cb5cfea4b2c873","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"es","translated":"Creando un código de configuración seguro…","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"379f129fa0265e870df00181ff9ada6682d2e603f4c21ff31b5fabd984d4c5c4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"es","translated":"15s","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"37cf0df35a3bdb11995ed17569da7e4fbc15caf8cafd126935ffa037b426d25f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"es","translated":"Ejecuta /pair qr de nuevo para generar un código de configuración nuevo.","updated_at":"2026-07-01T10:31:44.118Z"} {"cache_key":"3a1660cddd64db2ad0b071bf8b4ab6eafe91db10fbefb185e7e73377b5057c07","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"es","translated":"Tarea de Gateway","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"3af2820713cf1dbb5574caf424b787f2c6a5aa06960cc8751f8f54fd3b6f5bd4","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"es","translated":"{count} adjuntos","updated_at":"2026-05-30T15:38:14.082Z"} -{"cache_key":"3b6e019b0462be0370f33a14d3714f1afadbf59675146b731b2b103b7638d671","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"es","translated":"Código nuevo","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"3e4d926e9f4b6eb6080f086fb86a66e79a3bb697be3d70a1e311d161969bc745","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"es","translated":"listo sin asignar","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"4034ced2fac74a2f1d94f4221365ccbb957526dac319af0cb197ff91fc942403","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"es","translated":"Densidad de tarjeta compacta","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"41e03e51bb95d1aceaa9cd242e859b21450d99f79f51b4ef9d7b030f27b74f98","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"es","translated":"Faltante","updated_at":"2026-06-16T14:14:23.912Z"} @@ -47,21 +46,21 @@ {"cache_key":"4dd1bfce3ebedb8bc85187ac13fa74ab7bd0b90aec567a2d9f7620cbb176f00c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"es","translated":"{count} dependencias están completadas.","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"4e389db5f9e56a451f9fd7fd0101cf33369669df718a09ee9cb5c208db4599e8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"es","translated":"Diagnósticos","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"4efaface250d3700306cc2271c1f2c50dce9af442eabaeac0bca420261aacf23","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"es","translated":"Tablero: {board}","updated_at":"2026-06-16T14:14:11.215Z"} -{"cache_key":"510e7c4c07464da8dad2437b20a7ec49e79fa928cb0a52055acc54eb1d813683","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"es","translated":"Mostrar código de configuración","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"51393930b033f6a7a118fcc23fca6d247bf5c89c0d71c9366dde265e4b8a6630","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"es","translated":"Usar el chat actual para las solicitudes de revisión","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"51d0fe0c003bc4f1c282463c6e237c6fa411e90d0dc6046e0a57014bce6af45f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"es","translated":"Todas las tarjetas","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"5374b6991e3457597aa34c1ac94740da287f12cb2241f61c42acbfee762a8997","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"es","translated":"Artefactos","updated_at":"2026-06-16T14:14:23.912Z"} {"cache_key":"53a39119b041c8b526ef9cf2fae89094f1299c8f5e3772df4dab61042c3f4518","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"es","translated":"Comentarios","updated_at":"2026-07-01T01:06:39.912Z"} {"cache_key":"58020a74f665c649c802f71d76b737c7d876b96fd95acbe932a5a73db85a1a6d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"es","translated":"Sin comprobante","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"5d3b656957833268e13752ae570040470f719d92fc1d45948503ac0bf86e3eef","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"es","translated":"intentos fallidos","updated_at":"2026-06-17T14:14:13.986Z"} -{"cache_key":"5d718b7e8446c6a7d6fbd0a685487c954a0c35bd1d5793224f32ab3299907ae7","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"es","translated":"Ayuda de vinculación","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"5fa5f353c2dd6b0a8085e1f7e44c161a8ce5765c6cfea51dd771218c6123ce2f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"es","translated":"Tenant: {tenant}","updated_at":"2026-06-16T14:14:11.215Z"} {"cache_key":"60a8bfa89026989e6a4bc79e4b7094af5910881bf1b554adf36142e19535951a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"es","translated":"5s","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"618a589f34990673e9d3eddf322bbd5d18841f3510743957f09b5c059b79f04e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"es","translated":"Actualizado: {time}","updated_at":"2026-06-16T14:14:11.215Z"} {"cache_key":"6507260f0e6299cf81dbdbc2207ca6ac8e8f6f178284325722ab7eb267c75f0b","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"es","translated":"Todas las sesiones","updated_at":"2026-07-03T07:37:29.859Z"} {"cache_key":"6c6c6069b6fb6a9dc628ad4ec9e869934381638273aec42671fd8104e921a2e5","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"es","translated":"हिन्दी (hindi)","updated_at":"2026-06-26T21:43:26.471Z"} +{"cache_key":"6c72a1f5f12168533784ec7ef65e91e4ecda20ac2a59143a89438accefefbb28","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"es","translated":"Restaurar sesión","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"6ea59f0fa991864b06386537c007ce90cccf9705914be3603df500c1ced1ea24","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"es","translated":"Skills: {skills}","updated_at":"2026-06-16T14:14:11.215Z"} {"cache_key":"6f13aa82fec1312b440a33f2fbaaf977fa7c86b6c48a78d77f498f1d6e7226f8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"es","translated":"Buscar archivos","updated_at":"2026-06-16T14:14:23.912Z"} +{"cache_key":"6f62f7636946409dac22df794e16efaf8eb2683fa74603677bc0d28e28968495","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"es","translated":"Fijar sesión","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"713325192a8d93eddc1a0414d99dd61043f4dddf39e0ec5a9f769001b45e1919","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"es","translated":"{count} registros de worker","updated_at":"2026-05-30T15:38:14.082Z"} {"cache_key":"741afaad6e1cf9bde0a230015cae4918b6ca365d8742fc31daef6e6875216bef","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"es","translated":"Estado del tablero","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"77b5824fc802eb6f8bdfe3a985cc69693534dbbad989ca4cba836e36ce593d18","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"es","translated":"Mostrando los primeros archivos coincidentes. Refina la búsqueda para acotar los resultados.","updated_at":"2026-06-16T14:14:23.912Z"} @@ -71,20 +70,17 @@ {"cache_key":"7e18176699eac0ae96d948644c96bd1fec9ac860cdfaba78431d458157ebd9d9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"es","translated":"Descartar error de Talk","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"846b7c2027189bd5b3eb6e3b0ae58566d9e775ad9d5971857812c6f72ebdbcd3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"es","translated":"Notas del operador","updated_at":"2026-06-16T14:14:11.215Z"} {"cache_key":"861e85d3487d73960223977cfa17c5769f32372e1456c976304ca6018be294fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"es","translated":"No hay archivos en esta carpeta.","updated_at":"2026-06-16T14:14:23.912Z"} -{"cache_key":"88e35502ba529e2434ba3c2b858e7419f342a90b0a299b806795e63106c7485f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"es","translated":"Solicitudes de dispositivos pendientes de revisión: {count}","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"89023f4ddc33f10b00893cbcc9e1021d9c03afbcde5bd9e8a55098c554d3e3b7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"es","translated":"{agent} (sin configurar)","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"8acfd051cb9a6d54169c915f8f34d9647845ef1a3318082d3b1a5bf35a52e3f9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"es","translated":"Comprobante","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"8c4d5cbf48088dbe5587577ffe2a5deb0aa82a6e96ae4635245fb51b779993e9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"es","translated":"Protocolo del worker","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"8c5f3778cba55cfa6b32aaacde666fe221d786c1b05576b57b9d930f25ef8323","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"es","translated":"Orquestación","updated_at":"2026-05-30T15:38:14.082Z"} -{"cache_key":"8dd0755f6eb2b08932d44ad5a9c6a37e229918c73a842ab5ff5005ef8bf88cd4","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"es","translated":"OpenClaw móvil","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"93bf60a8d03bc5fc9e7a1450174087e3e7333dd85157ba6adf2566d05cfc7bbc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"es","translated":"Desactivada","updated_at":"2026-06-17T14:14:08.769Z"} -{"cache_key":"93ea6f90971066aa4d4b97e68095adea11e7964619fe0f80eca1f1c7be9f3751","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"es","translated":"Gestionar dispositivos","updated_at":"2026-07-04T16:45:40.446Z"} +{"cache_key":"96ec31b57381dbdedb16999a0c7b964345d7629414fced16af28179a4e6c2659","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"es","translated":"Cambiar nombre de la sesión","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"9a08dc9d0efcdd2c95631076b13296c7682c436e32acc73d68994f8ebf54e330","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"es","translated":"Resultados de búsqueda","updated_at":"2026-06-16T14:14:23.912Z"} {"cache_key":"9a0d2fbab44d89c8f7e0c012cb054fcde0a82b2946ed52d4f98cfdaceb7f1c0a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"es","translated":"Aún no hay notas del operador.","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"9d69982042ae98028cd211bab9a9b5f33b1ddaa2a7ee8231a6540e9e63302e25","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"es","translated":"Nota del objetivo","updated_at":"2026-05-29T21:00:16.236Z"} {"cache_key":"9dba87d5a8cc8d175cae54442c59ea40f4e46e1379e29d7e2d9a8d7cdc48457f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"es","translated":"{count} leídos","updated_at":"2026-06-16T14:14:23.912Z"} {"cache_key":"9f02d79b316e2526d7a2323ffe2400c3208dd80d9f1e5f25600bfff57ecb0305","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"es","translated":"en ejecución","updated_at":"2026-06-17T14:14:13.986Z"} -{"cache_key":"9f4f02f975e05b922fc54342f51d91ba0f15c7b1657752428b25b4b067d4695e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"es","translated":"Las apps móviles oficiales de OpenClaw se conectan automáticamente después de escanear.","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"a0dfb07197dbd2d61298f0f674a3ec1d316c9f1033934b8bfdff4cf102e16709","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"es","translated":"No hay archivos coincidentes.","updated_at":"2026-06-16T14:14:23.912Z"} {"cache_key":"a65d6b79ecb33fd78f2cbf89ab7c2664cae596cdc700d291cd184d2508320b45","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"es","translated":"MCP","updated_at":"2026-05-31T05:36:36.332Z"} {"cache_key":"a8f1d781c9a62ce871e05211a4400318d6c6b22c18d0b41dadd4cb8f480cf158","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"es","translated":"Tarjetas asignadas explícitamente al agente predeterminado configurado.","updated_at":"2026-06-17T14:14:08.769Z"} @@ -94,7 +90,6 @@ {"cache_key":"aef4287654e47e60147275960d915bd5f8d10edd938633336557c861317e8744","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.cwd","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"CWD","text_hash":"0217f1cb7725737f15a6710df3bcfa3bc10a239f0f7801ec3d7168e675f5ebd6","tgt_lang":"es","translated":"CWD","updated_at":"2026-06-16T14:14:26.057Z"} {"cache_key":"afda11bb82ba21ded4d56ce8795b54f277bcd940ec51ed4d827474d82bac0c97","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"es","translated":"Revisión","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"b128e2ee60d8a6fa93c4b8b6295b6dabae4033c0c6b77595b5780e14367cfd10","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"es","translated":"Esperando dependencias: {parents}.","updated_at":"2026-06-16T14:14:17.778Z"} -{"cache_key":"b1dab80e681b82261f3ac616047b9782ba28922e5993384f74ba2fcdd13585af","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"es","translated":"No se pudo crear un código de configuración.","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"b225ea403c576bc055525db691493ce2c303735997647c9c5bdd5b7966cde878","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"es","translated":"obsoleto","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"b2fbbc0f82aef769679235a8bcd3230c2f2aa81fd4918cd9c5fc82988c2a19b9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"es","translated":"Código QR de emparejamiento caducado","updated_at":"2026-07-01T10:31:44.118Z"} {"cache_key":"b3d0fa3e7ab4add7755631a45d1c8257ab2450fead99fff72259dce3d7e26cd6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"es","translated":"Ninguna tarjeta coincide con esta vista","updated_at":"2026-06-17T14:14:13.986Z"} @@ -103,25 +98,26 @@ {"cache_key":"ba36a535fce7b09adb4b08d7370f8055f63e0f2032db38b6db5b3c4a9e949663","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"es","translated":"Ver detalles","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"ba74374eeb633268908727f38cd0977dad6868c9f086cc5158cd76434bf3045a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"es","translated":"Enviar las solicitudes de revisión a la sesión de chat actual en lugar de la sesión de workshop de la propuesta.","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"bc6a68e2669a24e5fc5fbdd47416a06f4408715783d5b643b1cf0466bc69c99f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"es","translated":"Dependencias","updated_at":"2026-06-16T14:14:17.778Z"} +{"cache_key":"bc8397396464f5cd6cc5171e565c64b27d903ee6b6227d01cb24cd965c3a8f56","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"es","translated":"Desfijar sesión","updated_at":"2026-07-02T14:30:06.460Z"} +{"cache_key":"bc8d1346543a2e7019429c732e62422dd3f7f707bdb5c688252dee9e2f445ca9","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"es","translated":"Archivar sesión","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"bd0de00fd47ba196582aa778c5a6bd75c1df461bcd0ea9a841636fd84bdf25ce","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"es","translated":"Detalles de la tarjeta","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"bfe46abe1f3ef098417b8ae15d353966fb363db4f2c298b90e79f1ebbfa27db7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"es","translated":"Actualización automática","updated_at":"2026-06-17T14:14:08.769Z"} -{"cache_key":"c034bcc7915d907d00d7064fa1b30af12c19490d32ec7e2068024a86abbfc0a3","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"es","translated":"Escanea este código QR en la app móvil para conectar un teléfono nuevo.","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"c0db0c7a7347cdc21aee2735925ae2fe3d26be5289cbdc527a40c3e5ef2b4580","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"es","translated":"sin prueba","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"c10ff6efa3649e0a5ff0171fa5faaa5dcf46fbd0430509a73f6bc02440507aac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"es","translated":"Expandir espacio de trabajo de la sesión","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"c46272a015deaa2d1b1e8f26cca6d2b87e949c619f0834ddaec580e6512f3333","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"es","translated":"Registros del worker","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"c549d0a6a90861b4516568a865461326fe11f5a7aaf6d6330e51ee58cb0d2f8b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"es","translated":"Resumen: {summary}","updated_at":"2026-06-16T14:14:11.215Z"} {"cache_key":"c6df83db4e60725c6faf7d97eeb8422f85e23b15ca14aae14299ce59690ec94f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"es","translated":"Desconocido","updated_at":"2026-06-16T14:14:17.778Z"} {"cache_key":"c78a8fa8bc3e9658c5024829b5fca56a9b4a677daa30142e697a29686a5f16ba","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"es","translated":"Cambia la vista, la búsqueda, la prioridad, el agente o el filtro de archivo.","updated_at":"2026-06-17T14:14:13.986Z"} +{"cache_key":"c89f2e305ab2c1cb6f0ccc18de052ed410b24caa4d85f1bed234ecc12bfd605a","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"es","translated":"Solo archivadas","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"ca45270fcaf0260574b3ffa915f76a65ddec157a9b1579127631b11e6800d35c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"es","translated":"Modificados","updated_at":"2026-06-16T14:14:23.912Z"} +{"cache_key":"cbd2014fed793170afd71c1e725cffd177d3ebc993c60cb57f795534e624efa8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"es","translated":"Fijadas","updated_at":"2026-07-02T14:30:06.460Z"} {"cache_key":"cd6bd79f0c3ac7e7fa28c53b4da5cdb4bc58b08cefde568cf5ccb6225365fe63","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"es","translated":"Aún no se ha modificado ningún archivo en esta sesión","updated_at":"2026-06-16T14:14:17.778Z"} -{"cache_key":"cedf82d9a27a7a1c3c0f8d917e2422a78c9ffd4977ad66f0d5bd272208380f61","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"es","translated":"Vincular dispositivo móvil","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"d3c0d31bb509c9c12403e249ab6e0eee8afeb0c343c83f6bed8ce299892c3146","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"es","translated":"{count} mostrados","updated_at":"2026-06-16T14:14:23.912Z"} {"cache_key":"d4184cd5413c53fe2a84242ddc858b31f3289e1757d2e4c1a64d27f75acf9f64","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"es","translated":"Bloqueadas","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"d6941f6ef10efebf36299c6c4411c87d92ccb23b4981c70bafad8806c904bbf4","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"es","translated":"Revisa, perfecciona y aplica propuestas antes de que se conviertan en Skills activas.","updated_at":"2026-05-31T21:48:19.683Z"} {"cache_key":"d8c20e37a13a86954dcd60220a2da3f1db8e5ac108979eb780c3d29531ae940b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"es","translated":"60s","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"ddee0310ae22b896e63790dd27363c2fb84b6dbcd04f33dcdbe7d032b9e170d4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"es","translated":"Error al actualizar","updated_at":"2026-06-17T14:14:13.986Z"} {"cache_key":"e3d78817f4f01e6b7df5e7d4af14355d2043188b77d481a7633b82ba4a056dba","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"es","translated":"Русский (ruso)","updated_at":"2026-06-26T21:43:26.471Z"} -{"cache_key":"e7c904d77739d6567ab1cb843b5556b0c67138cc1cac02472bc91a78c9264535","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"es","translated":"Se requiere acceso de administrador para crear códigos de configuración.","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"e81fe39f884e84b2b3209c6f0eb133499339b1bbf35b54844028d539957743cf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"es","translated":"Agente predeterminado","updated_at":"2026-06-17T14:14:08.769Z"} {"cache_key":"e836e54ddf668f876ef6476d762674d44bd408f2ff48bde6ea5e42bf7a4ccc6c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"es","translated":"Actualizado","updated_at":"2026-06-16T14:14:11.214Z"} {"cache_key":"e881104fcebef0d2ddadfea984fa81170fd0ae63103cd788bc1d180ca5d6f34e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"es","translated":"Acciones de archivos del espacio de trabajo","updated_at":"2026-06-16T14:14:23.912Z"} @@ -129,9 +125,7 @@ {"cache_key":"e9f61efd82b4ecedff824fa08bbf380c765039c2a8d846a331d45d2be56b3e9a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.copyPath","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"es","translated":"Copiar ruta","updated_at":"2026-06-16T14:14:26.057Z"} {"cache_key":"edea18aa163b5af9c1c0c9a251bfb268dd1186f24567bcbe7408bbae9da8f6a2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"es","translated":"{count} artefactos","updated_at":"2026-06-16T14:14:23.912Z"} {"cache_key":"f0c3f9afe83ef51ca9833a8de96256ae03097b3d32b50632e8607a6eec9abd99","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"es","translated":"Raíz","updated_at":"2026-06-16T14:14:23.912Z"} -{"cache_key":"f30b061c1933fb3a09052d2f12686900f2ceb6026dac6dee063cc8a5f0fc6090","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"es","translated":"Código QR de vinculación de OpenClaw móvil","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"f365c0a18a8de732b2b12ae95e4d3e03fc97e3850816c668a49d2c22769f4df8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"es","translated":"Espacio de trabajo","updated_at":"2026-06-16T14:14:17.778Z"} -{"cache_key":"f71f34a9c6df8e9c9a2b5f39a6e1cc9e41b7192a15ccad6dfcaf8d205f9a87d7","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"es","translated":"Copiar código de configuración","updated_at":"2026-07-04T16:45:40.446Z"} {"cache_key":"fae30f5f28c27c8d8b67f7b40a7694c42853e655fdd95e75e4eeab553aa2bdc4","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"es","translated":"Taller de Skills","updated_at":"2026-05-31T21:48:19.683Z"} {"cache_key":"fbddcc4a76247dd3b8aa9c7cb3ea0d4237808120057f5fec5b29b4319ef11d97","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"es","translated":"Ruta del espacio de trabajo","updated_at":"2026-06-16T14:14:23.912Z"} {"cache_key":"fe6be05e50136e5c0a8bb63bf26a81d0e2cc76d0837ed10d761a352e30715550","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"es","translated":"Carpeta superior","updated_at":"2026-06-16T14:14:23.912Z"} diff --git a/ui/src/i18n/.i18n/fa.meta.json b/ui/src/i18n/.i18n/fa.meta.json index 032cb964fc33..d9ffc1dae39f 100644 --- a/ui/src/i18n/.i18n/fa.meta.json +++ b/ui/src/i18n/.i18n/fa.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:24:12.877Z", + "generatedAt": "2026-07-04T18:20:59.054Z", "locale": "fa", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/fa.tm.jsonl b/ui/src/i18n/.i18n/fa.tm.jsonl index 87ed3f31bcb6..772034a12508 100644 --- a/ui/src/i18n/.i18n/fa.tm.jsonl +++ b/ui/src/i18n/.i18n/fa.tm.jsonl @@ -1,16 +1,14 @@ -{"cache_key":"00648298734ff20b268219a466e1175a46c060e13eb02f5d7f497f5336a73f84","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"fa","translated":"برنامه‌های رسمی موبایل OpenClaw پس از اسکن به‌صورت خودکار متصل می‌شوند.","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"02d9671244d20a7a9b1e5654272d64dc82d1f3ad471ebfbc731fdc1256d0272b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"fa","translated":"خلاصه فضای کاری جلسه","updated_at":"2026-06-16T14:18:48.731Z"} {"cache_key":"0b4918568cf4f758cc8d4ed0824859e77ba244eb224d96c032c0fea0e0ff8b6e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"fa","translated":"پروتکل کارگر","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"0b87246355b6c9cc0b035cc0725e59723f3d58fa1ee925ad6e17c76dacff41cb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"fa","translated":"خودکارسازی","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"0dc0d538ee0463d76b626d15a4452efaea8e404b5694a9ed38f941665da45f6a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"fa","translated":"مسدود","updated_at":"2026-06-17T14:17:52.321Z"} {"cache_key":"0e1c51ecce1b7bc3b4b67bff67e33b9e8d000052caa46bc559548b25f3b9322d","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"fa","translated":"خطای ابزار","updated_at":"2026-05-31T06:44:11.985Z"} +{"cache_key":"113b0902e6bbde83fcd6d05027131c47415439d7dc9c52599d688ae23c7c94d3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"fa","translated":"فقط بایگانی‌شده‌ها","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"1167cc7da538df715e50c8e48d81c211e39137f01bb9c2553a3cae14faf4163f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"fa","translated":"جستجوی فایل‌ها","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"14160078de1ee3ae94b667d248d532efd92386a5b90d43494d5848e014c5223b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"fa","translated":"تخصیص‌نیافته (از {agent} استفاده می‌کند)","updated_at":"2026-06-17T14:17:45.318Z"} -{"cache_key":"141a7b5e7d7a54f2da9393d2e71d606f14f181ae9fb8fd08f9cf298b4b1e42d1","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"fa","translated":"کپی کد راه‌اندازی","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"1738c6f38084a9682b2810ea3320e5e0660e194b508a383fac0e4810c30f1c82","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"fa","translated":"{count} کارت","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"17ce746694204cec8dc74f98f01769251f6c37757123313fb90bc06aa09bc8fc","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"fa","translated":"हिन्दी (هندی)","updated_at":"2026-06-26T21:43:47.038Z"} {"cache_key":"196b631e1aa644da1a172721644d1fde9ba7fde80373610df4398549c87a5721","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"fa","translated":"کد QR جفت‌سازی منقضی شد","updated_at":"2026-07-01T10:34:16.869Z"} -{"cache_key":"1c0684604f9c4664838b1b14e8d0800cdd0ce085d87dd1d16fe697a728e1b929","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"fa","translated":"کد QR در دسترس نیست. به‌جای آن کد راه‌اندازی را کپی کنید.","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"1ccc8296bd027e777d834e2740449b34b1e1ab06122e1ad5d41ec51c5f31501b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"fa","translated":"بازبینی","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"1ef38d19380701c599cab94e903fcaa8728b53a44fc444fa663441300acc1b61","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"fa","translated":"اجرا","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"1f6dec88b77390e395c7b0bdcd671756e3aa20c23e6676339a41a692e670112e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"fa","translated":"تغییر‌یافته","updated_at":"2026-06-16T14:18:48.730Z"} @@ -32,14 +30,11 @@ {"cache_key":"37b71c66854af8d1f78936dd93652722cfbd9bf81807bc939b6a5856445c8eeb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"fa","translated":"اقدامات فایل فضای کاری","updated_at":"2026-06-16T14:18:48.731Z"} {"cache_key":"38426fef38dda50c8fca4cdedc6abdd3b1c41e558e76db703e60483b2742ada4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"fa","translated":"در حال بارگذاری فضای کاری نشست…","updated_at":"2026-06-16T14:18:40.803Z"} {"cache_key":"3a1da7b41d8ee65b23949bf1abaf9705e0de41a4c55b01e744ea1b683e55b6d1","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"fa","translated":"کارگر {state}","updated_at":"2026-05-30T15:38:53.293Z"} -{"cache_key":"3b60bddeb496d0ffd507b5e64f063c2a7e4f42df8cf733ae5056f779eb28e7e3","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"fa","translated":"جفت‌سازی دستگاه همراه","updated_at":"2026-07-04T16:50:00.815Z"} -{"cache_key":"3cd9d4c14ec33c654570743261de8f0770d34d08c356617ae83e620ab3fd8442","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"fa","translated":"نمایش کد راه‌اندازی","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"44d901f74425e595db46734b1b94bbddb45762fb5050a4933bca0c26119d36e7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"fa","translated":"فضای کاری","updated_at":"2026-06-16T14:18:40.803Z"} {"cache_key":"4a2cd58d849e04841d7cc035ec2c90d722c4fed30f648da173044275addefb73","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"fa","translated":"یادداشت هدف","updated_at":"2026-05-29T21:02:42.628Z"} {"cache_key":"4b3410a4b6f516ceee53cb8bd3f9908586ff34f7f7d0f5ad67f0a25b872596e5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"fa","translated":"به‌روزرسانی‌شده {time}","updated_at":"2026-06-17T14:17:52.321Z"} {"cache_key":"4d3c86d94fd85e9d9f21b4b00ec5cf11986ea1bfecac0f25d6172bd88637b7ed","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"fa","translated":"بدون اثبات","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"4def9f34a07feae1025733ee9857308490da54ed5b970caf4461a2aac7c59a1f","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"fa","translated":"کارگاه Skill","updated_at":"2026-05-31T21:48:45.878Z"} -{"cache_key":"4e1c6cd96692a27cf479a1e9787aa781dd107b64cb04c168315510efe4893d4f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"fa","translated":"برای اتصال یک تلفن جدید، این کد QR را در برنامه موبایل اسکن کنید.","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"4e41722247f5928aa983600766abc42f0d9b7d06088d20a0de5281dd1f9001cc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"fa","translated":"مسدود","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"4f05e9e5fbf924ac2f46d0911f2a6cb352db867595de5e9f02fa06e83f15e8bf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"fa","translated":"نما، جست‌وجو، اولویت، عامل یا فیلتر بایگانی را تغییر دهید.","updated_at":"2026-06-17T14:17:52.321Z"} {"cache_key":"5041238e315bbd454dc532fba706052bdb36409f20e76ee2f5990aacacb0bb5d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"fa","translated":"فضای کاری: {workspace}","updated_at":"2026-06-16T14:18:33.052Z"} @@ -47,7 +42,6 @@ {"cache_key":"52c71ec0fa1f6d7d7852134c41e334b08dd2785e3400941fb4c6d58347a3b3c6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"fa","translated":"حفظ توضیحات پس از پاسخ نهایی","updated_at":"2026-07-01T01:08:55.386Z"} {"cache_key":"540cc2e8155aae8b1273f29ebbc09c53a7c4789810c7d914400b0dc22757b6cf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"fa","translated":"جلسه","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"54c8ecb7d58cc6d0d7ffac0766b04a2def135e2b9c3e86bf6bb3fcc50df0b1f6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"fa","translated":"کارت‌هایی که صراحتاً به عامل پیش‌فرض پیکربندی‌شده تخصیص یافته‌اند.","updated_at":"2026-06-17T14:17:45.319Z"} -{"cache_key":"553ba285e59cb492d04e26f9b8fb599b1f6cb1852ab1b8d5207f91dff2c33d52","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"fa","translated":"برای ایجاد کدهای راه‌اندازی، دسترسی مدیر لازم است.","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"559e2b5c160f48b34db11fc54d811601888ed2667203f5ee61798fdcb822d07a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"fa","translated":"همه کارت‌ها","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"5710533c7d04197ac77d8547c66752f87b519b8ef33d754d9aea924f4b8a3cb3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"fa","translated":"{count} محصول","updated_at":"2026-06-16T14:18:48.731Z"} {"cache_key":"58d620da3ec92697d4c51048df4e33a7d8b581a2e4d7cfd20f195509f8736f44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"fa","translated":"جمع کردن فضای کاری نشست","updated_at":"2026-06-16T14:18:40.803Z"} @@ -59,20 +53,18 @@ {"cache_key":"5cfc6c543779ba2ab7dd243ef6285305b91e7d3aba3d5ac1ab4d873a3bbfe17b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"fa","translated":"تخته: {board}","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"604759b69ecb40fc0facc317775f0798f0ded8d565e07bbb0422fe9641380aba","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"fa","translated":"موجود نیست","updated_at":"2026-06-16T14:18:40.803Z"} {"cache_key":"6166886b96a12220c55e021fe5dfbcb01cb33f734b66131661b4d60f5f5a9366","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"fa","translated":"بدون اثبات","updated_at":"2026-06-17T14:17:52.321Z"} -{"cache_key":"627b2bcd35b6b1d45cd6e0b3d4e20ef8988cde687e08f68c9f722b1c59028954","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"fa","translated":"کد جدید","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"6333407525350a140f79b877b4d0ad7615f69fa188b8d7e135bd517b71756a4d","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"fa","translated":"{count} گزارش کارگر","updated_at":"2026-05-30T15:38:53.293Z"} -{"cache_key":"665be83563577b0eca373831cd9a593dcc645ba9c9a05df2a6c064bdc7de2359","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"fa","translated":"کد QR جفت‌سازی موبایل OpenClaw","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"666789db33683ac88abb27c19051141a2af2b80fbc5c995660a651f2e416a7b8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"fa","translated":"اثبات","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"6e3f54b8e3bcbde213c3d56867a41e7beb31c8adc28684a83a1f32dc9bfe9de0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"fa","translated":"جزئیات کارت","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"6e999e5d61688afc736924ed06b678c8a17b5e0ddb842cceb65dcd2cb2dc6e49","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"fa","translated":"به‌روزرسانی شده: {time}","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"6eb011c90346fe17d02cd816e174da805f295354d65abe887491744e3ce8dbe2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewStale","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Stale","text_hash":"40c9e59c5e152b0ae9affc84d8461c29b75a7709e4506307eeedf246b526014e","tgt_lang":"fa","translated":"کهنه","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"71d09491009ef87d3af376ce4162e9b18d97162c0d76ba6553a489cc3cd638c6","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"fa","translated":"هدف","updated_at":"2026-05-29T21:02:42.628Z"} {"cache_key":"723b55b0c0233417d363ebafdd34cb163391b2b5c9c3691fad1f606d7b4a759e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"fa","translated":"به‌روزرسانی ناموفق بود","updated_at":"2026-06-17T14:17:52.321Z"} -{"cache_key":"7255ea674b40cf546365b4cf4743eca6fc54cd7a5033c1dab262b16f9fb78ac3","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"fa","translated":"امکان ایجاد کد راه‌اندازی وجود نداشت.","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"76659a8b8a9fa42252f2480601c776c04176bbac7145ed3541cc2213737be828","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"fa","translated":"مستأجر: {tenant}","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"78292b5d7cc8b83041ae63c22675f0139b4ee5ccfe7552a26055e55fa2578c04","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"fa","translated":"پیشنهادها را پیش از تبدیل‌شدن به مهارت‌های فعال، بررسی، اصلاح و اعمال کنید.","updated_at":"2026-05-31T21:48:45.878Z"} {"cache_key":"7869df26537d18f729ff3ba597b8d1be67944a88dc72000f803614a035531c7d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"fa","translated":"موجود نیست","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"7ac26b727fbfea39dc4a648c0a8fd18d122872c7c45cc19188b22dd0bc234345","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"fa","translated":"فضای کاری نشست","updated_at":"2026-06-16T14:18:40.803Z"} +{"cache_key":"7bf0af6405064656103a20a37ff1bbf064c3a3a85ef22e84ab4939608bdaf671","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"fa","translated":"برای ارسال پیام، این نشست را بازیابی کنید.","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"7c5fbdbae20ca45446efbb97c35f3829358add3d933da949d623057ca02a3aa7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"fa","translated":"{count} آماده","updated_at":"2026-06-16T14:18:40.803Z"} {"cache_key":"7caf9f422005ab160c8fb0c2428e7e1108400fa60d74c2bea6db3cd752666556","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"fa","translated":"آماده و واگذارنشده","updated_at":"2026-06-17T14:17:52.321Z"} {"cache_key":"7f3309f62d0595fb6e9ff8ca60781de37a31f103af496cbabc203b93c5773da6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"fa","translated":"نمای Workboard","updated_at":"2026-06-17T14:17:45.319Z"} @@ -81,7 +73,6 @@ {"cache_key":"86f85ee22a9b66b7e19a07b1c41c0253e6291a94753b7d8dbbb081dc119b9a44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"fa","translated":"نادیده گرفتن خطای Talk","updated_at":"2026-06-16T14:18:40.803Z"} {"cache_key":"897425d6e8301dca477baf0fde9ff0b3ac0345eb1a8dff6cbe45eaee82cbc89c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"fa","translated":"برای ساخت کد راه‌اندازی جدید، دوباره /pair qr را اجرا کنید.","updated_at":"2026-07-01T10:34:16.869Z"} {"cache_key":"8c423cdb24e6534aace221dd11a3f8a76a501e1ff7bb05955939701c9dfc9520","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"fa","translated":"۱۵ث","updated_at":"2026-06-17T14:17:52.321Z"} -{"cache_key":"8d3bb0da2ebe7a483059aca5a3b9ab88312b59bb2fac73c9e02557e8ea27829c","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"fa","translated":"راهنمای جفت‌سازی","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"8e259c6fcb011a9ad39a534c55c1bc8d2ca6d1e013c756df185503a4247cb867","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"fa","translated":"کارت‌های بدون عامل مشخص.","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"8e794913f198dbaf7a24541bca9f7b00e2cb847e49afd87dcaf4782ce5a6c8e4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"fa","translated":"نتایج جستجو","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"8eae1c77efe81ee013bd1c9bae4ce6ee5dba30c7a72b10eb9acfdb41e0934171","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"fa","translated":"سرورهای MCP، احراز هویت، ابزارها و عیب‌یابی.","updated_at":"2026-05-31T05:36:59.400Z"} @@ -92,23 +83,26 @@ {"cache_key":"9f251ed08e2f4250b28675e035a89babd8e0bdc44c1fbcf6c942aa6b7811818e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"fa","translated":"Skills: {skills}","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"a63186f92f68ab0571da7caa5a0159ccbeaf3794f99747ffd98edb15c6e03995","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"fa","translated":"سلامت Workboard","updated_at":"2026-06-17T14:17:52.321Z"} {"cache_key":"a715e45b3d4c07d3a7f83ab54dbba87397ef021877c3856534acb1266f6fb365","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"fa","translated":"تازه‌سازی فضای کاری نشست","updated_at":"2026-06-16T14:18:40.803Z"} +{"cache_key":"a7f004e699cd5b5a439714aa9f39c8105a645d799881c109bba1c97e55b48075","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"fa","translated":"سنجاق کردن نشست","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"b0df530f6de3c9280322fea2e3b399c5bc569c8feea54a6e013a502b6d8c33a6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"fa","translated":"هیچ فایلی در این پوشه نیست.","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"b390c352546af2d56f239187ddc98ebd47df9a27c51fc0b0ac63c160452e5c63","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"fa","translated":"۵ث","updated_at":"2026-06-17T14:17:52.321Z"} {"cache_key":"b434c46d9c20feb0cbf2ad53b3f3f4570cb58a88a0f7e61a529a53c4b443ae97","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"fa","translated":"محصولات","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"b47a9802787901aee75193ae58997c34cea4acc2a980c5baee4b8745cfaa08ac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"fa","translated":"هیچ فایل منطبقی یافت نشد.","updated_at":"2026-06-16T14:18:48.730Z"} -{"cache_key":"b8ff49f63b6e486f1f60a96bcc59f1d81e3791485085caa18c8a006d67442afe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"fa","translated":"بازبینی","updated_at":"2026-06-17T14:17:45.319Z"} +{"cache_key":"b6e423ab2fd0a32671c1b6512e8c8aeee4dbb9020d067995344c6caec8e45757","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"fa","translated":"فقط نشست‌های بایگانی‌شده را نشان بده.","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"bd005f39f912466a9df4ea368306d5682396883895bb4b620e346c8f53eedea4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"fa","translated":"ریشه","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"c1b99226bb553fe0cc26f1f982f2153d46daadd6347b81c4b1ed65b8d3feb25d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"fa","translated":"خاموش","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"c21bb780e32a380c855b2d8bd2432a100a98f820dcbf8bb61777b0e258d4bf76","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"fa","translated":"اخیراً انجام‌شده","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"c224fbbe8f914f07cfada5282998ef85925f265f012f390875d3b97b37e2238d","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"fa","translated":"تلنگر به توزیع‌کننده","updated_at":"2026-05-30T15:38:53.293Z"} -{"cache_key":"c3944e9f5282cb9563fce3279ad25051dd8c4304c0d42a6df3211ff4ec2aad24","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"fa","translated":"درخواست‌های دستگاه در انتظار بررسی: {count}","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"c3f50e12f7e56217124e689758cab88086b513ce681d7a16a3eec9537b83cbf8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"fa","translated":"افزودن یادداشت","updated_at":"2026-06-16T14:18:40.803Z"} +{"cache_key":"c4ded1c99bbeb9f5908493474494aec0fc0a7189c0dd7ba7da16c532e7955a60","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"fa","translated":"تغییر نام نشست","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"c511ba24de88e008507ab6941d1d56b37613badd186ab112e5c83b670c1de315","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"fa","translated":"در انتظار وابستگی‌ها: {parents}.","updated_at":"2026-06-16T14:18:40.803Z"} {"cache_key":"c774cfac170ec300f78f3a03ea85489925c7b34c0fd34d6b06ba2d468527c667","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"fa","translated":"یادداشت‌های اپراتور","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"c8681bbf55dea4b34f0459d152ad35ca8226a63c65099c2dbdca1e3dc385c529","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"fa","translated":"استفاده از گفتگوی فعلی","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"c959866805a063b9f2043fddd668f6c969d1695b7e464399870baaa049c00a6c","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"fa","translated":"ارکستراسیون","updated_at":"2026-05-30T15:38:53.293Z"} {"cache_key":"cc7b868c1a4e91b2d44c82ef24e3269e70a73bb0c796c69c9082ab28b1283d0b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.copyPath","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"fa","translated":"کپی مسیر","updated_at":"2026-06-16T14:18:51.277Z"} {"cache_key":"cd88f356c91cb8d551562b5bef380cdb34cc21f5bc0b419f9be21ee825942c66","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"fa","translated":"پیش‌نمایش","updated_at":"2026-06-16T14:18:51.277Z"} +{"cache_key":"cdb7f56b2a86203baa799f23fda25dce5a1c6ef8443b273eb648ec7b043a39a1","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"fa","translated":"سنجاق‌شده","updated_at":"2026-07-02T14:31:11.668Z"} +{"cache_key":"cdc0461aed5f50182aabdfcbecc28fe4cc6275f29b0f424a23d7e130610221ac","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"fa","translated":"بایگانی کردن نشست","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"ce091beb24b5445b84eafdc88461606c69e3e996e61bb7c59f27a61a434abd99","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"fa","translated":"افزودن تصمیم، مانع یا یادداشت اثبات...","updated_at":"2026-06-16T14:18:40.803Z"} {"cache_key":"ceaed13452c6b0d8fe998414b0158704dfb224a4ada336ba3ec4750e44166fd4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"fa","translated":"فایل‌های پروژه","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"d076b185dde51a685ed5cbce6a07a848c7ab0429c8383c0287f99dd692f454fe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"fa","translated":"هیچ کارتی با این نما مطابقت ندارد","updated_at":"2026-06-17T14:17:52.321Z"} @@ -116,17 +110,17 @@ {"cache_key":"d23743acc47c4c60c5610d150887c98c519d4a2272be4bb422478a9eb71f4bd9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"fa","translated":"تازه‌سازی خودکار","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"d26497e841d86df0c24bb71aa610a5ca3ed3a03f7ad7158ffd48091e5f15aec5","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"fa","translated":"{count} پیوست","updated_at":"2026-05-30T15:38:53.293Z"} {"cache_key":"d7b71240a68af96f61f42d15ffd83e0c8b89797cc224f8d861a76507bb640320","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"fa","translated":"تراکم راحت کارت","updated_at":"2026-06-17T14:17:45.319Z"} -{"cache_key":"dfe75c5001bab2c7d15a24ff8a38631ec4f1c31fb20a9d798d7dd40aa8cc70a2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"fa","translated":"مدیریت دستگاه‌ها","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"e24b600fc00614c5f07bd2eb03c4b7adaffdaba1ad6566074b502869864be0b2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"fa","translated":"مشاهده جزئیات","updated_at":"2026-06-16T14:18:33.052Z"} -{"cache_key":"e5313121e703b804139fc984d90f652e724b8ade49f2d985e9b23189ec14323c","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"fa","translated":"در حال ایجاد کد راه‌اندازی امن…","updated_at":"2026-07-04T16:50:00.815Z"} {"cache_key":"e5671dde47a046d4ae207896246463bf1dd9a5649a40fd9f0964e7b49ac64cc7","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"fa","translated":"نقض پروتکل","updated_at":"2026-05-30T15:38:53.293Z"} {"cache_key":"e85e8416304e0238c803711e50bd564f0c670b93698cff6a7c236c7d4d65081f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"fa","translated":"پوشه والد","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"e964b7679d0c8add45aad251607f4a5d8453641af4eec558bf79995993bcf2d4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"fa","translated":"در حال اجرا","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"eab1d4d167b17cbdf61f6763fa850e375a6f762623a929bc24e750048fcd753c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"fa","translated":"خلاصه: {summary}","updated_at":"2026-06-16T14:18:33.052Z"} +{"cache_key":"eb0a62ceb2d3d0371bfe8359c7e0b649063855519e431f6832efbe55d7d84403","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"fa","translated":"تغییر نام نشست","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"ede1541c9883467e30a69e28e42f9a7645dbf141314de4413d907bb822661669","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"fa","translated":"تشخیص‌ها","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"ee07d9c38ec34d86d93ae159dee46e342e635bd09a2593177ab6fc4ba2197a15","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"fa","translated":"اولین فایل‌های منطبق نمایش داده می‌شود. برای محدود کردن نتایج جستجو را دقیق‌تر کنید.","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"eed0b4edffee3f8177346e12d6ee2a3d4a735c235a9d8ab94e653ac33df6fe6d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"fa","translated":"استفاده از گفتگوی فعلی برای درخواست‌های بازبینی","updated_at":"2026-06-16T14:18:33.052Z"} {"cache_key":"f0562c95b72b6f07b09521db6761a791f501a32ea913d2de41023d10ffc073cd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"fa","translated":"هنوز یادداشتی از اپراتور وجود ندارد.","updated_at":"2026-06-16T14:18:40.803Z"} +{"cache_key":"f078dfc0bbe6fafa59cde1bcb8253f0e45a159384081258d9482c4ae8a7cab82","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"fa","translated":"بازیابی نشست","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"f10d218fb1295745ed689095752712ff7dccad268aa0740bc645e99af166f565","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"fa","translated":"{count} خوانده‌شده","updated_at":"2026-06-16T14:18:48.731Z"} {"cache_key":"f172dd5638b7ddfd049be254a95d47ee9f060d7a2dbc491bde0b5ae4cf3c6d12","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"fa","translated":"{count} وابستگی انجام شده است.","updated_at":"2026-06-16T14:18:40.803Z"} {"cache_key":"f2208997e475eac37b5ec3d9aadd4b4408976d0c631a6f69579254fac75456b2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"fa","translated":"هنوز فایلی در این نشست تغییر نکرده است","updated_at":"2026-06-16T14:18:40.803Z"} @@ -134,6 +128,6 @@ {"cache_key":"f4bd1e57495fd4de54425c3213c29356dd7c3e9cb4f92a10d5c80a809fa0193b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"fa","translated":"آماده","updated_at":"2026-06-17T14:17:45.319Z"} {"cache_key":"f692ec8dd98ae7135a148e44bd7812bf062aa08eed067e968ee60d5c31dfac48","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"fa","translated":"خوانده‌شده","updated_at":"2026-06-16T14:18:48.730Z"} {"cache_key":"f6f8d10d5c6161177fc19e288e8c787280eaa9abb297251338a1efd240d6651a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"fa","translated":"مسیر فضای کاری","updated_at":"2026-06-16T14:18:48.730Z"} -{"cache_key":"f77640ac7e3ef1537406e3001c01ed64faf7c0f342442dd860f10e28f2e5a5cc","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"fa","translated":"موبایل OpenClaw","updated_at":"2026-07-04T16:50:00.815Z"} +{"cache_key":"fb7d1fa78a10d7f85914aee2870bb81ca931e2a87ca266184c8b380391dadfc2","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"fa","translated":"برداشتن سنجاق نشست","updated_at":"2026-07-02T14:31:11.668Z"} {"cache_key":"fc281b186db31721cae978c1279f1178c2348b454106107070f41c723c099cfb","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"fa","translated":"Русский (روسی)","updated_at":"2026-06-26T21:43:47.038Z"} {"cache_key":"fee1ecf6de721566cb0ee367343fc7f17d6a3d7d1f72d7be9e76c9f398d08702","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/fa.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"fa","translated":"{count} تغییر‌یافته","updated_at":"2026-06-16T14:18:48.731Z"} diff --git a/ui/src/i18n/.i18n/fr.meta.json b/ui/src/i18n/.i18n/fr.meta.json index bce722e7cc74..2b463fda878b 100644 --- a/ui/src/i18n/.i18n/fr.meta.json +++ b/ui/src/i18n/.i18n/fr.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:21:58.976Z", + "generatedAt": "2026-07-04T18:20:56.772Z", "locale": "fr", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/fr.tm.jsonl b/ui/src/i18n/.i18n/fr.tm.jsonl index d104fed711bd..37d8a8c6780a 100644 --- a/ui/src/i18n/.i18n/fr.tm.jsonl +++ b/ui/src/i18n/.i18n/fr.tm.jsonl @@ -6,11 +6,8 @@ {"cache_key":"10c47c1f85d37e29ce8e5d39c4fb55a31878f742cd3731dce30ef89ffd6266a7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"fr","translated":"Densité de carte confortable","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"11b685e8bc937c3f3a8cccf7984408a1803d5bca4bddfce6bf1f74d1bb994cea","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"fr","translated":"MCP","updated_at":"2026-05-31T05:36:40.482Z"} {"cache_key":"127526bce2459285625f3d800371e550e4d1d76f82d98c4b50a42eb9584a4d0c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"fr","translated":"Actualiser l'espace de travail de session","updated_at":"2026-06-16T14:14:33.243Z"} -{"cache_key":"14e8124cd7bffa4ba81659e216ae788f1cd8d42010077c132ecd068afcdd392d","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"fr","translated":"Code QR d’association OpenClaw mobile","updated_at":"2026-07-04T16:46:02.634Z"} -{"cache_key":"17cbf98f693ac2755c4894a7a88d43a9cc9c8597227b00d3e2533bea136aeb38","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"fr","translated":"Création d’un code de configuration sécurisé…","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"193a027d27061ee7de015b356ae38b8e2b41f3d1b6c6c25b62bcf92b835cca1e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"fr","translated":"Locataire : {tenant}","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"19a2dfca17792e57db3d8190d8446902ff7f995966691a370b575e79d84ac0f6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"fr","translated":"Ajouter une décision, un blocage ou une note de preuve...","updated_at":"2026-06-16T14:14:33.243Z"} -{"cache_key":"1b32e155bee408b358e4cd90b2fb458d6cdc43665b5f9ad796623fbda6e0da01","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"fr","translated":"Révision","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"1badc8ec37f3de047ab11f96a0029ddc3036aaa079b38099c789876304b8f1b0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"fr","translated":"Développer l'espace de travail de session","updated_at":"2026-06-16T14:14:33.243Z"} {"cache_key":"1cc235967bf9e738046472d651b35350a9cc43cfa4be1c9021cebab91911863c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"fr","translated":"Artefacts","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"1dfd8f335c574b5b17d3bc5d093241f9a55f81b3e843b363f56688b9eb6939c5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"fr","translated":"{count} bloquées","updated_at":"2026-06-16T14:14:33.243Z"} @@ -21,32 +18,29 @@ {"cache_key":"32508a245b7f297458454d0d6b421f18bfb8e5b30cc485b6c8efb83efe906823","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"fr","translated":"60s","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"33f08936ac956a25ff1afbce1f6967a17269c18d5c829f5fc007dde85c338703","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"fr","translated":"Toutes les cartes","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"35481b71eba2317119d4dbf7dbed0f878b2be2d53e7b666729eeed5f8c10d827","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"fr","translated":"Commentaire","updated_at":"2026-07-01T01:07:07.533Z"} -{"cache_key":"39582587dc74a16de86cdc7b2ef9f5be9b600cec3773f0a026bf690813b85990","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"fr","translated":"Gérer les appareils","updated_at":"2026-07-04T16:46:02.634Z"} +{"cache_key":"3a1b658e2766ee33c4a8154d7d81cc7fcaefff6a4118e2e08ce5d29f41434b66","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"fr","translated":"Restaurer la session","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"3bc46432de64f9d82a3e78b6cbc8170448add64ef00783823c1f0266d24ad3b8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"fr","translated":"Cartes explicitement assignées à l'agent par défaut configuré.","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"3c0184dd360912c8947b9a78652bf0b39064c358953f3403ff700329ef79a36f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"fr","translated":"État du tableau de travail","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"3ced3592ea6acebeac1c5b2e06684c1aabd1fdc1deed38906e62320a9fe51bd4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"fr","translated":"{agent} (par défaut)","updated_at":"2026-06-17T14:14:32.183Z"} -{"cache_key":"3d2f3abe6fdec8192ae0e74eb50272c9edbbc62a19ec21caef9b9fa885ea8a0d","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"fr","translated":"Un accès administrateur est requis pour créer des codes de configuration.","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"3edc00a38a3d13ced2578a684c2ae62a7bcd6cc73e235c78fb64d7a572b8b8fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"fr","translated":"Skills : {skills}","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"3f2a1680c4c8f175feb2765b734b9dfbeb633b1c333c3ed7d1d00945c8a7dae1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"fr","translated":"Aucune carte ne correspond à cette vue","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"41417f07a0c6ed543378910a910566667149f9b110fab6e5ffb36d64400e9f12","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"fr","translated":"Expiré","updated_at":"2026-07-01T10:32:03.163Z"} {"cache_key":"424f9b28aaa8357013433e8caad46c51ad5141083f8de91f9215824cad9d34e3","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"fr","translated":"Orchestration","updated_at":"2026-05-30T15:38:23.998Z"} {"cache_key":"4255a719f4f69f4be7edecc3dbdc5279ceb09478ea0e2d4f428d4b10b5dd6740","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"fr","translated":"Dossier parent","updated_at":"2026-06-16T14:14:38.496Z"} -{"cache_key":"440b1a3f8247eceb003fcbd0721a039281dee07c56586f7f751e00269647e0a0","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"fr","translated":"Nouveau code","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"45f77c4535f74f1e1b676ac2ecc391404f91a9c8b4c84ffc37d34273717001c8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"fr","translated":"Modifiez la vue, la recherche, la priorité, l'agent ou le filtre d'archive.","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"4667b37b6fd6426af22fc905b46caad8f5b3f36ea33cf521c3e3ea151c9e0aeb","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"fr","translated":"Violation du protocole","updated_at":"2026-05-30T15:38:23.998Z"} +{"cache_key":"47100c749023e0e173a6e87f2187d80d2925ee743b18a6e6a32f7da166feea49","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"fr","translated":"Restaurez cette session pour envoyer des messages.","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"4824a546c58c73b812aefc3b0586dbbb84dcf0416d42300c89fb365b4cebc002","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"fr","translated":"Aperçu","updated_at":"2026-06-16T14:14:40.922Z"} {"cache_key":"4a89f35579bd9d4cb52527a62258f4a49ac70aee4e74574fd8c7cd80ea504fe2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"fr","translated":"battement {age}","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"4ac700808f572afd6546dc0ae78fbadb8a02ef8aea0c6db3a460894afd56338c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"fr","translated":"Mis à jour","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"4b5db0ad286694866b63bc0cc6e371abefbff497099982cfc517f7681ede0e9c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"fr","translated":"Chemin de l'espace de travail","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"4d1c670522c8374ec8bf4004ecdf2df6153c2553637f3f5e0fe4e242c5928673","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"fr","translated":"obsolète","updated_at":"2026-06-17T14:14:37.466Z"} -{"cache_key":"4dd917c6ebaf0e47a289f4923f06d9208b284fe558f19787222c47a206e2f91a","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"fr","translated":"Les applications mobiles officielles OpenClaw se connectent automatiquement après la numérisation.","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"5141910c57fb2558f451f18f8b90bb0f60eb86a6b8e0dc6de89ad93b5be8cf44","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"fr","translated":"worker {state}","updated_at":"2026-05-30T15:38:23.998Z"} -{"cache_key":"527321e6a42d4780b0796b369d05804fcd9cf3021146dcbbdc0c51f4941261ec","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"fr","translated":"Demandes d’appareils en attente d’examen : {count}","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"52b4758a7dd38e57f35fc7bb361e6cfb4c65bd72299fd083ddc150236aa31430","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"fr","translated":"Racine","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"578bb582711633223c7729d971ee2bdb4eed82aea839375438c2d01a9ffe0053","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"fr","translated":"Erreur de l’outil","updated_at":"2026-05-31T06:43:56.223Z"} {"cache_key":"58301673e3a99cd4120a852d09e7b02bd6910264d0a03b50d96375b29e95bcde","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"fr","translated":"En attente de dépendances : {parents}.","updated_at":"2026-06-16T14:14:33.243Z"} +{"cache_key":"5a61afd8e13a832b6e8506b0b4bb16b3289e5eb3cdea7ab8ab1644508cecb194","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"fr","translated":"Afficher uniquement les sessions archivées.","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"5b2e0dd2b5bb202e6ecef6fa600de352d52bd94c344b9d543deffdf7563d2456","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"fr","translated":"{count} artefacts","updated_at":"2026-06-16T14:14:38.496Z"} -{"cache_key":"5b88b6a8b56ecbb799ef6ff1c0588c8c62954f20ece7f6a9e78111795c97776f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"fr","translated":"Aide à l’association","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"5bc8019001e7a0367c3f99398dc9c2ce155837a5b2c710ba829542c3c185d2cb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"fr","translated":"Notes de l'opérateur","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"5d00df2458f95482fc2608b8b57fa266f2e259785acb0d5c8e1eb5221c498b06","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"fr","translated":"Détails de la carte","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"5e558b657ccaf02af17707249b719ad1bd7debb134f069adffbf755d8c9aff15","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"fr","translated":"Diagnostics","updated_at":"2026-06-16T14:14:26.985Z"} @@ -54,19 +48,17 @@ {"cache_key":"5f16d115ffe9e89761fff4bcaf5ed813a8f4de140bd458d6ad0c8cbbc087d9af","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"fr","translated":"Serveurs MCP, authentification, outils et diagnostics.","updated_at":"2026-05-31T05:36:40.482Z"} {"cache_key":"5f210b70ec285a1fdb5309333b64ed2f2efbf2a51def2ca7e0e079bf9f612706","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"fr","translated":"Aucune note d'opérateur pour le moment.","updated_at":"2026-06-16T14:14:33.243Z"} {"cache_key":"5f7c421af66cbd61cf05969b1caae9b2be804ca09542d1022ee6d399a874e03d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"fr","translated":"{count} dépendances sont terminées.","updated_at":"2026-06-16T14:14:33.243Z"} -{"cache_key":"6092472c9e05127103c5f3fd09131ffbb9264ceba49e54c35e2941b136aa09f2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"fr","translated":"Scannez ce code QR dans l’application mobile pour connecter un nouveau téléphone.","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"67ca21d363a785e3b9a2e41848560836983a23cd988eb8add97bcebe06a84b46","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"fr","translated":"Vue du tableau de travail","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"68f6b2e5cc499a1d635c7fd9b2363b1b9d0047d4d7007488dced4c0c6d795330","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"fr","translated":"Session","updated_at":"2026-06-16T14:14:33.243Z"} {"cache_key":"6a7849ff15ea111f06a321fd96464667ee69938ab69a94ba33a5b6b02ad7f386","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"fr","translated":"Ajouter une note","updated_at":"2026-06-16T14:14:33.243Z"} -{"cache_key":"6a8d7a5c08c0efa8ad6b27e1994a1be2e430177ddc4e6ea154330da256108d2d","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"fr","translated":"OpenClaw mobile","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"6abe60843e794988fefd7905ac60565e6a5f0e835efd5548fdc85bae0396dab2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"fr","translated":"Densité de carte compacte","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"6b3b656273d0f13e29ae1e0286cf8c5cba6662e46f6657d1108bb9230565cc7c","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"fr","translated":"Relancer le répartiteur","updated_at":"2026-05-30T15:38:23.998Z"} {"cache_key":"6ee4700695cb437e9ca635a8d61793548a701d4755daf6a71484698aa6533063","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.copyPath","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"fr","translated":"Copier le chemin","updated_at":"2026-06-16T14:14:40.922Z"} -{"cache_key":"6fcc559f2479ff452f331ebdcc3c6df07c039093a08a17076297429723d6fb27","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"fr","translated":"Afficher le code de configuration","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"70ffd7a78b610cc2643444d10e4aa03c2e553a1166ecc02d6b54c9a309df2ab3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"fr","translated":"Cartes sans agent explicite.","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"75eeffde33d9a4f451b77a2e7fffeb88539d8d8d32811da06ce468da6916db3d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"fr","translated":"Preuve","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"7645e0eedba437e1cac2433e0bf38b1ee2255ea40c88f6b5450bab93f24bb1ff","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"fr","translated":"Affichage des premiers fichiers correspondants. Affinez la recherche pour réduire les résultats.","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"78cc82a6282b5f7b980cf96128494d5dbcf42077806877b4592b2402e06b3cd8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"fr","translated":"Dépendances","updated_at":"2026-06-16T14:14:33.243Z"} +{"cache_key":"7b89af9b738646ccb50364ac9873eeadadbbe27bc01d9cfbbfd36e8b55ac45a7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"fr","translated":"Épingler la session","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"7c080dbae61c58ee3f14258bdbfe35f939b47f96b2d2e89dca5d18f2bd6343e6","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"fr","translated":"Toutes les sessions","updated_at":"2026-07-03T07:37:43.296Z"} {"cache_key":"7db98ed1985cdddbda35001dc08b5334f248249d3dbd05104971d0139b53e53c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"fr","translated":"{count} modifiés","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"810b51afce34bdcaf6f590104802bc3f9aaa917675f8457b57fddff3278c64c0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"fr","translated":"en cours","updated_at":"2026-06-17T14:14:37.466Z"} @@ -75,17 +67,19 @@ {"cache_key":"899c54e4537943aa09fb972d4ed1160342eaa6d28da0c2d27d9c992ab5e02edd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"fr","translated":"Tâche Gateway","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"8d53af57f69db68d6c9c73501a4cccdb878660a2f410f8b62bd255391be33753","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"fr","translated":"{agent} (non configuré)","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"8f06525900f13a5e36695bc02d3ac91aae42b666b37b9a23caa9096441ffd1b3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewStale","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Stale","text_hash":"40c9e59c5e152b0ae9affc84d8461c29b75a7709e4506307eeedf246b526014e","tgt_lang":"fr","translated":"Obsolète","updated_at":"2026-06-17T14:14:32.183Z"} +{"cache_key":"907b09795789cf241ab4c1f560071c47dca23b82e4753a7a236f92801226603e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"fr","translated":"Épinglée","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"967f8313ca28e793e44686d5cc03aa93f3e0ee149b45cde549a8292ef0244f3b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"fr","translated":"{count} h","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"986854b4ba372b725ef75a0356de1baad04b9f7cd7e60eaaa1ef6bb9cc0bfa9f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"fr","translated":"Révision","updated_at":"2026-06-17T14:14:32.183Z"} -{"cache_key":"99488034b6600d9412ae9b35278f331d56b40ad749ae9a198d3bc2ca16d1df28","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"fr","translated":"Code QR indisponible. Copiez plutôt le code de configuration.","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"9c50d9db0cd45b064b9244da54e52274506ce429291257708505edec0ae03110","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"fr","translated":"Résumé de l'espace de travail de la session","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"a20d3c7263b6279d4fa2aa712d1b8e3da90e8f4f03a2b27439bcf94bdea3b139","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"fr","translated":"Résumé : {summary}","updated_at":"2026-06-16T14:14:26.985Z"} +{"cache_key":"a4772bbc71a0c72df5ed517a5f797bb4b225f1c5a1c5e0eaeab8070da311a13e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"fr","translated":"Archiver la session","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"a497402fa9c26ae89b6ace98e172ad09963fa3b6c529dd08fad9b3b5ff5ae430","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"fr","translated":"Envoyer les demandes de révision à la session de discussion actuelle plutôt qu'à la session d'atelier de la proposition.","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"a66fd73a5cb8320cb17077351f3778ab13271c3c2ff5af9a652bbd7cac9ad6b5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"fr","translated":"Fichiers du projet","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"a71bfd639c8a0eddb11d0e6a929e63eb918c442210e2d80b40f0076e7082de34","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"fr","translated":"Rechercher des fichiers","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"a86338a58426dafac0130575a4db7bed1ee9520804005543ee75e99e9bea97dc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"fr","translated":"{count} affichés","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"a87418ac5fda24f3add83f591aa5c4b8ebd1d3914f11b428e8eac9cc3b959fc5","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"fr","translated":"Note d’objectif","updated_at":"2026-05-29T21:00:46.685Z"} {"cache_key":"a8ee16008a634cb944cf433fedacb19d2ffb603742dc3abdf03ed0716b50a57c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"fr","translated":"Agent par défaut","updated_at":"2026-06-17T14:14:32.183Z"} +{"cache_key":"aa05cbcdd6afc1a01029a74336f78104d6dbc72b8f6783dfa4d403395804ac93","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"fr","translated":"Archivées uniquement","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"ab429001453beacc7a0148b2079394fce8c3123ace3dee241cfeb049846fa62c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"fr","translated":"bloqué","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"ae1c4f710fd0b813ec9fc5f7de8294f953bad91984d826da10faaabd81ba9ae3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"fr","translated":"preuve manquante","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"aed90dc6280045598d163409ef823bbd1c0ae4cae6fc9755d7ff296bea88dbb8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"fr","translated":"Exécution","updated_at":"2026-06-16T14:14:26.985Z"} @@ -97,7 +91,6 @@ {"cache_key":"badfd3d137e60b3582157fb6a4afd4788b3d11939008f2b84da44b8b34136da9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"fr","translated":"Aucun fichier modifié dans cette session pour le moment","updated_at":"2026-06-16T14:14:33.243Z"} {"cache_key":"bc9904c49788d97a2f27902a8f087c23d266ef3ca0e6d6c96df9547dbe90f54e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"fr","translated":"prêt non attribué","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"bdfdbb3aea12cc2a90a8dfed0804800180dd1ea048727b8b9772244fffd4a2a1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"fr","translated":"Échec de l'actualisation","updated_at":"2026-06-17T14:14:37.466Z"} -{"cache_key":"be80684dc0794c2639792f407b99ab913b1e4a3e07b36c5052806587f856d48a","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"fr","translated":"Impossible de créer un code de configuration.","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"bef535e7635a45bf77d764f80a9e2b7dc173fdeea801e0039566227172ea9dfa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"fr","translated":"15s","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"bff9e9d6c21068b2333181dbd4976ef3056cdd8d502937aa49a1bdbcc752007f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"fr","translated":"QR d'appairage expiré","updated_at":"2026-07-01T10:32:03.163Z"} {"cache_key":"c2ad01204fbf7466b5e4e7a4f67f6acbfcce78e915e9feb47107250b068b6c05","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"fr","translated":"Récemment terminé","updated_at":"2026-06-17T14:14:32.183Z"} @@ -106,7 +99,6 @@ {"cache_key":"c57b6089a8064cf26172436a1e12bc7cc7b9a38b747a13ea3bb5f69143dac266","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"fr","translated":"Tableau : {board}","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"c85905c4a1943074a8ab1f9268d4ab9d93fd4f812cdf2c024b881f003d746cfa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"fr","translated":"5s","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"c887bbafc896df50744c8745177bdd449c1e66a8962c65af5b22e8f3221ac9d2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"fr","translated":"Lu","updated_at":"2026-06-16T14:14:38.496Z"} -{"cache_key":"ca3c5573a1dec90ee8d0aaf705fb720d17ff50edb2860636621c82a01f17ccd3","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"fr","translated":"Copier le code de configuration","updated_at":"2026-07-04T16:46:02.634Z"} {"cache_key":"ca4f3560f1c3259a3c8c58d0151768624f085c2bfbf4d48f3c30d2fe47fb794c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"fr","translated":"Mis à jour {time}","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"cb49264e87f513faec8b9301db790ce23fef424469204a743337e52a199f291e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"fr","translated":"Conserver le commentaire après la réponse finale","updated_at":"2026-07-01T01:07:07.533Z"} {"cache_key":"ceea53168997ca7b65bf9974ae044a3bfb38b661e2cb54a1ef1a5e9e7f99b9ec","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"fr","translated":"En cours","updated_at":"2026-06-17T14:14:32.183Z"} @@ -117,6 +109,7 @@ {"cache_key":"d26d54f103b4bbb61dc5a45ab84b223e43051b9313a9e49da8da91dc161aa590","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"fr","translated":"{count} journaux de worker","updated_at":"2026-05-30T15:38:23.998Z"} {"cache_key":"d321120597c21a4c2112eefb541b872d45e34c82c6c3136bc5324eada06c8873","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"fr","translated":"{count} lus","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"d4042ae3c9008b6e58effba20022bc8fad3392ca9ba50876b80b28b401ff4184","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"fr","translated":"Automatisation","updated_at":"2026-06-16T14:14:26.985Z"} +{"cache_key":"d7e7c590ca7259006bed696ffefdc1ca41205fbe47894658e54f1d2b0c939bcf","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"fr","translated":"Désépingler la session","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"d8b82816b2447f9cdf8c1f6e790726787cc313c2e9369bee4b5573e70acc0dad","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"fr","translated":"30s","updated_at":"2026-06-17T14:14:37.466Z"} {"cache_key":"dcb48055b5aafa2813da3820617d5c5653945c57dfbf0d6cb81d4c7f74049b43","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"fr","translated":"Aucun fichier dans ce dossier.","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"dee70ec60c911896eff244b9424d125be13ed5078065ad7ad11270460cad9168","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"fr","translated":"Espace de travail de session","updated_at":"2026-06-16T14:14:33.243Z"} @@ -130,10 +123,11 @@ {"cache_key":"e601fd3af074100b5ca609dcbecf632f30e3a51fbb4c1010f61a18f17af1f071","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"fr","translated":"Réduire l'espace de travail de session","updated_at":"2026-06-16T14:14:33.243Z"} {"cache_key":"e7ce17017703abfa56accdea7fd8c7cec085be211a621f43e7cebff5f58ea654","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"fr","translated":"Non assigné (utilise {agent})","updated_at":"2026-06-17T14:14:32.183Z"} {"cache_key":"ec4996f0075eb153d7704652af430d00e292437c6c9a849aa1b31df2bd098640","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"fr","translated":"Actions sur les fichiers de l'espace de travail","updated_at":"2026-06-16T14:14:38.496Z"} +{"cache_key":"ec805f5f1e24e175b37c5de16b5a08a3df462459a66246dffb1cfa422fd636c0","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"fr","translated":"Renommer la session","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"ecb4a7b21d0b781c0b91593d35292e9744d7ee2cb809859a97bab0cf76043f97","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"fr","translated":"Inconnu","updated_at":"2026-06-16T14:14:33.243Z"} {"cache_key":"ecdc1be0d950aeab683ca3bf442d8c820b15127d4d2bed1ce45433ca2ebea713","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"fr","translated":"हिन्दी (hindi)","updated_at":"2026-06-26T21:43:30.551Z"} {"cache_key":"ef20cf4779b1faf9d7e72f0c196d9d3ab4a48ac5ed59f34412ed07be86eb522a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"fr","translated":"Journaux du worker","updated_at":"2026-06-16T14:14:26.985Z"} {"cache_key":"ef8651b25fc9e66a484efa8c31484827299fa1589240dbe3f6b6faf50f8fc6d7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"fr","translated":"Résultats de recherche","updated_at":"2026-06-16T14:14:38.496Z"} {"cache_key":"f17ed95b76278c50b592087cc112d448162b91e7a486fe0f3b7c13abf341cb37","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"fr","translated":"Atelier Skills","updated_at":"2026-05-31T21:48:25.015Z"} +{"cache_key":"f66ed36e8fc5edf56e4c228748a9be1d51b2d4cac0e0a57fd3734517c956c69c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"fr","translated":"Renommer la session","updated_at":"2026-07-02T14:30:13.470Z"} {"cache_key":"f77ad88f824118b4b9c4d2bedd2fde2c6f82962461ee7c45c1ea641d958ca1b1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"fr","translated":"Protocole du worker","updated_at":"2026-06-16T14:14:26.985Z"} -{"cache_key":"f77dde31bba851a1a15bfbe6c1c235a915d45a548da82ad752b6e7bab775f8b6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"fr","translated":"Associer un appareil mobile","updated_at":"2026-07-04T16:46:02.633Z"} diff --git a/ui/src/i18n/.i18n/hi.meta.json b/ui/src/i18n/.i18n/hi.meta.json index 5232b858f907..3c9a0f5fe8ff 100644 --- a/ui/src/i18n/.i18n/hi.meta.json +++ b/ui/src/i18n/.i18n/hi.meta.json @@ -1,33 +1,11 @@ { - "fallbackKeys": [ - "chat.commentaryLabel", - "chat.commentaryToggle", - "chat.pairingQrExpired.badge", - "chat.pairingQrExpired.reason", - "chat.pairingQrExpired.title", - "chat.sidebar.allSessions", - "nodes.pairing.adminRequired", - "nodes.pairing.button", - "nodes.pairing.copySetupCode", - "nodes.pairing.failed", - "nodes.pairing.generating", - "nodes.pairing.help", - "nodes.pairing.manageDevices", - "nodes.pairing.newCode", - "nodes.pairing.pending", - "nodes.pairing.qrAlt", - "nodes.pairing.qrUnavailable", - "nodes.pairing.showSetupCode", - "nodes.pairing.subtitle", - "nodes.pairing.title", - "nodes.pairing.waiting" - ], - "generatedAt": "2026-07-04T18:11:17.837Z", + "fallbackKeys": [], + "generatedAt": "2026-07-04T18:20:57.063Z", "locale": "hi", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1431, + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/hi.tm.jsonl b/ui/src/i18n/.i18n/hi.tm.jsonl index 9e682da6559d..532e54946850 100644 --- a/ui/src/i18n/.i18n/hi.tm.jsonl +++ b/ui/src/i18n/.i18n/hi.tm.jsonl @@ -1,5 +1,6 @@ {"cache_key":"000fdd51dd213aee9a1e72ff96e7ab3168b3b392c9047c51e825236e06963fcd","model":"gpt-5.5","provider":"openai","segment_id":"usage.mosaic.eightPm","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"8pm","text_hash":"232df857db5e72521b783719e674c41bce48738283c637b44ed2a80fa81ec56c","tgt_lang":"hi","translated":"रात 8 बजे","updated_at":"2026-06-26T21:35:33.329Z"} {"cache_key":"0034af534be58a94abecb8b614e3e2eabc2b7e48547151c24ab723dba3e8eb54","model":"gpt-5.5","provider":"openai","segment_id":"common.audience","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Audience","text_hash":"545c02357695a6ffed97b01a94a46b9aeb4686f4480173da6d0faeae8eb85053","tgt_lang":"hi","translated":"दर्शक","updated_at":"2026-06-26T21:29:32.270Z"} +{"cache_key":"003e83cfa9d3b43b08840a81e37ee0d3a326f07d81d9b30721ffadb965b9f50f","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"hi","translated":"सत्र बहाल करें","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"007d63f80287711fc7bab8edf2d4f221ff9697670db96fd21f43657fb08720e3","model":"gpt-5.5","provider":"openai","segment_id":"common.na","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"n/a","text_hash":"a683c5c5349f6f7fb903ba8a9e7e55d0ba1b8f03579f95be83f4954c33e81098","tgt_lang":"hi","translated":"लागू नहीं","updated_at":"2026-06-26T21:29:24.057Z"} {"cache_key":"00ef3bb0f763c94ee1d6ee32c6bb61990ee8de0a36dc4de01a10a5aedd75fde7","model":"gpt-5.5","provider":"openai","segment_id":"nodes.binding.loadConfigHint","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Load config to edit bindings.","text_hash":"075f4d7948e28bf0f85baefbdfe31e6a11a86d94ac38cbc3c100fdf8981c8839","tgt_lang":"hi","translated":"बाइंडिंग संपादित करने के लिए कॉन्फ़िग लोड करें।","updated_at":"2026-06-26T21:29:59.416Z"} {"cache_key":"00fb88201b08d35df4d2caf821399d202059689f85cb896f5c230d5cc96db111","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.delivery.isolated.description","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Run in its own session","text_hash":"de38879262c558e16382c61088d87a333c82ef508db4a6f0077cd305e03dee06","tgt_lang":"hi","translated":"अपने अलग सत्र में चलाएँ","updated_at":"2026-06-26T21:36:59.582Z"} @@ -73,6 +74,7 @@ {"cache_key":"0eb970747628cf4b419d62f66374cceb76a0ef09e0d6eada1f4040d70862bd4d","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.appearance","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Theme, UI, and setup wizard settings.","text_hash":"5b80d29d431c5b7aba941188ef192192dc8e59aa94a1fd0368c2372188ad72eb","tgt_lang":"hi","translated":"थीम, UI, और सेटअप विज़ार्ड सेटिंग्स।","updated_at":"2026-06-26T21:31:44.582Z"} {"cache_key":"0ed7667c7ece56f3080901f976aeffb58f437ce50632df2acbf491b911b5298d","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.selectFile","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Select a file to edit.","text_hash":"c0e9ac91b0432b9f2cf9d928011da75aa0ce078a15d915299d11bedcbdf705d4","tgt_lang":"hi","translated":"संपादित करने के लिए कोई फ़ाइल चुनें।","updated_at":"2026-06-26T21:30:58.804Z"} {"cache_key":"0eddf9599b6d71f51e2a6f3c364eeac627f8da780532a9fef062288b180e9f7f","model":"gpt-5.5","provider":"openai","segment_id":"usage.query.matching","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"{shown} of {total} sessions match","text_hash":"083883e7e8242df6bfca399e168ab9e7f86e05b26fd26f59fc8e2f98366a5d06","tgt_lang":"hi","translated":"{total} में से {shown} सत्र मेल खाते हैं","updated_at":"2026-06-26T21:34:43.709Z"} +{"cache_key":"0edec00895957ac3e4c52b3cd1149315de604110988657240190663d31c9d8b1","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"hi","translated":"सभी सत्र","updated_at":"2026-07-03T07:53:14.119Z"} {"cache_key":"0f6b1af340bd55073fddb1be59060e51ef9d7165d0829491393a536180463bf5","model":"gpt-5.5","provider":"openai","segment_id":"overview.snapshot.lastChannelsRefresh","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Last Channels Refresh","text_hash":"97a20d4f5b29914b8a08748cfc55d704a4d52ed948180cc90b7c1e06267c692f","tgt_lang":"hi","translated":"अंतिम चैनल रिफ्रेश","updated_at":"2026-06-26T21:33:15.607Z"} {"cache_key":"0fa51fb9e7d660ff75b9a91e4e0461a162f3b5b02106ba4c56c7e7c7295b7033","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.phrases.filingLooseThoughts","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"filing away loose thoughts…","text_hash":"352e9ecf138c39219228e6e09c7d8fde37b02f1dd93fe411cdf781257e9be521","tgt_lang":"hi","translated":"बिखरे विचारों को फ़ाइल किया जा रहा है…","updated_at":"2026-06-26T21:34:24.815Z"} {"cache_key":"0fe85a1bf4b86ecdc3a2d90e92bc495f67df949b23bf5e5c36aea457a49c7863","model":"gpt-5.5","provider":"openai","segment_id":"chat.refreshTitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Refresh chat data","text_hash":"40b8edfd9a1326939cf9db6cca2b31d4af4e815606fe6d86f7982f4f9534e268","tgt_lang":"hi","translated":"चैट डेटा रीफ़्रेश करें","updated_at":"2026-06-26T21:36:16.800Z"} @@ -190,6 +192,7 @@ {"cache_key":"22127f92d635eb81fb21a68e04dcbe55f815cb99fba174e09a451b14481439ab","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"hi","translated":"worker {state}","updated_at":"2026-06-26T21:32:45.360Z"} {"cache_key":"2225826b87c6eb504c8ce8932a9dd65a7d16e6c94493a001b0be8290cdb67a63","model":"gpt-5.5","provider":"openai","segment_id":"channels.health.title","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Channel health","text_hash":"b3575639c4703c004745caf32e50f3458615d3a75b993ef9e7cf58ec1436eadb","tgt_lang":"hi","translated":"चैनल स्वास्थ्य","updated_at":"2026-06-26T21:29:38.612Z"} {"cache_key":"222f87e6ce5cc5f0a004bb298a7f8685925a06caa91176ee2e9e7bb7590490d1","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobs.searchPlaceholder","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Name, description, or agent","text_hash":"1674f571a915b6d9959a4ca175474dc4e5710c3b3ec8ab3479480c29b98fa6f1","tgt_lang":"hi","translated":"नाम, विवरण, या एजेंट","updated_at":"2026-06-26T21:37:06.965Z"} +{"cache_key":"2237e2e4a102a90aa010dd3a5532535336397a2cd04e3b0abe267b7dd1751f95","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"hi","translated":"केवल संग्रहीत","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"226013a92c3c6798a40dc77e3c8eebeefefe6f0c3a6783ddac85a0df840c0612","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.days","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Days","text_hash":"e08c0aa8f558f39fa99077e92036cf7d2210fe88ffae4d3b30fd489d9ac99e02","tgt_lang":"hi","translated":"दिन","updated_at":"2026-06-26T21:37:38.564Z"} {"cache_key":"2260c9dc3a4c7df2e1657ce474510805aab5109df345c965a18edbb3a2c26073","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.toolsUsed","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"tools used","text_hash":"6b8956397b4b2d4c5ffa56aaa71dedc923afc6618e4043f3c5a0805fdff2d1d2","tgt_lang":"hi","translated":"उपयोग किए गए टूल","updated_at":"2026-06-26T21:34:59.501Z"} {"cache_key":"2292e947427923d3408831c234b287d3bee23e76c9e829371aa34f4a244c8b50","model":"gpt-5.5","provider":"openai","segment_id":"common.probe","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Probe","text_hash":"3bd51ab9c14f9514ea37fac91f5f245e93cf5733bd39ca1652e5525a1d67b5d1","tgt_lang":"hi","translated":"जाँच करें","updated_at":"2026-06-26T21:29:19.678Z"} @@ -336,6 +339,7 @@ {"cache_key":"3754bb51c74fe8bb91cc9a7dc4dbf6e3dcd892be4699c347ba779894f8af1667","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.tool","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Tool","text_hash":"2e53bdcd0740867b597599e733c04a994f55fb17c89a61595183a001742e5705","tgt_lang":"hi","translated":"टूल","updated_at":"2026-06-26T21:35:26.182Z"} {"cache_key":"37a7b9c6410bcd4c90169d93d21474534821c314c04a5fd2f6fa4e538f0aca71","model":"gpt-5.5","provider":"openai","segment_id":"workboard.engineDisabledRuntime","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"{agent} uses the {runtime} ACP runtime. Use default start for that session.","text_hash":"5a51973a498c27afdbbce6e423b4e17d59720f6596f1b9741ce0a55ca74f25a5","tgt_lang":"hi","translated":"{agent} {runtime} ACP रनटाइम का उपयोग करता है। उस सत्र के लिए डिफ़ॉल्ट स्टार्ट का उपयोग करें।","updated_at":"2026-06-26T21:32:25.929Z"} {"cache_key":"37c15a95b0d7e637f46b3b0f2b776f5aab296f36765f488163755d93de937571","model":"gpt-5.5","provider":"openai","segment_id":"nodes.binding.node","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Node","text_hash":"e93372533f323b2f12783aa3a586135cf421486439c2cdcde47411b78f9839ec","tgt_lang":"hi","translated":"नोड","updated_at":"2026-06-26T21:30:08.627Z"} +{"cache_key":"37dcae8413c77868e2c1fc6c17a67ab1c9a6bfd90ab1afaa61c5a4bdd6b039d0","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"hi","translated":"सत्र का नाम बदलें","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"38002e842d3a2420fccda567c1478e9ca627f0dab7957848ae2a34e5220a798d","model":"gpt-5.5","provider":"openai","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"hi","translated":"अनुपलब्ध","updated_at":"2026-06-26T21:36:47.054Z"} {"cache_key":"38022bd463afd04ce61ba70be71be5631c511fb9d3511837bf76a99079099fa3","model":"gpt-5.5","provider":"openai","segment_id":"overview.pairing.metadataUpgradeTitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Device metadata change pending approval.","text_hash":"e87def1876a39381e02aca01b40a31388746b85ef4fcc1b89f231e98c5bf45c4","tgt_lang":"hi","translated":"Device metadata change approval के लिए लंबित है।","updated_at":"2026-06-26T21:33:25.942Z"} {"cache_key":"3827f230abe861f32d1bac244f6ea5ca04374c5bb34ff72e917790260b84c8a8","model":"gpt-5.5","provider":"openai","segment_id":"debug.security.warnings","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"{count} warnings","text_hash":"20c152eb8c81aba048645d91a49f254c1f8cb41ed6e6290ac1e6c3bf4a94b913","tgt_lang":"hi","translated":"{count} चेतावनियाँ","updated_at":"2026-06-26T21:31:05.913Z"} @@ -345,6 +349,7 @@ {"cache_key":"38a27b698d7bb838ff630f499dadc41707bc6afc6dfb5cc2fe1291f60218a8f3","model":"gpt-5.5","provider":"openai","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"hi","translated":"सेशन वर्कस्पेस","updated_at":"2026-06-26T21:36:39.137Z"} {"cache_key":"38af1ea1413f2a9da7529e931b9d470596a085bedb88cac087488cc51518a6c9","model":"gpt-5.5","provider":"openai","segment_id":"usage.metrics.cost","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Cost","text_hash":"204a5eb2cd28bcfdf3be9f8c765948e9e831609e3c57048cdbd6b8a94cf49126","tgt_lang":"hi","translated":"लागत","updated_at":"2026-06-26T21:34:31.303Z"} {"cache_key":"38b9f55897e2104ae7ef358093917cb0bee85b1969c36f2878c91c434ab67227","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.off","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"off","text_hash":"b4dc66dde806261bdda8607d8707aa727d308cd80272381a5583f63899918467","tgt_lang":"hi","translated":"बंद","updated_at":"2026-06-26T21:30:21.093Z"} +{"cache_key":"38e6abf56473f02ca886d83f6c44176251111c12e4087a8596e17d94b89dd274","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"hi","translated":"संदेश भेजने के लिए इस सत्र को बहाल करें।","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"3932bd95f537bf091d871d6b8dec89a9189e5018c0b649270b5e9220b3733c23","model":"gpt-5.5","provider":"openai","segment_id":"channels.nostr.websiteHelp","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Your personal website","text_hash":"53b16b8c3ad0dd04970b1988ac06507a2927c2cd378897e57d5c5f9768d5a938","tgt_lang":"hi","translated":"आपकी निजी वेबसाइट","updated_at":"2026-06-26T21:29:59.416Z"} {"cache_key":"39761ea25a0266cd3e2427cbedc2ebdb24dd93fceb11847616eb63dfb721f044","model":"gpt-5.5","provider":"openai","segment_id":"common.ok","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"OK","text_hash":"565339bc4d33d72817b583024112eb7f5cdf3e5eef0252d6ec1b9c9a94e12bb3","tgt_lang":"hi","translated":"ठीक है","updated_at":"2026-06-26T21:29:19.678Z"} {"cache_key":"397c1ba004f7db64c453bc90edccbcf6dc847a9af32cb62393fc0dfe842ea59d","model":"gpt-5.5","provider":"openai","segment_id":"nav.expand","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Expand sidebar","text_hash":"37a5d6485e109bf695382308d0e2cd33913c3e5f7e9ab990e8f1a5f4287b2c6a","tgt_lang":"hi","translated":"साइडबार फैलाएँ","updated_at":"2026-06-26T21:31:23.820Z"} @@ -427,6 +432,7 @@ {"cache_key":"462a5e28c33703cf6ac4ffb3d0e070960ed684c49aef71fad413fd7dd7f55865","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.advanced.shortTermDescription","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Current short-term candidates waiting to graduate into real memory.","text_hash":"0895c842efb140d4ebcd01bd1e976ecfa7e8d7318bd70d4ff1874976ba4729b8","tgt_lang":"hi","translated":"वास्तविक मेमोरी में जाने की प्रतीक्षा कर रहे मौजूदा अल्पकालिक उम्मीदवार।","updated_at":"2026-06-26T21:34:03.457Z"} {"cache_key":"46455cbfd6e86b4db7404548287eb5e0dc635b311640b262fd24074524a05d32","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.updatedUnknown","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Updated Unknown","text_hash":"ae7b1778740ab9aa0178ad72df21881176e9af2e1e4b9e56fb1371524887d319","tgt_lang":"hi","translated":"अपडेट अज्ञात","updated_at":"2026-06-26T21:31:05.913Z"} {"cache_key":"4655bbf79cba7eab9578f835631e6c24348c54f987cf20408b373ecdf298a866","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.channel","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Channel","text_hash":"ce4683e7013a18cdf3d224bfcb4e9594ea8f559e946a837c633defe7d3c32172","tgt_lang":"hi","translated":"चैनल","updated_at":"2026-06-26T21:37:57.526Z"} +{"cache_key":"46647e64e32dd11b3ec7b02d8c87af0b47e6caa59276b7519a30c0bcde0ba97c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"hi","translated":"पिन किया गया","updated_at":"2026-06-26T21:34:37.383Z"} {"cache_key":"4677042df4f414e8266008d2317415b07282a965faf81d510cbbb949bb77a5ef","model":"gpt-5.5","provider":"openai","segment_id":"usage.filters.tool","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Tool","text_hash":"2e53bdcd0740867b597599e733c04a994f55fb17c89a61595183a001742e5705","tgt_lang":"hi","translated":"Tool","updated_at":"2026-06-26T21:34:43.709Z"} {"cache_key":"46ddd616fb81810a8d4e1332c5116ab5109331ee9f14b04d1c0c9ca80070c305","model":"gpt-5.5","provider":"openai","segment_id":"activity.visibleCount","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"{visible} of {total}","text_hash":"9ba4e8a044fb7345bfed5e198ae4d10bcf326b845d2cecc7459c6739a81588af","tgt_lang":"hi","translated":"{total} में से {visible}","updated_at":"2026-06-26T21:31:44.582Z"} {"cache_key":"46dfb525ad3a9827dd206968e31af4342b576e1a06f9cfc7beba37a3b45f30d8","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobState.status","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Status","text_hash":"920e413c7d411b61ef3e8c63b1cb6ad058d5f95f8b481dbafe60248387d8c355","tgt_lang":"hi","translated":"स्थिति","updated_at":"2026-06-26T21:38:12.145Z"} @@ -524,6 +530,7 @@ {"cache_key":"59c0ee0f11c53f3c416ac60395760b9a057315fd16ce60576a1674bbe405c35a","model":"gpt-5.5","provider":"openai","segment_id":"usage.sessions.total","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"{count} total","text_hash":"704e245c4fe1695703fc369c35152938e726c0ed9977ae622db7a3c751ec69d9","tgt_lang":"hi","translated":"कुल {count}","updated_at":"2026-06-26T21:35:13.036Z"} {"cache_key":"59c5a45333e8f6b805a8f8496c569d3cd90ca8f563a6476209f803ec2d9551ae","model":"gpt-5.5","provider":"openai","segment_id":"activity.toolFilter","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Tool","text_hash":"2e53bdcd0740867b597599e733c04a994f55fb17c89a61595183a001742e5705","tgt_lang":"hi","translated":"टूल","updated_at":"2026-06-26T21:31:44.582Z"} {"cache_key":"59c8bb69e7bdfa474939e9747f7f279291f440d4b6839878aa2bacd89e2aa007","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.loadHint","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Load the agent workspace files to edit core instructions.","text_hash":"dfa4dead18217a28f883b93bceed8058424799e23436f0fc8dbf1d7c61cb4ad8","tgt_lang":"hi","translated":"मुख्य निर्देशों को संपादित करने के लिए एजेंट वर्कस्पेस फ़ाइलें लोड करें।","updated_at":"2026-06-26T21:30:58.804Z"} +{"cache_key":"5a109f7e5a1023bda5a791c487bbc081269d1cda65df70fbd0348a66ca0f2057","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"hi","translated":"सत्र पिन करें","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"5a19fa0379637f71b89a82cdd07ac554c084ff0894bad77bedf40fc8a3202cae","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.messagesAbbrev","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"msgs","text_hash":"8dc321b9135ee4fbee83a304b911e871f83e7ae84d344bae6f464804f77b2f86","tgt_lang":"hi","translated":"msgs","updated_at":"2026-06-26T21:34:59.501Z"} {"cache_key":"5a778d51a9edd7000f9834384901aae5b108042f69975bbb1306b1c3d70f854d","model":"gpt-5.5","provider":"openai","segment_id":"workboard.enableConfigKey","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"plugins.entries.workboard.enabled = true","text_hash":"a518af5219772b9cbcbf63f90c12c6e048059e4e5b23a97e9785b36850a77022","tgt_lang":"hi","translated":"plugins.entries.workboard.enabled = true","updated_at":"2026-06-26T21:31:58.515Z"} {"cache_key":"5a98214cd1b635ad74ee8086f092cb8819a110ceca1ea565445578f929dbb86c","model":"gpt-5.5","provider":"openai","segment_id":"common.loadApprovals","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Load approvals","text_hash":"854a446fcdfbfd05db219ccfe9d13527f151c87ba40591c6e7512baca4008045","tgt_lang":"hi","translated":"अनुमोदन लोड करें","updated_at":"2026-06-26T21:29:32.270Z"} @@ -548,6 +555,7 @@ {"cache_key":"5e4d2709a3e8380518a4bd0890e4afa786e60a43218828aa1138edd9c769dea3","model":"gpt-5.5","provider":"openai","segment_id":"usage.sessions.noneInRange","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"No sessions in range","text_hash":"9344ef674e0c4bb1278fcd880df4a06bb1a80b5a5eb50e65b3eea9844c7c1d74","tgt_lang":"hi","translated":"इस सीमा में कोई सत्र नहीं","updated_at":"2026-06-26T21:35:13.036Z"} {"cache_key":"5e5c47944f46b47109cb0877eb732070ead6d6fecd8a625bb70fd490f26d1572","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.phrases.reorganizingAttic","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"reorganizing the memory attic…","text_hash":"29ce330059eccd078fde850d433f7929bc8bee3097efa5f3313377c9989e929b","tgt_lang":"hi","translated":"स्मृति के अटारीघर को पुनर्व्यवस्थित किया जा रहा है…","updated_at":"2026-06-26T21:34:24.815Z"} {"cache_key":"5e5cc07a09dc0b22f9a0905a76f6727176d996304542358923027fec4706e107","model":"gpt-5.5","provider":"openai","segment_id":"login.failure.authFailed.summary","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"The supplied credential was rejected. The most common cause is a stale token or a token copied from another Gateway URL.","text_hash":"3ad5f226785129949e1955cfc8081733ac60eb1304fe003a751202594a014512","tgt_lang":"hi","translated":"प्रदान किया गया क्रेडेंशियल अस्वीकार कर दिया गया। सबसे आम कारण पुराना टोकन या किसी अन्य Gateway URL से कॉपी किया गया टोकन है।","updated_at":"2026-06-26T21:35:56.265Z"} +{"cache_key":"5e6a922e794a9567c9defac5c464a76c8d2045513bca0ecbe214bc77871c7ff2","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"hi","translated":"सत्र का नाम बदलें","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"5e7e6e1cd96d6adbe4dedc11ec6017edb6be1abcb06c54fb9523be98cd9e5f41","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"hi","translated":"Talk त्रुटि खारिज करें","updated_at":"2026-06-26T21:36:30.561Z"} {"cache_key":"5ed5eda24154f39f27917e44110a6385b3c1f4f79c8570a820476d15e5b62652","model":"gpt-5.5","provider":"openai","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"hi","translated":"{count}h","updated_at":"2026-06-26T21:32:39.899Z"} {"cache_key":"5f4e0dc51a4d6fb35b4df216762941975d87b47321c59b8c6858675d012b3098","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobList.clone","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Clone","text_hash":"5779f32fab00c2aae390fe9f63877444b90eb7c12cca5e8903f7c02d2759f9db","tgt_lang":"hi","translated":"क्लोन करें","updated_at":"2026-06-26T21:38:12.145Z"} @@ -656,6 +664,7 @@ {"cache_key":"7475b6f27b432ed097c01158dd60d08aadb18bb131c92f4dc0936a412d05542e","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.placeholderDisconnected","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Connect to the gateway to start chatting...","text_hash":"e764c7024b6aa9a4bf077e298ef69d2fa462e9a7a431783cc23b52c056dff33a","tgt_lang":"hi","translated":"चैट शुरू करने के लिए gateway से कनेक्ट करें...","updated_at":"2026-06-26T21:36:30.561Z"} {"cache_key":"75f5da3e2aa4933ec7a6aec3ceb47c4e290414b2f3e91f5732513d8a1ad793a4","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.advancedHelp","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Optional overrides for delivery guarantees, schedule jitter, and model controls.","text_hash":"a470ce680d28996a5d0ea9c39691bd8b804b85c6766d6bb0ee81c1b01d5fc82f","tgt_lang":"hi","translated":"डिलीवरी गारंटी, शेड्यूल जिटर, और मॉडल नियंत्रणों के लिए वैकल्पिक ओवरराइड।","updated_at":"2026-06-26T21:37:57.526Z"} {"cache_key":"7608cb5d52a7dc34e876143bc36fbbf64624b4160bb417323348bf604ce9cd9b","model":"gpt-5.5","provider":"openai","segment_id":"usage.export.json","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"JSON","text_hash":"db1a21a0bc2ef8fbe13ac4cf044e8c9116d29137d5ed8b916ab63dcb2d4290df","tgt_lang":"hi","translated":"JSON","updated_at":"2026-06-26T21:34:43.709Z"} +{"cache_key":"764e7f39462f45422643f9eb465129ba0990bc2328ffac2efe28182967762425","model":"gpt-5.5","provider":"openai","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"hi","translated":"नया सेटअप कोड जनरेट करने के लिए /pair qr फिर से चलाएँ।","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"769f887b6e8eecd95de99250b3e0cb35187d9a194601eb4a2b5d91c3f92e57e2","model":"gpt-5.5","provider":"openai","segment_id":"usage.daily.byType","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"By Type","text_hash":"26901eeda3b27dae03e02ed92d2af1757fefe9929a2cbaf8bc17e193256d1ba8","tgt_lang":"hi","translated":"प्रकार के अनुसार","updated_at":"2026-06-26T21:34:51.300Z"} {"cache_key":"76d4c25cdce68ea4c5a90f02cee42dcb014bed25b5fe62eec92eb4005d5a6d54","model":"gpt-5.5","provider":"openai","segment_id":"overview.connection.insecureHttpDocsTitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Insecure HTTP docs (opens in new tab)","text_hash":"10642206faa50df2cdcf2852e455c9bf65abd99845a755e832ee5fd1394a2087","tgt_lang":"hi","translated":"Insecure HTTP डॉक्स (नए टैब में खुलता है)","updated_at":"2026-06-26T21:33:34.535Z"} {"cache_key":"76f1566f4ab562bdc2d9b7574483ad1e635b3b247496c30efc81d0a97ed757e1","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.trace.groundedLed","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"grounded-led","text_hash":"28ac99cfc445d54fd3f7e2aa8c5d6f4cf86da63878b58cce1a91911b1cee91b5","tgt_lang":"hi","translated":"grounded-led","updated_at":"2026-06-26T21:34:11.503Z"} @@ -767,6 +776,7 @@ {"cache_key":"8b6ff92095eeea69eb7108349bf596c8626db5578951958f589be69d4cf575ee","model":"gpt-5.5","provider":"openai","segment_id":"overview.access.showPassword","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Show password","text_hash":"6aeaa6a53d09dcad071fdda6280b1e7c42aa164cd0514304ff162e7da440ffaa","tgt_lang":"hi","translated":"पासवर्ड दिखाएँ","updated_at":"2026-06-26T21:33:15.607Z"} {"cache_key":"8b7fabf664f7e144837c86656db40739c5ef3da1713c8f957605a85f4828ba43","model":"gpt-5.5","provider":"openai","segment_id":"agents.channels.title","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Channels","text_hash":"4c8906cf76f5740ab8792aef9f0033fe21a92045e90b357816064e9f6860a03e","tgt_lang":"hi","translated":"चैनल","updated_at":"2026-06-26T21:30:49.011Z"} {"cache_key":"8b910b588857629c268f83c0e3c52ca33584523b4d6df955df74a800a4196278","model":"gpt-5.5","provider":"openai","segment_id":"cron.summary.no","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"No","text_hash":"1ea442a134b2a184bd5d40104401f2a37fbc09ccf3f4bc9da161c6099be3691d","tgt_lang":"hi","translated":"नहीं","updated_at":"2026-06-26T21:37:06.965Z"} +{"cache_key":"8bb561a9ea73353237d27124045028890e9340830df49b96af42c7334dc49a16","model":"gpt-5.5","provider":"openai","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"hi","translated":"पेयरिंग QR की समय-सीमा समाप्त हो गई","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"8bc7b6a5731f0c1b9d2dd29bbe1a5bdd3b4201cac8af1a99e547768705b17e18","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.trace.emptyShortTerm","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"No active short-term items.","text_hash":"e3a71c5ac02b76384ed603efc99062bf70b21092fd094fb3a7c0b3e2647ee757","tgt_lang":"hi","translated":"कोई सक्रिय अल्पकालिक आइटम नहीं हैं।","updated_at":"2026-06-26T21:34:11.503Z"} {"cache_key":"8bee331e89900c48f56f777a004252af572f318f0298e84b0cc181d006b6927a","model":"gpt-5.5","provider":"openai","segment_id":"login.failure.pairing.scopeTitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Scope upgrade pending","text_hash":"530ca34000694683d2bf9162a4680082d71e0e27ea0e4a6f1437fd497682a341","tgt_lang":"hi","translated":"Scope upgrade लंबित है","updated_at":"2026-06-26T21:35:56.265Z"} {"cache_key":"8c16673edff9141682f55be8cbb9d11ee582ee3ea302272b13a6671a700ab4e6","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.newJob","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"New Job","text_hash":"ddacafb76972da324383c04b284cdb4ab1f50620959a20f4682fafb325ee12df","tgt_lang":"hi","translated":"नया जॉब","updated_at":"2026-06-26T21:37:31.894Z"} @@ -818,6 +828,7 @@ {"cache_key":"9413469794cc5015827d25b6fe3aae04d5781c8507825ea0454e6bdc5ce001d9","model":"gpt-5.5","provider":"openai","segment_id":"overview.stats.sessions","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Sessions","text_hash":"6fa3cbf451b2a1d54159d42c3ea5ab8725b0c8620d831f8c1602676b38ab00e6","tgt_lang":"hi","translated":"सत्र","updated_at":"2026-06-26T21:33:15.607Z"} {"cache_key":"94311565912fc66d679216ace0bcdbac5eeb86378ab2bff2f8554f78947f809a","model":"gpt-5.5","provider":"openai","segment_id":"usage.filters.agent","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Agent","text_hash":"11b39c93777e8f1f3983bdba7c72b22fe68cfea20c677e9de53e17cb7dbfb19f","tgt_lang":"hi","translated":"Agent","updated_at":"2026-06-26T21:34:43.709Z"} {"cache_key":"943d56b4ff8c4b2c8edb60f238a69a1cf09538b58587ec396ed7793ac0b7ed4a","model":"gpt-5.5","provider":"openai","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"hi","translated":"कार्ड विवरण","updated_at":"2026-06-26T21:32:04.902Z"} +{"cache_key":"94d2683786f70ee5e88e86db3265b4d09d3182f85b367476fbc15946478bcaea","model":"gpt-5.5","provider":"openai","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"hi","translated":"समाप्त","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"94e2a39dba6dae506b087f6dd109a238546e0e508f83bef66f8767eb423ffe0a","model":"gpt-5.5","provider":"openai","segment_id":"languages.es","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Español (Spanish)","text_hash":"b785e11e822c061a3a5368c55fbeb3f436766ef1e9b3448a605083d0b06ecddb","tgt_lang":"hi","translated":"Español (स्पेनिश)","updated_at":"2026-06-26T21:36:47.054Z"} {"cache_key":"94f5a6afaddecbb4909f5149b3ec3caa80ada513e45328abfc264bc747c80b0d","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.addJob","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Add job","text_hash":"30984d76f83a02109b01e7d7b2fabb4695ddadf3cdfc5c5b79a3d596b8fbb2ba","tgt_lang":"hi","translated":"जॉब जोड़ें","updated_at":"2026-06-26T21:38:06.735Z"} {"cache_key":"94fe349fb3e54cf0cb402dbca91f8bd41ef4f1d7796e4e8ad6ee90420a1306af","model":"gpt-5.5","provider":"openai","segment_id":"agents.selectTitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Select an agent","text_hash":"7d4cf06874635248d725bcf641438f58529d029c16d9c2a88cdb5aee88142b1d","tgt_lang":"hi","translated":"एजेंट चुनें","updated_at":"2026-06-26T21:30:41.032Z"} @@ -880,6 +891,7 @@ {"cache_key":"9eba82fc02325435bbe67d8fce68d46e2a32778bb6bfbfd37d9fa36a65cd0549","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.restartConfirmation.failed","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Could not apply change. Check your connection and try again.","text_hash":"5edd67e358d9d0d506cd4eb7f51c803950ccf17dbef7ea2afa768041c4920018","tgt_lang":"hi","translated":"परिवर्तन लागू नहीं किया जा सका। अपना कनेक्शन जांचें और फिर से प्रयास करें।","updated_at":"2026-06-26T21:33:54.151Z"} {"cache_key":"9ed1fda167ceefc864e84a447ccec31b47ffe8a8c023e692d376685fe4e7a495","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.trace.promoted","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Promoted","text_hash":"0cf04463c4276a6276986c22155bd4a32ce81e8dd162a657dedfa9afb97a7371","tgt_lang":"hi","translated":"प्रमोट किया गया","updated_at":"2026-06-26T21:34:11.503Z"} {"cache_key":"9f0d43b6cf6309665c9408c5f674c101abbcd2414e11e04f06aa739266a5977c","model":"gpt-5.5","provider":"openai","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"hi","translated":"सेशन वर्कस्पेस संक्षिप्त करें","updated_at":"2026-06-26T21:36:39.137Z"} +{"cache_key":"9f5b917ec7b97a1e7013d4a2929a5b32584973de1cf78d9a8d2650ac6cfe75fe","model":"gpt-5.5","provider":"openai","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"hi","translated":"टिप्पणी","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"9f6539da7bf394ebe2c5f4640891e40646b58af9bf2ce8ad0ff8a5877b3a17d3","model":"gpt-5.5","provider":"openai","segment_id":"agents.channels.empty","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"No channels found.","text_hash":"308bdae31be27cbdfcbfaaf785edcc9f949495f540a563ad4fcef7682d108f2c","tgt_lang":"hi","translated":"कोई चैनल नहीं मिला।","updated_at":"2026-06-26T21:30:49.011Z"} {"cache_key":"9f6937e7db1430b8ff0c1dc0fb93497b0e450933131e92573551e6672f0692ca","model":"gpt-5.5","provider":"openai","segment_id":"overview.access.connectHint","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Click Connect to apply connection changes.","text_hash":"473e1a24ad5a8bff00b2db667b8ff16a9b537a5b127dd2af42842620ea830b6d","tgt_lang":"hi","translated":"कनेक्शन बदलाव लागू करने के लिए Connect पर क्लिक करें।","updated_at":"2026-06-26T21:33:07.229Z"} {"cache_key":"9f90ffc0cc2e506fc34a61c89e7861b0c5fa90c96a525e9cfb7f9003c3503f69","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"hi","translated":"लक्ष्य नोट","updated_at":"2026-06-26T21:30:21.093Z"} @@ -974,6 +986,7 @@ {"cache_key":"b08c5e47c1d5364d493de5e1ef6827443438659524e0369e89e5517db15dc530","model":"gpt-5.5","provider":"openai","segment_id":"login.failure.docsInsecure","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Insecure HTTP docs","text_hash":"e3e0b65e3d23e872e78682ef1999987843b57fddcadd99029c9e27af7e7fdad8","tgt_lang":"hi","translated":"असुरक्षित HTTP दस्तावेज़","updated_at":"2026-06-26T21:35:43.732Z"} {"cache_key":"b0f5200b2fc59538aa699db7d3811e30236b1b34baaf190c48c67404d89a70f1","model":"gpt-5.5","provider":"openai","segment_id":"usage.filters.timeZoneUtc","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"UTC","text_hash":"7e5f76c94a635c217e282f79db4fc7ee4bfd9b64044166714067602cc4be620c","tgt_lang":"hi","translated":"UTC","updated_at":"2026-06-26T21:34:37.383Z"} {"cache_key":"b0fcc2f55dc00379da47d26c7bdf774cbdaa86422f792029df1bd5fc6f718a50","model":"gpt-5.5","provider":"openai","segment_id":"overview.stats.cronNext","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Next wake {time}","text_hash":"e66ba10846e8db186b6d61def765932e1da962ecb4bf2ae2ab711934043413f3","tgt_lang":"hi","translated":"अगला वेक {time}","updated_at":"2026-06-26T21:33:15.607Z"} +{"cache_key":"b12631bbecd98249d96bba4d4674396ae20a785c309fda0ed882f125a29b948c","model":"gpt-5.5","provider":"openai","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"hi","translated":"अंतिम उत्तर के बाद टिप्पणी बनाए रखें","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"b13c354b77a877f97337e2419ba7249fb2fa150b051bc42d78f04761de31de81","model":"gpt-5.5","provider":"openai","segment_id":"languages.th","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"ไทย (Thai)","text_hash":"0339954ca7e472c2f007782682a76629a864d63d3e419430bb5f6c72c4c1c88d","tgt_lang":"hi","translated":"ไทย (Thai)","updated_at":"2026-06-26T21:36:51.337Z"} {"cache_key":"b1515ac7dbfe2ebd87d218b9aa63f09ace0af07118b753173ac0fedf84ca52d5","model":"gpt-5.5","provider":"openai","segment_id":"overview.snapshot.subtitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Latest gateway handshake information.","text_hash":"02c4ea80485c6beaf97787975883e58d65e0d1d4dd30e0c4c101e862fb45634a","tgt_lang":"hi","translated":"नवीनतम गेटवे हैंडशेक जानकारी.","updated_at":"2026-06-26T21:33:15.607Z"} {"cache_key":"b15c8a45ccc747c0ea46338b054ef14be199ab0a55fdcaa692cd74a39ee59e8c","model":"gpt-5.5","provider":"openai","segment_id":"workboard.fieldStatus","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Status","text_hash":"920e413c7d411b61ef3e8c63b1cb6ad058d5f95f8b481dbafe60248387d8c355","tgt_lang":"hi","translated":"स्थिति","updated_at":"2026-06-26T21:32:39.899Z"} @@ -1097,6 +1110,7 @@ {"cache_key":"c5b4b517b18f482c247dce2989734811f1a5a408ad7ee5a98123f3f4a5452429","model":"gpt-5.5","provider":"openai","segment_id":"nav.collapse","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Collapse sidebar","text_hash":"aab31cde23ba9783050a754575b80c05e0e799b1542990b24b4b4bde2327e37e","tgt_lang":"hi","translated":"साइडबार समेटें","updated_at":"2026-06-26T21:31:23.820Z"} {"cache_key":"c5be7fa9e1b0e99376972da49b8be745112f63bb08c67a0566008f02b1f76816","model":"gpt-5.5","provider":"openai","segment_id":"cron.runs.selectJobHint","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Select a job to inspect run history.","text_hash":"cd1410f81b92c15d46b317f73d250066fbcaf4dc1f9e1978309f36ab21f17135","tgt_lang":"hi","translated":"रन इतिहास देखने के लिए कोई जॉब चुनें।","updated_at":"2026-06-26T21:37:23.206Z"} {"cache_key":"c6211c52443052cc248ea6004d6abc5472b814d6bab127570b679b72cb7cfa22","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.advanced.sortRecent","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Most recent","text_hash":"7459b8690410d3da0417aab2c54d61c54472d9f59b353a09e11570dd5542fc2a","tgt_lang":"hi","translated":"सबसे हालिया","updated_at":"2026-06-26T21:34:03.457Z"} +{"cache_key":"c63d8d0f04ff683c610a4eaaa7c6ac238c1f64c621a518060e6445133e9a0392","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"hi","translated":"केवल संग्रहीत सत्र दिखाएँ।","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"c64ca148a2750f0eae198e3da1566c4f63632f2dc64a546ca8055782b76ff1ec","model":"gpt-5.5","provider":"openai","segment_id":"common.offline","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Offline","text_hash":"a1794783aab72d205dc532b1170d1be63ebdce8816b57c21acb451c15dab969a","tgt_lang":"hi","translated":"ऑफ़लाइन","updated_at":"2026-06-26T21:29:19.678Z"} {"cache_key":"c742d658df960c718d20a60c0c4ab793b94449094e52f8bf8d21743eeb40a434","model":"gpt-5.5","provider":"openai","segment_id":"overview.palette.items.overview","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Overview","text_hash":"d4b1ea5708dd532930a85188b45aff6f0a3ed458500c7577e0127a538eb0d100","tgt_lang":"hi","translated":"ओवरव्यू","updated_at":"2026-06-26T21:33:46.154Z"} {"cache_key":"c759859ea5e5a0e2667e2f749fa9725f284bed216545f93e1e81da220e297952","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.trace.emptyGrounded","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"No staged grounded items.","text_hash":"896991a7f5bb7b2b05b5eab90680bda0ffd534a9ff068e8bf627ec084307f64b","tgt_lang":"hi","translated":"कोई स्टेज किए गए ग्राउंडेड आइटम नहीं हैं।","updated_at":"2026-06-26T21:34:11.503Z"} @@ -1277,6 +1291,7 @@ {"cache_key":"e5f593257c3abc0015088157b572bd222c965e0da5bf94a8f969b7489aaa287b","model":"gpt-5.5","provider":"openai","segment_id":"workboard.openSession","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Open session","text_hash":"b205bb47f81a30968789eac28cefb848c4b849245d4d12f9311557c5f56ce770","tgt_lang":"hi","translated":"सेशन खोलें","updated_at":"2026-06-26T21:32:11.358Z"} {"cache_key":"e5ffb0d436f88d3a9935896463d36bba8cfc2ea98709107e251ce9b6faa5881b","model":"gpt-5.5","provider":"openai","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"hi","translated":"आर्टिफ़ैक्ट्स","updated_at":"2026-06-26T21:36:39.137Z"} {"cache_key":"e66307ff5147f90d1dabefa33e39505d27dfa5968f3ce374b37b4f6e0ea4154c","model":"gpt-5.5","provider":"openai","segment_id":"usage.filters.pinned","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"hi","translated":"पिन किया गया","updated_at":"2026-06-26T21:34:37.383Z"} +{"cache_key":"e68ed279b3a717d7bc36be0a29c5c97f9969472055d32601b9267664820957cd","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"hi","translated":"सत्र संग्रहीत करें","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"e6df9830aafa6a2376e2395fc06fe0c0e5045d2a650c5e53f297fc491699110b","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.missingHint","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"This file is missing. Saving will create it in the agent workspace.","text_hash":"76f5e100c76d9904ada60584ca5d595980600e772931fdfeb9c5e6c85039ff22","tgt_lang":"hi","translated":"यह फ़ाइल गुम है। सेव करने पर यह एजेंट वर्कस्पेस में बन जाएगी।","updated_at":"2026-06-26T21:31:05.913Z"} {"cache_key":"e737578327e3837f1ac3c86660c4a342566c57099991611f9da5fce8443f5e23","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.advanced.shortTermTitle","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Waiting for Promotion","text_hash":"7c0139f0d89fd220354f1db6f5495cbeb80ebd35bf9006c8aa0e23a92a20844d","tgt_lang":"hi","translated":"प्रमोशन की प्रतीक्षा में","updated_at":"2026-06-26T21:34:03.457Z"} {"cache_key":"e73b6ed6f8450b9e60e30a8fb6975e8b01d4ce39bc0c6379786a2a10596db744","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobs.all","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"All","text_hash":"a52ace420f2175d08b1577a1bea5445e36801229c074ef9ed6c55a73401fd9c2","tgt_lang":"hi","translated":"सभी","updated_at":"2026-06-26T21:37:06.965Z"} @@ -1317,6 +1332,7 @@ {"cache_key":"ee2ad4e283aae140c6d741bdbeb13cf80efbe5b19c09a74bbe22e716216412a8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.statusLive","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Live","text_hash":"b64ac05f17e64d037db81a98f51e2688216e292ae9748f979f04dfbac49fd7fc","tgt_lang":"hi","translated":"लाइव","updated_at":"2026-06-26T21:30:26.471Z"} {"cache_key":"ee4b4b1d4fbd75b4f41ca778325bb88b902789e09d0614503cb0b038b4740c07","model":"gpt-5.5","provider":"openai","segment_id":"nav.control","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Control","text_hash":"32d7e82082479b8cb546187ff0af11e4915e4d17bc28e02dca7425288b79badd","tgt_lang":"hi","translated":"नियंत्रण","updated_at":"2026-06-26T21:31:18.653Z"} {"cache_key":"ee910da56d9e1e4a43f1ad4eff9cb0fd89faa40cf2375517263071986f1b73f7","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.messagesHint","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Total user and assistant messages in range.","text_hash":"fb47849222e3d9e020ec16c1a413c4a9d28d7028ba5496612a57ce0c597fc09a","tgt_lang":"hi","translated":"इस अवधि में कुल उपयोगकर्ता और सहायक संदेश।","updated_at":"2026-06-26T21:34:59.501Z"} +{"cache_key":"eecc51ef2cc6a5972d2e18b16496f44cb975d95515674b39963b327760d9a56d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"hi","translated":"सत्र अनपिन करें","updated_at":"2026-07-02T14:30:19.917Z"} {"cache_key":"ef1210a197447d1cec4fa595953bee23b7ea314b84bbfa8977a7814649494b30","model":"gpt-5.5","provider":"openai","segment_id":"login.failure.pairing.stepApprove","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Approve the pending browser/device request from that list.","text_hash":"d1a4ba76c4f75efa957637632b5a0155d593ed02a3696002cab59d7ec94e933d","tgt_lang":"hi","translated":"उस सूची से लंबित browser/device request को स्वीकृत करें।","updated_at":"2026-06-26T21:35:56.265Z"} {"cache_key":"ef196cef5346a32545e024561276c0866f5f1b9ed89324e93465426a74e9b752","model":"gpt-5.5","provider":"openai","segment_id":"usage.sessions.clearSelection","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Clear Selection","text_hash":"c52ff5ea803d577544a8224d1404ecefa836b803f029d87cd7450af6c18a70ef","tgt_lang":"hi","translated":"चयन साफ़ करें","updated_at":"2026-06-26T21:35:13.036Z"} {"cache_key":"f00c046c6916fc3b5b4677c58ab7de0547954445126c4a534a38dc18f53d509e","model":"gpt-5.5","provider":"openai","segment_id":"common.saving","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Saving…","text_hash":"23e39291d6135814ed7c936e278974544b0df5fbf0eb0427b6700979b7472a93","tgt_lang":"hi","translated":"सहेजा जा रहा है…","updated_at":"2026-06-26T21:29:38.612Z"} diff --git a/ui/src/i18n/.i18n/id.meta.json b/ui/src/i18n/.i18n/id.meta.json index d95eefa21e34..9633c33cf523 100644 --- a/ui/src/i18n/.i18n/id.meta.json +++ b/ui/src/i18n/.i18n/id.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:23:29.293Z", + "generatedAt": "2026-07-04T18:20:58.156Z", "locale": "id", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/id.tm.jsonl b/ui/src/i18n/.i18n/id.tm.jsonl index fe0f6cfd0871..48f1fb9b53e5 100644 --- a/ui/src/i18n/.i18n/id.tm.jsonl +++ b/ui/src/i18n/.i18n/id.tm.jsonl @@ -23,6 +23,7 @@ {"cache_key":"29ff67f80005ca5b6bee49f15be068eecca99873204ec2cf08598606e87363c9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"id","translated":"Nonaktif","updated_at":"2026-06-17T14:15:58.283Z"} {"cache_key":"2a295f5f22ad4dc1600ec4f861d172409054baea9480ad9e75a757a96887392f","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"id","translated":"Skill Workshop","updated_at":"2026-05-31T21:48:34.877Z"} {"cache_key":"2b375a43961fdfcfdd974503b29314f06a33027760f2592eace136fda36732fd","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"id","translated":"Catatan tujuan","updated_at":"2026-05-29T21:01:43.534Z"} +{"cache_key":"2baad8e513c8f1f6bbdbe955b58a98803fa706e73db9eeb8dae841fd64f6d587","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"id","translated":"Pulihkan sesi","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"2bf8183ada97a0a707f6ae0e36e680beeab499564d713ba5f4bedd5d09cd8c22","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"id","translated":"Dependensi","updated_at":"2026-06-16T14:16:50.406Z"} {"cache_key":"2c1c9d989d89df08a4aa50275df82ecb6e55e3e0974e1f8c3ac57ab88ced5b2e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"id","translated":"Kepadatan kartu ringkas","updated_at":"2026-06-17T14:15:58.283Z"} {"cache_key":"2cc67433bf17a54dc88a0d02574db01506421c940c871d840e15e132c670ea15","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"id","translated":"bukti hilang","updated_at":"2026-06-17T14:16:03.086Z"} @@ -39,6 +40,7 @@ {"cache_key":"3c5e5f12e693a6bec8942647373a2f89c6377925e17d80ae0241388d0089d062","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"id","translated":"Tenant: {tenant}","updated_at":"2026-06-16T14:16:44.079Z"} {"cache_key":"3cac7496974e1465804f5501aef81e8d8b959775bbfdee652eeb59bd4843f7a8","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"id","translated":"Dorong dispatcher","updated_at":"2026-05-30T15:38:39.708Z"} {"cache_key":"3cec18685f19cb05472d5d94f723a1ea0638ec412c095b04b172edad7d2fcda8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"id","translated":"Log worker","updated_at":"2026-06-16T14:16:44.079Z"} +{"cache_key":"3db628ace8f6c6b26e89ce04c18492511523a060e7d39d779dc37d7fe9f1045e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"id","translated":"Disematkan","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"4358bc7fb0c6ad391fcdf8bf43aee357afcc725f1ba7481cac8debd7ec254b98","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"id","translated":"Jalur workspace","updated_at":"2026-06-16T14:16:57.267Z"} {"cache_key":"44404222a34c516946794befe2144dac14c34a4c67a9549d1608813e3c1dffc4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"id","translated":"heartbeat {age}","updated_at":"2026-06-17T14:16:03.086Z"} {"cache_key":"483399d71c48cef6276e0e5a6c34a573ee1d57fcdaac2e9ecde554a5d2d90cd9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"id","translated":"berjalan","updated_at":"2026-06-17T14:16:03.086Z"} @@ -66,8 +68,10 @@ {"cache_key":"77c942c82967461d4a622bc16e449c7259112b6e1bb83e09e3d1d0ec9cf61fff","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"id","translated":"Jalankan","updated_at":"2026-06-16T14:16:44.079Z"} {"cache_key":"781981308f67efff0c5ade16e91efa5f13004fcb8d532008e842d617b29f30f2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"id","translated":"Salin kode penyiapan","updated_at":"2026-07-04T16:48:34.014Z"} {"cache_key":"79770d539897d7ac9b8a732af75bc5bcf8108cfaceee352cb9afd4ad73d1c881","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"id","translated":"Kartu yang secara eksplisit ditetapkan ke agen default yang dikonfigurasi.","updated_at":"2026-06-17T14:15:58.283Z"} +{"cache_key":"7c0a57eec8cc11751e844b98508b6102b05404426a8edc87cab3b53221a59ea2","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"id","translated":"Ganti nama sesi","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"8013fce2e44ac76d5f3d1250031b386d22ddd7b761fd9f1da253a256a1ee1444","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"id","translated":"Dibaca","updated_at":"2026-06-16T14:16:57.267Z"} {"cache_key":"82335a79758d012cc71569cd356d68ed8d220749afe5f38b659989e09a6b85b7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"id","translated":"Belum ada catatan operator.","updated_at":"2026-06-16T14:16:50.406Z"} +{"cache_key":"83279b9347999113353e3329acd5681eabb3c3bca3c2229fbb5322c59a7bfa1f","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"id","translated":"Lepas sematan sesi","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"83c5760dbb67987691e4fc4f45359c36906186ae798f1cfb1eabbd97f52c469f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"id","translated":"File proyek","updated_at":"2026-06-16T14:16:57.267Z"} {"cache_key":"84098be15ea5251843b529e5e9e6ac794abe3493901a02efe1d7f612638a88af","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"id","translated":"हिन्दी (Hindi)","updated_at":"2026-06-26T21:43:38.080Z"} {"cache_key":"848d8ef6ebd0fec027a88754bd59c104c8f91617514c25d7cc18421b0150cf47","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"id","translated":"Penyegaran otomatis","updated_at":"2026-06-17T14:15:58.283Z"} @@ -94,6 +98,7 @@ {"cache_key":"aa183c70182ec36fb394d2a21b4eca2d0a81cf81fa4c932ffddfcf20c8558e86","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"id","translated":"{count} dependensi selesai.","updated_at":"2026-06-16T14:16:50.406Z"} {"cache_key":"ab4087229a73b3b71e608dd538e061894816e3175b3fa75c1fa29e9353286567","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"id","translated":"{count} log worker","updated_at":"2026-05-30T15:38:39.708Z"} {"cache_key":"ac61625915d6c867fcc679be097eacd5a54e725384ba3195e072ff64eec3ab83","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"id","translated":"Gunakan obrolan saat ini","updated_at":"2026-06-16T14:16:44.079Z"} +{"cache_key":"ad2e68ef1284c4ccb35761e655323c14da5a27a153f3eba2db3b0fd3c9b235e7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"id","translated":"Ganti nama sesi","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"b037f499cec5f612909a291b6ad3f50324e90bb86d66166f35577ce496c3d16f","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"id","translated":"Русский (Russian)","updated_at":"2026-06-26T21:43:38.080Z"} {"cache_key":"b0e566400a353278f40b1479ba6dd94a64e6303abedadf5914829b199f9fd71b","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"id","translated":"Pelanggaran protokol","updated_at":"2026-05-30T15:38:39.708Z"} {"cache_key":"b2e9da596a5ae9d2c97878e926450c189fe2b110b6696586037eeb4ab8149ec0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"id","translated":"Tindakan file workspace","updated_at":"2026-06-16T14:16:57.267Z"} @@ -101,6 +106,7 @@ {"cache_key":"b437b0517f5e8f792cbb4b4c541d9a19807a38027d4d37e46126adc0971f5848","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"id","translated":"Skills: {skills}","updated_at":"2026-06-16T14:16:44.079Z"} {"cache_key":"bbd4fd1f40eb45f0dcd04d75032626584650aa393d208be9578c7862e05373cf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"id","translated":"terblokir","updated_at":"2026-06-17T14:16:03.086Z"} {"cache_key":"bc30c976e2acb3f362c9dd434f7e1ecfac8da80e65f5f3075d739c59621fa1d2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"id","translated":"Tidak ada file yang cocok.","updated_at":"2026-06-16T14:16:57.267Z"} +{"cache_key":"bc943e1fdb3cebd9366a4b42fc82605da5ec87056f1c2ca1f450144a728919c1","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"id","translated":"Hanya yang diarsipkan","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"c1975e34b46eb09c6e3da8876097d8d07b1eb3f19a3e156f4009e4dc195bfc06","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"id","translated":"5d","updated_at":"2026-06-17T14:16:03.086Z"} {"cache_key":"ca1cd7818628ba3e873a7b3679124ff1868b88af4647fb8ed91f168f8997199b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"id","translated":"15d","updated_at":"2026-06-17T14:16:03.086Z"} {"cache_key":"cc4f677ef970eba7cfb5bb33c25da843f3438a1e73df10d900d7a08c5b041d02","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"id","translated":"Jalankan /pair qr lagi untuk membuat kode pengaturan baru.","updated_at":"2026-07-01T10:33:20.040Z"} @@ -122,13 +128,17 @@ {"cache_key":"da6a4973169844d42ff83c5ef81720c02c4e49e1f8af9d4739ffabb2792af076","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"id","translated":"Ubah filter tampilan, pencarian, prioritas, agen, atau arsip.","updated_at":"2026-06-17T14:16:03.086Z"} {"cache_key":"db0ab96e8ec3965f1ac322a9152776dd03849c5c65facbc7d3c4db16698b4a03","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"id","translated":"Diubah","updated_at":"2026-06-16T14:16:57.267Z"} {"cache_key":"dbd97a91bf0cda6d66e22e140333d00e402ba31801432e7f1287141a5be8b3ae","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"id","translated":"Sesi","updated_at":"2026-06-16T14:16:57.267Z"} +{"cache_key":"de1884dd0281c220a976cc36a217e6be883564a443581542bb49bf08b25f56bb","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"id","translated":"Arsipkan sesi","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"df66b8402857730723182ecd1fbb8c77ca79e4c7539e4269eb19ca3f7f4258f1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"id","translated":"Simpan komentar setelah jawaban akhir","updated_at":"2026-07-01T01:07:58.622Z"} {"cache_key":"df7bab00f1962121b0163be590b8067de30660a13ac55ca455b2678170f234d0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"id","translated":"Folder induk","updated_at":"2026-06-16T14:16:57.267Z"} {"cache_key":"dfac13a3dd2a4bb6dbf3011bc4941d16b54a1f82328322ed3ccb43679ce5c397","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"id","translated":"Kode baru","updated_at":"2026-07-04T16:48:34.014Z"} {"cache_key":"e0737763eb51a1941b8cccb78d07d18432f7685c3e7f35b454f4ad894fa2476f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"id","translated":"{count} terblokir","updated_at":"2026-06-16T14:16:50.406Z"} {"cache_key":"e2c8af96e934057f2b7fc227e2eced830ee92d6aab1f84a51b93667e24097977","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"id","translated":"Penyegaran gagal","updated_at":"2026-06-17T14:16:03.086Z"} {"cache_key":"e349b49ef9df4d1985ca260adc9b3e5fddad17b5cf2157baa62e44e0c8ffdaa9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"id","translated":"Otomatisasi","updated_at":"2026-06-16T14:16:44.079Z"} +{"cache_key":"e523524bb221cdd7479f54d061c011bb5c8900235d23fd5f41602d2f20c10a93","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"id","translated":"Sematkan sesi","updated_at":"2026-07-02T14:30:35.554Z"} +{"cache_key":"e5ca3610e62811f451e93aaef650cbca7eff29c9259d28163bb022c39ae779a6","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"id","translated":"Tampilkan hanya sesi yang diarsipkan.","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"ed18d308aa8a5361e94da93bf2ef239f124417a6391de6147a3536b17e1d1314","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"id","translated":"QR pemasangan kedaluwarsa","updated_at":"2026-07-01T10:33:20.040Z"} +{"cache_key":"ef4733e9cd59bbe281ad226372a26388b4d1243a965f45f3ff06fdb682a4fbad","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"id","translated":"Pulihkan sesi ini untuk mengirim pesan.","updated_at":"2026-07-02T14:30:35.554Z"} {"cache_key":"f0163012fcadddfa9d3c798df4a3f6bb7195ad94443a8cf06f67e055f705e9c0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"id","translated":"Segarkan workspace sesi","updated_at":"2026-06-16T14:16:50.406Z"} {"cache_key":"f0e57bc79f7d7c9f2e562157cf337291af5fca264e55ac6d1ee9cb6b25b53062","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.cwd","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"CWD","text_hash":"0217f1cb7725737f15a6710df3bcfa3bc10a239f0f7801ec3d7168e675f5ebd6","tgt_lang":"id","translated":"CWD","updated_at":"2026-06-16T14:16:59.378Z"} {"cache_key":"f17ae074606bfb4498c7f17735484904a13da9850bcfa58d9567c823f9bec93f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/id.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"id","translated":"Terblokir","updated_at":"2026-06-17T14:15:58.283Z"} diff --git a/ui/src/i18n/.i18n/it.meta.json b/ui/src/i18n/.i18n/it.meta.json index e815c6d512e6..0ae3ab96d4e6 100644 --- a/ui/src/i18n/.i18n/it.meta.json +++ b/ui/src/i18n/.i18n/it.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:22:46.507Z", + "generatedAt": "2026-07-04T18:20:57.531Z", "locale": "it", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/it.tm.jsonl b/ui/src/i18n/.i18n/it.tm.jsonl index 4c2b44dde6cd..4cb96e842951 100644 --- a/ui/src/i18n/.i18n/it.tm.jsonl +++ b/ui/src/i18n/.i18n/it.tm.jsonl @@ -2,15 +2,12 @@ {"cache_key":"00d64ee41ffc286cca5ea5e05fa10ad88e4cf8c71bffef011df5ead1bfc91673","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"it","translated":"Comando","updated_at":"2026-06-16T14:15:54.477Z"} {"cache_key":"0211275042f628fca767913ee41baf7cfe767fe528e674dd67f9ef332c2ecf6f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"it","translated":"{count} dipendenze completate.","updated_at":"2026-06-16T14:15:46.111Z"} {"cache_key":"034b6f860ea33856004add07b07ef3c5c40658482954d7b1cf22eb4d0d1dddf8","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"it","translated":"Orchestrazione","updated_at":"2026-05-30T15:38:31.723Z"} -{"cache_key":"04738bf12a9ed34976e4cac947a7f059e3a36ac7bba5e926015a6f786f5fa7d1","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"it","translated":"Mostra codice di configurazione","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"04ffebf265ed2c2f33aeb8c7ee5552a7311b922e6e88f95a39a47510aeac37be","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"it","translated":"{count} pronte","updated_at":"2026-06-16T14:15:46.111Z"} -{"cache_key":"061e77f3a5b27e54b31ed75e99f1fffc3c1302350fffb6011e1c065d78636611","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"it","translated":"OpenClaw mobile","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"07d31d045a2692f4a59176552a7e449802b4b94c15e6323e890e03f40ab01122","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"it","translated":"Modifica la vista, la ricerca, la priorità, l'agente o il filtro archivio.","updated_at":"2026-06-17T14:19:20.279Z"} {"cache_key":"0af5dc13dd7beb73ec4e5c1d63a69452633a23d474cf033e1c8c18a0ec2f44e3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"it","translated":"Riepilogo area di lavoro della sessione","updated_at":"2026-06-16T14:15:52.376Z"} {"cache_key":"0b6ba71d23553c82ef73deee7bfaa013bb96bee4b8fd5631640bd87485313d47","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"it","translated":"Errore dello strumento","updated_at":"2026-05-31T06:43:59.338Z"} {"cache_key":"0c3e432fffea19a4117099904f55f1155639bf673d40c18ac3eda41bd456b51a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"it","translated":"Nessun file in questa cartella.","updated_at":"2026-06-16T14:15:52.375Z"} {"cache_key":"0d9c68c1213a48fd5937323f8282a4d6ffc053ea81c98acbad27ae4bceba09de","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"it","translated":"Workspace","updated_at":"2026-06-16T14:15:46.112Z"} -{"cache_key":"0e0338b0454eb6e5ed7815424c54b074f4b7dfe8c8778b5575031bf6996711d7","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"it","translated":"Aiuto per l'associazione","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"0e783ef164969aa22668cfc50d9755a299eb5d0378536bfc16ca66177f3999d1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"it","translated":"Radice","updated_at":"2026-06-16T14:15:52.375Z"} {"cache_key":"10f9390d7ed31c5188b9663412768c8d085c4311c703765a09cee4309636e530","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"it","translated":"Nota sull'obiettivo","updated_at":"2026-05-29T21:01:19.873Z"} {"cache_key":"139297788916303e49a2370e4db7e1accdbfd2002cc35a51d7702dffd4b4c88a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"it","translated":"Aggiornato {time}","updated_at":"2026-06-17T14:19:20.279Z"} @@ -18,16 +15,15 @@ {"cache_key":"1833f4179a53df5f8c5e8452c219fbc9b210dee901a52a96ced1b78cbfd339ac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"it","translated":"Automazione","updated_at":"2026-06-16T14:15:39.914Z"} {"cache_key":"1946d8b254aa060689b7a9ca8650b12d6304a69396b8082291594055a67cf771","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"it","translated":"{count} artefatti","updated_at":"2026-06-16T14:15:52.376Z"} {"cache_key":"19f1ef9adadbdb2c9162dda73652cb9690f5e7ba86e581ac51d0fbea5e1d83f7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"it","translated":"Letti","updated_at":"2026-06-16T14:15:52.375Z"} -{"cache_key":"1c03bb7f862e6113141c4b989116ba7b0c228687d5b65e1c17e452ea9714576d","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"it","translated":"QR non disponibile. Copia invece il codice di configurazione.","updated_at":"2026-07-04T16:47:11.053Z"} +{"cache_key":"1aba1c935b5775d5c8168944c98577a8eb86fc6fa6d452c351ceeceb9474b3d8","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"it","translated":"Ripristina questa sessione per inviare messaggi.","updated_at":"2026-07-02T14:30:25.278Z"} {"cache_key":"1c3c542ff6e1c9e7a4c8d5dca28709225c42297de2fd2f313fa716a396400dd3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"it","translated":"{count} modificati","updated_at":"2026-06-16T14:15:52.376Z"} {"cache_key":"1d063cd1bd0599740405fac0d38914b864f8ee20a4dbcf8edbb0a26df3652052","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"it","translated":"Sollecita dispatcher","updated_at":"2026-05-30T15:38:31.723Z"} {"cache_key":"21b1f4fe9bbddbc68d658d5fb5a3b1243fe26b9682b5fdab570482090e78d522","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.copyPath","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"it","translated":"Copia percorso","updated_at":"2026-06-16T14:15:54.477Z"} -{"cache_key":"21dadee26ed4bc1b28e0615c09401bb944110d9ea37777c9df7ab3d5d62d90e4","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"it","translated":"Richieste di dispositivi in attesa di revisione: {count}","updated_at":"2026-07-04T16:47:11.053Z"} +{"cache_key":"238edc13badd2fa46344bbd30f94b606fe5fff4327ae2b1505f922b157a69281","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"it","translated":"Ripristina sessione","updated_at":"2026-07-02T14:30:25.278Z"} {"cache_key":"24ea6e0a55e9e0638ab4f850cff97e1fa1ee9e449a84f9f00839c0657ef105f4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"it","translated":"Dettagli scheda","updated_at":"2026-06-16T14:15:39.914Z"} {"cache_key":"26570ca48dcdb7f65a7d88352c75d60754829da2f8294455fdbcb1f7860a2089","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"it","translated":"Cartella superiore","updated_at":"2026-06-16T14:15:52.375Z"} {"cache_key":"2b3e57d2c58f49b9438c99d917d15a33d2fc266adccb75e66192ffb8ef3915d7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"it","translated":"Caricamento workspace sessione…","updated_at":"2026-06-16T14:15:46.112Z"} {"cache_key":"2b9638a5eeac2a8946d76138d789f33ee36c3338869cac40fc499bb4de1eac54","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"it","translated":"obsoleto","updated_at":"2026-06-17T14:19:20.279Z"} -{"cache_key":"2c40678d2edf958056cb7378e48c8a1d48bf7cff05a921c23545e8e8121291b2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"it","translated":"Nuovo codice","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"2cf47ba786f2a034c95cf8b1fdd1057caedc9f7c5ded7f0fed4e46f3acaadcc9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"it","translated":"Ignora errore Talk","updated_at":"2026-06-16T14:15:46.111Z"} {"cache_key":"2d1c8b6aaa4fbab39282f20bde0c45cdafde6023c920c37729409cf1d28fc3f7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"it","translated":"Schede senza un agente esplicito.","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"2d92adf8b2eede705bae297dd0b2e4916cd60ac4a2042484c8d5c33f692376fc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"it","translated":"Nessun file modificato in questa sessione","updated_at":"2026-06-16T14:15:46.112Z"} @@ -43,10 +39,7 @@ {"cache_key":"37422243d0eb94fff9e401158ee263567e94d694b8422472c33c6b87b0c25b03","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"it","translated":"Tutte le schede","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"376d8bc71134b338c7d5701640f516299078c0f1206e65e4e9857589e082a73d","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"it","translated":"{count} allegati","updated_at":"2026-05-30T15:38:31.723Z"} {"cache_key":"38b73b55e0e54885508225249705edb8752340d685679d501b7729c24492359f","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"it","translated":"worker {state}","updated_at":"2026-05-30T15:38:31.723Z"} -{"cache_key":"3bfdafd6d3dfb181d8ab0aca277c602c4ad1c2699b8026f2053c4e2cb87d3fa8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"it","translated":"Revisione","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"3cec7f61410edbc952139da9adb2a14497f2d131b6b4de67c23ba17535d11bd2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"it","translated":"{count} mostrati","updated_at":"2026-06-16T14:15:52.376Z"} -{"cache_key":"433b2e077637c59a3221510d21000a0b0313e7930c2eccc68fa4d5de9680f299","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"it","translated":"Scansiona questo codice QR nell'app mobile per connettere un nuovo telefono.","updated_at":"2026-07-04T16:47:11.053Z"} -{"cache_key":"442220f2407155295d80000e577cbd23bdae3fccd5b5972bf49f46719ee1fc42","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"it","translated":"Gestisci dispositivi","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"4697a1cee1d84a3d9dfa1c844aab06c43ba62029f243e442f45f6f2c6aa0ce53","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"it","translated":"Bloccate","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"469ac76a2993a9eabe4c514efea0c257ec7ec6c2e32036a242a534326900f9e6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"it","translated":"Densità schede comoda","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"4a6a4c2df665fbcc390de9178b7ce5feca13d046338909c4f7735bec9886901a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"it","translated":"bloccato","updated_at":"2026-06-17T14:19:20.279Z"} @@ -56,13 +49,17 @@ {"cache_key":"50f8cfe47fa3eb8b9a4c62307887d4263dfbd8ebbe1ad2d961c6f2d34fb95f5a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"it","translated":"Scaduto","updated_at":"2026-07-01T10:32:45.702Z"} {"cache_key":"5122dfd79664f9ca79ac88138556c669167e5f1aba543b8000fde90714a8bff6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"it","translated":"Attività Gateway","updated_at":"2026-06-16T14:15:39.914Z"} {"cache_key":"5169ddd7972e6b5213a43ee9b491193037e5f2cdee351586bdee64ff194d36cc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"it","translated":"Risultati della ricerca","updated_at":"2026-06-16T14:15:52.375Z"} +{"cache_key":"51bd9787e98f662b1ed309d71fafa5e668cfc400f58b9248b1fed2f268b0e2fd","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"it","translated":"Solo archiviate","updated_at":"2026-07-02T14:30:25.278Z"} {"cache_key":"522aad631e9eb6e8b781667f04babfa67e7e872f356e3bf006fffcf0047219a0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"it","translated":"Aggiungi una decisione, un blocco o una nota di verifica...","updated_at":"2026-06-16T14:15:46.111Z"} {"cache_key":"527f7b455533c166625ce1dbeabc38f4490c46b0cf1bb0b128695c75c682c024","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"it","translated":"Nessun file corrispondente.","updated_at":"2026-06-16T14:15:52.375Z"} {"cache_key":"5a9548f1084f3cf999e6ebe5a9f4936eef0a834882640344c5ae0ae724cdf8b3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"it","translated":"Revisione","updated_at":"2026-06-17T14:19:01.168Z"} +{"cache_key":"5d85969158f163e81ecbb25c8c41ca5de39c1c5ab0c980b1414ce87f8ebfb245","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"it","translated":"Mostra solo le sessioni archiviate.","updated_at":"2026-07-02T14:30:25.278Z"} {"cache_key":"5fd9092703d43b8ce7f885f30ed053516a60425f8f1384dd1c2ed5373e844ab8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"it","translated":"Mantieni il commento dopo la risposta finale","updated_at":"2026-07-01T01:07:32.292Z"} {"cache_key":"5fddf1e0355e6c7ad4b84090846ae14761144c19b8ee135c3dd639e9e0652e8d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"it","translated":"Azioni sui file dell'area di lavoro","updated_at":"2026-06-16T14:15:52.376Z"} {"cache_key":"63c50429a6e4dfd077cc94e3719c1f6a81da1831499bbbdeb4dd873408136fb3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"it","translated":"Workspace sessione","updated_at":"2026-06-16T14:15:46.111Z"} {"cache_key":"64d7150aad7e267d8fd248a1c6be0d8e9c45f75f65e3b7942da5a4cb1992b587","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"it","translated":"Comprimi workspace sessione","updated_at":"2026-06-16T14:15:46.112Z"} +{"cache_key":"64f116b99db45b068a4aa09ec0f320035601a323b63b17b4e5b5f63533fbd81c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"it","translated":"Fissa sessione","updated_at":"2026-07-02T14:30:25.278Z"} +{"cache_key":"65b5fe6f0e3b45f8bf91233d684b649f8ac6e699987f4d8627bae32ac4b79b7d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"it","translated":"Fissate","updated_at":"2026-07-02T14:30:25.278Z"} {"cache_key":"67f69efc75f0991664a402cfb7bcf1ef1f6470a3002a73311604969e3785062a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"it","translated":"{count} bloccate","updated_at":"2026-06-16T14:15:46.111Z"} {"cache_key":"68197fccfd5758980aebad06c5fae915d1f5ac25a093a12c9dc2c0ce1729a6a2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"it","translated":"Mancante","updated_at":"2026-06-16T14:15:46.111Z"} {"cache_key":"69c3fe1ecfb902c40d78a12c2c5841c7d359a59718af63b5eebf288020b718b2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"it","translated":"in esecuzione","updated_at":"2026-06-17T14:19:20.279Z"} @@ -80,7 +77,6 @@ {"cache_key":"7d787e13c42d51c7c3d2b5f0cdb02912d1ae422eb341ab80c43f9576cb0ebd21","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"it","translated":"30s","updated_at":"2026-06-17T14:19:20.279Z"} {"cache_key":"81435fccbed10b32567dd2f9d1a96774ee201ea88d8244a9ac65c535575ef840","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"it","translated":"हिन्दी (Hindi)","updated_at":"2026-06-26T21:43:33.749Z"} {"cache_key":"85132489f056e73c204760a92a7fcebba0bc13187acb73da9c1bd32db8c1a9d5","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"it","translated":"Esamina, perfeziona e applica le proposte prima che diventino skill attive.","updated_at":"2026-05-31T21:48:28.496Z"} -{"cache_key":"87d47d110c1b6166da881437dac723f101c5cbb383804d69067ec3b042f97ee1","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"it","translated":"Impossibile creare un codice di configurazione.","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"8ae364941577e40a34bbbb97bebe2005a623b0a7c2cdfd93a46439b5c1c42074","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"it","translated":"Stato workboard","updated_at":"2026-06-17T14:19:20.279Z"} {"cache_key":"8b03d20ad1be391725af51f8297a13b882a47795b1e2f27bfff8f833f641f214","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"it","translated":"QR di pairing scaduto","updated_at":"2026-07-01T10:32:45.702Z"} {"cache_key":"8bc992c95973ae45bbe50bba495103ec642179b547f3643035e12b23f4e19702","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"it","translated":"Commento","updated_at":"2026-07-01T01:07:32.292Z"} @@ -92,12 +88,12 @@ {"cache_key":"96d54701dd5e84767890450eb26e7452721b238930dcc1cd62a1380cca07d553","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"it","translated":"Schede assegnate esplicitamente all'agente predefinito configurato.","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"984e7a270295ad43620fa65cd5f9924fbcf03823d071ee21643dca9f3127cd67","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewStale","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Stale","text_hash":"40c9e59c5e152b0ae9affc84d8461c29b75a7709e4506307eeedf246b526014e","tgt_lang":"it","translated":"Obsolete","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"99ab86a5ef6a3084155e5647586eadeac7a19daab46eb9c3239720cec3fd48a5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"it","translated":"{count} schede","updated_at":"2026-06-17T14:19:01.168Z"} +{"cache_key":"9a3d6f7318815374d3fdcaf08ad12638338ba2e55f8fd11adca10e92e71e4f4c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"it","translated":"Rinomina sessione","updated_at":"2026-07-02T14:30:25.278Z"} {"cache_key":"9d66a88ab3aea566e971400b6e35e09b4574f1f67778bdc75beb01af502ba5b2","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"it","translated":"Server MCP, autenticazione, strumenti e diagnostica.","updated_at":"2026-05-31T05:36:43.452Z"} {"cache_key":"9da85ece1ae1ed53b77e7c7fe92ee25b0d848e3813de443fe10e33f3e8433dac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"it","translated":"{count} letti","updated_at":"2026-06-16T14:15:52.376Z"} {"cache_key":"9f3eff21998d04aaf3c9e3109e705980fd19431d37018057418c343d9b29397c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"it","translated":"Artefatti","updated_at":"2026-06-16T14:15:52.375Z"} {"cache_key":"9f8e84cb4fe101205ba11057fa60c524992d9b4273be5d4b3f64573acc71902c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"it","translated":"Prova","updated_at":"2026-06-16T14:15:39.914Z"} {"cache_key":"a25663846ed52baa56a6aff1672aafbadecfb5750ab4a13193e10b386e30e8c1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"it","translated":"Anteprima","updated_at":"2026-06-16T14:15:54.477Z"} -{"cache_key":"a281502f3f15eac2395ca87960d8e820d9a042e71fc728717b89727723436bcb","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"it","translated":"Creazione di un codice di configurazione sicuro…","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"a3f68978aafcd63d792cc9413c546889620dba867ebef1192718754c87eddbd6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"it","translated":"Aggiorna workspace sessione","updated_at":"2026-06-16T14:15:46.112Z"} {"cache_key":"a4519524e4ec52524f8f667fbb1201e9abefd49235ebb7aa36be1c6ef7b23223","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"it","translated":"Percorso area di lavoro","updated_at":"2026-06-16T14:15:52.375Z"} {"cache_key":"a8aee7f248098dcaeeaf23a6303ad265847c71d1b40f5d2c073fffacfbde5776","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"it","translated":"Protocollo del worker","updated_at":"2026-06-16T14:15:39.914Z"} @@ -117,23 +113,21 @@ {"cache_key":"c8039dc836c4bf4718d5c6c17173995582fd8b973276f619512b671b6e4e65a4","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"it","translated":"Русский (Russian)","updated_at":"2026-06-26T21:43:33.749Z"} {"cache_key":"c8742ae661dc1fb3e13b331525e41ea355b2495bca25794d3242c154ac23e10c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"it","translated":"5s","updated_at":"2026-06-17T14:19:20.279Z"} {"cache_key":"c9876a8713c0fc961213a09937aea3ddb5ea70549b4554529756ae91ccddc2d1","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"it","translated":"MCP","updated_at":"2026-05-31T05:36:43.452Z"} -{"cache_key":"cbd646b7ffbb08ce3947e75f652ddc02a0f21491e825b8baaf6b3296332693f0","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"it","translated":"Codice QR di associazione di OpenClaw mobile","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"d03cf78717957e2642994a48ca8715ca340a0e649a5c92fb5067bf8a183a57ed","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"it","translated":"Agente predefinito","updated_at":"2026-06-17T14:19:01.168Z"} +{"cache_key":"d30b3563d6bd3ca3993ad2ac2ca69dba8d97047c99cee996d3de33f92ba5da4b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"it","translated":"Rimuovi sessione fissata","updated_at":"2026-07-02T14:30:25.278Z"} {"cache_key":"d6a8a9541f05c7c0b955a4a0bd534889f6c1e96c439721a7d0d76aa4beac4042","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"it","translated":"Esecuzione","updated_at":"2026-06-16T14:15:39.914Z"} {"cache_key":"d77da3b821794cd290567494746273d099c6360e24e203840ee755a457ab0968","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"it","translated":"Pronte","updated_at":"2026-06-17T14:19:01.168Z"} -{"cache_key":"d927f342d06436fc7122daeaf767d8c0608fdef689cc27b8985bc2ea5a440bff","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"it","translated":"Copia codice di configurazione","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"db1e5463f95e00564af990dd0038ca490c7876d98c37d2fd2e65dba136ccc0e5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"it","translated":"Vista workboard","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"dfb2a92486e663dec2716acd4dc7bc94f83983500ca8221a08c54a82785172a5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"it","translated":"Densità schede compatta","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"e07fe4644104233a4c3e29c7ad2c23358eae26b86f11644d2fc8053f96750a1f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"it","translated":"Diagnostica","updated_at":"2026-06-16T14:15:39.914Z"} -{"cache_key":"ec111e769e31926fa21fbabf2b3f42c12b41ad0b0064cde1a9b82bbf1b1b06e6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"it","translated":"Le app mobile ufficiali di OpenClaw si connettono automaticamente dopo la scansione.","updated_at":"2026-07-04T16:47:11.053Z"} +{"cache_key":"e46ec9eee700a9a153d4c046b2a558c48108f2a76ebf7968b8f5bbc12329e6b8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"it","translated":"Rinomina sessione","updated_at":"2026-07-02T14:30:25.278Z"} +{"cache_key":"eaf02e5b4ba84a2d6cdda27d28e772c930240ad03bae57f187cc7ee06279bf29","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"it","translated":"Archivia sessione","updated_at":"2026-07-02T14:30:25.278Z"} {"cache_key":"ec16ac5b84adf36e1b6d8f21763b84bc9627036f4cc02afb3d743ee26f0e1f83","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"it","translated":"Vengono mostrati i primi file corrispondenti. Affina la ricerca per restringere i risultati.","updated_at":"2026-06-16T14:15:52.376Z"} {"cache_key":"eeced37dc422b08083d1feadf555739866efcf58c91fad61fa5f215851c0a36a","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"it","translated":"Workshop delle Skill","updated_at":"2026-05-31T21:48:28.496Z"} {"cache_key":"f0073fdbbc0395f4a7896f85e4a7e9348a52e38fc322ce119449a230e9df8144","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"it","translated":"In attesa di dipendenze: {parents}.","updated_at":"2026-06-16T14:15:46.111Z"} -{"cache_key":"f1d6520276f93046a20799754bdd9c47f9744b0a569fb606da117db2439d35d6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"it","translated":"È necessario l'accesso da amministratore per creare codici di configurazione.","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"f337053b6efc58e0b0b34e4e9ceafce96863cb5116e65b59ec28c2b40a08b359","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"it","translated":"Cerca file","updated_at":"2026-06-16T14:15:52.375Z"} {"cache_key":"f7eafb8ff2741c1c4b8b96872e5f58bdd454a1765cff0f31d0a1614c64aa135f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"it","translated":"Note dell'operatore","updated_at":"2026-06-16T14:15:39.914Z"} {"cache_key":"f89df9b85d49891bd2d5e2edd082f0b0ce8e9c35216140ac1bc99d006ed99cd5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"it","translated":"Sessione","updated_at":"2026-06-16T14:15:52.376Z"} -{"cache_key":"fcc48dfd5282bb65fba73bb5325ca7f22fb1bb6d86169f00e3466e047d5422c0","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"it","translated":"Associa dispositivo mobile","updated_at":"2026-07-04T16:47:11.053Z"} {"cache_key":"fd90ff517fe38f8f425f638c15307c2e4e1a23a0915baa0f8e3396e74ef2fc41","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"it","translated":"Completate di recente","updated_at":"2026-06-17T14:19:01.168Z"} {"cache_key":"fdedb51f0df618cd58829d6441986fe05e18a11317d336484f92021fbd7a4400","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"it","translated":"Workspace: {workspace}","updated_at":"2026-06-16T14:15:39.914Z"} {"cache_key":"fe2dd36bd9ffc987d984a2d7bfa18da5e5fff1c20c34d1d6446f9314b42acf36","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"it","translated":"Aggiungi nota","updated_at":"2026-06-16T14:15:46.111Z"} diff --git a/ui/src/i18n/.i18n/ja-JP.meta.json b/ui/src/i18n/.i18n/ja-JP.meta.json index 12af58f58981..06b2dddf0d1e 100644 --- a/ui/src/i18n/.i18n/ja-JP.meta.json +++ b/ui/src/i18n/.i18n/ja-JP.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:21:58.655Z", + "generatedAt": "2026-07-04T18:20:55.898Z", "locale": "ja-JP", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/ja-JP.tm.jsonl b/ui/src/i18n/.i18n/ja-JP.tm.jsonl index 3de843c781c1..ca3d228a699e 100644 --- a/ui/src/i18n/.i18n/ja-JP.tm.jsonl +++ b/ui/src/i18n/.i18n/ja-JP.tm.jsonl @@ -1,9 +1,11 @@ {"cache_key":"006407b5237759e896afaf3d8148871d7d9209df0948f6ec4f9dfa7b81e3855c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"ja-JP","translated":"セッション","updated_at":"2026-06-16T14:14:13.256Z"} +{"cache_key":"0085d8808eeb05adc17f1991603d424b5de6bba9f02435dd3bf7c5a2e693f05a","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"ja-JP","translated":"アーカイブ済みのみ","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"01e35e79010ce3bb0b4ec1261d75952f5cbc18eaf6ad9fd832698881853d13b8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"ja-JP","translated":"概要: {summary}","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"021ef74a883eb57eee66395b69faa99471af46b59970a74cea7b03948f6e4312","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"ja-JP","translated":"修正リクエストに現在のチャットを使用","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"048a3f6be3460115f38a014e6b0836da436a9bfcc7dc2c5ca45af4a50f620535","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"ja-JP","translated":"失敗した試行","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"09124221295fdeb3724f25d7dcb59d631c6da05808d24986ff758869c4188a49","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"ja-JP","translated":"{count} 件読み取り","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"0a8a9cdabf5314038b163d124dc468e64c3a1a4e9adeb004acc9c134be364cae","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"ja-JP","translated":"このビューに一致するカードはありません","updated_at":"2026-06-17T14:14:09.255Z"} +{"cache_key":"0aa7fc44541de1472246b7f1e236c59f0e559974384100d432785c05809371a7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"ja-JP","translated":"セッションを復元","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"0b186c6ce4e7be3cef7a0a7aa338fe0a5f68a091cfbd24eeb2b359a59177c907","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"ja-JP","translated":"自動更新","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"11a891277b97903cbca169aebb523895b61151412e09a88cc5530ffb1dbf0d67","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"ja-JP","translated":"実行中","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"130a12035aed62707b508a46e200295409fc1211f5efa9e4546b322ad191f30e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"ja-JP","translated":"{count} 件の依存関係が完了しました。","updated_at":"2026-06-16T14:14:13.256Z"} @@ -21,16 +23,14 @@ {"cache_key":"2c6b47456f6d05c3efd10d5ef8ae6c8a3e05795537aaffab18fb76cfddf92760","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"ja-JP","translated":"現在のチャットを使用","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"2ddb5a80f38d8627f3c42b93e02deb77df239f2ad75e90ac4563bae2a15392cf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"ja-JP","translated":"Talkエラーを閉じる","updated_at":"2026-06-16T14:14:13.256Z"} {"cache_key":"2ef47ce75ae0c1c79f5c317cf628b6ea77c5324ef1b6506af78452b90d61ea7e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"ja-JP","translated":"証跡なし","updated_at":"2026-06-17T14:14:09.255Z"} +{"cache_key":"3001c86201dd2786efd8f6b7f0068df0d1d2e3c3781ae077fbb76da20270f64e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"ja-JP","translated":"セッション名を変更","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"3452c228a51e4eb032dd07ebdefcbc573de94584a51771d63bc35e75a8b39e0e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"ja-JP","translated":"30秒","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"351407cf9a2dc3b2e70edd7baaffa64e50c9498e3a5a09d606be2b8fee2290e8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"ja-JP","translated":"ワーカープロトコル","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"385cee2f0cb4a83b0e53424167bef43a488ba00da62ea56753a393ec6b6960b2","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"ja-JP","translated":"目標","updated_at":"2026-05-29T21:00:25.945Z"} {"cache_key":"3866d6fe833dd0a39bea788940dd5a29c8a3fc5be06434254440ee77432b1234","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"ja-JP","translated":"未割り当て({agent} を使用)","updated_at":"2026-06-17T14:14:04.172Z"} -{"cache_key":"3ac90f0c4badb27ed79f13cd01dcf7da54b91ef0157cdc692603a8a60b555ad6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"ja-JP","translated":"新しいスマートフォンを接続するには、モバイルアプリでこのQRコードをスキャンしてください。","updated_at":"2026-07-04T16:45:54.273Z"} -{"cache_key":"3b8f919747eeaed4cfcbddcc22ae5422dbc337d10daa90e15a6a64f60d170da4","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"ja-JP","translated":"デバイスを管理","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"3b9ab9b0ee0e50fd5dc801f82f5a2afeba36c5eb5957b91be8a3bf4d1f5bb4fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"ja-JP","translated":"ビュー、検索、優先度、エージェント、またはアーカイブのフィルターを変更してください。","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"3bc368fb375458b8dad3c0497e819c7a670d4e2f82be9ea2dbcaca621aca375f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"ja-JP","translated":"レビュー","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"3ce3b0e0236c2154bbf50cb7b7a756f54288fa115c4c2a3f407b3b3f52fad90a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"ja-JP","translated":"セッションワークスペース","updated_at":"2026-06-16T14:14:13.256Z"} -{"cache_key":"3df094347cba7046bd1307d05ce771182b999aa442c15e4a9c6c73abdced5972","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"ja-JP","translated":"公式の OpenClaw mobile アプリはスキャン後に自動的に接続します。","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"3f447024a24678f806bb734ad4784c0242ce4cbac643e19cd4a125e7c02aa6b8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"ja-JP","translated":"{count} 件のカード","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"432128a92ae53f46390b5e3136ba2b83dabd39da3589eff1e689d7a823a17638","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"ja-JP","translated":"欠落","updated_at":"2026-06-16T14:14:13.256Z"} {"cache_key":"442e876e3bcb7fb73140de10b7e97da17a2a1d2d9925d051420fd9865910e41c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"ja-JP","translated":"更新日時: {time}","updated_at":"2026-06-16T14:14:04.101Z"} @@ -38,7 +38,6 @@ {"cache_key":"47ae58fcddc0a2dadf4cfb8d7718eb8c885feb72f446ea1b3e07c4e1a967448a","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"ja-JP","translated":"プロトコル違反","updated_at":"2026-05-30T15:38:16.645Z"} {"cache_key":"4d80fe4679170b8925079a81a06764f716990ca60feb8a42733c3d36bca4c182","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"ja-JP","translated":"更新済み {time}","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"4dab7c420f96d7d99f81ed15732d8b36ac0994aef66a1db81e38272e3622408c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"ja-JP","translated":"準備完了・未割り当て","updated_at":"2026-06-17T14:14:09.255Z"} -{"cache_key":"4e0dbe1ca4831ffb4e90b6a8ddfdf7bdc544b6cb5ec3307b862b9a71196eeeeb","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"ja-JP","translated":"QRを利用できません。代わりにセットアップコードをコピーしてください。","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"4e3047fafef6120a7f61a29865e47979da19db4274ddd2ac968cb0dc32371bc4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"ja-JP","translated":"期限切れ","updated_at":"2026-07-01T10:31:53.015Z"} {"cache_key":"4f010f09248a9fab789bebc7de6d1532acf9506731f9ae3bc9d5ee1fdda65c94","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"ja-JP","translated":"変更済み","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"4f0aa58136e8af655092d668b85f40ec49949813d16d2d981787196053ff9a1c","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"ja-JP","translated":"ワーカー {state}","updated_at":"2026-05-30T15:38:16.645Z"} @@ -50,11 +49,9 @@ {"cache_key":"55ac19834db57e4cfcdaef2cab55692592acd72896a6d33e49f6d58f4243aadc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"ja-JP","translated":"親フォルダー","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"563d0fa1ab6421d1083a34628dc0eb6e2dafc9472e9f756ea820827a44467cf5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"ja-JP","translated":"ワークスペースのパス","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"59ccbd444e326ac0a30af907d541eb0690c55f3d8f34091fee9a05a99a3ad32d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"ja-JP","translated":"テナント: {tenant}","updated_at":"2026-06-16T14:14:04.101Z"} -{"cache_key":"5a678d040f1dfe15cda619f0d1c80da66d1391040dffeceab13f9aa04c370921","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"ja-JP","translated":"ペアリングのヘルプ","updated_at":"2026-07-04T16:45:54.273Z"} -{"cache_key":"5bcfede198df1a05bb2a7f3b21eba04a8f00d1fce14f516e40364fb5976d19c4","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"ja-JP","translated":"レビュー待ちのデバイスリクエスト: {count}","updated_at":"2026-07-04T16:45:54.273Z"} -{"cache_key":"5c83134b921a1771a2353a351124cc047c4dc209afd6bc0747a49ffe30b5c47f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"ja-JP","translated":"OpenClaw mobile ペアリングQRコード","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"5e55b04188534e8f4b34286e6a219a1b0e139a5b9c736493ef93472843f73681","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"ja-JP","translated":"{count} 件ブロック中","updated_at":"2026-06-16T14:14:13.256Z"} {"cache_key":"603262ecc6d47b82918c35c4c70d1c86fec0b832705fdb6ab76625a718dcca51","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"ja-JP","translated":"検索結果","updated_at":"2026-06-16T14:14:19.257Z"} +{"cache_key":"64e32f15dd91d9c1630bb202ffeb765e39ed8bb7f53d6184bb1a0860fb9ca636","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"ja-JP","translated":"セッションをピン留め","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"6538d7661d4744c83c2387c9e808dd8d50d2ca5a8b5033e26906e380e0577c22","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"ja-JP","translated":"ワークボードビュー","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"666163b90dc4f46f8db133e2d20fccabb1c52c173db28a6f77adcfb25665bb08","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"ja-JP","translated":"明示的なエージェントがないカード。","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"66eab4d4ed09f189f44a0813d4e385cd985e4a8df428337082aed7b991259c49","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"ja-JP","translated":"हिन्दी (ヒンディー語)","updated_at":"2026-06-26T21:43:27.880Z"} @@ -63,18 +60,15 @@ {"cache_key":"6fd643605be97b117d68111ea4ed6d6da05c0ce7f2880df9b15e63d79be3411d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"ja-JP","translated":"このフォルダーにファイルはありません。","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"6fd82965e1b9919018ec76c9379372b26fc4e633cab0f1c72647895a671fdf45","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"ja-JP","translated":"コメント","updated_at":"2026-07-01T01:06:53.045Z"} {"cache_key":"7032ea193e157bd2e91c64f3fc49a6f9711b68e0495bb57c71b650f95bbcc194","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"ja-JP","translated":"ワークボードの状態","updated_at":"2026-06-17T14:14:09.255Z"} -{"cache_key":"706985600f5359cf9ca5b201b62e5e1e0a520728f990cb42cbe48f9530625800","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"ja-JP","translated":"セットアップコードを作成できませんでした。","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"72c184ed5db446880bf6d56893c5f172b86ebd075fac9a236af5af6e86d2e941","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"ja-JP","translated":"Русский (ロシア語)","updated_at":"2026-06-26T21:43:27.880Z"} {"cache_key":"7a805e49dd5b20f9b92d98c5001a31aa3eb357c6eaee8a9819e881abb4999e9a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"ja-JP","translated":"5秒","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"7bb05575fca6bd71d81a7f765b29bf12727a860bc43487431194d0cda4f51209","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"ja-JP","translated":"添付ファイルが追加されました","updated_at":"2026-05-30T15:38:16.645Z"} {"cache_key":"7e0fafe9210a70cd3b7aeed4be6a6432ea3fd5787d235d42e27b89eb1ae0a7dc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"ja-JP","translated":"セッション","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"7e421b9e40710ea6c957b86850e81b387cf5b6716d89d174ec6a9c1f24611905","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"ja-JP","translated":"不明","updated_at":"2026-06-16T14:14:13.256Z"} -{"cache_key":"7fec028a7353af5c37aae885d30d0b8ec72d6c8c21bf4cbc94b8935af2d5b91b","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"ja-JP","translated":"セットアップコードを表示","updated_at":"2026-07-04T16:45:54.273Z"} +{"cache_key":"7f38bbded7ed760bb71e2d08fcb5861692b8c695643187b90931f2bbd991e1f5","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"ja-JP","translated":"セッションのピン留めを解除","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"83e3008ef062ae16a460f2574f19c677ab084f34c1a47062b66af61a4bf4fc49","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"ja-JP","translated":"オーケストレーション","updated_at":"2026-05-30T15:38:16.645Z"} {"cache_key":"84aef0146791963f3b8e38ca57d502c58609eab48c2df66f58816b68f5881f08","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"ja-JP","translated":"すべてのカード","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"84d9378e9eb6129bd39934ca77c9cb5da53b51fa37e116bc776dd48c6f4ec325","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"ja-JP","translated":"最初に一致したファイルを表示しています。検索条件を絞り込んで結果を絞ってください。","updated_at":"2026-06-16T14:14:19.257Z"} -{"cache_key":"854d0efc1e3e477306569f887074d50bb6f86e4488b295c39ed054161a4c8928","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"ja-JP","translated":"安全なセットアップコードを作成しています…","updated_at":"2026-07-04T16:45:54.273Z"} -{"cache_key":"86d81baad028bdb5b5894a78be2c8e259fcf7cada2f2c71c590d577b99191ea8","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"ja-JP","translated":"モバイルデバイスをペアリング","updated_at":"2026-07-04T16:45:54.272Z"} {"cache_key":"8809775a43d491eb956485f4b8e8033be545eda1b7e858f064b2c1cfb6ba2229","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"ja-JP","translated":"更新日時","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"8840a5168409760eea9ef33ccf154f697349a29ad6e16da6af6a8f24372a3227","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"ja-JP","translated":"オペレーターのメモはまだありません。","updated_at":"2026-06-16T14:14:13.256Z"} {"cache_key":"89db2213a71276a41d20aaee60f4d351887fc997aeee2a7a3b10895aa424267c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"ja-JP","translated":"依存関係","updated_at":"2026-06-16T14:14:13.256Z"} @@ -82,16 +76,17 @@ {"cache_key":"8ccfbd67b038beb504411a4aad18dabf77d1c9aef64db7f85528256739135dc2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"ja-JP","translated":"Skills: {skills}","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"8dfc5efe84dea73600d09ebc9284881470867cc6bf5a9ab9846ee4614af3f69f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"ja-JP","translated":"ワークスペース: {workspace}","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"8f13222e40ef70c72d03d866d5802e71b1b9901160007d84669961432e0d2681","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyMissing","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{parent} (missing)","text_hash":"8daa419059727391c01e3b7021e05d8d70b4da67f9c57cd2d80f302af77aac53","tgt_lang":"ja-JP","translated":"{parent}(欠落)","updated_at":"2026-06-16T14:14:13.256Z"} +{"cache_key":"8f476d843eea30212bf7c4716708859102026217f18bd5a8d69dfdb8e6c6db8f","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"ja-JP","translated":"ピン留め済み","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"8fb588b6b5e3c2da65abd9016c94517733e86b4a3286dfed1b05bc71a89637f3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"ja-JP","translated":"最終回答の後にコメントを残す","updated_at":"2026-07-01T01:06:53.045Z"} {"cache_key":"9182bd9f8c396e9816aeac9e137bd8bae0d451e4ec8fb9139032491c4e5d5354","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"ja-JP","translated":"古い","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"93309fb097ac6115ffa01bb8a34c155ed43cae5a9c78fc137d45bb1d1bd13c0a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"ja-JP","translated":"修正リクエストを提案のワークショップセッションではなく現在のチャットセッションに送信します。","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"93aa12fedaab987305f07af92bc0269591826006609125f17a2dc90afd7e6db8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"ja-JP","translated":"決定事項、ブロッカー、証跡メモを追加...","updated_at":"2026-06-16T14:14:13.256Z"} {"cache_key":"93d1bb3b893f58356b77e6e1e48dc8266ca4f80688ff35387e8cb9e8afbd6cc6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"ja-JP","translated":"セッションワークスペースを読み込み中…","updated_at":"2026-06-16T14:14:13.256Z"} +{"cache_key":"99c131312d7962e53e2437a2971f7af3f38c50410a29390d5bdc716f8cb6868f","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"ja-JP","translated":"アーカイブ済みのセッションのみを表示します。","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"9ae707474f244e728e4281b7cdc5c1f9503119f35a249e92700aa1a2abe7f7a3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"ja-JP","translated":"ゆったりしたカード密度","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"9cad2aac24d0737b1e93049f26ca186862d0ea7710c075ee2686887d71ab4aca","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"ja-JP","translated":"プロジェクトファイル","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"a045b2a8c9e64dc79dd0eef72ee8c5d9584bfc42b2b8444065eff292c72f8088","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"ja-JP","translated":"{count}時間","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"a324ad832f40f22762e3aa387046b591defc3d268fcc576594a46f165db1c4f7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"ja-JP","translated":"自動化","updated_at":"2026-06-16T14:14:04.101Z"} -{"cache_key":"a5c78fc7f3b23690780fe0df725417d935d43bdaa59da63b90700c330310a082","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"ja-JP","translated":"レビュー","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"a62ea0e62568cfec7746e0fc33d268b91ace89b71b083bdc3e35b13504586b39","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"ja-JP","translated":"更新に失敗しました","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"ac3b2e583623039a3990a982b7bf0d5a7afc0c7e44521518d466e6e530cde28f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"ja-JP","translated":"ブロック中","updated_at":"2026-06-17T14:14:09.255Z"} {"cache_key":"ad587aac57ecf89dc3440a75192ad536e55e91c91313dfde119fda5624a79f24","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"ja-JP","translated":"MCP","updated_at":"2026-05-31T05:36:37.677Z"} @@ -108,12 +103,11 @@ {"cache_key":"bde797b9366e7259834f728e4dfcdafaeb7d01e15c6ef50361669dbbc6aaccb8","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"ja-JP","translated":"{count} 件のワーカーログ","updated_at":"2026-05-30T15:38:16.645Z"} {"cache_key":"bf2a6c4d75130dc7532c712c94fa8603b7e523473c4528e8a973e010b70536bf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"ja-JP","translated":"成果物","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"bf72f9352c07342b018ca3b72dcb4acef0cae4a4be184b2a0f931446314ed0f4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"ja-JP","translated":"オペレーターのメモ","updated_at":"2026-06-16T14:14:04.101Z"} +{"cache_key":"c15909db12158eb4c5351169855bedcc938dba7d38ce130f5e8a87295e6b0dd3","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"ja-JP","translated":"メッセージを送信するには、このセッションを復元してください。","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"c3e71f222e93a637050638c6b26a43958c8dc80b50591b93151bdf9710e66881","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"ja-JP","translated":"ワークスペースファイルの操作","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"c45641c791a5e2d0765939afcfafe948f153e792c35cb2cbe4501113240c93f7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"ja-JP","translated":"{agent}(未設定)","updated_at":"2026-06-17T14:14:04.173Z"} -{"cache_key":"c49225285128521a43e4d766898c49ffa86a16a7c6e700328f80b3191d56b98e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"ja-JP","translated":"新しいコード","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"c531f0f5c42b8a7f6e9d8713361ccd668cea4cd855c16a02fa6f35545966d58c","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"ja-JP","translated":"ツールエラー","updated_at":"2026-05-31T06:43:53.079Z"} {"cache_key":"c581463816f2d81e6aae828ab02d202ff5ef9216a378ee373ec820e1033ed410","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"ja-JP","translated":"ワークスペース","updated_at":"2026-06-16T14:14:13.256Z"} -{"cache_key":"cb35f1345843a7069fbe5a219c47b7ccc0b5f93747c00328b9926b2b061b1cf6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"ja-JP","translated":"セットアップコードを作成するには管理者アクセスが必要です。","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"cce97507d9460ccf77f8f5aec55537a8eb97e8176cab912f98ed694cf142ec9d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"ja-JP","translated":"オフ","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"cd0d813d43d7b61d13e0ccf95ad3a2ae3b80f3f80964596324d5e5b86fac360e","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"ja-JP","translated":"{count} 件の添付ファイル","updated_at":"2026-05-30T15:38:16.645Z"} {"cache_key":"d0b56050176fa6d32b3e216097f41856a87dd23aedce798f0ece4013f3dfb26e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"ja-JP","translated":"ファイルを検索","updated_at":"2026-06-16T14:14:19.257Z"} @@ -123,17 +117,17 @@ {"cache_key":"dd71654873ab8832fd1538ad11fd43f16fc3e4df5fc5ee543387cdc39cf772f9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"ja-JP","translated":"コマンド","updated_at":"2026-06-16T14:14:21.377Z"} {"cache_key":"dd93eff4f74e0e927d70d2fb1842a7df7420785ed1a66ca1c55a61c462eeaeb3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"ja-JP","translated":"読み取り","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"de101c9ecc8d876cd19cf8446c8c5b0b8b82a30c39b2967cc5b310abbee3f2ea","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"ja-JP","translated":"実行中","updated_at":"2026-06-17T14:14:09.255Z"} +{"cache_key":"df1d1fa4c746a4060c48f6be7015279ad79fb30f46953038521cf39f82bc8868","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"ja-JP","translated":"セッションをアーカイブ","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"e555cff9d58775fd59213d3b007940d366b501c4e4e25e3fadcb60710466d027","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"ja-JP","translated":"セッションワークスペースを更新","updated_at":"2026-06-16T14:14:13.256Z"} {"cache_key":"e7a4c4b64c803f4f6be208599748d8e9e438b741fd85301436b596fd58bba136","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"ja-JP","translated":"一致するファイルはありません。","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"eba3b0f8128b91b14850c25d626a6997c16d0ebee07b899c49d6cf898d8f1f5f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"ja-JP","translated":"最近完了","updated_at":"2026-06-17T14:14:04.173Z"} -{"cache_key":"ec7fcff0507991f5121faae587c88c7d254f37975a26cdc6b3249bd46b8b59da","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"ja-JP","translated":"セットアップコードをコピー","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"f019f3d37d3af8aa1e960ce6f523acbb0ad17fb78644a8920f23b77e959c053a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"ja-JP","translated":"診断","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"f26f913eb167dc4d8b05c79a72a1d81f156ac1c2ad7e293cb853d8d128ae9cbe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"ja-JP","translated":"セッションワークスペースを展開","updated_at":"2026-06-16T14:14:13.256Z"} {"cache_key":"f3a84aa7b52e71f51872587f6089462d8f09d662e0a0f1ca62d3925969f3de90","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"ja-JP","translated":"{count} 件表示","updated_at":"2026-06-16T14:14:19.257Z"} {"cache_key":"f6db4db51f902e337c87e46c35596d920b934fce9fa224609e75759cb795eec0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"ja-JP","translated":"実行","updated_at":"2026-06-16T14:14:04.101Z"} {"cache_key":"fb7b9b3429c6927f1923ab946a58fbf03bfe48ca1b3c8fe48fc49dbc3409471a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"ja-JP","translated":"60秒","updated_at":"2026-06-17T14:14:09.255Z"} +{"cache_key":"fc09795792a7bc44e2ca2ce53e42721e663b6ea3d255359b6e1572d0763be16d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"ja-JP","translated":"セッション名を変更","updated_at":"2026-07-02T14:30:08.823Z"} {"cache_key":"fc7a478180d16315c014db8b4e0a659c02acfdb00403e7c1098b937f7a924acc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"ja-JP","translated":"コンパクトなカード密度","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"fcada3cf1f76407188ce20d99fc0bd332083f0417f28089ea067d09b383e62af","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"ja-JP","translated":"{agent}(デフォルト)","updated_at":"2026-06-17T14:14:04.173Z"} {"cache_key":"fd765beb94ce21d79c7702c77c14674126518b3200c166dfc2457430cc3bcd2e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"ja-JP","translated":"このセッションではまだ操作されたファイルがありません","updated_at":"2026-06-16T14:14:13.256Z"} -{"cache_key":"fdaa4aabaa4055a57b4af7074232997cd0bc68ce7ce52d452246bd9eca03a531","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"ja-JP","translated":"OpenClaw mobile","updated_at":"2026-07-04T16:45:54.273Z"} {"cache_key":"ffa34fb2a6fad47e48fccd120c135bd3abdfd4eb05f42b5da641a3f420eb4d7b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"ja-JP","translated":"{count} 件変更","updated_at":"2026-06-16T14:14:19.257Z"} diff --git a/ui/src/i18n/.i18n/ko.meta.json b/ui/src/i18n/.i18n/ko.meta.json index 109104586c08..4e1476c59d86 100644 --- a/ui/src/i18n/.i18n/ko.meta.json +++ b/ui/src/i18n/.i18n/ko.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:22:00.185Z", + "generatedAt": "2026-07-04T18:20:56.408Z", "locale": "ko", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/ko.tm.jsonl b/ui/src/i18n/.i18n/ko.tm.jsonl index e650f84f0aa3..ecff1f5fcec5 100644 --- a/ui/src/i18n/.i18n/ko.tm.jsonl +++ b/ui/src/i18n/.i18n/ko.tm.jsonl @@ -1,29 +1,29 @@ {"cache_key":"01b71c27bed60aa5a42e4fb51a0679095d9ed707326f010f2160780ceaff2670","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.cwd","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"CWD","text_hash":"0217f1cb7725737f15a6710df3bcfa3bc10a239f0f7801ec3d7168e675f5ebd6","tgt_lang":"ko","translated":"CWD","updated_at":"2026-06-16T14:14:37.028Z"} +{"cache_key":"040d5b73b1cb07c76d910dcde7fb3a60ff2086a799c51f69fd5d1b7e5793cc25","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"ko","translated":"세션 이름 변경","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"048f0efd597ce6b603a0a66cca52e07578f5b16ea8625087c2d0e331c6117578","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"ko","translated":"디스패처 넛지","updated_at":"2026-05-30T15:38:20.918Z"} {"cache_key":"07953a597f6300b1920472c01f242c65562c0067e7791c7be9cf11cd1cea4863","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"ko","translated":"5초","updated_at":"2026-06-17T14:14:20.553Z"} +{"cache_key":"0849d78831fd5dfbca1ac7147b15acefe44eed960bd56220a88c7cd736ce0c00","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"ko","translated":"보관된 세션만 표시합니다.","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"0a5161b3294e3010726b2862d4814bd3f5452b991dbe04395c9224fef6f4ce69","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"ko","translated":"MCP 서버, 인증, 도구 및 진단.","updated_at":"2026-05-31T05:36:39.349Z"} {"cache_key":"0ab27f4cb55751880aa7b1a7ab223a89e23206e14edb81f0f69132322c2476ba","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"ko","translated":"세션 작업 공간 새로고침","updated_at":"2026-06-16T14:14:28.718Z"} {"cache_key":"0ae83dbb2fe77bce89777cfbd10e3959cb08410884cc88473d2034710bcdbe1b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"ko","translated":"워크보드 보기","updated_at":"2026-06-17T14:14:15.283Z"} -{"cache_key":"0db0b0ceab31b6a55db61f7d99eee18aca77e4223148509436e952dee4cbbc82","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"ko","translated":"설정 코드 표시","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"0dbd78719eb9b7046bcbc74c322f7e6a36a22a0fb5eb0ad393df590753446ba5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"ko","translated":"여유로운 카드 밀도","updated_at":"2026-06-17T14:14:15.283Z"} -{"cache_key":"0f5698f64a28acb81516fbdca1ff90269bd3843d392b00df9ab274264e9d5821","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"ko","translated":"공식 OpenClaw 모바일 앱은 스캔 후 자동으로 연결됩니다.","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"0f98dcf0c762240a2f91e2c7c0d88c0c0d473851caed77941b1c4678b0d9df62","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"ko","translated":"만료됨","updated_at":"2026-07-01T10:32:01.566Z"} {"cache_key":"13966fa4ec838f4dd51da1c9b8bc0d422ffed54126c8330bd863a1ccffb673f3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"ko","translated":"처음 일치하는 파일을 표시합니다. 검색을 구체화하여 결과 범위를 좁히세요.","updated_at":"2026-06-16T14:14:34.760Z"} -{"cache_key":"13e2e88236570b818cbd549f5bd6f76fbe4a899d07e7a7c000d02b25b47b4603","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"ko","translated":"검토","updated_at":"2026-06-17T14:14:15.283Z"} {"cache_key":"14c4e7ec0505609f54af1c7d7e8d617094e1c4ecd82ee7f7f299ead3cf5a70d9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"ko","translated":"{agent} (기본값)","updated_at":"2026-06-17T14:14:15.283Z"} {"cache_key":"1514da5908fa67b69b781c34cb35975d70e90b4e6697f58b6253436e3ba0b97b","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"ko","translated":"첨부 파일 {count}개","updated_at":"2026-05-30T15:38:20.918Z"} -{"cache_key":"173ba78d162069a178e55a16e99a01a184ea04d63414032bd44c2b7a8de13f93","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"ko","translated":"모바일 기기 페어링","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"17965e94984aa8797fd23fa39e0ba34faee052b6588d1e82e318a087cf28f606","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"ko","translated":"아티팩트","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"1a2a0d2d28a1ee8782baa9950932c85a54cd09fbe21d95273abd149625604594","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"ko","translated":"없음","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"1b8241e69ecbb38c83a1d9386f5019193e71dea011ed65c6fad5c834dd38d147","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"ko","translated":"프로토콜 위반","updated_at":"2026-05-30T15:38:20.918Z"} +{"cache_key":"1d58e25d5d921bab46e72a8674418e0fe5030cbe103604f645948917a63f9daf","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"ko","translated":"메시지를 보내려면 이 세션을 복원하세요.","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"1dd73abd726e45213b6e637b2d56b1acbf49232cd97d04e06a27cbd556e512a5","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"ko","translated":"Русский (Russian)","updated_at":"2026-06-26T21:43:29.284Z"} {"cache_key":"1f8427a3216fef1fc3e482fb33ea952dead5c388a59273911484170ec390379e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"ko","translated":"검색 결과","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"209c98959f562e30a6e25a87c5631c6a5d0eb4469f11f21cade537809406c5cb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"ko","translated":"알 수 없음","updated_at":"2026-06-16T14:14:28.718Z"} -{"cache_key":"241a192f2ec8f2b479bd39b438f0e1eb3266cc84aa3419bf33130a13ca53e508","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"ko","translated":"설정 코드를 생성하려면 관리자 권한이 필요합니다.","updated_at":"2026-07-04T16:45:42.475Z"} +{"cache_key":"235d296175b4e7017813de465c22ed458957c8763f76e3b73a73deb34d5e5231","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"ko","translated":"세션 고정","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"260bf5722c36b88c2abeeb1aa858e4008aaebd131572e8f71f1f8e7605eb13eb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewStale","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Stale","text_hash":"40c9e59c5e152b0ae9affc84d8461c29b75a7709e4506307eeedf246b526014e","tgt_lang":"ko","translated":"오래됨","updated_at":"2026-06-17T14:14:15.283Z"} {"cache_key":"287c4a628650a78241412eeb369060aa4f8513ef2ae39a5a7ee2ee4e69e0f824","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"ko","translated":"준비됨 미할당","updated_at":"2026-06-17T14:14:20.553Z"} {"cache_key":"29d619da3a3117c2414ee474c54a0d92a6a6364d6c95a68294adf430cad97d88","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"ko","translated":"명령","updated_at":"2026-06-16T14:14:37.028Z"} {"cache_key":"2a6c598886904a42fb0c9e2d720ed550c2d722aead6fca26f54732013f9c12b6","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"ko","translated":"작업자 로그 {count}개","updated_at":"2026-05-30T15:38:20.918Z"} +{"cache_key":"2bcc1d9376048a3653ad27b6cfffecc3fc9cc7b3b9d10fe2f19076d36552c23e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"ko","translated":"세션 고정 해제","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"2cf44483a3d8f51f3bf2cb871f88962df848eaebf9fce3f5a0d0c23cb97f633c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"ko","translated":"변경됨","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"2d2d2e19db269caeca65f2fb38d9b3bad6470cb78bc28ad8f3044ea1e02dc119","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"ko","translated":"Gateway 작업","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"2d50f5d9b44d9abf6dc925e4af0d1212acb9f84a5052fb6e2b80978c1d47183e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"ko","translated":"{count}개 차단됨","updated_at":"2026-06-16T14:14:28.718Z"} @@ -58,7 +58,6 @@ {"cache_key":"596ba1f4740ff211af7f1f65132e1cca55b99e45cf46c3a355f0324a0d80ca7d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"ko","translated":"Skills: {skills}","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"5c921f6cd7c85c8c7f5dc097a38e884e8c18926d0c1334ebbf6c26c21cf40601","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"ko","translated":"{count}개의 종속성이 완료되었습니다.","updated_at":"2026-06-16T14:14:28.718Z"} {"cache_key":"5fae57cc0a889debcaa6162bd3a15d410e5085db0931c2c9adcd2709633a0bc0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"ko","translated":"세션 작업 공간","updated_at":"2026-06-16T14:14:28.718Z"} -{"cache_key":"60199d34fb8968b3211cdfd68d368360d10bfa0031c37d0da7f4d1237675daea","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"ko","translated":"페어링 도움말","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"61ec0bc0cfab27d4f52c9cfd23752496a4d90f6d870ae3c708468f33e5c8c71c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"ko","translated":"세션 작업 공간 로드 중…","updated_at":"2026-06-16T14:14:28.718Z"} {"cache_key":"654a4fbf6a33ed0c988ae0f0a7d856cdcd12ad4786c27305aefb0f802a4783ba","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"ko","translated":"작업 공간","updated_at":"2026-06-16T14:14:28.718Z"} {"cache_key":"65b436c3b4f2e5a995cd090ae06d13d3fb6c1e3ee34532e5953390662c2f9560","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"ko","translated":"할당되지 않음 ({agent} 사용)","updated_at":"2026-06-17T14:14:15.282Z"} @@ -66,28 +65,24 @@ {"cache_key":"6661a8580f033e530995fca7fa9a21141d4c43f499c3bac4b5bcde8e4f085a50","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"ko","translated":"목표 메모","updated_at":"2026-05-29T21:00:36.520Z"} {"cache_key":"696d861bfac8358a9de4a213c457761ac32ab99f42e6f44dd1b2f40f6390174a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"ko","translated":"세부 정보 보기","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"6c2c490ea3016befc0d5e801018f9149b0fe3b7e32e4016b60c0d3b6d3858b94","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"ko","translated":"새로 고침 실패","updated_at":"2026-06-17T14:14:20.553Z"} -{"cache_key":"6ca54e7ed774857e56a65d8dfb0771ddac450f8cd8e413eb404261a614b1de28","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"ko","translated":"OpenClaw 모바일","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"7065a3f0b57bf8ba195f5ab85a674eda290d06693bd5ab913add0752b8574795","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"ko","translated":"워크스페이스 경로","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"70c73aa2eb02fb0dce3c3e15cc57e7f4bab18baddbf5f2ea8423028aa5d2a115","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"ko","translated":"도구 오류","updated_at":"2026-05-31T06:43:54.853Z"} {"cache_key":"73504bfee80142c1038762a536714e38c2278a217cebdd5933837bb13d689126","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"ko","translated":"테넌트: {tenant}","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"76ac3074a0c4f3bfad8af10027638e4c8b1f41c75736588a26a48e8009a77a69","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"ko","translated":"{count}개 아티팩트","updated_at":"2026-06-16T14:14:34.761Z"} +{"cache_key":"7a5bb77efc011117a68833483bf8ab7f8e6e0989859803ffbd27d6fdf5b171a5","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"ko","translated":"고정됨","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"7c0c95addbc7d588230dceb07e672e22eb9f511022bbc3ea63e3105d92d056a4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"ko","translated":"파일 검색","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"7d0f49c18bbfb7dd9ffa76d04aa375467bb4a2f588ef5702e81eefdf825c7654","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"ko","translated":"끔","updated_at":"2026-06-17T14:14:15.283Z"} -{"cache_key":"7e19a29d9d8abc4ddb106fdc61223933fd99c1d3d2bf89eeb6772e4759f283af","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"ko","translated":"기기 관리","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"7e86df8b367e10c959319164715314b4919bd4144ced20872e22684892fb0fa5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"ko","translated":"Talk 오류 닫기","updated_at":"2026-06-16T14:14:28.718Z"} -{"cache_key":"7eee839d8af0dcbc91fa2f41c1bc750382b5cc19a36f5210dd3726224e753353","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"ko","translated":"보안 설정 코드를 생성하는 중…","updated_at":"2026-07-04T16:45:42.475Z"} +{"cache_key":"80b3ce367044cbfc4bfdcc00bb18319113c8fa68898414500840532fa95c3beb","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"ko","translated":"세션 복원","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"81034ed34163385370c34f4197f6caa9f2ba43b50ca9a8531b57f9150a5a7afe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"ko","translated":"최근 완료됨","updated_at":"2026-06-17T14:14:15.283Z"} {"cache_key":"81596ae54cb1ae27654b725d32f0a6611c64fbd4f392ee00c16dcfb350bb0956","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"ko","translated":"운영자 메모","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"82fedf5fb8b0bf01b3bfea578ab491695a11299b50858131164cfb6c3d7bfffb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"ko","translated":"검토","updated_at":"2026-06-17T14:14:15.283Z"} {"cache_key":"86a46448b64065bf73ea2043d995e6de4bd8f0df2b2eeb01e92c611840c3599f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"ko","translated":"{count}시간","updated_at":"2026-06-17T14:14:20.553Z"} -{"cache_key":"892b797378908ed6dbdcb3abc566662828d1d20dd55a37a94c9333d112f831e6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"ko","translated":"새 휴대폰을 연결하려면 모바일 앱에서 이 QR 코드를 스캔하세요.","updated_at":"2026-07-04T16:45:42.475Z"} -{"cache_key":"8b31b73b7a737bffddc5b65a410fd2383c6d59ff3f61863e93167dad47c24300","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"ko","translated":"QR을 사용할 수 없습니다. 대신 설정 코드를 복사하세요.","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"9290ac83e3ae6b6883d885401124314445aed79569372502b0709eca77fe154c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"ko","translated":"실패한 시도","updated_at":"2026-06-17T14:14:20.553Z"} {"cache_key":"96548f5481114d8529b125e0ac5f199e247a3666d202ffae60212ee7cd490eb6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"ko","translated":"워커 로그","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"9987b8273f7aeb4082b39d15139cb16c0719a8908419e9c51a8e50db237cc252","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"ko","translated":"수정 요청에 현재 채팅 사용","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"9a1d9909fe9457fb1acb69d7103e817a94b55a55014364e75225bf4298bae5a9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"ko","translated":"보기, 검색, 우선순위, 에이전트 또는 보관 필터를 변경하세요.","updated_at":"2026-06-17T14:14:20.553Z"} {"cache_key":"9e37919f47b0fd7686333724b9c6752e01084d7100d3780f7e55ba21b0116491","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"ko","translated":"일치하는 파일이 없습니다.","updated_at":"2026-06-16T14:14:34.760Z"} -{"cache_key":"9f9f7daedba5d86846ec4b456856990fdde280ed0fef210cb59bd0749f303139","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"ko","translated":"설정 코드 복사","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"a0e85694611b2e71b5e485ec71391891f31db8c275cba5b70774f4c0bfddb95f","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"ko","translated":"हिन्दी (Hindi)","updated_at":"2026-06-26T21:43:29.284Z"} {"cache_key":"a4b7d06a64a666e57f017a86d2fa9d4ef14255e292091dbaac474ed319a495bd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"ko","translated":"증빙 누락","updated_at":"2026-06-17T14:14:20.553Z"} {"cache_key":"a54853ef1243668b494e73b30d7c64728ac51e7b90681b55b366d7c93a0c3a76","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"ko","translated":"자동 새로 고침","updated_at":"2026-06-17T14:14:15.283Z"} @@ -95,14 +90,14 @@ {"cache_key":"aa28671e2bb6abaca352d105e6ab91bff5c1eeb57fd2cf99bcebe5021123a6b2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"ko","translated":"루트","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"ab318cfb91fd4c14318b6ebf1d29b6500a59d22ccc594e002f498cdd3be552b4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"ko","translated":"{count}개 표시됨","updated_at":"2026-06-16T14:14:34.761Z"} {"cache_key":"ac121a4495218e30180ef339886397d2bc69f965222b318979abccf53ae81874","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"ko","translated":"30초","updated_at":"2026-06-17T14:14:20.553Z"} -{"cache_key":"ae70c606b5a5279015d2bb415f4992279cb225b527f0ca53ba6fadf0405b6a92","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"ko","translated":"새 코드","updated_at":"2026-07-04T16:45:42.475Z"} +{"cache_key":"b1b15b7fed5263212b845aa4f905b10da2099099e706b297dc5f3c0bbaab89eb","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"ko","translated":"보관됨만","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"b211930758638c2f1173aeec5b620aea6203c1133715fbe541a7a9098ed606b8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"ko","translated":"카드 세부 정보","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"b2f5ef4524fa415483819b2d5f09c173a6fe2b0cd95a13f2e88fa50a07680eff","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"ko","translated":"명시적 에이전트가 없는 카드입니다.","updated_at":"2026-06-17T14:14:15.283Z"} +{"cache_key":"b450830c2c5a4d87eb8ced9f5dcf4ae0168b50b76f50bb9a7bd0370173285b1b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"ko","translated":"세션 이름 변경","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"b46856c9840a7e12df19fb04b069d74d5de94ead5f628dc1c9c52d8406a6a4b9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"ko","translated":"진단","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"b633626dd143f9763437f2d5f12db8732716e26b4fb0b8fe729e976fadd243d1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"ko","translated":"{count}개 읽음","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"b7de553c047296a506c91b7007a8392a3f3d99a096364957427891a346b71e96","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"ko","translated":"차단됨","updated_at":"2026-06-17T14:14:20.553Z"} {"cache_key":"be3725795ab816e36c9bf9dd095f4d510db041248acf8379a5ba98c60369b740","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"ko","translated":"60초","updated_at":"2026-06-17T14:14:20.553Z"} -{"cache_key":"c0f13adcbe1a120d3a9ca9e9ce8cacd22d2207ba4956851515eb833e9c60b4a7","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"ko","translated":"설정 코드를 생성할 수 없습니다.","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"c19bad0b9f16c6552151aa5f83fb0cbc581bf08f673824165d2090effb2fc6b4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"ko","translated":"요약: {summary}","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"c286d99f1f55e622098ff775ef08dcd811fa5011a05d2940eddbad41097758ac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"ko","translated":"증빙","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"c356ae536d83fdcd2fecf63b9a6704a618f294629b1871bd86ebc5d741b2a574","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"ko","translated":"세션 작업 공간 접기","updated_at":"2026-06-16T14:14:28.718Z"} @@ -111,6 +106,7 @@ {"cache_key":"c5e5431af96401ee983e9faa6f1877604bc6260499d14a0a90d30f7fdba7ae25","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"ko","translated":"작업자 {state}","updated_at":"2026-05-30T15:38:20.918Z"} {"cache_key":"cd0480cc4f2f762f7685991753d38b978727ef9349b15b5bd6bba92486f33f01","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"ko","translated":"워크스페이스: {workspace}","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"cd7a1ade73d2c2d70ba4349fec823e5d4662ef4ee810116728030f6dce21b497","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"ko","translated":"프로젝트 파일","updated_at":"2026-06-16T14:14:34.760Z"} +{"cache_key":"ceb57d76cdf38aa06b2ec69cbb5538dff13e70b7a8ef18285f58aa37c5f6395d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"ko","translated":"세션 보관","updated_at":"2026-07-02T14:30:10.945Z"} {"cache_key":"d0c5302df1a1a6fffe501baa8028563f89d2a1b468437d0aafc8bd45187fdc79","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"ko","translated":"{count}개 준비됨","updated_at":"2026-06-16T14:14:28.718Z"} {"cache_key":"d3a73d59f3e66732afa141c2c424d4d10f5ad14f31294ddb27a71b9de2d714fc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"ko","translated":"읽음","updated_at":"2026-06-16T14:14:34.760Z"} {"cache_key":"d91dc8464fb4262c365012b6f6ec17aed24ad81353ce925f6e4d0df1fb12d7dc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"ko","translated":"15초","updated_at":"2026-06-17T14:14:20.553Z"} @@ -125,7 +121,6 @@ {"cache_key":"e79300da7415b9125237edc0370bdd624813f16992a1519d3b0006aca737adea","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"ko","translated":"종속성 대기 중: {parents}.","updated_at":"2026-06-16T14:14:28.718Z"} {"cache_key":"e7c11ccf9f90b61836b988450545ec1b989a94063b51174e1c03b603e4cf3637","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"ko","translated":"업데이트됨 {time}","updated_at":"2026-06-17T14:14:20.553Z"} {"cache_key":"e8081578e7e6a0677dc97db787a50cc648eb66b509828be1acd8929fc1064dda","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"ko","translated":"결정, 차단 요소 또는 증빙 메모를 추가하세요...","updated_at":"2026-06-16T14:14:28.718Z"} -{"cache_key":"e96d66644482d2cb6d09e674701b7e3b2ddd10dcd276932c01e30ecb9758d059","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"ko","translated":"검토 대기 중인 기기 요청: {count}","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"eb949e3f2e0fa63843317a46e9048b29ae6830d1792fb79d6700ef5ee442759b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"ko","translated":"이 세션에서 아직 변경된 파일이 없습니다","updated_at":"2026-06-16T14:14:28.718Z"} {"cache_key":"ecc08fac0a27a0d1b3388d315eafedd83ba94b4634a205747019a9bf9893d2f6","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"ko","translated":"제안이 실제 Skills가 되기 전에 검토하고, 다듬고, 적용하세요.","updated_at":"2026-05-31T21:48:23.318Z"} {"cache_key":"ece1372bff4919019c00f1b63b01c63c11789645bf28c3d334319d82c6980e08","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"ko","translated":"{count}개 변경됨","updated_at":"2026-06-16T14:14:34.760Z"} @@ -135,5 +130,4 @@ {"cache_key":"f338cb1e5caff25b68f7411a9df74ff2b2047b1f5417f5f287aef8d1481ae456","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"ko","translated":"이 보기와 일치하는 카드가 없습니다","updated_at":"2026-06-17T14:14:20.553Z"} {"cache_key":"fa66a6052376197305c4f49ae4cbd10229bbe2376fe80c30a16e0335a1823722","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"ko","translated":"현재 채팅 사용","updated_at":"2026-06-16T14:14:21.259Z"} {"cache_key":"fabfa09f8370e02212d114ad149c6b6a57221b3752e6b220799886492d0142f4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"ko","translated":"세션","updated_at":"2026-06-16T14:14:34.760Z"} -{"cache_key":"fc9a8b4ef78047eb6a9214f49172fc9553dc2ce421ca1438ba5ba4a5841d90a2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"ko","translated":"OpenClaw 모바일 페어링 QR 코드","updated_at":"2026-07-04T16:45:42.475Z"} {"cache_key":"fcc8fa9801212b2849e713afa3bad7f0be90434a62a685381eb2d3cb9b70e2dd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"ko","translated":"자동화","updated_at":"2026-06-16T14:14:21.259Z"} diff --git a/ui/src/i18n/.i18n/nl.meta.json b/ui/src/i18n/.i18n/nl.meta.json index 32a2132317d9..054e81734476 100644 --- a/ui/src/i18n/.i18n/nl.meta.json +++ b/ui/src/i18n/.i18n/nl.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:24:22.529Z", + "generatedAt": "2026-07-04T18:20:58.888Z", "locale": "nl", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/nl.tm.jsonl b/ui/src/i18n/.i18n/nl.tm.jsonl index 391d14f5820c..d49b5a46bca0 100644 --- a/ui/src/i18n/.i18n/nl.tm.jsonl +++ b/ui/src/i18n/.i18n/nl.tm.jsonl @@ -10,19 +10,17 @@ {"cache_key":"17f367198a1420756107234b6e1cfd6a46ba6c9636d7f9a34ace0568f50bafeb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"nl","translated":"Werkruimte","updated_at":"2026-06-16T14:18:17.492Z"} {"cache_key":"19e2791d9f9649d4ca944f2858a4bd3efdc8c0ca6ed4481aed37bea60e8e24df","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"nl","translated":"{count} geblokkeerd","updated_at":"2026-06-16T14:18:17.492Z"} {"cache_key":"1aa8274ab05f073b830fa57e0addbbb5d72c424fd554bea0d49b12fcc0ec61aa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"nl","translated":"Ontbreekt","updated_at":"2026-06-16T14:18:24.009Z"} -{"cache_key":"1c726a4f54d115b77e6985f03530755cc21d99d9439bca58f03d25b8a38b40e3","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"nl","translated":"Kan geen installatiecode maken.","updated_at":"2026-07-04T16:50:04.358Z"} -{"cache_key":"1f5684f4296192bd8ef00d8d06b70494d7fce3107df682443b9b4eeb2b25879d","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"nl","translated":"Installatiecode kopiëren","updated_at":"2026-07-04T16:50:04.358Z"} -{"cache_key":"22357d4f51b2c0e537440861a8b4b164d771ca382b8ad0d4bf3107dca2d40daf","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"nl","translated":"Installatiecode weergeven","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"231ccce169faa26a414b57965244968fa63b768da99151dc8fe4c8d7f0583110","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"nl","translated":"Uit","updated_at":"2026-06-17T14:17:30.702Z"} -{"cache_key":"265fd602b75300f3efa895e12acccf2b41bb2580c333a2b07d62ef9c8028c286","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"nl","translated":"Apparaatverzoeken die wachten op beoordeling: {count}","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"273923c964acae871f4e49ef308f640a89c37712165725e85a6b6408d8c4c497","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"nl","translated":"Bekijk, verfijn en pas voorstellen toe voordat ze live Skills worden.","updated_at":"2026-05-31T21:48:43.501Z"} {"cache_key":"27a72ee3c9b0a6c2831ce0cd0854d01fdb74b51c907bf446845d7ae6f1c07b28","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"nl","translated":"Board: {board}","updated_at":"2026-06-16T14:18:09.152Z"} {"cache_key":"28ade9528aeb6b6f88ad54f36d8951ce2e67f1f53eaed15e56b235bcdb783e01","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"nl","translated":"{count} workerlogs","updated_at":"2026-05-30T15:38:50.257Z"} +{"cache_key":"2a690fd1141828516db2e958cb8157275d46b1588ce2ce186414cdc9f1897468","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"nl","translated":"Sessie hernoemen","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"2be26df94f517d8a47c7b006703bee0af0b80f664a71c00101cb5664c0cd91a5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"nl","translated":"Bijgewerkt: {time}","updated_at":"2026-06-16T14:18:09.152Z"} +{"cache_key":"2ccf12118a667c7829f8b0a23762c7d760e7f29678616b1219e26175827e1c75","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"nl","translated":"Toon alleen gearchiveerde sessies.","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"2d29a075dcf12f77c4147ed4219f7a5eb2eebca48df2ae44b8307c9f03a419cd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"nl","translated":"Bijgewerkt","updated_at":"2026-06-16T14:18:09.152Z"} +{"cache_key":"3190cf385bdc7d66c8d509b0b92f0dc9c46997c80901dc55264d1e338680c07b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"nl","translated":"Sessie archiveren","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"355247edf05e7a22822c9acd10b13e83e6ad765299b2014705fb4062adf331af","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"nl","translated":"30s","updated_at":"2026-06-17T14:17:36.035Z"} {"cache_key":"364641279de5e2180df2e1e5d38dd3d90b8594ac9a5cfaf23f09d51912688a74","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.copyPath","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"nl","translated":"Pad kopiëren","updated_at":"2026-06-16T14:18:26.226Z"} -{"cache_key":"3741c17ae8ba1096427e18a7bcb8386b195aaa9b6622ef832e3e0e2467de8dec","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"nl","translated":"Beoordeling","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"3a8f90934c613ad55413822bdb50730ee26508bd08db306bced987dbf47191d0","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"nl","translated":"Protocolschending","updated_at":"2026-05-30T15:38:50.257Z"} {"cache_key":"3b742bceb583fe4990be276d4796fba557994fc1795cf6f891aca83f1858529d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"nl","translated":"Comfortabele kaartdichtheid","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"3e312bebe9419efb1f31d12ad0a57c66665d0ce68f452458e82be9a69607aee9","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"nl","translated":"Dispatcher een zetje geven","updated_at":"2026-05-30T15:38:50.257Z"} @@ -45,15 +43,14 @@ {"cache_key":"58ad183e4bf807256f7724f44bea7d5110c99ab08809a4e0100800261f845838","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"nl","translated":"MCP","updated_at":"2026-05-31T05:36:57.350Z"} {"cache_key":"5aabc8f1bb457c23d3f11c53696e6eca1efcbb7c07f9752da95cebdfe467f8fe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"nl","translated":"Gateway-taak","updated_at":"2026-06-16T14:18:09.152Z"} {"cache_key":"61322d7dd6c6f6ecd12429935bddd35cf29c24771475df989fb28ab6a579b26b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"nl","translated":"Doelnotitie","updated_at":"2026-05-29T21:02:33.311Z"} +{"cache_key":"629e7dd809575ca02bdcf4617f3fccedf1f26b55586bff8d38133a32107803c7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"nl","translated":"Vastgezet","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"633050b5fcd1c0a148ea6fdaa6a5d9a3a51f271399240f5c56df3782351b7455","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"nl","translated":"Verlopen","updated_at":"2026-07-01T10:34:14.146Z"} {"cache_key":"64d590142d2cda2a3ace130d23a4d4e2cc9d8e2704cf02fbb91d12fcee45ce5b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"nl","translated":"Afhankelijkheden","updated_at":"2026-06-16T14:18:17.492Z"} {"cache_key":"65f80349e6597cb244832311510913ac3a9371262442bba5c6643e523d8573ec","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"nl","translated":"Operatornotities","updated_at":"2026-06-16T14:18:09.153Z"} {"cache_key":"689029d1ab9a757714310261412c6e5048e773050c7c4d3ddd0392504be639c3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"nl","translated":"{count} artefacten","updated_at":"2026-06-16T14:18:24.009Z"} {"cache_key":"68e52ea4d559f2b45ac155da5a3f054e76dff935d6c7811a562b1718b138383d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"nl","translated":"Bovenliggende map","updated_at":"2026-06-16T14:18:24.009Z"} {"cache_key":"6e4741d69b99e5ad3585d39bcb07f581cd2150079d2ce03af5c02ba0e824f4fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"nl","translated":"Zoekresultaten","updated_at":"2026-06-16T14:18:24.009Z"} -{"cache_key":"705b576dbc134bff092c6566b4d61be75e8985e3436dc5d719bc7d3ef6374051","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"nl","translated":"Hulp bij koppelen","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"7134a0e648f67fd07a3220c24dac1a91e75960a98f70275b0fba72dade9f78e2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"nl","translated":"Gereed","updated_at":"2026-06-17T14:17:30.702Z"} -{"cache_key":"7445cff610f7b4b7b8f5d008aeac8c91ff448fb2f2a00ea2db74bdd237948418","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"nl","translated":"Mobiel apparaat koppelen","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"747b8553f89a63a178d16656104fe0defa710052fa1c714c4d5285b8b3ed27b6","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"nl","translated":"worker {state}","updated_at":"2026-05-30T15:38:50.257Z"} {"cache_key":"7611e89145a7e55300dd1dcba8e1f56e4e8040ece6f82579a2d33363fab60cb9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"nl","translated":"ontbrekend bewijs","updated_at":"2026-06-17T14:17:36.035Z"} {"cache_key":"77412e5f1b5b396a7580ae4bc42975fe83463676719c0feb03766ca2a650224f","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"nl","translated":"{count} bijlagen","updated_at":"2026-05-30T15:38:50.257Z"} @@ -74,39 +71,38 @@ {"cache_key":"89ac2d3a996a971272db7938b905a569afc49b4d15e9c7d4d3921a81155c317c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"nl","translated":"Voorbeeld","updated_at":"2026-06-16T14:18:26.226Z"} {"cache_key":"8a5914deae3b6a3415029505fbae3e33ce16dd30e297fdecfdee8d8bb2f84013","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"nl","translated":"{agent} (niet geconfigureerd)","updated_at":"2026-06-17T14:17:30.701Z"} {"cache_key":"8abfc18ae4d560c9c368e9729f4f69c7b839f49d4e481648037ef3d041296984","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"nl","translated":"Vernieuwen mislukt","updated_at":"2026-06-17T14:17:36.035Z"} -{"cache_key":"8b70b5ddfe99be4332ed8c54b74843390cbeeab85165d6fa33a3c5586f430c1e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"nl","translated":"QR-code voor koppeling met OpenClaw mobiel","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"8e634d51ceaa70b148ec57668bbfc10ddf03c7676f6d4950eda24754ec430d07","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"nl","translated":"Bijgewerkt {time}","updated_at":"2026-06-17T14:17:36.035Z"} {"cache_key":"905f482878236d3cf9cf6f2eebce5f15ceb9c7ff8eae4ef0ff32a40755f994e8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"nl","translated":"Geen bestanden in deze map.","updated_at":"2026-06-16T14:18:24.009Z"} {"cache_key":"935a83f97dda5296a5e536cf81926cf36d26488286de3a09135f4e50a80c48ec","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewStale","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Stale","text_hash":"40c9e59c5e152b0ae9affc84d8461c29b75a7709e4506307eeedf246b526014e","tgt_lang":"nl","translated":"Verouderd","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"94c9fb7b1a6a8535c9de1e1091cd59b85f7bbc2f3a537589049651a7a70dcd8b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"nl","translated":"Recent voltooid","updated_at":"2026-06-17T14:17:30.702Z"} -{"cache_key":"95bb6f358bdfb1285335ee9df785dacd58a8f7e8dea4af659f6440cbf538daef","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"nl","translated":"Beheerderstoegang is vereist om installatiecodes te maken.","updated_at":"2026-07-04T16:50:04.358Z"} +{"cache_key":"96138da1d80964f55d26ed28f5f2f2fc85974a4e7b854589da17d160e6b29674","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"nl","translated":"Alleen gearchiveerd","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"9b94d044fbc9cc2e12b26ba5e1cec1ab0a5ba63d9e91135769dfb8e8d6af0531","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"nl","translated":"{count} kaarten","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"9d33a3dc2fbd6d067ae9073f31bb955f13a82b79dbef679d35eb7a60b36c5983","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"nl","translated":"Sessie","updated_at":"2026-06-16T14:18:24.009Z"} {"cache_key":"9fcac9cd9734a2f3637569101069d300348a9a940d50336a0c578eec7c262f2b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"nl","translated":"5s","updated_at":"2026-06-17T14:17:36.035Z"} +{"cache_key":"a0fdb9d195c8f6c31e4ea30f72fd8a57beda64a0cc2ea0cabc973c82a2a14e75","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"nl","translated":"Herstel deze sessie om berichten te verzenden.","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"a2f00687aea314d88d3f36ea3a01559d55da3328a8881c407914020fad526436","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"nl","translated":"Gewijzigd","updated_at":"2026-06-16T14:18:24.009Z"} {"cache_key":"a4e2c20feefcfecabbac97c766a0d8e469841737d603757efc5176841eb77a50","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"nl","translated":"{count} getoond","updated_at":"2026-06-16T14:18:24.009Z"} {"cache_key":"a79fe9d0dfd6822ded48b0aae70e53619f389e449455accf82038a46d5e907fd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"nl","translated":"Details bekijken","updated_at":"2026-06-16T14:18:09.152Z"} {"cache_key":"abd56df97701441cc8ca9ebd48631ee4d1d3e55067c4c7085a6423f0630c97e6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"nl","translated":"Hoofdmap","updated_at":"2026-06-16T14:18:24.009Z"} {"cache_key":"add2319ea76f9d99d29ff0f00fe0935799b8ab3cb4bfeea85f443147fbb1d4a8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"nl","translated":"Alle kaarten","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"afd8371e2bb1364f6d7f28f072af11344f9d15088f265fdf7e99a22b0549967b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"nl","translated":"heartbeat {age}","updated_at":"2026-06-17T14:17:36.035Z"} -{"cache_key":"b0cc3c128a6635dc7c224dd961c0b23a2e55d4e716a2a3726d129a733cc45e71","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"nl","translated":"Apparaten beheren","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"b2a24e6ba5288da3f1b64057ff0e6495f0da5c731413247eb98e072b9cfe1dca","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"nl","translated":"Standaardagent","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"b7991133da3b84a27cb46ac843f1f5fc5708d2930c0f4624d3684ce0f8af5d44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"nl","translated":"Sessiewerkruimte vernieuwen","updated_at":"2026-06-16T14:18:17.492Z"} {"cache_key":"b8d588e4b3ffb799a3668b45b9a1e8c5de2991f96f4d67e17e38ebcde0399935","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"nl","translated":"हिन्दी (Hindi)","updated_at":"2026-06-26T21:43:45.251Z"} {"cache_key":"bab23c08a2ab714aedcee07ef66855dc7e2cd9a54040d86ed567272edf3574ea","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"nl","translated":"Beoordeling","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"bba71a201d5878eb48d107f6dc995e20bd27941aaaf5d4c01ad0e3ad41a9dfd0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"nl","translated":"Sessiewerkruimte invouwen","updated_at":"2026-06-16T14:18:17.492Z"} {"cache_key":"bbc4ce34d65947b136b334bc6de173832f8df27f9a057f0a157883b44a9de3d4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"nl","translated":"{count} gelezen","updated_at":"2026-06-16T14:18:24.009Z"} +{"cache_key":"bc958fd40402013b5d5811be3290a4fe07db7bcc8198055e7cffa1a9ed1978ed","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"nl","translated":"Sessie hernoemen","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"bea2efe67c6d9fc7efe520d772b92edaf474a7d048e35d4e6d5e557151e5b0e2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"nl","translated":"Koppelings-QR verlopen","updated_at":"2026-07-01T10:34:14.146Z"} {"cache_key":"c210b3270e9de05dfe8ac280e9c022cb999e3c88276570f2fffb3b25fd3de70e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"nl","translated":"Bewijs ontbreekt","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"c5f5e4542ec64d4b1c6189d8d2f050afaee74595fc993ebb4d0ca13f0e854629","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"nl","translated":"Diagnostiek","updated_at":"2026-06-16T14:18:09.152Z"} {"cache_key":"c6009878738fc4b25708831ee4e8c2468efb3a4a0156770148e713d91ee0719f","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"nl","translated":"Skill Workshop","updated_at":"2026-05-31T21:48:43.501Z"} {"cache_key":"cb6c84443ca09ca66b9cc3bebc2538399e0634bbb3214579aa84fe99ee311828","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"nl","translated":"Samenvatting: {summary}","updated_at":"2026-06-16T14:18:09.152Z"} {"cache_key":"cb8038b94c017757920e8ad341143e8bc172fb9e33794542b1702bccc2245756","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"nl","translated":"Automatisch vernieuwen","updated_at":"2026-06-17T14:17:30.702Z"} +{"cache_key":"ccbb3082801b225c7dcfa386897983fe5030a274e3725112da01a906ed9936c3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"nl","translated":"Sessie vastzetten","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"ce7f256609a2bf160d44331f0c5277864a36f9c0f9ee4c7c64a50fd39e3b917d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"nl","translated":"Sessiewerkruimte","updated_at":"2026-06-16T14:18:17.492Z"} {"cache_key":"cef8e7ca7aa858ae93e78b6e22483364b1ba7a2d4562b98fef26981db9cfcf55","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"nl","translated":"Bijlage toegevoegd","updated_at":"2026-05-30T15:38:50.257Z"} {"cache_key":"cf13a84f99451bc287c6e2bf80a263868297f1404f553fa69fe3e0fb98d6d20d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"nl","translated":"{count} gewijzigd","updated_at":"2026-06-16T14:18:24.009Z"} -{"cache_key":"d126b0c94ef16181d75a94cfb3134865930d0583653f1dd3c2db53a73772778e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"nl","translated":"Officiële mobiele OpenClaw-apps verbinden automatisch na het scannen.","updated_at":"2026-07-04T16:50:04.358Z"} -{"cache_key":"d1bf5d3f0037c53ea96465d8f18ae467a68bb9b3dff0c5d0251ea390341fd556","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"nl","translated":"Nieuwe code","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"d1c04ba219c77963db77445898ff36fde37800fde27cebdfc215d1867bcccfad","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"nl","translated":"{count} gereed","updated_at":"2026-06-16T14:18:17.492Z"} {"cache_key":"d23e02644076b8d8fb39b9625b478ff0945ae4c1f43cf18392231914b0e43f1e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"nl","translated":"Nog geen operatornotities.","updated_at":"2026-06-16T14:18:17.492Z"} {"cache_key":"d2519c6b18fc40ac0135cdb3ddff374bc431c897121c6f860d3c6fc09a9fa73c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"nl","translated":"Niet toegewezen (gebruikt {agent})","updated_at":"2026-06-17T14:17:30.701Z"} @@ -114,17 +110,15 @@ {"cache_key":"d771a510805ad2fb446a50f7455e0b7b059492b353f7cb092c792f33d685fc6b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"nl","translated":"Worker-logboeken","updated_at":"2026-06-16T14:18:09.152Z"} {"cache_key":"d78e3bbfa65af411c29d5f738cd892a482898a079455ce8e4a457ebd7edd8ba7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"nl","translated":"klaar niet-toegewezen","updated_at":"2026-06-17T14:17:36.035Z"} {"cache_key":"d8a9c3f3554e93601780d385dc3e7ba7de95ae6c6ba4b9b84e3a5ce43cadf36f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"nl","translated":"Commentaar na het uiteindelijke antwoord behouden","updated_at":"2026-07-01T01:08:53.808Z"} -{"cache_key":"da9a8493ff8dc391397c416009ffc1615691e14d59d5ba6640290d02f06f158c","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"nl","translated":"Een veilige installatiecode maken…","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"db9ddb7605768fa72b2eddf2504d6719a2b908acb788f094e586d28d7a247a8e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"nl","translated":"Uitvoering","updated_at":"2026-06-16T14:18:09.152Z"} {"cache_key":"dffb6d4089b04b96d2b16470e1cde9b45856846fe44432ced18a23b11312dfbe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"nl","translated":"De eerste overeenkomende bestanden worden getoond. Verfijn de zoekopdracht om de resultaten te beperken.","updated_at":"2026-06-16T14:18:24.009Z"} {"cache_key":"e116b4ffade997d2825031156aa652af9a11e7bacbc0fde0240cd39021afab09","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"nl","translated":"Ontbreekt","updated_at":"2026-06-16T14:18:17.492Z"} -{"cache_key":"e2c0d870da2d29b8a81c77345218337f1add08d63964c097dff068661783fb7a","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"nl","translated":"QR niet beschikbaar. Kopieer in plaats daarvan de installatiecode.","updated_at":"2026-07-04T16:50:04.358Z"} +{"cache_key":"e1a64d3ebe6a8087d054933db0898bc0f1de91dbb6b19f798d6a25e687c67fcf","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"nl","translated":"Sessie herstellen","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"e569c8edeeec6e027dd3013a5e1add94707c6771ad3c1ae7eed07fbb9373afb7","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"nl","translated":"Orkestratie","updated_at":"2026-05-30T15:38:50.257Z"} -{"cache_key":"e5d6e8d54eb57c74d9c594688457da85c264be35272b794f592330e5fca930f6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"nl","translated":"Scan deze QR-code in de mobiele app om een nieuwe telefoon te verbinden.","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"e6f48f62b167833335d714cc2380e3b71081a01e7888678832b137bde4f4a747","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"nl","translated":"Doel","updated_at":"2026-05-29T21:02:33.311Z"} +{"cache_key":"e7b1e2d29045f3b78b45da6f754417e5e82928b4b9889f4d084ba17e6c47afdd","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"nl","translated":"Sessie losmaken","updated_at":"2026-07-02T14:31:07.310Z"} {"cache_key":"e7b5d8bc7926048628a8552c9eb53822eec42b7fd30223690e9dbc1ea08f6829","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"nl","translated":"Skills: {skills}","updated_at":"2026-06-16T14:18:09.152Z"} {"cache_key":"eab2584537fec740e4a1ec35af816c4cd3ad6dd020ab7502e89592a8a2e9d3e4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"nl","translated":"Actief","updated_at":"2026-06-17T14:17:30.702Z"} -{"cache_key":"eaeab538ff60bc05805a647ca6c24209f605c6b4220c8f3fddba3eb73798cab7","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"nl","translated":"OpenClaw mobiel","updated_at":"2026-07-04T16:50:04.358Z"} {"cache_key":"eb126db2ff8c08ead9df247ca29242ada9f2a2a2a3aad6e34c3adf355a5c2b73","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"nl","translated":"Geblokkeerd","updated_at":"2026-06-17T14:17:30.702Z"} {"cache_key":"ecf55f10effe7949c5b432c5714b8c56a9e583055e54b83d17e3ece3e1e8b45d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"nl","translated":"Commentaar","updated_at":"2026-07-01T01:08:53.808Z"} {"cache_key":"ed2dd36ed7de22efa8a3b9b41794981db00f932f76405c9f4a683d1bfa4ee0c5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"nl","translated":"Notitie toevoegen","updated_at":"2026-06-16T14:18:17.492Z"} diff --git a/ui/src/i18n/.i18n/pl.meta.json b/ui/src/i18n/.i18n/pl.meta.json index 14efd39b5d1c..de3b53996c7f 100644 --- a/ui/src/i18n/.i18n/pl.meta.json +++ b/ui/src/i18n/.i18n/pl.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:23:31.702Z", + "generatedAt": "2026-07-04T18:20:58.332Z", "locale": "pl", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/pl.tm.jsonl b/ui/src/i18n/.i18n/pl.tm.jsonl index ac3e4f75bb56..b7fe86318247 100644 --- a/ui/src/i18n/.i18n/pl.tm.jsonl +++ b/ui/src/i18n/.i18n/pl.tm.jsonl @@ -5,13 +5,11 @@ {"cache_key":"086c863c12458f16120fd4a26afc24dc2f2bdef8e0eb79d93023e5825f63b3de","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"pl","translated":"Orkiestracja","updated_at":"2026-05-30T15:38:42.049Z"} {"cache_key":"0a851cea32d108958a79ffaa0a9aa19eacdae272c8cd042d5278a3b6995d8ea2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"pl","translated":"Kondycja tablicy","updated_at":"2026-06-17T14:16:37.628Z"} {"cache_key":"0c9c9299905f7e7c7df89c1fcedba15ad5dbb2f93ea4f85aefc0f10825447e45","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"pl","translated":"Zaktualizowano: {time}","updated_at":"2026-06-16T14:17:07.423Z"} +{"cache_key":"0e5ab8788b7723ad29b9ffc38b3116bbbcfcfa5ce23dea2bc12c47bafa0b070a","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"pl","translated":"Przywróć tę sesję, aby wysyłać wiadomości.","updated_at":"2026-07-02T14:30:38.319Z"} {"cache_key":"0ead0750639426ab7a9908602b19a23757ad4857973fa5ee7178f8ccd8cdefeb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"pl","translated":"{agent} (nie skonfigurowano)","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"12885142433e7e36c3e3fea1cf4d3d6a70b22515935bdaef03f744aca1fe2f95","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"pl","translated":"Katalog główny","updated_at":"2026-06-16T14:17:21.494Z"} -{"cache_key":"133490e86d500d2b86d334d89c987d2dbbdae4ccae8ff2360fa180e4b84eb4bb","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"pl","translated":"Pomoc dotycząca parowania","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"159b53b073ecd7e4f586a985874d2d3df27e493ab75471c61b5aa7d4bd058d2d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"pl","translated":"{count} wyświetlonych","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"15a2aa358bef5e3a554507484321a12f00f474b35294e5383afe6c509faf4458","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"pl","translated":"Automatyzacja","updated_at":"2026-06-16T14:17:07.423Z"} -{"cache_key":"19f79dcee71d7b6ac144982b8fd887a513cac7b939957b8573bfe7e2a4652649","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"pl","translated":"Prośby o dodanie urządzeń oczekujące na sprawdzenie: {count}","updated_at":"2026-07-04T16:48:41.905Z"} -{"cache_key":"1aa75737b1f26092a41856b169712c128c98e9a07095a8435fe0945d288e714f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"pl","translated":"Kod QR parowania OpenClaw mobile","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"1ae8841ecfe18ff7f01a4bc6b10c01e94a75823bedd06c76476039d0bd8cbb0b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"pl","translated":"{agent} (domyślny)","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"1b336669341608dc242db59805a05237b2d407b150f018ab1f8d5e0b7559d2ac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"pl","translated":"{count} zmienionych","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"1b7563330091ae01f11bad7965239901cc54542e040274a38f4087e437cc537a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"pl","translated":"Zmień widok, wyszukiwanie, priorytet, agenta lub filtr archiwum.","updated_at":"2026-06-17T14:16:37.628Z"} @@ -26,6 +24,7 @@ {"cache_key":"2acde460079ef45fc20cf96806040bdfc567b2ee925d1c7ac1a288d8217461cd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"pl","translated":"Pliki projektu","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"2d2239a8d978723f93bfcc07317fb712c6ef638ab5a6fecefd348c949ec3e579","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"pl","translated":"Widok tablicy zadań","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"2e124053aa6bb951f121a72a512c8946f2259386b97055638e2c90714bc8de0c","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"pl","translated":"Daj znać dyspozytorowi","updated_at":"2026-05-30T15:38:42.049Z"} +{"cache_key":"2f7e3174e23e62718c04678d92e95a0508e305628ea42522f1a4003199eddc84","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"pl","translated":"Tylko zarchiwizowane","updated_at":"2026-07-02T14:30:38.319Z"} {"cache_key":"312a0a855e3bbe8e216cf56d3594e3c7a240a9ab38a538a31a1892251f9ea01d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"pl","translated":"Wysyłaj prośby o poprawki do bieżącej sesji czatu zamiast do sesji warsztatu propozycji.","updated_at":"2026-06-16T14:17:07.423Z"} {"cache_key":"3399c955450c7bae2a2aaf193b9236f45ca2c93b1a18caf59c5cff74319836db","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"pl","translated":"Kompaktowa gęstość kart","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"340c598b19bf09015b6c6bb15614b2b08f38f7abedd0a6994953aeff8316dca3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"pl","translated":"nieaktualne","updated_at":"2026-06-17T14:16:37.628Z"} @@ -42,7 +41,6 @@ {"cache_key":"446e1957af843a2757ea2284276cc803f27bbf598e5fa39844248e45e6accffc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"pl","translated":"W toku","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"451a0537619e8dc905ff397257b5ac5201f1092d4b8a15b0662d8541044b615f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"pl","translated":"Wygasł","updated_at":"2026-07-01T10:33:32.568Z"} {"cache_key":"45a57ddd58ec856228fd5868017a1f416da4559c8eca59f1c8a3ae4b2ec291c8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"pl","translated":"Karty bez jawnie przypisanego agenta.","updated_at":"2026-06-17T14:16:32.528Z"} -{"cache_key":"4939d9c6f18ea123b2a1aa513f0f433ef6c97f7f476a2375a9d38cebbc07b6e3","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"pl","translated":"Sparuj urządzenie mobilne","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"4a3eade8e0edfab9754da7c2afd9dec4519e971ef51ce28aef958221f9286e5d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"pl","translated":"Zaktualizowano","updated_at":"2026-06-16T14:17:07.423Z"} {"cache_key":"4a996ea93b1e7024b900c95391420e37636e7ed64abfc25acfbe69acf795558f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"pl","translated":"{count} h","updated_at":"2026-06-17T14:16:37.628Z"} {"cache_key":"4e8bbde905fdfd3afc269824e8a756dde3c59bd005fb9c1e358b606c824c8377","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"pl","translated":"brak dowodu","updated_at":"2026-06-17T14:16:37.628Z"} @@ -51,12 +49,13 @@ {"cache_key":"515c43c49884d15a19f3c0a132082fa353f23ed817d47f58d1de8556d191e413","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"pl","translated":"Zadanie Gateway","updated_at":"2026-06-16T14:17:07.423Z"} {"cache_key":"5a4288828e7b68757dfbeb5d29abe43ca378017402480936f0e3b8245796e98e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"pl","translated":"Szukaj plików","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"5b4dbdbb802db38136aa0e36564bd834c98ae263dad8e3ea40639b336b49c840","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"pl","translated":"Odrzuć błąd Talk","updated_at":"2026-06-16T14:17:14.640Z"} -{"cache_key":"5ca156a54e21089e8399f7d192d357581308e17c9828ce1f0ecb98cd9ef33cdc","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"pl","translated":"Nowy kod","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"5cbadd383960d08ae390acd46a1850660900d05d447730208127c012b826390d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"pl","translated":"Brak pasujących plików.","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"5d2a36dcd3cb08a5e61bbd85c021ea81f1aefae78da0450956aa440ee829e150","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"pl","translated":"Wyłączone","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"5d80a90503a485d8cb434a3929a3ac3e2505edd93dc47a939f80be04832e3449","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"pl","translated":"Podsumowanie: {summary}","updated_at":"2026-06-16T14:17:07.423Z"} {"cache_key":"6146b3c57e2541da113a9b030c0bd056b944a31d095e428a4fe63337968cf959","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"pl","translated":"30 s","updated_at":"2026-06-17T14:16:37.628Z"} {"cache_key":"6463f27ddc12d59648f80001e03837529d1c864a9e482d9d4b30b3b4f68dafd9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"pl","translated":"puls {age}","updated_at":"2026-06-17T14:16:37.628Z"} +{"cache_key":"65983f7390efa7ecb299ed4aac0e5dcdad3015667ab28f811e8b9ac48e563712","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"pl","translated":"Zmień nazwę sesji","updated_at":"2026-07-02T14:30:38.319Z"} +{"cache_key":"65e4147eaedf9eb4a8b101888b398f661674fba5e65f7eaf9f9586076c8effd3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"pl","translated":"Zarchiwizuj sesję","updated_at":"2026-07-02T14:30:38.319Z"} {"cache_key":"68b1fc7855b24ae357686991cdbd48c7abad06cb8cd1320cca57c6919f6b2c44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"pl","translated":"Komfortowa gęstość kart","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"69880080b3b6b126910ef0dbd611b242afa03c9df4e68694f74d8e81a5acd6d4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"pl","translated":"Nieznany","updated_at":"2026-06-16T14:17:14.640Z"} {"cache_key":"69ae375f1fff54e04cb8939701ae9a02a84086da3dda48a42fffc27d5957814b","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"pl","translated":"Русский (rosyjski)","updated_at":"2026-06-26T21:43:39.324Z"} @@ -68,9 +67,9 @@ {"cache_key":"76fc121d31ec13e2de35609b2cf72374afeb8553940063d6ae7d0f4a3cc32c05","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"pl","translated":"nieudane próby","updated_at":"2026-06-17T14:16:37.628Z"} {"cache_key":"773fde7f04e9c87f69cc58984950611863dcac306cfbb15353f410e900533db6","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"pl","translated":"Cel","updated_at":"2026-05-29T21:02:00.339Z"} {"cache_key":"77dd33b4734f2432203a41a302e40b548b2684d101f5fc81fb3becd28fcc7aca","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"pl","translated":"Podgląd","updated_at":"2026-06-16T14:17:24.224Z"} -{"cache_key":"783df953dc0c52ffe34a3258954b304437c56e04ec5415ffbfb7bc0b4f73c5c9","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"pl","translated":"Do utworzenia kodów konfiguracji wymagany jest dostęp administratora.","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"7b49c03b0dcebae277de25dcb072c4af6d87acbc77f16bde9322babbbb4a728a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"pl","translated":"Sesja","updated_at":"2026-06-16T14:17:14.640Z"} {"cache_key":"7da88cc692f36a8ef5531af85d058829f00f0dcae3d26ada5b2bf280f00f55da","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"pl","translated":"Wyświetlanie pierwszych pasujących plików. Zawęź wyszukiwanie, aby ograniczyć wyniki.","updated_at":"2026-06-16T14:17:21.494Z"} +{"cache_key":"81bf8ea9922f4995da967858c78c8a2502004208c158f60219618fe182ef4145","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"pl","translated":"Przypnij sesję","updated_at":"2026-07-02T14:30:38.319Z"} {"cache_key":"834bea4edfb9f1358823254499eec8840fa2715f2e195b0bdc41c135e46b3b99","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"pl","translated":"{count} zablokowanych","updated_at":"2026-06-16T14:17:14.640Z"} {"cache_key":"84bf5cf5555372d132aed6336c2dce28ea32891abee4def56a8511f34afb404f","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"pl","translated":"Naruszenie protokołu","updated_at":"2026-05-30T15:38:42.049Z"} {"cache_key":"8554169fc3b18fbd1a9e6fb44e93364fb0ca3751e5079d580547cd29364687f5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"pl","translated":"Odświeżanie nie powiodło się","updated_at":"2026-06-17T14:16:37.628Z"} @@ -81,22 +80,21 @@ {"cache_key":"8be18e1ff79ff44ae4507bec6911f05d2b03eb48f8e87b47b4886ae6172eb543","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"pl","translated":"Podsumowanie obszaru roboczego sesji","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"8d1578535b146d89ecb8f2784c385efc9fca780daa51dde4871cade66b2c7197","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"pl","translated":"Serwery MCP, uwierzytelnianie, narzędzia i diagnostyka.","updated_at":"2026-05-31T05:36:51.459Z"} {"cache_key":"8df6be9bca71f11691d6ef60fe58f682803b121ef655f219d9a2b83209bf4019","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"pl","translated":"Protokół workera","updated_at":"2026-06-16T14:17:07.423Z"} -{"cache_key":"8f1ee4dbb2b4c02f04cb300564e9911f3829f659b39bfa2eef2f4cf3355a63e1","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"pl","translated":"Oficjalne aplikacje mobilne OpenClaw łączą się automatycznie po zeskanowaniu.","updated_at":"2026-07-04T16:48:41.905Z"} -{"cache_key":"9711d4d7db486545be56166bf9facc7269b05698ad6f30f8f56c9817f31203d4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"pl","translated":"Do przeglądu","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"97d8cd4b3d898ec257fd4f49f241a337a29dd6b1fe77ba5543d094665ed7f793","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"pl","translated":"{count} artefaktów","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"97ed43a316faf65a1ce7bf5b1fe783afc6d2adc799adf02ab1b7df1b7ee5fdea","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"pl","translated":"Odśwież obszar roboczy sesji","updated_at":"2026-06-16T14:17:14.640Z"} +{"cache_key":"987aff595a792a622006803dbc2bf04c50cd05b42a0b7436036df0107f1f51b4","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"pl","translated":"Odepnij sesję","updated_at":"2026-07-02T14:30:38.319Z"} +{"cache_key":"98d56fd0512512f630d7a2be050c56786b65982ab686694e8b5c45f26cc545ef","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"pl","translated":"Pokaż tylko zarchiwizowane sesje.","updated_at":"2026-07-02T14:30:38.319Z"} {"cache_key":"98ef6246f39d01701b46dcc18bf5fac14155e8f5caa6882f5a0562a00f6041e5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"pl","translated":"Komentarz","updated_at":"2026-07-01T01:08:13.110Z"} {"cache_key":"9ae1ac32dd02417689af0ebb9a2336f3ac904cfb2e2fd483a2fcf9a202f0843d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"pl","translated":"Użyj bieżącego czatu do próśb o poprawki","updated_at":"2026-06-16T14:17:07.423Z"} {"cache_key":"9e997e50b54a117996a600e18094f063a9211dcf931c0b28158d5168217944bd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"pl","translated":"Diagnostyka","updated_at":"2026-06-16T14:17:07.423Z"} {"cache_key":"9ea7773002721e96653d413ea9c842df6e3fe806ec783b24bf4684c1a9a5c867","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"pl","translated":"Użyj bieżącego czatu","updated_at":"2026-06-16T14:17:07.422Z"} {"cache_key":"a0b7371e253f10f4c59fcb231d3fe6e7711e6679d27a27930038ddd2f40e8c42","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"pl","translated":"Obszar roboczy: {workspace}","updated_at":"2026-06-16T14:17:07.423Z"} -{"cache_key":"a2f6c8c536901b683033c80960683324baa1a18fc2169ef261af6757273ee3f6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"pl","translated":"Zarządzaj urządzeniami","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"a4791d53db6b4d42c83dd27a67a0ecc9c72af1dd7f9df05f84f11af2fe3c9a94","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"pl","translated":"Szczegóły karty","updated_at":"2026-06-16T14:17:07.423Z"} {"cache_key":"aa21ff42bc3a3139f08084c7429f9b347844c2fdc82b0d76f4090698d5b5f15e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"pl","translated":"Obszar roboczy sesji","updated_at":"2026-06-16T14:17:14.640Z"} {"cache_key":"abb39368433e089782235ba2a8283537b0d0790f18868098a13486019a08d5e2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"pl","translated":"Zmienione","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"abb424175a2758102955950042e17e914538497f95bdef30d9d2e8c8051fbc2e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"pl","translated":"Wszystkie karty","updated_at":"2026-06-17T14:16:32.528Z"} -{"cache_key":"aef0df2bdfeec70fe1920c852f9dc06d851b40cdcbf74bd2561d23a9e46a5bd2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"pl","translated":"Kod QR jest niedostępny. Zamiast tego skopiuj kod konfiguracji.","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"af581e6bfaf1c3da55719b56ab4ad2e41f26365afdb5e40e2cd6caa63907c2dd","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"pl","translated":"Dodano załącznik","updated_at":"2026-05-30T15:38:42.049Z"} +{"cache_key":"afb1950ce5fd7b9ed8119514f976e251d85c7485d3eb523c96e8155d9705444e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"pl","translated":"Przypięte","updated_at":"2026-07-02T14:30:38.319Z"} {"cache_key":"b107e5a8d94b6767e463fcf0af7cc9afffda3ed17180bd90fb5f3c22cf30f00a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"pl","translated":"60 s","updated_at":"2026-06-17T14:16:37.628Z"} {"cache_key":"b25fd3633ffb725cc8ec92f4e670aefac62535b06a0a474a8a060923d835efa0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"pl","translated":"{count} odczytanych","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"b403d30e80f32d63916b966022d4d2fa4802a6b1243829638409f35bfe6865b8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"pl","translated":"Brak dowodu","updated_at":"2026-06-17T14:16:32.528Z"} @@ -106,10 +104,9 @@ {"cache_key":"b5617116eb5a8dd0f87c6cc593285d53b5b7dbd511273001b51ff19c33d71989","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"pl","translated":"Gotowe","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"b59de60b2788e627ce37884086fd621c479616bc563de4746027e0b85265254b","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"pl","translated":"Błąd narzędzia","updated_at":"2026-05-31T06:44:05.057Z"} {"cache_key":"b7849d3b3c0060127e12587889942bf2e94f292b11427ecd5339f01815901ce8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"pl","translated":"Logi workera","updated_at":"2026-06-16T14:17:07.423Z"} -{"cache_key":"b91ab31436f625920b0548bec8ff8087e1af66dd285fbc5d667bc848cfc12a01","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"pl","translated":"OpenClaw mobile","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"ba853a8ee00a503478191bcf613947a52bf3dac3d53ae805c181dfefd0b67a41","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"pl","translated":"zablokowane","updated_at":"2026-06-17T14:16:37.628Z"} -{"cache_key":"bf65a420761c87cb470875686c3110b0260b93fca6e557624334b293cd79a2fa","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"pl","translated":"Tworzenie bezpiecznego kodu konfiguracji…","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"c451a81d01f3ee9e112a58e80af57103d08b1bff0b0b611aa51647c863b5320f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"pl","translated":"Kod QR parowania wygasł","updated_at":"2026-07-01T10:33:32.568Z"} +{"cache_key":"c6f33f6092383af5a4bf1d84e9dca0dd89ea9246a9c27388cdff24af2fb1e08f","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"pl","translated":"Zmień nazwę sesji","updated_at":"2026-07-02T14:30:38.319Z"} {"cache_key":"c73d3f240348f95a9782047c1a8ed7f3fcc333de4b7224cfd3a841f4876b608e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"pl","translated":"Zwiń obszar roboczy sesji","updated_at":"2026-06-16T14:17:14.640Z"} {"cache_key":"c76cce9d340f2e0a3f2361517d44ba1b1c5b5d8d84d5081b37046ec05686a004","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"pl","translated":"{count} załączników","updated_at":"2026-05-30T15:38:42.049Z"} {"cache_key":"c989cb43963eff0b0a75e2ca1abe177a3da2b64ceddd17c286919c93395e1e80","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"pl","translated":"{count} gotowych","updated_at":"2026-06-16T14:17:14.640Z"} @@ -118,13 +115,10 @@ {"cache_key":"d05741fa45849f85fdd8e8002569393ae8df7762d0f409d3ed17c5646e7f8001","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"pl","translated":"MCP","updated_at":"2026-05-31T05:36:51.459Z"} {"cache_key":"d0c9de8dc0c6459107953c844d76684182f562771ce5c7fea5a603ba7e226eab","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"pl","translated":"Do przeglądu","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"d0d7b8e08873037b55cb835745cf3330464e575a66a892aead4fc4000461c017","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"pl","translated":"Brak plików w tym folderze.","updated_at":"2026-06-16T14:17:21.494Z"} -{"cache_key":"d2c24c76691b1e19f0810d424cf568cf3c0642a0fbc522bfcd2ace1a39504578","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"pl","translated":"Pokaż kod konfiguracji","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"d38336ad413141b2e4dfd09c86b1e1a48aa6cad1356cc58ab93595996e607bbf","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"pl","translated":"Brakujące","updated_at":"2026-06-16T14:17:21.494Z"} {"cache_key":"d4e0323d4a24701bef014e0220006436bf76e842145b84f3e7249ec0be0ade9c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"pl","translated":"5 s","updated_at":"2026-06-17T14:16:37.628Z"} {"cache_key":"d657d9d4f507ea8080409534a7cf082d0230eb57a80e53afc2d835043e7911e2","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"pl","translated":"Wszystkie sesje","updated_at":"2026-07-03T07:40:09.757Z"} {"cache_key":"dba01cb4c4f5b280838377f2f4e896fb7c14ceb4eb8dfcd5e1eb77a47ba6a6ff","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"pl","translated":"Dodaj decyzję, blokadę lub notatkę dowodową...","updated_at":"2026-06-16T14:17:14.640Z"} -{"cache_key":"dd10ca55873912baea1ee49964738ba0fc2018a1b0789f85ebdb808edb488d76","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"pl","translated":"Kopiuj kod konfiguracji","updated_at":"2026-07-04T16:48:41.905Z"} -{"cache_key":"dd17cde6b6235b6991e153506bf5e13996660ee0c76782cda120578dad2c874e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"pl","translated":"Nie udało się utworzyć kodu konfiguracji.","updated_at":"2026-07-04T16:48:41.905Z"} {"cache_key":"deb00a4bd477bc1c463a6d2b341307e4fb3839061b73de4e1f349427cc618f86","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"pl","translated":"Przeglądaj, dopracowuj i stosuj propozycje, zanim staną się aktywnymi skills.","updated_at":"2026-05-31T21:48:36.995Z"} {"cache_key":"dfb06ffdc6bd6cef93f228a0d97b594933c70b9f98de742e9b8a4742bab68dce","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"pl","translated":"Domyślny agent","updated_at":"2026-06-17T14:16:32.528Z"} {"cache_key":"e4329aa6f7f264722f4561c825055d7ada884b6242bc6c7da377c2acd8b54560","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"pl","translated":"Obszar roboczy","updated_at":"2026-06-16T14:17:14.640Z"} @@ -132,7 +126,7 @@ {"cache_key":"e544c21ba97b023a5805b9a12c81fd842688b1ec3ca863ec3249ace9d7da9674","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"pl","translated":"Brak kart pasujących do tego widoku","updated_at":"2026-06-17T14:16:37.628Z"} {"cache_key":"e64bff54d837cd6b0d3bb8e262128a8b4b0bc4433f40a2614340484305c1f45a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"pl","translated":"Ładowanie obszaru roboczego sesji…","updated_at":"2026-06-16T14:17:14.640Z"} {"cache_key":"ec8a25609f58db275de801d929003f44ce9a2e2bfda2a283f128315db8740c1b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"pl","translated":"Uruchomienie","updated_at":"2026-06-16T14:17:07.423Z"} -{"cache_key":"ef72645ba291bcc1eb2065f4fff142a3297672bdb27c82a2942faad42b319348","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"pl","translated":"Zeskanuj ten kod QR w aplikacji mobilnej, aby połączyć nowy telefon.","updated_at":"2026-07-04T16:48:41.905Z"} +{"cache_key":"ed4c73ef1e8f31755a129a9d6ed365f3d03e17a5dcef7e744507f6f9498b024b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"pl","translated":"Przywróć sesję","updated_at":"2026-07-02T14:30:38.319Z"} {"cache_key":"f5247a7367d4306375f6905fa51992abf2f0f598e45847385a0e2525a9babc00","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"pl","translated":"Brak","updated_at":"2026-06-16T14:17:14.640Z"} {"cache_key":"fafb060d0479fd3602a1a411ffc2c63e8e70f3e8d223a95da1fbcab97bd23b3f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"pl","translated":"Dodaj notatkę","updated_at":"2026-06-16T14:17:14.640Z"} {"cache_key":"fb7be9136957451450aa541fa92e429d4658ddf08bdc8a574f29913135f35d97","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/pl.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"pl","translated":"pracownik {state}","updated_at":"2026-05-30T15:38:42.049Z"} diff --git a/ui/src/i18n/.i18n/pt-BR.meta.json b/ui/src/i18n/.i18n/pt-BR.meta.json index 957dc8df73ff..a2beea1a0d97 100644 --- a/ui/src/i18n/.i18n/pt-BR.meta.json +++ b/ui/src/i18n/.i18n/pt-BR.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:21:11.084Z", + "generatedAt": "2026-07-04T18:20:49.217Z", "locale": "pt-BR", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/pt-BR.tm.jsonl b/ui/src/i18n/.i18n/pt-BR.tm.jsonl index 5187574e7e38..57b4bc152381 100644 --- a/ui/src/i18n/.i18n/pt-BR.tm.jsonl +++ b/ui/src/i18n/.i18n/pt-BR.tm.jsonl @@ -8,37 +8,33 @@ {"cache_key":"0613deef39995601902688023959cd2e639c545495d1335fac099bae11d74c36","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"pt-BR","translated":"Expirado","updated_at":"2026-07-01T10:31:00.782Z"} {"cache_key":"08dc6f4e955bf3f335d3afb9e544cc7a6ea39ba25f1d85a1f151ebe4119169d4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"pt-BR","translated":"Ausente","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"0c4a8a34d4da8f042218f03bc496031194eb381004b2444ebe813abfb350869d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"pt-BR","translated":"60s","updated_at":"2026-06-17T14:13:19.801Z"} +{"cache_key":"0df8199942950468868454b555bd73577bfb9a45f0844daf333920fe7e747164","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"pt-BR","translated":"Fixadas","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"0ec17d42d507c693995aa202c0aa2c38d62fb7aa0c0663165a00896f206b352d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"pt-BR","translated":"Objetivo","updated_at":"2026-05-29T20:59:56.109Z"} {"cache_key":"10dce7dfe66936a2d2a07b68944f01f80479beac869a0dbeb382ca6a72bf476a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"pt-BR","translated":"Workspace: {workspace}","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"110ae46a7017718e8a55eccd65720463f259822631275f36a46535bb76ee9e7e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"pt-BR","translated":"{count} exibido(s)","updated_at":"2026-06-16T14:13:25.058Z"} -{"cache_key":"1274ddebb56af3aef1daf4828d80d97ec4a93b67990b0b55f94774b18529a0fb","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"pt-BR","translated":"QR indisponível. Copie o código de configuração em vez disso.","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"14253f5cfeaaaeb483be1a9f6f5495ba61c55dfbf37793b50229c5daa7a4b7a2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"pt-BR","translated":"5s","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"160ef9a5963d2c85d043f9fe2e6452b3de2d0de0199a32d1aef6d411dcb3cffd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"pt-BR","translated":"Falha na atualização","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"179ff53453e338a4a508f8623a8cd80e1fcbf425786ad6a22da05aba53bf211e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"pt-BR","translated":"Integridade do workboard","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"1a9710b3c9e635f7a54a4f0851b5e3a8def1828a980ee3400f7abaa118fce7ec","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"pt-BR","translated":"Atualizado: {time}","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"1d53c3631e9cdc4c0a02576d6ca7f31ace557acaaf56ab724c9fbee8a9460c65","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"pt-BR","translated":"{agent} (não configurado)","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"1e31a74cefeb52fb3311b33d14bb190c97c5fdf1c9c9c223bf6d45a29bc916c8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"pt-BR","translated":"Nenhum arquivo modificado nesta sessão ainda","updated_at":"2026-06-16T14:13:17.394Z"} -{"cache_key":"1fec729397948a6de1fd16530b2b43453762368a582f5f483202f85b4aba3985","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"pt-BR","translated":"É necessário acesso de administrador para criar códigos de configuração.","updated_at":"2026-07-04T16:44:36.260Z"} +{"cache_key":"2017da6d4e2102a0d7c74fb958e4a276a875f40621781606fb6342608b92e37c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"pt-BR","translated":"Desafixar sessão","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"20b497a56ed8bd32b399511bf37926f3d3a6fcda3dee19e8c823c3d19ce019f2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"pt-BR","translated":"Dependências","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"23e62615ae72efe1a36a998fbf6a7503c7997cc9e87b8eb15d8b1f042aafbce3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"pt-BR","translated":"Todos os cards","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"2582a2593221ed2625f3624da8e0ffbedaed08160e63d72a48f90c9b07367742","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"pt-BR","translated":"Detalhes do cartão","updated_at":"2026-06-16T14:13:11.260Z"} -{"cache_key":"27193b5a27e2512c98a1ff5abeeef7c254747080be16e6d793f534e1df91a67c","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"pt-BR","translated":"Novo código","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"2817fc682cb8dfe96ab382151d263b508ef0c8e0923b7ea75d89df7931002b8e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"pt-BR","translated":"Em execução","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"28668a02cb2681672396a70daa274c4364cce32882d43c2f58969eefaf947fee","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"pt-BR","translated":"Não atribuído (usa {agent})","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"28fd08f745525853e7e3e4c1237975f8c07d07cf4256c45d176d31c2cd1f0f88","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"pt-BR","translated":"Manter comentários após a resposta final","updated_at":"2026-07-01T01:06:19.014Z"} {"cache_key":"29a3bfdf9cb14ec24751f2be576b51d0cd47954b5ba67562df34b35cf115b3c5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"pt-BR","translated":"Usar conversa atual","updated_at":"2026-06-16T14:13:11.259Z"} {"cache_key":"2a6b55bd02cddd364a7b1464ec13a1b141da6d9889b97ec3bfc7a252346fe09b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"pt-BR","translated":"{count} artefatos","updated_at":"2026-06-16T14:13:25.058Z"} -{"cache_key":"2ae0c0802dd5f1567ea7281b3362098474fd24780f928ec08abd5f9b56d3f581","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"pt-BR","translated":"Criando um código de configuração seguro…","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"2b5f6b5471c21d5d4a6fb037d707b3db112d3903b48e24cfe2e79782e7ac2233","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"pt-BR","translated":"Comprovação","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"2cc26199dd8364292a5bf914820a7968ec35b8829f5377edd895c2198dad2ff4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"pt-BR","translated":"Nenhum cartão corresponde a esta visualização","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"2d440683dcdc1f2a6014267ec37197b9401f2088180d948a601d177fd739b218","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"pt-BR","translated":"Atualização automática","updated_at":"2026-06-17T14:13:15.046Z"} -{"cache_key":"2e03ae0fef26b962a48e1bfbb84c4126c02125f864f1d7fba0fbc023efa6c4f5","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"pt-BR","translated":"Solicitações de dispositivos aguardando análise: {count}","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"2eeef23403bf900df82006215ad440ee45b24cb7e75801167765f7bef800171b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"pt-BR","translated":"Notas do operador","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"2fd1b0311df8702e5bf88078211dfe1d867e780f15f2584a0363775a27951100","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"pt-BR","translated":"{count} bloqueadas","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"328b16b2532ada2bf594d3cc589637f1ec809902df1194b6f7644d4a2a78c033","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"pt-BR","translated":"Sessão","updated_at":"2026-06-16T14:13:25.058Z"} {"cache_key":"331a07a121ef71e48cafebfc387eda6d96f99f77da8aed536a8647c4ff9f6236","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"pt-BR","translated":"Acionar despachante","updated_at":"2026-05-30T15:38:08.357Z"} {"cache_key":"36525534b1f413e589721c84f48933c18fdcbe054592b7fada7ccfe3213aab52","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"pt-BR","translated":"Violação de protocolo","updated_at":"2026-05-30T15:38:08.357Z"} -{"cache_key":"36aff485d1d92d614ffae68a4c116a246c378b69455f6f46f253e1281f40ac02","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"pt-BR","translated":"Escaneie este código QR no app móvel para conectar um novo telefone.","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"3787cb051c50cfd1d726ae393887dc627c26cfca46bf9152db3911cdb310c29e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"pt-BR","translated":"Carregando espaço de trabalho da sessão…","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"37ce9fb07f07eb4380a4bdc80ccd6ba219b5306cc5e180754e06b5fba9918bf0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"pt-BR","translated":"{count} prontas","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"3c0cbfff49a72a30736ff7e0d40e5401f3644c2d048ecf5019dfaeb0250a8092","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"pt-BR","translated":"Atualizado {time}","updated_at":"2026-06-17T14:13:19.801Z"} @@ -55,6 +51,7 @@ {"cache_key":"4f015b7247310e7db6d80de0129d60608a832a0ca6aef5811070e485355ff03a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"pt-BR","translated":"Altere a visualização, a busca, a prioridade, o agente ou o filtro de arquivamento.","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"52a9feec0c469310241b70cb9eb738808938cc679594311dca976d59e9b03eef","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"pt-BR","translated":"Resumo: {summary}","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"56527849fb4f30cd42301746ad5168b5389e161274548ec099829b2e071ce346","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"pt-BR","translated":"{count} logs do worker","updated_at":"2026-05-30T15:38:08.357Z"} +{"cache_key":"58bd43433edec67c7e5bee58053bf2c5cb7eb59bce3bc3f44b0edae90afc2729","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"pt-BR","translated":"Fixar sessão","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"5a6b897f7bb8f1881075744d214e4ca670db2c38dffa36d7ff736412a3d35dac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"pt-BR","translated":"Recolher espaço de trabalho da sessão","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"5d13edebfeac4cb80217c58a835dd3e0043fc536b08e561bfcbbd5173fab541f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"pt-BR","translated":"comprovação ausente","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"5d4d0ec11dba787328994a28e38659b27d4f6170947ba80a4cdf1aeca212d753","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"pt-BR","translated":"Concluído recentemente","updated_at":"2026-06-17T14:13:15.046Z"} @@ -62,7 +59,6 @@ {"cache_key":"608011fd1684ab21843f93b54e3dc7a3c7453771a8d491cd370a6b118579933f","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"pt-BR","translated":"Servidores MCP, autenticação, ferramentas e diagnósticos.","updated_at":"2026-05-31T05:36:33.676Z"} {"cache_key":"612196f05682d1cd80927990a47588b0eea28b1ea71cc35874a8c70797451be7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"pt-BR","translated":"30s","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"62addd8685de3b46c8bde90e8b563d4345332b5374ed0e4d0f4dcd6974a72184","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"pt-BR","translated":"Enviar solicitações de revisão para a sessão de conversa atual em vez da sessão de workshop da proposta.","updated_at":"2026-06-16T14:13:11.260Z"} -{"cache_key":"662c0a8520bb601949dc70337f7b384ade758db46faa763214a7f80930c9ce0d","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"pt-BR","translated":"Copiar código de configuração","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"6a74017e37f35472190c4f57ee1fc5aaa5329be4ade6526b43a597993a5ab067","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"pt-BR","translated":"Cards sem um agente explícito.","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"6ab671a864dd54de5b1c3d2b9a0f071ecbc114a47151e7f90f57b301d8a543e0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"pt-BR","translated":"bloqueado","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"6bd3eef6e1978232e97b68dd337256f651d59a337d8f94586abd38d3f8fed72e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"pt-BR","translated":"{count}h","updated_at":"2026-06-17T14:13:19.801Z"} @@ -70,14 +66,14 @@ {"cache_key":"6e61762ce31db633d77e2c2eab324a11a89b4bfdfdead4f8bbf7b5898d55efe4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"pt-BR","translated":"Visualização do workboard","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"6ed6a9125817a95d0257ddf0a8c9896768f341b4a256142c43a9fd7e1f1d762e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"pt-BR","translated":"{count} dependências estão concluídas.","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"71a68f2f8fa85b6074691a84169abb6fa3e682b9677eba5c69635d0638daa187","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"pt-BR","translated":"Execute /pair qr novamente para gerar um novo código de configuração.","updated_at":"2026-07-01T10:31:00.782Z"} -{"cache_key":"71baccb40ea064b9a859edde78cc45b2da634f676ca6c796024b2af2e4c78a08","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"pt-BR","translated":"Código QR de pareamento do OpenClaw mobile","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"74fcd9889d4c41ccf8ef32728b424f1c0b71c6848579bb9e6cdab0bfb8a1c358","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"pt-BR","translated":"Resumo do workspace da sessão","updated_at":"2026-06-16T14:13:25.058Z"} {"cache_key":"77c5918867d611b7c15e4cee328340eb9be4902695bddcaf58f63ae632883aad","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"pt-BR","translated":"Ações de arquivos do workspace","updated_at":"2026-06-16T14:13:25.058Z"} {"cache_key":"791282826039ef4afd4ea2b7282eb45b466139b0387e16bc08f4fae0b4dc4ca0","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"pt-BR","translated":"हिन्दी (híndi)","updated_at":"2026-06-26T21:43:22.390Z"} -{"cache_key":"7a04fbc54c781a417e4b827568d444de2313a4b46848db42add4fda09d588dc5","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"pt-BR","translated":"Não foi possível criar um código de configuração.","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"7a36b55390955775cbe56f1e822100787337feba448d4eb94c87fb403a9fa655","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"pt-BR","translated":"Usar conversa atual para solicitações de revisão","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"7d064865e923b7dbc2f3e296e6ab3c5bfe569890aa63dce5dadb8232c49e2ee5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"pt-BR","translated":"Bloqueado","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"7fb0a0c4f9f4504ac791452533287dc30c984bc75a83a02bb5b6d418656f4ebd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"pt-BR","translated":"Cards atribuídos explicitamente ao agente padrão configurado.","updated_at":"2026-06-17T14:13:15.046Z"} +{"cache_key":"7fda2f6109c75c2891fedcf97e66008fa3852062f1d6ce74342a0f0ac50863e8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"pt-BR","translated":"Mostrar somente sessões arquivadas.","updated_at":"2026-07-02T14:30:01.256Z"} +{"cache_key":"8922f38063d15d1416eff0fcb59469c7166aee8fd3af92db2d14d752d91ecb0c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"pt-BR","translated":"Restaurar sessão","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"8a4c1ba0dde3db6bc2fe7eec30bedcd565b85ccd6326c09ac27446d62d1c8d64","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"pt-BR","translated":"Raiz","updated_at":"2026-06-16T14:13:25.058Z"} {"cache_key":"8a5394bf53b38de5b08d8c7fb25c98d145b9f8808056b2b3e181824fb52f5888","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"pt-BR","translated":"Desconhecido","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"8dc397b6b255a6c5f4942112a5f5062eb723d8408b8b13405d49354d2d660afe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"pt-BR","translated":"Sessão","updated_at":"2026-06-16T14:13:17.394Z"} @@ -87,8 +83,6 @@ {"cache_key":"91a9cfbbe6b8d1cab7ee71cf5a3479fbc95226eee06490a8e1ccf1c52050256f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewStale","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Stale","text_hash":"40c9e59c5e152b0ae9affc84d8461c29b75a7709e4506307eeedf246b526014e","tgt_lang":"pt-BR","translated":"Obsoleto","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"945fb4725e382dbce447cf68c998e902cd347429a820c41057cf46d5c3707fa6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"pt-BR","translated":"Logs do worker","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"97f0c9b45521b442581d84dcd0d404dd14d897d48d4a73e223b77b7f1a756f1d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"pt-BR","translated":"Aguardando dependências: {parents}.","updated_at":"2026-06-16T14:13:17.394Z"} -{"cache_key":"9802d59dd7d12605418b0317a150112e3d1c9a31f8ecfc245a6cffe9fb5fe562","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"pt-BR","translated":"OpenClaw mobile","updated_at":"2026-07-04T16:44:36.260Z"} -{"cache_key":"9b25bc38cb1dc713b1741da966a602c3f2e0bb71d0c2eaa3da86d0051105bc52","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"pt-BR","translated":"Ajuda de pareamento","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"9b3ad76185ddae5fcd8f5079b83c9b1a01490fa5c18286071524958ae525b969","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"pt-BR","translated":"Expandir espaço de trabalho da sessão","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"9d8838a7bceff9fee18509291eb0847b4a17fef0f73b7a47869655218ec05aba","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"pt-BR","translated":"Adicionar nota","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"a1cb01b3761d31fd13681276eca4d60dc8faae6502644c7ac2944e231234af6f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.cwd","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"CWD","text_hash":"0217f1cb7725737f15a6710df3bcfa3bc10a239f0f7801ec3d7168e675f5ebd6","tgt_lang":"pt-BR","translated":"CWD","updated_at":"2026-06-16T14:13:27.459Z"} @@ -98,20 +92,19 @@ {"cache_key":"aae3e4c2771dadd4bb5ed4c32f94e44a13ec9e97b00f864b54bcb255a760cadc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"pt-BR","translated":"{count} lido(s)","updated_at":"2026-06-16T14:13:25.058Z"} {"cache_key":"aae437b6a949776a04a7903a6847200de96d1e4e52a4b7ad02df6f11b068f82f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"pt-BR","translated":"heartbeat {age}","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"ab5cdcb2290b3fa0305107f2aa4b8b46dba91270e042b2df0ca869f6d5a387d3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"pt-BR","translated":"tentativas com falha","updated_at":"2026-06-17T14:13:19.801Z"} -{"cache_key":"ac066dfa5ed74e839300aa51c5ce0c1b4edcb8ab7e2385b460f8338585fc93a5","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"pt-BR","translated":"Gerenciar dispositivos","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"ad8ed5de23d762dd82e7cc56247cf1e9fe3183b020433004d2c35f9472fdb6e9","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"pt-BR","translated":"MCP","updated_at":"2026-05-31T05:36:33.676Z"} -{"cache_key":"b37120c79b101950fe5b300e081329d1a84ec9b20a0a4204d6c2d4e0954c747d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"pt-BR","translated":"Revisão","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"b50ad7d9145a6a7208035e4bb4b0b996f87630c1467991d90fdbb84fddf0d0f8","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"pt-BR","translated":"Revise, refine e aplique propostas antes que elas se tornem skills ativas.","updated_at":"2026-05-31T21:48:16.734Z"} {"cache_key":"b65bdce4e71ce28444509ba524111356b2a35f4f2fe7970390fc4a908355117b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"pt-BR","translated":"Arquivos do projeto","updated_at":"2026-06-16T14:13:25.058Z"} {"cache_key":"b7e0d3670497eadf0f79edd7a87081f6e5fe131ee063832911ddf941eb089681","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"pt-BR","translated":"obsoleto","updated_at":"2026-06-17T14:13:19.801Z"} {"cache_key":"bb76a41dd82e6591923b3b8957fc6211f7743b55cb5ccf51f9da9aa908680fc9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"pt-BR","translated":"Exibindo os primeiros arquivos correspondentes. Refine a pesquisa para restringir os resultados.","updated_at":"2026-06-16T14:13:25.058Z"} +{"cache_key":"bdd521107afdb42db676e8e0da464a5ae9712339b762fc357cefb8f1c2e53028","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"pt-BR","translated":"Renomear sessão","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"bf5d0d78c050f9e4e5347bf603a69da4e5799d7204beb7a36c67fb884b7aa61c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"pt-BR","translated":"QR de pareamento expirado","updated_at":"2026-07-01T10:31:00.782Z"} {"cache_key":"c434861697e533c8617971120993836b4c551e540f42b36a6a8d0d5658a77009","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"pt-BR","translated":"Agente padrão","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"c47a8164a46a7bcf752fb340b07a59ed6f0b18f404897a04a74ee0bb69714236","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"pt-BR","translated":"Densidade de card confortável","updated_at":"2026-06-17T14:13:15.046Z"} -{"cache_key":"c4fbf446f1c92389c909cb027953edec99dcaebcd959e3ec375344163d7f1c20","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"pt-BR","translated":"Mostrar código de configuração","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"c57769d62c377e80d924d00cd4c5cc639615dc3b2ecb28082be44fc48d3e8e44","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"pt-BR","translated":"Erro da ferramenta","updated_at":"2026-05-31T06:43:47.543Z"} +{"cache_key":"c6883f9a362a9500c3a805e89675c9ea091bc2f76a35c9a456149e90376c1086","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"pt-BR","translated":"Restaure esta sessão para enviar mensagens.","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"c68b4639d8b8a0732c9be1f5308f3a23aaadd1f0f2182b065c92692c053f0ced","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"pt-BR","translated":"Ausente","updated_at":"2026-06-16T14:13:25.058Z"} -{"cache_key":"c9fac818004ed0b6ba3fd1dde2a14a5440cd4e9865010ad837a133df2bddb9fb","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"pt-BR","translated":"Os apps móveis oficiais do OpenClaw se conectam automaticamente após a leitura.","updated_at":"2026-07-04T16:44:36.260Z"} +{"cache_key":"d23a8d2922fc9bd7f120a8394f1b53bfa74808430b8a94127987450d6a574592","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"pt-BR","translated":"Arquivar sessão","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"d50ec3d6cdda5e85f94e7ffb61547f3f5696d9b39d9612ffd7221d90ab11d005","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"pt-BR","translated":"Nenhuma nota do operador ainda.","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"d54e130ea95e9c4b23bac3149fe4730de8ecf8c063e278d8e8eb59daf59382ab","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"pt-BR","translated":"Sem comprovação","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"d66ada3975a18ef7bef1ec6f4cdc3944b4a9b1ee5cd97a3d591ebb62dc0389da","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"pt-BR","translated":"Tenant: {tenant}","updated_at":"2026-06-16T14:13:11.260Z"} @@ -121,19 +114,20 @@ {"cache_key":"dbf9d1bd653cf75877ad3e71c8987b6622c15dac41b6a1ad029b9aa59c78e7ed","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"pt-BR","translated":"Desativada","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"dd2e6bf65a182d58a88aa87e99268ae7052a151638bb76802e6f3f732436833d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"pt-BR","translated":"Revisão","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"df82ec15898716123b7d8b0ee93e49c31dd348e632f5c98788759d3ff3e2ec70","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"pt-BR","translated":"15s","updated_at":"2026-06-17T14:13:19.801Z"} +{"cache_key":"e124b6901688ec52c2f4ad6e008d129ed2a7b0310d1a06c23e9c1df742d49844","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"pt-BR","translated":"Somente arquivadas","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"e22dfd9372c83d8ad71eee6fff842e5e6de2ae54728755d295638aebdad3012c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"pt-BR","translated":"Ver detalhes","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"e23da57a026da692a0c2e517232f87f33571c85ba5a86fac103c65462d70cc11","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"pt-BR","translated":"Descartar erro do Talk","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"e31cd66e142bae39642471c7b1981a3205507bfaaae9d715bc162caa0faad452","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"pt-BR","translated":"{count} anexos","updated_at":"2026-05-30T15:38:08.357Z"} {"cache_key":"e67fbd2381e4037fa51378a2553b422e7e84ad182a1ef99f80223a2c5849af61","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"pt-BR","translated":"Atualizar espaço de trabalho da sessão","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"ea9deb7b47e4cdfa527378dcdc35ca2a92b1c3afa75d8071627f2b3c76f34320","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"pt-BR","translated":"Lido","updated_at":"2026-06-16T14:13:25.058Z"} {"cache_key":"ed2a87b13d6372d493087e37f8ac2013fa200dc8323122a31c3923fcaa1e4949","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"pt-BR","translated":"Espaço de trabalho da sessão","updated_at":"2026-06-16T14:13:17.394Z"} +{"cache_key":"ed712f1f5a40d696e38be09d397fcd14163721abc2b704649f0bb73105869ff3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"pt-BR","translated":"Renomear sessão","updated_at":"2026-07-02T14:30:01.256Z"} {"cache_key":"eef7c73fdcad94e35086277b7e4b0d7832346407a2fff070c8aadbd0812fac3c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"pt-BR","translated":"Quadro: {board}","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"ef1797c3b71150753c536676682bb0eeefc4d2ab674b44b4d8af63d4b7f9b324","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"pt-BR","translated":"Espaço de trabalho","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"efd794d81e11134018026a23bf1c2ec8eb5f948d3bc0f4bca6cb6d8b2f772735","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"pt-BR","translated":"Pasta superior","updated_at":"2026-06-16T14:13:25.058Z"} {"cache_key":"efe8a0787adb86cbcf78b9b62a55a57a536379d1a94e82f341f842b4e77fe2f2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"pt-BR","translated":"{agent} (padrão)","updated_at":"2026-06-17T14:13:15.046Z"} {"cache_key":"f09f2517f09740aff2cc8fa779c97ef7285d461a9a94469f02fe56fb6de53aeb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"pt-BR","translated":"Automação","updated_at":"2026-06-16T14:13:11.260Z"} {"cache_key":"f2f51a0996d691ef35cc517d6a98be6ae2160ceb9c023ac17d7cea0e8151a4ac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"pt-BR","translated":"em execução","updated_at":"2026-06-17T14:13:19.801Z"} -{"cache_key":"f589f02312480b65df642ac94792c2ce2e6efc963879696c7d03e76c1214dd2c","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"pt-BR","translated":"Parear dispositivo móvel","updated_at":"2026-07-04T16:44:36.260Z"} {"cache_key":"fd587e677a6fab1699155302642242711c95431c2f5f45cb8ff30fff73e45d88","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"pt-BR","translated":"Adicione uma decisão, bloqueio ou nota de comprovação...","updated_at":"2026-06-16T14:13:17.394Z"} {"cache_key":"fd94af84717b680ce5aff29147c87c69bc0e7a6a83801ac9f5ea2389a917a75e","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"pt-BR","translated":"Oficina de Skills","updated_at":"2026-05-31T21:48:16.734Z"} {"cache_key":"ff56bf8c96eae6f882b8c3ddda88697b6cd14165569be390c7cb4b3954250fb5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.copyPath","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"pt-BR","translated":"Copiar caminho","updated_at":"2026-06-16T14:13:27.459Z"} diff --git a/ui/src/i18n/.i18n/ru.meta.json b/ui/src/i18n/.i18n/ru.meta.json index 2fd8c1ee213e..c767a7090e81 100644 --- a/ui/src/i18n/.i18n/ru.meta.json +++ b/ui/src/i18n/.i18n/ru.meta.json @@ -1,33 +1,11 @@ { - "fallbackKeys": [ - "chat.commentaryLabel", - "chat.commentaryToggle", - "chat.pairingQrExpired.badge", - "chat.pairingQrExpired.reason", - "chat.pairingQrExpired.title", - "chat.sidebar.allSessions", - "nodes.pairing.adminRequired", - "nodes.pairing.button", - "nodes.pairing.copySetupCode", - "nodes.pairing.failed", - "nodes.pairing.generating", - "nodes.pairing.help", - "nodes.pairing.manageDevices", - "nodes.pairing.newCode", - "nodes.pairing.pending", - "nodes.pairing.qrAlt", - "nodes.pairing.qrUnavailable", - "nodes.pairing.showSetupCode", - "nodes.pairing.subtitle", - "nodes.pairing.title", - "nodes.pairing.waiting" - ], - "generatedAt": "2026-07-04T18:11:20.222Z", + "fallbackKeys": [], + "generatedAt": "2026-07-04T18:20:59.216Z", "locale": "ru", "model": "gpt-5.5", "provider": "openai", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1431, + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/ru.tm.jsonl b/ui/src/i18n/.i18n/ru.tm.jsonl index d22bca7f0e74..e990c7f5dfd7 100644 --- a/ui/src/i18n/.i18n/ru.tm.jsonl +++ b/ui/src/i18n/.i18n/ru.tm.jsonl @@ -10,6 +10,7 @@ {"cache_key":"011005194cc419d648d11602c00bd49d6abca93d0f4f506a9efcf24a83051ac1","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.space","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Space","text_hash":"20eac5aae274985fd88629d19eddccbbec21dacd82a8c7a7dd99661f2135be02","tgt_lang":"ru","translated":"Пространство","updated_at":"2026-06-26T21:39:01.088Z"} {"cache_key":"015e6beebf4725aa859e575c9bc76d7da6e4426d5d7f249c0d3e6e755f99022e","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobState.next","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Next","text_hash":"1ff57a29d7c9d11bdf61c1b80f2b289b44c1ea844824d4b94a0d52b6ba5fc858","tgt_lang":"ru","translated":"Следующий","updated_at":"2026-06-26T21:42:47.554Z"} {"cache_key":"0181a79102b23bd6c3887c73d8a3b0149a02fea64c8a508fdb1ff548e86f3704","model":"gpt-5.5","provider":"openai","segment_id":"cron.errors.webhookUrlRequired","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Webhook URL is required.","text_hash":"a84533e7d336c2821ad97847dbe84fd1f7f0219b710e98d4e5f978485dc5008a","tgt_lang":"ru","translated":"Требуется URL вебхука.","updated_at":"2026-06-26T21:42:51.343Z"} +{"cache_key":"0199cd7facf158691626f2485f4fd259c877c0a061665027ef1657c7b607948b","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"ru","translated":"Восстановите этот сеанс, чтобы отправлять сообщения.","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"02000a1fb40c5be42cf0f74ebf4944cf32f1911b8dd621de2167a737bf7fbaae","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeStale","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"ru","translated":"устарело","updated_at":"2026-06-26T21:40:03.947Z"} {"cache_key":"0204376974723072eed2c1a9b795d8698e4ee708546f3e0bbf70dd5098ff47a1","model":"gpt-5.5","provider":"openai","segment_id":"overview.cards.modelAuthAttentionExpiredTitle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Model auth expired","text_hash":"0c37d888df561b1ff2a86a41b7297f5935431ea0c56d3c983942912387e496ad","tgt_lang":"ru","translated":"Срок действия авторизации модели истек","updated_at":"2026-06-26T21:40:31.548Z"} {"cache_key":"02480e3fcb443216c3ed93feef1fe46516d13b47c3f3cbf45696af292a39fb8f","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.basics","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Basics","text_hash":"8fdd2ee8475e29bcb7acc41b731a943957e4dc3d07012c23f8b7b028de620267","tgt_lang":"ru","translated":"Основное","updated_at":"2026-06-26T21:42:28.070Z"} @@ -114,6 +115,7 @@ {"cache_key":"11ec7f79fc0dc43a102c2a6f6d4afebd927fbc20da31e4ac430209e80ba85d9b","model":"gpt-5.5","provider":"openai","segment_id":"workboard.lifecycleMissing","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Session missing","text_hash":"d48d870c4419a406a0883369913c52bff48e48c2c66dacf7a49467905ef1d9bb","tgt_lang":"ru","translated":"Сеанс отсутствует","updated_at":"2026-06-26T21:40:07.529Z"} {"cache_key":"11ed202aa3c509b79068b434deea5aab57f3062fbc81f99762397dfb389f0674","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.avgSession","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"avg session","text_hash":"a8ce1dc2f9461f5c3cf015b40c54888e55840ac786b8f878465ff1c77348a6df","tgt_lang":"ru","translated":"средний сеанс","updated_at":"2026-06-26T21:41:16.423Z"} {"cache_key":"12381bdcb422017d2abc0d756837567af08a6d7c516f68db4fd0eb7dc2a9fdcc","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dependencyMissing","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"{parent} (missing)","text_hash":"8daa419059727391c01e3b7021e05d8d70b4da67f9c57cd2d80f302af77aac53","tgt_lang":"ru","translated":"{parent} (отсутствует)","updated_at":"2026-06-26T21:39:54.162Z"} +{"cache_key":"126fabb6e2f4af7265b641cd55f5afec69a2ef58cff6e56315fd1705422bc284","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"ru","translated":"Все сеансы","updated_at":"2026-07-03T07:53:18.171Z"} {"cache_key":"127649c7e18e486eee06d5f6efd66d0ec5eaf2da92e3620a00e2e1b406ecad7a","model":"gpt-5.5","provider":"openai","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"ru","translated":"Все карточки","updated_at":"2026-06-26T21:39:50.999Z"} {"cache_key":"12a20e4ab63737d32c6148cd93086672910a5bba9c00b3dd93fb3537ce49cc7c","model":"gpt-5.5","provider":"openai","segment_id":"overview.connection.step2","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Get a tokenized dashboard URL:","text_hash":"c697a6e03fa9ac7f8036204eb6c2a95a143a4de97961318cb00b3e5c039b1794","tgt_lang":"ru","translated":"Получите URL панели мониторинга с токеном:","updated_at":"2026-06-26T21:40:28.260Z"} {"cache_key":"12bf21a9b59890db730d730e3fb4a676d78ec8b291820e8f3ed30c298562c26c","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.filtered","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"(filtered)","text_hash":"ff5bcbf42db8f900aa7678f0c3859d3f48f33f9279f6582e19952c885cea371b","tgt_lang":"ru","translated":"(отфильтровано)","updated_at":"2026-06-26T21:41:23.762Z"} @@ -170,6 +172,7 @@ {"cache_key":"1c86104ba60997594082c363b3feb4382c0988d888df02bd366ddcd5443d2ef2","model":"gpt-5.5","provider":"openai","segment_id":"activity.statusFilters","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Status filters","text_hash":"9bfa1c5a7d114a46d8ac9fd44cc0d11bfd837eb705927fbd4789ba2b01d30e06","tgt_lang":"ru","translated":"Фильтры статуса","updated_at":"2026-06-26T21:39:35.030Z"} {"cache_key":"1cc9a0d9f0abb4c4dc69d96c014833cbdaddb1910048c04772e32c278e5febbd","model":"gpt-5.5","provider":"openai","segment_id":"debug.security.runSuffix","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"for details.","text_hash":"c14ed31f0bdf407b54d5074863c6ac679b898c5123e7d0a89c9d016215894177","tgt_lang":"ru","translated":"для получения подробностей.","updated_at":"2026-06-26T21:39:17.817Z"} {"cache_key":"1ccd134aefba8d4a1e4774ee825f93c88e7c1f8b74c6c26e7ec1e95d8e63706a","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.howHint","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Choose how results are delivered.","text_hash":"6c3235002460d7c6395e387dd0e3a199a8be40a0ba819a675111fb90b04f48a9","tgt_lang":"ru","translated":"Выберите способ доставки результатов.","updated_at":"2026-06-26T21:42:18.092Z"} +{"cache_key":"1cda5fb383a5ee0af796a613afe1bed3bc58b2c1ef0a0a538112974f24c4125b","model":"gpt-5.5","provider":"openai","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"ru","translated":"Сохранять комментарии после окончательного ответа","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"1d02503bccb559edb5b3c44795640ceeafc0c7d96387b188b2e1c5737a309a28","model":"gpt-5.5","provider":"openai","segment_id":"workboard.open","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Open","text_hash":"ed077f3d8125d60dca1979c7133601bd187d47c73ed9975028f677e49e709942","tgt_lang":"ru","translated":"Открыть","updated_at":"2026-06-26T21:39:54.162Z"} {"cache_key":"1dbc3875e2227281b200a0b63e2ba9bbe87ba8bd9644af0471f6564571b3b3dc","model":"gpt-5.5","provider":"openai","segment_id":"common.resources","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Resources","text_hash":"e89b30aa1dc30a6ad8c34610689bede83b3ef645a24c3a84a0990826687fb735","tgt_lang":"ru","translated":"Ресурсы","updated_at":"2026-06-26T21:38:30.975Z"} {"cache_key":"1deeff59af97bdcb8f6de9c7bde79e9fb3e42df4927da89485f41cabc2c1968a","model":"gpt-5.5","provider":"openai","segment_id":"overview.access.hideToken","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Hide token","text_hash":"ae132305cb4bfbfe5508d7a36a29a914ce321156b8b2e26d5cbddd29d033c713","tgt_lang":"ru","translated":"Скрыть токен","updated_at":"2026-06-26T21:40:14.690Z"} @@ -320,6 +323,7 @@ {"cache_key":"346e57d995e536eb7d599eb2271d196f3f71fa060efe6c02766e577b24cd15a8","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.cumulative","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Cumulative","text_hash":"cecf2aade089366e0a1d7c3dfc5acb40de8bb0d84c71b890d96da2f2de96c152","tgt_lang":"ru","translated":"Накопительно","updated_at":"2026-06-26T21:41:23.762Z"} {"cache_key":"34d72373e9b9502f861fdb4c6ac9be5178894ca4aa0e2bf460e382723ff189ff","model":"gpt-5.5","provider":"openai","segment_id":"workboard.taskStatus.queued","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Task queued","text_hash":"1f81e55472b4a703f158d6aee85b835df71ba944c7b7362dde55abf2691db4b6","tgt_lang":"ru","translated":"Задача в очереди","updated_at":"2026-06-26T21:40:07.529Z"} {"cache_key":"34e0bead544b70dfafeb59f125756a798adb39bb56a8d05b386f8e15bb50a2ad","model":"gpt-5.5","provider":"openai","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"ru","translated":"Свернуть рабочую область сеанса","updated_at":"2026-06-26T21:42:03.305Z"} +{"cache_key":"34f58bfc31e54bb7b5cacd2f417b0f7e3a35ebb8505ed32396c951c4afa2b26e","model":"gpt-5.5","provider":"openai","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"ru","translated":"QR-код сопряжения истек","updated_at":"2026-07-02T14:31:15.337Z"} {"cache_key":"34fb9c1f37f765c4db784ae053c1dfb20d079ebf30a138ce05721ae7cee6f748","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.usageOverTime","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Usage Over Time","text_hash":"c58fed4f5cb59cb8475b85914c1c7c8aed2321506c24303467a59cb44eaabe03","tgt_lang":"ru","translated":"Использование с течением времени","updated_at":"2026-06-26T21:41:23.762Z"} {"cache_key":"3569ef2bfc7e434d776e3283ee38acf5ea1c97987bec291d30ab2c632871a8f8","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.wakeMode","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Wake mode","text_hash":"0cdf77cce3335e6f2107f1f1fee1e34d7b105fd90a5b78e15f1a297dd4f89256","tgt_lang":"ru","translated":"Режим активации","updated_at":"2026-06-26T21:42:31.568Z"} {"cache_key":"356bf86286155680b307dde76fe5ca1463905ba2880665abb36083644be924ce","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.phrases.connectingDots","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"connecting distant dots…","text_hash":"167c47f1f6e5d7399326f6a72572cef9ab8cf655c4e17f4bf250e25f76478812","tgt_lang":"ru","translated":"соединение отдаленных точек…","updated_at":"2026-06-26T21:40:53.979Z"} @@ -430,6 +434,7 @@ {"cache_key":"4653d7c2c335a6b59baf0b85e51df2d631dcdd94745e85ad0302a712c709a8ad","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.startTalk","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Start Talk","text_hash":"d82bd5d6c334fe2bdb41712ad3eb3559680b9f4f27a9d22229a874bd6e4cb6e1","tgt_lang":"ru","translated":"Начать Talk","updated_at":"2026-06-26T21:41:59.944Z"} {"cache_key":"4663d7762a07d552756b98fc3f50245b9e6338d00b3ae9020a395f4630543042","model":"gpt-5.5","provider":"openai","segment_id":"cron.errors.agentMessageRequiredShort","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Agent message required.","text_hash":"d1709c155073bef73f53c7f372f797c41348e86bcb38d278a3cc3dfd8682f29b","tgt_lang":"ru","translated":"Требуется сообщение агента.","updated_at":"2026-06-26T21:42:51.343Z"} {"cache_key":"46665eb908225d7d6a8cfaa81515094e894e9550e1bad64d9f631694d99c66a0","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.reasoning","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Reasoning","text_hash":"d8211e24e83d1600a1b0cfe2f7baa68e4d4eb71131a0b2b1b2050cba111ea481","tgt_lang":"ru","translated":"Рассуждение","updated_at":"2026-06-26T21:38:54.164Z"} +{"cache_key":"466bc776ec43d3e98b0e7723f692362e5fdcdc017af114a35be03140ce20e3ff","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"ru","translated":"Закрепить сеанс","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"467c7a5c08e790c5525225102261836db6591e9ebb146061a0b85ffff596b89a","model":"gpt-5.5","provider":"openai","segment_id":"agents.files.closePreview","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Close preview","text_hash":"7d8ab368210c5ae8d2cec7bb577afe1e7cf9489c88f031e0f9de7555c9f20b66","tgt_lang":"ru","translated":"Закрыть предпросмотр","updated_at":"2026-06-26T21:39:11.166Z"} {"cache_key":"468980531154aa5de0e19eeafc2c0bb8fe868103378afb3dc1eb997566eb6bc7","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.advanced.promotedTitle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Recent Promotions","text_hash":"85051af6bfc0dd7be0988540e19a83f9855e93be2642c8b39a3d9a352ede92ff","tgt_lang":"ru","translated":"Недавние повышения","updated_at":"2026-06-26T21:40:44.743Z"} {"cache_key":"469ba482eec99c1d75abd3d99c62b4e28109d84dd7e942e7b1ee225b70828333","model":"gpt-5.5","provider":"openai","segment_id":"cron.errors.agentMessageRequired","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Agent message is required.","text_hash":"499060a1c91b80f430d179f155fde32729f817fe998fa3e378812bff577cb009","tgt_lang":"ru","translated":"Требуется сообщение агента.","updated_at":"2026-06-26T21:42:51.343Z"} @@ -440,6 +445,7 @@ {"cache_key":"478b46acba1f979fd5a007f5d29a304adc39f29fe518e2f4a2ec32c2463ccf65","model":"gpt-5.5","provider":"openai","segment_id":"usage.query.matching","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"{shown} of {total} sessions match","text_hash":"083883e7e8242df6bfca399e168ab9e7f86e05b26fd26f59fc8e2f98366a5d06","tgt_lang":"ru","translated":"Совпадает сеансов: {shown} из {total}","updated_at":"2026-06-26T21:41:03.285Z"} {"cache_key":"487afb3b4a58b8473e5b223e8147e0d1b54e66d7261314825c4cb72e12c60f4a","model":"gpt-5.5","provider":"openai","segment_id":"execApproval.labels.security","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Security","text_hash":"8f6fb4eb7f42c0e245e29e63f5b82cc3ba19852681d1ed9aed291f59cf75ec0e","tgt_lang":"ru","translated":"Безопасность","updated_at":"2026-06-26T21:39:20.621Z"} {"cache_key":"4964a69fca8d6848181a5bbf56cb4cc5ef41bcd26e4ddf31242b0a4b04e942de","model":"gpt-5.5","provider":"openai","segment_id":"logsView.truncated","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Log output truncated; showing latest chunk.","text_hash":"54cc74a976c9d17c5565753bb89162ee78bc5a028eb501586cfb443d9b54a794","tgt_lang":"ru","translated":"Вывод лога усечен; показан последний фрагмент.","updated_at":"2026-06-26T21:39:38.402Z"} +{"cache_key":"496c1373e6989c30a5233381a9816ddd48afc97347115b5439a661b558119018","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"ru","translated":"Переименовать сеанс","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"498d60745e5596ef3824d4f3867eef775d0ca1e422c4a003c53091069ee66069","model":"gpt-5.5","provider":"openai","segment_id":"overview.connection.authDocsLink","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Docs: Control UI auth","text_hash":"2643725608b446e5c8c6810cb458b90d6d2d78437927acc96aa229615b2da336","tgt_lang":"ru","translated":"Документация: аутентификация Control UI","updated_at":"2026-06-26T21:40:28.260Z"} {"cache_key":"49a45c4377208f93cfd6dee996bbd4782358ebad6c342a731128eec9113fb05d","model":"gpt-5.5","provider":"openai","segment_id":"channels.nostr.advanced","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Advanced","text_hash":"9f088dbebd6c3c70a5ddbc2c943b11e4ca9acea5757b0b4f2b32479f0dbb747e","tgt_lang":"ru","translated":"Расширенные","updated_at":"2026-06-26T21:38:40.558Z"} {"cache_key":"49bbf4a927842f5148d3723af2295a932abd13511ccf29c518ebc57f017b6544","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobList.selectJob","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"(select a job)","text_hash":"9a488dc0fea9e045777355a2d0c54a2fd6d9e2ced2411770933824a7d8ccfd07","tgt_lang":"ru","translated":"(выберите задание)","updated_at":"2026-06-26T21:42:44.768Z"} @@ -485,6 +491,7 @@ {"cache_key":"5062ee9df93bd064640ce47aa7b9660985d74da1ada67d44b2f87fcb4920ad4a","model":"gpt-5.5","provider":"openai","segment_id":"workboard.status.triage","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Triage","text_hash":"4ffbef3c08edfa3878c8357dbe3de3f11c3c74c594d8405f40243390f7f2d118","tgt_lang":"ru","translated":"Разбор","updated_at":"2026-06-26T21:39:38.402Z"} {"cache_key":"50f30afaf27c1e3a6b79d1703e6ff179b693ed53e51cac31e5a161f67aedcf4b","model":"gpt-5.5","provider":"openai","segment_id":"common.importFromRelays","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Import from Relays","text_hash":"b6a7b8934731285270b7f1671978dc0fc3147998f52405b2cc418eb4927bfc99","tgt_lang":"ru","translated":"Импортировать из Relays","updated_at":"2026-06-26T21:38:34.337Z"} {"cache_key":"511f7369e4c2d3921db1acfd66cb7433614530acc61ffc310b18707dbf46a8f6","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobDetail.system","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"System","text_hash":"6725e7bbcd28f3a8a586fa34bf191fd72dde8b61756932cd3237c17a6f196f1a","tgt_lang":"ru","translated":"Система","updated_at":"2026-06-26T21:42:47.554Z"} +{"cache_key":"51af6884a849ca22da640c9277dc9e375fc1b801d16930c195400e65a284b046","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"ru","translated":"Открепить сеанс","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"51c91d86efadde8dc7a996fcbd047af4dd29c3fbfbfb7838f873ad69614e4a3b","model":"gpt-5.5","provider":"openai","segment_id":"languages.ptBR","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Português (Brazilian Portuguese)","text_hash":"218d74650d53faa34f3263ebca533ed034422d1aec61d98ebd2ef353c0b9d492","tgt_lang":"ru","translated":"Português (бразильский португальский)","updated_at":"2026-06-26T21:42:06.902Z"} {"cache_key":"525871bfc635985172a59b24050a5a497062516f83a131c45a55ecc3d96aa960","model":"gpt-5.5","provider":"openai","segment_id":"activity.searchPlaceholder","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Filter by tool, summary, run, session","text_hash":"f8bd7464d3d4cfa89578618229d3d06859d4ddefe0647493b19f62a57940e658","tgt_lang":"ru","translated":"Фильтр по инструменту, сводке, запуску, сессии","updated_at":"2026-06-26T21:39:31.958Z"} {"cache_key":"528f5a7b2cdc59b4a0c2699a981cff56f3c9029c3b3c47d312bb4c23441c4b8d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.inherit","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"inherit","text_hash":"035300f3afee55ae79b77ca5bc61ff29fc3c7abb56f751bd524fa331b50d8ee0","tgt_lang":"ru","translated":"наследовать","updated_at":"2026-06-26T21:38:54.164Z"} @@ -562,6 +569,7 @@ {"cache_key":"60f9d2e8aa8414e5df6ecd0dc99cedccf7f19451bd09f8c97621771aa8031480","model":"gpt-5.5","provider":"openai","segment_id":"channels.health.noSnapshotYet","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"No snapshot yet.","text_hash":"3b578b0bf270913e649934e72f7ef6584ed56b1e10dc563b541384ff660bbfbc","tgt_lang":"ru","translated":"Снимков пока нет.","updated_at":"2026-06-26T21:38:34.337Z"} {"cache_key":"61074c253bc865b43cf2da4560ecb7e228536d53d86c528f6d78a4ef8b7e9cf7","model":"gpt-5.5","provider":"openai","segment_id":"overview.pairing.scopeUpgradeTitle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Scope upgrade pending approval.","text_hash":"01f51310417022d876b39bac2b047896b7a52e4be59e9ea7ce5416ae0c9010b3","tgt_lang":"ru","translated":"Расширение области доступа ожидает подтверждения.","updated_at":"2026-06-26T21:40:23.750Z"} {"cache_key":"615432233823e4648f8a4b39d2a72da42a0a35a0415afa6d823ef355b766f3b7","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.systemShort","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Sys","text_hash":"a34a3472060a7340185039557366a9dee34a3d929efabfbde16828e94d9b5924","tgt_lang":"ru","translated":"Сист.","updated_at":"2026-06-26T21:41:28.060Z"} +{"cache_key":"61fddba4b04c08b442b83e02669d2942a008d790efde944edc545b2b0ab92b4d","model":"gpt-5.5","provider":"openai","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"ru","translated":"Запустите /pair qr еще раз, чтобы создать новый код настройки.","updated_at":"2026-07-02T14:31:15.337Z"} {"cache_key":"62239f97ac9d7ceb5e20be1aee92fc75ab46750685aa1f15ab1d4a5d5f9f79d2","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.cronOption","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Cron","text_hash":"dd9d24965dbedc026915308732b77c1af68dcf52d3c0ca2421b1fdb0d197aca1","tgt_lang":"ru","translated":"Cron","updated_at":"2026-06-26T21:42:31.568Z"} {"cache_key":"622fe50951db268b10d2fcc0dc5493a6afde573e78366bd5f579f871bac966de","model":"gpt-5.5","provider":"openai","segment_id":"cron.runs.noMatching","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"No matching runs.","text_hash":"567dd6add9cc8e3c398162d00493ca9f17fcd61ca079c5d8650f02d3f8ee0410","tgt_lang":"ru","translated":"Нет подходящих запусков.","updated_at":"2026-06-26T21:42:24.729Z"} {"cache_key":"623007461742d991ffc811a36eaaa46333f8b2cbeb57fa6fe9c172a76e3cfb38","model":"gpt-5.5","provider":"openai","segment_id":"common.no","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"No","text_hash":"1ea442a134b2a184bd5d40104401f2a37fbc09ccf3f4bc9da161c6099be3691d","tgt_lang":"ru","translated":"Нет","updated_at":"2026-06-26T21:38:25.777Z"} @@ -818,6 +826,7 @@ {"cache_key":"8a98cbca75413f33bcbb603ad083999ec8e0c4658e2a51c3852f714e9ea8ac19","model":"gpt-5.5","provider":"openai","segment_id":"chat.onboardingDisabled","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Disabled during setup","text_hash":"9790a355d748c87f8c5497ffa7fd924d6b539bab8ff2a06d6f85dc7a3b4805f1","tgt_lang":"ru","translated":"Отключено во время настройки","updated_at":"2026-06-26T21:41:50.918Z"} {"cache_key":"8ac17f280731cf4c29e4fa8d055d2dc5120ddec4aa38467ef9c3610afbbb6174","model":"gpt-5.5","provider":"openai","segment_id":"common.probe","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Probe","text_hash":"3bd51ab9c14f9514ea37fac91f5f245e93cf5733bd39ca1652e5525a1d67b5d1","tgt_lang":"ru","translated":"Проверить","updated_at":"2026-06-26T21:38:25.777Z"} {"cache_key":"8ac694f6cbec5f2d587deb4f159be62f7806e4a21fc93de598ca7ffe1a5ffed3","model":"gpt-5.5","provider":"openai","segment_id":"overview.access.togglePasswordVisibility","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Toggle password visibility","text_hash":"1016c07b0f58d365790cc799fb215afd92fde1aeb5ac47cd17260e327465b2d6","tgt_lang":"ru","translated":"Переключить видимость пароля","updated_at":"2026-06-26T21:40:18.156Z"} +{"cache_key":"8b096b6eaf59ec57d3e5c55c6fb0f2fcecd4f7aa3985c25bbe8e8c96b71c29f7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"ru","translated":"Закреплено","updated_at":"2026-06-26T21:40:59.486Z"} {"cache_key":"8b283af9a4359dd4b55640b4e10f6c297c1ec6966cb8c6fc5b1632fc4f286441","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.sessionDetails","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Session details","text_hash":"caa57975e45173a0b92b1139e8c94c9ecb28ad7f1fd2bfd68247cdafefa61754","tgt_lang":"ru","translated":"Сведения о сеансе","updated_at":"2026-06-26T21:38:57.762Z"} {"cache_key":"8b76549434ac2b6acd125bf9d1db1dc256b66b25d10cb26558772e7307022961","model":"gpt-5.5","provider":"openai","segment_id":"tabs.communications","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Communications","text_hash":"919a92533fbe1d8129cc12e67ce06b13c83f1cc619b4e0b2088bbd2d4cc9583c","tgt_lang":"ru","translated":"Коммуникации","updated_at":"2026-06-26T21:39:23.906Z"} {"cache_key":"8b8cdfca48e2f5191254881f0f0e8578434f1cdec8dbe25db6056a735e52980a","model":"gpt-5.5","provider":"openai","segment_id":"usage.filters.title","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Filters","text_hash":"546ebb8eb993ea561029d9febd84c363bdb09010bb2cb915a8287762b76b9a64","tgt_lang":"ru","translated":"Фильтры","updated_at":"2026-06-26T21:40:59.486Z"} @@ -871,6 +880,7 @@ {"cache_key":"94e01138237d6dcf2f36aefebbe791fd4e18f9a52546186472963b1ce6c79e57","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.promptPlaceholder","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"e.g., Check my inbox for urgent emails and summarize them...","text_hash":"f4675787351dcf3b421c7f187fc9e501f37cb0a5ca79ddcde938cf99efe2dac1","tgt_lang":"ru","translated":"например, проверить мою почту на срочные письма и кратко их изложить...","updated_at":"2026-06-26T21:42:14.996Z"} {"cache_key":"94ffca768f547482015bff4d6a4d0936c5a93e5bbfcb1b24d64b5deb7aedc9cf","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.advanced.summaryWaiting","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"waiting","text_hash":"80cfa3e7f28dde4df64436b652230aff28d7779116d1369c21ef2bbf37261d71","tgt_lang":"ru","translated":"ожидает","updated_at":"2026-06-26T21:40:44.743Z"} {"cache_key":"950c57f25d73388437b62128167eb1bac4b31f4bc7fbe4c47bfd22ec785a94f1","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.selectSession","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Select session","text_hash":"803814693885dfb92ec8373f4c02f31015541e0e97e437287cdd0db681e0dae6","tgt_lang":"ru","translated":"Выбрать сеанс","updated_at":"2026-06-26T21:38:51.115Z"} +{"cache_key":"951f07004d59f67ec218df5d308d40eb794d43730c5333fb688266deb8454a8a","model":"gpt-5.5","provider":"openai","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"ru","translated":"Комментарии","updated_at":"2026-07-02T14:31:15.337Z"} {"cache_key":"954c8dc9e0b68ae6b825ac987e5c3cd8dd8405da6bf01e3b42dfc8c6ff560cbb","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.updateSubtitle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Update the selected scheduled job.","text_hash":"ed99ca1a9cd6abc6cef3c8ab9022ec162d7b7080c2fb4c5c9d3b58be2229c803","tgt_lang":"ru","translated":"Обновите выбранное запланированное задание.","updated_at":"2026-06-26T21:42:28.070Z"} {"cache_key":"9612b0a668adab8279904c95a7414df34832bb5d28f1236ae29ae19211d0b1e7","model":"gpt-5.5","provider":"openai","segment_id":"usage.overview.peakErrorHours","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Peak Error Hours","text_hash":"d549fec62ae3b5a839e25b808949b2cae7c3c55b558db510872616464028d103","tgt_lang":"ru","translated":"Часы пиковых ошибок","updated_at":"2026-06-26T21:41:20.199Z"} {"cache_key":"9636bb48736b76ec504761256e2358094486eb1757512528055effa22d3504c0","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.compaction","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Compaction","text_hash":"a0ade140bc8e408639e51492b949bc4d31641625ef070015b5d4a5e92ef0edb0","tgt_lang":"ru","translated":"Сжатие","updated_at":"2026-06-26T21:38:51.115Z"} @@ -933,6 +943,7 @@ {"cache_key":"9fb3bf4015bf1f9f43f42baef95690030003670477d27b2caf7fd26fe56f9916","model":"gpt-5.5","provider":"openai","segment_id":"usage.mosaic.fourAm","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"4am","text_hash":"c2a15a1684ec7e544681bcb5cc60f3c192fa87ed733d0a4b6b975db88724a9fb","tgt_lang":"ru","translated":"4:00","updated_at":"2026-06-26T21:41:31.698Z"} {"cache_key":"9ff4033241d74e761926a5464a179fedc75d6ed788af068e0c1942edc25e028b","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventCommentAdded","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Comment added","text_hash":"b474791acafe8d8b4796982afdcecd5cbf2492435fb6a5598069e6d8ff4230df","tgt_lang":"ru","translated":"Комментарий добавлен","updated_at":"2026-06-26T21:40:10.976Z"} {"cache_key":"9ff679553ae0b58c585b5a6631d5666996da43312368b70c1812bb815b5969ec","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.webhookUrl","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Webhook URL","text_hash":"84805a7574a82052bdd5b3b98119cfd838d04036ec4bd3d667a95698e7097ad6","tgt_lang":"ru","translated":"URL вебхука","updated_at":"2026-06-26T21:42:41.136Z"} +{"cache_key":"a004a0adf3ba082596a485737609ccf6896c228e3ea70f0bf6f017faeca5dba9","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"ru","translated":"Переименовать сеанс","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"a00e157791329f019988585d6c574984eced214c1abae0e50e8f4e1ceb85c2fa","model":"gpt-5.5","provider":"openai","segment_id":"overview.palette.items.sessions","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Sessions","text_hash":"6fa3cbf451b2a1d54159d42c3ea5ab8725b0c8620d831f8c1602676b38ab00e6","tgt_lang":"ru","translated":"Сеансы","updated_at":"2026-06-26T21:40:37.573Z"} {"cache_key":"a017bad16da84cdbecab0c1584038b36a170a014888ad744ed87b5ff69e4c8e7","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobs.emptyTitle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"No scheduled jobs yet.","text_hash":"1a9aea39a9d508aa68ec86002fb75bf6636358775b5a223e971471561dc5abc3","tgt_lang":"ru","translated":"Запланированных заданий пока нет.","updated_at":"2026-06-26T21:42:21.706Z"} {"cache_key":"a026bf201825eae5ac345608100d2515887b62b8a4effa24dd042d4d1903347d","model":"gpt-5.5","provider":"openai","segment_id":"overview.pairing.mobileHint","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"On mobile? Copy the full URL (including #token=...) from openclaw dashboard --no-open on your desktop.","text_hash":"643a873cbcaeb3d3b7482411636f4c1bb74140384acc1736313cf7d71de4b083","tgt_lang":"ru","translated":"На мобильном устройстве? Скопируйте полный URL (включая #token=...) из openclaw dashboard --no-open на настольном компьютере.","updated_at":"2026-06-26T21:40:23.750Z"} @@ -1294,6 +1305,7 @@ {"cache_key":"e8535e849dd059b083263bf8454864e1892ff7d7e0d3fe4a09348e4b6fa6f0ec","model":"gpt-5.5","provider":"openai","segment_id":"chat.welcome.suggestions.whatCanYouDo","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"What can you do?","text_hash":"2e5519b5b4943706022dc2fc66bf34a62e44b46edaa1af3dfd21b0ecb8dd5b23","tgt_lang":"ru","translated":"Что вы умеете?","updated_at":"2026-06-26T21:41:54.248Z"} {"cache_key":"e877221b431e5669f0f97c0349ad3354058d3b3a0d03757110910e8b7af03b37","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.activeTooltip","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Loads sessions updated in the last {count} minutes.","text_hash":"7e8c0a32d33d65b9fbfd6173971736fc74a247637ad1ead7d712c628c6758936","tgt_lang":"ru","translated":"Загружает сеансы, обновленные за последние {count} минут.","updated_at":"2026-06-26T21:38:51.115Z"} {"cache_key":"e8a9f3dd22ab94f75259c2eedcdb063650ec7b8437f18046049570efe116277f","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.clearAgentHelp","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Force this job to use the gateway default assistant.","text_hash":"8e78752a8dff28cb0975c91d2244c582d27030801018a7f0101e1c6b82e59c0b","tgt_lang":"ru","translated":"Принудительно использовать для этого задания ассистента шлюза по умолчанию.","updated_at":"2026-06-26T21:42:41.136Z"} +{"cache_key":"e8c5c31889e1a03429dbbbcfad6a1106636f47fefa58997a6735f4431310c6a9","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"ru","translated":"Восстановить сеанс","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"e8d248c10654093ade100a2f753a8310180736190a7e6cbcd9558668e8ab138c","model":"gpt-5.5","provider":"openai","segment_id":"cron.runs.scope","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Scope","text_hash":"b073f6c68ef8721107fd9815b19b2c35ec111d526b75c2123d1111ba64424000","tgt_lang":"ru","translated":"Область","updated_at":"2026-06-26T21:42:21.706Z"} {"cache_key":"e8e5b675bf2ca70cf366b76ef84ed5e7fb4c8e3752d09fc562c957abd0f68750","model":"gpt-5.5","provider":"openai","segment_id":"channels.nostr.account","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Account","text_hash":"7e1b0d5641f2640ce9a953ec231eea2c27a2a7633f7d3c273e5735e2b30c10b7","tgt_lang":"ru","translated":"Аккаунт","updated_at":"2026-06-26T21:38:40.558Z"} {"cache_key":"e8f258375267e55427da9b6da3062f49f338a0d3b7a2cc13f63dacbcb231534f","model":"gpt-5.5","provider":"openai","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"ru","translated":"Комфортная плотность карточек","updated_at":"2026-06-26T21:39:57.757Z"} @@ -1363,6 +1375,7 @@ {"cache_key":"f45a50be6ea1ee204ac2ac012154911be81e02cc430de9b7cda5e6f0d6bbb44b","model":"gpt-5.5","provider":"openai","segment_id":"usage.details.noDataInRange","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"No data in range","text_hash":"15ade27888fa80f7c32ce2563ad40035bcba81514dc431d2f6774d300a602647","tgt_lang":"ru","translated":"Нет данных в диапазоне","updated_at":"2026-06-26T21:41:23.762Z"} {"cache_key":"f45daf0d69b0c19fbc86d615ce506811ea5b972962cbac6d16451a60bc15e38e","model":"gpt-5.5","provider":"openai","segment_id":"usage.scope.instanceHint","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Show only the active session id for each logical session.","text_hash":"0a76b08d0a5201c80ac7ea92c073250bba81d0271232ce5e6c0297ada36598c9","tgt_lang":"ru","translated":"Показывать только идентификатор активного сеанса для каждого логического сеанса.","updated_at":"2026-06-26T21:40:56.975Z"} {"cache_key":"f45f1ae12bd4017e16d4abf7284ccef487108dec347f68364bd5a9cd6995ff1e","model":"gpt-5.5","provider":"openai","segment_id":"usage.breakdown.total","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Total","text_hash":"c9b3c38247f744e17dd26fda097d6a9ba9332586b6bdaa038bf8f313a863f2b8","tgt_lang":"ru","translated":"Итого","updated_at":"2026-06-26T21:41:08.400Z"} +{"cache_key":"f465a5d49d8f5075873dd8a11b1130ad24e793153e03ad1a9fe2e157ac86d1d4","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"ru","translated":"Только архивные","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"f495d2d3584660daca03ceb9d4c4d5dde327a8a20f12ef76573a3b4c762ee550","model":"gpt-5.5","provider":"openai","segment_id":"overview.notes.cronTitle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Cron reminders","text_hash":"b691bf454c30632ee7c03f2d9f3693ab0d165beffa1629a7db30cc09bcfe8591","tgt_lang":"ru","translated":"Напоминания Cron","updated_at":"2026-06-26T21:40:23.750Z"} {"cache_key":"f4fdaecae21e514479654f8b186f0f225c58cfcf36323ee295c3481e9b1fefa5","model":"gpt-5.5","provider":"openai","segment_id":"overview.palette.footer.close","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"close","text_hash":"310ff200149b44a32f124023d7caba19a1a890763a980606813d3a3d4a085d36","tgt_lang":"ru","translated":"закрыть","updated_at":"2026-06-26T21:40:37.573Z"} {"cache_key":"f581f7d9cbde7ff09bb3bb7c485f57a7fff1a1f0c3a35db8026523055c778805","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.resultDelivery","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Result delivery","text_hash":"5c3dc0d7b06d54b07b7e063a8cc675baf44327d6bcdbfac874c94700afbc887b","tgt_lang":"ru","translated":"Доставка результата","updated_at":"2026-06-26T21:42:36.583Z"} @@ -1370,6 +1383,7 @@ {"cache_key":"f688573008c31d07eaf24923ebff37fc26d2cbfcd18433e62abd653268f933cd","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.session","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"ru","translated":"Сеанс","updated_at":"2026-06-26T21:42:31.568Z"} {"cache_key":"f6a0f1d9267e68813031f100fa872809fbd285f2778951644634fcd257777ea2","model":"gpt-5.5","provider":"openai","segment_id":"overview.connection.tailscaleDocsLink","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Docs: Tailscale Serve","text_hash":"331a0eb6bb5691842d5881802ac7756e67763d8687f2ea5d96d4d33b06700c6e","tgt_lang":"ru","translated":"Документация: Tailscale Serve","updated_at":"2026-06-26T21:40:28.260Z"} {"cache_key":"f6be7f62993154c11549f82f8af601ec5004b2caf7703bb1742027cf9114895f","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"ru","translated":"{count} вложений","updated_at":"2026-06-26T21:40:03.947Z"} +{"cache_key":"f6f0af20e387a4508e41ce0184e4c530f1595437171c103a528d5baee01c329e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"ru","translated":"Показывать только архивные сеансы.","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"f703e8fce629a38f5ac491022891ed03c7422e33faa73c489733a6e43fae27a1","model":"gpt-5.5","provider":"openai","segment_id":"cron.runs.deliveryDelivered","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Delivered","text_hash":"906115657390f3675639f46a572eee069155214169a45be4046933527a95c67b","tgt_lang":"ru","translated":"Доставлено","updated_at":"2026-06-26T21:42:24.729Z"} {"cache_key":"f72b3b55f47809562a94330009b1955bce010cfb223b81a562315d1b9ba78fcb","model":"gpt-5.5","provider":"openai","segment_id":"agents.context.configurationSubtitle","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Workspace, identity, and model configuration.","text_hash":"0252834e99161cfe1a797fcf2d85e3f6ad97ae06fa937822e11e071275ed36f0","tgt_lang":"ru","translated":"Настройка рабочей области, удостоверения и модели.","updated_at":"2026-06-26T21:39:07.415Z"} {"cache_key":"f742d97678769dcac2b5e4ffdb1cc778ba4c96fca4a42b355aa9245cf9c5cbd8","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"ru","translated":"Оркестрация","updated_at":"2026-06-26T21:40:14.690Z"} @@ -1377,11 +1391,13 @@ {"cache_key":"f75ea1b486b3e5dc45cab28684bd03f8298513442aee916ae50718bea399d253","model":"gpt-5.5","provider":"openai","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"ru","translated":"Протокол worker","updated_at":"2026-06-26T21:39:47.132Z"} {"cache_key":"f769c0922980d92f7396bdde989aaf9e016dc3ce27287eafad9ec01f4d770639","model":"gpt-5.5","provider":"openai","segment_id":"common.docs","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Docs","text_hash":"7af023c43013b9a53fbff7dd4b5821588bba3319308878229740489152c43f6d","tgt_lang":"ru","translated":"Документация","updated_at":"2026-06-26T21:38:30.975Z"} {"cache_key":"f7b7e5f9c18bf6a89d4520aaa57a9fa6480ef7adbf27d7eee556129ec8d824de","model":"gpt-5.5","provider":"openai","segment_id":"agents.channels.empty","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"No channels found.","text_hash":"308bdae31be27cbdfcbfaaf785edcc9f949495f540a563ad4fcef7682d108f2c","tgt_lang":"ru","translated":"Каналы не найдены.","updated_at":"2026-06-26T21:39:07.415Z"} +{"cache_key":"f7e42abc1cc64b6d5f8afa9f28e011b8cff50f5fcf0e0a397a136735782652b8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"ru","translated":"Архивировать сеанс","updated_at":"2026-07-02T14:31:15.336Z"} {"cache_key":"f7f7bda338db1af1aafd27983e94948acb04094b1fa8253482196423dc41da9f","model":"gpt-5.5","provider":"openai","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"ru","translated":"Отправлять запросы на доработку в текущую сессию чата вместо сессии мастерской предложения.","updated_at":"2026-06-26T21:39:31.958Z"} {"cache_key":"f818c8d0368081cf4627a7495ea6b933a8578c91c1ee3e2e90c39d9fc10bfc09","model":"gpt-5.5","provider":"openai","segment_id":"agents.channels.setupGuide","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Setup guide","text_hash":"f91058b1dfbde985a500035ec38aab18acd48c844c391b58bc22593630c2a3ff","tgt_lang":"ru","translated":"Руководство по настройке","updated_at":"2026-06-26T21:39:07.415Z"} {"cache_key":"f86ce0fbee87217702d44a021e0d51c0a564fd15a0c4f1e43f2d669cc5077e35","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.defaultOption","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Default ({value})","text_hash":"a9d6571117890ef77ecc72f77ba43e9d8b05ed82c1c64ff27a352c02dff3c2bd","tgt_lang":"ru","translated":"По умолчанию ({value})","updated_at":"2026-06-26T21:38:54.164Z"} {"cache_key":"f86e511303df40c74db4a1e9ad9803c68716427fbdba55e442a64433d69eb5ad","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.filters","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Filters","text_hash":"546ebb8eb993ea561029d9febd84c363bdb09010bb2cb915a8287762b76b9a64","tgt_lang":"ru","translated":"Фильтры","updated_at":"2026-06-26T21:38:48.084Z"} {"cache_key":"f8d1664c037238e7b7966b8af30a892ceffd853ee093dcda14325c2e08271995","model":"gpt-5.5","provider":"openai","segment_id":"common.reloadConfig","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Reload Config","text_hash":"48e6315352561c36be84097326fbb3558b4c2fa3fc4f833402d32040ccb640f7","tgt_lang":"ru","translated":"Перезагрузить конфигурацию","updated_at":"2026-06-26T21:38:30.975Z"} +{"cache_key":"f916c281d5f241348f6f364e7942c224b9012e916e535e12186a3853b5147be1","model":"gpt-5.5","provider":"openai","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"ru","translated":"Истек","updated_at":"2026-07-02T14:31:15.337Z"} {"cache_key":"f91a236def82c3784f620f2d18efe01815c6935cb7811f86f6b4c3f54a3eeb46","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.scene.reset","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Reset","text_hash":"daee7606b339f3c339076fe2c9f372a3ff40c8ee896005d829c7481b64ca5303","tgt_lang":"ru","translated":"Сбросить","updated_at":"2026-06-26T21:40:41.209Z"} {"cache_key":"f992df98213e03e70f7be426db789681b79e23a3d33f1a5127456869a5bc61c5","model":"gpt-5.5","provider":"openai","segment_id":"channels.nostr.website","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Website","text_hash":"b5a229ac8becc6035511f432ca6018f581f0627233eada6ae8e12b505d44af7f","tgt_lang":"ru","translated":"Веб-сайт","updated_at":"2026-06-26T21:38:44.774Z"} {"cache_key":"f9b53639754e3c25d9e3a2cef7cd5f20bf1b77abc9341b0f8df95e540f82debe","model":"gpt-5.5","provider":"openai","segment_id":"usage.sessions.title","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Sessions","text_hash":"6fa3cbf451b2a1d54159d42c3ea5ab8725b0c8620d831f8c1602676b38ab00e6","tgt_lang":"ru","translated":"Сеансы","updated_at":"2026-06-26T21:41:20.199Z"} diff --git a/ui/src/i18n/.i18n/th.meta.json b/ui/src/i18n/.i18n/th.meta.json index e35b52601bf5..79afcef2223d 100644 --- a/ui/src/i18n/.i18n/th.meta.json +++ b/ui/src/i18n/.i18n/th.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:23:32.349Z", + "generatedAt": "2026-07-04T18:20:58.527Z", "locale": "th", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/th.tm.jsonl b/ui/src/i18n/.i18n/th.tm.jsonl index 82e0722b0958..a94df35d8c3d 100644 --- a/ui/src/i18n/.i18n/th.tm.jsonl +++ b/ui/src/i18n/.i18n/th.tm.jsonl @@ -1,5 +1,6 @@ {"cache_key":"00f71fe0137a44b95888cd9f932cc685365cde685f55dc2595b7f7d4378e7d4b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"th","translated":"ยังไม่มีไฟล์ที่แก้ไขในเซสชันนี้","updated_at":"2026-06-16T14:17:26.503Z"} {"cache_key":"029299cf3080b71633fc64e23b18841237d32425586b7641397c3ce840dd8d32","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"th","translated":"ยุบพื้นที่ทำงานของเซสชัน","updated_at":"2026-06-16T14:17:26.503Z"} +{"cache_key":"0416370cd59079f175bef46a208ff2e710e82732ed4e4325741f4a765afb3b25","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"th","translated":"เปลี่ยนชื่อเซสชัน","updated_at":"2026-07-02T14:30:49.122Z"} {"cache_key":"07f2d9fb15a802a2cfb871ebb4649d597039c451b8baa8b2e2731dafa050884d","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"th","translated":"เพิ่มไฟล์แนบแล้ว","updated_at":"2026-05-30T15:38:44.763Z"} {"cache_key":"0898d34ed3959d5ee042391a34a6896f5a7ff366e2960620197f13a4e596d248","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"th","translated":"คำขออุปกรณ์ที่รอการตรวจสอบ: {count}","updated_at":"2026-07-04T16:48:49.478Z"} {"cache_key":"0a0ec19cae45a3fcea7db6c2eac7bf5fb31baad7bba06854b23057b7386e3794","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"th","translated":"ยังไม่มีบันทึกจากผู้ดำเนินการ","updated_at":"2026-06-16T14:17:26.503Z"} @@ -8,6 +9,8 @@ {"cache_key":"0ec7ca78abd6c47bb1eab4547ebc27509a9b62c330e66608a915baa09104da65","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"th","translated":"พร้อม","updated_at":"2026-06-17T14:16:47.316Z"} {"cache_key":"12129c925e7e62bb39a3a851677ac36b6f55bc6fe39999fbf6db5a861274360a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"th","translated":"กำลังรอการพึ่งพา: {parents}","updated_at":"2026-06-16T14:17:26.503Z"} {"cache_key":"16c94329a7310d2030ffb1bb4cbc326d87dccebc4eae64382d101c49ce848188","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"th","translated":"หลักฐาน","updated_at":"2026-06-16T14:17:16.747Z"} +{"cache_key":"19e493e602e83c7045fea79ced8ade100c556c32f14060c1c09683cac4395730","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"th","translated":"เปลี่ยนชื่อเซสชัน","updated_at":"2026-07-02T14:30:49.122Z"} +{"cache_key":"1b19ada136dbf679849e1ca50a0b34184af700cd31f4f7ac3859001299d45e80","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"th","translated":"ปักหมุดแล้ว","updated_at":"2026-07-02T14:30:49.122Z"} {"cache_key":"1c70eb34ee0d5aa8e0cab6de75a4a21d638646a1d0dcdb6aff11dc54b95e6b11","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"th","translated":"ระบบอัตโนมัติ","updated_at":"2026-06-16T14:17:16.747Z"} {"cache_key":"1c72a541cca8ccaab24900c24ea71704faf5fd4be16984deda9db7b83ad81398","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"th","translated":"ขาดหลักฐาน","updated_at":"2026-06-17T14:16:47.316Z"} {"cache_key":"1d2a2fcee818cb09c7b8c325434d35a4851219cf0ef2e9659cb77a86e28a971a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"th","translated":"สรุป Workspace ของเซสชัน","updated_at":"2026-06-16T14:17:36.173Z"} @@ -39,17 +42,22 @@ {"cache_key":"458d5f79ca3c3a42dc4f3ff021a62bf5596e20dbfd565d92d5ff44c0ace06b97","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"th","translated":"ขาดหายไป","updated_at":"2026-06-16T14:17:36.173Z"} {"cache_key":"49fd0ac92802175ac5684421b46287b291f54f5a2d4d2f36681b92a8ce207041","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"th","translated":"5 วินาที","updated_at":"2026-06-17T14:16:55.187Z"} {"cache_key":"509defbafdfd12227921ae61d30ab8c09a8ba87ff32413c32bf6c1e098458449","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"th","translated":"คำอธิบาย","updated_at":"2026-07-01T01:08:34.109Z"} +{"cache_key":"50da449dd88713a25b6c2d14425e10eefd29052c0b63790fdec91aa3b20f4474","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"th","translated":"แสดงเฉพาะเซสชันที่เก็บถาวร","updated_at":"2026-07-02T14:30:49.122Z"} +{"cache_key":"52c56b26a75fb8de007b3c16d5b1a04948bec0828d07b49adb53aae99a38387f","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"th","translated":"ปักหมุดเซสชัน","updated_at":"2026-07-02T14:30:49.122Z"} {"cache_key":"53a1ada7ff279b0d93db641f2980a4de3dccfa92752cbfa74c1dd4943fdf165b","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"th","translated":"ไฟล์แนบ {count} รายการ","updated_at":"2026-05-30T15:38:44.763Z"} {"cache_key":"5a327590fef3c90c616f3dfaf750dbbb6c3587cb46806d6ec704d03ffdde28b7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"th","translated":"ใช้แชทปัจจุบันสำหรับคำขอแก้ไข","updated_at":"2026-06-16T14:17:16.747Z"} {"cache_key":"5ad0874d4b88cbec52942933ce469d2fd21f7a845e39319cd329b675581b5c3f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"th","translated":"อัปเดตแล้ว: {time}","updated_at":"2026-06-16T14:17:16.747Z"} {"cache_key":"5b710a8a4f245608cf816817d346b6dfffbea82184f82009ab4bb2eca46b6e21","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"th","translated":"ยังไม่ได้มอบหมาย (ใช้ {agent})","updated_at":"2026-06-17T14:16:47.316Z"} {"cache_key":"5c022ff710d6da649b919281c77ac09342249eff66327284c5c2063e2e2af66e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"th","translated":"ความพยายามที่ล้มเหลว","updated_at":"2026-06-17T14:16:55.187Z"} +{"cache_key":"5ddacc892c1409bd3ad5c981d296b785a911a616747b184ccf76995888165c09","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"th","translated":"เก็บเซสชันถาวร","updated_at":"2026-07-02T14:30:49.122Z"} {"cache_key":"5f8bfbd9a28aa730b16508c6024c2c21accdb27958503b83c4f9be0f22ae559b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"th","translated":"ค้นหาไฟล์","updated_at":"2026-06-16T14:17:36.173Z"} +{"cache_key":"60125fdafaa5b7b3486e7104b5511ca6e737b2709233bb86f1c5d58ac06376e1","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"th","translated":"กู้คืนเซสชัน","updated_at":"2026-07-02T14:30:49.122Z"} {"cache_key":"6215378cddd31803cb7f5c14d3949bf3b86e6e18085a0f354b9f34220ffa5b85","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"th","translated":"हिन्दी (ฮินดี)","updated_at":"2026-06-26T21:43:41.993Z"} {"cache_key":"64fcb939f341d1abad95e9042de94e4fd78efeea7a236741eea23776c13ecabb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"th","translated":"{count} การ์ด","updated_at":"2026-06-17T14:16:47.316Z"} {"cache_key":"69697ca507a7c1e06a7506278a9ef16d7f4d0eb99bdeb78463b2487960e16fe7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"th","translated":"เส้นทาง Workspace","updated_at":"2026-06-16T14:17:36.173Z"} {"cache_key":"6d49f61e8daf29818cae027ae36136a2ebff9dc4e819e22d841c22f70ffae4cf","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"th","translated":"เป้าหมาย","updated_at":"2026-05-29T21:02:14.030Z"} {"cache_key":"6d5f3bb7d2b885270a7256579ad859a29c02462ae2f7a08cf8100b2902f07f2d","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"th","translated":"การละเมิดโปรโตคอล","updated_at":"2026-05-30T15:38:44.763Z"} +{"cache_key":"6e29e91effb60554142d13abcbeb406fc720b094154bf69901f5cf9e0033ecdf","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"th","translated":"เลิกปักหมุดเซสชัน","updated_at":"2026-07-02T14:30:49.122Z"} {"cache_key":"6e3c71f59e1f2bfd6ee7b64891f24fca14e739d61a076bf99aab0f620442ee2c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"th","translated":"{agent} (ค่าเริ่มต้น)","updated_at":"2026-06-17T14:16:47.316Z"} {"cache_key":"7364137fa0c32929da15f23d85af9c84b275c87da596db00b4db550c6c12e02e","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"th","translated":"การประสานงาน","updated_at":"2026-05-30T15:38:44.763Z"} {"cache_key":"7478e0f3ad002416f61e2d76f6c6831fe43b9d2ccafec65fbef565d89bf0d650","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"th","translated":"แอป OpenClaw mobile อย่างเป็นทางการจะเชื่อมต่อโดยอัตโนมัติหลังจากสแกน","updated_at":"2026-07-04T16:48:49.478Z"} @@ -69,6 +77,7 @@ {"cache_key":"895b48e798824bb5d8dcbdd29e0927968872219708b704934fd0bedb074604af","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"th","translated":"จับคู่อุปกรณ์มือถือ","updated_at":"2026-07-04T16:48:49.478Z"} {"cache_key":"8c28cca12583fe8e9a65f7c4e24e0f416802f2f4861297449e382cc762e449ee","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"th","translated":"เซสชัน","updated_at":"2026-06-16T14:17:36.173Z"} {"cache_key":"8d184816aa95260f0602636f979b1633a86ee62c6698e62e2b3d729c8ccbd186","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"th","translated":"การพึ่งพา {count} รายการเสร็จสิ้นแล้ว","updated_at":"2026-06-16T14:17:26.503Z"} +{"cache_key":"8d7952ecaa05b146d5b5f4bfb05a18743c0b451e269d1c9a6624bcdf2f9799a8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"th","translated":"เฉพาะที่เก็บถาวร","updated_at":"2026-07-02T14:30:49.122Z"} {"cache_key":"8d7e346573a15fc2e0f31949b34b45f879d0b2ba7e7ec7a1c161fa3f433a3f36","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"th","translated":"สถานะ Workboard","updated_at":"2026-06-17T14:16:55.187Z"} {"cache_key":"9072842fe4b1539dcb033d38e139ad57bf8e3692314f555332f58543eab67312","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"th","translated":"60 วินาที","updated_at":"2026-06-17T14:16:55.187Z"} {"cache_key":"92f7fe101dab2f52e6d8cc16df664a2373244726ce0e622db420fd10e6c587a7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"th","translated":"กำลังทำงาน","updated_at":"2026-06-17T14:16:55.187Z"} @@ -79,6 +88,7 @@ {"cache_key":"98019030d7e9c92f10b9b592e18f46317a99118e18fd4f66fcf05ee9c8ed2ff6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"th","translated":"คำสั่ง","updated_at":"2026-06-16T14:17:40.285Z"} {"cache_key":"99760959266429519ed5d568c3e92bb3d86b4e46f6918c0566ac0d7f8af3fa60","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"th","translated":"Agent เริ่มต้น","updated_at":"2026-06-17T14:16:47.316Z"} {"cache_key":"9cdab3ee824fd3a1e972babdfb124801663f10ab689495030dbf74467a9fade2","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"th","translated":"Skill Workshop","updated_at":"2026-05-31T21:48:38.842Z"} +{"cache_key":"9d01f146ffd9569099c0a526049d63de8aca2ca80d2306cf64bed13a924800ec","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"th","translated":"กู้คืนเซสชันนี้เพื่อส่งข้อความ","updated_at":"2026-07-02T14:30:49.122Z"} {"cache_key":"9e3fe364d025a1cdc171e0c04c3efefe8736e11172389dc56f912858b748a2c2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"th","translated":"อัปเดตแล้ว","updated_at":"2026-06-16T14:17:16.747Z"} {"cache_key":"9f01af4efb3d6a9052f7b6b0637530c746e17c41776d73b315aa17efbe1aa7c9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"th","translated":"ไม่มีไฟล์ในโฟลเดอร์นี้","updated_at":"2026-06-16T14:17:36.173Z"} {"cache_key":"a22e9b5d43731fec0c7dcbcd982d4aacb9936cda33d4cbf8d2a01e7302a993fe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/th.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"th","translated":"พื้นที่ทำงาน","updated_at":"2026-06-16T14:17:26.503Z"} diff --git a/ui/src/i18n/.i18n/tr.meta.json b/ui/src/i18n/.i18n/tr.meta.json index 0447c37c65ef..551b1648a71b 100644 --- a/ui/src/i18n/.i18n/tr.meta.json +++ b/ui/src/i18n/.i18n/tr.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:22:45.612Z", + "generatedAt": "2026-07-04T18:20:57.729Z", "locale": "tr", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/tr.tm.jsonl b/ui/src/i18n/.i18n/tr.tm.jsonl index 2fc72b87e3f7..a29364a663d3 100644 --- a/ui/src/i18n/.i18n/tr.tm.jsonl +++ b/ui/src/i18n/.i18n/tr.tm.jsonl @@ -7,7 +7,7 @@ {"cache_key":"07c264746ea25b54900b320697ee4a23074083621609e3fcf326fdbe5fdb94f6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"tr","translated":"Yapılar","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"08370b21bf6293d2177080313901fa7cbf0e4f8a2acc0a00077473606a04ebe0","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"tr","translated":"Araç hatası","updated_at":"2026-05-31T06:44:00.618Z"} {"cache_key":"084b69e7056ef7e7626e7d6fb565317c9e3889288952ce6c7426abf50aaca7cb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyMissing","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{parent} (missing)","text_hash":"8daa419059727391c01e3b7021e05d8d70b4da67f9c57cd2d80f302af77aac53","tgt_lang":"tr","translated":"{parent} (eksik)","updated_at":"2026-06-16T14:15:44.904Z"} -{"cache_key":"0c8b21afa94b461ed4edfee00f719a6b609a64b54bcdabcdfce65b8ba588b06a","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"tr","translated":"Cihazları yönet","updated_at":"2026-07-04T16:47:22.584Z"} +{"cache_key":"1117d38f17ae930c76b31cf9aa8bf458eefe218c60f45d9635c89ab77723c101","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"tr","translated":"Oturumun sabitlemesini kaldır","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"114ab1d0416cafb81ef1d460c5b7a808a932c4ca481e69ecf4bf56780993342e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"tr","translated":"{count} hazır","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"130df49611dbb0acbb48d377657457fda7716dcbaece90d0bf65486dc81e1c1b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"tr","translated":"Bağımlılıklar","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"15db176f83390bf298f9cfb15ac2e8f561bb1dc4840da437bc7c4b8f2787821f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"tr","translated":"Oturum çalışma alanı","updated_at":"2026-06-16T14:15:44.904Z"} @@ -28,22 +28,19 @@ {"cache_key":"2f3b8cd23d05af407664a47c5b3184d976d97079839efdc2da399c424a4012e8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"tr","translated":"Kart ayrıntıları","updated_at":"2026-06-16T14:15:38.462Z"} {"cache_key":"2f46eda777bbefb9704a66f38d1f3f953e438aa2f91f5f4546d7d5d1b186593c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"tr","translated":"Üst klasör","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"3023faf202f2bbf68051b31882e4934981dc2eb2e43862bc81c0b3d5ff8562b0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"tr","translated":"Süresi doldu","updated_at":"2026-07-01T10:32:46.731Z"} -{"cache_key":"3036bead0ff93c9d4abc4c1ea29ad195730db64f2910d3a8e8d01774097c2853","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"tr","translated":"İnceleme","updated_at":"2026-06-17T14:15:34.754Z"} {"cache_key":"336528accf7135887cc90636ed3fe47e62ee31a3d45247ba8ad5b85c27b9dc91","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"tr","translated":"{count} ek","updated_at":"2026-05-30T15:38:34.866Z"} {"cache_key":"36be73e9fb370228e02836deff7764c817c047412649daf2a280d57c0d5c8c8b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"tr","translated":"Revizyon istekleri için mevcut sohbeti kullan","updated_at":"2026-06-16T14:15:38.462Z"} -{"cache_key":"3778857e75e093a717e11134f4c4e45c45dec728db7c3b0735edca0ec9244aa9","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"tr","translated":"Kurulum kodunu göster","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"37ed3377cd8af0cac4482d15591b5c0e3f9334a169acc801596266464a4a35df","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"tr","translated":"5sn","updated_at":"2026-06-17T14:15:40.711Z"} {"cache_key":"38931383e024c35cba9ed5349c17d08a16258b6673823a131c5db5c7a75707df","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"tr","translated":"Çalışma alanı","updated_at":"2026-06-16T14:15:44.904Z"} -{"cache_key":"38f25cb13e946ce082d3eb67cbb8b91ed646f21a015dbc66b958b71ca55ce777","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"tr","translated":"Resmi OpenClaw mobil uygulamaları taramadan sonra otomatik olarak bağlanır.","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"392a546b38f14051b5d086ce6e4c15371da5d5f2cdb6f78d0cad589345a41f52","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"tr","translated":"{count}sa","updated_at":"2026-06-17T14:15:40.711Z"} -{"cache_key":"3a0abbe1f01fabc7443fea33d820b04401fc638b03a7a9e3aebdb86b3c446934","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"tr","translated":"Mobil cihazı eşleştir","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"3b44764a589dc75297e399aa6c7b4bb7da5cb008c495f875fb569849df225ce4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"tr","translated":"Yapılandırılan varsayılan agent'a açıkça atanan kartlar.","updated_at":"2026-06-17T14:15:34.754Z"} {"cache_key":"3c52db77e1ac0d7dae803f505eb481e20445cf4ca5f8e18588ce7c62442580ca","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"tr","translated":"Pano: {board}","updated_at":"2026-06-16T14:15:38.462Z"} {"cache_key":"3d5c7a868f397abb9cd1d8beefe2943e5dc1b0e778d7223d66c1cccb208ae5fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"tr","translated":"Atanmamış ({agent} kullanılır)","updated_at":"2026-06-17T14:15:34.753Z"} {"cache_key":"3e1b0cb1fcae361de9c6b39f69da7c3affbc5b900cad6cd395d7d37f79ee7662","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"tr","translated":"Okundu","updated_at":"2026-06-16T14:15:55.679Z"} +{"cache_key":"401681284b43a2227310d254cf7f2b0dac1c14609336d32dcfb59ed92b75c247","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"tr","translated":"Yalnızca arşivlenenler","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"4086f53d892d97fec66f127b16a971d6e0e31a4da618fc49ed51dd53d3790ea6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"tr","translated":"30sn","updated_at":"2026-06-17T14:15:40.711Z"} -{"cache_key":"41817a599ecb01695dd035d24f86a21291a1dd80881b854f7edfe7169d81e00e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"tr","translated":"Güvenli bir kurulum kodu oluşturuluyor…","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"434c9c18245683962a47ba3184d0095a4f87a3321d5cdc38067050d9aee2dd35","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"tr","translated":"Kapalı","updated_at":"2026-06-17T14:15:34.754Z"} +{"cache_key":"43acb3047cb32a86fd855f4cf1b7aa100836d69fbd3584773aee3ee804af2cba","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"tr","translated":"Oturumu yeniden adlandır","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"43d707241e7e1419e6c5d077413015b0ba9c7da30b9cb26a20688794d5677bf6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"tr","translated":"İşçi günlükleri","updated_at":"2026-06-16T14:15:38.462Z"} {"cache_key":"45ece6358dcaf6287d91c195281607f6ef03a4f032ba169870afd6d77750023f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"tr","translated":"kalp atışı {age}","updated_at":"2026-06-17T14:15:40.711Z"} {"cache_key":"49600d3e11a0d4398c96cb495e14031dec519b26739de6d44a9b18bf9d8dadbc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"tr","translated":"Güncellendi: {time}","updated_at":"2026-06-16T14:15:38.462Z"} @@ -51,22 +48,19 @@ {"cache_key":"4beae23919439f69e2733c599a6546322d4d57814b2c386f8b9ec48884dbb9aa","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"tr","translated":"हिन्दी (Hintçe)","updated_at":"2026-06-26T21:43:35.090Z"} {"cache_key":"4c0dc3ff962e671322ac1a4f903a007a979d79b36b718e8900e3fa60c2e3afc4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"tr","translated":"Yeni bir kurulum kodu oluşturmak için /pair qr komutunu tekrar çalıştırın.","updated_at":"2026-07-01T10:32:46.731Z"} {"cache_key":"4c8422a0f909944c0263b23daa336086ddabe6a6793d56d47a353d7da22b1bb3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"tr","translated":"Görünümü, aramayı, önceliği, ajanı veya arşiv filtresini değiştirin.","updated_at":"2026-06-17T14:15:40.711Z"} -{"cache_key":"4cc8e78cdbfd03b4c9645ef1f242fb524575cc3a51c743a86230220e48ecec12","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"tr","translated":"QR kullanılamıyor. Bunun yerine kurulum kodunu kopyalayın.","updated_at":"2026-07-04T16:47:22.584Z"} -{"cache_key":"540ac61fa8a2577707cf6d95e8e3bb2b8d929dd5f3244d627dd9db4d91862d34","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"tr","translated":"İnceleme bekleyen cihaz istekleri: {count}","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"5421d84740de0531998f46c67fb63a826436afee47014f443e76670334afad34","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"tr","translated":"{agent} (yapılandırılmamış)","updated_at":"2026-06-17T14:15:34.754Z"} {"cache_key":"559d5ff10cecac26c56583816e401bd664cfbb22f8b3388e547fa90ca0f4077e","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"tr","translated":"Ek eklendi","updated_at":"2026-05-30T15:38:34.866Z"} +{"cache_key":"585af2b1bd102102de7266b295b202747618bd552d587a17389678ef5a16c5c4","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"tr","translated":"Yalnızca arşivlenen oturumları göster.","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"58a2a8240c897e2e58848ff53627786889b2c2594cc43305993797865e3f7956","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"tr","translated":"Oturum","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"5930cee5d49d05eb15f29e1e83a3af3829d750322cdbd0b8a81e90c433f939a6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"tr","translated":"hazır atanmamış","updated_at":"2026-06-17T14:15:40.711Z"} {"cache_key":"5da2baa863c9876d38f879d20305be9ca6ec5b45cda035c1955235ee77e81d17","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"tr","translated":"Bilinmiyor","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"5fdbc89d4308e28aa94e30e69d05d7b79dadf354922f5522b905070702f349fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"tr","translated":"Talk hatasını kapat","updated_at":"2026-06-16T14:15:44.904Z"} -{"cache_key":"609ffc00c6abf1641b2c75c8416427e9afcc842ab3cc12191628bd4fe112a289","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"tr","translated":"OpenClaw mobil","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"628ba89ce65fb39170d70bb9e9173c156ba9d577acab5f446258d7b44b16f002","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"tr","translated":"Varsayılan agent","updated_at":"2026-06-17T14:15:34.754Z"} {"cache_key":"63cf220dc15b0270a26980287a4d8cede4d094caff00474b97700434096f451c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"tr","translated":"Oturum çalışma alanı yükleniyor…","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"6521c8bf1076edb94c0756c62ae3d14e0a9b414a306ab1de34e85258a9756763","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"tr","translated":"Mevcut sohbeti kullan","updated_at":"2026-06-16T14:15:38.462Z"} {"cache_key":"658a5eca139f1df96184fef16455a4b71830beb9bbccce3154ed0172b4c527f4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"tr","translated":"Yenileme başarısız","updated_at":"2026-06-17T14:15:40.711Z"} {"cache_key":"6a0f14e1e99f863397a690666e302aa68762ab655547c2a652cc8092ee0f8f0f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"tr","translated":"kanıt eksik","updated_at":"2026-06-17T14:15:40.711Z"} {"cache_key":"6b0b169e30763403e3fb9000a88c80fb6a7c08b312372dd9d8d61b400aef3b8b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"tr","translated":"Çalışma panosu durumu","updated_at":"2026-06-17T14:15:40.711Z"} -{"cache_key":"6bc1669d42625c05abc166e0f6e941cbb694e15321dd012c47d9f5e1b4e45427","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"tr","translated":"OpenClaw mobil eşleştirme QR kodu","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"6d88c150bd277aaa537ae3055d71b0e223669d7a29523d84c684287de408a80f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"tr","translated":"Önizleme","updated_at":"2026-06-16T14:15:58.007Z"} {"cache_key":"6ecaf32bd6b038b29b99f4a5752a25ff239024274ab56573736f77975e413e97","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"tr","translated":"Bu oturumda henüz hiçbir dosyaya dokunulmadı","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"6ece2e2648fede9d3b6c310b32cad183b97eff6e8c30f760f6567a1d1213ddbb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"tr","translated":"Kanıt eksik","updated_at":"2026-06-17T14:15:34.754Z"} @@ -82,13 +76,14 @@ {"cache_key":"7b948c18323d4be1867dda751c9821948113e65a7495abb860de1546073c7086","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"tr","translated":"Oturum çalışma alanını genişlet","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"7e0f380e47bf1f155c1de859d51b0ae233ad0d38ccd077625c57179cd77b28c6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"tr","translated":"eski","updated_at":"2026-06-17T14:15:40.711Z"} {"cache_key":"7e1baab014c7c62c1043319fad1e2ddd77698cf70d26b39959de210a9f26cce2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"tr","translated":"Nihai yanıttan sonra yorumu koru","updated_at":"2026-07-01T01:07:50.766Z"} +{"cache_key":"7ffa8cfef54c3bdc90819fb8f0673de5edb5fa7af96f8c4b33de880903daa31e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"tr","translated":"Oturumu yeniden adlandır","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"830e86b81ff563467521c9eb63f3b28137906bceeef6d20d62b6c5c78efb4c07","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"tr","translated":"Eksik","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"872feda832616c9bd77f7722ba79c2517c23e1bedcdc94ea7b7b04938f63c2fd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"tr","translated":"Bu görünümle eşleşen kart yok","updated_at":"2026-06-17T14:15:40.711Z"} {"cache_key":"8825b9eff1f02c8960e38e68b2fb8630108648fba80bbf3b3e2151d9ac60f855","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"tr","translated":"{count} gösteriliyor","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"899366b1af448ceb0f1e556a61d9e4cffbdd0f4cc880761fe5e30d65405578ee","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"tr","translated":"Workboard görünümü","updated_at":"2026-06-17T14:15:34.754Z"} {"cache_key":"89dc18d52451cd19d966cefb92e113df76f096c54ef2c6c44a745639484261be","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"tr","translated":"Hedef","updated_at":"2026-05-29T21:01:25.865Z"} {"cache_key":"8ab7e2d4be2fe9c8ed09d5301995941af78d05be436cde23ec13e6f240582ea2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"tr","translated":"çalışıyor","updated_at":"2026-06-17T14:15:40.711Z"} -{"cache_key":"8e427146ca2209bbdd78f166ece455302a0ba18cb618f907d6bdfc3406810c33","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"tr","translated":"Kurulum kodunu kopyala","updated_at":"2026-07-04T16:47:22.584Z"} +{"cache_key":"8c9a788cba141728fe94dd1037da343bba80d14f04ccf0faf54f6ed2be1850f8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"tr","translated":"Oturumu geri yükle","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"8ff814b8050e54df90674b2fcc47c936da4031bdbd01279e668e9437f478fb71","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"tr","translated":"Proje dosyaları","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"9242a48fb9cd01ab01362cbd8906f959e075decc88c54b9553418e0e6ad4d73b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"tr","translated":"Bir karar, engel veya kanıt notu ekleyin...","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"9631d6dcffd86d7c5983627ad6062d794b2f493ae06634e7bb63c8563a0d514d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"tr","translated":"Revizyon isteklerini önerinin çalışma alanı oturumu yerine mevcut sohbet oturumuna gönder.","updated_at":"2026-06-16T14:15:38.462Z"} @@ -109,20 +104,21 @@ {"cache_key":"ab2830fe755f6834fb328183627a3f1005f663258f788e5c9f6444235eda1e42","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"tr","translated":"Oturum çalışma alanı özeti","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"ac4d586a485e0daa2f9f81350504b00c3ebff7558e928e325b0b8efb5a12f440","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"tr","translated":"{count} bağımlılık tamamlandı.","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"ae545453391754dc2f91e3c268f968cefab7d19c63288164fc90ba9b203f8919","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"tr","translated":"Çalışma alanı dosya işlemleri","updated_at":"2026-06-16T14:15:55.679Z"} -{"cache_key":"b552840ac1bd6a269a163fc97b65a56b6feed9be9592d3b61ac071a71eea9568","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"tr","translated":"Kurulum kodu oluşturulamadı.","updated_at":"2026-07-04T16:47:22.584Z"} +{"cache_key":"b0a1e2433c6758052f971414566fc03105ca1279a85dc17c1505c1c59f39b884","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"tr","translated":"Mesaj göndermek için bu oturumu geri yükleyin.","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"bc58bb9489ab7e42f0cb9853119310847cb29bbfb65f7fdd3ec56a8a8590dc42","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"tr","translated":"Değiştirildi","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"c20fa853de92ab1d3691d3e8a983247af05267463454e8fc0f2ca43ae751671d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"tr","translated":"Eşleşen ilk dosyalar gösteriliyor. Sonuçları daraltmak için aramayı hassaslaştırın.","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"c2906c6259d5dec700a0d97dd6c94b9b17a843d5019f01fbec95cc6440207423","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"tr","translated":"Sıkışık kart yoğunluğu","updated_at":"2026-06-17T14:15:34.754Z"} {"cache_key":"c8e5c11bc3b14b694aaaaee6256466a3078b4ec3153a30bb63f161a7ccaefea1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"tr","translated":"Engellendi","updated_at":"2026-06-17T14:15:34.754Z"} {"cache_key":"c98c03740098459302bd81344d19af43486479786bf07662face2a72434a8ace","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"tr","translated":"Arama sonuçları","updated_at":"2026-06-16T14:15:55.679Z"} +{"cache_key":"cb2062ad7c2713d59eb363a12521239e5267a72516c896509d017cb537b6daef","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"tr","translated":"Oturumu sabitle","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"cb393ab35f81444b9d33f8d6182a2fa053238e75a179a5d540c113774e694cd4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"tr","translated":"15sn","updated_at":"2026-06-17T14:15:40.711Z"} {"cache_key":"cb8098bc575ec3cb3804ac943e22bc642e6d49309d932a2b2bd84c3c91b9f645","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"tr","translated":"Henüz operatör notu yok.","updated_at":"2026-06-16T14:15:44.904Z"} -{"cache_key":"d840ae88d5082fa4f05f3a4dd9a598de3d04032b350a130a12ff9c5e76c3653b","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"tr","translated":"Kurulum kodları oluşturmak için yönetici erişimi gerekir.","updated_at":"2026-07-04T16:47:22.584Z"} +{"cache_key":"d3747bc35c921a3c10ffb58f269e16f5bed87b5f6b525f746ee73c42a9745d92","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"tr","translated":"Oturumu arşivle","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"dcfe3aedd94df7194b01dded7632ebf4ebd3c41ae2f269aa932b06ca3906a589","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"tr","translated":"Bağımlılıklar bekleniyor: {parents}.","updated_at":"2026-06-16T14:15:44.904Z"} {"cache_key":"dd0f888a71c1074fd1a430573e7ae9fc3593c9296ada9ddeb909ed2415ddaf46","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"tr","translated":"Komut","updated_at":"2026-06-16T14:15:58.007Z"} -{"cache_key":"de40dda251a4e7c629fec331b898d098474c3c5ef2025fc7a0e0ac876dd75e15","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"tr","translated":"Yeni kod","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"df28c20f2135ea21eb3e76bfe0230193c81d560a2ea0c63b84eb891e584434fc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"tr","translated":"Operatör notları","updated_at":"2026-06-16T14:15:38.462Z"} {"cache_key":"e19f3630c9be3dab26172388bd6487f95887e96adb6e491203a79efb20b28248","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"tr","translated":"engellendi","updated_at":"2026-06-17T14:15:40.711Z"} +{"cache_key":"e3ca75a603c24f83fb1f075167f718d00dbe2611757fe7a70369f537d9ef9f61","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"tr","translated":"Sabitlenmiş","updated_at":"2026-07-02T14:30:28.149Z"} {"cache_key":"e56806000a2d91a068122111f2e37f1053c82be101033d1f5a3a70300158c8c8","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"tr","translated":"Teklifleri canlı becerilere dönüşmeden önce gözden geçirin, iyileştirin ve uygulayın.","updated_at":"2026-05-31T21:48:30.961Z"} {"cache_key":"e57d32e76458f36c7b4b821a0a12afb3902b2ed61e5bf1bca504041d63cdcea2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"tr","translated":"{count} yapı","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"e6d955856bb891dda15fefea2ce840f83e6e51a19e94c302d6ead0876b2599e5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"tr","translated":"Otomasyon","updated_at":"2026-06-16T14:15:38.462Z"} @@ -130,9 +126,7 @@ {"cache_key":"e9c5c600012848585c8da32f5d5d2b5f30dc53ae156fa6f327f91f710c3e64d7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"tr","translated":"Kiracı: {tenant}","updated_at":"2026-06-16T14:15:38.462Z"} {"cache_key":"ef076ed851ac04302da699a1cb853733152b535c91f035429a64dcbf6a7f0d9e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"tr","translated":"Kök","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"f0aa575b9c8b229574dd918c574821d2cf1b52f60f014b88c2a556ddd3538639","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"tr","translated":"MCP","updated_at":"2026-05-31T05:36:46.292Z"} -{"cache_key":"f0b0e573d9a8daef2bc45e203c56d0079bb72588ab34797ec297adcf05905fd3","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"tr","translated":"Yeni bir telefonu bağlamak için mobil uygulamada bu QR kodunu tarayın.","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"f4d469d442c1143c4f9b5aba43306949ea1b48c4bf9f54750cf8919fa27b6db6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"tr","translated":"Çalışıyor","updated_at":"2026-06-17T14:15:34.754Z"} -{"cache_key":"f7724a4b7230ee6c6be954e89ec3a3e8b7cb6344227d3e57d0688504844582fc","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"tr","translated":"Eşleştirme yardımı","updated_at":"2026-07-04T16:47:22.584Z"} {"cache_key":"f873120ccd2393e18cb9b0d7011697a61d22139abfb2310f0a25a33c9daab421","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"tr","translated":"Hazır","updated_at":"2026-06-17T14:15:34.754Z"} {"cache_key":"f9ed2d7b6191ac88c862c30f763ea14d327735f11d666a546f47373978962203","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"tr","translated":"Bu klasörde dosya yok.","updated_at":"2026-06-16T14:15:55.679Z"} {"cache_key":"fd0999f79fb45a2011c1467e97048285e4fc608c7a8b4fc6d974f94ab81b25ba","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/tr.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"tr","translated":"Eşleşen dosya yok.","updated_at":"2026-06-16T14:15:55.679Z"} diff --git a/ui/src/i18n/.i18n/uk.meta.json b/ui/src/i18n/.i18n/uk.meta.json index 0e28818f79a0..03152ac2388b 100644 --- a/ui/src/i18n/.i18n/uk.meta.json +++ b/ui/src/i18n/.i18n/uk.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:22:54.058Z", + "generatedAt": "2026-07-04T18:20:57.942Z", "locale": "uk", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/uk.tm.jsonl b/ui/src/i18n/.i18n/uk.tm.jsonl index daab24c1d76e..edf479abbdc5 100644 --- a/ui/src/i18n/.i18n/uk.tm.jsonl +++ b/ui/src/i18n/.i18n/uk.tm.jsonl @@ -1,7 +1,6 @@ {"cache_key":"00edd8e52f20a670371b24130da387606cee9a2d20c543a34da2bb9cc7609442","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"uk","translated":"Артефактів: {count}","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"01a5e8d0d9237eb23d13f3103bf590502172eef0ad7dab6f423f105d7d97fbcb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"uk","translated":"Прочитано: {count}","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"031895a60725568d82c3204b7673038bc53c2aa706eff8cac78f3e378be405e9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"uk","translated":"Зручна щільність карток","updated_at":"2026-06-17T14:15:28.579Z"} -{"cache_key":"03b0f3c8c7c739ea2c20ce634be6f889aee26933d139c4d1c490b10eb0ec4dda","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"uk","translated":"Створення безпечного коду налаштування…","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"066be83c24912e9deb86d673d64400afa7cd6fd89daf79700d8f747bef1c711b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"uk","translated":"Попередній перегляд","updated_at":"2026-06-16T14:16:03.477Z"} {"cache_key":"07add817afa8bb6340995808db4ad538b3d64267268ee506371ffe3cb9e718f1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"uk","translated":"Невідомо","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"09294813dbb1acf17b1c08fd8a25144030b41a659e16b48be549cfa8ed083ea5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"uk","translated":"Батьківська папка","updated_at":"2026-06-16T14:16:00.845Z"} @@ -20,26 +19,22 @@ {"cache_key":"20ca1c9d9f6087c562e62ca9482bcd389c9fdb75e8354992ede846966427982e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"uk","translated":"30 с","updated_at":"2026-06-17T14:15:34.049Z"} {"cache_key":"23fa5440c24c70faf78beea4be5dcab0f6259ad3e52553e23142294d993f2cdd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"uk","translated":"готово без виконавця","updated_at":"2026-06-17T14:15:34.049Z"} {"cache_key":"2519012c0cea071166893e4c6a972adf3275520ef6a57663eda83c311e650952","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"uk","translated":"{count} готово","updated_at":"2026-06-16T14:15:46.702Z"} -{"cache_key":"25eab5799c1fb128ed6bef046723269aaaaeb6156a0096769ab01b436323c52e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"uk","translated":"QR-код недоступний. Натомість скопіюйте код налаштування.","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"27e10e346e943b7426ee7ffd41d8fb351336bbe77546ebfaed88c94ef9dacf79","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"uk","translated":"Завдання Gateway","updated_at":"2026-06-16T14:15:40.204Z"} -{"cache_key":"2894c5361845984d8f71a70d73da0736a1c08c2c4cc8f0ad8aac69079f1c6704","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"uk","translated":"OpenClaw mobile","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"28d3b2dca887a72ef38765db37d00b8f657acb490d9bebf81cd915eb6f94fe16","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"uk","translated":"Додайте рішення, перешкоду або нотатку-доказ...","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"2a64a67355d3b55752bc3391b1669cb51d4e4fd2be99da9ad911de7d0d12eb1c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"uk","translated":"Автоматизація","updated_at":"2026-06-16T14:15:40.204Z"} +{"cache_key":"2a90d3e31e0c3febb2ef62202807f1bc84c73ce0e36ccc81f899b3d92dad37cf","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"uk","translated":"Закріпити сеанс","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"2d7406c2df89c36b38884f459eec6fa58c4ffca86a0b98344a592e4d39991791","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"uk","translated":"Надсилати запити на перегляд до поточної сесії чату замість сесії воркшопу пропозиції.","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"2db75831425c5fa5c029ea9a0faae90e8c3257d8716e16fdc4edaafbd3d0f0f0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"uk","translated":"Застарів","updated_at":"2026-07-01T10:32:49.580Z"} {"cache_key":"2f3b2059127889733c5a1fed32a4d537aa95fd3bee640678f55b1919405ecf31","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"uk","translated":"Змінено","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"2fe98f28b9ba5b9bf164632812bd362f25b3012e277f5737c642acf1cabad821","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.copyPath","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Copy path","text_hash":"720ff4160412b943370afdb8fc1c082ff057d54713d5fb4b4b7a9634bfabf5fe","tgt_lang":"uk","translated":"Копіювати шлях","updated_at":"2026-06-16T14:16:03.477Z"} {"cache_key":"30645e5290c1bae02b4ceabee0577b43d6e2d8921e006059098862b3ed086c6c","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"uk","translated":"Русский (російська)","updated_at":"2026-06-26T21:43:36.497Z"} -{"cache_key":"31fa453b2eb1180ceefa7a0c440c3c8f74d9771923dd9bfaaf7c0fd246454273","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"uk","translated":"Керувати пристроями","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"32ac49cd2e946a825fd35e17566263c6dce47551b1415842aebe036512c3baeb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"uk","translated":"{count} заблоковано","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"334d8465ffb1a5486a7aba4247d69f29ea77d0f6fc99c3bfd4d339e8cff19277","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"uk","translated":"{count} вкладень","updated_at":"2026-05-30T15:38:37.442Z"} -{"cache_key":"34ec2a7ee85bfd890ea3431e75722ca2ccac4a2e67657ab3ee330925383861c9","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"uk","translated":"Для створення кодів налаштування потрібен доступ адміністратора.","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"37451377e4cfea26a58ec80fed40c15b4e25346efd10c586bb460360a6de3acb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"uk","translated":"Показано перші відповідні файли. Уточніть пошук, щоб звузити результати.","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"3a51abedec6222bb386c77edeadcc2f894d07496bf67556a2727a728d5a76784","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"uk","translated":"Робочий простір: {workspace}","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"3b1a5b306e171d47cd6b9b4e3e0ba01abf56376b85fb5864b975229f7df31372","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"uk","translated":"Показано: {count}","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"3b320ebf5e479af151c73e241fc8a5bac858a01a1d0d5ae589b8243cf991ca00","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"uk","translated":"{count} год","updated_at":"2026-06-17T14:15:34.049Z"} {"cache_key":"3b6b69c1870ecbaa1622f3bf8eef5b6f59e2be19a15924d74504a0cf9c73f204","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"uk","translated":"15 с","updated_at":"2026-06-17T14:15:34.049Z"} -{"cache_key":"3bfbb871d94e325f3cc479f7cd4b8870584380db21e305f5bf8fb1f406e3fb8e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"uk","translated":"Показати код налаштування","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"3c652bf117dee819aaa82a407cd06b9c182e1602e2198cd4e373d6a15663b434","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"uk","translated":"Змініть подання, пошук, пріоритет, агента або фільтр архіву.","updated_at":"2026-06-17T14:15:34.049Z"} {"cache_key":"41e02022b2a32b13928ee4f8490b6a25b1399bae6b3fb25601bd0bb5e84e5e64","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"uk","translated":"Залежностей виконано: {count}.","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"41f75b441d2c989f97d18602a39c0675106c0c561e17d3fb97f657e896679814","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"uk","translated":"Додати нотатку","updated_at":"2026-06-16T14:15:46.702Z"} @@ -56,12 +51,10 @@ {"cache_key":"51a639e1f8ff509f668d9ecbad395b86eddbc7dceaffbe9c8853be038c6d1a41","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"uk","translated":"Орендар: {tenant}","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"54376ca33bf3762d6589c3937cbaad3bec8419b6b3f962a991bf3f5a26263e41","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"uk","translated":"{agent} (за замовчуванням)","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"54a989e93e132964763649ad99a2597ec88a2e599521a689f5ed11c5177596b9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"uk","translated":"Сесія","updated_at":"2026-06-16T14:15:46.702Z"} -{"cache_key":"57b309bebb18c2883713a19f2a09d216ff20c8d9b3a907cf3c029e7860c805ce","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"uk","translated":"QR-код сполучення OpenClaw mobile","updated_at":"2026-07-04T16:47:34.961Z"} -{"cache_key":"5880925d0bfe97bea380f0db6cea8bc8f4015d7dbdbddedd29366565073fb52a","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"uk","translated":"Довідка щодо сполучення","updated_at":"2026-07-04T16:47:34.961Z"} +{"cache_key":"57603e7ac7b88a5ac759867f20139904deb2b569d9e06840176b93bc589c003a","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"uk","translated":"Закріплено","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"59f2fc8d806554f713380760a9c50315f61ce34a382b51803adab5e475e2f36b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"uk","translated":"Перегляд","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"5a941fd700a454d3619c127a2729eea201880488b8b2400275ab6949bbace71f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"uk","translated":"Жодна картка не відповідає цьому поданню","updated_at":"2026-06-17T14:15:34.049Z"} {"cache_key":"5cea58e2ef58e31febcfa16d55dbee9226ddedecc2612a4217ae148c159dd4b0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"uk","translated":"Виконується","updated_at":"2026-06-17T14:15:28.579Z"} -{"cache_key":"610871a7be003d467045e08fa22bdaa921f12bb5e19ad105ae7667fba8a8f818","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"uk","translated":"Скопіювати код налаштування","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"62f234669bbe6ccb59ee316fbb10772c2b7696fc3c7d6e3ebd8436c24c03648a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"uk","translated":"Запуск","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"63e2026f39986f216a8604a8fb3f0af22487e6341b375f17fce83a3b740b7f16","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"uk","translated":"{agent} (не налаштовано)","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"669ec2776a910df0e2864c77588015f49e983aec710f8b71b5541f114d3a858c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"uk","translated":"Переглянути деталі","updated_at":"2026-06-16T14:15:40.204Z"} @@ -69,8 +62,6 @@ {"cache_key":"6b12d258b8368a58d2a4a3c79c017c428bdafafac61ab86df2281d4b6f1676f5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"uk","translated":"Картки без явно вказаного агента.","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"6dcf64414685c3402c8c72b45c0191347e7ae29d31b1a87f71499f513e6070d2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"uk","translated":"Діагностика","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"6deb510045453ccb641e6f4424ba19d115e46e2f3e9ae4e8231d329a0899629a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"uk","translated":"Нотатки оператора","updated_at":"2026-06-16T14:15:40.204Z"} -{"cache_key":"701b7aa71959bdcd1da0f02c30194bec92db60b5beb9f24410e8d8782b2a1da5","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"uk","translated":"Запити пристроїв, що очікують на перегляд: {count}","updated_at":"2026-07-04T16:47:34.961Z"} -{"cache_key":"712c9a92386ec25ac03add13606771f6e59ce04bbdd5a8d5b4a13e25bbfa8dc4","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"uk","translated":"Новий код","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"71dfca605cd5cdb7d994f40e7ef8f98e2c8f19c754744f8e971e89cd324654cb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"uk","translated":"Змінено: {count}","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"7244fa211e75153821b796f18200426d60a930f9ace8558d300ddbd3e3bc6895","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"uk","translated":"Файли проєкту","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"725a64f4ee6bce0f9a75159ddf9335f2ccbb78c7868fe68c048016d95cebef67","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"uk","translated":"Шлях до робочого простору","updated_at":"2026-06-16T14:16:00.845Z"} @@ -89,23 +80,25 @@ {"cache_key":"90fd997d151da52e48e0c7557a34336ccf9e357592c8cab9933f781172590f47","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"uk","translated":"Коментар","updated_at":"2026-07-01T01:07:59.455Z"} {"cache_key":"993354ed03232adab1033ad5392b8d53265d914356478f578eddfeebb0415391","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"uk","translated":"Підтвердження","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"a30cdf03bb03e6ba41ceceb31762ab6ffed0a93212ef15dd8a262ce60f06e7bd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"uk","translated":"60 с","updated_at":"2026-06-17T14:15:34.049Z"} -{"cache_key":"a62eeec8b09ecdd1a10658dca4c5eb5c7f71e188108f4f7e9abd26f9bd15f4f2","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"uk","translated":"Не вдалося створити код налаштування.","updated_at":"2026-07-04T16:47:34.961Z"} +{"cache_key":"a4bd9b98e64254c411c083ac52654b0c5a466de108351420e2ba205a16996e64","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"uk","translated":"Архівувати сеанс","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"a791027ff4c63da58580abdd6865d2955a927dc8cd3333bf944a1e04e4c88139","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"uk","translated":"Компактна щільність карток","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"a7d851a926019e2bd61845a835cdb17a9a048573abc8e000276b4c4f79fe979d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"uk","translated":"Мета","updated_at":"2026-05-29T21:01:33.548Z"} {"cache_key":"a90eb8914ad74d8b449bde19d55b6ac2d26718ee75a54c8da96d5d2e5dbd2757","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"uk","translated":"Корінь","updated_at":"2026-06-16T14:16:00.845Z"} +{"cache_key":"a9d74ea22575ac277d779a75149afb183a4d731888292a3f97d1701b1a80a39e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"uk","translated":"Відкріпити сеанс","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"abcd8c3a24da3712183372978ec54a923d2a8c384df74e3b0af539e134ce86bc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"uk","translated":"Оновлено {time}","updated_at":"2026-06-17T14:15:34.049Z"} {"cache_key":"acba8d9c1643ca66175db2286579a38038816b594d68b5a3d571b5f08b40e591","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"uk","translated":"Деталі картки","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"acd419d5af80b6af11673076b770f0794e2749eceb6deacff8fbbf3d22187182","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"uk","translated":"Відсутня","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"ae5513cbe7150365efd5a0a4dbe32f9bc2071d61305998a26fccd44fe260ec72","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.skillWorkshop","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Review, refine, and apply proposals before they become live skills.","text_hash":"f907c72e9f18a205027257cd6fecdd52b03732227a17dcec0db038e11de3f8cc","tgt_lang":"uk","translated":"Переглядайте, уточнюйте й застосовуйте пропозиції, перш ніж вони стануть активними Skills.","updated_at":"2026-05-31T21:48:33.168Z"} {"cache_key":"b254ce53482c927fceaa5579001975d2af9fb778b7dbaa15cfc66755df6f4eb8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"uk","translated":"Пошук файлів","updated_at":"2026-06-16T14:16:00.845Z"} +{"cache_key":"b2826d8888a5d1e9e359144e46ae15ddaf465090f7bda8577af7c86692ade127","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"uk","translated":"Показувати лише архівовані сеанси.","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"b347d8bd8341e11ac35c037dc25c9c5d132d567bf96da2fa3fbc9c099ec5860e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"uk","translated":"сигнал {age}","updated_at":"2026-06-17T14:15:34.049Z"} {"cache_key":"b4811e5fa17d0761d1e3db804b1d5e926c86a048d16d24de67a19aa30a5a0b8f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"uk","translated":"Протокол воркера","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"b548bf989175808afced3647ca6e1dd4ae98d7a73f9d74cacf7d34d81cbb6d3f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"uk","translated":"Залежності","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"b6095799115b413d4a9177fa960ba36a7fefda5bbb3688977edebe009ab8cb43","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"uk","translated":"невдалі спроби","updated_at":"2026-06-17T14:15:34.049Z"} {"cache_key":"b74912e5d6ed30c3cec1993ab7420b469f53d63415e33574719987c10a6c3948","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"uk","translated":"Зберігати коментар після остаточної відповіді","updated_at":"2026-07-01T01:07:59.454Z"} {"cache_key":"bab660bc64e7a08a01caff0fe11083689684c8dffed0213b8e79b04ce29ca124","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"uk","translated":"Порушення протоколу","updated_at":"2026-05-30T15:38:37.442Z"} -{"cache_key":"bf193435c040bf020d65f0dfbe1cbedb54d3309d845581e6f0fac8a389e22b1b","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"uk","translated":"Під’єднати мобільний пристрій","updated_at":"2026-07-04T16:47:34.961Z"} {"cache_key":"c436a35b396e8435e1c7c549670fbccdbe87f59cbc74b6d56a41f4f7fbecca3d","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"uk","translated":"воркер {state}","updated_at":"2026-05-30T15:38:37.442Z"} +{"cache_key":"c43ec14e49e0856ad29b29ab89364a6f13f5645c04e9659a2e9c9ca2be2fece8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"uk","translated":"Лише архівовані","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"c7f724db3e12f5bd45d1c3442c296d13af320b1fdb7cbb18ce5b46e75ced267e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"uk","translated":"Згорнути робочу область сесії","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"c853376976b76b1203cb6bfc76d4b259b5fe7731d56809505778d0e6c554d48a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"uk","translated":"Очікування залежностей: {parents}.","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"c85d34de35782e010ec3087e297218594809ba987668bb1a2ed2d0a814133f7f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"uk","translated":"Дії з файлами робочого простору","updated_at":"2026-06-16T14:16:00.845Z"} @@ -116,24 +109,25 @@ {"cache_key":"d068e6b36b34c51da41b0565891581d8fbaaefbcad50dfde4e901c546fb4cb96","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"uk","translated":"Без призначення (використовує {agent})","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"d14385d739b5dcf69aaa657960811324eed785a82275bd28925f0369158eddc8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"uk","translated":"Оновити робочу область сесії","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"d212b367b883eb42d4c59e7de652385a9494c411858b58b5dbbbe4829957f770","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"uk","translated":"Журнали воркера","updated_at":"2026-06-16T14:15:40.204Z"} -{"cache_key":"d2554e7f05e29d7b651b1d8f21dfe44e866f29e884cbb52dcd5bae5d325d7a7e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"uk","translated":"Офіційні мобільні застосунки OpenClaw підключаються автоматично після сканування.","updated_at":"2026-07-04T16:47:34.961Z"} +{"cache_key":"d2cab62904eadd3f7b1a0e14bbeb50f6a8cc1eb8133eb5967a67f123b95323f5","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"uk","translated":"Перейменувати сеанс","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"d3c51af5c0eecaa43dfa0c0c81a98228699cf7071e9bb500e11763062b27e07f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"uk","translated":"У цій папці немає файлів.","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"d875f25432822895910002f7110391bee3af71a3dab0d65debdc313b67a45df7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"uk","translated":"Бракує підтвердження","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"da2158c4fe22e65d185fdd0f45caf658ce7aba70551e3108ee62f9527d51e2a8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh5s","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"5s","text_hash":"93e3d8c5b10657d2884f177488b689aadf82a83f962237cb602b3314386ab3b7","tgt_lang":"uk","translated":"5 с","updated_at":"2026-06-17T14:15:34.049Z"} +{"cache_key":"daad459d126be4e2c1c23c8fd93aa5a6bf5672673704f601ac46c8b5c239f065","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"uk","translated":"Відновіть цей сеанс, щоб надсилати повідомлення.","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"db2bfe31afc3fb44571f26559c3006023e26d066ae9ebd0f96224ac572c4ab44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"uk","translated":"Вигляд дошки","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"dedb11177207212fde54d00e08984d4382e7da9912cd9bdba88b37f4512eaa3d","model":"gpt-5.5","provider":"openai","segment_id":"workboard.dispatch","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Nudge dispatcher","text_hash":"c3d20147447cc75f5e1e8cc895af0bc287a4a720372aec4795c5dfbfa9eeda67","tgt_lang":"uk","translated":"Підштовхнути диспетчер","updated_at":"2026-05-30T15:38:37.442Z"} {"cache_key":"e307d37915b742e2324cd02df9e4f646561e619d023622bdc2e341548d0e6bbe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"uk","translated":"Нещодавно завершені","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"e30d0e43ea0665e1aa83e6070231dd79a2cc8be101b6a6006ba579fcc2a27c91","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"uk","translated":"Артефакти","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"e595c1a2c5f2b2a1bc5b93fff0fc26ce961e391ec2e4c6ac2ba988e95c979e46","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"uk","translated":"застаріло","updated_at":"2026-06-17T14:15:34.049Z"} -{"cache_key":"e82a594c9cf0dbea75dfe90e31111be5e9765cd0b82607a3ccd3b570f89c4b53","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"uk","translated":"Перегляд","updated_at":"2026-06-17T14:15:28.579Z"} {"cache_key":"e9e5679c8aa7f6455e15cdbc96cf5bff401a8efebfe18e72e95e717b3cd1baf2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"uk","translated":"Розгорнути робочу область сесії","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"ec19d2598feafd6be42da40cd70b79fbe99c37de3256337b59d0f02ccb7020d1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"uk","translated":"Робоча область сесії","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"f069777eb717da7ef079658967389d00585d1463efc3572db1437b2f3514451e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"uk","translated":"Робоча область","updated_at":"2026-06-16T14:15:46.702Z"} {"cache_key":"f4a4af01b98f179a5a9583671c6f623aeb80fa8711ddbbc9062ba4388ba26126","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"uk","translated":"Агент за замовчуванням","updated_at":"2026-06-17T14:15:28.579Z"} -{"cache_key":"f66a709e11591bad4618e754072ac1cfb232587746c07c35e996177a6048aa89","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"uk","translated":"Відскануйте цей QR-код у мобільному застосунку, щоб підключити новий телефон.","updated_at":"2026-07-04T16:47:34.961Z"} +{"cache_key":"f6a4b2bf2ef896fa97bc6db277ed290b1eb2c111e86072e6d13d6ecde36f1b63","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"uk","translated":"Відновити сеанс","updated_at":"2026-07-02T14:30:30.852Z"} {"cache_key":"f9794fc6d5b1c25c987d22be5005ce2a2b67f20d585f17f927a182a73561368d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"uk","translated":"Дошка: {board}","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"f9c8f9bf26f8e657adbe5881f30a3be0cb4ebbc934f5aa86b8643cd682b1e0bd","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"uk","translated":"Усі сеанси","updated_at":"2026-07-03T07:39:04.008Z"} {"cache_key":"f9e957cebd511cff7144b3cf3e3c5c916d3a24373135219c6b2660a7fdfe0e6c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"uk","translated":"Підсумок: {summary}","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"fab03774a8d15f9ff6336f6376e16f4cf87decaa85b823c1abf44541440d66b0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"uk","translated":"Skills: {skills}","updated_at":"2026-06-16T14:15:40.204Z"} {"cache_key":"fc24d5796f0d84e84daa8dd5cc8b15bdac7f4a6f9d0d6e4cfd146e1c9205021f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"uk","translated":"Відсутній","updated_at":"2026-06-16T14:16:00.845Z"} {"cache_key":"fda778f2e0dd55bd47c6e8f3625878a83271b960c0a63e81de2809fe381fbda1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"uk","translated":"заблоковано","updated_at":"2026-06-17T14:15:34.049Z"} +{"cache_key":"ff202db71e1bcfc27018eaed7c49c335ea96fa7f0dbc60c616646d9b856f3bb9","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"uk","translated":"Перейменувати сеанс","updated_at":"2026-07-02T14:30:30.852Z"} diff --git a/ui/src/i18n/.i18n/vi.meta.json b/ui/src/i18n/.i18n/vi.meta.json index 67d744ab4f90..7d289f1423f4 100644 --- a/ui/src/i18n/.i18n/vi.meta.json +++ b/ui/src/i18n/.i18n/vi.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:23:31.430Z", + "generatedAt": "2026-07-04T18:20:58.699Z", "locale": "vi", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/vi.tm.jsonl b/ui/src/i18n/.i18n/vi.tm.jsonl index b489e355a42e..9749b7a19b76 100644 --- a/ui/src/i18n/.i18n/vi.tm.jsonl +++ b/ui/src/i18n/.i18n/vi.tm.jsonl @@ -6,22 +6,17 @@ {"cache_key":"0909d1b89b2f7a8e0189816e79e40b759bbb6db383723cdeeff0455c2bd253b2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"vi","translated":"Mật độ thẻ thoải mái","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"09465a8b0457c8f057de14044b4d2dc8b69dc774d15823f25b62061ecaa48c4d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"vi","translated":"Đang hiển thị các tệp phù hợp đầu tiên. Tinh chỉnh tìm kiếm để thu hẹp kết quả.","updated_at":"2026-06-16T14:17:25.260Z"} {"cache_key":"0a8fd2d0b38f3a09d8c7a146fad128e4c51b941068a80e031ebd6f3ea25fcfb3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"vi","translated":"Gửi yêu cầu chỉnh sửa đến phiên trò chuyện hiện tại thay vì phiên workshop của đề xuất.","updated_at":"2026-06-16T14:17:10.180Z"} +{"cache_key":"1031a8b3ca50fd073590424aa37a1ca975268625d7b9b6d4f809eaf6751edda5","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"vi","translated":"Khôi phục phiên","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"1044cfc9dd8ae87648af6835bd4c39dfda7b2e3e4009f21dbc6a933013c8995f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyMissing","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{parent} (missing)","text_hash":"8daa419059727391c01e3b7021e05d8d70b4da67f9c57cd2d80f302af77aac53","tgt_lang":"vi","translated":"{parent} (thiếu)","updated_at":"2026-06-16T14:17:17.661Z"} -{"cache_key":"10e959dc32d7fb9105c7e5814185327b77945ed4cada74e6e74cd064dc7de045","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"vi","translated":"Cần quyền truy cập quản trị viên để tạo mã thiết lập.","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"10f3c94c45878e5edf7e5d8026880ebf343d1db442736dcc286f448641bc0f12","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"vi","translated":"Chi tiết thẻ","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"111eab03141a588b9708246e0a13ecebece640be3d5868ec5dc715b16f3f13ab","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.refresh","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Refresh session workspace","text_hash":"c7a97b20a3a3ce348239c4893c99f1902d44877567cb32f752c30cdfbc9a2468","tgt_lang":"vi","translated":"Làm mới không gian làm việc phiên","updated_at":"2026-06-16T14:17:17.661Z"} {"cache_key":"1406e16c3ca4daff4c49c195129977b2b50f03248f4f714c1495d025ca72ccff","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"vi","translated":"Giao thức worker","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"14962bfabd96527927263394ac2e3ea08fddb11a1a0e9e00105f41bd202a4401","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefaultHelp","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Cards explicitly assigned to the configured default agent.","text_hash":"9bb80530da1dfd473936d94642b83cc668b7362cb65675a565f17569937af92f","tgt_lang":"vi","translated":"Thẻ được gán rõ ràng cho agent mặc định đã cấu hình.","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"184025cec6aa484782d74899df65dcc6ae3a27d8cbcd2522fe3a9df609d61982","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"vi","translated":"Không có tệp phù hợp.","updated_at":"2026-06-16T14:17:25.260Z"} -{"cache_key":"18c550a9110dea64e233004ee537f3fd27946e21ef40258e663f17d68e1ba275","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"vi","translated":"Sao chép mã thiết lập","updated_at":"2026-07-04T16:48:59.359Z"} -{"cache_key":"1c398f422e8c4153e310fb78161df4c11327381c0c86845f86e814bddcfa4e9e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"vi","translated":"Ghép nối thiết bị di động","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"1d64ba17a78b897dbf06c991e38f4fc02ac4463b9012fce47d57364c3d0935e9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"vi","translated":"Xem trước","updated_at":"2026-06-16T14:17:27.580Z"} -{"cache_key":"1f1feea00be73bd30c6d6a52a77748d3afdf2009987297e54659f263dda3df29","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"vi","translated":"Quản lý thiết bị","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"1fd98e657e50be6b5bc8e307f98eaeb14c21192938e81d92fa51ca6e71d93ec0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"vi","translated":"Phụ thuộc","updated_at":"2026-06-16T14:17:17.661Z"} {"cache_key":"21a88c314891be44781519fd6b3b6347f030f523b9820d4236c9a17c0e1ab1ca","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"vi","translated":"Đã cập nhật","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"21ff19bd0584a704efa8fd81922fdc04e4d8cce51679102eebd804f1b65f4bcc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"vi","translated":"Bỏ qua lỗi Talk","updated_at":"2026-06-16T14:17:17.661Z"} -{"cache_key":"228d9b461685555c97b0ef747874cbfb4e2d169e22631e1d7e7aed545ec10404","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"vi","translated":"OpenClaw di động","updated_at":"2026-07-04T16:48:59.359Z"} -{"cache_key":"24a5e995bb53cde89d635ffb95fdd247acb015791f361985f7b66112be483b07","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"vi","translated":"Đánh giá","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"261907465e05ba45d23b58fae6a406c96832110418d80622691c8e4cbf51836c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"vi","translated":"Chế độ xem Workboard","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"26d329290a42d303c5308bbd8874a1f271c3109f39d8726190c462b4ee753f78","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"vi","translated":"{agent} (mặc định)","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"287da8512d992495b3ffc6b1aba4397ac20fd6a2d688048b74e4199f0853cc10","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"vi","translated":"Tệp dự án","updated_at":"2026-06-16T14:17:25.260Z"} @@ -29,12 +24,10 @@ {"cache_key":"2b120af49ebbf648655ecc52257f419513e4510f122d808e8e9a4af10175c2ba","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"vi","translated":"Tenant: {tenant}","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"2df6eb3758595245f408bc2a93b6e0f271c880a49ac2379e6406473c9fba0558","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChat","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Use current chat","text_hash":"fbc1ffd63daa506e927c7a85f6e43acd11e0b8c9f52a3951fc782b236ce9a787","tgt_lang":"vi","translated":"Dùng cuộc trò chuyện hiện tại","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"3018088bf994335e79a2ccc0d40b848fc15f74fce6d15379e623d436f771b384","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"vi","translated":"Bảng: {board}","updated_at":"2026-06-16T14:17:10.180Z"} -{"cache_key":"33ac9d373f209817d06363bf3495831b41ae48538f6f5ecabf9ca11bb00f8880","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"vi","translated":"Quét mã QR này trong ứng dụng di động để kết nối điện thoại mới.","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"356b19869b1e2bf6eecb2fd221450379802c32d3234af567c267d65155ad3087","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"vi","translated":"Thư mục cha","updated_at":"2026-06-16T14:17:25.260Z"} {"cache_key":"364f0b235a275cee318660b3b1752b99c197a0317cc9e5e02de6215fcd552514","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"vi","translated":"Thu gọn không gian làm việc phiên","updated_at":"2026-06-16T14:17:17.661Z"} {"cache_key":"376ab10e7b6a84be26bacd3abcfb29611dcb2bb6a3a8b2764b683e25d1f00ed3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"vi","translated":"Giữ lại bình luận sau câu trả lời cuối cùng","updated_at":"2026-07-01T01:08:40.341Z"} {"cache_key":"37e1b972340d8c2b5d14328166f22dff7683c48e7e618680b94cf4a5846e3395","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"vi","translated":"Ghi chú mục tiêu","updated_at":"2026-05-29T21:02:23.226Z"} -{"cache_key":"3a678aeed61f70485e7bc6e260c029540161ee531b685e984418ee21b278ab99","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"vi","translated":"Mã mới","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"3c55b509d2b7d4d4af2d96aa24ce93cccfe8dc5524ed4c057044896b8b402f41","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"vi","translated":"Phiên","updated_at":"2026-06-16T14:17:25.260Z"} {"cache_key":"3faca6c079e1806d45c2701efe2a5d74cd24e183c62467548e9c8da220168eb2","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"vi","translated":"Xưởng kỹ năng","updated_at":"2026-05-31T21:48:40.396Z"} {"cache_key":"410164dfe55960ae6b69b11130219635d873542ce478b8c68726601922e07a1b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"vi","translated":"{count} hiện vật","updated_at":"2026-06-16T14:17:25.260Z"} @@ -42,8 +35,8 @@ {"cache_key":"42c6f57368198afb4b1d0a696b840be0152464e5b151287d23fa2b27855eaafe","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"vi","translated":"Sẵn sàng","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"43f1e8210c4b51a6b7bec6e129f9c12e423963303ff465536947c0b5f0f77a3b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"vi","translated":"Tác vụ Gateway","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"4497e0b1d5e4ddd804d27801124f80cb68e194437186485eee19b94fcbfa7af7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"vi","translated":"Tóm tắt workspace của phiên","updated_at":"2026-06-16T14:17:25.260Z"} +{"cache_key":"482dc8766aa1cab4be6818d438e20129df5647d6a57df88d3b8ac63376ddc0cc","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"vi","translated":"Đổi tên phiên","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"4a41ed8aa4beb8e5d676d787174f4d22869a4dc810260b71d511344cca453c1c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthReadyUnassigned","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"ready unassigned","text_hash":"1e1a31a02e9da6ffa99a459bfd82e5af723794c2175405a5a0d6a32afa955167","tgt_lang":"vi","translated":"sẵn sàng nhưng chưa gán","updated_at":"2026-06-17T14:17:32.599Z"} -{"cache_key":"4a497ee7c2f5dd0145e92bcba894db5b8ef32dc768ea3ed2aa0558cca8e7ad3b","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"vi","translated":"Mã QR ghép nối OpenClaw di động","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"4bca752bc46ee4ca4bc78009b232268c99c61efdc1019b208f554d46f1d205a6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"vi","translated":"Bị chặn","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"51c6e1587421aeeb21a152bbff7bdb7d40c6415cc36341621a5727584194fa0b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"vi","translated":"15s","updated_at":"2026-06-17T14:17:32.599Z"} {"cache_key":"530f2982ecd7d8033c4bcdb270e90d38c32fbd4b05dd96f2f6230ba630df56e0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"vi","translated":"Thiếu bằng chứng","updated_at":"2026-06-17T14:17:26.553Z"} @@ -51,10 +44,7 @@ {"cache_key":"55fb0ec86b311fd7604341bf2d46666f01c708d361b36adeb65536e40c498099","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"vi","translated":"Tắt","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"5726fb2125863893ba5476fd28508692ef5ed2eab90d2cb302762af6e8ee8b20","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthLabel","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Workboard health","text_hash":"85416c4a6d64e35611bdd9747b82815936c38b41d820796ba1fbfbb7539d906b","tgt_lang":"vi","translated":"Tình trạng Workboard","updated_at":"2026-06-17T14:17:32.599Z"} {"cache_key":"58a44dada88d4aa3eb5070c040e389313e42a870a142fc5f9f7fe565510dd9eb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"vi","translated":"Đường dẫn workspace","updated_at":"2026-06-16T14:17:25.260Z"} -{"cache_key":"590b5796d62f050c63291332ae6e0a5b3799c27b6d37deda50b42f70a22b961e","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"vi","translated":"Không có mã QR. Hãy sao chép mã thiết lập thay thế.","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"59ccde94b1b66c5db5fb7d868c85e427814f84078ba4bb93b197a39e43506ced","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"vi","translated":"Đã hết hạn","updated_at":"2026-07-01T10:33:49.015Z"} -{"cache_key":"5bb7c007e1a4e3f81ea7d19a4d561295dccd96e5beef431304e2b91877b0ad69","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"vi","translated":"Không thể tạo mã thiết lập.","updated_at":"2026-07-04T16:48:59.359Z"} -{"cache_key":"5c85f39a2fc2427b7a07719f8592a4ad8e184fe908b414c7b368c3a864c949ba","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"vi","translated":"Yêu cầu thiết bị đang chờ xem xét: {count}","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"5c8b9f2091735ed4f662e3ad3ee25af389be1228f12817bab6401bbb18bfd9f5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"vi","translated":"Chẩn đoán","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"5cc0c473c35ab990c68ace357a909dc84eb9bc73ff89285f6d8fa1be55e0b7d4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"vi","translated":"lần thử thất bại","updated_at":"2026-06-17T14:17:32.599Z"} {"cache_key":"5f3ae080e4f68d4a1db63ac7bb3791767e2a66b18b59767f68d568513ccb59a4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"vi","translated":"Chưa có tệp nào được chỉnh sửa trong phiên này","updated_at":"2026-06-16T14:17:17.661Z"} @@ -70,6 +60,7 @@ {"cache_key":"6e9175f527cd906481e840e8273c2324c84e13e497ebd0323df6e928d639d87e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"vi","translated":"Phiên","updated_at":"2026-06-16T14:17:17.661Z"} {"cache_key":"6f78e5ecf196e5d888afd53dd1e1283eba16d77aab50e8dbe95aec42bf4f8beb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.cwd","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"CWD","text_hash":"0217f1cb7725737f15a6710df3bcfa3bc10a239f0f7801ec3d7168e675f5ebd6","tgt_lang":"vi","translated":"CWD","updated_at":"2026-06-16T14:17:27.580Z"} {"cache_key":"7489da649d88812f0cb8d046c9ba88655edbcddce508e0a8c695b771df4da8bb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"vi","translated":"Dùng cuộc trò chuyện hiện tại cho yêu cầu chỉnh sửa","updated_at":"2026-06-16T14:17:10.180Z"} +{"cache_key":"761cd159fca651bd58f97de7fc19cf26793e2a54cd4511c9d35d328eaf76f0d5","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"vi","translated":"Chỉ đã lưu trữ","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"78516d2d73669f02772b158a096cde396288c1735ab77dc935854ac2b4fbb429","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"vi","translated":"Đang tải không gian làm việc phiên…","updated_at":"2026-06-16T14:17:17.661Z"} {"cache_key":"7af5f14251eb1ff4a30fd87fbb90658d5d02138ad9c713d0db6144b4ce7c7f65","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"vi","translated":"Điều phối","updated_at":"2026-05-30T15:38:47.755Z"} {"cache_key":"7b166db1d3c158f04a4664b80c83712ff40b410472c642be7e82dd2048241d1a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"vi","translated":"Tự động hóa","updated_at":"2026-06-16T14:17:10.180Z"} @@ -83,10 +74,11 @@ {"cache_key":"8c254d6c4b07c9d90e1becd74f6cad2f80dda6899e3b89037df2f511c96e7864","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"vi","translated":"Lệnh","updated_at":"2026-06-16T14:17:27.580Z"} {"cache_key":"8fa558436942fe615594dfb23ad0ef1f0a32493303bdf62ebe1a0005ec392c0e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"vi","translated":"Bình luận","updated_at":"2026-07-01T01:08:40.341Z"} {"cache_key":"91b4ce3ac5daa6eb31ddd0b846247227dbf6fd0a263302b0fc8e98c1ff0a8bcb","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"vi","translated":"{count} nhật ký worker","updated_at":"2026-05-30T15:38:47.755Z"} -{"cache_key":"926e370b703f494a38a52dba338ccbf446c9ba730b8796ad85b4636a9ccd2ea6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"vi","translated":"Các ứng dụng di động OpenClaw chính thức sẽ tự động kết nối sau khi quét.","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"95b11fdc7cc941dc3aec80d1229fe752549bf09007010227ed891541e909f07b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"vi","translated":"Tìm tệp","updated_at":"2026-06-16T14:17:25.260Z"} {"cache_key":"97256cc69ed01e0d33e68667ef78073de7338b91a2aecbbbdbcd5a5c3545ca44","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"vi","translated":"Không có thẻ nào khớp với chế độ xem này","updated_at":"2026-06-17T14:17:32.599Z"} {"cache_key":"99e8247212a9470ff17e5a8551b7d798bed7dc57abc47523fe79894745d99707","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"vi","translated":"MCP","updated_at":"2026-05-31T05:36:54.545Z"} +{"cache_key":"9da3618ac5e7c6caa6b676e3a58b0f334c23c82986c345420c6c52e0023db5fb","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"vi","translated":"Chỉ hiển thị các phiên đã lưu trữ.","updated_at":"2026-07-02T14:31:04.850Z"} +{"cache_key":"9ec77c844379c9721838049797aa0cba6806f3c1ed362daa14294322ebe42e03","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"vi","translated":"Bỏ ghim phiên","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"9f25df3d28afc496cf721ef5b2b4da22061e6d40087ef46d829da16f506c3566","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"vi","translated":"Nhật ký worker","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"9f82448c27e9870fb2b9ad19eee7695a126d74fca10544a55b9eee6b4cba0dce","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"vi","translated":"Chưa gán (dùng {agent})","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"a1a090fb87c5cd37e6380452ad79868f24a732e25dc6d2826bb9abc4720beb98","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"vi","translated":"Chưa có ghi chú nào của người vận hành.","updated_at":"2026-06-16T14:17:17.661Z"} @@ -102,16 +94,17 @@ {"cache_key":"b504f68891582c7260867b0279cef36946b2df5287180bf05c74be411c69bd6f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"vi","translated":"Skills: {skills}","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"b59f3810b01b1504e238778f75f3ad6b78b7ed057cc3ca93ee8cf8d8ecc0f2b4","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"vi","translated":"Tất cả phiên","updated_at":"2026-07-03T07:40:44.965Z"} {"cache_key":"b83c90feafc322737817b144bafa5d92ea866f5b5e4ae44e32e1aa9401c71455","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"vi","translated":"worker {state}","updated_at":"2026-05-30T15:38:47.755Z"} +{"cache_key":"bcbf52f7735cfc2ef1b3b31c9879aa1a7d24f76d8ab5234a5481ca3ff0700d35","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"vi","translated":"Ghim phiên","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"bd0a58cc11eb68f3726c86537ad9778bc1d8514d5a62d7fbd52105d569fcf9f4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdatedValue","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Updated: {time}","text_hash":"5e72d5445f018c9d08aa34ae0178fb9aa49eea6a0afd0c8d379f20b7af3e8aa0","tgt_lang":"vi","translated":"Đã cập nhật: {time}","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"bfedf0d0680045c5d36954b6530dfe1eea08724dbe4cbdf2e69cc61a9476e4b0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"vi","translated":"Tất cả thẻ","updated_at":"2026-06-17T14:17:26.553Z"} +{"cache_key":"c150ec02ee6d5f9de80a7c0dde9183cd5d9be54458e79f2eb8be2ab9164dfdfb","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"vi","translated":"Lưu trữ phiên","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"c24d62dfbc91a39bb6d4115cb0be59e2ddc2eaa86d897015703606fb355d8f54","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"vi","translated":"Đã thay đổi","updated_at":"2026-06-16T14:17:25.260Z"} {"cache_key":"c3358c58b4c97a709c1b7a7641c8f7c7a13bdcee7aa328f3e8dc0c38dbdcbb71","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"vi","translated":"हिन्दी (Tiếng Hindi)","updated_at":"2026-06-26T21:43:43.463Z"} {"cache_key":"c33c4c52087dfece6da76f6e17c23e0b9cfa909e448ebd512e809beda901c976","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"vi","translated":"Gốc","updated_at":"2026-06-16T14:17:25.260Z"} {"cache_key":"c5ac111d8ed618e49c10f987602b9f893f9041679b87960d39291979c332e2e0","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"vi","translated":"{count} tệp đính kèm","updated_at":"2026-05-30T15:38:47.755Z"} {"cache_key":"c8957a8ee0627f74ac54f2ad0f696bf8c9922d35fd381ff9ef8cd40b5ce2c6f9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"vi","translated":"Thao tác tệp workspace","updated_at":"2026-06-16T14:17:25.261Z"} {"cache_key":"c9c84c2bc40c83168861eee263c38ca3b752ccc5064d99038f842ec6c349c6f6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"vi","translated":"Thiếu","updated_at":"2026-06-16T14:17:25.260Z"} -{"cache_key":"ccc89bf8d6cb4fa242d6432bcaa9f1b0bb212742f971300cb931fcc6f424fb15","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"vi","translated":"Đang tạo mã thiết lập bảo mật…","updated_at":"2026-07-04T16:48:59.359Z"} -{"cache_key":"cf78a9b8013a3f35dccaca3667d51d5fb163ccaad850fb583ccbb6a695b32a8f","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"vi","translated":"Trợ giúp ghép nối","updated_at":"2026-07-04T16:48:59.359Z"} +{"cache_key":"ca3d9f0be854fde5d3d5f6623e51bbb605e95b2de51ad8bea8a7bc74e1a91c99","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"vi","translated":"Đổi tên phiên","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"d009f2c33ba9c725ba973873382c712b8065730f793dead160aabe05e45ade80","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"vi","translated":"Không gian làm việc","updated_at":"2026-06-16T14:17:17.661Z"} {"cache_key":"d05a189c3cb6ffdc2798ba19a42f9f293105421fdf0c78a0a5762c36c5f635c1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"vi","translated":"Đánh giá","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"d289a26a1d2313192a4a4f93d8af6b453ef602aef8b1302de6799434ac6921c8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"vi","translated":"Ghi chú của người vận hành","updated_at":"2026-06-16T14:17:10.180Z"} @@ -126,14 +119,15 @@ {"cache_key":"e2bbbb40e4762fbcef24f0c2e92d6616d053b4bd2813138b8bb255155c1f84f3","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"vi","translated":"Русский (Tiếng Nga)","updated_at":"2026-06-26T21:43:43.463Z"} {"cache_key":"e39d27795fa7de145c54b6a83c2ccfdb454bb2604a51fd38e6ccc2e27d7af9d7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"vi","translated":"{count} sẵn sàng","updated_at":"2026-06-16T14:17:17.661Z"} {"cache_key":"e44a23e420b369513c273a390bc2de83e04665bb690caf4313daf3e61c9b8eff","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailRun","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Run","text_hash":"00d60e31a4e6b8344d4201f25a6a7dee770713107f6d097abb01559d32b17f26","tgt_lang":"vi","translated":"Lần chạy","updated_at":"2026-06-16T14:17:10.180Z"} +{"cache_key":"e596898910fd11f083e10a545054286557295e3f3a79f7fffc9002272fe72b5b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"vi","translated":"Đã ghim","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"e788cc22fe43a2bf9a84a6b2c97c5e327f45fb971f7a606011afe15a11dd3771","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"vi","translated":"Thêm quyết định, vướng mắc hoặc ghi chú bằng chứng...","updated_at":"2026-06-16T14:17:17.661Z"} {"cache_key":"e838ed4a97c9d53b108884796abba20f89bb75016484a49e5eb245a2bc181d2b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"vi","translated":"bị chặn","updated_at":"2026-06-17T14:17:32.599Z"} {"cache_key":"ed80f1deb625fb3411795b11027b8878d6fbce19d0e577546c4a395f54144c34","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"vi","translated":"{count} thẻ","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"edc7228cdc148ded641b3943afb02e3ebf8a1ddf2c1a4e8e1181fc2e1c4509c8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDefaultAgent","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Default agent","text_hash":"94da52ecd6c5c3b77b89b8427f4bcaf11a40ddf68f5b00171977349fb2e6abc9","tgt_lang":"vi","translated":"Agent mặc định","updated_at":"2026-06-17T14:17:26.553Z"} {"cache_key":"f494b186ac8af21f9f852aab60dad607db57a77d1221643bc2fb5cab695589b6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"vi","translated":"đang chạy","updated_at":"2026-06-17T14:17:32.599Z"} -{"cache_key":"f5665441e736f7d907e1102bbe4ca1e219c56df8fa268440fbb71239c71cc361","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"vi","translated":"Hiển thị mã thiết lập","updated_at":"2026-07-04T16:48:59.359Z"} {"cache_key":"f72ea7ba7b70a1dac852f601584cc4241b3fa6d1957165fe8c7a8ea0319b48f0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"vi","translated":"Không gian làm việc: {workspace}","updated_at":"2026-06-16T14:17:10.180Z"} {"cache_key":"fb67030033dd67a9e139f6137af39f3e6fa454b5c8dfca6fac6c9cb0e3f2b4e6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"vi","translated":"{count} đã đọc","updated_at":"2026-06-16T14:17:25.260Z"} {"cache_key":"fbe7507469319c05bcfbd615f6df59ebbd682e249f4a7dfbabb6419013c5ee3e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"vi","translated":"Thay đổi chế độ xem, tìm kiếm, mức ưu tiên, agent, hoặc bộ lọc lưu trữ.","updated_at":"2026-06-17T14:17:32.599Z"} +{"cache_key":"fc5250549b39c609a7d41bc8d195346b9c63d0cfb28dbcf09e39beb9ff1b08c1","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"vi","translated":"Khôi phục phiên này để gửi tin nhắn.","updated_at":"2026-07-02T14:31:04.850Z"} {"cache_key":"fd630e4d80a6cdc30e7ef68557b78a8c92e3452a1a54d7bf98fac7ffd4356c7e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"vi","translated":"thiếu bằng chứng","updated_at":"2026-06-17T14:17:32.599Z"} {"cache_key":"fde35d59e3a43b7b06a8531859196131833ba5e32446f6d5f6e407db24d419d0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/vi.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"vi","translated":"{count} bị chặn","updated_at":"2026-06-16T14:17:17.661Z"} diff --git a/ui/src/i18n/.i18n/zh-CN.meta.json b/ui/src/i18n/.i18n/zh-CN.meta.json index 235dceff22b0..2e9dabcc10bc 100644 --- a/ui/src/i18n/.i18n/zh-CN.meta.json +++ b/ui/src/i18n/.i18n/zh-CN.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:21:20.433Z", + "generatedAt": "2026-07-04T18:20:48.863Z", "locale": "zh-CN", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/zh-CN.tm.jsonl b/ui/src/i18n/.i18n/zh-CN.tm.jsonl index 9d4270a21bdd..33d9ba9c2e05 100644 --- a/ui/src/i18n/.i18n/zh-CN.tm.jsonl +++ b/ui/src/i18n/.i18n/zh-CN.tm.jsonl @@ -1,4 +1,5 @@ {"cache_key":"002b98e2f8bf8e4a94a9f0b51b717bfd2d8417bc29103056e128a0c77120a6d9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"zh-CN","translated":"60秒","updated_at":"2026-06-17T14:13:17.789Z"} +{"cache_key":"027d9f79927731d6c1fcde09e98adff20cfbae77f05775a849cb79cdbe7982d2","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"zh-CN","translated":"置顶会话","updated_at":"2026-07-02T14:29:55.120Z"} {"cache_key":"04cb02b359b4b245dd2dc3e8e0a782fd170066c7caeabbe2a6cda230e191c765","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReadyTitle","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{count} dependencies are done.","text_hash":"559fe92cd5fe39b4f511a146fc7ce6b51e7f528e1d388bbfde1d85dddb60604d","tgt_lang":"zh-CN","translated":"{count} 个依赖项已完成。","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"07c8e0a8adea694a6bc2f3fcaf4643c3c4ccd23339402526f73a4c57a8e63793","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"zh-CN","translated":"全部卡片","updated_at":"2026-06-17T14:13:12.872Z"} {"cache_key":"089266810099fc107c4aeb2ea83e26fe29e3009237e62cda5043c5fc1d727d10","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.workspace","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"zh-CN","translated":"会话","updated_at":"2026-06-16T14:13:02.064Z"} @@ -7,30 +8,27 @@ {"cache_key":"0bdefb51170cd60179bd89971a6d2eedd3cee83b40d7cb16157b554e324e33cd","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"zh-CN","translated":"{count} 条 worker 日志","updated_at":"2026-05-30T15:38:03.150Z"} {"cache_key":"0c2ff501479b9c6e086c00c4f42452eee15e1c14fcd268624b5446b7d3194bb4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"zh-CN","translated":"{agent}(默认)","updated_at":"2026-06-17T14:13:12.872Z"} {"cache_key":"0fbae0ca5b0747a797ed77774a9b37431cf67bd905e45ad237bcdab01b3f4dea","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"zh-CN","translated":"工件","updated_at":"2026-06-16T14:13:07.632Z"} -{"cache_key":"11f917e567ffeff13acd3f946d23d9f952f03bfe59d818af1b732e02af9a3f9d","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"zh-CN","translated":"官方 OpenClaw 移动应用在扫描后会自动连接。","updated_at":"2026-07-04T16:44:19.398Z"} -{"cache_key":"15680224f9c0651f895ee18d01ac62d98dd3c548bf08764d13a9bb8017ecbda8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"zh-CN","translated":"审核","updated_at":"2026-06-17T14:13:12.872Z"} +{"cache_key":"14a037bbc62849750d3c927fdcd005a459bda33a372f22ce78dca739547ebbf5","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"zh-CN","translated":"已置顶","updated_at":"2026-07-02T14:29:55.120Z"} {"cache_key":"1569a4dd4bd2449dcfef09f17f95f55a234baf2e6c7ba098c47597dcd6cbf615","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerProtocol","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"worker {state}","text_hash":"f16b9e04d42182b421ce4f4e982b2ef75fab9bd581bdc8b87e62899ba28de11c","tgt_lang":"zh-CN","translated":"worker {state}","updated_at":"2026-05-30T15:38:03.150Z"} {"cache_key":"15fe689e0bc48f264380114c7b42e1e140cf9c27515efec11f7deee366f7ce1d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"zh-CN","translated":"租户:{tenant}","updated_at":"2026-06-16T14:12:56.348Z"} {"cache_key":"163c0d72d5fc971751f197d8cf2bb9fd0dcc94c1af36cd2f8c6fabd098dd8287","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyMissing","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{parent} (missing)","text_hash":"8daa419059727391c01e3b7021e05d8d70b4da67f9c57cd2d80f302af77aac53","tgt_lang":"zh-CN","translated":"{parent}(缺失)","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"16d2d66c5ce557b07810d901c0e5017d28e519f478d01f157fd53203def3470e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"zh-CN","translated":"此文件夹中没有文件。","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"17afeaa34fdaad1bbb762c06931abfff0dbff85740cc24a05e9e55343b29b093","model":"gpt-5.5","provider":"openai","segment_id":"subtitles.mcp","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"MCP servers, auth, tools, and diagnostics.","text_hash":"3eb7bf08a81e00ed41da1b60096320c5b90ff4d4e78b3f84ecd9ce45a62eaea1","tgt_lang":"zh-CN","translated":"MCP 服务器、身份验证、工具和诊断。","updated_at":"2026-05-31T05:36:30.556Z"} -{"cache_key":"1a1157897d01e246eefc4925709e3ec2d0ef7dec377a58acb9be7da38a9dec26","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"zh-CN","translated":"配对移动设备","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"1a4e379f0e9b7c69962825460f1a0161763529482315a90f326aa85cc1345e46","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"zh-CN","translated":"运行中","updated_at":"2026-06-17T14:13:17.789Z"} {"cache_key":"1b3423d8cd4d5e2a06c1187dc75b7c9c2c8d9924c531499c659f94c42eb440a5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"zh-CN","translated":"会话工作区摘要","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"1d05e0086e02f8afe1a1faf6e0c09abe7260dd777ddb6244b096a47c90539da6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.collapse","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Collapse session workspace","text_hash":"b6174b882c37a98e640339d728652a0c1fa70d28ed53d8ccfb6e99363e86973b","tgt_lang":"zh-CN","translated":"收起会话工作区","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"1d0cebd0320484e71767c34cacb9fc38a1f80cf7d3badf7baeaa584e41e6b8e1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.readCount","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{count} read","text_hash":"b3c6c64f1153fb7b2672d2894f532d3f7adea1dd1c473363587fc520be35998e","tgt_lang":"zh-CN","translated":"{count} 个已读取","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"1ee1c9653ecde6367eccef26d50ed5b69cd022d8bac8692b4685f2de862ce2e5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"zh-CN","translated":"摘要:{summary}","updated_at":"2026-06-16T14:12:56.348Z"} +{"cache_key":"227028963ad8f220a4fdb38eaaf4f68ef7a0b48512784da6b9aacc253268422a","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"zh-CN","translated":"取消置顶会话","updated_at":"2026-07-02T14:29:55.120Z"} {"cache_key":"2373bff203ca1ad410a396ce9c05d29c24a25dd24f151cd0fd7e87e920846a91","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"zh-CN","translated":"{count}小时","updated_at":"2026-06-17T14:13:17.789Z"} -{"cache_key":"23aac4236569733912b8e2ee704ce8827d651728574052b27073348f1dc2959d","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"zh-CN","translated":"复制设置码","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"2529fcfb18f9925db612f84c7800431ac8cdcd3a617a215bb3eabd24e0a32e66","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTitle","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Card details","text_hash":"93985f84673405070ffdf7e6f64175caff0f2c489c10e40627718525e79af631","tgt_lang":"zh-CN","translated":"卡片详情","updated_at":"2026-06-16T14:12:56.348Z"} {"cache_key":"2569c6f57755774e881c8a994a8442a4b69e2828c12c41b0ab561a288bc79c07","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"zh-CN","translated":"根目录","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"261e2b1f75f0a5f3c9d24e018907eddab9b8974724e7d420df8a2ddbdf1ed76a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.dismissTalkError","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Dismiss Talk error","text_hash":"72f032a5a37e7197cc94ea95f5da0829abb2262396cdcc35229bd8ce9a52de1e","tgt_lang":"zh-CN","translated":"忽略 Talk 错误","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"27ebe89dcbef0e20d72d0b23eb363cc668963ccf551cdabc38b5bcac85ac723a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"zh-CN","translated":"操作员备注","updated_at":"2026-06-16T14:12:56.348Z"} {"cache_key":"2a028605530b320bf1db8f947997b056df5dccbcc2b9328d2724ba4005e9d439","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"zh-CN","translated":"未知","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"2a042e30e5ed0ab3249aec806adeb23faae45e54129d891c66be56c053691fac","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRunning","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Running","text_hash":"f4ccae29e1bb0c20a124570a1b43f4347ea94bba9f84ffdfddd9c7445b126128","tgt_lang":"zh-CN","translated":"运行中","updated_at":"2026-06-17T14:13:12.872Z"} -{"cache_key":"2a1ebfc3e49f5b7cce900e889a6ec0598dbef9ea245cd3c4719232cb223534e0","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"zh-CN","translated":"配对帮助","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"2e34d6b2c15bef88f3956b229e7f19e159bbec33ebbb1a4f1e12b09c2f220c9c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"zh-CN","translated":"未分配(使用 {agent})","updated_at":"2026-06-17T14:13:12.872Z"} -{"cache_key":"2e7ef1634a423afb186660ae85559ed804b5ca9350874314b36ce047553b99f1","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"zh-CN","translated":"正在创建安全设置码…","updated_at":"2026-07-04T16:44:19.398Z"} +{"cache_key":"2f2e23b461b50eed2f1fe7e316cacc9254672116ec9122a8f15c203f7c6199c1","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"zh-CN","translated":"归档会话","updated_at":"2026-07-02T14:29:55.120Z"} {"cache_key":"31bd00f16cecd8300c9596052aa31783e4befbb742ecc30d75dd3d8f5513a6ab","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"zh-CN","translated":"已添加附件","updated_at":"2026-05-30T15:38:03.150Z"} {"cache_key":"320080affff39b949ba3816ddf77eb4875a338382703f0d62212a7d30b63112b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryToggle","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Keep commentary after the final answer","text_hash":"febdff0791d1ceeddb2b7596da8282f4dcfd5cff4cad11dea4c0a23944cd8ffe","tgt_lang":"zh-CN","translated":"在最终答案后保留评述","updated_at":"2026-07-01T01:06:06.598Z"} {"cache_key":"3299610a677df332f9cd54acd764153498ae8932b2e71f7a31b42a6ebe8470d1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"zh-CN","translated":"工作区:{workspace}","updated_at":"2026-06-16T14:12:56.348Z"} @@ -38,8 +36,9 @@ {"cache_key":"33a6f66bf057a04f7e4130cef76902c4d1cd71e6e052781f4ac5cecaadc89066","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"zh-CN","translated":"已阻塞","updated_at":"2026-06-17T14:13:17.789Z"} {"cache_key":"36bef9a73ad94b17f0428509d5cc6a054f7ff2cd824bca972e4c4e564946dcfd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"zh-CN","translated":"审核","updated_at":"2026-06-17T14:13:12.872Z"} {"cache_key":"38594261e6f72d80ee6934189fe9a5c2ecba499697165fbf09fa07a07c1f6b7f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"zh-CN","translated":"添加备注","updated_at":"2026-06-16T14:13:02.064Z"} +{"cache_key":"3a459258cbe0cb114b294918134cf2c2e83654d66c6895c83e4ecefe3de6a84a","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"zh-CN","translated":"恢复此会话以发送消息。","updated_at":"2026-07-02T14:29:55.120Z"} +{"cache_key":"3b0004c723b469ead9ad4af743c8f486d01e2e734a9a87ccbf8dd25059bc6052","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"zh-CN","translated":"重命名会话","updated_at":"2026-07-02T14:29:55.120Z"} {"cache_key":"3c454dff9f1b5d923cc290599b2cb04c860ca448836d89b0243ea74ee5e39fc4","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.allSessions","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"All sessions","text_hash":"78648d4d66499d8dc19049a4e3bad87b404f99ea7a7f125ced52546e2d92bb79","tgt_lang":"zh-CN","translated":"所有会话","updated_at":"2026-07-03T07:36:07.006Z"} -{"cache_key":"3d58c724ce453aaa46c2d2b45e2f26f1c956d506d740f72d27b78fdf32b1b915","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"zh-CN","translated":"管理设备","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"3ea9e6e56aca10a3a583c75136399ee34969e78dd3b1a0c657becb0c81618314","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"zh-CN","translated":"Worker 协议","updated_at":"2026-06-16T14:12:56.348Z"} {"cache_key":"402b76e1651d2b58033b57b3f2c3a11114af221590ad73cf7b96f1b822db6aa6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browser","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Project files","text_hash":"2a3d9a240c9206964ee7237a1d99fda05ed501a485262e18f33c446c9f735d1c","tgt_lang":"zh-CN","translated":"项目文件","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"42e61c4a1e8cc2adc7be569404fcb614713f78d530dcfc2e3188842f01186c0b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"zh-CN","translated":"此会话中尚未处理任何文件","updated_at":"2026-06-16T14:13:02.065Z"} @@ -51,13 +50,10 @@ {"cache_key":"493835cefc927aaca2276aaab70150ce5f377bf4e4b0459139e9009bcce6d586","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlockedTitle","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Waiting on dependencies: {parents}.","text_hash":"50fb8f9b1326b69bd67d25583ddb4f70b9d75ae6e3ff8a9056a9361daa4b7d8b","tgt_lang":"zh-CN","translated":"正在等待依赖项:{parents}。","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"496da448fb016bb024270dcbd5392a7b3f04b31d4ad6893b3a0e87825383cd3d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"zh-CN","translated":"没有匹配的文件。","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"4db81d22b1656ea356a6a78cbf47e987809783db1c8176c66bcfc49ef2d8b98c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"zh-CN","translated":"Gateway 任务","updated_at":"2026-06-16T14:12:56.348Z"} -{"cache_key":"4dcab6f7a76998491c21932797bf697989ff5bdbf3c4287df1e0450f382ed3f9","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"zh-CN","translated":"无法创建设置码。","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"4e87b36fe252bfe59aa3510ac0112b3c7637554802306cffe0ca74e7bd3f3322","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"zh-CN","translated":"{count} 项受阻","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"50295bdef42931479a04e58f947cc727b463cd8663844a817deddbfa57bdfe47","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthMissingProof","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"missing proof","text_hash":"748797f5ab1c31c8aeeaf7f76bce76064b175a1d1f530849ec683cacbe6555eb","tgt_lang":"zh-CN","translated":"缺少凭证","updated_at":"2026-06-17T14:13:17.789Z"} -{"cache_key":"50758ae73694d0bc68ca7356a9b94769269765731717d5817a1f919854c5a032","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"zh-CN","translated":"等待审核的设备请求:{count}","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"50a5daadc1dd8e76f30020d58f996b528bd77b8b7c379930f05643861e014d36","model":"gpt-5.5","provider":"openai","segment_id":"languages.ru","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Русский (Russian)","text_hash":"ea81bf0fd46410b501bddee074ab6f00b0cdf377a6cafe608dcf2c28f7cb2f4e","tgt_lang":"zh-CN","translated":"Русский(俄语)","updated_at":"2026-06-26T21:43:19.728Z"} {"cache_key":"51efad7a7ea87c645dc8c1d2ecf08a5db18883a5a6652e7ccd06fa5fe89a06a6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"zh-CN","translated":"心跳 {age}","updated_at":"2026-06-17T14:13:17.789Z"} -{"cache_key":"582941eb81bbfb6f276056f528cac889c0cce166ef41bd5c228616e2ed52695b","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"zh-CN","translated":"OpenClaw 移动版","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"5a37568f873447915d0c3be6ca5b1d91059fbe3eaa6387c8433ef56c40333d14","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"zh-CN","translated":"依赖项","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"5d1db0fdaff8b1e11d8faab861d9df56e5109ed6725c3137237b5c724bc44703","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"zh-CN","translated":"已阻塞","updated_at":"2026-06-17T14:13:12.872Z"} {"cache_key":"5e92f8017b80a151f1ed0e2e5443ae49a974ac4206672dbd70ee52448ac7e3f2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"zh-CN","translated":"添加决策、阻碍或证明备注……","updated_at":"2026-06-16T14:13:02.064Z"} @@ -73,19 +69,21 @@ {"cache_key":"6f95652c601b1ea8d236b15f5670521a4c863d133cc4e2f66374400963e7a250","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.badge","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Expired","text_hash":"424a2551d356754c882d04ac16c63e6b50b80b159549d23231001f629455756e","tgt_lang":"zh-CN","translated":"已过期","updated_at":"2026-07-01T10:31:03.966Z"} {"cache_key":"70023d170826b81b8c0449f796f630afb0487347c42cb1348ed2246665b18959","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"zh-CN","translated":"{count} 项就绪","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"71a7e46d96256b5a077c7d7a9b787172c9705701f0b9d2adc30710fd3680fe65","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"zh-CN","translated":"缺失","updated_at":"2026-06-16T14:13:02.064Z"} +{"cache_key":"73a2d02f0dc9601214ede6186b47d9aa5afd2a2bf0ed683d3e375225e402aa15","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"zh-CN","translated":"仅已归档","updated_at":"2026-07-02T14:29:55.119Z"} {"cache_key":"79718c67a32a5715aaf82061c30249f8f266d590ab01ac64dc9d6c4e7768fbe3","model":"gpt-5.5","provider":"openai","segment_id":"chat.toolCards.toolError","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Tool error","text_hash":"a6c64c286a8795034ac5030b74633d3b476b5375e094485698b982879b0bb617","tgt_lang":"zh-CN","translated":"工具错误","updated_at":"2026-05-31T06:43:44.343Z"} {"cache_key":"79fde73afecc4d79dd4bd35fe6a18eac854011d3d87673f3cc357e1c282c37c5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"zh-CN","translated":"30秒","updated_at":"2026-06-17T14:13:17.789Z"} {"cache_key":"7a8df63f8b703bbc628a80688de508d6b42d8382901f1d64f9d230f166c9fbd9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"zh-CN","translated":"评述","updated_at":"2026-07-01T01:06:06.598Z"} {"cache_key":"7bf21d49d9e5d33a53cd17520b7bdd327e84c67d252f1d27b37f7fc8565da821","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"zh-CN","translated":"凭证","updated_at":"2026-06-16T14:12:56.348Z"} {"cache_key":"7cd65ecbd8ff8e1bd46b6b7415a7c9ca5af9e1e14f4ad2852d1ef922844c5e24","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"zh-CN","translated":"失败尝试","updated_at":"2026-06-17T14:13:17.789Z"} +{"cache_key":"7d719ac4e46bfc2a113fcf810d6b5b93eaa6ef1398afdf1aef33a8460c5937dd","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"zh-CN","translated":"恢复会话","updated_at":"2026-07-02T14:29:55.120Z"} {"cache_key":"80fb46ada1bdfad885dc5352cab2a62a36de308027367cb11269c689b1c6448a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"zh-CN","translated":"搜索结果","updated_at":"2026-06-16T14:13:07.632Z"} -{"cache_key":"83cfbd90abc98b73bcf424535793b981e211d9f3945abcabed7b50b3933b78dc","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"zh-CN","translated":"OpenClaw 移动版配对二维码","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"83e6a198426627d98dd2adbe8d10a3d1c73dd13e687b6871f47be3d886545d25","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"zh-CN","translated":"已过期","updated_at":"2026-06-17T14:13:17.789Z"} {"cache_key":"847af1a927fa455ca46dbb4aa45ced05520eb59a5e4a4961978c2ae1e27c97c8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPreset","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Workboard view","text_hash":"cc2b05179ad742029156bb45578e880c46599fd28e1c2ab66f5a6f9e7f8fa08e","tgt_lang":"zh-CN","translated":"工作板视图","updated_at":"2026-06-17T14:13:12.872Z"} {"cache_key":"85d17e2530b7aae7b69ae9dfa5e55d0d7c06465be5db9773c6f93ab186ed5d76","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh15s","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"15s","text_hash":"21b5f52ded33ab19c16a680c4e280b8f9992395b514290163abf272f06394a6f","tgt_lang":"zh-CN","translated":"15秒","updated_at":"2026-06-17T14:13:17.789Z"} {"cache_key":"86dced81359e00a0bbd6f7237fad8f5c1e94e06eb8a1c8aaa659d0d8d352cdc1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"zh-CN","translated":"未明确指定 agent 的卡片。","updated_at":"2026-06-17T14:13:12.872Z"} {"cache_key":"8c4a9363b4253c0f1a0471adb5a63487ef8d11eecf15e2da62ad06c19d6f17c3","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"zh-CN","translated":"最近完成","updated_at":"2026-06-17T14:13:12.872Z"} {"cache_key":"8e53a8a0ab4a78c328102f9f69906a4435905db7f71cfd14ea3a86635387dcce","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.cwd","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"CWD","text_hash":"0217f1cb7725737f15a6710df3bcfa3bc10a239f0f7801ec3d7168e675f5ebd6","tgt_lang":"zh-CN","translated":"CWD","updated_at":"2026-06-16T14:13:09.659Z"} +{"cache_key":"909cbf5b9e1b516be1ccbdec207c086dc90926548b85b1633525fb3b4045dca3","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"zh-CN","translated":"重命名会话","updated_at":"2026-07-02T14:29:55.120Z"} {"cache_key":"91b44f9eb4aaa5d195f09bd3ac1b751d42e6267c3450226c8a3466a2a75dd24a","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventOrchestration","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Orchestration","text_hash":"ed4fdd1721677737cffb2862fe34d5b63901c7cc76b8c67c51e92a467b31a5e7","tgt_lang":"zh-CN","translated":"编排","updated_at":"2026-05-30T15:38:03.150Z"} {"cache_key":"931bb31dc9e5fc62578d7b84cddc6c28d0a91462b1d70d91346f70d2f6c5bf46","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.files","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Workspace","text_hash":"87bb59ba2f92f2a5a9f13e021fd58dd14ae5c065b1046146875e6e68d5ebc8b7","tgt_lang":"zh-CN","translated":"工作区","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"94d027d3632d0b83dbad662a48213bff9192175966a3232a12fe80c0c3429d35","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"zh-CN","translated":"工作区路径","updated_at":"2026-06-16T14:13:07.632Z"} @@ -103,7 +101,6 @@ {"cache_key":"b76e0984ea02f710842b4cfe0a5e585a61ee95556247438dedd1e7c27cecbf2c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"zh-CN","translated":"{count} 个已更改","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"bc8de5c3147c2a650544490d0721eaffab55237b070092a0d63bad6f2f592fe6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.title","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Pairing QR expired","text_hash":"74e09eafc1d35cad5b62b7a9c321a4d090bb8fefdfa8b33913d6194186eadda6","tgt_lang":"zh-CN","translated":"配对二维码已过期","updated_at":"2026-07-01T10:31:03.966Z"} {"cache_key":"bdb340670df47cbae54025a5ec4856428df61d622ead9150c27a1068dc55e2fb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"zh-CN","translated":"将当前聊天用于修订请求","updated_at":"2026-06-16T14:12:56.348Z"} -{"cache_key":"c054a8f5ffc1bd1a82ba80b5bf6cd5e4acc9e5fd6cd07a90acbe0bd92009a3ec","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"zh-CN","translated":"二维码不可用。请改为复制设置码。","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"c090fc1c2de7a56fb0aa912c902092f18d463241874f3d3ceca44f7d48892897","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"zh-CN","translated":"没有卡片符合此视图","updated_at":"2026-06-17T14:13:17.789Z"} {"cache_key":"c3737355859b78b9222efcbfa3779cc9168eeeb1159a0dd9f5bde1b8682af781","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"zh-CN","translated":"查看详情","updated_at":"2026-06-16T14:12:56.348Z"} {"cache_key":"c54ea95851f6fb9a3e7edbae4319c9ff1bff70ebe7939a5a9f7b14d830a7d5b6","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"zh-CN","translated":"已更新 {time}","updated_at":"2026-06-17T14:13:17.789Z"} @@ -116,8 +113,7 @@ {"cache_key":"ce82a02c0474c34be4475bb5ae1d8b7816343f5a5c9ec0c1355a2b4d51dcc8a2","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"zh-CN","translated":"MCP","updated_at":"2026-05-31T05:36:30.556Z"} {"cache_key":"cf40349339fb34426c7595898e8675a217ed3e327664a2ff545bdf6cbf89b891","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.missing","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"zh-CN","translated":"缺失","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"d1627828da337af84dbfe02a3f7fb8b6aabb517330f02120629d42b86c2176b5","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"zh-CN","translated":"技能工坊","updated_at":"2026-05-31T21:48:12.452Z"} -{"cache_key":"d3e989bec03df637b0223c03e82f46395ee7da6c876acbb3ce7cae3894ded6f6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"zh-CN","translated":"新代码","updated_at":"2026-07-04T16:44:19.398Z"} -{"cache_key":"d74a0f1ff6f53c26b538a3a171e09773323afbdc1499e099a889d751f8127570","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"zh-CN","translated":"需要管理员访问权限才能创建设置码。","updated_at":"2026-07-04T16:44:19.398Z"} +{"cache_key":"d2e5734783075ea488053bb07d9f1d8e6f5ac0f77cce21752949b0ede2a23a95","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"zh-CN","translated":"仅显示已归档的会话。","updated_at":"2026-07-02T14:29:55.120Z"} {"cache_key":"d783c793b1846038502c37af4680597a34dacf9f2c93108ac6b424d11c60df51","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"zh-CN","translated":"正在加载会话工作区……","updated_at":"2026-06-16T14:13:02.064Z"} {"cache_key":"d8d059a2a919eeb288a8748c5d2bf04ec0e36ca1a6504bcbcc9cfc559ee5385c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"zh-CN","translated":"已更新","updated_at":"2026-06-16T14:12:56.348Z"} {"cache_key":"dc924c18bea888d8a915f542c2b759d5a6ff221ca12869798aede279629bedfa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"zh-CN","translated":"工作区文件操作","updated_at":"2026-06-16T14:13:07.632Z"} @@ -134,6 +130,4 @@ {"cache_key":"f62f4b4d9b2bf1fd1ba005cead9446cc7d53503516f349f7454892a7dd76634e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.parentFolder","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Parent folder","text_hash":"158f5a01ef8cfb1e6d91f8c321dd3a63f5e457f9650eecd662857701762bd31d","tgt_lang":"zh-CN","translated":"上级文件夹","updated_at":"2026-06-16T14:13:07.632Z"} {"cache_key":"f74c8a67deacc2bd29ad93ece5f8f833fdcd48c8065337b00e8d99c601a6156c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewPresetCount","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"{count} cards","text_hash":"4b3e5442ebd2f839d45fddf95b2c2a18427dbd6ac06c8b57f9d9e996dcb73607","tgt_lang":"zh-CN","translated":"{count} 张卡片","updated_at":"2026-06-17T14:13:12.872Z"} {"cache_key":"fafa46ac079c00c67736f73d6b0a33c705236b1077c76a15e5a89999541b81fd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"zh-CN","translated":"宽松卡片密度","updated_at":"2026-06-17T14:13:12.872Z"} -{"cache_key":"fd1e509ecc4e8917023dd28c029c7647fa907a6addaf12c492624255d6a107c6","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"zh-CN","translated":"显示设置码","updated_at":"2026-07-04T16:44:19.398Z"} -{"cache_key":"fe3fe3bc070777d2cc2583648482f40203b8fd0bfa438e74c830dd38dea8b118","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"zh-CN","translated":"在移动应用中扫描此二维码以连接新手机。","updated_at":"2026-07-04T16:44:19.398Z"} {"cache_key":"ff53b13be5fc05507e7c7a3db5313c493249976916ac6124140c813a3568bb0e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.label","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Session workspace","text_hash":"c0e8ea0cf983d14e8ba3f8fc28976954d637fd50f807c69e9695715fd2384b78","tgt_lang":"zh-CN","translated":"会话工作区","updated_at":"2026-06-16T14:13:02.064Z"} diff --git a/ui/src/i18n/.i18n/zh-TW.meta.json b/ui/src/i18n/.i18n/zh-TW.meta.json index 650e3f38f1e0..a1b169561a99 100644 --- a/ui/src/i18n/.i18n/zh-TW.meta.json +++ b/ui/src/i18n/.i18n/zh-TW.meta.json @@ -1,11 +1,11 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-04T18:21:10.279Z", + "generatedAt": "2026-07-04T18:20:49.058Z", "locale": "zh-TW", - "model": "claude-opus-4-8", - "provider": "anthropic", - "sourceHash": "2ba8357d0e74acfac4159662ea2c07c723a125b47606daaffe785ecccc48299e", - "totalKeys": 1452, - "translatedKeys": 1452, + "model": "gpt-5.5", + "provider": "openai", + "sourceHash": "f2d6998c09b3009d63c98ca8b0187b55c47e60df8b8b33bc1f4dab2ba65130cc", + "totalKeys": 1462, + "translatedKeys": 1462, "workflow": 1 } diff --git a/ui/src/i18n/.i18n/zh-TW.tm.jsonl b/ui/src/i18n/.i18n/zh-TW.tm.jsonl index 02ff13d6a475..b930e0d83cef 100644 --- a/ui/src/i18n/.i18n/zh-TW.tm.jsonl +++ b/ui/src/i18n/.i18n/zh-TW.tm.jsonl @@ -1,6 +1,4 @@ {"cache_key":"0058dd63eb3fb24da56f4bdc46818c272c7219177e42c8da79de612edbc78e2d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"zh-TW","translated":"展開工作階段工作區","updated_at":"2026-06-16T14:13:10.175Z"} -{"cache_key":"03235dad662f4c394cd34cd301b64375f4bb20b62ee59d78881bc2a6b27f6b57","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.title","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"OpenClaw mobile","text_hash":"9adab9e889c70409aa1138dbcbba43edfc6d39f9e2855f8182f0f801aaf3541f","tgt_lang":"zh-TW","translated":"OpenClaw 行動版","updated_at":"2026-07-04T16:44:18.250Z"} -{"cache_key":"0423157b10fea767011d913cc122ed764cfeeda9eb5897ff492cac1f4f34e5ed","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.generating","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Creating a secure setup code…","text_hash":"eca7942aeec595e3a1ebf01564b7dfc4ad90868636da4337f0470dcf1d97bc52","tgt_lang":"zh-TW","translated":"正在建立安全的設定代碼…","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"04fc76d25faad9f51b570fb4f1f7306b7ff547836da07e12a23bfc61b65b2b33","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.root","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Root","text_hash":"44cb005ee2e65d9cc817b0a083579369fb6c24a4be728cb43fd9d4c3ca7f4c2e","tgt_lang":"zh-TW","translated":"根目錄","updated_at":"2026-06-16T14:13:16.738Z"} {"cache_key":"06771a5ec62ce1986d8cdc8ce5570aaec28a25e00725985bc483f5f0871db53b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.loading","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Loading session workspace…","text_hash":"bc6b2400fad20ee1d95d8de4ec6eef9ff1818ab080f86513384029519eaf4f4e","tgt_lang":"zh-TW","translated":"正在載入工作階段工作區…","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"08982382123994fc547aa072a6d9ffbc0baf90cb80de0cba19d2c31c94a02510","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"zh-TW","translated":"{count} 個已變更","updated_at":"2026-06-16T14:13:16.738Z"} @@ -18,21 +16,20 @@ {"cache_key":"1721597dddda5bdbcd49e85f8038348ca5f343e1ec730814a95aa85993633366","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReady","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Ready","text_hash":"5fa7aac5375c5815787fba3f49559f9b45b14023147ce0652803387974144e5f","tgt_lang":"zh-TW","translated":"就緒","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"174fffd0f63f0ec52a7aefef498c252fc72e9ab966d5cbf7d0848613a1881113","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.ageHours","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{count}h","text_hash":"5828ef1c1e95e0bae1c98548d1795a2482cc8e14a8b161b183960a06018ce10d","tgt_lang":"zh-TW","translated":"{count}小時","updated_at":"2026-06-17T14:13:23.259Z"} {"cache_key":"183daaee34447df4b2d689e3c0d0af831237865a3191cf90432d38a3de2d3cad","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"zh-TW","translated":"已封鎖","updated_at":"2026-06-17T14:13:23.259Z"} -{"cache_key":"185c8734b5c35e41b1de7c2568933d8afefbdb3031b922bb4699b56a3b0b80dc","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.failed","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Could not create a setup code.","text_hash":"bc3b0c8b6d41d7975d2ad4bd6c6b8603819d888916a2e87ca09ec575f23158c2","tgt_lang":"zh-TW","translated":"無法建立設定代碼。","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"18f56c2053e1f56ae92d04066c2b3b8b16a93807717d09e06a2c45dbb6d28f49","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerLogs","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Worker logs","text_hash":"67766b9f222a7ccdae6beb7d6e7877d1a13abb8a346a8c5c803a4380bdf851b1","tgt_lang":"zh-TW","translated":"Worker 記錄","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"1a43bd4f0a3e7bb82614b3c32b805be639b426446e181ff2c041a4d94b58bd45","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.unknownStatus","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Unknown","text_hash":"b764cdc0eab7137467211272fa539f1260d1bf2e71bcf6ff3bdc960f5c16aa14","tgt_lang":"zh-TW","translated":"未知","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"1b57f63393eb9e3fcc52fb45e85f7f229a6c7e124cf2e639eae6eb94acb58b58","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthFailedAttempts","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"failed attempts","text_hash":"fd9023af0795825a458100ddbe894a7a8f603324a2b7ad2305d4c9d2334cbd26","tgt_lang":"zh-TW","translated":"失敗嘗試","updated_at":"2026-06-17T14:13:23.259Z"} {"cache_key":"1eedf3a9f4a8a161a15d4187d1962531aeb80fe63e7627c31fc9038bb34e7944","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeWorkerLogs","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{count} worker logs","text_hash":"2923e2a84e6ed0ca048d280206198b156da6148859e474498d335a44e323e0a8","tgt_lang":"zh-TW","translated":"{count} 則工作者記錄","updated_at":"2026-05-30T15:38:05.607Z"} {"cache_key":"239830e854cd40a1a174c34d37efde05b8b2c8a469068a0771ea117cf4642af6","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventAttachmentAdded","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Attachment added","text_hash":"f39a309fb0054d8e6c512733d6f3a4791c6b63157a388d72f635574d98b49b3e","tgt_lang":"zh-TW","translated":"已新增附件","updated_at":"2026-05-30T15:38:05.607Z"} {"cache_key":"267cd9e2120e64ee28fc527714522de3b91c2a4f729ccd7c267e3c63dcd7c9a4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.path","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Workspace path","text_hash":"1fddb73e40f0f5cc6fbf747930a11f857b7a37991caeb4d8677433bbc50a2a38","tgt_lang":"zh-TW","translated":"工作區路徑","updated_at":"2026-06-16T14:13:16.738Z"} +{"cache_key":"26b24ef4a9d46efa15c790026e63b376dab6ad86cd0a5dc2f089ec35a893195e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"zh-TW","translated":"還原工作階段","updated_at":"2026-07-02T14:29:59.020Z"} {"cache_key":"281ebd5735d09792156416816e295aabf6ad64a80038a9f5d16c1aef9bd1c8bc","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthRunning","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"running","text_hash":"c071cf5f5ed6f884cc70155b6f05f755fd46a302d05e4261b7e92ce878bbfed8","tgt_lang":"zh-TW","translated":"執行中","updated_at":"2026-06-17T14:13:23.259Z"} {"cache_key":"28a992a7c181ad351097f4f53ecff5e5705fb89ed61a2c422fc2e4c88b5844a8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"zh-TW","translated":"工作階段","updated_at":"2026-06-16T14:13:16.738Z"} +{"cache_key":"2aa479ea5662ceaf049d7b0e1f5bcaa30d0a1afaa1ebd401c19e6dbb769f4feb","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSessionPrompt","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"zh-TW","translated":"重新命名工作階段","updated_at":"2026-07-02T14:29:59.020Z"} {"cache_key":"2dcadb6a56d4dae637c2be124e0077eaa96831f70abeafe9eeb50d47de750d67","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutCompact","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Compact card density","text_hash":"2f532993d5a6ccda4d758c7ecdda8bebaa857218045a3d4a011fec73d9728785","tgt_lang":"zh-TW","translated":"精簡卡片密度","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"2e3c5d81d525ededb9f9fb2b74cd666a54ea490456bb465e19a4ca06dbf823d2","model":"gpt-5.5","provider":"openai","segment_id":"workboard.badgeAttachments","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{count} attachments","text_hash":"7bb1847693bc91e6e4624d996a96840396a71052786ab143ccb47fbdaa77cf41","tgt_lang":"zh-TW","translated":"{count} 個附件","updated_at":"2026-05-30T15:38:05.607Z"} {"cache_key":"3079b1a8bf5ee737871de1a3c7683989f4459c96fa92db422a76603e2a92d748","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changed","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Changed","text_hash":"2a6141e43be0c2125e3b5d9f74b4ff1261a0b320ff927c83d4d9b1b65585bad7","tgt_lang":"zh-TW","translated":"已變更","updated_at":"2026-06-16T14:13:16.738Z"} {"cache_key":"30a2bcf0485a73ede97f894f87ba3fb9852fc44f692a63c20dffb0ca7b0e5202","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredHint","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Change the view, search, priority, agent, or archive filter.","text_hash":"049dfae940263ace9707334af06b298c1223c38a449b1cec5a712553badebbd0","tgt_lang":"zh-TW","translated":"變更檢視、搜尋、優先順序、代理或封存篩選條件。","updated_at":"2026-06-17T14:13:23.259Z"} -{"cache_key":"32e915714f76472a221d1680f6c5ac5c3c75b8facc4deb863e4e80bf17ad9043","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrUnavailable","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"QR unavailable. Copy the setup code instead.","text_hash":"e8d0d53b8389740ab80b08474ac2539c28e54ad279bd2658fab050e92755b42f","tgt_lang":"zh-TW","translated":"QR 無法使用。請改為複製設定代碼。","updated_at":"2026-07-04T16:44:18.250Z"} -{"cache_key":"33a4c9722a9b764a0c303288481d20fbafd53948a517f2beb2d74e594f48dbae","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.help","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Pairing help","text_hash":"38b889fa410f64c497158988bdf8da130164f09128b2960c1dc3f3da24636ac2","tgt_lang":"zh-TW","translated":"配對說明","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"354885e514d3d7e78e589fb33ff5e6aea796e7ccf065b94443f22a804f43f799","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"zh-TW","translated":"缺少","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"356ddab7e2e3a943b24d4cb0b4f2ffb41517cdf32e1c53b18bbb6fc984dbe71c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.commentaryLabel","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Commentary","text_hash":"4a7a75ab79cde05b0b1baa8f7a704c991586ea44d7e7793e57178ef56f778ff8","tgt_lang":"zh-TW","translated":"評註","updated_at":"2026-07-01T01:06:02.438Z"} {"cache_key":"37db510b10e60bebd333f2087cbcf645bb39dc0edaa1d4c8b14d4151ade20ebb","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.summary","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Session workspace summary","text_hash":"1ed422c34dc1802d4c7366164ae810c496e206fe82e8e6565cefc38230b56bb4","tgt_lang":"zh-TW","translated":"工作階段工作區摘要","updated_at":"2026-06-16T14:13:16.738Z"} @@ -48,6 +45,7 @@ {"cache_key":"4abbdb553062d3d4824b1a0a1524fa02b0dc9c611e66b9390e8e6c3a260a72be","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"zh-TW","translated":"{count} 個受阻","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"4e1b2773f104a5a4ad192afef4f37a74a8d766a831d7a99e81593d4cfa899408","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewMissingProof","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Missing proof","text_hash":"b46debe888e32eec183dc5936c79d22ea43bec580c410c2b3c1aa24aaa75d677","tgt_lang":"zh-TW","translated":"缺少證明","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"4f177fdbb26337eb2db30ba994ac05b55cfb472f4d830559b3394c11610eb957","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.truncated","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Showing the first matching files. Refine the search to narrow results.","text_hash":"62005877ff0fc1f73ce05ca4c459157c57a8c57a3443245b1df4d3b033df98e9","tgt_lang":"zh-TW","translated":"顯示前幾個符合的檔案。請調整搜尋以縮小結果範圍。","updated_at":"2026-06-16T14:13:16.738Z"} +{"cache_key":"501b2bbc7235d896c0dd9c2651034305d435fb6175276866a44f7aeb49e481e8","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinned","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Pinned","text_hash":"f20c879465551f0d1457a13d4390d0f1ece456b115d75463169c5d55341b9b1e","tgt_lang":"zh-TW","translated":"已釘選","updated_at":"2026-07-02T14:29:59.019Z"} {"cache_key":"50be400be8862d08c57270cb9620dbeea1600e27bcbce68e28fa210e8fdbbc38","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh60s","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"60s","text_hash":"f79f071ab5b033ca8fb42c077f39708930d194b18f4608eb26ac1d9665a8836f","tgt_lang":"zh-TW","translated":"60秒","updated_at":"2026-06-17T14:13:23.259Z"} {"cache_key":"564fcd615407c9dd0b1d6278ebbfad922f73e6acd80e07a2e7fb8ac0e0e3129b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.searchResults","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Search results","text_hash":"e978b00de465a271a13bf2f6b9d74d67fdfaa7d973a37378fa32f988c3280599","tgt_lang":"zh-TW","translated":"搜尋結果","updated_at":"2026-06-16T14:13:16.738Z"} {"cache_key":"57c0ab4300c720949d4268d7d4618c1e24f5e72ac6f5c79cc603bdb0ed78be6b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"zh-TW","translated":"檢視詳細資料","updated_at":"2026-06-16T14:12:59.400Z"} @@ -55,7 +53,6 @@ {"cache_key":"5af0160814a0540948b587d27afba0a04ad887266104b48f291d139007cc1aa9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Auto-refresh","text_hash":"9ea4d7fd1550f0866089d18b1344546bfed91502b41c0484d6023ceb0fdeb75c","tgt_lang":"zh-TW","translated":"自動重新整理","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"5afb29184638b931fa5ce6ff85ce2fff231728cd0f07067a58ea9633eded9ef9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.layoutComfortable","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Comfortable card density","text_hash":"bfaaf4553fd254bf24431ebabf62faebfd862685e9e7a52f5e799b11488dc7fe","tgt_lang":"zh-TW","translated":"舒適卡片密度","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"5d1e8d597098ba19a9c87ac363c32879aeb18bee837770d06098a0e52ba393df","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"zh-TW","translated":"預覽","updated_at":"2026-06-16T14:13:18.599Z"} -{"cache_key":"66a22c0668dad32ebad6265eb12fd80017d6f2ed37e62f05a95646b528ab6cf0","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.manageDevices","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Manage devices","text_hash":"3511575c8f3ee17581f629d4cf559c5c2fe4550d4249be8268404a1eb67920f8","tgt_lang":"zh-TW","translated":"管理裝置","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"68e197e5c60c5c190c1c1910c14549358df6212d61205c104928b20d4fed2d41","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationTenant","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Tenant: {tenant}","text_hash":"e896dc96a6847d7aaa593069e890e7a712fd60d7be60280ee24e1942e10411b0","tgt_lang":"zh-TW","translated":"租戶:{tenant}","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"69651c1b633ce1fcb92d8d1586fd21b6dc26c4b828f4290cb90fbb2707235591","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.empty","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"No files touched in this session yet","text_hash":"6b295e4e11bcdd52340c4cc7987565f848cf477a1f0b96c0f47a2b718418298e","tgt_lang":"zh-TW","translated":"此工作階段尚未變更任何檔案","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"6d2665398b5ae919883e0965313055d2d1b8de713cb1aa70b0a2b9f6623ad91c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewRecentlyDone","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Recently done","text_hash":"46b889592a2f5e79197f62b5f96c76993545626bf207740ea58632ceed9623be","tgt_lang":"zh-TW","translated":"近期完成","updated_at":"2026-06-17T14:13:17.815Z"} @@ -73,25 +70,27 @@ {"cache_key":"7aa68f9954a1609d0a4ff34f36498fd64aa3d78b8ce2749ebd7bb64121e622c2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"zh-TW","translated":"工作區檔案操作","updated_at":"2026-06-16T14:13:16.738Z"} {"cache_key":"7bf02dd8146f15240036f48e0562270bfbdc827fe428c82094945a740043fb80","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noBrowserFiles","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"No files in this folder.","text_hash":"3847020c79b1c74e28aa550f0ae53838b764e87f1daf1480dd6aae45ae0529d6","tgt_lang":"zh-TW","translated":"此資料夾沒有檔案。","updated_at":"2026-06-16T14:13:16.738Z"} {"cache_key":"7c3a1ccdce05c71f2e7debb6cf500e221a601706bcfe4030e128e5b23d5f0b0f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"zh-TW","translated":"已更新","updated_at":"2026-06-16T14:12:59.400Z"} -{"cache_key":"7d9c467ed5468dac478fff16de68e0edbc0a33a2079fd478e313b433de290227","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.waiting","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Official OpenClaw mobile apps connect automatically after scanning.","text_hash":"40dd288c9aa182a2809e74f4511402a69db7b153685db075bb5d216d964c3be1","tgt_lang":"zh-TW","translated":"官方 OpenClaw 行動 App 會在掃描後自動連接。","updated_at":"2026-07-04T16:44:18.250Z"} +{"cache_key":"7d04aba9a0d017c59e00efe0c3b798bb4b9bad33fdc6c863c812ea4b75100460","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnly","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Archived only","text_hash":"da7ebd8c482f38dc2a5b693a0ec25aaa04ea7b5fa3713cce9fa41a28f2201a6b","tgt_lang":"zh-TW","translated":"僅已封存","updated_at":"2026-07-02T14:29:59.019Z"} {"cache_key":"7edabd9d6f643852a532b979fd0baebdd731016b041c8b595a7238d7edaaf215","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"zh-TW","translated":"MCP","updated_at":"2026-05-31T05:36:31.990Z"} {"cache_key":"826b48585a8f13512d608fb30b352d6f944f1b0e0673c169d5d41c66af1287e8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.noSearchResults","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"No matching files.","text_hash":"6ba2ede6c6019b640f63e7e48c5ee8238e701c6e539ce9abb5a7a9d9c71d8a73","tgt_lang":"zh-TW","translated":"沒有符合的檔案。","updated_at":"2026-06-16T14:13:16.738Z"} -{"cache_key":"827b6a548c9ddd2631d69677cca0a7e2b71b95a9fb75386f004722dc480afa74","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.pending","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Device requests waiting for review: {count}","text_hash":"0bc0822235b930faa4038f1d7859f695de2519c989595c438505f8ce100a5801","tgt_lang":"zh-TW","translated":"等待審核的裝置要求:{count}","updated_at":"2026-07-04T16:44:18.250Z"} +{"cache_key":"84eb7f989a1b603c37cc3c034a3cefadddb7e2e5b4dd2594aacffc2699fbc507","model":"gpt-5.5","provider":"openai","segment_id":"chat.archivedSessionDisabled","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Restore this session to send messages.","text_hash":"c21bd35c2bef2266f419cbb4214cfd7b0af89386aef88ec8b11837844240e0d9","tgt_lang":"zh-TW","translated":"還原此工作階段以傳送訊息。","updated_at":"2026-07-02T14:29:59.020Z"} {"cache_key":"892ea67d82274b90a2a037200cf74c6aafd1ee90ec43db2208517e488500c0c2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifactCount","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{count} artifacts","text_hash":"022b6b55a10f1864b7aff7c307e49ab37e11e6999111fb87349f040d100abba3","tgt_lang":"zh-TW","translated":"{count} 個成品","updated_at":"2026-06-16T14:13:16.738Z"} +{"cache_key":"8eafdd71c805e6f90f537427ba07bf27a903c96b1a301fc642daea0fe90f4e45","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.pinSession","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Pin session","text_hash":"813273b54d2df112a0fa1903110e9386779f8848ae288142d3f91d7a5891c8ff","tgt_lang":"zh-TW","translated":"釘選工作階段","updated_at":"2026-07-02T14:29:59.020Z"} {"cache_key":"925657f5c27104973a054ec9ac589584058de47dca67f743fc7ad5dc3bc0b27d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.read","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Read","text_hash":"9b9a8d05a7ec353bda84f9c1bb3178c299de3001b5e970508ddc889c487f92ca","tgt_lang":"zh-TW","translated":"已讀取","updated_at":"2026-06-16T14:13:16.738Z"} {"cache_key":"92ab447ae071265b2f6d8682737ce5556fad717d1ff8f95de2db03b52464f862","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"zh-TW","translated":"指令","updated_at":"2026-06-16T14:13:18.599Z"} {"cache_key":"9453b56e509d26313c25cf74cec0e9bcbbbc9c0163f9e933e9eb0d382d81530e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencies","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Dependencies","text_hash":"2e41b118eb209c139f2bcbf690486f6e1509ab978aa96feb053877a70a1a5a09","tgt_lang":"zh-TW","translated":"相依項目","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"949d22edb0aec22c16ffdc84039184e55b037559a9d316c9f8a33ec3e71b105b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefresh30s","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"30s","text_hash":"d3382a4f0e03f8b14cf99424376886c236f1503d4b332137667484fc96d58fc4","tgt_lang":"zh-TW","translated":"30秒","updated_at":"2026-06-17T14:13:23.259Z"} +{"cache_key":"96c905bd13add9b967968ffb872acaa82316a2ebe6d958a6c606ba67f7c87d83","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archivedOnlyTooltip","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Show only archived sessions.","text_hash":"de4c6803e169c7f2d3116da6fa5b95417d952edf88f69b0b279d5d17e9e34e87","tgt_lang":"zh-TW","translated":"僅顯示已封存的工作階段。","updated_at":"2026-07-02T14:29:59.019Z"} +{"cache_key":"971f9cdf8108eb62348e40417fd50f4b6e10b9a0d2090ca408506693b0f6a66e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameSession","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Rename session","text_hash":"2cad0766accc2d3f70f007068524cea983e0d53f54543e9e82a4e73b2a07987f","tgt_lang":"zh-TW","translated":"重新命名工作階段","updated_at":"2026-07-02T14:29:59.020Z"} {"cache_key":"987e79a166a2b686b309f13f4b5205438859562a799dd44a3e599d269a1fa0d0","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationWorkspace","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Workspace: {workspace}","text_hash":"17f5e696e557a646a9003fc8448f6f6761f5fe6bdf7478f750f471496e87c17b","tgt_lang":"zh-TW","translated":"工作區:{workspace}","updated_at":"2026-06-16T14:12:59.400Z"} +{"cache_key":"98f5b98568834cd92a7de13536e3ca6a7579d615d6abb0ac6585a2f4d220a49a","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.archiveSession","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Archive session","text_hash":"740ded37480365eae8bf833ccaaa58350fc9434a77b7bb65b5516eba476fbec3","tgt_lang":"zh-TW","translated":"封存工作階段","updated_at":"2026-07-02T14:29:59.020Z"} {"cache_key":"9979d886403e24df60997801e043da82148173019f9df8141fce453200283ac0","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goalNote","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Goal note","text_hash":"1afb7855a394ef7078728de1c804d6b995413db4eafe7d74190076cb9ed2c9f5","tgt_lang":"zh-TW","translated":"目標備註","updated_at":"2026-05-29T20:59:51.845Z"} {"cache_key":"99a41ad86108940067d4f9f76ab79dd780de0f0f6cb751e9e2b3625e79cab983","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSummary","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Summary: {summary}","text_hash":"3a2270b3cd47b523936c13efec489f36112e0a64fe763dbe972d21fef029e814","tgt_lang":"zh-TW","translated":"摘要:{summary}","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"9b598144b59bd4c5544201dafa4170984c21e16e8d0dcea21d5570e44ef7c1ff","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"zh-TW","translated":"{agent}(預設)","updated_at":"2026-06-17T14:13:17.815Z"} -{"cache_key":"9f8dad5448276426d0777ad95f75c1a116a51879250cb4b59e2cb8c430540464","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.adminRequired","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Administrator access is required to create setup codes.","text_hash":"ebdddd4b5a8fa32c796cb3ae068c328a3f8564f67217b781c65a072ab7bcc9ff","tgt_lang":"zh-TW","translated":"需要系統管理員存取權才能建立設定代碼。","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"9fef17754bf65f1dc47ebe02a1b4ed804fcdfa45de89d9ee7e2b645cc5c7e586","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.badgeHeartbeat","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"heartbeat {age}","text_hash":"000637b3800ae069edbbe207cfad0a3f5037f06e9661ee89d70a1dfe6f404485","tgt_lang":"zh-TW","translated":"心跳 {age}","updated_at":"2026-06-17T14:13:23.259Z"} {"cache_key":"a111f4b4990f470fab42a780c89cd24f606ed4b24326e7f406dd7104845173a8","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomation","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Automation","text_hash":"d909750b1bbb71a39b6330ba8f81f4f8f6e889ed96d7ab366e74857909750c64","tgt_lang":"zh-TW","translated":"自動化","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"a91a71a2f73b2030bf54d60b4932c6362c4c8e5e34b4859265e224d4ef091a19","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNoNotes","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"No operator notes yet.","text_hash":"497e07f47e33851483b6fb1254e88dc640d9fb25525c51f89934a7d39d7b2b9c","tgt_lang":"zh-TW","translated":"尚無操作員備註。","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"ab06fbb9d8d974c976c4d5c0da4b57f0f0e4f52e02901f510e2beab725d000b1","model":"gpt-5.5","provider":"openai","segment_id":"workboard.eventProtocolViolation","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Protocol violation","text_hash":"367bb2052963f7d75beb672d3ca0430d7d49ac48a2759d578c7df933178fe564","tgt_lang":"zh-TW","translated":"通訊協定違規","updated_at":"2026-05-30T15:38:05.607Z"} -{"cache_key":"ac9b66de55b3fc736586dff3fc4e399d09d39a2c8d8489ff394957b2c6caf643","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.newCode","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"New code","text_hash":"3edce074d60711e799f3ebf89c7d2b12069c421558d76340108e13cdc16c6e57","tgt_lang":"zh-TW","translated":"新代碼","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"b15c1b608fdf836eb76f8ebb5c941e58aa26ea9cfc16820d247164a08e05f027","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.lastRefreshed","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Updated {time}","text_hash":"2f87419441e6111b4d62893d3c4ef5ddeb2c8e1af82fabab6132856faf77f907","tgt_lang":"zh-TW","translated":"已於 {time} 更新","updated_at":"2026-06-17T14:13:23.259Z"} {"cache_key":"b2ca53fb2712b0eb0cd0e1a76f10855ed6f871f65e3bdce25c6185cf15860425","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationBoard","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Board: {board}","text_hash":"96d7493589e40e17803b3bf643dff1b891a4ebf57f5d2b36af0a7ddd09e64b84","tgt_lang":"zh-TW","translated":"面板:{board}","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"b39fd60c0abac153fcfbc0a09a2074511f958c619922f3f349ff65f79ac29e25","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.pairingQrExpired.reason","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Run /pair qr again to generate a fresh setup code.","text_hash":"876a304827f54ae5996c4e804aa72953f43568d31e8a15dd2a5b5a40d91c13d3","tgt_lang":"zh-TW","translated":"請再次執行 /pair qr 以產生新的設定碼。","updated_at":"2026-07-01T10:30:56.042Z"} @@ -104,7 +103,6 @@ {"cache_key":"bad90f5193fd4085041d69433704d0f0b0b2d9b5bd013504760b1fedc6056c8b","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailOperatorNotes","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Operator notes","text_hash":"7d2a121620cebfb9c4f6c0f82b693b75d65a4210b8232d77ef87e45fce334347","tgt_lang":"zh-TW","translated":"操作員備註","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"bb30d72179bf02a357ba2c875d1730d8d4bbdede6fc387503165f47a9548b7af","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewAll","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"All cards","text_hash":"2306120917506b5998ec702f3661068b102dac538ba4c9e4634d65fe33eea98a","tgt_lang":"zh-TW","translated":"所有卡片","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"bb3e10d71875b61d47bc962e2812ed3258ed178a40cbd6b0f17a77ea310ec2b4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatAria","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Use current chat for revision requests","text_hash":"9c551a423ae74aedaaa90e4df9899dbdc02f846d6ee058bf2576a812e2c52119","tgt_lang":"zh-TW","translated":"使用目前的對話進行修訂請求","updated_at":"2026-06-16T14:12:59.400Z"} -{"cache_key":"c06cd49b713bebb5ab44e46d72082c03e8b7de525627750a43570e380e221670","model":"claude-opus-4-8","provider":"anthropic","segment_id":"nodes.pairing.review","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"zh-TW","translated":"審查","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"c2b674c569aa61cd0608286bf9e7a2cf3582e40f9a59151a99b2e8054f2fcc1f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"skillWorkshop.header.useCurrentChatTooltip","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Send revision requests to the current chat session instead of the proposal's workshop session.","text_hash":"9db782d40e88750d4faed33c8a73c24552070f101483881c60af8cf446c674a6","tgt_lang":"zh-TW","translated":"將修訂請求傳送至目前的對話工作階段,而非提案的工作坊工作階段。","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"c2c2e45fc3581350cb2d0ed404cf96605a73c976ed106d79f0f259c7566e70e7","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyMissing","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{parent} (missing)","text_hash":"8daa419059727391c01e3b7021e05d8d70b4da67f9c57cd2d80f302af77aac53","tgt_lang":"zh-TW","translated":"{parent}(缺少)","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"c38e20ce158639649de7b4ea479cc4b1d764fcbefcbe7a2a56d4e591797dd315","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailWorkerProtocol","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Worker protocol","text_hash":"e445d823cfa48c4e8fa1d8854771e9939955e772428be6d7957deec0f7968764","tgt_lang":"zh-TW","translated":"Worker 通訊協定","updated_at":"2026-06-16T14:12:59.400Z"} @@ -114,7 +112,6 @@ {"cache_key":"d082cf53a732e9463630017b5e5b86b179258b9517e2c7b302e97863a918cabd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.refreshError","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Refresh failed","text_hash":"8fa7e6d90bef4e5cb735233347bf6a71b5b30d96e7c1a50b73f10cb441b275c2","tgt_lang":"zh-TW","translated":"重新整理失敗","updated_at":"2026-06-17T14:13:23.259Z"} {"cache_key":"d4a6c06bfdfedcffd1b6e7ad155ea4138c82ffa07146a06a5747b8b69310a82d","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.goal","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Goal","text_hash":"cdbf6975e8a35b0d03558be6822dfae166482c24fb86b0433f60e8167f5c91e4","tgt_lang":"zh-TW","translated":"目標","updated_at":"2026-05-29T20:59:51.845Z"} {"cache_key":"d850899057ac1e583498793ba5ea9d62fe586c2ba8dc39681dccff2a59db48dd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"zh-TW","translated":"證明","updated_at":"2026-06-16T14:12:59.400Z"} -{"cache_key":"de19874a9a38d64a68aa22b9ba1d4efb794f087a10be6e27beb0eecf82d1568b","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.subtitle","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Scan this QR code in the mobile app to connect a new phone.","text_hash":"d2db2ea1eaa4ab5857be526ed93b485e1bd8f84a559eeb382151a683a576232e","tgt_lang":"zh-TW","translated":"在行動 App 中掃描此 QR code,以連接新手機。","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"de30a91cd96b0c6054b0d6430b4e87693b5f85f443fcee040ece1fa617582706","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewReview","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Review","text_hash":"aff0766a5290e117b8433c351bae7b7b23bed682b2369bd822d88a647cc58512","tgt_lang":"zh-TW","translated":"審查","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"de54436908f6d2fd9df0c009a66b27614fc3f0ea242b416d07b12aabd6700d26","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"zh-TW","translated":"{agent}(未設定)","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"e217dfe5418b9eff51fa4bf31c37ee90a94d3a29bb3e93713758d96fd31fb042","model":"gpt-5.5","provider":"openai","segment_id":"languages.hi","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"हिन्दी (Hindi)","text_hash":"fab2abfce45382f3031c59477017700a8cb5dfaf8d15379dc24304809b97c7d5","tgt_lang":"zh-TW","translated":"हिन्दी (Hindi)","updated_at":"2026-06-26T21:43:20.866Z"} @@ -122,18 +119,15 @@ {"cache_key":"e412df61823a2907233b6633207f1be24c0e1aa01d632c06245bc3b32a3423f2","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailNotePlaceholder","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Add a decision, blocker, or proof note...","text_hash":"0e40ea8371be2fcbd8379458b0da541ca0dce5dc86357dea64a4d8fac1c742dc","tgt_lang":"zh-TW","translated":"新增決策、阻礙或驗證備註…","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"e5ac548645d9837c323dfd6196904e0b9a7781db16ba3c3bab721adf9954060d","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewBlocked","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Blocked","text_hash":"18f2a0947f9d6523991b29b450307f22773f57d65f7efb98d48a167df04d6b1d","tgt_lang":"zh-TW","translated":"已封鎖","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"e69bbdd9df33ab895e53778ce3f8f4e1e65fcb08d8527be16d27ed7f01b25f7e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.autoRefreshOff","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Off","text_hash":"ca7981b46ecf2c1787b6d76d81d9fd7fa0ca95842e2fcc2a452869891a9334d1","tgt_lang":"zh-TW","translated":"關閉","updated_at":"2026-06-17T14:13:17.815Z"} +{"cache_key":"e6ae7651c8fd279ed69f2c40d56b56b696daf9302431a98290f5f54d683a8ac9","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.unpinSession","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Unpin session","text_hash":"f4c582ee4d7a87bf069d05a49bb0211759d3db8366daab4ba7024425961af5dc","tgt_lang":"zh-TW","translated":"取消釘選工作階段","updated_at":"2026-07-02T14:29:59.020Z"} {"cache_key":"e9988e477c8d4af18134f8cf365500e393e00099c63e0cdcd80b6bcf51e4d55e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.search","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Search files","text_hash":"179fed85ec50a433bb23932745d18f1ade2f84a6ebe145b0025ed3ce5f89fd5a","tgt_lang":"zh-TW","translated":"搜尋檔案","updated_at":"2026-06-16T14:13:16.738Z"} -{"cache_key":"e9c359be13c29d61f632ed847f7737cf0f6899f13c580574b6c0654a4cacf7fd","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.qrAlt","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"OpenClaw mobile pairing QR code","text_hash":"6c402a1c5d7208ea5d5ebf5dd95c5826c9bb81f74a880c0cebe4c2eb1347a7bf","tgt_lang":"zh-TW","translated":"OpenClaw 行動版配對 QR code","updated_at":"2026-07-04T16:44:18.250Z"} -{"cache_key":"e9dc8b08d90e1f41c178c482f68a49f02b87d3eec8a94e7d3d7ed07fb4d6e686","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.showSetupCode","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Show setup code","text_hash":"dc4fa0026fedf726e622f08eafb87102dfe74b6f27a47c5bc3e78df69498296b","tgt_lang":"zh-TW","translated":"顯示設定代碼","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"eb2f53c8c7ec2fadd7ff92828d314da255664c15c900ba8f2ce06694630a60be","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"zh-TW","translated":"Skills:{skills}","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"ebc88b7b4ea809cbb95681c81ec701aeb5d72d86526f22e6454329ccefcd90bd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailTask","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Gateway task","text_hash":"6696e7c592238747dd39d7ba000db600a92f843add73ee90b028c72a2dfd37dd","tgt_lang":"zh-TW","translated":"Gateway 任務","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"ed5b13f86ba6754ba9c9320b591b7f4dab51f0a76f3d1b10a90fffac13eb8b10","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassignedHelp","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Cards without an explicit agent.","text_hash":"f716a36252b33511df056fe7d1092be598eca17ea76bedc5d6d3532ec6b0ffea","tgt_lang":"zh-TW","translated":"未明確指派代理人的卡片。","updated_at":"2026-06-17T14:13:17.815Z"} {"cache_key":"eeab87134e31ac0db534306fa8784c3618002e0dff86d9d2c3dd91cf348d5ca4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.artifacts","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Artifacts","text_hash":"314ae71b8c8dc9c952f0ffc58e35e6d9a41b5cf4756471c7cab0c9476cd5d20b","tgt_lang":"zh-TW","translated":"成品","updated_at":"2026-06-16T14:13:16.738Z"} {"cache_key":"f1fc51b55dc6e3f5a6ab83f0bf252d3b941dd99ec72b1d6d3c64c3b272c14d3a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.browserCount","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"zh-TW","translated":"已顯示 {count} 個","updated_at":"2026-06-16T14:13:16.738Z"} -{"cache_key":"f3595907bed9cd6ab6bb27758c6abea57eb7fe83ebabfc4ee7d86f79eb6deb2b","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.button","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Pair mobile device","text_hash":"7ac73fd1ec7be4a7e633b8b9022e0815b7d262765fe2cffdf71bd63a0ae6780d","tgt_lang":"zh-TW","translated":"配對行動裝置","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"f5d1f922c846ba4d8cbb864e1a790945e8d13feb67fc8fe60c90bbe02dc285fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAddNote","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Add note","text_hash":"63565c0485fec4f743719849734553a5d7947f5962ec9e831e3bce131b3c47fb","tgt_lang":"zh-TW","translated":"新增備註","updated_at":"2026-06-16T14:13:10.175Z"} {"cache_key":"f725cc564ca677916917da2b7c52b1769a1588dfef4e9bb5f26eb3f75fcfbb82","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterUnassigned","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Unassigned (uses {agent})","text_hash":"2700af0c4ab5e86726f72a723ecdf50370b87690db35f00b83723d6457879c8e","tgt_lang":"zh-TW","translated":"未指派(使用 {agent})","updated_at":"2026-06-17T14:13:17.814Z"} {"cache_key":"f7572a09c157f969a2b7c81023fbbe3078ec606f3fe951ac483b4dbb8fcc7fa5","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthStale","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"stale","text_hash":"a03f2386ae06b21109577020844df367857b72c2fcce384c1896fed98a89c82b","tgt_lang":"zh-TW","translated":"過時","updated_at":"2026-06-17T14:13:23.259Z"} -{"cache_key":"f932258a2bad186626271549e34e90c3e5c0bc36cb9ccdadac756a3e2caf39e3","model":"gpt-5.5","provider":"openai","segment_id":"nodes.pairing.copySetupCode","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Copy setup code","text_hash":"277aeb7a54e49353875cc3dc3bb548f26aef0af0c328d6709455c0f813b3efa8","tgt_lang":"zh-TW","translated":"複製設定代碼","updated_at":"2026-07-04T16:44:18.250Z"} {"cache_key":"fae21f57902d46cec227ed9783d0aca914b2d0354c4081087df4ebe3ce7cf633","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailDiagnostics","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"Diagnostics","text_hash":"268f14bbfe119c1e92150583af960a086d7db9619a097f8aa72ff6779842f610","tgt_lang":"zh-TW","translated":"診斷","updated_at":"2026-06-16T14:12:59.400Z"} {"cache_key":"fc692a69e2adb5a42f774a5adde96dd42dd90a3b96312c263af731c4ed699872","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/zh-TW.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"zh-TW","translated":"{count} 個就緒","updated_at":"2026-06-16T14:13:10.175Z"} diff --git a/ui/src/i18n/locales/ar.ts b/ui/src/i18n/locales/ar.ts index d8ef1f9be84b..03aa4ccac0b6 100644 --- a/ui/src/i18n/locales/ar.ts +++ b/ui/src/i18n/locales/ar.ts @@ -184,11 +184,13 @@ export const ar: TranslationMap = { global: "عام", unknown: "غير معروف", showArchived: "إظهار المؤرشفة", + archivedOnly: "المؤرشفة فقط", activeTooltip: "تم التحديث خلال آخر {count} دقيقة.", limitTooltip: "الحد الأقصى للجلسات المراد تحميلها.", globalTooltip: "تضمين الجلسات العامة.", unknownTooltip: "تضمين الجلسات غير المعروفة.", showArchivedTooltip: "تضمين الجلسات المؤرشفة.", + archivedOnlyTooltip: "إظهار الجلسات المؤرشفة فقط.", minutesPlaceholder: "دقيقة", searchPlaceholder: "تصفية حسب المفتاح أو الوكيل أو التسمية أو النوع…", selected: "تم تحديد {count}", @@ -255,6 +257,13 @@ export const ar: TranslationMap = { sessionId: "معرّف الجلسة", activeRun: "تشغيل نشط", archived: "مؤرشف", + pinned: "مثبّتة", + renameSession: "إعادة تسمية الجلسة", + renameSessionPrompt: "إعادة تسمية الجلسة", + pinSession: "تثبيت الجلسة", + unpinSession: "إلغاء تثبيت الجلسة", + archiveSession: "أرشفة الجلسة", + restoreSession: "استعادة الجلسة", loadingCheckpoints: "جارٍ تحميل نقاط التحقق…", noCheckpoints: "لم تُسجَّل نقاط تحقق للضغط لهذه الجلسة.", noSummary: "لم يتم التقاط أي ملخص.", @@ -1343,6 +1352,7 @@ export const ar: TranslationMap = { }, chat: { disconnected: "تم قطع الاتصال بـ Gateway.", + archivedSessionDisabled: "استعِد هذه الجلسة لإرسال الرسائل.", refreshTitle: "تحديث بيانات الدردشة", settings: "إعدادات الدردشة", thinkingToggle: "تبديل مخرجات تفكير/عمل المساعد", diff --git a/ui/src/i18n/locales/de.ts b/ui/src/i18n/locales/de.ts index 33bb05348192..b9cde39e16a5 100644 --- a/ui/src/i18n/locales/de.ts +++ b/ui/src/i18n/locales/de.ts @@ -188,11 +188,13 @@ export const de: TranslationMap = { global: "Global", unknown: "Unbekannt", showArchived: "Archivierte anzeigen", + archivedOnly: "Nur archivierte", activeTooltip: "In den letzten {count} Minuten aktualisiert.", limitTooltip: "Maximale Anzahl zu ladender Sitzungen.", globalTooltip: "Globale Sitzungen einbeziehen.", unknownTooltip: "Unbekannte Sitzungen einbeziehen.", showArchivedTooltip: "Archivierte Sitzungen einbeziehen.", + archivedOnlyTooltip: "Nur archivierte Sitzungen anzeigen.", minutesPlaceholder: "Min.", searchPlaceholder: "Nach Schlüssel, Agent, Label, Art filtern…", selected: "{count} ausgewählt", @@ -259,6 +261,13 @@ export const de: TranslationMap = { sessionId: "Sitzungs-ID", activeRun: "Aktiver Durchlauf", archived: "Archiviert", + pinned: "Angeheftet", + renameSession: "Sitzung umbenennen", + renameSessionPrompt: "Sitzung umbenennen", + pinSession: "Sitzung anheften", + unpinSession: "Sitzung lösen", + archiveSession: "Sitzung archivieren", + restoreSession: "Sitzung wiederherstellen", loadingCheckpoints: "Checkpoints werden geladen…", noCheckpoints: "Für diese Sitzung wurden keine Kompaktierungs-Checkpoints aufgezeichnet.", noSummary: "Keine Zusammenfassung erfasst.", @@ -1370,6 +1379,7 @@ export const de: TranslationMap = { }, chat: { disconnected: "Verbindung zum Gateway getrennt.", + archivedSessionDisabled: "Stellen Sie diese Sitzung wieder her, um Nachrichten zu senden.", refreshTitle: "Chat-Daten aktualisieren", settings: "Chat-Einstellungen", thinkingToggle: "Ausgabe des Assistenten ein-/ausblenden", diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 668591be5ac3..1c0db1f1552e 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -183,11 +183,13 @@ export const en: TranslationMap = { global: "Global", unknown: "Unknown", showArchived: "Show archived", + archivedOnly: "Archived only", activeTooltip: "Loads sessions updated in the last {count} minutes.", limitTooltip: "Max sessions to load.", globalTooltip: "Include global sessions.", unknownTooltip: "Include unknown sessions.", showArchivedTooltip: "Include archived sessions.", + archivedOnlyTooltip: "Show only archived sessions.", minutesPlaceholder: "min", searchPlaceholder: "Filter by key, agent, label, kind…", selected: "{count} selected", @@ -254,6 +256,13 @@ export const en: TranslationMap = { sessionId: "Session ID", activeRun: "Active run", archived: "Archived", + pinned: "Pinned", + renameSession: "Rename session", + renameSessionPrompt: "Rename session", + pinSession: "Pin session", + unpinSession: "Unpin session", + archiveSession: "Archive session", + restoreSession: "Restore session", loadingCheckpoints: "Loading checkpoints…", noCheckpoints: "No compaction checkpoints recorded for this session.", noSummary: "No summary captured.", @@ -1349,6 +1358,7 @@ export const en: TranslationMap = { }, chat: { disconnected: "Disconnected from gateway.", + archivedSessionDisabled: "Restore this session to send messages.", refreshTitle: "Refresh chat data", settings: "Chat settings", thinkingToggle: "Toggle assistant thinking/working output", diff --git a/ui/src/i18n/locales/es.ts b/ui/src/i18n/locales/es.ts index c339f9b60d99..677644a80100 100644 --- a/ui/src/i18n/locales/es.ts +++ b/ui/src/i18n/locales/es.ts @@ -186,11 +186,13 @@ export const es: TranslationMap = { global: "Global", unknown: "Desconocido", showArchived: "Mostrar archivadas", + archivedOnly: "Solo archivadas", activeTooltip: "Actualizadas en los últimos {count} minutos.", limitTooltip: "Máximo de sesiones para cargar.", globalTooltip: "Incluir sesiones globales.", unknownTooltip: "Incluir sesiones desconocidas.", showArchivedTooltip: "Incluir sesiones archivadas.", + archivedOnlyTooltip: "Mostrar solo las sesiones archivadas.", minutesPlaceholder: "min", searchPlaceholder: "Filtrar por clave, agente, etiqueta, tipo…", selected: "{count} seleccionados", @@ -257,6 +259,13 @@ export const es: TranslationMap = { sessionId: "ID de sesión", activeRun: "Ejecución activa", archived: "Archivada", + pinned: "Fijadas", + renameSession: "Cambiar nombre de la sesión", + renameSessionPrompt: "Cambiar nombre de la sesión", + pinSession: "Fijar sesión", + unpinSession: "Desfijar sesión", + archiveSession: "Archivar sesión", + restoreSession: "Restaurar sesión", loadingCheckpoints: "Cargando puntos de control…", noCheckpoints: "No hay puntos de control de compactación registrados para esta sesión.", noSummary: "No se capturó ningún resumen.", @@ -1368,6 +1377,7 @@ export const es: TranslationMap = { }, chat: { disconnected: "Desconectado de la puerta de enlace.", + archivedSessionDisabled: "Restaura esta sesión para enviar mensajes.", refreshTitle: "Actualizar datos del chat", settings: "Configuración del chat", thinkingToggle: "Alternar salida de pensamiento/trabajo del asistente", diff --git a/ui/src/i18n/locales/fa.ts b/ui/src/i18n/locales/fa.ts index d0720faa7364..b1a641633aac 100644 --- a/ui/src/i18n/locales/fa.ts +++ b/ui/src/i18n/locales/fa.ts @@ -186,11 +186,13 @@ export const fa: TranslationMap = { global: "سراسری", unknown: "نامشخص", showArchived: "نمایش بایگانی‌شده‌ها", + archivedOnly: "فقط بایگانی‌شده‌ها", activeTooltip: "در {count} دقیقهٔ گذشته به‌روزرسانی شده است.", limitTooltip: "حداکثر جلسه‌هایی که بارگیری می‌شوند.", globalTooltip: "شامل جلسه‌های سراسری شود.", unknownTooltip: "شامل جلسه‌های ناشناخته شود.", showArchivedTooltip: "شامل جلسه‌های بایگانی‌شده شود.", + archivedOnlyTooltip: "فقط نشست‌های بایگانی‌شده را نشان بده.", minutesPlaceholder: "دقیقه", searchPlaceholder: "فیلتر بر اساس کلید، عامل، برچسب، نوع…", selected: "{count} انتخاب‌شده", @@ -257,6 +259,13 @@ export const fa: TranslationMap = { sessionId: "شناسه جلسه", activeRun: "اجرای فعال", archived: "بایگانی‌شده", + pinned: "سنجاق‌شده", + renameSession: "تغییر نام نشست", + renameSessionPrompt: "تغییر نام نشست", + pinSession: "سنجاق کردن نشست", + unpinSession: "برداشتن سنجاق نشست", + archiveSession: "بایگانی کردن نشست", + restoreSession: "بازیابی نشست", loadingCheckpoints: "در حال بارگذاری نقاط وارسی…", noCheckpoints: "هیچ نقطهٔ وارسی فشرده‌سازی برای این نشست ثبت نشده است.", noSummary: "هیچ خلاصه‌ای ثبت نشده است.", @@ -1361,6 +1370,7 @@ export const fa: TranslationMap = { }, chat: { disconnected: "اتصال از Gateway قطع شد.", + archivedSessionDisabled: "برای ارسال پیام، این نشست را بازیابی کنید.", refreshTitle: "تازه‌سازی داده‌های چت", settings: "تنظیمات چت", thinkingToggle: "تغییر وضعیت خروجی فکر/کار دستیار", diff --git a/ui/src/i18n/locales/fr.ts b/ui/src/i18n/locales/fr.ts index 51a3ac2b942b..4c92bd47c2df 100644 --- a/ui/src/i18n/locales/fr.ts +++ b/ui/src/i18n/locales/fr.ts @@ -188,11 +188,13 @@ export const fr: TranslationMap = { global: "Global", unknown: "Inconnu", showArchived: "Afficher les sessions archivées", + archivedOnly: "Archivées uniquement", activeTooltip: "Mis à jour au cours des {count} dernières minutes.", limitTooltip: "Nombre maximal de sessions à charger.", globalTooltip: "Inclure les sessions globales.", unknownTooltip: "Inclure les sessions inconnues.", showArchivedTooltip: "Inclure les sessions archivées.", + archivedOnlyTooltip: "Afficher uniquement les sessions archivées.", minutesPlaceholder: "min", searchPlaceholder: "Filtrer par clé, agent, libellé, type…", selected: "{count} sélectionné(s)", @@ -259,6 +261,13 @@ export const fr: TranslationMap = { sessionId: "ID de session", activeRun: "Exécution active", archived: "Archivée", + pinned: "Épinglée", + renameSession: "Renommer la session", + renameSessionPrompt: "Renommer la session", + pinSession: "Épingler la session", + unpinSession: "Désépingler la session", + archiveSession: "Archiver la session", + restoreSession: "Restaurer la session", loadingCheckpoints: "Chargement des points de contrôle…", noCheckpoints: "Aucun point de contrôle de compactage enregistré pour cette session.", noSummary: "Aucun résumé capturé.", @@ -1375,6 +1384,7 @@ export const fr: TranslationMap = { }, chat: { disconnected: "Déconnecté du Gateway.", + archivedSessionDisabled: "Restaurez cette session pour envoyer des messages.", refreshTitle: "Actualiser les données du chat", settings: "Paramètres de chat", thinkingToggle: "Afficher/masquer la sortie de réflexion/travail de l’assistant", diff --git a/ui/src/i18n/locales/hi.ts b/ui/src/i18n/locales/hi.ts index 96ee3e91e3cc..9c93ac8a4480 100644 --- a/ui/src/i18n/locales/hi.ts +++ b/ui/src/i18n/locales/hi.ts @@ -185,11 +185,13 @@ export const hi: TranslationMap = { global: "वैश्विक", unknown: "अज्ञात", showArchived: "आर्काइव किए गए दिखाएँ", + archivedOnly: "केवल संग्रहीत", activeTooltip: "पिछले {count} मिनटों में अपडेट किए गए सेशन लोड करता है।", limitTooltip: "लोड करने के लिए अधिकतम सेशन।", globalTooltip: "ग्लोबल सेशन शामिल करें।", unknownTooltip: "अज्ञात सेशन शामिल करें।", showArchivedTooltip: "आर्काइव किए गए सेशन शामिल करें।", + archivedOnlyTooltip: "केवल संग्रहीत सत्र दिखाएँ।", minutesPlaceholder: "मिनट", searchPlaceholder: "की, एजेंट, लेबल, प्रकार के आधार पर फ़िल्टर करें…", selected: "{count} चयनित", @@ -256,6 +258,13 @@ export const hi: TranslationMap = { sessionId: "सेशन ID", activeRun: "सक्रिय रन", archived: "आर्काइव किया गया", + pinned: "पिन किया गया", + renameSession: "सत्र का नाम बदलें", + renameSessionPrompt: "सत्र का नाम बदलें", + pinSession: "सत्र पिन करें", + unpinSession: "सत्र अनपिन करें", + archiveSession: "सत्र संग्रहीत करें", + restoreSession: "सत्र बहाल करें", loadingCheckpoints: "चेकपॉइंट लोड हो रहे हैं…", noCheckpoints: "इस सेशन के लिए कोई कॉम्पैक्शन चेकपॉइंट रिकॉर्ड नहीं किया गया।", noSummary: "कोई सारांश कैप्चर नहीं किया गया।", @@ -1345,12 +1354,13 @@ export const hi: TranslationMap = { }, chat: { disconnected: "Gateway से डिस्कनेक्ट हो गया।", + archivedSessionDisabled: "संदेश भेजने के लिए इस सत्र को बहाल करें।", refreshTitle: "चैट डेटा रीफ़्रेश करें", settings: "चैट सेटिंग्स", thinkingToggle: "सहायक की सोच/कार्य आउटपुट टॉगल करें", toolCallsToggle: "टूल कॉल और टूल परिणाम टॉगल करें", - commentaryToggle: "Keep commentary after the final answer", - commentaryLabel: "Commentary", + commentaryToggle: "अंतिम उत्तर के बाद टिप्पणी बनाए रखें", + commentaryLabel: "टिप्पणी", autoScrollMode: "ऑटो-स्क्रॉल मोड", autoScrollAlways: "हमेशा", autoScrollNearBottom: "नीचे के पास", @@ -1370,7 +1380,7 @@ export const hi: TranslationMap = { dismissUpdateBanner: "अपडेट बैनर हटाएँ", switchedSession: "{session} पर स्विच किया गया", sidebar: { - allSessions: "All sessions", + allSessions: "सभी सत्र", }, welcome: { ready: "चैट के लिए तैयार", @@ -1400,9 +1410,9 @@ export const hi: TranslationMap = { retryQueuedMessage: "कतारबद्ध संदेश का पुनः प्रयास करें", }, pairingQrExpired: { - title: "Pairing QR expired", - reason: "Run /pair qr again to generate a fresh setup code.", - badge: "Expired", + title: "पेयरिंग QR की समय-सीमा समाप्त हो गई", + reason: "नया सेटअप कोड जनरेट करने के लिए /pair qr फिर से चलाएँ।", + badge: "समाप्त", }, composer: { placeholder: "{name} को संदेश भेजें (भेजने के लिए Enter)", diff --git a/ui/src/i18n/locales/id.ts b/ui/src/i18n/locales/id.ts index 6d793d0c901d..55d781b7bce1 100644 --- a/ui/src/i18n/locales/id.ts +++ b/ui/src/i18n/locales/id.ts @@ -185,11 +185,13 @@ export const id: TranslationMap = { global: "Global", unknown: "Tidak diketahui", showArchived: "Tampilkan yang diarsipkan", + archivedOnly: "Hanya yang diarsipkan", activeTooltip: "Diperbarui dalam {count} menit terakhir.", limitTooltip: "Jumlah sesi maksimum untuk dimuat.", globalTooltip: "Sertakan sesi global.", unknownTooltip: "Sertakan sesi tidak dikenal.", showArchivedTooltip: "Sertakan sesi yang diarsipkan.", + archivedOnlyTooltip: "Tampilkan hanya sesi yang diarsipkan.", minutesPlaceholder: "mnt", searchPlaceholder: "Filter menurut kunci, agen, label, jenis…", selected: "{count} dipilih", @@ -256,6 +258,13 @@ export const id: TranslationMap = { sessionId: "ID sesi", activeRun: "Jalankan aktif", archived: "Diarsipkan", + pinned: "Disematkan", + renameSession: "Ganti nama sesi", + renameSessionPrompt: "Ganti nama sesi", + pinSession: "Sematkan sesi", + unpinSession: "Lepas sematan sesi", + archiveSession: "Arsipkan sesi", + restoreSession: "Pulihkan sesi", loadingCheckpoints: "Memuat checkpoint…", noCheckpoints: "Tidak ada checkpoint pemadatan yang tercatat untuk sesi ini.", noSummary: "Tidak ada ringkasan yang direkam.", @@ -1359,6 +1368,7 @@ export const id: TranslationMap = { }, chat: { disconnected: "Terputus dari gateway.", + archivedSessionDisabled: "Pulihkan sesi ini untuk mengirim pesan.", refreshTitle: "Refresh data chat", settings: "Pengaturan chat", thinkingToggle: "Alihkan output berpikir/bekerja asisten", diff --git a/ui/src/i18n/locales/it.ts b/ui/src/i18n/locales/it.ts index b1229b7bbc5b..fd766da1cf4a 100644 --- a/ui/src/i18n/locales/it.ts +++ b/ui/src/i18n/locales/it.ts @@ -187,11 +187,13 @@ export const it: TranslationMap = { global: "Globale", unknown: "Sconosciuto", showArchived: "Mostra archiviate", + archivedOnly: "Solo archiviate", activeTooltip: "Aggiornate negli ultimi {count} minuti.", limitTooltip: "Numero massimo di sessioni da caricare.", globalTooltip: "Includi sessioni globali.", unknownTooltip: "Includi sessioni sconosciute.", showArchivedTooltip: "Includi sessioni archiviate.", + archivedOnlyTooltip: "Mostra solo le sessioni archiviate.", minutesPlaceholder: "min", searchPlaceholder: "Filtra per chiave, agente, etichetta, tipo…", selected: "{count} selezionati", @@ -258,6 +260,13 @@ export const it: TranslationMap = { sessionId: "ID sessione", activeRun: "Esecuzione attiva", archived: "Archiviata", + pinned: "Fissate", + renameSession: "Rinomina sessione", + renameSessionPrompt: "Rinomina sessione", + pinSession: "Fissa sessione", + unpinSession: "Rimuovi sessione fissata", + archiveSession: "Archivia sessione", + restoreSession: "Ripristina sessione", loadingCheckpoints: "Caricamento checkpoint…", noCheckpoints: "Nessun checkpoint di compattazione registrato per questa sessione.", noSummary: "Nessun riepilogo acquisito.", @@ -1368,6 +1377,7 @@ export const it: TranslationMap = { }, chat: { disconnected: "Disconnesso dal gateway.", + archivedSessionDisabled: "Ripristina questa sessione per inviare messaggi.", refreshTitle: "Aggiorna dati chat", settings: "Impostazioni chat", thinkingToggle: "Attiva/disattiva output di pensiero/elaborazione dell'assistente", diff --git a/ui/src/i18n/locales/ja-JP.ts b/ui/src/i18n/locales/ja-JP.ts index fb5515349066..25ca480d1c7b 100644 --- a/ui/src/i18n/locales/ja-JP.ts +++ b/ui/src/i18n/locales/ja-JP.ts @@ -189,11 +189,13 @@ export const ja_JP: TranslationMap = { global: "グローバル", unknown: "不明", showArchived: "アーカイブ済みを表示", + archivedOnly: "アーカイブ済みのみ", activeTooltip: "過去 {count} 分以内に更新されました。", limitTooltip: "読み込むセッションの最大数。", globalTooltip: "グローバルセッションを含めます。", unknownTooltip: "不明なセッションを含めます。", showArchivedTooltip: "アーカイブ済みセッションを含めます。", + archivedOnlyTooltip: "アーカイブ済みのセッションのみを表示します。", minutesPlaceholder: "分", searchPlaceholder: "キー、エージェント、ラベル、種類で絞り込み…", selected: "{count} 件選択中", @@ -260,6 +262,13 @@ export const ja_JP: TranslationMap = { sessionId: "セッション ID", activeRun: "アクティブな実行", archived: "アーカイブ済み", + pinned: "ピン留め済み", + renameSession: "セッション名を変更", + renameSessionPrompt: "セッション名を変更", + pinSession: "セッションをピン留め", + unpinSession: "セッションのピン留めを解除", + archiveSession: "セッションをアーカイブ", + restoreSession: "セッションを復元", loadingCheckpoints: "チェックポイントを読み込み中…", noCheckpoints: "このセッションには圧縮チェックポイントが記録されていません。", noSummary: "要約は取得されていません。", @@ -1365,6 +1374,7 @@ export const ja_JP: TranslationMap = { }, chat: { disconnected: "Gateway から切断されました。", + archivedSessionDisabled: "メッセージを送信するには、このセッションを復元してください。", refreshTitle: "チャットデータを更新", settings: "チャット設定", thinkingToggle: "アシスタントの思考 / 作業出力の表示を切り替え", diff --git a/ui/src/i18n/locales/ko.ts b/ui/src/i18n/locales/ko.ts index 8149b8869c68..f30d1b46e89e 100644 --- a/ui/src/i18n/locales/ko.ts +++ b/ui/src/i18n/locales/ko.ts @@ -184,11 +184,13 @@ export const ko: TranslationMap = { global: "전역", unknown: "알 수 없음", showArchived: "보관된 세션 표시", + archivedOnly: "보관됨만", activeTooltip: "최근 {count}분 이내에 업데이트됨.", limitTooltip: "로드할 최대 세션 수.", globalTooltip: "전역 세션 포함.", unknownTooltip: "알 수 없는 세션 포함.", showArchivedTooltip: "보관된 세션 포함.", + archivedOnlyTooltip: "보관된 세션만 표시합니다.", minutesPlaceholder: "분", searchPlaceholder: "키, 에이전트, 레이블, 종류로 필터링…", selected: "{count}개 선택됨", @@ -255,6 +257,13 @@ export const ko: TranslationMap = { sessionId: "세션 ID", activeRun: "활성 실행", archived: "보관됨", + pinned: "고정됨", + renameSession: "세션 이름 변경", + renameSessionPrompt: "세션 이름 변경", + pinSession: "세션 고정", + unpinSession: "세션 고정 해제", + archiveSession: "세션 보관", + restoreSession: "세션 복원", loadingCheckpoints: "체크포인트 불러오는 중…", noCheckpoints: "이 세션에 기록된 압축 체크포인트가 없습니다.", noSummary: "캡처된 요약이 없습니다.", @@ -1350,6 +1359,7 @@ export const ko: TranslationMap = { }, chat: { disconnected: "Gateway와 연결이 끊어졌습니다.", + archivedSessionDisabled: "메시지를 보내려면 이 세션을 복원하세요.", refreshTitle: "채팅 데이터 새로고침", settings: "채팅 설정", thinkingToggle: "어시스턴트 생각/작업 출력 전환", diff --git a/ui/src/i18n/locales/nl.ts b/ui/src/i18n/locales/nl.ts index b6c1641b75ed..2d43aca23d5d 100644 --- a/ui/src/i18n/locales/nl.ts +++ b/ui/src/i18n/locales/nl.ts @@ -187,11 +187,13 @@ export const nl: TranslationMap = { global: "Globaal", unknown: "Onbekend", showArchived: "Gearchiveerde tonen", + archivedOnly: "Alleen gearchiveerd", activeTooltip: "Bijgewerkt in de afgelopen {count} minuten.", limitTooltip: "Maximaal aantal te laden sessies.", globalTooltip: "Globale sessies opnemen.", unknownTooltip: "Onbekende sessies opnemen.", showArchivedTooltip: "Gearchiveerde sessies opnemen.", + archivedOnlyTooltip: "Toon alleen gearchiveerde sessies.", minutesPlaceholder: "min", searchPlaceholder: "Filter op sleutel, agent, label, type…", selected: "{count} geselecteerd", @@ -258,6 +260,13 @@ export const nl: TranslationMap = { sessionId: "Sessie-ID", activeRun: "Actieve uitvoering", archived: "Gearchiveerd", + pinned: "Vastgezet", + renameSession: "Sessie hernoemen", + renameSessionPrompt: "Sessie hernoemen", + pinSession: "Sessie vastzetten", + unpinSession: "Sessie losmaken", + archiveSession: "Sessie archiveren", + restoreSession: "Sessie herstellen", loadingCheckpoints: "Controlepunten laden…", noCheckpoints: "Geen compactiecontrolepunten vastgelegd voor deze sessie.", noSummary: "Geen samenvatting vastgelegd.", @@ -1365,6 +1374,7 @@ export const nl: TranslationMap = { }, chat: { disconnected: "Verbinding met Gateway verbroken.", + archivedSessionDisabled: "Herstel deze sessie om berichten te verzenden.", refreshTitle: "Chatgegevens vernieuwen", settings: "Chatinstellingen", thinkingToggle: "Denken-/werken-output van assistent schakelen", diff --git a/ui/src/i18n/locales/pl.ts b/ui/src/i18n/locales/pl.ts index e5b1ca9039ae..d7dda907b39d 100644 --- a/ui/src/i18n/locales/pl.ts +++ b/ui/src/i18n/locales/pl.ts @@ -186,11 +186,13 @@ export const pl: TranslationMap = { global: "Globalne", unknown: "Nieznane", showArchived: "Pokaż zarchiwizowane", + archivedOnly: "Tylko zarchiwizowane", activeTooltip: "Zaktualizowano w ciągu ostatnich {count} minut.", limitTooltip: "Maksymalna liczba sesji do wczytania.", globalTooltip: "Uwzględnij sesje globalne.", unknownTooltip: "Uwzględnij nieznane sesje.", showArchivedTooltip: "Uwzględnij zarchiwizowane sesje.", + archivedOnlyTooltip: "Pokaż tylko zarchiwizowane sesje.", minutesPlaceholder: "min", searchPlaceholder: "Filtruj według klucza, agenta, etykiety, rodzaju…", selected: "Wybrano: {count}", @@ -257,6 +259,13 @@ export const pl: TranslationMap = { sessionId: "ID sesji", activeRun: "Aktywne uruchomienie", archived: "Zarchiwizowano", + pinned: "Przypięte", + renameSession: "Zmień nazwę sesji", + renameSessionPrompt: "Zmień nazwę sesji", + pinSession: "Przypnij sesję", + unpinSession: "Odepnij sesję", + archiveSession: "Zarchiwizuj sesję", + restoreSession: "Przywróć sesję", loadingCheckpoints: "Ładowanie punktów kontrolnych…", noCheckpoints: "Dla tej sesji nie zarejestrowano punktów kontrolnych kompaktowania.", noSummary: "Nie zapisano podsumowania.", @@ -1364,6 +1373,7 @@ export const pl: TranslationMap = { }, chat: { disconnected: "Rozłączono z Gateway.", + archivedSessionDisabled: "Przywróć tę sesję, aby wysyłać wiadomości.", refreshTitle: "Odśwież dane czatu", settings: "Ustawienia czatu", thinkingToggle: "Przełącz wyświetlanie myślenia/pracy asystenta", diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts index e29ca9702cf5..29cad60af900 100644 --- a/ui/src/i18n/locales/pt-BR.ts +++ b/ui/src/i18n/locales/pt-BR.ts @@ -185,11 +185,13 @@ export const pt_BR: TranslationMap = { global: "Global", unknown: "Desconhecido", showArchived: "Mostrar arquivadas", + archivedOnly: "Somente arquivadas", activeTooltip: "Atualizadas nos últimos {count} minutos.", limitTooltip: "Máximo de sessões a carregar.", globalTooltip: "Incluir sessões globais.", unknownTooltip: "Incluir sessões desconhecidas.", showArchivedTooltip: "Incluir sessões arquivadas.", + archivedOnlyTooltip: "Mostrar somente sessões arquivadas.", minutesPlaceholder: "min", searchPlaceholder: "Filtrar por chave, agente, rótulo, tipo…", selected: "{count} selecionado(s)", @@ -256,6 +258,13 @@ export const pt_BR: TranslationMap = { sessionId: "ID da sessão", activeRun: "Execução ativa", archived: "Arquivada", + pinned: "Fixadas", + renameSession: "Renomear sessão", + renameSessionPrompt: "Renomear sessão", + pinSession: "Fixar sessão", + unpinSession: "Desafixar sessão", + archiveSession: "Arquivar sessão", + restoreSession: "Restaurar sessão", loadingCheckpoints: "Carregando checkpoints…", noCheckpoints: "Nenhum checkpoint de compactação registrado para esta sessão.", noSummary: "Nenhum resumo capturado.", @@ -1361,6 +1370,7 @@ export const pt_BR: TranslationMap = { }, chat: { disconnected: "Desconectado do gateway.", + archivedSessionDisabled: "Restaure esta sessão para enviar mensagens.", refreshTitle: "Atualizar dados do chat", settings: "Configurações do chat", thinkingToggle: "Alternar saída de pensamento/trabalho do assistente", diff --git a/ui/src/i18n/locales/ru.ts b/ui/src/i18n/locales/ru.ts index 3cb4922fbe54..813d39a637b3 100644 --- a/ui/src/i18n/locales/ru.ts +++ b/ui/src/i18n/locales/ru.ts @@ -186,11 +186,13 @@ export const ru: TranslationMap = { global: "Глобально", unknown: "Неизвестно", showArchived: "Показать архивные", + archivedOnly: "Только архивные", activeTooltip: "Загружает сеансы, обновленные за последние {count} минут.", limitTooltip: "Максимальное количество сеансов для загрузки.", globalTooltip: "Включить глобальные сеансы.", unknownTooltip: "Включить неизвестные сеансы.", showArchivedTooltip: "Включить архивные сеансы.", + archivedOnlyTooltip: "Показывать только архивные сеансы.", minutesPlaceholder: "мин", searchPlaceholder: "Фильтр по ключу, агенту, метке, типу…", selected: "Выбрано: {count}", @@ -257,6 +259,13 @@ export const ru: TranslationMap = { sessionId: "ID сеанса", activeRun: "Активный запуск", archived: "В архиве", + pinned: "Закреплено", + renameSession: "Переименовать сеанс", + renameSessionPrompt: "Переименовать сеанс", + pinSession: "Закрепить сеанс", + unpinSession: "Открепить сеанс", + archiveSession: "Архивировать сеанс", + restoreSession: "Восстановить сеанс", loadingCheckpoints: "Загрузка контрольных точек…", noCheckpoints: "Для этого сеанса не записано контрольных точек сжатия.", noSummary: "Сводка не сохранена.", @@ -1369,12 +1378,13 @@ export const ru: TranslationMap = { }, chat: { disconnected: "Отключено от gateway.", + archivedSessionDisabled: "Восстановите этот сеанс, чтобы отправлять сообщения.", refreshTitle: "Обновить данные чата", settings: "Настройки чата", thinkingToggle: "Показать или скрыть вывод размышлений/работы ассистента", toolCallsToggle: "Показать или скрыть вызовы инструментов и их результаты", - commentaryToggle: "Keep commentary after the final answer", - commentaryLabel: "Commentary", + commentaryToggle: "Сохранять комментарии после окончательного ответа", + commentaryLabel: "Комментарии", autoScrollMode: "Режим автопрокрутки", autoScrollAlways: "Всегда", autoScrollNearBottom: "Рядом с нижним краем", @@ -1394,7 +1404,7 @@ export const ru: TranslationMap = { dismissUpdateBanner: "Скрыть баннер обновления", switchedSession: "Переключено на {session}", sidebar: { - allSessions: "All sessions", + allSessions: "Все сеансы", }, welcome: { ready: "Готово к чату", @@ -1424,9 +1434,9 @@ export const ru: TranslationMap = { retryQueuedMessage: "Повторить сообщение в очереди", }, pairingQrExpired: { - title: "Pairing QR expired", - reason: "Run /pair qr again to generate a fresh setup code.", - badge: "Expired", + title: "QR-код сопряжения истек", + reason: "Запустите /pair qr еще раз, чтобы создать новый код настройки.", + badge: "Истек", }, composer: { placeholder: "Сообщение {name} (Enter для отправки)", diff --git a/ui/src/i18n/locales/th.ts b/ui/src/i18n/locales/th.ts index 9c01d0a27de2..333627fff3a8 100644 --- a/ui/src/i18n/locales/th.ts +++ b/ui/src/i18n/locales/th.ts @@ -183,11 +183,13 @@ export const th: TranslationMap = { global: "ส่วนกลาง", unknown: "ไม่ทราบ", showArchived: "แสดงที่เก็บถาวร", + archivedOnly: "เฉพาะที่เก็บถาวร", activeTooltip: "อัปเดตในช่วง {count} นาทีที่ผ่านมา", limitTooltip: "จำนวนเซสชันสูงสุดที่จะโหลด", globalTooltip: "รวมเซสชันส่วนกลาง", unknownTooltip: "รวมเซสชันที่ไม่รู้จัก", showArchivedTooltip: "รวมเซสชันที่เก็บถาวร", + archivedOnlyTooltip: "แสดงเฉพาะเซสชันที่เก็บถาวร", minutesPlaceholder: "นาที", searchPlaceholder: "กรองตามคีย์, agent, ป้ายกำกับ, ชนิด…", selected: "เลือกแล้ว {count} รายการ", @@ -254,6 +256,13 @@ export const th: TranslationMap = { sessionId: "รหัสเซสชัน", activeRun: "การเรียกใช้งานที่ใช้งานอยู่", archived: "เก็บถาวรแล้ว", + pinned: "ปักหมุดแล้ว", + renameSession: "เปลี่ยนชื่อเซสชัน", + renameSessionPrompt: "เปลี่ยนชื่อเซสชัน", + pinSession: "ปักหมุดเซสชัน", + unpinSession: "เลิกปักหมุดเซสชัน", + archiveSession: "เก็บเซสชันถาวร", + restoreSession: "กู้คืนเซสชัน", loadingCheckpoints: "กำลังโหลดเช็กพอยต์…", noCheckpoints: "ไม่มีเช็กพอยต์การบีบอัดที่บันทึกไว้สำหรับเซสชันนี้", noSummary: "ไม่มีสรุปที่บันทึกไว้", @@ -1327,6 +1336,7 @@ export const th: TranslationMap = { }, chat: { disconnected: "ตัดการเชื่อมต่อจากเกตเวย์แล้ว", + archivedSessionDisabled: "กู้คืนเซสชันนี้เพื่อส่งข้อความ", refreshTitle: "รีเฟรชข้อมูลแชต", settings: "การตั้งค่าแชท", thinkingToggle: "สลับการแสดงผลการคิด/การทำงานของผู้ช่วย", diff --git a/ui/src/i18n/locales/tr.ts b/ui/src/i18n/locales/tr.ts index 5d847e109df6..efcb69ff0dd1 100644 --- a/ui/src/i18n/locales/tr.ts +++ b/ui/src/i18n/locales/tr.ts @@ -187,11 +187,13 @@ export const tr: TranslationMap = { global: "Genel", unknown: "Bilinmiyor", showArchived: "Arşivlenmişleri göster", + archivedOnly: "Yalnızca arşivlenenler", activeTooltip: "Son {count} dakika içinde güncellendi.", limitTooltip: "Yüklenecek maksimum oturum sayısı.", globalTooltip: "Genel oturumları dahil et.", unknownTooltip: "Bilinmeyen oturumları dahil et.", showArchivedTooltip: "Arşivlenmiş oturumları dahil et.", + archivedOnlyTooltip: "Yalnızca arşivlenen oturumları göster.", minutesPlaceholder: "dk", searchPlaceholder: "Anahtara, ajana, etikete, türe göre filtrele…", selected: "{count} seçildi", @@ -258,6 +260,13 @@ export const tr: TranslationMap = { sessionId: "Oturum kimliği", activeRun: "Etkin çalıştırma", archived: "Arşivlendi", + pinned: "Sabitlenmiş", + renameSession: "Oturumu yeniden adlandır", + renameSessionPrompt: "Oturumu yeniden adlandır", + pinSession: "Oturumu sabitle", + unpinSession: "Oturumun sabitlemesini kaldır", + archiveSession: "Oturumu arşivle", + restoreSession: "Oturumu geri yükle", loadingCheckpoints: "Kontrol noktaları yükleniyor…", noCheckpoints: "Bu oturum için kaydedilmiş sıkıştırma kontrol noktası yok.", noSummary: "Özet yakalanmadı.", @@ -1365,6 +1374,7 @@ export const tr: TranslationMap = { }, chat: { disconnected: "Gateway bağlantısı kesildi.", + archivedSessionDisabled: "Mesaj göndermek için bu oturumu geri yükleyin.", refreshTitle: "Sohbet verilerini yenile", settings: "Sohbet ayarları", thinkingToggle: "Asistanın düşünme/çalışma çıktısını aç/kapat", diff --git a/ui/src/i18n/locales/uk.ts b/ui/src/i18n/locales/uk.ts index e02397a8da5a..502df169f22a 100644 --- a/ui/src/i18n/locales/uk.ts +++ b/ui/src/i18n/locales/uk.ts @@ -186,11 +186,13 @@ export const uk: TranslationMap = { global: "Глобально", unknown: "Невідомо", showArchived: "Показати архівовані", + archivedOnly: "Лише архівовані", activeTooltip: "Оновлено за останні {count} хвилин.", limitTooltip: "Максимальна кількість сеансів для завантаження.", globalTooltip: "Включити глобальні сеанси.", unknownTooltip: "Включити невідомі сеанси.", showArchivedTooltip: "Включити архівовані сеанси.", + archivedOnlyTooltip: "Показувати лише архівовані сеанси.", minutesPlaceholder: "хв", searchPlaceholder: "Фільтрувати за ключем, агентом, міткою, типом…", selected: "Вибрано: {count}", @@ -257,6 +259,13 @@ export const uk: TranslationMap = { sessionId: "ID сеансу", activeRun: "Активний запуск", archived: "Архівовано", + pinned: "Закріплено", + renameSession: "Перейменувати сеанс", + renameSessionPrompt: "Перейменувати сеанс", + pinSession: "Закріпити сеанс", + unpinSession: "Відкріпити сеанс", + archiveSession: "Архівувати сеанс", + restoreSession: "Відновити сеанс", loadingCheckpoints: "Завантаження контрольних точок…", noCheckpoints: "Для цього сеансу не записано контрольних точок стиснення.", noSummary: "Підсумок не зафіксовано.", @@ -1363,6 +1372,7 @@ export const uk: TranslationMap = { }, chat: { disconnected: "Відключено від шлюзу.", + archivedSessionDisabled: "Відновіть цей сеанс, щоб надсилати повідомлення.", refreshTitle: "Оновити дані чату", settings: "Налаштування чату", thinkingToggle: "Перемкнути показ мислення/роботи асистента", diff --git a/ui/src/i18n/locales/vi.ts b/ui/src/i18n/locales/vi.ts index eb52c35c6af5..9a3c23cac845 100644 --- a/ui/src/i18n/locales/vi.ts +++ b/ui/src/i18n/locales/vi.ts @@ -185,11 +185,13 @@ export const vi: TranslationMap = { global: "Toàn cục", unknown: "Không rõ", showArchived: "Hiển thị đã lưu trữ", + archivedOnly: "Chỉ đã lưu trữ", activeTooltip: "Đã cập nhật trong {count} phút gần đây.", limitTooltip: "Số phiên tối đa cần tải.", globalTooltip: "Bao gồm các phiên toàn cục.", unknownTooltip: "Bao gồm các phiên không xác định.", showArchivedTooltip: "Bao gồm các phiên đã lưu trữ.", + archivedOnlyTooltip: "Chỉ hiển thị các phiên đã lưu trữ.", minutesPlaceholder: "phút", searchPlaceholder: "Lọc theo khóa, agent, nhãn, loại…", selected: "Đã chọn {count}", @@ -256,6 +258,13 @@ export const vi: TranslationMap = { sessionId: "ID phiên", activeRun: "Lần chạy đang hoạt động", archived: "Đã lưu trữ", + pinned: "Đã ghim", + renameSession: "Đổi tên phiên", + renameSessionPrompt: "Đổi tên phiên", + pinSession: "Ghim phiên", + unpinSession: "Bỏ ghim phiên", + archiveSession: "Lưu trữ phiên", + restoreSession: "Khôi phục phiên", loadingCheckpoints: "Đang tải điểm kiểm tra…", noCheckpoints: "Không có điểm kiểm tra nén nào được ghi lại cho phiên này.", noSummary: "Chưa ghi nhận bản tóm tắt.", @@ -1351,6 +1360,7 @@ export const vi: TranslationMap = { }, chat: { disconnected: "Đã ngắt kết nối khỏi gateway.", + archivedSessionDisabled: "Khôi phục phiên này để gửi tin nhắn.", refreshTitle: "Làm mới dữ liệu trò chuyện", settings: "Cài đặt trò chuyện", thinkingToggle: "Bật/tắt đầu ra suy nghĩ/đang xử lý của trợ lý", diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index f467164fdd40..bb119a232e2c 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -183,11 +183,13 @@ export const zh_CN: TranslationMap = { global: "全局", unknown: "未知", showArchived: "显示已归档", + archivedOnly: "仅已归档", activeTooltip: "在过去 {count} 分钟内更新。", limitTooltip: "要加载的最大会话数。", globalTooltip: "包含全局会话。", unknownTooltip: "包含未知会话。", showArchivedTooltip: "包含已归档会话。", + archivedOnlyTooltip: "仅显示已归档的会话。", minutesPlaceholder: "分钟", searchPlaceholder: "按密钥、代理、标签、类型筛选…", selected: "已选择 {count} 项", @@ -254,6 +256,13 @@ export const zh_CN: TranslationMap = { sessionId: "会话 ID", activeRun: "活动运行", archived: "已归档", + pinned: "已置顶", + renameSession: "重命名会话", + renameSessionPrompt: "重命名会话", + pinSession: "置顶会话", + unpinSession: "取消置顶会话", + archiveSession: "归档会话", + restoreSession: "恢复会话", loadingCheckpoints: "正在加载检查点…", noCheckpoints: "此会话没有记录压缩检查点。", noSummary: "未捕获摘要。", @@ -1322,6 +1331,7 @@ export const zh_CN: TranslationMap = { }, chat: { disconnected: "已断开与网关的连接。", + archivedSessionDisabled: "恢复此会话以发送消息。", refreshTitle: "刷新聊天数据", settings: "聊天设置", thinkingToggle: "切换助手思考/工作输出", diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts index fb4103273bd6..2372e19d8bf6 100644 --- a/ui/src/i18n/locales/zh-TW.ts +++ b/ui/src/i18n/locales/zh-TW.ts @@ -183,11 +183,13 @@ export const zh_TW: TranslationMap = { global: "全域", unknown: "未知", showArchived: "顯示已封存項目", + archivedOnly: "僅已封存", activeTooltip: "在過去 {count} 分鐘內更新。", limitTooltip: "要載入的工作階段上限。", globalTooltip: "包含全域工作階段。", unknownTooltip: "包含未知工作階段。", showArchivedTooltip: "包含已封存工作階段。", + archivedOnlyTooltip: "僅顯示已封存的工作階段。", minutesPlaceholder: "分鐘", searchPlaceholder: "依金鑰、代理程式、標籤、類型篩選…", selected: "已選取 {count} 個", @@ -254,6 +256,13 @@ export const zh_TW: TranslationMap = { sessionId: "工作階段 ID", activeRun: "進行中的執行", archived: "已封存", + pinned: "已釘選", + renameSession: "重新命名工作階段", + renameSessionPrompt: "重新命名工作階段", + pinSession: "釘選工作階段", + unpinSession: "取消釘選工作階段", + archiveSession: "封存工作階段", + restoreSession: "還原工作階段", loadingCheckpoints: "正在載入檢查點…", noCheckpoints: "此工作階段沒有記錄壓縮檢查點。", noSummary: "未擷取摘要。", @@ -1324,6 +1333,7 @@ export const zh_TW: TranslationMap = { }, chat: { disconnected: "已斷開與網關的連接。", + archivedSessionDisabled: "還原此工作階段以傳送訊息。", refreshTitle: "刷新聊天數據", settings: "聊天設定", thinkingToggle: "切換助手思考/工作輸出", diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index c7b01771c26d..d51aa723cb6f 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -1968,16 +1968,40 @@ overflow-y: auto; } +/* Rows are single-line and minimal: label + trailing time/spinner. The + whole row (option button + aside) shares one hover surface so the + session-row hover-reveal actions feel like part of the row. */ +.chat-session-picker__option-row { + display: flex; + align-items: center; + min-width: 0; + padding-right: 6px; + border: 1px solid transparent; + border-radius: var(--radius-sm); + transition: + background var(--duration-fast) ease, + border-color var(--duration-fast) ease; +} + +.chat-session-picker__option-row:hover, +.chat-session-picker__option-row:focus-within { + background: var(--bg-hover); + border-color: color-mix(in srgb, var(--border) 78%, transparent); +} + +.chat-session-picker__option-row--selected { + background: color-mix(in srgb, var(--accent-subtle) 72%, transparent); + border-color: color-mix(in srgb, var(--accent) 28%, transparent); +} + .chat-session-picker__option { display: flex; align-items: center; - justify-content: space-between; - gap: 10px; - width: 100%; - min-height: 44px; - padding: 8px 10px; - border: 1px solid transparent; - border-radius: var(--radius-sm); + flex: 1 1 auto; + min-width: 0; + min-height: 34px; + padding: 6px 8px 6px 10px; + border: none; background: transparent; color: var(--text); font: inherit; @@ -1985,55 +2009,31 @@ cursor: pointer; } -.chat-session-picker__option:hover, -.chat-session-picker__option:focus-visible { - background: var(--bg-hover); - border-color: color-mix(in srgb, var(--border) 78%, transparent); -} - -.chat-session-picker__option--selected { - background: color-mix(in srgb, var(--accent-subtle) 72%, transparent); - border-color: color-mix(in srgb, var(--accent) 28%, transparent); -} - -.chat-session-picker__option-main { - display: grid; - gap: 3px; +.chat-session-picker__option-label { min-width: 0; -} - -.chat-session-picker__option-label, -.chat-session-picker__option-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; -} - -.chat-session-picker__option-label { color: var(--text-strong); font-size: 13px; + font-weight: 500; +} + +.chat-session-picker__option-row--selected .chat-session-picker__option-label { font-weight: 600; } -.chat-session-picker__option-meta { - color: var(--muted); - font-size: 12px; /* was 11px */ -} +/* Touch has no hover: the picker is a deliberate management surface, so + keep the actions visible there instead of unreachable. */ +@media (hover: none) { + .chat-session-picker .session-action { + opacity: 1; + pointer-events: auto; + } -.chat-session-picker__option-check { - display: inline-flex; - width: 16px; - height: 16px; - color: var(--accent); - flex: 0 0 auto; -} - -.chat-session-picker__option-check svg { - width: 16px; - height: 16px; - stroke: currentColor; - fill: none; - stroke-width: 1.5px; + .chat-session-picker .session-row-trail { + opacity: 0; + } } .chat-session-picker__status { diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 973a08345397..9efcb15facc9 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -6686,3 +6686,143 @@ details[open] > .ov-expandable-toggle::after { gap: 4px; } } + +/* ── Session row primitives ── + Shared by the sidebar recents list and the chat session picker. Rows read + as plain text lines: the trailing aside stacks a relative time (or the run + spinner) and the management actions in one grid cell, so hovering swaps + them without any layout shift. The pin action doubles as the pinned badge + and stays visible for pinned rows even without hover. */ +.session-row-host { + position: relative; +} + +.session-row-aside { + display: grid; + align-items: center; + justify-items: end; + flex: 0 0 auto; +} + +.session-row-aside > * { + grid-area: 1 / 1; +} + +.session-row-trail { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--muted); + font-size: 11px; + font-variant-numeric: tabular-nums; + white-space: nowrap; + transition: opacity var(--duration-fast) ease; + /* Display-only, and its opacity transition forms a stacking context that + would otherwise sit above the action buttons and swallow their clicks. */ + pointer-events: none; +} + +.session-row-actions { + display: inline-flex; + align-items: center; + gap: 1px; +} + +.session-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: var(--radius-sm); + background: transparent; + color: var(--muted); + cursor: pointer; + opacity: 0; + pointer-events: none; + transition: + opacity var(--duration-fast) ease, + background var(--duration-fast) ease, + color var(--duration-fast) ease; +} + +.session-action svg { + width: 14px; + height: 14px; + stroke: currentColor; + fill: none; + stroke-width: 1.6px; + stroke-linecap: round; + stroke-linejoin: round; +} + +.session-action:hover:not(:disabled) { + background: color-mix(in srgb, var(--bg-hover) 92%, transparent); + color: var(--text); +} + +.session-action:disabled { + cursor: not-allowed; +} + +.session-row-host:hover .session-action, +.session-row-host:focus-within .session-action { + opacity: 1; + pointer-events: auto; +} + +/* Disabled actions still surface on hover (dimmed) so the tooltip can + explain why archiving is unavailable instead of the button vanishing. */ +.session-row-host:hover .session-action:disabled, +.session-row-host:focus-within .session-action:disabled { + opacity: 0.35; + pointer-events: auto; +} + +.session-row-host:hover .session-row-trail, +.session-row-host:focus-within .session-row-trail { + opacity: 0; +} + +/* Pinned badge: accent pin visible without hover. A live run wins the + trailing slot, so the badge only holds it while the session is idle; + both indicators share one grid cell and would overlap otherwise. */ +.session-row-host--pinned .session-action--pin { + color: var(--accent); +} + +.session-row-host--pinned:not(.session-row-host--running) .session-action--pin { + opacity: 1; + pointer-events: auto; +} + +.session-row-host--pinned:not(.session-row-host--running) .session-row-trail { + opacity: 0; +} + +.session-run-spinner { + box-sizing: border-box; + display: inline-block; + width: 12px; + height: 12px; + flex: 0 0 auto; + border: 1.5px solid color-mix(in srgb, var(--accent) 28%, transparent); + border-top-color: var(--accent); + border-radius: var(--radius-full); + animation: session-run-spin 0.8s linear infinite; +} + +@keyframes session-run-spin { + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: reduce) { + .session-run-spinner { + animation: none; + border-color: var(--accent); + } +} diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index b4e701a88f75..ed94e68be284 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -834,16 +834,13 @@ } .sidebar-recent-session { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; + display: flex; align-items: center; - gap: 8px; min-height: 34px; - padding: 5px 9px; + padding-right: 4px; border: 1px solid transparent; border-radius: var(--radius-md); color: var(--muted); - text-decoration: none; transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease, @@ -853,7 +850,6 @@ .sidebar-recent-session:hover { background: color-mix(in srgb, var(--bg-hover) 78%, transparent); color: var(--text); - text-decoration: none; } .sidebar-recent-session--active { @@ -862,11 +858,19 @@ color: var(--text-strong); } -.sidebar-recent-session__body { +.sidebar-recent-session__link { display: flex; - align-items: baseline; + align-items: center; gap: 8px; + flex: 1 1 auto; min-width: 0; + padding: 5px 4px 5px 9px; + color: inherit; + text-decoration: none; +} + +.sidebar-recent-session__link:hover { + text-decoration: none; } .sidebar-recent-session__name { @@ -885,21 +889,8 @@ color: var(--text-strong); } -.sidebar-recent-session__meta { - flex: 0 0 auto; - font-size: 11px; - font-variant-numeric: tabular-nums; - color: var(--muted); - opacity: 0.85; -} - -.sidebar-recent-session__live { - width: 7px; - height: 7px; - border-radius: var(--radius-full); - background: var(--ok); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 12%, transparent); - animation: pulse-subtle 2s ease-in-out infinite; +.sidebar-recent-session__aside { + padding-right: 2px; } .sidebar-nav::-webkit-scrollbar { diff --git a/ui/src/ui/app-chat.test.ts b/ui/src/ui/app-chat.test.ts index ccf0e2a383df..be1bf84fa39c 100644 --- a/ui/src/ui/app-chat.test.ts +++ b/ui/src/ui/app-chat.test.ts @@ -54,6 +54,7 @@ let markQueuedChatSendsWaitingForReconnect: typeof import("./app-chat.ts").markQ let retryReconnectableQueuedChatSends: typeof import("./app-chat.ts").retryReconnectableQueuedChatSends; let recordChatSendServerTiming: typeof import("./app-chat.ts").recordChatSendServerTiming; let recordFirstAssistantChatTiming: typeof import("./app-chat.ts").recordFirstAssistantChatTiming; +let createChatSessionsLoadOverrides: typeof import("./app-chat.ts").createChatSessionsLoadOverrides; async function loadChatHelpers(): Promise { ({ @@ -70,6 +71,7 @@ async function loadChatHelpers(): Promise { retryReconnectableQueuedChatSends, recordChatSendServerTiming, recordFirstAssistantChatTiming, + createChatSessionsLoadOverrides, } = await import("./app-chat.ts")); } @@ -249,6 +251,15 @@ describe("refreshChat", () => { await loadChatHelpers(); }); + it("keeps Chat session refreshes active-only when Sessions shows archived rows", () => { + expect(createChatSessionsLoadOverrides({ sessionsShowArchived: true })).toMatchObject({ + activeMinutes: 0, + limit: 50, + showArchived: false, + preserveSessionsViewResult: true, + }); + }); + it("dispatches chat refresh work without waiting for slow history or metadata RPCs", async () => { const request = vi.fn(() => pendingPromise()); const requestUpdate = vi.fn(); @@ -1489,6 +1500,48 @@ describe("handleSendChat", () => { expect(host.refreshSessionsAfterChat.size).toBe(0); }); + it("keeps a completed reset successful without replacing the Sessions table", async () => { + const request = vi.fn(async (method: string, params?: unknown) => { + if (method === "chat.send") { + const payload = requireRecord(params, "chat send payload"); + return { runId: payload.idempotencyKey, status: "ok" }; + } + if (method === "chat.history") { + return { messages: [] }; + } + if (method === "sessions.list") { + return createSessionsResult([ + row("agent:main", { hasActiveRun: false, status: "done" }), + ]); + } + throw new Error(`Unexpected request: ${method}`); + }); + const archivedSessions = createSessionsResult([ + row("agent:main:archived", { archived: true, status: "done" }), + ]); + const host = makeHost({ + client: { request } as unknown as ChatHost["client"], + chatMessage: "/reset", + sessionKey: "agent:main", + tab: "sessions", + sessionsShowArchived: true, + sessionsResult: archivedSessions, + }); + + await handleSendChat(host); + + const runState = host as ChatHost & { lastLocalTerminalReconcile?: unknown }; + expect(runState.lastLocalTerminalReconcile).toMatchObject({ + phase: "done", + sessionKey: "agent:main", + sessionStatus: "done", + }); + await vi.waitFor(() => + expect(request.mock.calls.some(([method]) => method === "sessions.list")).toBe(true), + ); + expect(host.sessionsResult).toBe(archivedSessions); + }); + it("marks terminal error ACK sends failed instead of accepting the queued message", async () => { const request = vi.fn(async (method: string, params?: unknown) => { if (method === "chat.send") { diff --git a/ui/src/ui/app-chat.ts b/ui/src/ui/app-chat.ts index 3232fc08cdce..090f9dbfe090 100644 --- a/ui/src/ui/app-chat.ts +++ b/ui/src/ui/app-chat.ts @@ -205,7 +205,7 @@ export const CHAT_SESSIONS_ACTIVE_MINUTES = 0; export const CHAT_SESSIONS_REFRESH_LIMIT = 50; export function createChatSessionsLoadOverrides( - state: { sessionsShowArchived?: boolean }, + _state: { sessionsShowArchived?: boolean }, options: { offset?: number; append?: boolean; search?: string | null } = {}, ): LoadSessionsOverrides { const overrides: LoadSessionsOverrides = { @@ -214,10 +214,9 @@ export function createChatSessionsLoadOverrides( includeGlobal: true, includeUnknown: true, configuredAgentsOnly: true, + showArchived: false, + preserveSessionsViewResult: true, }; - if (typeof state.sessionsShowArchived === "boolean") { - overrides.showArchived = state.sessionsShowArchived; - } const search = normalizeOptionalString(options.search ?? undefined); if (search) { overrides.search = search; diff --git a/ui/src/ui/app-gateway.sessions.node.test.ts b/ui/src/ui/app-gateway.sessions.node.test.ts index 778e0f1cfd16..9305a9810fe8 100644 --- a/ui/src/ui/app-gateway.sessions.node.test.ts +++ b/ui/src/ui/app-gateway.sessions.node.test.ts @@ -9,6 +9,7 @@ const flushChatQueueForEventMock = vi.fn(); const handleChatEventMock = vi.fn((_state?: any) => "idle"); const handleSessionOperationEventMock = vi.fn(); const recordFirstAssistantChatTimingMock = vi.fn(); +const switchChatSessionMock = vi.fn(); vi.mock("./app-chat.ts", () => ({ CHAT_SESSIONS_ACTIVE_MINUTES: 10, @@ -49,6 +50,9 @@ vi.mock("./app-settings.ts", () => ({ refreshActiveTab: vi.fn(), setLastActiveSessionKey: vi.fn(), })); +vi.mock("./app-render.helpers.ts", () => ({ + switchChatSession: switchChatSessionMock, +})); vi.mock("./app-tool-stream.ts", () => ({ handleAgentEvent: vi.fn(), handleSessionOperationEvent: handleSessionOperationEventMock, @@ -101,6 +105,7 @@ const { addExecApproval } = await vi.importActual { vi.doUnmock("./app-chat.ts"); vi.doUnmock("./app-settings.ts"); + vi.doUnmock("./app-render.helpers.ts"); vi.doUnmock("./app-tool-stream.ts"); vi.doUnmock("./controllers/agents.ts"); vi.doUnmock("./controllers/assistant-identity.ts"); @@ -193,6 +198,27 @@ describe("handleGatewayEvent sessions.changed", () => { }); }); + it("does not replace the Sessions table after a background chat run completes", () => { + loadSessionsMock.mockReset(); + handleChatEventMock.mockReset().mockReturnValue("final"); + const host = createHost(); + host.tab = "sessions"; + host.refreshSessionsAfterChat.set("run-1", { sessionKey: "agent:ops:main" }); + + handleGatewayEvent(host, { + type: "event", + event: "chat", + payload: { state: "final", runId: "run-1", sessionKey: "agent:ops:main" }, + seq: 1, + }); + + expect(loadSessionsMock).toHaveBeenCalledWith(host, { + activeMinutes: 10, + limit: 25, + agentId: "ops", + }); + }); + it("scopes selected-global chat session refreshes after a completed run", () => { loadSessionsMock.mockReset(); handleChatEventMock.mockReset().mockReturnValue("final"); @@ -238,6 +264,51 @@ describe("handleGatewayEvent sessions.changed", () => { expect(loadSessionsMock).not.toHaveBeenCalled(); }); + it("switches to the main session when another client deletes the selected session", () => { + loadSessionsMock.mockReset(); + switchChatSessionMock.mockReset(); + applySessionsChangedEventMock.mockReset().mockReturnValue({ + applied: true, + change: "deleted", + deletedSession: { key: "agent:ops:archived", agentId: "ops", selected: true }, + }); + const host = createHost(); + host.sessionKey = "agent:ops:archived"; + + handleGatewayEvent(host, { + type: "event", + event: "sessions.changed", + payload: { + sessionKey: "agent:ops:archived", + reason: "delete", + }, + seq: 1, + }); + + expect(switchChatSessionMock).toHaveBeenCalledWith(host, "agent:ops:main"); + expect(loadSessionsMock).not.toHaveBeenCalled(); + }); + + it("preserves the selected agent for canonical global deletion events", () => { + switchChatSessionMock.mockReset(); + applySessionsChangedEventMock.mockReset().mockReturnValue({ + applied: true, + change: "deleted", + deletedSession: { key: "global", agentId: "work", selected: true }, + }); + const host = createHost(); + host.sessionKey = "global"; + + handleGatewayEvent(host, { + type: "event", + event: "sessions.changed", + payload: { sessionKey: "global", agentId: "work", reason: "delete" }, + seq: 1, + }); + + expect(switchChatSessionMock).toHaveBeenCalledWith(host, "agent:work:main"); + }); + it("flushes queued chat work when an applied session patch clears the active run", () => { loadSessionsMock.mockReset(); clearPendingQueueItemsForRunMock.mockReset(); diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index a991e03cb7d8..93caa3d8c304 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -19,6 +19,7 @@ import { scopedAgentParamsForSession, } from "./app-chat.ts"; import type { EventLogEntry } from "./app-events.ts"; +import { switchChatSession } from "./app-render.helpers.ts"; import { applySettings, loadCron, @@ -553,6 +554,31 @@ function resolveMainSessionFallback(host: GatewayHost): string { }); } +function resolveDeletedSessionFallback( + host: GatewayHost, + deletedSession: { key: string; agentId?: string }, +): string { + const snapshot = host.hello?.snapshot as + | { sessionDefaults?: SessionDefaultsSnapshot } + | undefined; + const defaults = snapshot?.sessionDefaults; + const configuredMainKey = defaults?.mainKey?.trim() || host.agentsList?.mainKey?.trim(); + const parsedConfiguredMainKey = parseAgentSessionKey(configuredMainKey ?? ""); + const parsedSelectedKey = parseAgentSessionKey(host.sessionKey); + const parsedDeletedKey = parseAgentSessionKey(deletedSession.key); + return buildAgentMainSessionKey({ + agentId: + parsedSelectedKey?.agentId ?? + parsedDeletedKey?.agentId ?? + deletedSession.agentId ?? + resolveDefaultAgentId(host), + mainKey: + parsedConfiguredMainKey?.rest ?? + configuredMainKey ?? + parseAgentSessionKey(defaults?.mainSessionKey ?? "")?.rest, + }); +} + function resolveDefaultAgentId(host: GatewayHost): string { return resolveUiDefaultAgentId(host); } @@ -1388,6 +1414,12 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { runIdBeforeApply, ); } + if (result.deletedSession?.selected) { + switchChatSession( + host as unknown as Parameters[0], + resolveDeletedSessionFallback(host, result.deletedSession), + ); + } return; } if (isChatTurnSessionChangedPayload(evt.payload)) { diff --git a/ui/src/ui/app-render.assistant-avatar.test.ts b/ui/src/ui/app-render.assistant-avatar.test.ts index a2be264032c6..68523c70d9e0 100644 --- a/ui/src/ui/app-render.assistant-avatar.test.ts +++ b/ui/src/ui/app-render.assistant-avatar.test.ts @@ -361,6 +361,18 @@ describe("renderApp assistant avatar routing", () => { expect(chatProps.current?.autoExpandToolCalls).toBe(false); }); + it("makes an archived chat session read-only", () => { + renderApp( + createState({ + tab: "chat", + selectedChatSessionArchived: true, + }), + ); + + expect(chatProps.current?.canSend).toBe(false); + expect(chatProps.current?.disabledReason).toBe("Restore this session to send messages."); + }); + it("does not render chat errors in non-chat page headers", () => { const container = document.createElement("div"); diff --git a/ui/src/ui/app-render.helpers.node.test.ts b/ui/src/ui/app-render.helpers.node.test.ts index 307b07b7a931..6cb54100f7aa 100644 --- a/ui/src/ui/app-render.helpers.node.test.ts +++ b/ui/src/ui/app-render.helpers.node.test.ts @@ -8,6 +8,7 @@ const { loadChatHistoryMock, createSessionAndRefreshMock, loadSessionsMock, + patchSessionMock, syncSelectedSessionMessageSubscriptionMock, } = vi.hoisted(() => ({ refreshChatMock: vi.fn(), @@ -17,6 +18,7 @@ const { loadChatHistoryMock: vi.fn(), createSessionAndRefreshMock: vi.fn(), loadSessionsMock: vi.fn(), + patchSessionMock: vi.fn(), syncSelectedSessionMessageSubscriptionMock: vi.fn(), })); @@ -67,6 +69,7 @@ vi.mock("./controllers/chat.ts", () => ({ vi.mock("./controllers/sessions.ts", () => ({ createSessionAndRefresh: createSessionAndRefreshMock, loadSessions: loadSessionsMock, + patchSession: patchSessionMock, syncSelectedSessionMessageSubscription: syncSelectedSessionMessageSubscriptionMock, })); @@ -75,9 +78,12 @@ import { dismissChatError, dismissRealtimeTalkError, handleChatManualRefresh, + isCurrentChatSessionArchived, + openCurrentSessionCheckpoints, isCronSessionKey, isTerminalAvailable, parseSessionKey, + patchSessionFromSessionsView, resolveAssistantAttachmentAuthToken, resolveDashboardHeaderContext, resolveSessionOptionGroups, @@ -98,6 +104,7 @@ beforeEach(() => { loadChatHistoryMock.mockReset(); createSessionAndRefreshMock.mockReset(); loadSessionsMock.mockReset(); + patchSessionMock.mockReset(); syncSelectedSessionMessageSubscriptionMock.mockReset(); }); @@ -1055,6 +1062,112 @@ describe("createChatSession", () => { }); describe("switchChatSession", () => { + it("finds an archived current session in the chat row cache", () => { + const state = createChatSessionState({ + sessionKey: "agent:ops:archived", + sessionsResult: null, + chatAgentSessionRowsByAgent: { + ops: [row({ key: "agent:ops:archived", archived: true })], + }, + }); + + expect(isCurrentChatSessionArchived(state)).toBe(true); + }); + + it("uses selected chat history archive status after active caches evict the row", () => { + const state = createChatSessionState({ + sessionKey: "agent:ops:archived", + selectedChatSessionArchived: true, + sessionsResult: null, + chatAgentSessionRowsByAgent: {}, + }); + + expect(isCurrentChatSessionArchived(state)).toBe(true); + }); + + it("loads checkpoints within the selected agent scope", async () => { + const setTab = vi.fn(); + const state = createChatSessionState({ + sessionKey: "global", + assistantAgentId: "ops", + selectedChatSessionArchived: true, + setTab, + }); + + openCurrentSessionCheckpoints(state); + await Promise.resolve(); + + expect(setTab).toHaveBeenCalledWith("sessions"); + expect(state.sessionsExpandedCheckpointKey).toBe("global"); + expect(state.sessionsShowArchived).toBe(true); + expect(loadSessionsMock).toHaveBeenCalledWith(state, { + activeMinutes: 0, + limit: 0, + includeGlobal: true, + includeUnknown: true, + showArchived: true, + agentId: "ops", + }); + }); + + it("switches chat and message subscription after archiving the selected session", async () => { + const state = createChatSessionState({ + sessionKey: "agent:ops:review", + tab: "sessions", + agentsList: { defaultId: "main", mainKey: "home", agents: [], scope: "global" }, + }); + patchSessionMock.mockResolvedValue(true); + loadChatHistoryMock.mockResolvedValue(undefined); + loadSessionsMock.mockResolvedValue(undefined); + syncSelectedSessionMessageSubscriptionMock.mockResolvedValue(undefined); + + await patchSessionFromSessionsView(state, "agent:ops:review", { archived: true }); + await Promise.resolve(); + + expect(patchSessionMock).toHaveBeenCalledWith(state, "agent:ops:review", { + archived: true, + }); + expect(state.sessionKey).toBe("agent:ops:home"); + expect(state.selectedChatSessionArchived).toBe(false); + expect(loadChatHistoryMock).toHaveBeenCalledWith(state); + expect(syncSelectedSessionMessageSubscriptionMock).toHaveBeenCalledWith(state); + expect(loadSessionsMock).toHaveBeenCalledWith(state, { + activeMinutes: 120, + limit: 50, + includeGlobal: true, + includeUnknown: true, + showArchived: false, + agentId: "ops", + }); + }); + + it("clears selected chat archive status when restoring the selected session", async () => { + const state = createChatSessionState({ + sessionKey: "agent:ops:review", + selectedChatSessionArchived: true, + }); + patchSessionMock.mockResolvedValue(true); + + await patchSessionFromSessionsView(state, "agent:ops:review", { archived: false }); + + expect(state.selectedChatSessionArchived).toBe(false); + expect(state.sessionKey).toBe("agent:ops:review"); + expect(loadChatHistoryMock).not.toHaveBeenCalled(); + }); + + it("removes an archived row from the Sessions bulk selection", async () => { + const state = createChatSessionState({ + sessionKey: "agent:ops:home", + sessionsSelectedKeys: new Set(["agent:ops:review", "agent:ops:keep"]), + }); + patchSessionMock.mockResolvedValue(true); + + await patchSessionFromSessionsView(state, "agent:ops:review", { archived: true }); + + expect(state.sessionsSelectedKeys).toEqual(new Set(["agent:ops:keep"])); + expect(state.sessionKey).toBe("agent:ops:home"); + }); + it("waits for the initial history and message subscription when requested", async () => { let resolveHistory!: () => void; let resolveSubscription!: () => void; @@ -1129,6 +1242,23 @@ describe("switchChatSession", () => { expect(state.sessionKey).toBe("agent:main:review"); }); + it("preserves known archive status when switching to an archived session", () => { + const state = createChatSessionState({ + sessionKey: "agent:ops:home", + sessionsResult: { + ts: 1, + path: "(multiple)", + count: 1, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [row({ key: "agent:ops:archived", archived: true })], + }, + }); + + switchChatSession(state, "agent:ops:archived"); + + expect(state.selectedChatSessionArchived).toBe(true); + }); + it("refreshes the chat avatar after clearing session-scoped state", async () => { const settings = createSettings(); const state = { diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 049650d4b793..e1bf4d7ec580 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -28,6 +28,7 @@ import type { ChatState } from "./controllers/chat.ts"; import { createSessionAndRefresh, loadSessions, + patchSession, syncSelectedSessionMessageSubscription, } from "./controllers/sessions.ts"; import { isGatewayMethodAdvertised } from "./gateway-methods.ts"; @@ -35,6 +36,8 @@ import { icons } from "./icons.ts"; import { iconForTab, isSettingsTab, pathForTab, titleForTab, type Tab } from "./navigation.ts"; import { isCronSessionKey, parseSessionKey, resolveSessionDisplayName } from "./session-display.ts"; import { + areUiSessionKeysEquivalent, + buildAgentMainSessionKey, isSessionKeyTiedToAgent, normalizeAgentId, parseAgentSessionKey, @@ -180,6 +183,7 @@ function resetChatStateForSessionSwitch(state: AppViewState, sessionKey: string) saveChatQueueForSession(state, previousSessionKey); saveChatMessagesForSession(state, previousSessionKey); state.sessionKey = sessionKey; + state.selectedChatSessionArchived = false; if (previousSessionKey !== sessionKey) { resetChatSessionPickerState(state); } @@ -725,6 +729,7 @@ function switchChatSessionInternal( state.chatSessionPickerResult?.sessions.find((row) => row.key === nextSessionKey); const nextSessionLabel = resolveSessionDisplayName(nextSessionKey, nextSessionRow); resetChatStateForSessionSwitch(state, nextSessionKey); + state.selectedChatSessionArchived = nextSessionRow?.archived === true; if (previousSessionKey !== nextSessionKey) { state.announceSessionSwitch?.(nextSessionKey, nextSessionLabel); } @@ -775,6 +780,69 @@ export function switchChatSessionAndWait( ); } +export function isCurrentChatSessionArchived(state: AppViewState): boolean { + if (state.selectedChatSessionArchived === true) { + return true; + } + return [ + ...(state.sessionsResult?.sessions ?? []), + ...Object.values(state.chatAgentSessionRowsByAgent ?? {}).flat(), + ].some( + (row) => row.archived === true && areUiSessionKeysEquivalent(row.key, state.sessionKey), + ); +} + +export function openCurrentSessionCheckpoints(state: AppViewState): void { + const showArchived = isCurrentChatSessionArchived(state); + state.sessionsExpandedCheckpointKey = state.sessionKey; + state.sessionsFilterActive = ""; + state.sessionsFilterLimit = ""; + state.sessionsIncludeGlobal = true; + state.sessionsIncludeUnknown = true; + state.sessionsShowArchived = showArchived; + state.sessionsSearchQuery = ""; + state.sessionsSelectedKeys = new Set(); + state.sessionsPage = 0; + state.setTab("sessions"); + void loadSessions(state, { + activeMinutes: 0, + limit: 0, + includeGlobal: true, + includeUnknown: true, + showArchived, + ...scopedAgentListParamsForSession(state, state.sessionKey), + }); +} + +export async function patchSessionFromSessionsView( + state: AppViewState, + key: string, + patch: { label?: string | null; archived?: boolean; pinned?: boolean }, +): Promise { + const patched = await patchSession(state, key, patch); + if (patched && patch.archived !== undefined && state.sessionsSelectedKeys?.has(key)) { + const selectedKeys = new Set(state.sessionsSelectedKeys); + selectedKeys.delete(key); + state.sessionsSelectedKeys = selectedKeys; + } + const patchesSelectedArchiveState = + patch.archived !== undefined && areUiSessionKeysEquivalent(key, state.sessionKey); + if (!patched || !patchesSelectedArchiveState) { + return patched; + } + state.selectedChatSessionArchived = patch.archived; + if (!patch.archived) { + return true; + } + const parsed = parseAgentSessionKey(key); + const fallbackKey = buildAgentMainSessionKey({ + agentId: parsed?.agentId ?? state.agentsList?.defaultId ?? "main", + mainKey: state.agentsList?.mainKey ?? undefined, + }); + switchChatSession(state, fallbackKey); + return true; +} + export function dismissRealtimeTalkError(state: AppViewState) { if (state.realtimeTalkStatus !== "error") { return; diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 5bebe40e4c0f..5f4d446d801a 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -9,7 +9,6 @@ import { hasAbortableSessionRun, refreshChat, refreshChatCommands, - scopedAgentListParamsForSession, scopedAgentParamsForSession, } from "./app-chat.ts"; import { DEFAULT_CRON_FORM } from "./app-defaults.ts"; @@ -25,7 +24,10 @@ import { createChatSession, dismissChatError, dismissRealtimeTalkError, + isCurrentChatSessionArchived, isTerminalAvailable, + openCurrentSessionCheckpoints, + patchSessionFromSessionsView, switchChatSession, switchChatSessionAndWait, } from "./app-render.helpers.ts"; @@ -132,6 +134,7 @@ import { loadNodes } from "./controllers/nodes.ts"; import { loadPresence } from "./controllers/presence.ts"; import { branchSessionFromCheckpoint, + compareSessionRowsByUpdatedAt, createSessionAndRefresh, deleteSessionsAndRefresh, loadSessions, @@ -184,11 +187,13 @@ import { isCronSessionKey, resolveSessionDisplayName } from "./session-display.t import { areUiSessionKeysEquivalent, buildAgentMainSessionKey, + canArchiveSessionRow, isSessionKeyTiedToAgent, isSubagentSessionKey, normalizeAgentId, parseAgentSessionKey, resolveAgentIdFromSessionKey, + resolveUiConfiguredMainKey, resolveUiSelectedGlobalAgentId, uiSessionRowMatchesSelectedChat, } from "./session-key.ts"; @@ -558,7 +563,7 @@ function resolveSidebarRecentSessions(state: AppViewState): GatewaySessionRow[] !isActiveSidebarSessionRow(state, row.key) && (!shouldFilterByAgent || isSidebarSessionForSelectedAgent(state, row, selectedAgentId)), ) - .toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)) + .toSorted(compareSessionRowsByUpdatedAt) .slice(0, 9); } @@ -574,21 +579,23 @@ function isActiveSidebarSessionRow(state: AppViewState, rowKey: string): boolean // open chat from every tab. function renderSidebarChatFallbackRow(state: AppViewState) { return html` - { - if (event.defaultPrevented || event.button !== 0 || hasModifierKey(event)) { - return; - } - event.preventDefault(); - state.setTab("chat" as import("./navigation.ts").Tab); - }} > - + { + if (event.defaultPrevented || event.button !== 0 || hasModifierKey(event)) { + return; + } + event.preventDefault(); + state.setTab("chat" as import("./navigation.ts").Tab); + }} + > ${t("nav.chat")} - - + + `; } @@ -738,34 +745,75 @@ function renderSidebarRecentSession(state: AppViewState, row: GatewaySessionRow) const label = resolveSessionDisplayName(row.key, row); const meta = row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : ""; const href = `${pathForTab("chat", state.basePath)}?session=${encodeURIComponent(row.key)}`; + const pinned = row.pinned === true; + const running = row.hasActiveRun === true; + const controlsDisabled = !state.connected || !state.client; + const archiveAllowed = canArchiveSessionRow(row, resolveUiConfiguredMainKey(state)); + const rowClass = [ + "sidebar-recent-session", + "session-row-host", + active ? "sidebar-recent-session--active" : "", + pinned ? "session-row-host--pinned" : "", + running ? "session-row-host--running" : "", + ] + .filter(Boolean) + .join(" "); return html` - { - if (event.defaultPrevented || event.button !== 0 || hasModifierKey(event)) { - return; - } - event.preventDefault(); - if (!isActiveSidebarSessionRow(state, row.key)) { - switchChatSession(state, row.key); - } - state.setTab("chat" as import("./navigation.ts").Tab); - }} - > - +
+ { + if (event.defaultPrevented || event.button !== 0 || hasModifierKey(event)) { + return; + } + event.preventDefault(); + if (!isActiveSidebarSessionRow(state, row.key)) { + switchChatSession(state, row.key); + } + state.setTab("chat" as import("./navigation.ts").Tab); + }} + > ${label} - ${meta ? html`${meta}` : nothing} + + + + ${running + ? html`` + : meta} + + + + + - ${row.hasActiveRun - ? html`` - : nothing} - +
`; } @@ -1496,7 +1544,12 @@ export function renderApp(state: AppViewState) { const presenceCount = state.presenceEntries.length; const sessionsCount = state.sessionsResult?.count ?? null; const cronNext = state.cronStatus?.nextWakeAtMs ?? null; - const chatDisabledReason = state.connected ? null : t("chat.disconnected"); + const chatSessionArchived = isCurrentChatSessionArchived(state); + const chatDisabledReason = !state.connected + ? t("chat.disconnected") + : chatSessionArchived + ? t("chat.archivedSessionDisabled") + : null; const isChat = state.tab === "chat"; const headerError = !isChat && state.lastError !== state.chatError ? state.lastError : null; const chatViewError = state.lastError; @@ -2994,6 +3047,7 @@ export function renderApp(state: AppViewState) { includeGlobal: state.sessionsIncludeGlobal, includeUnknown: state.sessionsIncludeUnknown, showArchived: state.sessionsShowArchived, + mainKey: state.agentsList?.mainKey ?? "main", filtersCollapsed: state.sessionsFiltersCollapsed, basePath: state.basePath, searchQuery: state.sessionsSearchQuery, @@ -3038,7 +3092,7 @@ export function renderApp(state: AppViewState) { state.sessionsFilterLimit = ""; state.sessionsIncludeGlobal = true; state.sessionsIncludeUnknown = true; - state.sessionsShowArchived = true; + state.sessionsShowArchived = false; state.sessionsSearchQuery = ""; state.sessionsSelectedKeys = new Set(); state.sessionsPage = 0; @@ -3047,7 +3101,7 @@ export function renderApp(state: AppViewState) { limit: 0, includeGlobal: true, includeUnknown: true, - showArchived: true, + showArchived: false, }); }, onSearchChange: (q) => { @@ -3067,7 +3121,7 @@ export function renderApp(state: AppViewState) { state.sessionsPage = 0; }, onRefresh: () => void loadSessions(state), - onPatch: (key, patch) => void patchSession(state, key, patch), + onPatch: (key, patch) => void patchSessionFromSessionsView(state, key, patch), onToggleSelect: (key) => { const next = new Set(state.sessionsSelectedKeys); if (next.has(key)) { @@ -3931,7 +3985,7 @@ export function renderApp(state: AppViewState) { realtimeTalkOptions: state.realtimeTalkOptions, realtimeTalkCatalogProviders: state.realtimeTalkCatalogProviders, connected: state.connected, - canSend: state.connected, + canSend: state.connected && !chatSessionArchived, disabledReason: chatDisabledReason, error: chatViewError, runStatus: state.chatRunStatus, @@ -3973,14 +4027,7 @@ export function renderApp(state: AppViewState) { onAttachmentsChange: (next) => (state.chatAttachments = next), onSend: () => void state.handleSendChat(), onCompact: () => void state.handleSendChat("/compact", { restoreDraft: true }), - onOpenSessionCheckpoints: () => { - state.sessionsExpandedCheckpointKey = state.sessionKey; - state.setTab("sessions" as import("./navigation.ts").Tab); - void loadSessions(state, { - ...createChatSessionsLoadOverrides(state), - ...scopedAgentListParamsForSession(state, state.sessionKey), - }); - }, + onOpenSessionCheckpoints: () => openCurrentSessionCheckpoints(state), onToggleRealtimeTalk: () => void state.toggleRealtimeTalk(), onToggleRealtimeTalkOptions: () => { state.realtimeTalkOptionsOpen = !state.realtimeTalkOptionsOpen; diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index 8ff024d751d1..78ebd7cda33a 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -144,6 +144,8 @@ export type AppViewState = { chatSessionPickerError: string | null; chatSessionPickerResult: SessionsListResult | null; sessionsResultAgentId?: string | null; + sessionsResultShowArchived?: boolean; + selectedChatSessionArchived?: boolean; chatAgentSessionRowsByAgent?: Record; announceSessionSwitch?: (sessionKey: string, label: string) => void; chatQueue: ChatQueueItem[]; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 0749767741e2..8dda0cba009b 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -503,6 +503,8 @@ export class OpenClawApp extends LitElement { @state() sessionsLoading = false; @state() sessionsResult: SessionsListResult | null = null; + @state() sessionsResultShowArchived = false; + @state() selectedChatSessionArchived = false; @state() sessionsError: string | null = null; @state() sessionsFilterActive = DEFAULT_SESSIONS_FILTERS.activeMinutes; @state() sessionsFilterLimit = DEFAULT_SESSIONS_FILTERS.limit; diff --git a/ui/src/ui/chat/session-controls.ts b/ui/src/ui/chat/session-controls.ts index d6282af794f0..c53766492cbe 100644 --- a/ui/src/ui/chat/session-controls.ts +++ b/ui/src/ui/chat/session-controls.ts @@ -14,8 +14,8 @@ import { resolveChatModelSelectState, } from "../chat-model-select-state.ts"; import { refreshVisibleToolsEffectiveForCurrentSession } from "../controllers/agents.ts"; -import { loadSessions } from "../controllers/sessions.ts"; -import { formatDateTimeMs } from "../format.ts"; +import { loadSessions, patchSession } from "../controllers/sessions.ts"; +import { formatDateTimeMs, formatRelativeTimestamp } from "../format.ts"; import { icons } from "../icons.ts"; import { isMonitoredAuthProvider } from "../model-auth-helpers.ts"; import { pathForTab } from "../navigation.ts"; @@ -23,11 +23,14 @@ import { collectQuotaWindowsFromAuthStatus, formatQuotaReset } from "../provider import { pushUniqueTrimmedSelectOption } from "../select-options.ts"; import { isCronSessionKey, resolveSessionDisplayName } from "../session-display.ts"; import { + areUiSessionKeysEquivalent, buildAgentMainSessionKey, + canArchiveSessionRow, isSessionKeyTiedToAgent, isSubagentSessionKey, normalizeAgentId, parseAgentSessionKey, + resolveUiConfiguredMainKey, } from "../session-key.ts"; import { sessionModelMatchesDefaults } from "../session-model-defaults.ts"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts"; @@ -380,13 +383,7 @@ function createChatSessionPickerRequestParams( return params; } -function projectChatSessionPickerResult( - state: AppViewState, - result: SessionsListResult, -): SessionsListResult { - if (state.sessionsShowArchived) { - return result; - } +function projectChatSessionPickerResult(result: SessionsListResult): SessionsListResult { const sessions = result.sessions.filter((row) => row.key && row.archived !== true); return { ...result, @@ -440,7 +437,6 @@ async function loadChatSessionPickerPage( requestHostUpdate(state); try { const page = projectChatSessionPickerResult( - state, await state.client.request( "sessions.list", createChatSessionPickerRequestParams(state, { query, offset: options.offset }), @@ -601,18 +597,52 @@ function resolveSelectedChatSessionLabel( ); } -function formatChatSessionPickerMeta(row: SessionsListResult["sessions"][number]): string { - const parts = [ +// Rows stay single-line minimal: relative time in the row, full detail +// (surface, model, absolute timestamp) only in the hover tooltip. +function formatChatSessionPickerTooltip( + row: SessionsListResult["sessions"][number], + label: string, +): string { + return [ + label, normalizeOptionalString(row.surface), [normalizeOptionalString(row.modelProvider), normalizeOptionalString(row.model)] .filter(Boolean) .join("/"), - ].filter(Boolean); - const updatedAt = formatDateTimeMs(row.updatedAt, undefined, ""); - if (updatedAt) { - parts.push(updatedAt); + formatDateTimeMs(row.updatedAt, undefined, ""), + ] + .filter(Boolean) + .join(" · "); +} + +async function patchChatSessionFromPicker(params: { + state: AppViewState; + row: SessionsListResult["sessions"][number]; + patch: { label?: string | null; archived?: boolean; pinned?: boolean }; + onSwitchSession: ChatSessionSwitchHandler; +}) { + const { state, row, patch, onSwitchSession } = params; + const patched = await patchSession(state, row.key, patch, { + ...createChatSessionsLoadOverrides(state), + showArchived: false, + }); + if (!patched) { + state.chatSessionPickerError = state.sessionsError ?? "Failed to update session"; + return; } - return parts.join(" · "); + if (patch.archived === true && areUiSessionKeysEquivalent(row.key, state.sessionKey)) { + const parsed = parseAgentSessionKey(row.key); + const fallbackKey = buildAgentMainSessionKey({ + agentId: parsed?.agentId ?? state.agentsList?.defaultId ?? "main", + mainKey: state.agentsList?.mainKey ?? undefined, + }); + closeChatSessionPicker(state); + onSwitchSession(state, fallbackKey); + return; + } + await loadChatSessionPickerPage(state, { + query: state.chatSessionPickerAppliedQuery, + }); } function renderChatSessionPicker(params: { @@ -759,38 +789,119 @@ function renderChatSessionPickerPopover( (entry) => entry.row.key, (entry) => { const { row, label } = entry; - const meta = formatChatSessionPickerMeta(row); const selected = row.key === state.sessionKey; + const pinned = row.pinned === true; + const running = row.hasActiveRun === true; + const archiveAllowed = canArchiveSessionRow(row, resolveUiConfiguredMainKey(state)); + const rowClass = [ + "chat-session-picker__option-row", + "session-row-host", + selected ? "chat-session-picker__option-row--selected" : "", + pinned ? "session-row-host--pinned" : "", + running ? "session-row-host--running" : "", + ] + .filter(Boolean) + .join(" "); + const updatedAt = formatRelativeTimestamp(row.updatedAt, { fallback: "" }); return html` - + + + ${running + ? html`` + : updatedAt} + + + + + + - ${selected - ? html`` - : ""} - + `; }, )} diff --git a/ui/src/ui/chat/slash-command-executor.node.test.ts b/ui/src/ui/chat/slash-command-executor.node.test.ts index 685928744cbc..a92158848482 100644 --- a/ui/src/ui/chat/slash-command-executor.node.test.ts +++ b/ui/src/ui/chat/slash-command-executor.node.test.ts @@ -152,6 +152,30 @@ describe("executeSlashCommand directives", () => { }); }); + it("surfaces terminal compaction failures instead of reporting a skip", async () => { + const request = vi.fn(async (method: string) => { + if (method === "sessions.compact") { + return { + ok: false, + compacted: false, + reason: "codex app-server compaction timed out", + }; + } + throw new Error(`unexpected method: ${method}`); + }); + + const result = await executeSlashCommand( + { request } as unknown as GatewayBrowserClient, + "main", + "compact", + "", + ); + + expect(result).toEqual({ + content: "Compaction failed: codex app-server compaction timed out", + }); + }); + it("uses the local model catalog to qualify raw /model overrides when the patch response omits provider", async () => { const request = vi.fn(async (method: string, _payload?: unknown) => { if (method === "sessions.patch") { diff --git a/ui/src/ui/chat/slash-command-executor.ts b/ui/src/ui/chat/slash-command-executor.ts index 39b0b0ea1a2f..fb7eee370984 100644 --- a/ui/src/ui/chat/slash-command-executor.ts +++ b/ui/src/ui/chat/slash-command-executor.ts @@ -155,10 +155,15 @@ async function executeCompact( ): Promise { try { const result = await client.request<{ + ok?: boolean; compacted?: boolean; reason?: string; result?: { tokensBefore?: number; tokensAfter?: number }; }>("sessions.compact", { key: sessionKey, ...selectedGlobalScope(sessionKey, context) }); + if (result?.ok !== true) { + const reason = typeof result?.reason === "string" ? result.reason.trim() : ""; + return { content: reason ? `Compaction failed: ${reason}` : "Compaction failed." }; + } if (result?.compacted) { const before = result.result?.tokensBefore; const after = result.result?.tokensAfter; diff --git a/ui/src/ui/controllers/sessions.test.ts b/ui/src/ui/controllers/sessions.test.ts index d1f222465f4a..b18e7b96c7c3 100644 --- a/ui/src/ui/controllers/sessions.test.ts +++ b/ui/src/ui/controllers/sessions.test.ts @@ -555,9 +555,93 @@ describe("patchSession", () => { agentId: "work", }); }); + + it("keeps non-Sessions patch refreshes active-only after enabling archived view", async () => { + const request = vi.fn(async () => ({ ok: true })); + const state = createState(request, { + tab: "overview", + sessionsShowArchived: true, + }); + + await patchSession(state, "agent:main:main", { fastMode: true }); + + expect(request).toHaveBeenNthCalledWith(2, "sessions.list", { + includeGlobal: true, + includeUnknown: true, + configuredAgentsOnly: true, + }); + }); }); describe("loadSessions", () => { + it("records the loaded archive scope separately from the Sessions view preference", async () => { + const request = vi.fn(async () => ({ + ts: 1, + path: "(multiple)", + count: 1, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [{ key: "agent:main:main", kind: "direct", updatedAt: 2 }], + })); + const state = createState(request, { tab: "sessions", sessionsShowArchived: true }); + + await loadSessions(state, { showArchived: false }); + + expect(state.sessionsResultShowArchived).toBe(false); + expect(request).toHaveBeenCalledWith("sessions.list", { + includeGlobal: true, + includeUnknown: true, + configuredAgentsOnly: true, + }); + }); + + it("reconciles the current chat without replacing the Sessions view result", async () => { + const request = vi.fn(async () => ({ + ts: 2, + path: "(multiple)", + count: 1, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [ + { + key: "agent:main:active", + kind: "direct", + updatedAt: 2, + hasActiveRun: false, + status: "done", + }, + ], + })); + const archivedResult = { + ts: 1, + path: "(multiple)", + count: 1, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [ + { + key: "agent:main:archived", + kind: "direct" as const, + updatedAt: 1, + archived: true, + }, + ], + }; + const state = createState(request, { + tab: "sessions", + sessionKey: "agent:main:active", + chatRunId: "run-active", + sessionsShowArchived: true, + sessionsResultShowArchived: true, + sessionsResult: archivedResult, + }); + + await loadSessions(state, { + showArchived: false, + preserveSessionsViewResult: true, + }); + + expect(state.sessionsResult).toBe(archivedResult); + expect(state.chatRunId).toBeNull(); + }); + it("hides explicitly archived sessions by default", async () => { const request = vi.fn(async (method: string) => { if (method !== "sessions.list") { @@ -590,7 +674,7 @@ describe("loadSessions", () => { expect(state.sessionsResult?.count).toBe(1); }); - it("includes explicitly archived sessions when explicitly shown", async () => { + it("shows only archived sessions in the archived view", async () => { const request = vi.fn(async (method: string) => { if (method !== "sessions.list") { throw new Error(`unexpected method: ${method}`); @@ -612,15 +696,14 @@ describe("loadSessions", () => { ], }; }); - const state = createState(request, { sessionsShowArchived: true }); + const state = createState(request, { tab: "sessions", sessionsShowArchived: true }); await loadSessions(state); expect(state.sessionsResult?.sessions.map((session) => session.key)).toEqual([ - "agent:main:main", "agent:main:subagent:archived", ]); - expect(state.sessionsResult?.count).toBe(2); + expect(state.sessionsResult?.count).toBe(1); }); it("keeps terminal non-archived sessions visible by default", async () => { @@ -655,6 +738,29 @@ describe("loadSessions", () => { expect(state.sessionsResult?.count).toBe(2); }); + it.each(["overview", "workboard"])( + "keeps %s loads active-only after the Sessions archived filter was enabled", + async (tab) => { + const request = vi.fn(async () => ({ + ts: 1, + path: "(multiple)", + count: 0, + defaults: {}, + sessions: [], + })); + const state = createState(request, { tab, sessionsShowArchived: true }); + + await loadSessions(state); + + expect(request).toHaveBeenCalledWith("sessions.list", { + includeGlobal: true, + includeUnknown: true, + configuredAgentsOnly: true, + }); + expect(state.sessionsResultShowArchived).toBe(false); + }, + ); + it("keeps local run tracking while the session list reports an active terminal snapshot", async () => { vi.useFakeTimers(); try { @@ -791,6 +897,7 @@ describe("loadSessions", () => { }; }); const state = createState(request, { + tab: "sessions", sessionsFilterActive: "120", sessionsFilterLimit: "50", sessionsShowArchived: true, @@ -803,6 +910,7 @@ describe("loadSessions", () => { includeGlobal: true, includeUnknown: true, configuredAgentsOnly: true, + archived: true, }); }); @@ -1225,6 +1333,63 @@ describe("loadSessions", () => { }); describe("applySessionsChangedEvent", () => { + it.each([ + { archived: true, previousArchived: false }, + { archived: false, previousArchived: true }, + ])( + "tracks selected chat archive state when a remote event sets archived=$archived", + ({ archived, previousArchived }) => { + const key = "agent:main:review"; + const state = createState(async () => undefined, { + sessionKey: key, + selectedChatSessionArchived: previousArchived, + sessionsResultShowArchived: previousArchived, + sessionsResult: { + ts: 1, + path: "(multiple)", + count: 1, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [{ key, kind: "direct", updatedAt: 1, archived: previousArchived }], + }, + }); + + const applied = applySessionsChangedEvent(state, { + sessionKey: key, + sessionId: "sess-review", + archived, + ts: 2, + }); + + expect(applied).toEqual({ applied: true, change: "deleted" }); + expect(state.selectedChatSessionArchived).toBe(archived); + }, + ); + + it("reconciles against the loaded active scope after leaving archived view enabled", () => { + const state = createState(async () => undefined, { + sessionsShowArchived: true, + sessionsResultShowArchived: false, + sessionsResult: { + ts: 1, + path: "(multiple)", + count: 1, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [{ key: "agent:main:review", kind: "direct", updatedAt: 1 }], + }, + }); + + const applied = applySessionsChangedEvent(state, { + sessionKey: "agent:main:review", + sessionId: "sess-review", + status: "done", + archived: false, + ts: 2, + }); + + expect(applied).toEqual({ applied: true, change: "updated" }); + expect(state.sessionsResult?.sessions).toHaveLength(1); + }); + it("replaces stale effective fast metadata from session change events", () => { const state = createState(async () => undefined, { sessionsResult: { @@ -1284,7 +1449,11 @@ describe("applySessionsChangedEvent", () => { ts: 2, }); - expect(applied).toEqual({ applied: true, change: "deleted" }); + expect(applied).toEqual({ + applied: true, + change: "deleted", + deletedSession: { key: "agent:main:old", agentId: "main", selected: false }, + }); expect(state.sessionsResult?.sessions.map((session) => session.key)).toEqual([ "agent:main:main", ]); @@ -1314,7 +1483,15 @@ describe("applySessionsChangedEvent", () => { ts: 2, }); - expect(applied).toEqual({ applied: true, change: "deleted" }); + expect(applied).toEqual({ + applied: true, + change: "deleted", + deletedSession: { + key: "agent:work:dashboard:deleted", + agentId: "work", + selected: false, + }, + }); expect(state.sessionsResult?.sessions.map((session) => session.key)).toEqual([ "agent:main:main", ]); @@ -1323,6 +1500,73 @@ describe("applySessionsChangedEvent", () => { ]); }); + it("reports deletion of the selected chat even when its row is not cached", () => { + const state = createState(async () => undefined, { + sessionKey: "agent:main:archived", + selectedChatSessionArchived: true, + sessionsResult: { + ts: 1, + path: "(multiple)", + count: 1, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [{ key: "agent:main:main", kind: "direct", updatedAt: 1 }], + }, + }); + + const applied = applySessionsChangedEvent(state, { + sessionKey: "agent:main:archived", + reason: "delete", + ts: 2, + }); + + expect(applied).toEqual({ + applied: true, + change: "deleted", + deletedSession: { key: "agent:main:archived", agentId: "main", selected: true }, + }); + }); + + it("reports deletion of the selected chat before a session list is loaded", () => { + const state = createState(async () => undefined, { + sessionKey: "agent:ops:archived", + selectedChatSessionArchived: true, + sessionsResult: null, + }); + + const applied = applySessionsChangedEvent(state, { + sessionKey: "agent:ops:archived", + reason: "delete", + ts: 2, + }); + + expect(applied).toEqual({ + applied: true, + change: "deleted", + deletedSession: { key: "agent:ops:archived", agentId: "ops", selected: true }, + }); + }); + + it("matches canonical global deletion events to the selected agent alias", () => { + const state = createState(async () => undefined, { + sessionKey: "agent:work:main", + selectedChatSessionArchived: true, + sessionsResult: null, + }); + + const applied = applySessionsChangedEvent(state, { + sessionKey: "global", + agentId: "work", + reason: "delete", + ts: 2, + }); + + expect(applied).toEqual({ + applied: true, + change: "deleted", + deletedSession: { key: "global", agentId: "work", selected: true }, + }); + }); + it("keeps out-of-scope session events out of scoped results", () => { const state = createState(async () => undefined, { sessionsResultAgentId: "work", @@ -1553,6 +1797,38 @@ describe("applySessionsChangedEvent", () => { expect(state.sessionsResult?.sessions).toStrictEqual([]); }); + it("clears pin timestamps from unpin events", () => { + const state = createState(async () => undefined, { + sessionsResult: { + ts: 1, + path: "(multiple)", + count: 1, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [ + { + key: "agent:main:project", + kind: "direct", + updatedAt: 1, + pinned: true, + pinnedAt: 2, + }, + ], + }, + }); + + const applied = applySessionsChangedEvent(state, { + sessionKey: "agent:main:project", + sessionId: "sess-project", + pinned: false, + pinnedAt: null, + ts: 3, + }); + + expect(applied).toEqual({ applied: true, change: "updated" }); + expect(state.sessionsResult?.sessions[0]).toMatchObject({ pinned: false }); + expect(state.sessionsResult?.sessions[0]?.pinnedAt).toBeUndefined(); + }); + it("keeps terminal status updates visible while archived sessions are hidden", () => { const state = createState(async () => undefined, { sessionsResult: { @@ -2169,6 +2445,33 @@ describe("applySessionsChangedEvent", () => { }); }); + it("retains selected chat archive status outside active-only results", () => { + const state = createState(async () => undefined, { + sessionKey: "agent:main:archived", + sessionsResultShowArchived: false, + sessionsResult: { + ts: 1, + path: "(multiple)", + count: 0, + defaults: { modelProvider: null, model: null, contextTokens: null }, + sessions: [], + }, + }); + + const applied = applyChatHistorySessionInfo(state, { + key: "agent:main:archived", + kind: "direct", + updatedAt: 1, + archived: true, + status: "done", + hasActiveRun: false, + }); + + expect(applied).toBe(false); + expect(state.selectedChatSessionArchived).toBe(true); + expect(state.sessionsResult?.sessions).toStrictEqual([]); + }); + it("does not create visible rows from synthetic chat history session info", () => { const state = createState(async () => undefined, { sessionsResult: null, diff --git a/ui/src/ui/controllers/sessions.ts b/ui/src/ui/controllers/sessions.ts index 412977a99482..175dd58e6db5 100644 --- a/ui/src/ui/controllers/sessions.ts +++ b/ui/src/ui/controllers/sessions.ts @@ -43,9 +43,11 @@ type SessionsChatRunState = { export type SessionsState = SessionsChatRunState & { client: GatewayBrowserClient | null; connected: boolean; + tab?: string; sessionsLoading: boolean; sessionsResult: SessionsListResult | null; sessionsResultAgentId?: string | null; + sessionsResultShowArchived?: boolean; chatAgentSessionRowsByAgent?: Record; sessionsError: string | null; sessionsFilterActive: string; @@ -62,6 +64,7 @@ export type SessionsState = SessionsChatRunState & { chatSessionMessageSubscriptionRequestedKey?: string | null; chatSessionMessageSubscriptionAgentId?: string | null; assistantAgentId?: string | null; + selectedChatSessionArchived?: boolean; agentsList?: { defaultId?: string | null; mainKey?: string | null } | null; hello?: GatewayHelloOk | null; }; @@ -85,6 +88,7 @@ export type LoadSessionsOverrides = { // The filtered Sessions view must NOT set this; there a filtered or deleted // row is expected to disappear from the list. backgroundHydrate?: boolean; + preserveSessionsViewResult?: boolean; }; type CreateSessionParams = { @@ -114,6 +118,10 @@ function hasCurrentChatSession( return typeof state.sessionKey === "string" && state.sessionKey.trim() !== ""; } +function resultShowsArchivedSessions(state: SessionsState): boolean { + return state.sessionsResultShowArchived ?? state.sessionsShowArchived; +} + function normalizeSubscriptionKey(value: string | null | undefined): string | null { const normalized = typeof value === "string" ? value.trim() : ""; return normalized ? normalized : null; @@ -329,6 +337,9 @@ const SESSION_EVENT_ROW_FIELDS = [ "startedAt", "status", "archived", + "archivedAt", + "pinned", + "pinnedAt", "subject", "surface", "systemSent", @@ -416,7 +427,7 @@ function filterAvailableSessionRows( rows: GatewaySessionRow[], options: { showArchived: boolean }, ): GatewaySessionRow[] { - return rows.filter((row) => row.key && (options.showArchived || !isArchivedSessionRow(row))); + return rows.filter((row) => row.key && isArchivedSessionRow(row) === options.showArchived); } function projectSessionsResultForAvailability( @@ -462,7 +473,13 @@ function appendSessionsResult( }; } -function compareSessionRowsByUpdatedAt(a: GatewaySessionRow, b: GatewaySessionRow): number { +// Pinned sessions float above recency everywhere a session list renders +// (sessions view, chat picker, sidebar recents); keep this the only sort. +export function compareSessionRowsByUpdatedAt(a: GatewaySessionRow, b: GatewaySessionRow): number { + const pinnedDiff = (b.pinnedAt ?? 0) - (a.pinnedAt ?? 0); + if (pinnedDiff !== 0) { + return pinnedDiff; + } return (b.updatedAt ?? 0) - (a.updatedAt ?? 0); } @@ -609,7 +626,7 @@ function resolveCachedChatAgentSessionRowAgentId( } function upsertCachedChatAgentSessionRow(state: SessionsState, row: GatewaySessionRow): boolean { - if (!state.sessionsShowArchived && isArchivedSessionRow(row)) { + if (isArchivedSessionRow(row)) { return invalidateCachedChatAgentSessionRow(state, row.key); } const agentId = resolveCachedChatAgentSessionRowAgentId(state, row); @@ -715,15 +732,53 @@ export type SessionsChangedApplyResult = | { applied: true; change: "deleted" | "inserted" | "updated"; + deletedSession?: { key: string; agentId?: string; selected: boolean }; clearedChatRun?: boolean; clearedChatRunStatus?: Pick; }; +function deletedSessionMatchesSelectedChat( + state: SessionsState, + payload: Record, + key: string, +): boolean { + if (!hasCurrentChatSession(state)) { + return false; + } + if (areUiSessionKeysEquivalent(key, state.sessionKey)) { + return true; + } + return Boolean( + isUiGlobalSessionKey(key) && + resolveUiGlobalAliasAgentId(state, state.sessionKey) && + sessionsChangedGlobalAgentMatches(state, payload, key), + ); +} + +function buildDeletedSessionChange( + state: SessionsState, + payload: Record, + eventSession: Record | null, + key: string, +) { + const parsedAgentId = parseAgentSessionKey(key)?.agentId; + const eventAgentId = readSessionsChangedEventAgentId(payload, eventSession); + const agentId = + parsedAgentId ?? + eventAgentId ?? + (isUiGlobalSessionKey(key) ? resolveDefaultGlobalAgentId(state) : undefined); + return { + key, + ...(agentId ? { agentId: normalizeAgentId(agentId) } : {}), + selected: deletedSessionMatchesSelectedChat(state, payload, key), + }; +} + export function applySessionsChangedEvent( state: SessionsState, payload: unknown, ): SessionsChangedApplyResult { - if (!isRecord(payload) || !state.sessionsResult) { + if (!isRecord(payload)) { return { applied: false }; } const eventSession = isRecord(payload.session) ? payload.session : null; @@ -740,19 +795,27 @@ export function applySessionsChangedEvent( return { applied: false }; } - const previousRows = state.sessionsResult.sessions; - const existingIndex = previousRows.findIndex((row) => row.key === key); - const existing = existingIndex >= 0 ? previousRows[existingIndex] : undefined; if (payload.reason === "delete") { + const deletedSession = buildDeletedSessionChange(state, payload, eventSession, key); const removedCachedRow = invalidateCachedChatAgentSessionRow(state, key); - if ( - !sessionsChangedGlobalAgentMatches(state, payload, key) || - !sessionsChangedResultScopeMatches(state, payload, eventSession, key, existing) - ) { - return removedCachedRow ? { applied: true, change: "deleted" } : { applied: false }; + if (!state.sessionsResult) { + return removedCachedRow || deletedSession.selected + ? { applied: true, change: "deleted", deletedSession } + : { applied: false }; + } + + const previousRows = state.sessionsResult.sessions; + const existingIndex = previousRows.findIndex((row) => row.key === key); + const existing = existingIndex >= 0 ? previousRows[existingIndex] : undefined; + if (!sessionsChangedResultScopeMatches(state, payload, eventSession, key, existing)) { + return removedCachedRow || deletedSession.selected + ? { applied: true, change: "deleted", deletedSession } + : { applied: false }; } if (existingIndex < 0) { - return removedCachedRow ? { applied: true, change: "deleted" } : { applied: false }; + return removedCachedRow || deletedSession.selected + ? { applied: true, change: "deleted", deletedSession } + : { applied: false }; } state.sessionsResult = { ...state.sessionsResult, @@ -760,8 +823,15 @@ export function applySessionsChangedEvent( sessions: previousRows.filter((row) => row.key !== key), }; invalidateCheckpointCacheForKey(state, key); - return { applied: true, change: "deleted" }; + return { applied: true, change: "deleted", deletedSession }; } + if (!state.sessionsResult) { + return { applied: false }; + } + + const previousRows = state.sessionsResult.sessions; + const existingIndex = previousRows.findIndex((row) => row.key === key); + const existing = existingIndex >= 0 ? previousRows[existingIndex] : undefined; const matchesResultScope = sessionsChangedGlobalAgentMatches(state, payload, key) && sessionsChangedResultScopeMatches(state, payload, eventSession, key, existing); @@ -786,7 +856,9 @@ export function applySessionsChangedEvent( continue; } const value = hasTopLevelGoalClear ? null : source[field]; - if (value === undefined || (field === "goal" && value === null)) { + const clearsManagementTimestamp = + (field === "archivedAt" || field === "pinnedAt") && value === null; + if (value === undefined || (field === "goal" && value === null) || clearsManagementTimestamp) { delete mutableNext[field]; } else { mutableNext[field] = value; @@ -804,12 +876,20 @@ export function applySessionsChangedEvent( if (nextRow.totalTokensFresh === false && !hasOwn(source, "totalTokens")) { delete nextRow.totalTokens; } + if ( + hasOwn(source, "archived") && + hasCurrentChatSession(state) && + areUiSessionKeysEquivalent(key, state.sessionKey) && + sessionsChangedGlobalAgentMatches(state, payload, key) + ) { + state.selectedChatSessionArchived = nextRow.archived === true; + } if (!matchesResultScope) { return upsertCachedChatAgentSessionRow(state, nextRow) ? { applied: true, change: existingIndex >= 0 ? "updated" : "inserted" } : { applied: false }; } - if (!state.sessionsShowArchived && isArchivedSessionRow(nextRow)) { + if (isArchivedSessionRow(nextRow) !== resultShowsArchivedSessions(state)) { const removedCachedRow = invalidateCachedChatAgentSessionRow(state, key); if (existingIndex < 0) { return removedCachedRow ? { applied: true, change: "deleted" } : { applied: false }; @@ -883,6 +963,9 @@ export function applyChatHistorySessionInfo( return false; } const session = sanitizeChatHistorySessionRow(row); + if (hasCurrentChatSession(state) && areUiSessionKeysEquivalent(session.key, state.sessionKey)) { + state.selectedChatSessionArchived = session.archived === true; + } if (!state.sessionsResult) { if (!isPersistedChatHistorySessionRow(session)) { if (!defaults) { @@ -897,7 +980,8 @@ export function applyChatHistorySessionInfo( }; return true; } - const sessions = state.sessionsShowArchived || !isArchivedSessionRow(session) ? [session] : []; + const showArchived = resultShowsArchivedSessions(state); + const sessions = isArchivedSessionRow(session) === showArchived ? [session] : []; state.sessionsResult = { ts: Date.now(), path: "", @@ -910,6 +994,7 @@ export function applyChatHistorySessionInfo( sessions, }; state.sessionsResultAgentId = resolveChatHistorySessionResultAgentId(state, session); + state.sessionsResultShowArchived = showArchived; upsertCachedChatAgentSessionRow(state, session); if (hasCurrentChatSession(state)) { const reconciled = reconcileChatRunFromSessionRow(state, session, { publishRunStatus: true }); @@ -1133,7 +1218,8 @@ async function loadSessionsOnce( ); const includeGlobal = overrides?.includeGlobal ?? state.sessionsIncludeGlobal; const includeUnknown = overrides?.includeUnknown ?? state.sessionsIncludeUnknown; - const showArchived = overrides?.showArchived ?? state.sessionsShowArchived; + const showArchived = + overrides?.showArchived ?? (state.tab === "sessions" && state.sessionsShowArchived); const activeMinutes = showArchived ? 0 : (normalizeSessionsFilterOverride(overrides?.activeMinutes) ?? @@ -1147,6 +1233,9 @@ async function loadSessionsOnce( includeUnknown, configuredAgentsOnly, }; + if (showArchived) { + params.archived = true; + } const agentId = overrides?.agentId?.trim(); const resultAgentId = agentId ? normalizeAgentId(agentId) : null; if (agentId) { @@ -1172,6 +1261,22 @@ async function loadSessionsOnce( const res = await client.request("sessions.list", params); if (res) { const projected = projectSessionsResultForAvailability(res, { showArchived }); + if (overrides?.preserveSessionsViewResult === true && state.tab === "sessions") { + for (const row of projected.sessions) { + upsertCachedChatAgentSessionRow(state, row); + } + if (hasCurrentChatSession(state)) { + const selectedRow = projected.sessions.find((row) => + areUiSessionKeysEquivalent(row.key, state.sessionKey), + ); + if (selectedRow) { + reconcileChatRunFromSessionRow(state, selectedRow, { + publishRunStatus: overrides.publishChatRunStatus !== false, + }); + } + } + return; + } let nextResult = overrides?.append === true && offset > 0 && state.sessionsResult ? appendSessionsResult(state.sessionsResult, projected) @@ -1219,6 +1324,7 @@ async function loadSessionsOnce( } state.sessionsResult = nextResult; state.sessionsResultAgentId = resultAgentId; + state.sessionsResultShowArchived = showArchived; if (hasCurrentChatSession(state) && overrides?.backgroundHydrate !== true) { reconcileChatRunFromCurrentSessionRow(state, { publishRunStatus: overrides?.publishChatRunStatus !== false, @@ -1264,14 +1370,17 @@ export async function patchSession( key: string, patch: { label?: string | null; + archived?: boolean; + pinned?: boolean; thinkingLevel?: string | null; fastMode?: FastMode | null; verboseLevel?: string | null; reasoningLevel?: string | null; }, -) { + refreshOverrides?: LoadSessionsOverrides, +): Promise { if (!state.client || !state.connected) { - return; + return false; } const params: Record = { key, @@ -1279,6 +1388,8 @@ export async function patchSession( }; for (const field of [ "label", + "archived", + "pinned", "thinkingLevel", "fastMode", "verboseLevel", @@ -1290,12 +1401,14 @@ export async function patchSession( } try { await state.client.request("sessions.patch", params); - await loadSessions( - state, - isUiGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : undefined, - ); + await loadSessions(state, { + ...refreshOverrides, + ...(isUiGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : {}), + }); + return true; } catch (err) { state.sessionsError = String(err); + return false; } } diff --git a/ui/src/ui/e2e/chat-flow.e2e.test.ts b/ui/src/ui/e2e/chat-flow.e2e.test.ts index 4f2d45b28906..a30e85bba040 100644 --- a/ui/src/ui/e2e/chat-flow.e2e.test.ts +++ b/ui/src/ui/e2e/chat-flow.e2e.test.ts @@ -1123,7 +1123,9 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { await page.getByText("Current session placeholder").waitFor({ timeout: 10_000 }); await page - .locator('a.sidebar-recent-session[data-session-key="agent:main:session-b"]') + .locator( + '.sidebar-recent-session[data-session-key="agent:main:session-b"] a.sidebar-recent-session__link', + ) .click(); const historyRequest = await gateway.waitForRequest("chat.history"); expect(requireRecord(historyRequest.params)).toMatchObject({ @@ -1288,7 +1290,9 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { ); await page - .locator('a.sidebar-recent-session[data-session-key="agent:main:session-b"]') + .locator( + '.sidebar-recent-session[data-session-key="agent:main:session-b"] a.sidebar-recent-session__link', + ) .click(); await page.locator(".sidebar-recent-session--active").getByText("Session B").waitFor({ timeout: 10_000, @@ -1297,7 +1301,9 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => { expect(await modelSelect.getAttribute("data-chat-select-value")).toBe(""); await page - .locator('a.sidebar-recent-session[data-session-key="agent:main:session-a"]') + .locator( + '.sidebar-recent-session[data-session-key="agent:main:session-a"] a.sidebar-recent-session__link', + ) .click(); await page.locator(".sidebar-recent-session--active").getByText("Session A").waitFor({ timeout: 10_000, diff --git a/ui/src/ui/e2e/session-management.e2e.test.ts b/ui/src/ui/e2e/session-management.e2e.test.ts new file mode 100644 index 000000000000..56e33e22d1a1 --- /dev/null +++ b/ui/src/ui/e2e/session-management.e2e.test.ts @@ -0,0 +1,240 @@ +// Control UI tests cover session management through the sidebar and chat picker. +import { mkdir } from "node:fs/promises"; +import path from "node:path"; +import { chromium, type Browser, type Locator, type Page } from "playwright"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { + canRunPlaywrightChromium, + installMockGateway, + resolvePlaywrightChromiumExecutablePath, + startControlUiE2eServer, + type ControlUiE2eServer, + type MockGatewayControls, + type MockGatewayRequest, +} from "../../test-helpers/control-ui-e2e.ts"; + +const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath()); +const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath); +const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1"; +const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip; + +let browser: Browser; +let server: ControlUiE2eServer; + +function sessionRow( + key: string, + label: string, + updatedAt: number, + options: { pinned?: boolean; pinnedAt?: number; hasActiveRun?: boolean; status?: string } = {}, +) { + return { + contextTokens: null, + displayName: label, + hasActiveRun: false, + key, + kind: "direct", + label, + model: "gpt-5.5", + modelProvider: "openai", + status: "done", + totalTokens: 0, + updatedAt, + ...options, + }; +} + +function sessionsListResponse(sessions: unknown[]) { + return { + count: sessions.length, + defaults: { + contextTokens: null, + model: "gpt-5.5", + modelProvider: "openai", + }, + hasMore: false, + limitApplied: 50, + nextOffset: null, + offset: 0, + path: "", + sessions, + totalCount: sessions.length, + ts: Date.now(), + }; +} + +function requireRecord(value: unknown): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("Expected object value"); + } + return value as Record; +} + +async function waitForPatch( + gateway: MockGatewayControls, + predicate: (params: Record) => boolean, +): Promise { + const deadline = Date.now() + 10_000; + let requests: MockGatewayRequest[] = []; + while (Date.now() < deadline) { + requests = await gateway.getRequests("sessions.patch"); + const match = requests.find((request) => predicate(requireRecord(request.params))); + if (match) { + return match; + } + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); + } + throw new Error(`No matching sessions.patch request found: ${JSON.stringify(requests)}`); +} + +function actionOpacity(button: Locator): Promise { + return button.evaluate((element) => globalThis.getComputedStyle(element).opacity); +} + +async function captureUiProof(page: Page, fileName: string) { + if (process.env.OPENCLAW_CAPTURE_UI_PROOF !== "1") { + return; + } + const artifactDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "thread-management"); + await mkdir(artifactDir, { recursive: true }); + await page.screenshot({ fullPage: true, path: path.join(artifactDir, fileName) }); +} + +describeControlUiE2e("Control UI session management mocked Gateway E2E", () => { + beforeAll(async () => { + if (!chromiumAvailable) { + throw new Error( + `Playwright Chromium is not installed or cannot start at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`, + ); + } + server = await startControlUiE2eServer(); + browser = await chromium.launch({ executablePath: chromiumExecutablePath }); + }); + + afterAll(async () => { + await browser?.close(); + await server?.close(); + }); + + it("manages sessions through the sidebar and chat picker", async () => { + const baseTime = Date.parse("2026-07-01T16:00:00.000Z"); + const context = await browser.newContext({ + locale: "en-US", + serviceWorkers: "block", + viewport: { height: 900, width: 1280 }, + }); + const page = await context.newPage(); + const gateway = await installMockGateway(page, { + methodResponses: { + "sessions.list": sessionsListResponse([ + sessionRow("agent:main:main", "Main", baseTime), + sessionRow("agent:main:release", "Release planning", baseTime - 60_000, { + pinned: true, + pinnedAt: baseTime - 30_000, + }), + sessionRow("agent:main:migration", "Data migration", baseTime - 90_000, { + hasActiveRun: true, + status: "running", + }), + sessionRow("agent:main:research", "Research notes", baseTime - 120_000), + ]), + "sessions.patch": {}, + }, + sessionKey: "agent:main:main", + }); + + try { + await page.goto(`${server.baseUrl}chat`); + + // Sidebar recents: pinned first, live-run spinner, hover-revealed actions. + const sidebarRows = page.locator(".sidebar-recent-sessions__list .sidebar-recent-session"); + await sidebarRows.first().waitFor({ state: "visible", timeout: 10_000 }); + await expect.poll(() => sidebarRows.first().textContent()).toContain("Release planning"); + const sidebarMigration = sidebarRows.filter({ hasText: "Data migration" }); + await expect + .poll(() => sidebarMigration.locator(".session-run-spinner").isVisible()) + .toBe(true); + + const sidebarResearch = sidebarRows.filter({ hasText: "Research notes" }); + const sidebarResearchPin = sidebarResearch.getByRole("button", { name: "Pin session" }); + await page.mouse.move(900, 500); + await expect.poll(() => actionOpacity(sidebarResearchPin)).toBe("0"); + const sidebarReleasePin = sidebarRows + .filter({ hasText: "Release planning" }) + .getByRole("button", { name: "Unpin session" }); + // Pinned badge stays visible without hover. + await expect.poll(() => actionOpacity(sidebarReleasePin)).toBe("1"); + await sidebarResearch.hover(); + await expect.poll(() => actionOpacity(sidebarResearchPin)).toBe("1"); + await captureUiProof(page, "sidebar-sessions.png"); + + // Chat picker: single-line rows with hover-revealed management actions. + await page.getByRole("button", { name: "Search sessions" }).click(); + const releaseRow = page + .locator(".chat-session-picker__option-row") + .filter({ hasText: "Release planning" }); + await releaseRow.waitFor({ state: "visible", timeout: 10_000 }); + await expect.poll(() => releaseRow.getByRole("button").count()).toBe(3); + + const migrationRow = page + .locator(".chat-session-picker__option-row") + .filter({ hasText: "Data migration" }); + await expect.poll(() => migrationRow.locator(".session-run-spinner").isVisible()).toBe(true); + + const mainRow = page.locator(".chat-session-picker__option-row").filter({ hasText: "Main" }); + await expect + .poll(() => mainRow.getByRole("button", { name: "Archive session" }).isDisabled()) + .toBe(true); + + const researchRow = page + .locator(".chat-session-picker__option-row") + .filter({ hasText: "Research notes" }); + const researchArchive = researchRow.getByRole("button", { name: "Archive session" }); + await page.mouse.move(900, 500); + await expect.poll(() => actionOpacity(researchArchive)).toBe("0"); + await expect + .poll(() => actionOpacity(releaseRow.getByRole("button", { name: "Unpin session" }))) + .toBe("1"); + await researchRow.hover(); + await expect.poll(() => actionOpacity(researchArchive)).toBe("1"); + await captureUiProof(page, "chat-session-management.png"); + + await releaseRow.hover(); + await releaseRow.getByRole("button", { name: "Unpin session" }).click(); + const pinPatch = await waitForPatch( + gateway, + (params) => params.key === "agent:main:release" && params.pinned === false, + ); + expect(requireRecord(pinPatch.params)).toMatchObject({ + key: "agent:main:release", + pinned: false, + }); + + page.once("dialog", (dialog) => dialog.accept("Launch plan")); + await releaseRow.hover(); + await releaseRow.getByRole("button", { name: "Rename session" }).click(); + const renamePatch = await waitForPatch( + gateway, + (params) => params.key === "agent:main:release" && params.label === "Launch plan", + ); + expect(requireRecord(renamePatch.params)).toMatchObject({ + key: "agent:main:release", + label: "Launch plan", + }); + + await researchRow.hover(); + await researchArchive.click(); + const archivePatch = await waitForPatch( + gateway, + (params) => params.key === "agent:main:research" && params.archived === true, + ); + expect(requireRecord(archivePatch.params)).toMatchObject({ + archived: true, + key: "agent:main:research", + }); + } finally { + await context.close(); + } + }); +}); diff --git a/ui/src/ui/navigation.browser.test.ts b/ui/src/ui/navigation.browser.test.ts index 06d39a9e803f..8b81d55de978 100644 --- a/ui/src/ui/navigation.browser.test.ts +++ b/ui/src/ui/navigation.browser.test.ts @@ -511,7 +511,7 @@ describe("control UI routing", () => { ]) as typeof app.sessionsResult; await app.updateComplete; - const recent = Array.from(app.querySelectorAll(".sidebar-recent-session")); + const recent = Array.from(app.querySelectorAll(".sidebar-recent-session")); expect(recent.map((entry) => entry.textContent?.replace(/\s+/g, " ").trim())).toEqual([ "Second workspace just now", "First workspace 5m ago", @@ -539,7 +539,9 @@ describe("control UI routing", () => { expect(recentToggle.getAttribute("aria-expanded")).toBe("true"); expect([...recentSection.classList]).not.toContain("sidebar-recent-sessions--collapsed"); - recent[1]?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); + recent[1] + ?.querySelector("a.sidebar-recent-session__link") + ?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); await app.updateComplete; expect(app.tab).toBe("chat"); @@ -591,7 +593,7 @@ describe("control UI routing", () => { ) as typeof app.sessionsResult; await app.updateComplete; - const rows = Array.from(app.querySelectorAll(".sidebar-recent-session")); + const rows = Array.from(app.querySelectorAll(".sidebar-recent-session")); // Pinned active row plus the nine-row recents cap. expect(rows).toHaveLength(10); expect(rows[0]?.dataset.sessionKey).toBe("agent:main:oldest"); @@ -707,7 +709,7 @@ describe("control UI routing", () => { const activeRow = expectElement( app, - 'a.sidebar-recent-session[data-session-key="agent:main:subagent:task-123"]', + '.sidebar-recent-session[data-session-key="agent:main:subagent:task-123"] a.sidebar-recent-session__link', HTMLAnchorElement, ); activeRow.dispatchEvent( @@ -740,7 +742,7 @@ describe("control UI routing", () => { const chatRow = expectElement( app, - 'a.sidebar-recent-session[data-session-key="agent:main:subagent:task-123"]', + '.sidebar-recent-session[data-session-key="agent:main:subagent:task-123"] a.sidebar-recent-session__link', HTMLAnchorElement, ); chatRow.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 })); diff --git a/ui/src/ui/session-key.ts b/ui/src/ui/session-key.ts index 258f07ca26c9..33bcf53b30cd 100644 --- a/ui/src/ui/session-key.ts +++ b/ui/src/ui/session-key.ts @@ -47,6 +47,47 @@ function normalizeMainKey(value: string | undefined | null): string { return normalizeOptionalLowercaseString(value) ?? DEFAULT_MAIN_KEY; } +function normalizeSessionKeyForUiComparison(sessionKey: string | undefined | null): string { + const raw = normalizeOptionalString(sessionKey); + if (!raw) { + return ""; + } + const parts = raw.split(":"); + let bodyStart = 0; + while (parts.length - bodyStart >= 3 && parts[bodyStart]?.toLowerCase() === "agent") { + parts[bodyStart] = "agent"; + parts[bodyStart + 1] = parts[bodyStart + 1]?.toLowerCase() ?? ""; + bodyStart += 2; + } + while (bodyStart < parts.length && !parts[bodyStart]?.trim()) { + bodyStart += 1; + } + const channel = parts[bodyStart]?.toLowerCase(); + const peerKind = parts[bodyStart + 1]?.toLowerCase(); + const preservesMatrixTail = + channel === "matrix" && (peerKind === "channel" || peerKind === "group"); + const preservesSignalGroup = channel === "signal" && peerKind === "group"; + if (!preservesMatrixTail && !preservesSignalGroup) { + return raw.toLowerCase(); + } + parts[bodyStart] = channel; + parts[bodyStart + 1] = peerKind; + if (preservesMatrixTail) { + for (let index = parts.length - 2; index >= bodyStart + 2; index -= 1) { + if (parts[index]?.toLowerCase() === "thread") { + parts[index] = "thread"; + break; + } + } + } else { + parts[bodyStart + 2] = parts[bodyStart + 2]?.trim() ?? ""; + for (let index = bodyStart + 3; index < parts.length; index += 1) { + parts[index] = parts[index]?.toLowerCase() ?? ""; + } + } + return parts.join(":"); +} + function readSessionDefaults( host: Pick, ): { defaultAgentId?: string | null; mainKey?: string | null } | undefined { @@ -156,7 +197,7 @@ export function buildAgentMainSessionKey(params: { } function normalizeDefaultMainSessionAliasForUi(sessionKey: string | undefined | null): string { - const normalized = normalizeLowercaseStringOrEmpty(sessionKey); + const normalized = normalizeSessionKeyForUiComparison(sessionKey); return normalized === DEFAULT_MAIN_KEY ? buildAgentMainSessionKey({ agentId: DEFAULT_AGENT_ID, mainKey: DEFAULT_MAIN_KEY }) : normalized; @@ -176,6 +217,23 @@ export function resolveAgentIdFromSessionKey(sessionKey: string | undefined | nu return normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID); } +// Archive policy shared by the chat picker, sidebar recents, and Sessions +// table: agent main sessions must stay reachable, live runs must finish +// first, and global/unknown scopes are not archivable conversation threads. +export function canArchiveSessionRow( + row: { key: string; kind?: string; hasActiveRun?: boolean }, + configuredMainKey: string, +): boolean { + if (row.hasActiveRun === true || row.kind === "global" || row.kind === "unknown") { + return false; + } + const isMainSession = + row.key === "main" || + normalizeLowercaseStringOrEmpty(parseAgentSessionKey(row.key)?.rest) === + normalizeMainKey(configuredMainKey); + return !isMainSession; +} + export function isSessionKeyTiedToAgent( sessionKey: string | undefined | null, agentId: string, diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 16c47aae23bd..dfe85bf5c87c 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -477,6 +477,10 @@ export type GatewaySessionRow = { room?: string; space?: string; updatedAt: number | null; + archived?: boolean; + archivedAt?: number; + pinned?: boolean; + pinnedAt?: number; sessionId?: string; systemSent?: boolean; abortedLastRun?: boolean; @@ -496,7 +500,6 @@ export type GatewaySessionRow = { totalTokens?: number; totalTokensFresh?: boolean; status?: SessionRunStatus; - archived?: boolean; hasActiveRun?: boolean; subagentRunState?: SubagentRunState; hasActiveSubagentRun?: boolean; diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 5f7b311ce3f1..b49ecb37d2d7 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -50,6 +50,7 @@ const loadSessionsMock = vi.hoisted(() => } }), ); +const patchSessionMock = vi.hoisted(() => vi.fn(async () => true)); const buildChatItemsMock = vi.hoisted(() => vi.fn((props: { messages: unknown[]; stream: string | null; streamStartedAt: number | null }) => { if ( @@ -193,6 +194,7 @@ vi.mock("../controllers/agents.ts", () => ({ vi.mock("../controllers/sessions.ts", () => ({ loadSessions: loadSessionsMock, + patchSession: patchSessionMock, syncSelectedSessionMessageSubscription: vi.fn(async () => undefined), })); @@ -977,6 +979,44 @@ describe("chat goal status", () => { }); describe("chat composer workbench", () => { + it("keeps archived sessions read-only across composer interactions", () => { + const onSend = vi.fn(); + const onAttachmentsChange = vi.fn(); + const disabledReason = "Restore this session to send messages."; + const container = renderChatView({ + canSend: false, + disabledReason, + draft: "unsent draft", + onSend, + onAttachmentsChange, + }); + const textarea = container.querySelector("textarea"); + const fileInput = container.querySelector(".agent-chat__file-input"); + const attachButton = container.querySelector( + `[aria-label="${t("chat.composer.attachFile")}"]`, + ); + const sendButton = container.querySelector(".chat-send-btn"); + const chat = container.querySelector(".card.chat"); + + expect(textarea?.disabled).toBe(true); + expect(textarea?.placeholder).toBe(disabledReason); + expect(fileInput?.disabled).toBe(true); + expect(attachButton?.disabled).toBe(true); + expect(sendButton?.disabled).toBe(true); + + textarea?.dispatchEvent( + new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }), + ); + sendButton?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); + fileInput?.dispatchEvent(new Event("change", { bubbles: true })); + const drop = new Event("drop", { bubbles: true, cancelable: true }); + chat?.dispatchEvent(drop); + + expect(onSend).not.toHaveBeenCalled(); + expect(onAttachmentsChange).not.toHaveBeenCalled(); + expect(drop.defaultPrevented).toBe(true); + }); + it("renders session controls in the composer and workspace files in the expanded rail", () => { const onToggleCollapsed = vi.fn(); const onRefresh = vi.fn(); @@ -1180,6 +1220,7 @@ afterEach(() => { renderMessageGroupMock.mockClear(); assistantAttachmentRenderVersionMock.value = 0; loadSessionsMock.mockClear(); + patchSessionMock.mockClear(); refreshVisibleToolsEffectiveForCurrentSessionMock.mockClear(); resetChatViewState(); resetChatAttachmentPayloadStoreForTest(); @@ -3126,6 +3167,85 @@ describe("chat session controls", () => { expect(onSwitchSession).toHaveBeenCalledWith(state, targetSessionKey); }); + it("pins sessions from the chat session picker", async () => { + const { state } = createChatHeaderState(); + const container = document.createElement("div"); + render(renderChatSessionSelect(state), container); + + container.querySelector('button[data-chat-session-select="true"]')!.click(); + await vi.waitFor(() => expect(state.chatSessionPickerResult).not.toBeNull()); + render(renderChatSessionSelect(state), container); + container.querySelector('button[data-chat-session-pin="true"]')!.click(); + + await vi.waitFor(() => + expect(patchSessionMock).toHaveBeenCalledWith( + state, + "main", + { pinned: true }, + { + activeMinutes: 0, + configuredAgentsOnly: true, + includeGlobal: true, + includeUnknown: true, + limit: 50, + preserveSessionsViewResult: true, + showArchived: false, + }, + ), + ); + }); + + it("switches away after archiving a selected legacy session alias", async () => { + const { state } = createChatHeaderState(); + const onSwitchSession = vi.fn(); + state.sessionKey = "Agent:Main:Work"; + state.settings.sessionKey = state.sessionKey; + state.sessionsResult = createSessionsResultFromRows([ + { key: "agent:main:work", kind: "direct", label: "Work", updatedAt: 1 }, + ]); + state.chatSessionPickerOpen = true; + state.chatSessionPickerSurface = "desktop"; + state.chatSessionPickerResult = state.sessionsResult; + const container = document.createElement("div"); + render(renderChatSessionSelect(state, onSwitchSession), container); + + container.querySelector('button[data-chat-session-archive="true"]')!.click(); + + await vi.waitFor(() => expect(onSwitchSession).toHaveBeenCalledWith(state, "agent:main:main")); + }); + + it.each([ + [ + "Matrix room", + "agent:main:matrix:channel:!MixedRoomAbC:example.org", + "agent:main:matrix:channel:!mixedroomabc:example.org", + ], + ["Signal group", "agent:main:signal:group:AbC123=", "agent:main:signal:group:abc123="], + ])( + "does not switch after archiving a case-distinct opaque %s", + async (_name, selectedKey, rowKey) => { + const { state } = createChatHeaderState(); + const onSwitchSession = vi.fn(); + state.sessionKey = selectedKey; + state.settings.sessionKey = selectedKey; + state.sessionsResult = createSessionsResultFromRows([ + { key: rowKey, kind: "direct", label: "Other session", updatedAt: 1 }, + ]); + state.chatSessionPickerOpen = true; + state.chatSessionPickerSurface = "desktop"; + state.chatSessionPickerResult = state.sessionsResult; + const container = document.createElement("div"); + render(renderChatSessionSelect(state, onSwitchSession), container); + + container + .querySelector('button[data-chat-session-archive="true"]')! + .click(); + + await vi.waitFor(() => expect(patchSessionMock).toHaveBeenCalled()); + expect(onSwitchSession).not.toHaveBeenCalled(); + }, + ); + it("clears applied chat session picker search when the input is cleared", async () => { const { state } = createChatHeaderState(); state.sessionsIncludeGlobal = false; diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index 5ae86affd3ee..5d00154882ae 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -1711,7 +1711,9 @@ function selectSlashCommand( if (cmd.executeLocal && !cmd.args) { commitComposerDraft(props, `/${cmd.name}`); requestUpdate(); - props.onSend(); + if (props.connected && props.canSend) { + props.onSend(); + } } else { commitComposerDraft(props, `/${cmd.name} `); requestUpdate(); @@ -1753,7 +1755,7 @@ function selectSlashArg( resetSlashMenuState(); commitComposerDraft(props, `/${cmdName} ${arg}`); requestUpdate(); - if (execute) { + if (execute && props.connected && props.canSend) { props.onSend(); } } @@ -2141,7 +2143,7 @@ function createReplyContextMenuButton(onClick: () => void): HTMLButtonElement { } export function renderChat(props: ChatProps) { - const canCompose = props.connected; + const canCompose = props.connected && props.canSend; const isBusy = props.sending || props.stream !== null; const canAbort = Boolean(props.canAbort && props.onAbort); const hasTerminalStatus = hasTerminalRunStatus(props.runStatus); @@ -2171,11 +2173,13 @@ export function renderChat(props: ChatProps) { const tokens = tokenEstimate(visibleDraft); const composerControls = props.composerControls; - const placeholder = props.connected - ? hasAttachments - ? t("chat.composer.placeholderWithAttachments") - : t("chat.composer.placeholder", { name: props.assistantName || "agent" }) - : t("chat.composer.placeholderDisconnected"); + const placeholder = !props.connected + ? t("chat.composer.placeholderDisconnected") + : !canCompose && props.disabledReason + ? props.disabledReason + : hasAttachments + ? t("chat.composer.placeholderWithAttachments") + : t("chat.composer.placeholder", { name: props.assistantName || "agent" }); const requestUpdate = props.onRequestUpdate ?? (() => {}); const splitRatio = props.splitRatio ?? 0.6; @@ -2633,16 +2637,14 @@ export function renderChat(props: ChatProps) { // Send on Enter (without shift) if (e.key === "Enter" && !e.shiftKey) { - if (!props.connected) { + if (!canCompose) { return; } e.preventDefault(); - if (canCompose) { - const target = e.target as HTMLTextAreaElement; - commitComposerDraft(props, target.value); - props.onSend(); - syncComposerDraftAfterSend(target); - } + const target = e.target as HTMLTextAreaElement; + commitComposerDraft(props, target.value); + props.onSend(); + syncComposerDraftAfterSend(target); } }; @@ -2694,19 +2696,22 @@ export function renderChat(props: ChatProps) { commitComposerDraft(props, target.value); }; const handleSend = () => { + if (!canCompose) { + return; + } commitComposerDraft(props, draftMirror.value); props.onSend(); syncComposerDraftAfterSend(composerTextarea); }; - const slashMenuVisible = isSlashMenuVisible(); + const slashMenuVisible = canCompose && isSlashMenuVisible(); const activeSlashMenuOptionId = getActiveSlashMenuOptionId(); const activeSlashMenuOptionLabel = getActiveSlashMenuOptionLabel(); const chatColumnFooter = html` ${renderChatQueue({ queue: props.queue, canAbort: showAbortableUi, - onQueueRetry: props.onQueueRetry, - onQueueSteer: props.onQueueSteer, + onQueueRetry: canCompose ? props.onQueueRetry : undefined, + onQueueSteer: canCompose ? props.onQueueSteer : undefined, onQueueRemove: props.onQueueRemove, })} ${renderSideResult(props.sideResult, props.onDismissSideResult)} @@ -2721,9 +2726,10 @@ export function renderChat(props: ChatProps) {
focusComposerFromChrome(event, props.connected)} + @click=${(event: MouseEvent) => focusComposerFromChrome(event, canCompose)} > - ${renderSlashMenu(requestUpdate, props, visibleDraft)} ${renderAttachmentPreview(props)} + ${slashMenuVisible ? renderSlashMenu(requestUpdate, props, visibleDraft) : nothing} + ${renderAttachmentPreview(props)} ${props.replyTarget ? html`
@@ -2758,7 +2764,12 @@ export function renderChat(props: ChatProps) { accept=${CHAT_ATTACHMENT_ACCEPT} multiple class="agent-chat__file-input" - @change=${(e: Event) => handleFileSelect(e, props)} + ?disabled=${!canCompose} + @change=${(e: Event) => { + if (canCompose) { + handleFileSelect(e, props); + } + }} /> ${renderRealtimeTalkOptions(props)} @@ -2806,7 +2817,7 @@ export function renderChat(props: ChatProps) { })} .value=${visibleDraft} dir=${detectTextDirection(visibleDraft)} - ?disabled=${!props.connected} + ?disabled=${!canCompose} aria-autocomplete="list" aria-controls=${ifDefined(slashMenuVisible ? SLASH_MENU_LISTBOX_ID : undefined)} aria-activedescendant=${ifDefined(activeSlashMenuOptionId ?? undefined)} @@ -2819,7 +2830,11 @@ export function renderChat(props: ChatProps) { }} @compositionend=${handleCompositionEnd} @blur=${handleBlur} - @paste=${(e: ClipboardEvent) => handlePaste(e, props)} + @paste=${(e: ClipboardEvent) => { + if (canCompose) { + handlePaste(e, props); + } + }} placeholder=${placeholder} rows="1" > @@ -2841,7 +2856,7 @@ export function renderChat(props: ChatProps) { @click=${clickComposerFileInput} title=${t("chat.composer.attachFile")} aria-label=${t("chat.composer.attachFile")} - ?disabled=${!props.connected} + ?disabled=${!canCompose} > ${icons.paperclip} ${t("chat.composer.attachFile")} @@ -2860,7 +2875,7 @@ export function renderChat(props: ChatProps) { aria-label=${props.realtimeTalkActive ? t("chat.composer.stopTalk") : t("chat.composer.startTalk")} - ?disabled=${!props.connected} + ?disabled=${!canCompose && !props.realtimeTalkActive} > ${props.realtimeTalkActive ? icons.volume2 : icons.radio} ${icons.settings} Talk settings @@ -2897,12 +2912,12 @@ export function renderChat(props: ChatProps) { : nothing} ${renderContextNotice(activeSession, props.sessions?.defaults?.contextTokens ?? null, { compactBusy, - compactDisabled: !props.connected || isBusy || showAbortableUi, + compactDisabled: !canCompose || isBusy || showAbortableUi, onCompact: props.onCompact, })} ${renderChatRunControls({ canAbort: showAbortableUi, - connected: props.connected, + connected: canCompose, draft: visibleDraft, hasMessages: props.messages.length > 0, isBusy, @@ -2921,7 +2936,12 @@ export function renderChat(props: ChatProps) { return html`
handleDrop(e, props)} + @drop=${(e: DragEvent) => { + e.preventDefault(); + if (canCompose) { + handleDrop(e, props); + } + }} @dragover=${(e: DragEvent) => e.preventDefault()} @keydown=${(e: KeyboardEvent) => { if (e.key === "Escape" && props.replyTarget && !e.defaultPrevented) { diff --git a/ui/src/ui/views/sessions.test.ts b/ui/src/ui/views/sessions.test.ts index d66dd7f9070c..1fe0e24542c6 100644 --- a/ui/src/ui/views/sessions.test.ts +++ b/ui/src/ui/views/sessions.test.ts @@ -38,6 +38,7 @@ function buildProps(result: SessionsListResult): SessionsProps { includeGlobal: false, includeUnknown: false, showArchived: false, + mainKey: "main", filtersCollapsed: false, basePath: "", searchQuery: "", @@ -158,6 +159,71 @@ describe("sessions view", () => { expect(onAddToWorkboard).toHaveBeenCalledWith(session); }); + it("pins, archives, and restores sessions from row actions", async () => { + const container = document.createElement("div"); + const onPatch = vi.fn(); + render( + renderSessions({ + ...buildProps( + buildResult({ + key: "agent:main:dashboard:1", + kind: "direct", + updatedAt: Date.now(), + pinned: false, + archived: false, + }), + ), + onPatch, + }), + container, + ); + await Promise.resolve(); + + container.querySelector('button[title="Pin session"]')!.click(); + container.querySelector('button[title="Archive session"]')!.click(); + expect(onPatch).toHaveBeenNthCalledWith(1, "agent:main:dashboard:1", { pinned: true }); + expect(onPatch).toHaveBeenNthCalledWith(2, "agent:main:dashboard:1", { archived: true }); + + render( + renderSessions({ + ...buildProps( + buildResult({ + key: "agent:main:dashboard:1", + kind: "direct", + updatedAt: Date.now(), + archived: true, + }), + ), + onPatch, + }), + container, + ); + await Promise.resolve(); + container.querySelector('button[title="Restore session"]')!.click(); + expect(onPatch).toHaveBeenLastCalledWith("agent:main:dashboard:1", { archived: false }); + }); + + it("keeps pinned sessions above newer unpinned sessions", async () => { + const container = document.createElement("div"); + render( + renderSessions({ + ...buildProps( + buildMultiResult([ + { key: "newer", kind: "direct", updatedAt: 200 }, + { key: "pinned", kind: "direct", updatedAt: 100, pinned: true, pinnedAt: 300 }, + ]), + ), + }), + container, + ); + await Promise.resolve(); + + const keys = Array.from(container.querySelectorAll("tbody .session-data-row")).map((row) => + row.querySelector(".session-key-cell")?.textContent?.trim(), + ); + expect(keys).toEqual(["pinned", "newer"]); + }); + it("marks sessions that already have workboard cards", async () => { const container = document.createElement("div"); render( @@ -214,7 +280,7 @@ describe("sessions view", () => { expect(limitField?.getAttribute("data-tooltip")).toBe("Max sessions to load."); expect(globalToggle?.getAttribute("data-tooltip")).toBe("Include global sessions."); expect(unknownToggle?.getAttribute("data-tooltip")).toBe("Include unknown sessions."); - expect(archivedToggle?.getAttribute("data-tooltip")).toBe("Include archived sessions."); + expect(archivedToggle?.getAttribute("data-tooltip")).toBe("Show only archived sessions."); expect( Array.from(filters?.querySelectorAll("[title]") ?? []).map((node) => node.className), ).toStrictEqual([]); diff --git a/ui/src/ui/views/sessions.ts b/ui/src/ui/views/sessions.ts index aba948df89bc..699ce17bbad2 100644 --- a/ui/src/ui/views/sessions.ts +++ b/ui/src/ui/views/sessions.ts @@ -6,6 +6,7 @@ import { icons } from "../icons.ts"; import { pathForTab } from "../navigation.ts"; import { formatSessionTokens } from "../presenter.ts"; import { formatGoalDetail, formatGoalSummary } from "../session-goal.ts"; +import { parseAgentSessionKey } from "../session-key.ts"; import { sessionModelMatchesDefaults } from "../session-model-defaults.ts"; import { isSessionRunActive } from "../session-run-state.ts"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts"; @@ -34,6 +35,7 @@ export type SessionsProps = { includeGlobal: boolean; includeUnknown: boolean; showArchived: boolean; + mainKey: string; filtersCollapsed: boolean; basePath: string; searchQuery: string; @@ -66,6 +68,8 @@ export type SessionsProps = { key: string, patch: { label?: string | null; + archived?: boolean; + pinned?: boolean; thinkingLevel?: string | null; fastMode?: FastMode | null; verboseLevel?: string | null; @@ -285,6 +289,10 @@ function sortRows( ): GatewaySessionRow[] { const cmp = dir === "asc" ? 1 : -1; return [...rows].toSorted((a, b) => { + const pinnedDiff = (b.pinnedAt ?? 0) - (a.pinnedAt ?? 0); + if (pinnedDiff !== 0) { + return pinnedDiff; + } let diff = 0; switch (column) { case "key": @@ -448,6 +456,12 @@ function sessionDetailItems(params: { value: row.archived ? t("common.yes") : t("common.no"), }); } + if (typeof row.pinned === "boolean") { + details.push({ + label: t("sessionsView.pinned"), + value: row.pinned ? t("common.yes") : t("common.no"), + }); + } return details; } @@ -503,7 +517,7 @@ export function renderSessions(props: SessionsProps) { const limitTooltip = t("sessionsView.limitTooltip"); const globalTooltip = t("sessionsView.globalTooltip"); const unknownTooltip = t("sessionsView.unknownTooltip"); - const showArchivedTooltip = t("sessionsView.showArchivedTooltip"); + const showArchivedTooltip = t("sessionsView.archivedOnlyTooltip"); const filtersExpanded = !props.filtersCollapsed; const filterPanelTitle = t("sessionsView.filters"); const filterToggleLabel = filtersExpanded @@ -638,7 +652,7 @@ export function renderSessions(props: SessionsProps) { ${renderFilterToggle({ name: "showArchived", checked: props.showArchived, - label: t("sessionsView.showArchived"), + label: t("sessionsView.archivedOnly"), title: showArchivedTooltip, extraClass: "session-archive-toggle", onChange: (checked) => @@ -829,6 +843,9 @@ function renderRows(row: GatewaySessionRow, props: SessionsProps) { displayName && displayName !== row.key && displayName !== trimmedLabel, ); const keyParts = parseSessionKeyParts(row.key); + const isMainSession = + row.key === "main" || + parseAgentSessionKey(row.key)?.rest === normalizeLowercaseStringOrEmpty(props.mainKey); const agentIdentity = keyParts ? getAgentIdentity(props.agentIdentityById, keyParts.agentId) : null; @@ -1052,6 +1069,39 @@ function renderRows(row: GatewaySessionRow, props: SessionsProps) { + + ${props.onAddToWorkboard && canLink ? html`