oysterun 1.6.0-beta.2 → 1.6.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/oysterun.mjs +64 -17
- package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
- package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
- package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
- package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
- package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
- package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
- package/dev/client/connectivity/managed-host-browser.mjs +10 -0
- package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
- package/dev/client/connectivity/session-transition-state.mjs +18 -6
- package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
- package/dev/client/web/index.html +5367 -609
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
- package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
- package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
- package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
- package/dev/client/web-chat/dist/index.html +1 -2
- package/docs/routec/dev/cloud_deplyments.md +5 -0
- package/docs/routec/dev/npm_native_dependency_install.md +6 -2
- package/docs/routec/dev/npmjs_publish.md +55 -19
- package/docs/routec/dev/p262_setup_input_file.md +117 -0
- package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
- package/docs/routec/host_setup.md +39 -10
- package/docs/routec/oysterun_operation_interface_spec.md +78 -2
- package/docs/routec/provider_contract.md +35 -5
- package/host-service/README.md +36 -0
- package/host-service/adapters/claude-code-adapter.mjs +48 -5
- package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
- package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
- package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
- package/host-service/agent-config.mjs +16 -5
- package/host-service/agent-folder-availability.mjs +41 -0
- package/host-service/apns-complete-message-dispatcher.mjs +1 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
- package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
- package/host-service/callback-durable-store.mjs +354 -47
- package/host-service/callback-foundation.mjs +21 -1
- package/host-service/callback-host-integration.mjs +345 -30
- package/host-service/claude-acp-model-discovery.mjs +23 -18
- package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
- package/host-service/cli/installer-recovery-guidance.mjs +20 -0
- package/host-service/cli/prepare-restart-restore.mjs +2 -0
- package/host-service/cli/product-cli.mjs +34 -33
- package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
- package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
- package/host-service/cli/run-host-lifecycle.mjs +2 -0
- package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
- package/host-service/codex-model-discovery.mjs +23 -18
- package/host-service/complete-cli-uninstall.mjs +176 -0
- package/host-service/complete-message-notification-predicate.mjs +8 -6
- package/host-service/complete-message-notification-title.mjs +4 -0
- package/host-service/completed-session-unread.mjs +425 -42
- package/host-service/completed-turn-navigation.mjs +444 -144
- package/host-service/completed-turn-notification-observer.mjs +116 -0
- package/host-service/completed-turn-service.mjs +445 -0
- package/host-service/config.mjs +115 -9
- package/host-service/desired-open-session-state.mjs +331 -0
- package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
- package/host-service/frp-agent.mjs +9 -9
- package/host-service/grok-acp-model-discovery.mjs +31 -33
- package/host-service/host-lifecycle-recovery.mjs +37 -0
- package/host-service/host-port-resolution.mjs +98 -0
- package/host-service/host-runtime-diagnostics.mjs +18 -1
- package/host-service/initial-preflight-recovery.mjs +71 -0
- package/host-service/installer-recovery-guidance.mjs +85 -0
- package/host-service/installer-reinstall-bindings.mjs +49 -0
- package/host-service/installer-reinstall-target.mjs +49 -0
- package/host-service/legacy-continuity-observation.mjs +33 -0
- package/host-service/lifecycle-compat-cli.mjs +13 -0
- package/host-service/lifecycle-compat-preload.mjs +89 -0
- package/host-service/lifecycle-compatibility.mjs +164 -0
- package/host-service/lifecycle-continuity-assets.mjs +222 -0
- package/host-service/lifecycle-continuity-probe.mjs +97 -0
- package/host-service/lifecycle-continuity-projection.mjs +29 -0
- package/host-service/lifecycle-continuity-runner.mjs +192 -0
- package/host-service/lifecycle-continuity.mjs +493 -0
- package/host-service/lifecycle-service-render-bundle.mjs +120 -0
- package/host-service/lifecycle-user-path.mjs +34 -0
- package/host-service/link-annotations.mjs +10 -0
- package/host-service/matrix-credential-issuer.mjs +32 -1
- package/host-service/matrix-event-correlator.mjs +166 -27
- package/host-service/matrix-facade.mjs +96 -19
- package/host-service/matrix-room-binding.mjs +24 -0
- package/host-service/matrix-transcript-read-adapter.mjs +17 -3
- package/host-service/model-discovery-guardian.mjs +40 -0
- package/host-service/model-discovery-process.mjs +141 -0
- package/host-service/onboard-lifecycle-status.mjs +28 -0
- package/host-service/onboard.mjs +479 -0
- package/host-service/package.json +2 -1
- package/host-service/portable-scheduler-definitions.mjs +28 -0
- package/host-service/product-runtime-host-origin.mjs +22 -0
- package/host-service/provider-model-params.mjs +14 -17
- package/host-service/provider-model-refresh-runner.mjs +124 -169
- package/host-service/provider-model-refresh-schedule.mjs +11 -0
- package/host-service/provider-resume-contract.mjs +9 -3
- package/host-service/restart-restore-state.mjs +25 -0
- package/host-service/retained-host-state-holder.mjs +57 -0
- package/host-service/routec-matrix-sqlite-store.mjs +155 -10
- package/host-service/routec-matrix-storage-adapter.mjs +208 -25
- package/host-service/routec-matrix-storage-paths.mjs +12 -0
- package/host-service/routec-request-delay-window.mjs +55 -0
- package/host-service/scheduler-acp-provider-runner.mjs +2 -1
- package/host-service/scheduler-direct-provider-runner.mjs +71 -9
- package/host-service/scheduler-run-timeout-policy.mjs +85 -0
- package/host-service/scheduler-service.mjs +96 -9
- package/host-service/scheduler-store.mjs +9 -2
- package/host-service/server.mjs +2202 -552
- package/host-service/service-definition-migration.mjs +10 -3
- package/host-service/session-collection-service.mjs +320 -0
- package/host-service/session-history.mjs +99 -15
- package/host-service/session-list-preview-service.mjs +87 -12
- package/host-service/session-list-projection-service.mjs +182 -0
- package/host-service/session-loop-definitions.mjs +702 -75
- package/host-service/session-manager.mjs +473 -38
- package/host-service/session-switcher-projection.mjs +11 -3
- package/host-service/session-tags.mjs +451 -0
- package/host-service/session-transcript-target.mjs +103 -0
- package/host-service/setup-input-file.mjs +91 -0
- package/host-service/setup-provider-model-defaults.mjs +53 -0
- package/host-service/setup.mjs +133 -101
- package/host-service/stopped-user-host-package.mjs +60 -0
- package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
- package/host-service/update-reminder-state.mjs +7 -0
- package/host-service/voice-input-receipt-store.mjs +60 -0
- package/package.json +1 -1
- package/static/oysterun-social-preview.png +0 -0
- package/static/site/assets/oysterun-social-preview.png +0 -0
- package/static/site/blog/en/index.html +17 -0
- package/static/site/blog/generated_manifest.json +9 -9
- package/static/site/blog/index.html +18 -0
- package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
- package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
- package/static/site/blog/zh/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
- package/static/site/docs/en/index.html +17 -0
- package/static/site/docs/generated_manifest.json +60 -60
- package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
- package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
- package/static/site/docs/index.html +18 -0
- package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
- package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
- package/static/site/docs/read-large-tool-results/en/index.html +17 -0
- package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
- package/static/site/docs/send-images-and-media/en/index.html +17 -0
- package/static/site/docs/send-images-and-media/zh/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
- package/static/site/docs/understand-agent-activity/en/index.html +19 -2
- package/static/site/docs/understand-agent-activity/en/index.md +2 -2
- package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
- package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
- package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
- package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
- package/static/site/docs/zh/index.html +17 -0
- package/static/site/en/index.html +18 -1
- package/static/site/en/index.md +1 -1
- package/static/site/faq/en/index.html +17 -0
- package/static/site/faq/index.html +18 -0
- package/static/site/faq/zh/index.html +17 -0
- package/static/site/getting-started/en/index.html +23 -1
- package/static/site/getting-started/en/index.md +7 -1
- package/static/site/getting-started/index.html +18 -0
- package/static/site/getting-started/zh/index.html +23 -1
- package/static/site/getting-started/zh/index.md +7 -1
- package/static/site/index.html +18 -0
- package/static/site/install.sh +676 -127
- package/static/site/overview/en/index.html +18 -1
- package/static/site/overview/en/index.md +2 -2
- package/static/site/overview/index.html +18 -0
- package/static/site/overview/zh/index.html +18 -1
- package/static/site/overview/zh/index.md +2 -2
- package/static/site/privacy/en/index.html +17 -0
- package/static/site/privacy/index.html +18 -0
- package/static/site/privacy/zh/index.html +17 -0
- package/static/site/zh/index.html +18 -1
- package/static/site/zh/index.md +1 -1
- package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
- package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
- package/static/site_source/downloads/install.sh +676 -127
- package/static/site_source/manifests/host_install.json +2 -10
- package/static/site_source/pages/getting-started/en.html +6 -1
- package/static/site_source/pages/getting-started/zh.html +6 -1
- package/static/site_source/pages/home/en.html +1 -1
- package/static/site_source/pages/home/zh.html +1 -1
- package/static/site_source/pages/home_main.html +3 -2
- package/static/site_source/pages/overview/en.html +1 -1
- package/static/site_source/pages/overview/zh.html +1 -1
- package/tool_scripts/install_oysterun_update.mjs +78 -13
- package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
- package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
- package/tool_scripts/oysterun_release_service.sh +5 -0
- package/tool_scripts/oysterun_service_common.sh +59 -24
- package/tool_scripts/oysterun_setup_input.sh +97 -0
- package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
- package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
- package/tool_scripts/resolve_user_host_port.mjs +15 -0
- package/tool_scripts/restart_oysterun.sh +8 -2
- package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
- package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
- package/tool_scripts/sync_host_stack_config.mjs +9 -6
- package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
- package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
- package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
- package/dev/client/web-chat/dist/assets/web-c70YaskK.js +0 -1
package/host-service/server.mjs
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import { VoiceInputReceiptStore, voiceInputFingerprint } from './voice-input-receipt-store.mjs';
|
|
2
|
+
import { readAgentFolderAvailability, requireAgentFolder, agentFolderErrorResponse } from "./agent-folder-availability.mjs";
|
|
3
|
+
import { resolveSessionTranscriptTarget, sessionTranscriptTargetFields, createHistoryTranscriptPager } from "./session-transcript-target.mjs";
|
|
4
|
+
import { withRouteCMatrixTranscriptReadOnly } from "./routec-matrix-storage-adapter.mjs";
|
|
5
|
+
import { getServiceDefinitionIdentity, buildServiceRenderBundle } from "./lifecycle-service-render-bundle.mjs";
|
|
6
|
+
import { parseHostPort, readUserHostPort } from "./host-port-resolution.mjs";
|
|
7
|
+
import {
|
|
8
|
+
assertConfirmedUpdateTarget, assertFreshContinuitySnapshot, continuityDigest, continuityFailure, recordContinuityRecovery,
|
|
9
|
+
continuityReceiptPath, prepareContinuity, readContinuity, bindFreshContinuityBaseline, writeContinuityAtomic,
|
|
10
|
+
hasCompleteContinuityResumeIdentity, compareContinuityIdentities,
|
|
11
|
+
} from "./lifecycle-continuity.mjs";
|
|
12
|
+
import { recordContinuityUserPath } from "./lifecycle-user-path.mjs";
|
|
13
|
+
import { projectContinuityOperation } from "./lifecycle-continuity-projection.mjs";
|
|
14
|
+
import { resolveContinuityQualificationOrigin } from "./lifecycle-continuity-runner.mjs";
|
|
15
|
+
import { retainContinuityFile, digestContinuityPackage } from "./lifecycle-continuity-assets.mjs";
|
|
16
|
+
import {
|
|
17
|
+
createContinuityProbeGrant, verifyContinuityProbeGrant,
|
|
18
|
+
} from "./lifecycle-continuity-probe.mjs";
|
|
1
19
|
import { spawn, spawnSync } from "child_process";
|
|
2
20
|
import { createRequire } from "module";
|
|
3
21
|
import { createServer } from "http";
|
|
@@ -24,6 +42,7 @@ import { performance } from "perf_hooks";
|
|
|
24
42
|
import { brotliCompressSync, gzipSync } from "zlib";
|
|
25
43
|
import { homedir } from "os";
|
|
26
44
|
import { WebSocketServer } from "ws";
|
|
45
|
+
import { createRouteCRequestDelayWindowController } from "./routec-request-delay-window.mjs";
|
|
27
46
|
import { SessionManager } from "./session-manager.mjs";
|
|
28
47
|
import {
|
|
29
48
|
DEFAULT_HOST_PRODUCT_CAPABILITIES,
|
|
@@ -48,12 +67,14 @@ import {
|
|
|
48
67
|
} from "./scheduler-run-product-contract.mjs";
|
|
49
68
|
import { finalizeSchedulerMutationResponse } from "./scheduler-mutation-audit.mjs";
|
|
50
69
|
import { buildSessionLifecycleRevision } from "./session-lifecycle-snapshot.mjs";
|
|
70
|
+
import { resolveProductRuntimeHostOrigin } from "./product-runtime-host-origin.mjs";
|
|
51
71
|
import { createTunnelAgent } from "./tunnel-agent-factory.mjs";
|
|
52
72
|
import { buildWebsiteCanonicalUrlPayload } from "./website-canonical-url.mjs";
|
|
53
73
|
import {
|
|
54
74
|
buildInitialSharedAgentConfig,
|
|
55
75
|
getConfigDir,
|
|
56
76
|
getConfigPath,
|
|
77
|
+
readRawConfigSource,
|
|
57
78
|
getConfigSource,
|
|
58
79
|
getDefaultConfig,
|
|
59
80
|
getDefaultHostDisplayName,
|
|
@@ -168,7 +189,20 @@ import {
|
|
|
168
189
|
logProviderStartupEvent,
|
|
169
190
|
preflightProviderStartup,
|
|
170
191
|
} from "./provider-startup-preflight.mjs";
|
|
171
|
-
import {
|
|
192
|
+
import {
|
|
193
|
+
getSessionHistory,
|
|
194
|
+
updateSessionRecord,
|
|
195
|
+
captureSessionTagsHistoryRollback,
|
|
196
|
+
} from "./session-history.mjs";
|
|
197
|
+
import {
|
|
198
|
+
RecentSessionTagsStore,
|
|
199
|
+
SessionTagLabelsStore,
|
|
200
|
+
commitSessionTagSave,
|
|
201
|
+
resolveSessionStartTags,
|
|
202
|
+
canonicalSessionTags,
|
|
203
|
+
projectSessionTags,
|
|
204
|
+
normalizeSessionTags,
|
|
205
|
+
} from "./session-tags.mjs";
|
|
172
206
|
import {
|
|
173
207
|
buildDefaultBranchName,
|
|
174
208
|
buildSessionNameConflictMessage,
|
|
@@ -212,6 +246,7 @@ import {
|
|
|
212
246
|
isLoopbackRemoteAddress,
|
|
213
247
|
verifyLocalServiceControlToken,
|
|
214
248
|
} from "./service-control-token.mjs";
|
|
249
|
+
import { reconcileFailedInitialPreflight, failedInitialPreflightState } from "./initial-preflight-recovery.mjs";
|
|
215
250
|
import {
|
|
216
251
|
UPDATE_RESTART_RUNNER_LEASE_MS,
|
|
217
252
|
evaluateCompletedRestartRecovery,
|
|
@@ -241,6 +276,7 @@ import {
|
|
|
241
276
|
} from "./host-restart-login-gate.mjs";
|
|
242
277
|
import {
|
|
243
278
|
appendHostRuntimeDiagnostic,
|
|
279
|
+
getHostRuntimeMetadataPath,
|
|
244
280
|
readHostRuntimeMetadataStatus,
|
|
245
281
|
serializeRuntimeError,
|
|
246
282
|
writeHostRuntimeMetadata,
|
|
@@ -293,12 +329,20 @@ import { TransportUsageCircuitStore } from "./connectivity/transport-usage-circu
|
|
|
293
329
|
import { createRouteCMatrixFacade } from "./matrix-facade.mjs";
|
|
294
330
|
import {
|
|
295
331
|
openRouteCMatrixSQLiteStore,
|
|
332
|
+
readRouteCMatrixSQLiteEvent,
|
|
333
|
+
readRouteCMatrixSQLiteLatestTimelineSeqs,
|
|
334
|
+
withRouteCMatrixSQLiteRoomReadContext,
|
|
296
335
|
} from "./routec-matrix-sqlite-store.mjs";
|
|
297
336
|
import { openDurableCallbackStore } from "./callback-durable-store.mjs";
|
|
298
337
|
import { createCallbackHostIntegration } from "./callback-host-integration.mjs";
|
|
338
|
+
import {
|
|
339
|
+
createCompleteMessageNotificationBroadcaster,
|
|
340
|
+
observeCommittedCompletedTurn,
|
|
341
|
+
} from "./completed-turn-notification-observer.mjs";
|
|
299
342
|
import {
|
|
300
343
|
createRouteCMatrixRoomBinding,
|
|
301
344
|
getRouteCMatrixRoomBinding,
|
|
345
|
+
getRouteCMatrixRoomBindings,
|
|
302
346
|
requireRouteCMatrixRoomBinding,
|
|
303
347
|
} from "./matrix-room-binding.mjs";
|
|
304
348
|
import {
|
|
@@ -309,12 +353,19 @@ import {
|
|
|
309
353
|
readMatrixTranscriptRecentTail,
|
|
310
354
|
} from "./matrix-transcript-read-adapter.mjs";
|
|
311
355
|
import { SessionListPreviewService } from "./session-list-preview-service.mjs";
|
|
312
|
-
import {
|
|
356
|
+
import { SessionListProjectionService } from "./session-list-projection-service.mjs";
|
|
357
|
+
import {
|
|
358
|
+
admitSessionCollectionLiveSession,
|
|
359
|
+
SessionCollectionService,
|
|
360
|
+
isSessionCollectionLiveSessionAdmitted,
|
|
361
|
+
} from "./session-collection-service.mjs";
|
|
313
362
|
import {
|
|
314
363
|
HOST_OWNER_READ_CURSOR_USER_ID,
|
|
315
364
|
acknowledgeCompletedSessionRead,
|
|
365
|
+
beginCompletedSessionChatVisit,
|
|
316
366
|
ensureCompletedSessionUnreadSchema,
|
|
317
|
-
|
|
367
|
+
createCompletedSessionUnreadReader,
|
|
368
|
+
markLatestCompletedSessionUnread,
|
|
318
369
|
} from "./completed-session-unread.mjs";
|
|
319
370
|
import { buildLiveSessionProfilePayload } from "./session-profile-runtime.mjs";
|
|
320
371
|
import {
|
|
@@ -333,6 +384,7 @@ import {
|
|
|
333
384
|
import {
|
|
334
385
|
cancelRouteCHost2IntakeForMatrixEvent,
|
|
335
386
|
getRouteCHost2IntakeProof,
|
|
387
|
+
restoreRouteCHost2IntakeFromCommittedMatrixEvent,
|
|
336
388
|
} from "./matrix-event-correlator.mjs";
|
|
337
389
|
import {
|
|
338
390
|
buildDemotedLoopStopCommandResponse,
|
|
@@ -521,14 +573,25 @@ try {
|
|
|
521
573
|
// ── Config (env vars take precedence over ~/.oysterun/config.json) ──
|
|
522
574
|
|
|
523
575
|
const DEFAULT_HOST_CONFIG = getDefaultConfig();
|
|
576
|
+
const RESOLVED_HOST_PORT = process.env.OYSTERUN_PORT !== undefined
|
|
577
|
+
? parseHostPort(process.env.OYSTERUN_PORT)
|
|
578
|
+
: getActiveStackName() && getActiveStackName() !== "production"
|
|
579
|
+
? parseHostPort(readRawConfigSource().port ?? DEFAULT_HOST_CONFIG.port)
|
|
580
|
+
: readUserHostPort({
|
|
581
|
+
configPath: getConfigPath(), runtimePath: getHostRuntimeMetadataPath(), configDir: getConfigDir(),
|
|
582
|
+
existingServicePaths: [join(getConfigDir(), "run", "oysterun-host.pid"),
|
|
583
|
+
join(getConfigDir(), "run", "oysterun-host.origin.tsv"),
|
|
584
|
+
join(homedir(), "Library", "LaunchAgents", "com.oysterun.host.plist")],
|
|
585
|
+
});
|
|
586
|
+
if (RESOLVED_HOST_PORT === null) throw new Error("Invalid Host port; use an integer from 1 to 65535.");
|
|
587
|
+
if (process.env.OYSTERUN_PORT === undefined && existsSync(getConfigPath()) &&
|
|
588
|
+
!Object.hasOwn(readRawConfigSource(), "port")) writeConfig({ port: RESOLVED_HOST_PORT });
|
|
524
589
|
const CONNECTION_MODE =
|
|
525
590
|
getConfigValue("connection_mode", "OYSTERUN_CONNECTION_MODE") === "cloud"
|
|
526
591
|
? "cloud"
|
|
527
592
|
: "direct";
|
|
528
|
-
const PORT =
|
|
529
|
-
|
|
530
|
-
10
|
|
531
|
-
);
|
|
593
|
+
const PORT = RESOLVED_HOST_PORT;
|
|
594
|
+
if (PORT === null) throw new Error("Invalid Host port; use an integer from 1 to 65535.");
|
|
532
595
|
let localServiceControlReady = false;
|
|
533
596
|
try {
|
|
534
597
|
ensureLocalServiceControlToken({ configDir: getConfigDir() });
|
|
@@ -1010,23 +1073,6 @@ function isRouteCMatrixBindingNotFoundError(err) {
|
|
|
1010
1073
|
);
|
|
1011
1074
|
}
|
|
1012
1075
|
|
|
1013
|
-
function getLastMatrixMessagePreviewForBinding(binding) {
|
|
1014
|
-
return sessionListPreviewService.readForBinding(binding).last_message_preview;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function getLastMessagePreview(agentFolder, agentId, sessionId) {
|
|
1018
|
-
const binding = getRouteCMatrixRoomBinding(sessionId);
|
|
1019
|
-
if (binding) {
|
|
1020
|
-
if (agentId && binding.host_agent_id !== agentId) return null;
|
|
1021
|
-
try {
|
|
1022
|
-
return getLastMatrixMessagePreviewForBinding(binding);
|
|
1023
|
-
} catch {
|
|
1024
|
-
return null;
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
return null;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
1076
|
function shouldIncludeSessionListPreviews(url) {
|
|
1031
1077
|
const raw = normalizeString(url.searchParams.get("include_previews"));
|
|
1032
1078
|
return raw !== "0" && raw !== "false" && raw !== "no";
|
|
@@ -1562,6 +1608,39 @@ function isCleanSessionChatAppShellPath(requestPath) {
|
|
|
1562
1608
|
return /^\/app\/sessions\/[^/]+\/chat\/?$/.test(requestPath);
|
|
1563
1609
|
}
|
|
1564
1610
|
|
|
1611
|
+
function isCleanSessionMigratedSurfaceAppShellPath(requestPath) {
|
|
1612
|
+
return /^\/app\/sessions\/[^/]+\/(?:profile|loop|explorer|file-preview)\/?$/.test(requestPath);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function buildLegacySessionSurfaceRedirectLocation(requestUrl) {
|
|
1616
|
+
if (!(requestUrl instanceof URL)) return null;
|
|
1617
|
+
const surface =
|
|
1618
|
+
requestUrl.pathname === "/app/agent-profile"
|
|
1619
|
+
? "profile"
|
|
1620
|
+
: requestUrl.pathname === "/app/session-loop"
|
|
1621
|
+
? "loop"
|
|
1622
|
+
: null;
|
|
1623
|
+
if (!surface) return null;
|
|
1624
|
+
const sessionId = normalizeString(requestUrl.searchParams.get("session_id"));
|
|
1625
|
+
if (!sessionId) return null;
|
|
1626
|
+
return `/app/sessions/${encodeURIComponent(sessionId)}/${surface}${requestUrl.search}`;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function isStandaloneMigratedSurfaceAppShellPath(requestPath) {
|
|
1630
|
+
return /^\/app\/(?:file-preview|web|browser|mail(?:\/[^/]+)?)\/?$/.test(requestPath);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function isStandaloneMigratedBrowseAppShellPath(requestPath) {
|
|
1634
|
+
return requestPath === "/app/explorer" ||
|
|
1635
|
+
requestPath === "/app/explorer/" ||
|
|
1636
|
+
requestPath === "/app/folder-picker" ||
|
|
1637
|
+
requestPath === "/app/folder-picker/" ||
|
|
1638
|
+
requestPath === "/app/change-password" ||
|
|
1639
|
+
requestPath === "/app/change-password/" ||
|
|
1640
|
+
requestPath === "/app/tool-detail" ||
|
|
1641
|
+
requestPath === "/app/tool-detail/";
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1565
1644
|
function getCleanSessionChatAppShellSessionId(requestPath) {
|
|
1566
1645
|
const match =
|
|
1567
1646
|
typeof requestPath === "string"
|
|
@@ -1575,6 +1654,23 @@ function getCleanSessionChatAppShellSessionId(requestPath) {
|
|
|
1575
1654
|
}
|
|
1576
1655
|
}
|
|
1577
1656
|
|
|
1657
|
+
function getCleanSessionAppShellSessionId(requestPath) {
|
|
1658
|
+
const chatSessionId = getCleanSessionChatAppShellSessionId(requestPath);
|
|
1659
|
+
if (chatSessionId) return chatSessionId;
|
|
1660
|
+
const match =
|
|
1661
|
+
typeof requestPath === "string"
|
|
1662
|
+
? requestPath.match(
|
|
1663
|
+
/^\/app\/sessions\/([^/]+)\/(?:profile|loop|explorer|file-preview)\/?$/,
|
|
1664
|
+
)
|
|
1665
|
+
: null;
|
|
1666
|
+
if (!match) return null;
|
|
1667
|
+
try {
|
|
1668
|
+
return decodeURIComponent(match[1]);
|
|
1669
|
+
} catch {
|
|
1670
|
+
return match[1];
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1578
1674
|
function buildStaleCleanSessionChatRedirectLocation() {
|
|
1579
1675
|
return "/app/sessions?notice=stale_clean_chat";
|
|
1580
1676
|
}
|
|
@@ -1634,7 +1730,11 @@ function serveWebChatAppDocument(req, res) {
|
|
|
1634
1730
|
|
|
1635
1731
|
const response = buildRouteCStaticResponse(
|
|
1636
1732
|
req,
|
|
1637
|
-
readFileSync(ROUTEC_WEB_CHAT_INDEX_PATH),
|
|
1733
|
+
readFileSync(ROUTEC_WEB_CHAT_INDEX_PATH, "utf8").replace("</head>",
|
|
1734
|
+
`<script id="host-default-session-defaults-json" type="application/json">${JSON.stringify(getDefaultSessionDefaults()).replace(/</g, "\\u003c")}</script>` +
|
|
1735
|
+
`<script id="host-default-shared-allowed-paths-json" type="application/json">${JSON.stringify(getDefaultSharedAllowedPaths()).replace(/</g, "\\u003c")}</script>` +
|
|
1736
|
+
`<script id="host-routec-auth-loss-diagnostics-json" type="application/json">${JSON.stringify(readConfig().debug_routec_auth_loss_diagnostics_enabled === true)}</script>` +
|
|
1737
|
+
`<script id="host-system-timezone-json" type="application/json">${JSON.stringify(getHostSystemTimezone()).replace(/</g, "\\u003c")}</script></head>`),
|
|
1638
1738
|
getWebChatAppAssetHeaders(ROUTEC_WEB_CHAT_INDEX_PATH),
|
|
1639
1739
|
{ assetPath: ROUTEC_WEB_CHAT_INDEX_PATH }
|
|
1640
1740
|
);
|
|
@@ -1804,6 +1904,7 @@ function getDashboardSessionRecord(token) {
|
|
|
1804
1904
|
function dashboardAuthenticate(req) {
|
|
1805
1905
|
const token = getDashboardSessionToken(req);
|
|
1806
1906
|
if (!token) return null;
|
|
1907
|
+
recordContinuityAuthenticatedUserPath(req);
|
|
1807
1908
|
return {
|
|
1808
1909
|
user_id: "dashboard-admin",
|
|
1809
1910
|
device_id: DEVICE_ID || "local",
|
|
@@ -2486,6 +2587,7 @@ async function dispatchHostScheduleThroughTransientAcp(
|
|
|
2486
2587
|
messageId,
|
|
2487
2588
|
env: preparedRuntimeWorkspace.normalizedRuntimeEnv,
|
|
2488
2589
|
redactionValues: mailCapability?.redactionValues || [],
|
|
2590
|
+
timeoutMs: schedule.run_timeout_seconds * 1000,
|
|
2489
2591
|
});
|
|
2490
2592
|
return {
|
|
2491
2593
|
...result,
|
|
@@ -2526,6 +2628,7 @@ function createSchedulerSessionDispatcher(
|
|
|
2526
2628
|
messageId,
|
|
2527
2629
|
env: mailCapability?.env || {},
|
|
2528
2630
|
redactionValues: mailCapability?.redactionValues || [],
|
|
2631
|
+
timeoutMs: schedule.run_timeout_seconds * 1000,
|
|
2529
2632
|
});
|
|
2530
2633
|
return {
|
|
2531
2634
|
...result,
|
|
@@ -3015,6 +3118,9 @@ function buildHostSchedulerPayloadFromBody(body) {
|
|
|
3015
3118
|
rule: buildHostSchedulerRuleFromBody(body),
|
|
3016
3119
|
timezone: normalizeString(body.timezone),
|
|
3017
3120
|
enabled: body.enabled === true,
|
|
3121
|
+
runTimeoutSeconds: hasBodyField(body, "run_timeout_seconds")
|
|
3122
|
+
? body.run_timeout_seconds
|
|
3123
|
+
: undefined,
|
|
3018
3124
|
};
|
|
3019
3125
|
}
|
|
3020
3126
|
|
|
@@ -3045,6 +3151,9 @@ function buildHostSchedulerPatchPayloadFromBody(body) {
|
|
|
3045
3151
|
if (hasBodyField(body, "enabled")) {
|
|
3046
3152
|
payload.enabled = body.enabled === true;
|
|
3047
3153
|
}
|
|
3154
|
+
if (hasBodyField(body, "run_timeout_seconds")) {
|
|
3155
|
+
payload.runTimeoutSeconds = body.run_timeout_seconds;
|
|
3156
|
+
}
|
|
3048
3157
|
return payload;
|
|
3049
3158
|
}
|
|
3050
3159
|
|
|
@@ -3053,16 +3162,11 @@ function getLoopbackHostOrigin() {
|
|
|
3053
3162
|
}
|
|
3054
3163
|
|
|
3055
3164
|
function getProductRuntimeHostOrigin() {
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
);
|
|
3062
|
-
if (configuredDirectHostUrl) {
|
|
3063
|
-
return configuredDirectHostUrl.replace(/\/+$/, "");
|
|
3064
|
-
}
|
|
3065
|
-
return getLoopbackHostOrigin();
|
|
3165
|
+
return resolveProductRuntimeHostOrigin({
|
|
3166
|
+
config: readConfig(),
|
|
3167
|
+
directHostUrl: DIRECT_HOST_URL,
|
|
3168
|
+
port: PORT,
|
|
3169
|
+
});
|
|
3066
3170
|
}
|
|
3067
3171
|
|
|
3068
3172
|
function getSchedulerMailHostOrigin() {
|
|
@@ -3511,6 +3615,7 @@ function resolveSchedulerTargetToDirectProviderRun({ targetBinding }) {
|
|
|
3511
3615
|
}
|
|
3512
3616
|
|
|
3513
3617
|
const sessionManager = new SessionManager();
|
|
3618
|
+
const voiceInputReceipts = new VoiceInputReceiptStore();
|
|
3514
3619
|
const voiceInputSingleFlight = new VoiceInputSingleFlight();
|
|
3515
3620
|
const voiceInputUsageStore = new VoiceInputUsageStore({
|
|
3516
3621
|
configDir: getConfigDir(),
|
|
@@ -3520,7 +3625,153 @@ const voiceInputRecordingCollector = new VoiceInputRecordingCollector({
|
|
|
3520
3625
|
isOwnerCaptureEnabled: () =>
|
|
3521
3626
|
readVoiceInputPrivateSettings(getConfigDir()).owner_capture_enabled === true,
|
|
3522
3627
|
});
|
|
3523
|
-
|
|
3628
|
+
let sessionListProjectionReadState = null;
|
|
3629
|
+
function getSessionListProjectionReadState() {
|
|
3630
|
+
if (sessionListProjectionReadState) return sessionListProjectionReadState;
|
|
3631
|
+
const db = openRouteCMatrixSQLiteStore({
|
|
3632
|
+
jsonStoragePath: getRouteCMatrixStoragePath(),
|
|
3633
|
+
});
|
|
3634
|
+
const unreadReader = createCompletedSessionUnreadReader(db, {
|
|
3635
|
+
hostUserId: HOST_OWNER_READ_CURSOR_USER_ID,
|
|
3636
|
+
});
|
|
3637
|
+
sessionListProjectionReadState = { db, unreadReader };
|
|
3638
|
+
return sessionListProjectionReadState;
|
|
3639
|
+
}
|
|
3640
|
+
const sessionListPreviewService = new SessionListPreviewService({
|
|
3641
|
+
readCheckpoints: ({ bindings }) => {
|
|
3642
|
+
const { db } = getSessionListProjectionReadState();
|
|
3643
|
+
return readRouteCMatrixSQLiteLatestTimelineSeqs(db, {
|
|
3644
|
+
roomIds: bindings.map((binding) => binding.matrix_room_id),
|
|
3645
|
+
});
|
|
3646
|
+
},
|
|
3647
|
+
});
|
|
3648
|
+
const sessionListProjectionService = new SessionListProjectionService({
|
|
3649
|
+
readTagLabels: currentSessionTagLabels,
|
|
3650
|
+
readBindings: getRouteCMatrixRoomBindings,
|
|
3651
|
+
previewService: sessionListPreviewService,
|
|
3652
|
+
prepareDecorationRead: () => {
|
|
3653
|
+
const { db, unreadReader } = getSessionListProjectionReadState();
|
|
3654
|
+
return {
|
|
3655
|
+
schemaVersion: unreadReader.schema_version,
|
|
3656
|
+
readUnread: (binding, readContext) =>
|
|
3657
|
+
unreadReader.read(binding, readContext),
|
|
3658
|
+
withRoomRead: (roomId, read) => withRouteCMatrixSQLiteRoomReadContext(db, { roomId }, read),
|
|
3659
|
+
};
|
|
3660
|
+
},
|
|
3661
|
+
});
|
|
3662
|
+
|
|
3663
|
+
const sessionCollectionAdmittedSessionIds = new Set();
|
|
3664
|
+
|
|
3665
|
+
function buildCanonicalSessionCollectionRows() {
|
|
3666
|
+
const allLiveSessions = sessionManager.list();
|
|
3667
|
+
const desiredRestore = sessionManager.getDesiredOpenRestoreStatus();
|
|
3668
|
+
const failedRestoreBySessionId = new Map(
|
|
3669
|
+
desiredRestore.sessions
|
|
3670
|
+
.filter((entry) => entry.state === "failed")
|
|
3671
|
+
.map((entry) => [entry.session_id, entry])
|
|
3672
|
+
);
|
|
3673
|
+
const liveSessions = allLiveSessions.filter((session) => {
|
|
3674
|
+
const sessionId = session.id || session.sessionId;
|
|
3675
|
+
return (
|
|
3676
|
+
!failedRestoreBySessionId.has(sessionId) &&
|
|
3677
|
+
sessionCollectionAdmittedSessionIds.has(sessionId) &&
|
|
3678
|
+
isSessionCollectionLiveSessionAdmitted(
|
|
3679
|
+
sessionManager.getSession(sessionId),
|
|
3680
|
+
getRouteCMatrixRoomBinding(sessionId)
|
|
3681
|
+
)
|
|
3682
|
+
);
|
|
3683
|
+
});
|
|
3684
|
+
const liveSessionIds = new Set(
|
|
3685
|
+
liveSessions.map((session) => session.id || session.sessionId)
|
|
3686
|
+
);
|
|
3687
|
+
const liveRows = sessionListProjectionService
|
|
3688
|
+
.metadata(liveSessions)
|
|
3689
|
+
.session_rows.map((row) => {
|
|
3690
|
+
const pendingControlRequests = serializePendingControlRequests(
|
|
3691
|
+
sessionManager.getSession(row.session_id)
|
|
3692
|
+
);
|
|
3693
|
+
const providerLifecycle = sessionManager.getRouteCProviderLifecycle(
|
|
3694
|
+
row.session_id,
|
|
3695
|
+
{ pendingControlRequests }
|
|
3696
|
+
);
|
|
3697
|
+
return {
|
|
3698
|
+
...row,
|
|
3699
|
+
provider_lifecycle_active: providerLifecycle.active === true,
|
|
3700
|
+
restore_failed: false,
|
|
3701
|
+
restore_failed_reason: null,
|
|
3702
|
+
manual_start_supported: false,
|
|
3703
|
+
manual_start_setup_reason: null,
|
|
3704
|
+
};
|
|
3705
|
+
});
|
|
3706
|
+
const historyRows = [];
|
|
3707
|
+
const historyRecords = [...getSessionHistory()];
|
|
3708
|
+
const historyRecordBySessionId = new Map(
|
|
3709
|
+
historyRecords.map((record) => [record.session_id, record])
|
|
3710
|
+
);
|
|
3711
|
+
for (const failed of failedRestoreBySessionId.values()) {
|
|
3712
|
+
if (historyRecordBySessionId.has(failed.session_id)) continue;
|
|
3713
|
+
historyRecords.push({
|
|
3714
|
+
session_id: failed.session_id,
|
|
3715
|
+
session_name: failed.session_name || failed.session_id,
|
|
3716
|
+
agent_id: failed.agent_id,
|
|
3717
|
+
agent_folder: failed.agent_folder || null,
|
|
3718
|
+
runtime: failed.provider || null,
|
|
3719
|
+
model: failed.model || null,
|
|
3720
|
+
created_at: failed.started_at || null,
|
|
3721
|
+
last_active_at: failed.last_active_at || failed.started_at || null,
|
|
3722
|
+
tags: [],
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
for (const record of historyRecords) {
|
|
3726
|
+
if (!record.session_id || liveSessionIds.has(record.session_id)) continue;
|
|
3727
|
+
const restoreFailure =
|
|
3728
|
+
failedRestoreBySessionId.get(record.session_id) ||
|
|
3729
|
+
(record.restore_failed_reason
|
|
3730
|
+
? {
|
|
3731
|
+
session_id: record.session_id,
|
|
3732
|
+
state: "failed",
|
|
3733
|
+
reason: record.restore_failed_reason,
|
|
3734
|
+
}
|
|
3735
|
+
: null);
|
|
3736
|
+
const folderAvailable = Boolean(
|
|
3737
|
+
record.agent_folder && existsSync(record.agent_folder)
|
|
3738
|
+
);
|
|
3739
|
+
// Retained History membership is lifecycle-owned, even when its folder is gone.
|
|
3740
|
+
if (!record.agent_id) continue;
|
|
3741
|
+
const binding = getRouteCMatrixRoomBinding(record.session_id);
|
|
3742
|
+
const disposition = buildHistoryResumeDisposition(record, Boolean(binding));
|
|
3743
|
+
historyRows.push({
|
|
3744
|
+
kind: "history",
|
|
3745
|
+
session_id: record.session_id,
|
|
3746
|
+
agent_id: record.agent_id,
|
|
3747
|
+
session_name: record.session_name || record.session_id,
|
|
3748
|
+
...projectSessionTags(record),
|
|
3749
|
+
agent_folder: record.agent_folder || null,
|
|
3750
|
+
runtime: record.runtime || null,
|
|
3751
|
+
model: record.model || null,
|
|
3752
|
+
started_at: record.created_at || null,
|
|
3753
|
+
last_active_at: record.last_active_at || record.created_at || null,
|
|
3754
|
+
running: false,
|
|
3755
|
+
resume_supported: disposition.resume_supported,
|
|
3756
|
+
resume_unavailable_reason: disposition.resume_unavailable_reason,
|
|
3757
|
+
restore_failed: Boolean(restoreFailure),
|
|
3758
|
+
restore_failed_reason: restoreFailure?.reason || null,
|
|
3759
|
+
manual_start_supported:
|
|
3760
|
+
Boolean(restoreFailure) && folderAvailable && disposition.resume_supported,
|
|
3761
|
+
manual_start_setup_reason: restoreFailure
|
|
3762
|
+
? folderAvailable
|
|
3763
|
+
? disposition.resume_unavailable_reason
|
|
3764
|
+
: "agent_folder_missing"
|
|
3765
|
+
: null,
|
|
3766
|
+
});
|
|
3767
|
+
}
|
|
3768
|
+
return [...liveRows, ...historyRows];
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
const sessionCollectionService = new SessionCollectionService({
|
|
3772
|
+
readRows: buildCanonicalSessionCollectionRows,
|
|
3773
|
+
retention: 128,
|
|
3774
|
+
});
|
|
3524
3775
|
const browserRootProjectIndex = new BrowserRootProjectIndex({ logger: console });
|
|
3525
3776
|
setTranscriptSiteProjectFolderResolver((projectId) =>
|
|
3526
3777
|
browserRootProjectIndex.resolveProjectFolder(projectId, { allowInvalid: true })
|
|
@@ -3548,6 +3799,10 @@ const toolEventDetailStore = createToolEventDetailStore({
|
|
|
3548
3799
|
});
|
|
3549
3800
|
const schedulerService = new SchedulerService({
|
|
3550
3801
|
mailStore,
|
|
3802
|
+
sessionLoopUnavailableGraceMs:
|
|
3803
|
+
process.env.OYSTERUN_SESSION_LOOP_DEFINITION_UNAVAILABLE_GRACE_MS === undefined
|
|
3804
|
+
? undefined
|
|
3805
|
+
: Number(process.env.OYSTERUN_SESSION_LOOP_DEFINITION_UNAVAILABLE_GRACE_MS),
|
|
3551
3806
|
getHostOrigin: getSchedulerMailHostOrigin,
|
|
3552
3807
|
getBrowserRootProjects: listBrowserRootSchedulerProjects,
|
|
3553
3808
|
getBrowserRootMutationProjects: listBrowserRootSchedulerMutationProjects,
|
|
@@ -3589,31 +3844,17 @@ routeCMatrixFacade = createRouteCMatrixFacade({
|
|
|
3589
3844
|
onCommittedMatrixEvent: async (matrixEvent) => {
|
|
3590
3845
|
sessionListPreviewService.invalidateRoom(matrixEvent?.room_id);
|
|
3591
3846
|
voiceInputRecordingCollector.observeCommittedMatrixEvent(matrixEvent);
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
const terminalCompletion =
|
|
3604
|
-
semantic.provider_completion_marker ===
|
|
3605
|
-
"oysterun_provider_complete_message_notification_v1" &&
|
|
3606
|
-
semantic.provider_completion_state !== "pending_turn_completion";
|
|
3607
|
-
setImmediate(async () => {
|
|
3608
|
-
try {
|
|
3609
|
-
await callbackHostIntegration.observePendingForTarget(targetSessionId, {
|
|
3610
|
-
finalizeSuccessfulWithoutMessage: terminalCompletion,
|
|
3611
|
-
});
|
|
3612
|
-
} catch (err) {
|
|
3613
|
-
console.warn(`[p203-callback] committed truth observation failed: ${err.message || err}`);
|
|
3614
|
-
}
|
|
3615
|
-
});
|
|
3616
|
-
}
|
|
3847
|
+
observeCommittedCompletedTurn({
|
|
3848
|
+
matrixEvent,
|
|
3849
|
+
matrixStore: STARTUP_MATRIX_STORAGE,
|
|
3850
|
+
getSession: (sessionId) => sessionManager.getSession(sessionId),
|
|
3851
|
+
broadcastCandidate: broadcastCompleteMessageNotificationCandidate,
|
|
3852
|
+
dispatchCandidate: (candidate, options) =>
|
|
3853
|
+
apnsCompleteMessageDispatcher
|
|
3854
|
+
.dispatchNotifiableProviderOutputNotificationCandidate(candidate, options),
|
|
3855
|
+
callbackHostIntegration,
|
|
3856
|
+
recordProjectionDiagnostic: recordCompletedTurnNotificationProjectionDiagnostic,
|
|
3857
|
+
});
|
|
3617
3858
|
},
|
|
3618
3859
|
});
|
|
3619
3860
|
// Matrix storage must be bootstrapped before the first consumer opens it. Keep
|
|
@@ -3659,6 +3900,30 @@ callbackHostIntegration = createCallbackHostIntegration({
|
|
|
3659
3900
|
serverEventId,
|
|
3660
3901
|
}) => {
|
|
3661
3902
|
const binding = requireRouteCMatrixRoomBinding(session.id);
|
|
3903
|
+
if (
|
|
3904
|
+
deliveryKind === "callback_result" &&
|
|
3905
|
+
!getRouteCHost2IntakeProof({
|
|
3906
|
+
hostSessionId: session.id,
|
|
3907
|
+
matrixRoomId: binding.matrix_room_id,
|
|
3908
|
+
serverEventId,
|
|
3909
|
+
})
|
|
3910
|
+
) {
|
|
3911
|
+
const committed = readRouteCMatrixSQLiteEvent(STARTUP_MATRIX_STORAGE, {
|
|
3912
|
+
roomId: binding.matrix_room_id,
|
|
3913
|
+
eventId: serverEventId,
|
|
3914
|
+
});
|
|
3915
|
+
if (!committed) {
|
|
3916
|
+
throw new Error("P238 callback provider recovery requires committed Matrix truth");
|
|
3917
|
+
}
|
|
3918
|
+
restoreRouteCHost2IntakeFromCommittedMatrixEvent({
|
|
3919
|
+
hostSessionId: session.id,
|
|
3920
|
+
matrixUserId: committed.sender,
|
|
3921
|
+
matrixRoomId: binding.matrix_room_id,
|
|
3922
|
+
txnId: messageId,
|
|
3923
|
+
serverEventId,
|
|
3924
|
+
content: committed.content,
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3662
3927
|
return sessionManager.deliverRouteCMatrixUserEventToProvider({
|
|
3663
3928
|
sessionId: session.id,
|
|
3664
3929
|
matrixUserId: binding.matrix_user_id,
|
|
@@ -3674,6 +3939,12 @@ callbackHostIntegration = createCallbackHostIntegration({
|
|
|
3674
3939
|
},
|
|
3675
3940
|
cancelProviderDelivery: ({ sessionId, messageId }) =>
|
|
3676
3941
|
sessionManager.cancelOutboxMessage(sessionId, messageId),
|
|
3942
|
+
reconcileProviderCancellation: ({ sessionId, messageId, providerText }) =>
|
|
3943
|
+
sessionManager.reconcileProviderMessageCancellation(sessionId, {
|
|
3944
|
+
messageId,
|
|
3945
|
+
text: providerText,
|
|
3946
|
+
reconcileProviderAcceptance: true,
|
|
3947
|
+
}),
|
|
3677
3948
|
});
|
|
3678
3949
|
const schedulerSessionDispatcher = createSchedulerSessionDispatcher(
|
|
3679
3950
|
sessionManager,
|
|
@@ -3700,6 +3971,18 @@ const providerAuthManager = new ProviderAuthManager({
|
|
|
3700
3971
|
});
|
|
3701
3972
|
const terminalSessionManager = new TerminalSessionManager();
|
|
3702
3973
|
let lastHostRestartRestoreStatus = null;
|
|
3974
|
+
let lastDesiredOpenRestoreStatus = null;
|
|
3975
|
+
let desiredOpenReconciliationPromise = null;
|
|
3976
|
+
const DESIRED_OPEN_RESTORE_MAX_CONCURRENCY = 4;
|
|
3977
|
+
const DESIRED_OPEN_RESTORE_DEFAULT_SESSION_TIMEOUT_MS = 30_000;
|
|
3978
|
+
const DESIRED_OPEN_RESTORE_SESSION_TIMEOUT_MS = (() => {
|
|
3979
|
+
const injected = Number(
|
|
3980
|
+
process.env.OYSTERUN_DESIRED_OPEN_RESTORE_SESSION_TIMEOUT_MS
|
|
3981
|
+
);
|
|
3982
|
+
return isBoundaryTestRuntime() && Number.isInteger(injected) && injected >= 0
|
|
3983
|
+
? injected
|
|
3984
|
+
: DESIRED_OPEN_RESTORE_DEFAULT_SESSION_TIMEOUT_MS;
|
|
3985
|
+
})();
|
|
3703
3986
|
const telegramRealAdapter = createTelegramBotApiAdapter();
|
|
3704
3987
|
const telegramMediaTransactionStore = new TelegramMediaTransactionStore();
|
|
3705
3988
|
const telegramMockDebugGateClosedAdapter = {
|
|
@@ -3845,28 +4128,81 @@ let hostTelemetryScheduler = null;
|
|
|
3845
4128
|
*/
|
|
3846
4129
|
const sessionSubscribers = new Map();
|
|
3847
4130
|
const completeMessageNotificationSubscribers = new Set();
|
|
4131
|
+
const completedTurnNotificationProjectionDiagnosticKeys = new Set();
|
|
3848
4132
|
const mailNotificationSubscribers = new Set();
|
|
4133
|
+
const sessionCollectionSubscribers = new Set();
|
|
3849
4134
|
|
|
3850
|
-
function
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
const
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
4135
|
+
function recordCompletedTurnNotificationProjectionDiagnostic(completion) {
|
|
4136
|
+
const identity = completion?.identity || {};
|
|
4137
|
+
const terminal = completion?.terminal || {};
|
|
4138
|
+
const reason = completion?.diagnostics?.notification_projection_reason;
|
|
4139
|
+
const key = [
|
|
4140
|
+
reason,
|
|
4141
|
+
identity.host_session_id,
|
|
4142
|
+
identity.matrix_room_id,
|
|
4143
|
+
identity.provider_turn_id,
|
|
4144
|
+
terminal.completion_event_id,
|
|
4145
|
+
].join(":");
|
|
4146
|
+
if (completedTurnNotificationProjectionDiagnosticKeys.has(key)) return;
|
|
4147
|
+
completedTurnNotificationProjectionDiagnosticKeys.add(key);
|
|
4148
|
+
if (completedTurnNotificationProjectionDiagnosticKeys.size > 500) {
|
|
4149
|
+
const oldest = completedTurnNotificationProjectionDiagnosticKeys.values().next().value;
|
|
4150
|
+
if (oldest) completedTurnNotificationProjectionDiagnosticKeys.delete(oldest);
|
|
4151
|
+
}
|
|
4152
|
+
console.warn("[p218-notification] completed-message projection failed closed", {
|
|
4153
|
+
reason,
|
|
4154
|
+
projection_source: "shared_completed_turn_service",
|
|
4155
|
+
host_session_id: identity.host_session_id || null,
|
|
4156
|
+
matrix_room_id: identity.matrix_room_id || null,
|
|
4157
|
+
provider_turn_id: identity.provider_turn_id || null,
|
|
4158
|
+
completion_event_id: terminal.completion_event_id || null,
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
function commitSessionCollectionChange(changeKinds, affectedSessionIds = []) {
|
|
4163
|
+
return sessionCollectionService.reconcile({
|
|
4164
|
+
changeKinds,
|
|
4165
|
+
affectedSessionIds,
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
function admitSessionCollectionSession(
|
|
4170
|
+
sessionId,
|
|
4171
|
+
changeKinds = ["create"],
|
|
4172
|
+
{ required = false } = {}
|
|
4173
|
+
) {
|
|
4174
|
+
const session = sessionManager.getSession(sessionId);
|
|
4175
|
+
const binding = getRouteCMatrixRoomBinding(sessionId);
|
|
4176
|
+
const admittedSessionId = admitSessionCollectionLiveSession({
|
|
4177
|
+
session,
|
|
4178
|
+
binding,
|
|
4179
|
+
admittedSessionIds: sessionCollectionAdmittedSessionIds,
|
|
4180
|
+
required,
|
|
3857
4181
|
});
|
|
3858
|
-
|
|
4182
|
+
if (!admittedSessionId) return null;
|
|
4183
|
+
return commitSessionCollectionChange(changeKinds, [sessionId]);
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
sessionCollectionService.subscribe((event) => {
|
|
4187
|
+
const payload = JSON.stringify(event);
|
|
4188
|
+
for (const ws of [...sessionCollectionSubscribers]) {
|
|
3859
4189
|
if (ws.readyState !== 1) {
|
|
3860
|
-
|
|
4190
|
+
sessionCollectionSubscribers.delete(ws);
|
|
3861
4191
|
continue;
|
|
3862
4192
|
}
|
|
3863
4193
|
try {
|
|
3864
4194
|
ws.send(payload);
|
|
3865
4195
|
} catch {
|
|
3866
|
-
|
|
4196
|
+
sessionCollectionSubscribers.delete(ws);
|
|
3867
4197
|
}
|
|
3868
4198
|
}
|
|
3869
|
-
}
|
|
4199
|
+
});
|
|
4200
|
+
|
|
4201
|
+
const broadcastCompleteMessageNotificationCandidate =
|
|
4202
|
+
createCompleteMessageNotificationBroadcaster({
|
|
4203
|
+
subscribers: completeMessageNotificationSubscribers,
|
|
4204
|
+
isEnabled: isCompleteMessageCandidateNotificationEnabled,
|
|
4205
|
+
});
|
|
3870
4206
|
|
|
3871
4207
|
function broadcastMailNotificationCandidate(candidate) {
|
|
3872
4208
|
if (!candidate || candidate.source !== "mail_item_committed") return;
|
|
@@ -4456,6 +4792,13 @@ sessionManager.on("runtimeEvent", (agentId, event) => {
|
|
|
4456
4792
|
const enrichedEvent = ensurePersistableEventFields(
|
|
4457
4793
|
attachRuntimeTurnId(event, () => null)
|
|
4458
4794
|
);
|
|
4795
|
+
if (enrichedEvent.type === "session.ready") {
|
|
4796
|
+
const readySessionId =
|
|
4797
|
+
enrichedEvent.sessionId || enrichedEvent.session_id;
|
|
4798
|
+
if (sessionManager.getSession(readySessionId)?.hostRestartRestore) {
|
|
4799
|
+
admitSessionCollectionSession(readySessionId, ["restore"]);
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4459
4802
|
recordRuntimeEventTelemetry(enrichedEvent);
|
|
4460
4803
|
if (
|
|
4461
4804
|
enrichedEvent.type === "session.notice" &&
|
|
@@ -4496,6 +4839,24 @@ sessionManager.on("runtimeEvent", (agentId, event) => {
|
|
|
4496
4839
|
}
|
|
4497
4840
|
}
|
|
4498
4841
|
}
|
|
4842
|
+
if (
|
|
4843
|
+
enrichedEvent.type === "session.ready" &&
|
|
4844
|
+
callbackHostIntegration
|
|
4845
|
+
) {
|
|
4846
|
+
const readySessionId =
|
|
4847
|
+
enrichedEvent.sessionId || enrichedEvent.session_id || null;
|
|
4848
|
+
if (readySessionId) {
|
|
4849
|
+
setImmediate(async () => {
|
|
4850
|
+
try {
|
|
4851
|
+
await callbackHostIntegration.reconcileSourceReady(readySessionId);
|
|
4852
|
+
} catch (err) {
|
|
4853
|
+
console.warn(
|
|
4854
|
+
`[p238-callback] source-ready reconciliation failed: ${err.message || err}`
|
|
4855
|
+
);
|
|
4856
|
+
}
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4499
4860
|
const routeCSemanticRuntimeTypes = new Set([
|
|
4500
4861
|
"message.assistant",
|
|
4501
4862
|
"message.thinking",
|
|
@@ -4519,6 +4880,21 @@ sessionManager.on("runtimeEvent", (agentId, event) => {
|
|
|
4519
4880
|
) {
|
|
4520
4881
|
scheduleRouteCSemanticBridgeWrite(enrichedEvent);
|
|
4521
4882
|
}
|
|
4883
|
+
const lifecycleSessionId =
|
|
4884
|
+
enrichedEvent.sessionId || enrichedEvent.session_id || null;
|
|
4885
|
+
if (
|
|
4886
|
+
lifecycleSessionId &&
|
|
4887
|
+
(enrichedEvent.type === "outbox.message" ||
|
|
4888
|
+
enrichedEvent.type === "control.request" ||
|
|
4889
|
+
enrichedEvent.type === "control.outcome" ||
|
|
4890
|
+
(enrichedEvent.type === "session.notice" &&
|
|
4891
|
+
enrichedEvent.subtype === "delivery.state"))
|
|
4892
|
+
) {
|
|
4893
|
+
commitSessionCollectionChange(
|
|
4894
|
+
["provider_lifecycle"],
|
|
4895
|
+
[lifecycleSessionId]
|
|
4896
|
+
);
|
|
4897
|
+
}
|
|
4522
4898
|
broadcast(enrichedEvent.sessionId || enrichedEvent.session_id || null, {
|
|
4523
4899
|
type: "event",
|
|
4524
4900
|
event: enrichedEvent,
|
|
@@ -4855,6 +5231,115 @@ function materializeRouteCMatrixBindingForSessionSend(session) {
|
|
|
4855
5231
|
};
|
|
4856
5232
|
}
|
|
4857
5233
|
|
|
5234
|
+
function sessionTagLabelOwners() {
|
|
5235
|
+
// Current live rows may outlive the bounded retained history window.
|
|
5236
|
+
const owners = new Map(
|
|
5237
|
+
getSessionHistory().map((row) => [row.session_id, row])
|
|
5238
|
+
);
|
|
5239
|
+
for (const row of sessionManager.list())
|
|
5240
|
+
owners.set(row.id || row.sessionId, row);
|
|
5241
|
+
return [...owners.values()];
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
function currentSessionTagLabels() {
|
|
5245
|
+
return new SessionTagLabelsStore(getConfigDir()).labelsFor(
|
|
5246
|
+
sessionTagLabelOwners()
|
|
5247
|
+
);
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
function saveSuccessfulSessionTags(
|
|
5251
|
+
session,
|
|
5252
|
+
tags,
|
|
5253
|
+
{
|
|
5254
|
+
promote = true,
|
|
5255
|
+
admission = false,
|
|
5256
|
+
changeKinds = ["tags", "category"],
|
|
5257
|
+
restoreReady = false,
|
|
5258
|
+
} = {}
|
|
5259
|
+
) {
|
|
5260
|
+
const restoreHistory = captureSessionTagsHistoryRollback();
|
|
5261
|
+
const previousTags = session.tags;
|
|
5262
|
+
const wasDeferred = session.tagsPersistenceDeferred;
|
|
5263
|
+
const wasAdmitted = sessionCollectionAdmittedSessionIds.has(session.id);
|
|
5264
|
+
const previousRestoreOutcome = restoreReady
|
|
5265
|
+
? sessionManager.getDesiredOpenRestoreOutcome(session.id)
|
|
5266
|
+
: null;
|
|
5267
|
+
const saved = commitSessionTagSave({
|
|
5268
|
+
tags,
|
|
5269
|
+
store: new SessionTagLabelsStore(getConfigDir()),
|
|
5270
|
+
owners: sessionTagLabelOwners,
|
|
5271
|
+
promote,
|
|
5272
|
+
writeTags: (tags) => {
|
|
5273
|
+
updateSessionRecord(session.id, { tags });
|
|
5274
|
+
session.tags = tags;
|
|
5275
|
+
if (restoreReady) sessionManager.markDesiredOpenRestoreReady(session.id);
|
|
5276
|
+
},
|
|
5277
|
+
rollbackTags: () => {
|
|
5278
|
+
restoreHistory();
|
|
5279
|
+
session.tags = previousTags;
|
|
5280
|
+
session.tagsPersistenceDeferred = wasDeferred;
|
|
5281
|
+
if (previousRestoreOutcome) {
|
|
5282
|
+
sessionManager.desiredOpenRestoreOutcomes.set(
|
|
5283
|
+
session.id,
|
|
5284
|
+
previousRestoreOutcome
|
|
5285
|
+
);
|
|
5286
|
+
}
|
|
5287
|
+
},
|
|
5288
|
+
admit: () => {
|
|
5289
|
+
if (admission)
|
|
5290
|
+
admitSessionCollectionLiveSession({
|
|
5291
|
+
session,
|
|
5292
|
+
binding: getRouteCMatrixRoomBinding(session.id),
|
|
5293
|
+
admittedSessionIds: sessionCollectionAdmittedSessionIds,
|
|
5294
|
+
required: true,
|
|
5295
|
+
});
|
|
5296
|
+
},
|
|
5297
|
+
publish: () =>
|
|
5298
|
+
commitSessionCollectionChange(
|
|
5299
|
+
changeKinds,
|
|
5300
|
+
[session.id]
|
|
5301
|
+
),
|
|
5302
|
+
rollbackPublication: () =>
|
|
5303
|
+
commitSessionCollectionChange(
|
|
5304
|
+
["tags", "category"],
|
|
5305
|
+
[session.id]
|
|
5306
|
+
),
|
|
5307
|
+
rollbackAdmission: () => {
|
|
5308
|
+
if (!wasAdmitted) sessionCollectionAdmittedSessionIds.delete(session.id);
|
|
5309
|
+
},
|
|
5310
|
+
});
|
|
5311
|
+
session.tagsPersistenceDeferred = false;
|
|
5312
|
+
if (promote) recordSuccessfulSessionTagsUse(session, saved);
|
|
5313
|
+
return saved;
|
|
5314
|
+
}
|
|
5315
|
+
|
|
5316
|
+
function commitSuccessfulSessionTags(
|
|
5317
|
+
session,
|
|
5318
|
+
{ promote = true, changeKinds = ["create"], restoreReady = false } = {}
|
|
5319
|
+
) {
|
|
5320
|
+
return saveSuccessfulSessionTags(session, canonicalSessionTags(session), {
|
|
5321
|
+
promote,
|
|
5322
|
+
admission: true,
|
|
5323
|
+
restoreReady,
|
|
5324
|
+
changeKinds: [...changeKinds, "tags", "category"],
|
|
5325
|
+
});
|
|
5326
|
+
}
|
|
5327
|
+
|
|
5328
|
+
function refreshSessionBookkeepingWarning(response, session) {
|
|
5329
|
+
response.bookkeeping_warning = (session.bookkeepingWarnings || []).join("; ") || null;
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5332
|
+
function recordSuccessfulSessionTagsUse(session, tags) {
|
|
5333
|
+
try {
|
|
5334
|
+
new RecentSessionTagsStore(getConfigDir()).recordSuccessfulUse(tags);
|
|
5335
|
+
} catch (err) {
|
|
5336
|
+
session.bookkeepingWarnings = [
|
|
5337
|
+
...(session.bookkeepingWarnings || []),
|
|
5338
|
+
`recent_session_tags_unavailable: ${err.message || String(err)}`,
|
|
5339
|
+
];
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
|
|
4858
5343
|
function clearTelegramMockRuntimeForSession(sessionId, reason) {
|
|
4859
5344
|
const normalizedSessionId = normalizeString(sessionId);
|
|
4860
5345
|
if (!normalizedSessionId) {
|
|
@@ -6008,7 +6493,7 @@ function buildProviderModelRefreshRunnerOptions() {
|
|
|
6008
6493
|
backgroundRefreshEnabled: explicitEnable !== false,
|
|
6009
6494
|
backgroundRefreshSource:
|
|
6010
6495
|
explicitEnable === null
|
|
6011
|
-
? "
|
|
6496
|
+
? "default_product_background_daily_0900"
|
|
6012
6497
|
: PROVIDER_MODEL_REFRESH_BACKGROUND_ENABLE_ENV,
|
|
6013
6498
|
backgroundRefreshDisabledReason:
|
|
6014
6499
|
explicitEnable === false
|
|
@@ -6692,6 +7177,8 @@ function loadActiveHostLifecycleToolchainBundle() {
|
|
|
6692
7177
|
}
|
|
6693
7178
|
|
|
6694
7179
|
const OYSTERUN_NPM_PACKAGE_NAME = "oysterun";
|
|
7180
|
+
const P233_RUNNING_PACKAGE_VERSION = readOysterunPackageVersion();
|
|
7181
|
+
const p233AuthenticatedUserOrigins = new Map();
|
|
6695
7182
|
const OYSTERUN_UPDATE_DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
6696
7183
|
const OYSTERUN_UPDATE_RELEASE_CHANNEL = "latest";
|
|
6697
7184
|
const OYSTERUN_UPDATE_AUTOMATIC_COOLDOWN_MS = 30 * 60 * 1000;
|
|
@@ -6877,8 +7364,11 @@ function getCachedNewestVersion(source = "status") {
|
|
|
6877
7364
|
}
|
|
6878
7365
|
|
|
6879
7366
|
function reconcileOysterunUpdateOperation(currentVersion) {
|
|
6880
|
-
let operation =
|
|
7367
|
+
let operation = reconcileFailedInitialPreflight({ configDir: getConfigDir(), currentVersion });
|
|
6881
7368
|
if (!operation.operation_id) return null;
|
|
7369
|
+
if (operation.continuity_path === continuityReceiptPath(getConfigDir(), operation.operation_id)) {
|
|
7370
|
+
try { operation = projectContinuityOperation(operation, readContinuity(operation.continuity_path, operation.operation_id)); } catch { /* Missing authority stays blocked. */ }
|
|
7371
|
+
}
|
|
6882
7372
|
const targetVersion = normalizeString(operation.target_version);
|
|
6883
7373
|
const targetVersionComparison = targetVersion
|
|
6884
7374
|
? compareOysterunSemver(currentVersion, targetVersion)
|
|
@@ -6908,6 +7398,16 @@ function reconcileOysterunUpdateOperation(currentVersion) {
|
|
|
6908
7398
|
}
|
|
6909
7399
|
if (shouldRequireUpdateOperationRecovery(operation)) {
|
|
6910
7400
|
const restartOnly = operation.transaction_kind === "restart_only";
|
|
7401
|
+
if (operation.continuity_path) {
|
|
7402
|
+
try {
|
|
7403
|
+
const r = readContinuity(operation.continuity_path, operation.operation_id);
|
|
7404
|
+
if (!["COMMITTED", "CLEANED", "ROLLED_BACK_HEALTHY"].includes(r.state)) {
|
|
7405
|
+
const failed = recordContinuityRecovery(operation.continuity_path, operation.operation_id,
|
|
7406
|
+
continuityFailure(restartOnly ? "restart_runner" : "update_runner", "runner_lease_expired", "The external lifecycle runner did not publish a terminal receipt before its bounded lease expired."));
|
|
7407
|
+
operation = projectContinuityOperation(operation, failed);
|
|
7408
|
+
}
|
|
7409
|
+
} catch { /* Missing authority is reported by the recovery plan. */ }
|
|
7410
|
+
}
|
|
6911
7411
|
operation = writeUpdateOperationState(
|
|
6912
7412
|
{
|
|
6913
7413
|
...operation,
|
|
@@ -6923,8 +7423,8 @@ function reconcileOysterunUpdateOperation(currentVersion) {
|
|
|
6923
7423
|
? "The Host restart runner did not publish a terminal receipt before its bounded lease expired."
|
|
6924
7424
|
: "The update restart runner did not publish a terminal receipt before its bounded lease expired.",
|
|
6925
7425
|
recovery_boundary: operation.status,
|
|
6926
|
-
error:
|
|
6927
|
-
"The update transaction needs
|
|
7426
|
+
error: restartOnly ? "The Host restart needs recovery before another Host action."
|
|
7427
|
+
: "The update transaction needs recovery before another update or restart.",
|
|
6928
7428
|
error_redacted: true,
|
|
6929
7429
|
},
|
|
6930
7430
|
{ configDir: getConfigDir() }
|
|
@@ -6939,6 +7439,8 @@ function serializeOysterunUpdateOperation(operation) {
|
|
|
6939
7439
|
operation_id: operation.operation_id,
|
|
6940
7440
|
attempt_id: operation.attempt_id,
|
|
6941
7441
|
transaction_kind: operation.transaction_kind,
|
|
7442
|
+
continuity_state: operation.continuity_state || null,
|
|
7443
|
+
user_path_verification: operation.user_path_verification || null,
|
|
6942
7444
|
requested_action: operation.requested_action,
|
|
6943
7445
|
status: operation.status,
|
|
6944
7446
|
phase: operation.phase,
|
|
@@ -7282,6 +7784,8 @@ function digestOriginalLifecycleEvidence(operation, resultReceipt) {
|
|
|
7282
7784
|
return createHash("sha256").update(canonical).digest("hex");
|
|
7283
7785
|
}
|
|
7284
7786
|
|
|
7787
|
+
const continuityAutomaticRechecks = new Set();
|
|
7788
|
+
|
|
7285
7789
|
function maybeCloseSupersededOysterunUpdateRecovery() {
|
|
7286
7790
|
const currentVersion = readOysterunPackageVersion();
|
|
7287
7791
|
let operation = reconcileOysterunUpdateOperation(currentVersion);
|
|
@@ -7294,6 +7798,29 @@ function maybeCloseSupersededOysterunUpdateRecovery() {
|
|
|
7294
7798
|
update_operation: null,
|
|
7295
7799
|
};
|
|
7296
7800
|
}
|
|
7801
|
+
if (operation.continuity_path) {
|
|
7802
|
+
let receipt = null;
|
|
7803
|
+
try {
|
|
7804
|
+
if (operation.continuity_path !== continuityReceiptPath(getConfigDir(), operation.operation_id)) throw new Error("continuity path mismatch");
|
|
7805
|
+
receipt = readContinuity(operation.continuity_path, operation.operation_id);
|
|
7806
|
+
} catch { /* The recovery projection fails closed on missing authority. */ }
|
|
7807
|
+
if (receipt?.state === "RECOVERY_REQUIRED" && !operation.running && !isProcessRunning(operation.runner_pid) &&
|
|
7808
|
+
(operation.recovery_boundary === "p233_user_path_pending" || receipt.first_failure?.code === "p233_user_path_pending" ||
|
|
7809
|
+
(!receipt.restart_attempted && !receipt.restart_id)) && !continuityAutomaticRechecks.has(operation.operation_id)) {
|
|
7810
|
+
continuityAutomaticRechecks.add(operation.operation_id);
|
|
7811
|
+
try {
|
|
7812
|
+
scheduleContinuityRecovery({ kind: "restore_and_restart", source: "automatic_recheck", recheckOnly: true });
|
|
7813
|
+
operation = readUpdateOperationState({ configDir: getConfigDir() });
|
|
7814
|
+
} catch (error) {
|
|
7815
|
+
console.warn("[lifecycle] Automatic restoration recheck could not start:", sanitizeLifecycleDiagnostic(error.message));
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
const recoveryPlan = buildHostLifecycleRecoveryPlan({ operation, continuityReceipt: receipt,
|
|
7819
|
+
runnerAlive: isProcessRunning(operation.runner_pid) });
|
|
7820
|
+
return { status: "continuity_operation_observed", reconciled: false,
|
|
7821
|
+
recovery_boundary: recoveryPlan.reason_code, recovery_plan: recoveryPlan,
|
|
7822
|
+
recovery_action: recoveryPlan.user_action, update_operation: serializeOysterunUpdateOperation(operation) };
|
|
7823
|
+
}
|
|
7297
7824
|
if (operation.status === "superseded_reconciled") {
|
|
7298
7825
|
const configuredReceiptPath = normalizeString(
|
|
7299
7826
|
operation.reconciliation_receipt_path
|
|
@@ -7663,13 +8190,13 @@ function reconcileSupersededOysterunUpdateRecovery() {
|
|
|
7663
8190
|
}
|
|
7664
8191
|
|
|
7665
8192
|
function buildOysterunBetaUpdateStatusPayload(extra = {}) {
|
|
7666
|
-
const currentVersion =
|
|
8193
|
+
const currentVersion = P233_RUNNING_PACKAGE_VERSION;
|
|
7667
8194
|
const betaVersion = normalizeString(extra.beta_version);
|
|
7668
8195
|
const classification = betaVersion
|
|
7669
8196
|
? classifyOysterunBetaVersion({ currentVersion, betaVersion })
|
|
7670
8197
|
: { relation: "unknown", install_available: false };
|
|
7671
8198
|
const recoveryResolution = maybeCloseSupersededOysterunUpdateRecovery();
|
|
7672
|
-
const operation = readUpdateOperationState({ configDir: getConfigDir() });
|
|
8199
|
+
const operation = reconcileOysterunUpdateOperation(currentVersion) || readUpdateOperationState({ configDir: getConfigDir() });
|
|
7673
8200
|
const updateSupported = isOysterunUpdateSupported();
|
|
7674
8201
|
return {
|
|
7675
8202
|
status: extra.status || "beta_update_status",
|
|
@@ -7722,7 +8249,9 @@ function buildOysterunBetaUpdateStatusPayload(extra = {}) {
|
|
|
7722
8249
|
}
|
|
7723
8250
|
|
|
7724
8251
|
function buildOysterunUpdateStatusPayload(extra = {}) {
|
|
7725
|
-
|
|
8252
|
+
// Package exchange precedes restart. Keep the user-visible current version
|
|
8253
|
+
// bound to this running coordinator, including failed pre-handoff updates.
|
|
8254
|
+
const currentVersion = P233_RUNNING_PACKAGE_VERSION;
|
|
7726
8255
|
const channel = normalizeOysterunUpdateChannel(extra.channel, currentVersion);
|
|
7727
8256
|
const source = normalizeOysterunUpdateSource(extra.source, "status");
|
|
7728
8257
|
const reminder = readUpdateReminderState({ configDir: getConfigDir() });
|
|
@@ -7730,7 +8259,7 @@ function buildOysterunUpdateStatusPayload(extra = {}) {
|
|
|
7730
8259
|
normalizeString(extra.newest_version || extra.latest_version) ||
|
|
7731
8260
|
getCachedNewestVersion(source);
|
|
7732
8261
|
const recoveryResolution = maybeCloseSupersededOysterunUpdateRecovery();
|
|
7733
|
-
const operation = readUpdateOperationState({ configDir: getConfigDir() });
|
|
8262
|
+
const operation = reconcileOysterunUpdateOperation(currentVersion) || readUpdateOperationState({ configDir: getConfigDir() });
|
|
7734
8263
|
const updateAvailable = newestVersion
|
|
7735
8264
|
? compareOysterunSemver(newestVersion, currentVersion) === 1
|
|
7736
8265
|
: false;
|
|
@@ -8225,52 +8754,237 @@ function buildHostActiveRuntimeStatusSummary() {
|
|
|
8225
8754
|
};
|
|
8226
8755
|
}
|
|
8227
8756
|
|
|
8228
|
-
function
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
function buildHostRestartOperationLogPath() {
|
|
8244
|
-
const logDir = join(getConfigDir(), "operation_logs");
|
|
8245
|
-
mkdirSync(logDir, { recursive: true });
|
|
8246
|
-
return join(
|
|
8247
|
-
logDir,
|
|
8248
|
-
`host-restart-restore-${new Date().toISOString().replace(/[:.]/g, "-")}.log`
|
|
8249
|
-
);
|
|
8250
|
-
}
|
|
8251
|
-
|
|
8252
|
-
function assertHostRestartSchedulingAvailable() {
|
|
8253
|
-
const stackName = getActiveStackName();
|
|
8254
|
-
if (!stackName) {
|
|
8255
|
-
throw new Error(
|
|
8256
|
-
"Host restart is unavailable because the active stack name is not known."
|
|
8257
|
-
);
|
|
8757
|
+
function recordContinuityAuthenticatedUserPath(req) {
|
|
8758
|
+
const config = readConfig();
|
|
8759
|
+
const incomingHost = normalizeString(req.headers["x-forwarded-host"] || req.headers.host);
|
|
8760
|
+
let configuredRouteRecorded = false;
|
|
8761
|
+
for (const candidate of [config.managed_endpoint, config.direct_host_url, config.public_base_url]) {
|
|
8762
|
+
if (!candidate) continue;
|
|
8763
|
+
const origin = new URL(candidate).origin;
|
|
8764
|
+
if (new URL(origin).host !== incomingHost) continue;
|
|
8765
|
+
p233AuthenticatedUserOrigins.set(origin, { pid: process.pid, observed_at: new Date().toISOString() });
|
|
8766
|
+
configuredRouteRecorded = true;
|
|
8767
|
+
}
|
|
8768
|
+
if (configuredRouteRecorded) {
|
|
8769
|
+
completeContinuityUserPathEvidence();
|
|
8770
|
+
return;
|
|
8258
8771
|
}
|
|
8259
|
-
if (
|
|
8260
|
-
|
|
8772
|
+
if (!incomingHost) return;
|
|
8773
|
+
// A route-less local stack is intentionally reachable only through its
|
|
8774
|
+
// exact loopback port. Do not trust an arbitrary Host header as continuity
|
|
8775
|
+
// authority; accept only loopback aliases bound to this running Host.
|
|
8776
|
+
let incoming;
|
|
8777
|
+
try {
|
|
8778
|
+
incoming = new URL(`http://${incomingHost}`);
|
|
8779
|
+
} catch {
|
|
8780
|
+
return;
|
|
8261
8781
|
}
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8782
|
+
if (
|
|
8783
|
+
!["127.0.0.1", "localhost", "[::1]"].includes(incoming.hostname) ||
|
|
8784
|
+
Number(incoming.port || 80) !== Number(PORT)
|
|
8785
|
+
) {
|
|
8786
|
+
return;
|
|
8265
8787
|
}
|
|
8266
|
-
|
|
8788
|
+
const proof = { pid: process.pid, observed_at: new Date().toISOString() };
|
|
8789
|
+
for (const origin of [
|
|
8790
|
+
`http://127.0.0.1:${PORT}`,
|
|
8791
|
+
`http://localhost:${PORT}`,
|
|
8792
|
+
`http://[::1]:${PORT}`,
|
|
8793
|
+
]) {
|
|
8794
|
+
p233AuthenticatedUserOrigins.set(origin, proof);
|
|
8795
|
+
}
|
|
8796
|
+
completeContinuityUserPathEvidence();
|
|
8267
8797
|
}
|
|
8268
8798
|
|
|
8269
|
-
function
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8799
|
+
function completeContinuityUserPathEvidence() {
|
|
8800
|
+
try {
|
|
8801
|
+
const operation = readUpdateOperationState({ configDir: getConfigDir() });
|
|
8802
|
+
if (!operation.operation_id || operation.continuity_path !== continuityReceiptPath(getConfigDir(), operation.operation_id)) return;
|
|
8803
|
+
const receipt = readContinuity(operation.continuity_path, operation.operation_id);
|
|
8804
|
+
const origin = receipt.user_path_verification?.origin;
|
|
8805
|
+
const proof = p233AuthenticatedUserOrigins.get(origin);
|
|
8806
|
+
if (proof?.pid === process.pid) recordContinuityUserPath(operation.continuity_path, operation.operation_id, { origin, ...proof });
|
|
8807
|
+
} catch (error) {
|
|
8808
|
+
console.warn("[lifecycle] Could not record supplementary login verification:", sanitizeLifecycleDiagnostic(error.message));
|
|
8809
|
+
}
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8812
|
+
function buildHostContinuityObservation(retained = null) {
|
|
8813
|
+
const config = readConfig();
|
|
8814
|
+
const stack = getActiveStackName();
|
|
8815
|
+
const service = getUpdateServiceDefinitionIdentity(stack);
|
|
8816
|
+
const toolchain = loadActiveHostLifecycleToolchainBundle();
|
|
8817
|
+
if (realpathSync(process.execPath) !== toolchain.node_realpath) {
|
|
8818
|
+
throw continuityFailure("host_qualification", "running_node", "Running Host executable differs from the persisted Node runtime.");
|
|
8819
|
+
}
|
|
8820
|
+
const host = {
|
|
8821
|
+
version: P233_RUNNING_PACKAGE_VERSION, pid: process.pid, port: PORT, stack,
|
|
8822
|
+
package_root: REPO_ROOT, process_cwd: realpathSync(process.cwd()),
|
|
8823
|
+
node_version: process.version, runtime_identity_digest: toolchain.toolchain_identity_digest,
|
|
8824
|
+
device_id: normalizeString(config.device_id || config.host_id || DEVICE_ID),
|
|
8825
|
+
config_dir: realpathSync(getConfigDir()), host_folder: realpathSync(getConfigDir()),
|
|
8826
|
+
browser_root: realpathSync(resolveDefaultBrowsePath()),
|
|
8827
|
+
connection_mode: CONNECTION_MODE,
|
|
8828
|
+
managed_endpoint: normalizeString(config.managed_endpoint),
|
|
8829
|
+
direct_host_url: normalizeString(config.direct_host_url || config.public_base_url || DIRECT_HOST_URL),
|
|
8830
|
+
service_definition_sha256: existsSync(service.definitionPath)
|
|
8831
|
+
? continuityDigest(readFileSync(service.definitionPath)) : "absent",
|
|
8832
|
+
service_definition_path: service.definitionPath,
|
|
8833
|
+
};
|
|
8834
|
+
const sessions = sessionManager.snapshotHostRestartState()
|
|
8835
|
+
.filter((row) => row.alive_before_restart || row.ready_before_restart)
|
|
8836
|
+
.map((row) => {
|
|
8837
|
+
try {
|
|
8838
|
+
const binding = getRouteCMatrixRoomBinding(row.session_id);
|
|
8839
|
+
if (!binding) throw continuityFailure("session_qualification", "matrix_binding", `Session ${row.session_id} has no committed Matrix binding.`);
|
|
8840
|
+
const prior = retained?.sessions.find((entry) => entry.session_id === row.session_id);
|
|
8841
|
+
const anchorIds = prior ? prior.history_anchors.map((entry) => entry.event_id)
|
|
8842
|
+
: readMatrixTranscriptRecentTail({ binding, limit: 2 }).messages.map((message) => message.matrix_event_id);
|
|
8843
|
+
const historyAnchors = anchorIds.map((eventId) => {
|
|
8844
|
+
const event = readMatrixTranscriptContext({ binding, eventId, before: 0, after: 0 })?.target_message;
|
|
8845
|
+
if (!event) throw continuityFailure("session_qualification", "history_anchor_missing", `Session ${row.session_id} lost a retained Matrix event.`);
|
|
8846
|
+
return { event_id: eventId, sha256: continuityDigest(event) };
|
|
8847
|
+
});
|
|
8848
|
+
return {
|
|
8849
|
+
session_id: row.session_id, agent_id: row.agent_id, agent_folder: realpathSync(row.agent_folder),
|
|
8850
|
+
provider: row.provider, provider_resume_id: row.provider_resume_id,
|
|
8851
|
+
resume_identity_kind: row.resume_identity_kind || null,
|
|
8852
|
+
provider_thread_id: row.provider_thread_id || row.provider_resume_id,
|
|
8853
|
+
matrix_binding_host_session_id: binding.host_session_id,
|
|
8854
|
+
matrix_room_id: binding.matrix_room_id,
|
|
8855
|
+
adapter_resume_identity_kind: getAdapterResumeIdentityKind(sessionManager.getAdapter(row.provider)),
|
|
8856
|
+
adapter_registered: Boolean(sessionManager.getAdapter(row.provider)),
|
|
8857
|
+
alive: row.alive_before_restart, ready: row.ready_before_restart,
|
|
8858
|
+
history_anchors: historyAnchors,
|
|
8859
|
+
};
|
|
8860
|
+
} catch (error) {
|
|
8861
|
+
return { session_id: row.session_id, observation_error: error.code || "session_observation_failed" };
|
|
8862
|
+
}
|
|
8863
|
+
});
|
|
8864
|
+
const baseline = { host, sessions, captured_at: new Date().toISOString() };
|
|
8865
|
+
const restore = readHostRestartRestoreState({ configDir: getConfigDir() });
|
|
8866
|
+
const routeOrigin = resolveContinuityQualificationOrigin({
|
|
8867
|
+
connectionMode: host.connection_mode,
|
|
8868
|
+
managedEndpoint: host.managed_endpoint,
|
|
8869
|
+
directHostUrl: host.direct_host_url,
|
|
8870
|
+
loopbackOrigin: `http://127.0.0.1:${PORT}`,
|
|
8871
|
+
});
|
|
8872
|
+
return {
|
|
8873
|
+
baseline, host, sessions, desired_session_restore: sessionManager.getDesiredOpenRestoreStatus(),
|
|
8874
|
+
restore_result: restore?.restore_result || null,
|
|
8875
|
+
restart_id: restore?.restart_id || null, restart_state: restore?.state || null,
|
|
8876
|
+
authenticated_user_path: p233AuthenticatedUserOrigins.get(routeOrigin) || null,
|
|
8877
|
+
};
|
|
8878
|
+
}
|
|
8879
|
+
|
|
8880
|
+
function prepareHostContinuity({ operationId, targetVersion, toolchain, transactionKind, restartState = null }) {
|
|
8881
|
+
const path = continuityReceiptPath(getConfigDir(), operationId);
|
|
8882
|
+
const { baseline } = buildHostContinuityObservation();
|
|
8883
|
+
const authority = {};
|
|
8884
|
+
if (restartState) {
|
|
8885
|
+
authority.restart_id = restartState.restart_id;
|
|
8886
|
+
authority.restart_only_snapshot = retainContinuityFile(getHostRestartRestoreStatePath(getConfigDir()), `${path}.restore`);
|
|
8887
|
+
}
|
|
8888
|
+
const operation = readUpdateOperationState({ configDir: getConfigDir() });
|
|
8889
|
+
if (operation.operation_id !== operationId) throw continuityFailure("preparation", "operation_binding", "Lifecycle operation changed before its durable preparation.");
|
|
8890
|
+
writeUpdateOperationState({ ...operation, continuity_path: path }, { configDir: getConfigDir() });
|
|
8891
|
+
prepareContinuity({ path, operationId, transactionKind, authority,
|
|
8892
|
+
source: { version: P233_RUNNING_PACKAGE_VERSION, runtime_identity_digest: toolchain.toolchain_identity_digest,
|
|
8893
|
+
package_tree_sha256: digestContinuityPackage(REPO_ROOT), service_definition_sha256: baseline.host.service_definition_sha256 },
|
|
8894
|
+
target: { version: targetVersion, runtime_identity_digest: toolchain.toolchain_identity_digest }, baseline });
|
|
8895
|
+
compareContinuityIdentities(baseline.sessions, baseline.sessions, { path, operationId, stage: "preparation" });
|
|
8896
|
+
for (const row of baseline.sessions) {
|
|
8897
|
+
if (row.observation_error || !hasCompleteContinuityResumeIdentity(row) || !Array.isArray(row.history_anchors)) {
|
|
8898
|
+
throw continuityFailure("preparation", "session_resume_unavailable", `Session ${row.session_id} has no complete provider resume/history identity.`);
|
|
8899
|
+
}
|
|
8900
|
+
}
|
|
8901
|
+
if (restartState) assertFreshContinuitySnapshot(baseline, restartState.restart_id, restartState, { path, operationId });
|
|
8902
|
+
createContinuityProbeGrant(getConfigDir(), operationId);
|
|
8903
|
+
return path;
|
|
8904
|
+
}
|
|
8905
|
+
|
|
8906
|
+
function assertSessionRosterMutationAvailable() {
|
|
8907
|
+
const operation = readUpdateOperationState({ configDir: getConfigDir() });
|
|
8908
|
+
if (!operation.continuity_path || !isUpdateOperationBlocking(operation)) return;
|
|
8909
|
+
const receipt = readContinuity(operation.continuity_path, operation.operation_id);
|
|
8910
|
+
if (!receipt.restart_id || ["CLEANED", "ROLLED_BACK_HEALTHY"].includes(receipt.state)) return;
|
|
8911
|
+
const error = new Error("Host lifecycle recovery is preserving the fresh live Sessions. Finish that operation before changing Sessions.");
|
|
8912
|
+
error.code = "oysterun_session_roster_reserved";
|
|
8913
|
+
throw error;
|
|
8914
|
+
}
|
|
8915
|
+
|
|
8916
|
+
function recordContinuityRunnerHandoffFailure({ operationId, error, gateId = null, stage = null, check = null }) {
|
|
8917
|
+
const current = readUpdateOperationState({ configDir: getConfigDir() });
|
|
8918
|
+
if (current.operation_id !== operationId || !current.continuity_path) return;
|
|
8919
|
+
const prior = readContinuity(current.continuity_path, operationId);
|
|
8920
|
+
if (["CLEANED", "ROLLED_BACK_HEALTHY"].includes(prior.state)) return;
|
|
8921
|
+
const restartOnly = prior.transaction_kind === "restart_only";
|
|
8922
|
+
const failure = continuityFailure(stage || (restartOnly ? "restart_handoff" : "update_handoff"), check || (restartOnly ? "restart_runner_spawn_failed" : "update_runner_spawn_failed"),
|
|
8923
|
+
sanitizeLifecycleDiagnostic(error?.message || "The external lifecycle runner could not start."));
|
|
8924
|
+
const receipt = prior.state === "COMMITTED" ? prior : recordContinuityRecovery(current.continuity_path, operationId, failure);
|
|
8925
|
+
writeUpdateOperationState({ ...projectContinuityOperation(current, receipt),
|
|
8926
|
+
status: restartOnly ? "restart_recovery_required" : "update_recovery_required",
|
|
8927
|
+
phase: prior.state === "COMMITTED" ? "Committed cleanup requires continuation" : "External runner unavailable; recovery assets retained",
|
|
8928
|
+
running: false, runner_pid: null, runner_lease_expires_at: null, lifecycle_owner_pid: null,
|
|
8929
|
+
recovery_required_at: new Date().toISOString(), recovery_boundary: failure.code,
|
|
8930
|
+
recovery_reason: failure.message, error: failure.message, error_redacted: true,
|
|
8931
|
+
}, { configDir: getConfigDir() });
|
|
8932
|
+
releaseHostRestartLoginGateSafely({ gateId, reason: "continuity_runner_spawn_failed", source: "Continuity runner handoff" });
|
|
8933
|
+
}
|
|
8934
|
+
|
|
8935
|
+
function buildHostRestartRuntimeSnapshot({ trigger }) {
|
|
8936
|
+
return {
|
|
8937
|
+
trigger,
|
|
8938
|
+
captured_at: new Date().toISOString(),
|
|
8939
|
+
sessions: sessionManager.snapshotHostRestartState().map((row) => {
|
|
8940
|
+
const binding = getRouteCMatrixRoomBinding(row.session_id);
|
|
8941
|
+
return { ...row,
|
|
8942
|
+
matrix_binding_host_session_id: binding?.host_session_id || null,
|
|
8943
|
+
matrix_room_id: binding?.matrix_room_id || null,
|
|
8944
|
+
adapter_resume_identity_kind: getAdapterResumeIdentityKind(sessionManager.getAdapter(row.provider)),
|
|
8945
|
+
adapter_registered: Boolean(sessionManager.getAdapter(row.provider)) };
|
|
8946
|
+
}),
|
|
8947
|
+
loops: schedulerService.snapshotSessionLoopRestartState(),
|
|
8948
|
+
scheduler: schedulerService.snapshotHostRestartState(),
|
|
8949
|
+
terminal: terminalSessionManager.snapshotHostRestartState(),
|
|
8950
|
+
shell_execs: buildActiveShellExecRestartSnapshot(),
|
|
8951
|
+
telegram: telegramBridgeManager.snapshotHostRestartState(),
|
|
8952
|
+
prompt_replay_policy: HOST_RESTART_RESTORE_NO_PROMPT_REPLAY_POLICY,
|
|
8953
|
+
matrix_transcript_mutation: false,
|
|
8954
|
+
};
|
|
8955
|
+
}
|
|
8956
|
+
|
|
8957
|
+
function buildHostRestartOperationLogPath() {
|
|
8958
|
+
const logDir = join(getConfigDir(), "operation_logs");
|
|
8959
|
+
mkdirSync(logDir, { recursive: true });
|
|
8960
|
+
return join(
|
|
8961
|
+
logDir,
|
|
8962
|
+
`host-restart-restore-${new Date().toISOString().replace(/[:.]/g, "-")}.log`
|
|
8963
|
+
);
|
|
8964
|
+
}
|
|
8965
|
+
|
|
8966
|
+
function assertHostRestartSchedulingAvailable() {
|
|
8967
|
+
const stackName = getActiveStackName();
|
|
8968
|
+
if (!stackName) {
|
|
8969
|
+
throw new Error(
|
|
8970
|
+
"Host restart is unavailable because the active stack name is not known."
|
|
8971
|
+
);
|
|
8972
|
+
}
|
|
8973
|
+
if (isBoundaryTestRuntime()) {
|
|
8974
|
+
throw new Error("Host restart is unavailable from boundary test runtime.");
|
|
8975
|
+
}
|
|
8976
|
+
const restartScript = join(REPO_ROOT, "tool_scripts", "restart_oysterun.sh");
|
|
8977
|
+
if (!existsSync(restartScript)) {
|
|
8978
|
+
throw new Error(`Host restart script is missing: ${restartScript}`);
|
|
8979
|
+
}
|
|
8980
|
+
return { stackName, restartScript };
|
|
8981
|
+
}
|
|
8982
|
+
|
|
8983
|
+
function runCompleteHostLifecyclePreflight({
|
|
8984
|
+
toolchain,
|
|
8985
|
+
allowOperationId = null,
|
|
8986
|
+
expectedCurrentHostCwd = null,
|
|
8987
|
+
expectedSourceServiceDefinitionSha256 = null,
|
|
8274
8988
|
expectedTargetServiceDefinitionSha256 = null,
|
|
8275
8989
|
} = {}) {
|
|
8276
8990
|
const { stackName } = assertHostRestartSchedulingAvailable();
|
|
@@ -8352,114 +9066,12 @@ function runCompleteHostLifecyclePreflight({
|
|
|
8352
9066
|
}
|
|
8353
9067
|
|
|
8354
9068
|
function getUpdateServiceDefinitionIdentity(stackName) {
|
|
8355
|
-
|
|
8356
|
-
const configDir = resolve(getConfigDir());
|
|
8357
|
-
const stackRoot = production ? configDir : dirname(configDir);
|
|
8358
|
-
const logDir = production ? join(configDir, "logs") : join(stackRoot, "logs");
|
|
8359
|
-
const serviceLabel = production ? "com.oysterun.host" : `com.oysterun.host.${stackName}`;
|
|
8360
|
-
const stackKind = ["production", "staging", "test1", "test2", "test3", "test4"].includes(stackName)
|
|
8361
|
-
? "preset"
|
|
8362
|
-
: "custom";
|
|
8363
|
-
return {
|
|
8364
|
-
production,
|
|
8365
|
-
configDir,
|
|
8366
|
-
stackRoot,
|
|
8367
|
-
logDir,
|
|
8368
|
-
hostLog: join(logDir, stackKind === "custom" ? `oysterun-host-${stackName}.log` : "oysterun-host.log"),
|
|
8369
|
-
serviceLabel,
|
|
8370
|
-
stackKind,
|
|
8371
|
-
definitionPath: join(homedir(), "Library", "LaunchAgents", `${serviceLabel}.plist`),
|
|
8372
|
-
};
|
|
8373
|
-
}
|
|
8374
|
-
|
|
8375
|
-
function resolveFrozenProviderLaunchPath({ toolchain, packageRoot = REPO_ROOT }) {
|
|
8376
|
-
const result = spawnSync(
|
|
8377
|
-
toolchain.node_path,
|
|
8378
|
-
[
|
|
8379
|
-
join(packageRoot, "host-service", "provider-command-detection.mjs"),
|
|
8380
|
-
"--print-launch-path",
|
|
8381
|
-
"--config-path",
|
|
8382
|
-
getConfigPath(),
|
|
8383
|
-
"--node-bin",
|
|
8384
|
-
toolchain.node_path,
|
|
8385
|
-
],
|
|
8386
|
-
{
|
|
8387
|
-
cwd: packageRoot,
|
|
8388
|
-
env: buildNodeNpmChildEnvironment(toolchain, process.env),
|
|
8389
|
-
encoding: "utf8",
|
|
8390
|
-
stdio: "pipe",
|
|
8391
|
-
}
|
|
8392
|
-
);
|
|
8393
|
-
if (result.error) throw result.error;
|
|
8394
|
-
if (result.status !== 0) {
|
|
8395
|
-
throw new Error(`target provider resolution failed: ${sanitizeLifecycleDiagnostic(result.stderr || result.stdout)}`);
|
|
8396
|
-
}
|
|
8397
|
-
const pathEntries = String(result.stdout || "").trim().split(":").filter(Boolean);
|
|
8398
|
-
if (pathEntries.length === 0) throw new Error("target provider resolution returned an empty PATH");
|
|
8399
|
-
return pathEntries;
|
|
9069
|
+
return getServiceDefinitionIdentity({ stackName, configDir: getConfigDir() });
|
|
8400
9070
|
}
|
|
8401
9071
|
|
|
8402
|
-
function buildUpdateServiceRenderBundle({
|
|
8403
|
-
toolchain,
|
|
8404
|
-
|
|
8405
|
-
registry,
|
|
8406
|
-
packageRoot = REPO_ROOT,
|
|
8407
|
-
packageVersion = readOysterunPackageVersion(),
|
|
8408
|
-
}) {
|
|
8409
|
-
const identity = getUpdateServiceDefinitionIdentity(stackName);
|
|
8410
|
-
const manifestRaw = readFileSync(join(packageRoot, "package.json"), "utf8");
|
|
8411
|
-
const manifest = JSON.parse(manifestRaw);
|
|
8412
|
-
const versionOwned = manifest.oysterun_service_definition || {};
|
|
8413
|
-
return {
|
|
8414
|
-
schema_version: 1,
|
|
8415
|
-
artifact: {
|
|
8416
|
-
package_root: resolve(packageRoot),
|
|
8417
|
-
package_realpath: realpathSync(packageRoot),
|
|
8418
|
-
package_version: packageVersion,
|
|
8419
|
-
integrity_sha256: createHash("sha256").update(manifestRaw).digest("hex"),
|
|
8420
|
-
},
|
|
8421
|
-
host: {
|
|
8422
|
-
launch_domain: `gui/${process.getuid?.() ?? 0}`,
|
|
8423
|
-
user: process.env.USER || "unknown",
|
|
8424
|
-
stack_name: stackName,
|
|
8425
|
-
port: PORT,
|
|
8426
|
-
config_dir: identity.configDir,
|
|
8427
|
-
stack_root: identity.stackRoot,
|
|
8428
|
-
log_dir: identity.logDir,
|
|
8429
|
-
host_log: identity.hostLog,
|
|
8430
|
-
service_label: identity.serviceLabel,
|
|
8431
|
-
stack_kind: identity.stackKind,
|
|
8432
|
-
production: identity.production,
|
|
8433
|
-
},
|
|
8434
|
-
runtime: {
|
|
8435
|
-
node_path: toolchain.node_path,
|
|
8436
|
-
node_realpath: toolchain.node_realpath,
|
|
8437
|
-
node_version: toolchain.node_version,
|
|
8438
|
-
node_abi: toolchain.node_abi,
|
|
8439
|
-
npm_cli_path: toolchain.npm_cli_path,
|
|
8440
|
-
npm_cli_realpath: toolchain.npm_cli_realpath,
|
|
8441
|
-
npm_version: toolchain.npm_version,
|
|
8442
|
-
platform: toolchain.platform,
|
|
8443
|
-
architecture: toolchain.architecture,
|
|
8444
|
-
toolchain_identity_digest: toolchain.toolchain_identity_digest,
|
|
8445
|
-
},
|
|
8446
|
-
update: {
|
|
8447
|
-
registry_authority: registry.source,
|
|
8448
|
-
registry_url: registry.override_active ? registry.url : null,
|
|
8449
|
-
state_path: normalizeString(process.env.OYSTERUN_UPDATE_REGISTRY_STATE_PATH),
|
|
8450
|
-
},
|
|
8451
|
-
provider_resolution: {
|
|
8452
|
-
path_entries: resolveFrozenProviderLaunchPath({ toolchain, packageRoot }),
|
|
8453
|
-
environment: {},
|
|
8454
|
-
},
|
|
8455
|
-
version_owned: {
|
|
8456
|
-
environment: versionOwned.environment || {},
|
|
8457
|
-
top_level: versionOwned.top_level || {},
|
|
8458
|
-
run_at_load: versionOwned.run_at_load ?? true,
|
|
8459
|
-
keep_alive: versionOwned.keep_alive ?? true,
|
|
8460
|
-
throttle_interval: versionOwned.throttle_interval ?? 10,
|
|
8461
|
-
},
|
|
8462
|
-
};
|
|
9072
|
+
function buildUpdateServiceRenderBundle({ toolchain, stackName, registry, packageRoot = REPO_ROOT, packageVersion = readOysterunPackageVersion() }) {
|
|
9073
|
+
return buildServiceRenderBundle({ toolchain, stackName, registry, packageRoot, packageVersion,
|
|
9074
|
+
configDir: getConfigDir(), configPath: getConfigPath(), port: PORT });
|
|
8463
9075
|
}
|
|
8464
9076
|
|
|
8465
9077
|
function writeTargetServiceDefinitionAuthority({ receipt, targetBundle, targetRendered }) {
|
|
@@ -8652,6 +9264,7 @@ function buildHostRuntimeStatusPayload() {
|
|
|
8652
9264
|
sessions: sessionManager.list().length,
|
|
8653
9265
|
restart_restore: buildHostRestartRestoreSummary(transaction),
|
|
8654
9266
|
last_restart_restore: lastHostRestartRestoreStatus,
|
|
9267
|
+
desired_open_restore: buildDesiredOpenRestoreStatusPayload(),
|
|
8655
9268
|
active_runtime: buildHostActiveRuntimeStatusSummary(),
|
|
8656
9269
|
provider_model_refresh: providerModelRefreshRunner.getStatus(),
|
|
8657
9270
|
host_runtime_metadata: buildHostRuntimeMetadataStatus(),
|
|
@@ -8661,6 +9274,27 @@ function buildHostRuntimeStatusPayload() {
|
|
|
8661
9274
|
};
|
|
8662
9275
|
}
|
|
8663
9276
|
|
|
9277
|
+
function buildDesiredOpenRestoreStatusPayload() {
|
|
9278
|
+
const persisted = sessionManager.getDesiredOpenRestoreStatus();
|
|
9279
|
+
return {
|
|
9280
|
+
...(lastDesiredOpenRestoreStatus || {
|
|
9281
|
+
status: persisted.status === "available" ? "idle" : persisted.status,
|
|
9282
|
+
trigger: null,
|
|
9283
|
+
reconciliation_id: null,
|
|
9284
|
+
running: false,
|
|
9285
|
+
started_at: null,
|
|
9286
|
+
completed_at: null,
|
|
9287
|
+
}),
|
|
9288
|
+
preference_enabled:
|
|
9289
|
+
readConfig().restore_previous_sessions_after_restart !== false,
|
|
9290
|
+
max_concurrency: DESIRED_OPEN_RESTORE_MAX_CONCURRENCY,
|
|
9291
|
+
session_timeout_ms: DESIRED_OPEN_RESTORE_SESSION_TIMEOUT_MS,
|
|
9292
|
+
sessions: persisted.sessions,
|
|
9293
|
+
prompt_replay: false,
|
|
9294
|
+
terminal_command_replay: false,
|
|
9295
|
+
};
|
|
9296
|
+
}
|
|
9297
|
+
|
|
8664
9298
|
function serializeProviderModelRefreshResult(result = {}) {
|
|
8665
9299
|
return {
|
|
8666
9300
|
status: normalizeString(result.status) || "unknown",
|
|
@@ -8726,6 +9360,8 @@ function releaseHostRestartLoginGateSafely({ gateId, reason, source }) {
|
|
|
8726
9360
|
async function scheduleOysterunPackageUpdate({
|
|
8727
9361
|
channel = OYSTERUN_UPDATE_RELEASE_CHANNEL,
|
|
8728
9362
|
source = "host_preferences",
|
|
9363
|
+
expectedCurrentVersion = null,
|
|
9364
|
+
expectedTargetVersion = null,
|
|
8729
9365
|
recoveryKind = null,
|
|
8730
9366
|
recoveryOperation = null,
|
|
8731
9367
|
} = {}) {
|
|
@@ -8926,7 +9562,12 @@ async function scheduleOysterunPackageUpdate({
|
|
|
8926
9562
|
},
|
|
8927
9563
|
{ configDir: getConfigDir() }
|
|
8928
9564
|
);
|
|
9565
|
+
} else {
|
|
9566
|
+
writeUpdateOperationState(failedInitialPreflightState(operationState), {
|
|
9567
|
+
configDir: getConfigDir(),
|
|
9568
|
+
});
|
|
8929
9569
|
}
|
|
9570
|
+
|
|
8930
9571
|
throw error;
|
|
8931
9572
|
}
|
|
8932
9573
|
if (!repairRequested) {
|
|
@@ -9012,24 +9653,27 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9012
9653
|
}
|
|
9013
9654
|
throw new Error(`Could not resolve exact Oysterun package version for ${resolvedChannel}`);
|
|
9014
9655
|
}
|
|
9015
|
-
if (
|
|
9016
|
-
|
|
9017
|
-
{
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9656
|
+
if (!repairRequested) {
|
|
9657
|
+
try {
|
|
9658
|
+
assertConfirmedUpdateTarget({
|
|
9659
|
+
currentVersion: readOysterunPackageVersion(), targetVersion: exactVersion,
|
|
9660
|
+
expectedCurrentVersion, expectedTargetVersion, source,
|
|
9661
|
+
});
|
|
9662
|
+
} catch (error) {
|
|
9663
|
+
operationState = recordUpdateRootFailure(operationState, {
|
|
9664
|
+
...error.lifecycle_diagnostic,
|
|
9665
|
+
safe_message: error.message, observed_at: new Date().toISOString(),
|
|
9666
|
+
});
|
|
9667
|
+
writeUpdateOperationState({
|
|
9668
|
+
...operationState, status: "update_check_failed", phase: "Update confirmation rejected",
|
|
9669
|
+
running: false, lifecycle_owner_pid: null, runner_lease_expires_at: null,
|
|
9670
|
+
terminal_result_status: "update_check_failed", next_safe_action: null,
|
|
9671
|
+
restart_attempted: false, error: error.message, error_redacted: true,
|
|
9672
|
+
}, { configDir: getConfigDir() });
|
|
9673
|
+
throw error;
|
|
9674
|
+
}
|
|
9032
9675
|
}
|
|
9676
|
+
|
|
9033
9677
|
if (repairRequested) {
|
|
9034
9678
|
let metadata;
|
|
9035
9679
|
try {
|
|
@@ -9172,6 +9816,28 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9172
9816
|
login_gate_activated: false,
|
|
9173
9817
|
restore_snapshot_created: false,
|
|
9174
9818
|
});
|
|
9819
|
+
let continuityPath;
|
|
9820
|
+
try {
|
|
9821
|
+
continuityPath = prepareHostContinuity({
|
|
9822
|
+
operationId: operationState.operation_id, targetVersion: exactVersion,
|
|
9823
|
+
toolchain, transactionKind,
|
|
9824
|
+
});
|
|
9825
|
+
} catch (error) {
|
|
9826
|
+
const retainedPath = continuityReceiptPath(getConfigDir(), operationState.operation_id);
|
|
9827
|
+
const retained = existsSync(retainedPath)
|
|
9828
|
+
? recordContinuityRecovery(retainedPath, operationState.operation_id, error) : null;
|
|
9829
|
+
operationState = recordUpdateRootFailure(operationState, {
|
|
9830
|
+
stage: "session_preparation", check: error.lifecycle_diagnostic?.check || "fresh_roster",
|
|
9831
|
+
code: error.code || "fresh_roster_failed", safe_message: sanitizeLifecycleDiagnostic(error.message),
|
|
9832
|
+
observed_at: new Date().toISOString(),
|
|
9833
|
+
});
|
|
9834
|
+
writeUpdateOperationState({ ...operationState, continuity_path: retained ? retainedPath : null,
|
|
9835
|
+
status: retained ? "update_recovery_required" : "update_check_failed", phase: "Session preparation did not pass",
|
|
9836
|
+
running: false, lifecycle_owner_pid: null, restart_attempted: false, terminal_result_status: "update_check_failed",
|
|
9837
|
+
error: sanitizeLifecycleDiagnostic(error.message), error_redacted: true }, { configDir: getConfigDir() });
|
|
9838
|
+
throw error;
|
|
9839
|
+
}
|
|
9840
|
+
operationState = writeUpdateOperationState({ ...operationState, continuity_path: continuityPath }, { configDir: getConfigDir() });
|
|
9175
9841
|
const updateInstallerArgs = [
|
|
9176
9842
|
join(REPO_ROOT, "tool_scripts", "install_oysterun_update.mjs"),
|
|
9177
9843
|
"--package-spec",
|
|
@@ -9202,6 +9868,8 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9202
9868
|
join(getConfigDir(), "service-definition-authority.json"),
|
|
9203
9869
|
]
|
|
9204
9870
|
: []),
|
|
9871
|
+
"--continuity-path",
|
|
9872
|
+
continuityPath,
|
|
9205
9873
|
"--retain-for-restart",
|
|
9206
9874
|
"1",
|
|
9207
9875
|
...(repairRequested ? ["--force-reinstall", "1"] : []),
|
|
@@ -9223,9 +9891,10 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9223
9891
|
},
|
|
9224
9892
|
{ configDir: getConfigDir() }
|
|
9225
9893
|
);
|
|
9226
|
-
|
|
9894
|
+
let logFd;
|
|
9227
9895
|
let preparationChild;
|
|
9228
9896
|
try {
|
|
9897
|
+
logFd = openSync(logPath, "a");
|
|
9229
9898
|
preparationChild = spawn(toolchain.node_path, updateInstallerArgs, {
|
|
9230
9899
|
cwd: REPO_ROOT,
|
|
9231
9900
|
env: buildNodeNpmChildEnvironment(toolchain, {
|
|
@@ -9243,8 +9912,11 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9243
9912
|
},
|
|
9244
9913
|
{ configDir: getConfigDir() }
|
|
9245
9914
|
);
|
|
9915
|
+
} catch (error) {
|
|
9916
|
+
recordContinuityRunnerHandoffFailure({ operationId: operationState.operation_id, error, stage: "package_preparation", check: "installer_spawn_failed" });
|
|
9917
|
+
throw error;
|
|
9246
9918
|
} finally {
|
|
9247
|
-
closeSync(logFd);
|
|
9919
|
+
if (logFd !== undefined) closeSync(logFd);
|
|
9248
9920
|
}
|
|
9249
9921
|
try {
|
|
9250
9922
|
await new Promise((resolvePreparation, rejectPreparation) => {
|
|
@@ -9270,6 +9942,10 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9270
9942
|
// before it can publish its atomic structured result.
|
|
9271
9943
|
}
|
|
9272
9944
|
const failureCode = normalizeString(result?.failure_code) || "staging_install_failed";
|
|
9945
|
+
recordContinuityRecovery(continuityPath, operationState.operation_id,
|
|
9946
|
+
Object.assign(new Error(sanitizeLifecycleDiagnostic(result?.error || error.message)), {
|
|
9947
|
+
code: failureCode, lifecycle_diagnostic: { stage: result?.package_mutated ? "activation" : "package_staging", check: failureCode },
|
|
9948
|
+
}));
|
|
9273
9949
|
operationState = recordUpdateRootFailure(operationState, {
|
|
9274
9950
|
stage: normalizeString(result?.package_load_status) === "staged_and_activated_verified"
|
|
9275
9951
|
? "activation"
|
|
@@ -9283,19 +9959,21 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9283
9959
|
operationState.diagnostic_summary,
|
|
9284
9960
|
"rollback",
|
|
9285
9961
|
{
|
|
9286
|
-
result: result?.rollback_completed === true ? "passed" : "not_required",
|
|
9962
|
+
result: result?.rollback_completed === true ? "passed" : result?.package_mutated ? "incomplete" : "not_required",
|
|
9287
9963
|
code: result?.rollback_completed === true
|
|
9288
9964
|
? "prior_package_restored"
|
|
9965
|
+
: result?.package_mutated ? "retained_pair_recovery_pending"
|
|
9289
9966
|
: "package_activation_not_reached",
|
|
9290
9967
|
safe_message: result?.rollback_completed === true
|
|
9291
9968
|
? "The prior package was restored."
|
|
9969
|
+
: result?.package_mutated ? "The package changed; the retained prior package and definition still require qualified recovery."
|
|
9292
9970
|
: "Rollback was not required because the active package was not changed.",
|
|
9293
9971
|
}
|
|
9294
9972
|
);
|
|
9295
9973
|
failureSummary = recordLifecycleTerminalOutcome(failureSummary, "recovery", {
|
|
9296
9974
|
result: "available",
|
|
9297
|
-
code: "
|
|
9298
|
-
safe_message: "
|
|
9975
|
+
code: "retained_continuity_recovery_available",
|
|
9976
|
+
safe_message: "Continue this exact operation through its displayed recovery action; retained assets have not been cleaned.",
|
|
9299
9977
|
});
|
|
9300
9978
|
failureSummary = recordLifecycleTerminalOutcome(failureSummary, "cleanup", {
|
|
9301
9979
|
result: result?.operation_cleanup_completed === true ? "passed" : "incomplete",
|
|
@@ -9350,6 +10028,13 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9350
10028
|
},
|
|
9351
10029
|
{ configDir: getConfigDir() }
|
|
9352
10030
|
);
|
|
10031
|
+
continuityAutomaticRechecks.add(operationState.operation_id);
|
|
10032
|
+
try {
|
|
10033
|
+
scheduleContinuityRecovery({ kind: "restore_and_restart", source: "automatic_recovery", automatic: true,
|
|
10034
|
+
recheckOnly: result?.package_mutated !== true });
|
|
10035
|
+
} catch (recoveryError) {
|
|
10036
|
+
console.warn("[lifecycle] Automatic update recovery could not start:", sanitizeLifecycleDiagnostic(recoveryError.message));
|
|
10037
|
+
}
|
|
9353
10038
|
throw error;
|
|
9354
10039
|
} finally {
|
|
9355
10040
|
if (sourceServiceRenderBundle) {
|
|
@@ -9360,6 +10045,8 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9360
10045
|
try {
|
|
9361
10046
|
retainedPreparationResult = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
9362
10047
|
} catch (error) {
|
|
10048
|
+
recordContinuityRecovery(continuityPath, operationState.operation_id,
|
|
10049
|
+
continuityFailure("activation", "preparation_receipt_readback", "Update preparation completed without a readable result receipt."));
|
|
9363
10050
|
operationState = recordUpdateRootFailure(operationState, {
|
|
9364
10051
|
stage: "activation",
|
|
9365
10052
|
check: "preparation_receipt_readback",
|
|
@@ -9597,6 +10284,11 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9597
10284
|
service_definition_commit_started_at: new Date().toISOString(),
|
|
9598
10285
|
};
|
|
9599
10286
|
writeRecoveryReconciliationReceipt(resultPath, retainedPreparationResult);
|
|
10287
|
+
{
|
|
10288
|
+
const continuity = readContinuity(continuityPath, operationState.operation_id);
|
|
10289
|
+
writeContinuityAtomic(continuityPath, { ...continuity,
|
|
10290
|
+
target: { ...continuity.target, service_definition_sha256: effectiveTargetDefinitionDigest } });
|
|
10291
|
+
}
|
|
9600
10292
|
if (snapshot.existed) {
|
|
9601
10293
|
migrationStage = "replacement_commit";
|
|
9602
10294
|
migrationCheck = "atomic_target_render_commit";
|
|
@@ -9727,6 +10419,16 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9727
10419
|
? "host_preferences_repair_restart"
|
|
9728
10420
|
: "host_preferences_update_restart",
|
|
9729
10421
|
});
|
|
10422
|
+
{
|
|
10423
|
+
const freshBaseline = buildHostContinuityObservation().baseline;
|
|
10424
|
+
const continuity = bindFreshContinuityBaseline(continuityPath, operationState.operation_id,
|
|
10425
|
+
freshBaseline, restartRestore.restart_restore?.restart_id, readHostRestartRestoreState({ configDir: getConfigDir() }));
|
|
10426
|
+
writeContinuityAtomic(continuityPath, { ...continuity,
|
|
10427
|
+
target: { ...continuity.target, service_definition_sha256: retainedPreparationResult.target_service_definition_sha256 || "absent" },
|
|
10428
|
+
retained_files: { ...continuity.retained_files, fresh_restart_state: retainContinuityFile(
|
|
10429
|
+
getHostRestartRestoreStatePath(getConfigDir()), join(continuity.operation_root, "restart-restore-state.pre-restart.json")) },
|
|
10430
|
+
});
|
|
10431
|
+
}
|
|
9730
10432
|
operationState = recordUpdateLifecycleDiagnostic(operationState, {
|
|
9731
10433
|
component: "host_coordinator",
|
|
9732
10434
|
stage: "restart_snapshot",
|
|
@@ -9793,6 +10495,12 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9793
10495
|
};
|
|
9794
10496
|
writeRecoveryReconciliationReceipt(resultPath, retainedPreparationResult);
|
|
9795
10497
|
} catch (handoffError) {
|
|
10498
|
+
// Bind the causal failure before asking another process to recover it;
|
|
10499
|
+
// its recovery_requested control flag is not the incident's first cause.
|
|
10500
|
+
recordContinuityRecovery(continuityPath, operationState.operation_id,
|
|
10501
|
+
Object.assign(handoffError, { lifecycle_diagnostic: handoffError.lifecycle_diagnostic || {
|
|
10502
|
+
stage: migrationStage, check: migrationCheck, result: "failed",
|
|
10503
|
+
} }));
|
|
9796
10504
|
for (const childEvent of handoffError?.lifecycle_diagnostics || []) {
|
|
9797
10505
|
if (childEvent.result !== "passed") continue;
|
|
9798
10506
|
operationState = recordUpdateLifecycleDiagnostic(operationState, {
|
|
@@ -9831,6 +10539,7 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9831
10539
|
}),
|
|
9832
10540
|
{
|
|
9833
10541
|
cwd: REPO_ROOT,
|
|
10542
|
+
detached: true,
|
|
9834
10543
|
env: buildNodeNpmChildEnvironment(toolchain, {
|
|
9835
10544
|
...process.env,
|
|
9836
10545
|
OYSTERUN_DEBUG_HOST_ARTIFACT_WRITES_ENABLED:
|
|
@@ -9859,9 +10568,10 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9859
10568
|
) {
|
|
9860
10569
|
return;
|
|
9861
10570
|
}
|
|
9862
|
-
|
|
10571
|
+
let restartLogFd;
|
|
9863
10572
|
let child;
|
|
9864
10573
|
try {
|
|
10574
|
+
restartLogFd = openSync(logPath, "a");
|
|
9865
10575
|
const restartRunnerArgs = buildRestartRunnerArgs({
|
|
9866
10576
|
loginGateId: restartLoginGate.gate_id,
|
|
9867
10577
|
restartId: operationState.restart_id,
|
|
@@ -9883,10 +10593,17 @@ async function scheduleOysterunPackageUpdate({
|
|
|
9883
10593
|
stdio: ["ignore", restartLogFd, restartLogFd],
|
|
9884
10594
|
}
|
|
9885
10595
|
);
|
|
10596
|
+
} catch (error) {
|
|
10597
|
+
recordContinuityRunnerHandoffFailure({ operationId: operationState.operation_id, error, gateId: restartLoginGate.gate_id });
|
|
10598
|
+
return;
|
|
9886
10599
|
} finally {
|
|
9887
|
-
closeSync(restartLogFd);
|
|
10600
|
+
if (restartLogFd !== undefined) closeSync(restartLogFd);
|
|
9888
10601
|
}
|
|
9889
|
-
child.once("error", () => {
|
|
10602
|
+
child.once("error", (error) => {
|
|
10603
|
+
if (operationState.continuity_path) {
|
|
10604
|
+
recordContinuityRunnerHandoffFailure({ operationId: operationState.operation_id, error, gateId: restartLoginGate.gate_id });
|
|
10605
|
+
return;
|
|
10606
|
+
}
|
|
9890
10607
|
writeUpdateOperationState(
|
|
9891
10608
|
{
|
|
9892
10609
|
...operationState,
|
|
@@ -9961,6 +10678,9 @@ async function schedulePreparedLifecycleRecovery({
|
|
|
9961
10678
|
if (!["complete_update_and_restart", "restore_and_restart"].includes(kind)) {
|
|
9962
10679
|
throw new Error("Unsupported prepared Host recovery action.");
|
|
9963
10680
|
}
|
|
10681
|
+
if (readUpdateOperationState({ configDir: getConfigDir() }).continuity_path) {
|
|
10682
|
+
return scheduleContinuityRecovery({ kind, source });
|
|
10683
|
+
}
|
|
9964
10684
|
const resolution = maybeCloseSupersededOysterunUpdateRecovery();
|
|
9965
10685
|
let operationState = readUpdateOperationState({ configDir: getConfigDir() });
|
|
9966
10686
|
if (
|
|
@@ -10221,20 +10941,61 @@ async function schedulePreparedLifecycleRecovery({
|
|
|
10221
10941
|
});
|
|
10222
10942
|
}
|
|
10223
10943
|
|
|
10224
|
-
function scheduleHostRestart({
|
|
10944
|
+
function scheduleHostRestart(options = {}) {
|
|
10945
|
+
const priorOperation = readUpdateOperationState({ configDir: getConfigDir() });
|
|
10946
|
+
if (priorOperation.continuity_path && priorOperation.transaction_kind === "restart_only" &&
|
|
10947
|
+
priorOperation.status === "restart_recovery_required") {
|
|
10948
|
+
return scheduleContinuityRecovery({ kind: "restart_only", source: options.source || "host_preferences" });
|
|
10949
|
+
}
|
|
10950
|
+
assertNoOysterunUpdateOperationBlocking();
|
|
10951
|
+
let operation = writeUpdateOperationState({
|
|
10952
|
+
operation_id: randomUUID(), attempt_id: randomUUID(), status: "restart_preflight",
|
|
10953
|
+
phase: "Checking restart prerequisites", transaction_kind: "restart_only",
|
|
10954
|
+
source: options.source || "host_preferences", running: true,
|
|
10955
|
+
lifecycle_owner_pid: process.pid, restart_attempted: false,
|
|
10956
|
+
current_version: P233_RUNNING_PACKAGE_VERSION, target_version: P233_RUNNING_PACKAGE_VERSION,
|
|
10957
|
+
}, { configDir: getConfigDir() });
|
|
10958
|
+
try {
|
|
10959
|
+
return scheduleHostRestartPrepared({ ...options, operationId: operation.operation_id });
|
|
10960
|
+
} catch (error) {
|
|
10961
|
+
const retainedPath = continuityReceiptPath(getConfigDir(), operation.operation_id);
|
|
10962
|
+
const retained = existsSync(retainedPath)
|
|
10963
|
+
? recordContinuityRecovery(retainedPath, operation.operation_id, error) : null;
|
|
10964
|
+
operation = { ...operation, ...readUpdateOperationState({ configDir: getConfigDir() }),
|
|
10965
|
+
continuity_path: retained ? retainedPath : null };
|
|
10966
|
+
operation = recordUpdateRootFailure(operation, {
|
|
10967
|
+
stage: "restart_preflight", check: error.lifecycle_diagnostic?.check || "restart_prerequisites",
|
|
10968
|
+
code: error.code || "restart_preflight_failed", safe_message: sanitizeLifecycleDiagnostic(error.message),
|
|
10969
|
+
observed_at: new Date().toISOString(),
|
|
10970
|
+
});
|
|
10971
|
+
writeUpdateOperationState({ ...operation, status: retained ? "restart_recovery_required" : "restart_rejected", phase: "Restart did not start",
|
|
10972
|
+
running: false, lifecycle_owner_pid: null, restart_attempted: false,
|
|
10973
|
+
terminal_result_status: "restart_rejected", error: sanitizeLifecycleDiagnostic(error.message), error_redacted: true,
|
|
10974
|
+
}, { configDir: getConfigDir() });
|
|
10975
|
+
throw error;
|
|
10976
|
+
}
|
|
10977
|
+
}
|
|
10978
|
+
|
|
10979
|
+
function scheduleHostRestartPrepared({
|
|
10225
10980
|
redirectUrl = null,
|
|
10226
10981
|
source = "host_preferences",
|
|
10227
10982
|
trigger = "host_preferences_restart_host",
|
|
10983
|
+
operationId,
|
|
10228
10984
|
} = {}) {
|
|
10229
|
-
assertNoOysterunUpdateOperationBlocking();
|
|
10230
10985
|
const { stackName } = assertHostRestartSchedulingAvailable();
|
|
10231
10986
|
const toolchain = loadActiveHostLifecycleToolchainBundle();
|
|
10232
|
-
runCompleteHostLifecyclePreflight({ toolchain });
|
|
10987
|
+
runCompleteHostLifecyclePreflight({ toolchain, allowOperationId: operationId });
|
|
10233
10988
|
const restartRestore = prepareHostRestartRestore({ trigger });
|
|
10234
10989
|
const restartHandoff = createHostRestartHandoff();
|
|
10235
10990
|
const currentVersion = readOysterunPackageVersion();
|
|
10991
|
+
const continuityPath = prepareHostContinuity({ operationId, targetVersion: currentVersion, toolchain, transactionKind: "restart_only",
|
|
10992
|
+
restartState: readHostRestartRestoreState({ configDir: getConfigDir() }) });
|
|
10993
|
+
bindFreshContinuityBaseline(continuityPath, operationId, buildHostContinuityObservation().baseline,
|
|
10994
|
+
restartRestore.restart_restore?.restart_id, readHostRestartRestoreState({ configDir: getConfigDir() }));
|
|
10236
10995
|
let operationState = writeUpdateOperationState(
|
|
10237
10996
|
{
|
|
10997
|
+
operation_id: operationId,
|
|
10998
|
+
continuity_path: continuityPath,
|
|
10238
10999
|
status: "restart_scheduled",
|
|
10239
11000
|
phase: "Ready to restart",
|
|
10240
11001
|
running: true,
|
|
@@ -10318,9 +11079,10 @@ function scheduleHostRestart({
|
|
|
10318
11079
|
"--restart-id",
|
|
10319
11080
|
operationState.restart_id,
|
|
10320
11081
|
];
|
|
10321
|
-
|
|
11082
|
+
let logFd;
|
|
10322
11083
|
let child;
|
|
10323
11084
|
try {
|
|
11085
|
+
logFd = openSync(logPath, "a");
|
|
10324
11086
|
child = spawn(toolchain.node_path, runnerArgs, {
|
|
10325
11087
|
cwd: REPO_ROOT,
|
|
10326
11088
|
detached: true,
|
|
@@ -10332,30 +11094,14 @@ function scheduleHostRestart({
|
|
|
10332
11094
|
}),
|
|
10333
11095
|
stdio: ["ignore", logFd, logFd],
|
|
10334
11096
|
});
|
|
11097
|
+
} catch (error) {
|
|
11098
|
+
recordContinuityRunnerHandoffFailure({ operationId: operationState.operation_id, error, gateId: restartLoginGate.gate_id });
|
|
11099
|
+
return;
|
|
10335
11100
|
} finally {
|
|
10336
|
-
closeSync(logFd);
|
|
11101
|
+
if (logFd !== undefined) closeSync(logFd);
|
|
10337
11102
|
}
|
|
10338
|
-
child.once("error", () => {
|
|
10339
|
-
|
|
10340
|
-
{
|
|
10341
|
-
...operationState,
|
|
10342
|
-
status: "restart_recovery_required",
|
|
10343
|
-
phase: "Restart recovery required",
|
|
10344
|
-
running: false,
|
|
10345
|
-
recovery_required_at: new Date().toISOString(),
|
|
10346
|
-
recovery_reason:
|
|
10347
|
-
"The canonical restart runner could not start; the current Host state requires bounded recovery.",
|
|
10348
|
-
recovery_boundary: "restart_runner_spawn_failed",
|
|
10349
|
-
error: "The canonical restart runner could not start.",
|
|
10350
|
-
error_redacted: true,
|
|
10351
|
-
},
|
|
10352
|
-
{ configDir: getConfigDir() }
|
|
10353
|
-
);
|
|
10354
|
-
releaseHostRestartLoginGateSafely({
|
|
10355
|
-
gateId: restartLoginGate.gate_id,
|
|
10356
|
-
reason: "restart_runner_spawn_failed",
|
|
10357
|
-
source: "Restart runner spawn failure",
|
|
10358
|
-
});
|
|
11103
|
+
child.once("error", (error) => {
|
|
11104
|
+
recordContinuityRunnerHandoffFailure({ operationId: operationState.operation_id, error, gateId: restartLoginGate.gate_id });
|
|
10359
11105
|
});
|
|
10360
11106
|
child.once("exit", (code) => {
|
|
10361
11107
|
if (code === 0) return;
|
|
@@ -10380,10 +11126,69 @@ function scheduleHostRestart({
|
|
|
10380
11126
|
};
|
|
10381
11127
|
}
|
|
10382
11128
|
|
|
11129
|
+
function scheduleContinuityRecovery({ kind, source, recheckOnly = false, automatic = false }) {
|
|
11130
|
+
const resolution = recheckOnly || automatic ? null : maybeCloseSupersededOysterunUpdateRecovery();
|
|
11131
|
+
let operation = readUpdateOperationState({ configDir: getConfigDir() });
|
|
11132
|
+
if ((!recheckOnly && !automatic && resolution.recovery_plan?.user_action?.kind !== kind) || operation.running || isProcessRunning(operation.runner_pid)) {
|
|
11133
|
+
throw continuityFailure("recovery", "recovery_changed", "The recovery action no longer matches the retained operation.");
|
|
11134
|
+
}
|
|
11135
|
+
let receipt = readContinuity(operation.continuity_path, operation.operation_id);
|
|
11136
|
+
const toolchain = loadActiveHostLifecycleToolchainBundle();
|
|
11137
|
+
if (!recheckOnly && receipt.transaction_kind !== "restart_only" && !receipt.restart_attempted && !receipt.restart_id &&
|
|
11138
|
+
process.pid === receipt.baseline.host.pid && receipt.operation_root) {
|
|
11139
|
+
// Installation can finish while a newly created provider is still booting.
|
|
11140
|
+
// If no handoff snapshot was ever bound, recover from a fresh complete
|
|
11141
|
+
// live Sessions of the original running coordinator. Never replace a bound one.
|
|
11142
|
+
const fresh = buildHostContinuityObservation().baseline;
|
|
11143
|
+
if (fresh.sessions.some((row) => row.observation_error || !hasCompleteContinuityResumeIdentity(row))) throw continuityFailure("recovery", "fresh_roster_not_ready", "Wait for every current Session provider to be ready before recovering this unprepared handoff.");
|
|
11144
|
+
const prepared = prepareHostRestartRestore({ trigger: "p233_recovery_before_first_handoff_snapshot" });
|
|
11145
|
+
receipt = bindFreshContinuityBaseline(operation.continuity_path, operation.operation_id, fresh,
|
|
11146
|
+
prepared.restart_restore?.restart_id, readHostRestartRestoreState({ configDir: getConfigDir() }));
|
|
11147
|
+
receipt = writeContinuityAtomic(operation.continuity_path, { ...receipt,
|
|
11148
|
+
retained_files: { ...receipt.retained_files, fresh_restart_state: retainContinuityFile(
|
|
11149
|
+
getHostRestartRestoreStatePath(getConfigDir()), join(receipt.operation_root, "restart-restore-state.pre-restart.json")) } });
|
|
11150
|
+
}
|
|
11151
|
+
const runner = resolveOysterunUpdateRestartRunner(toolchain);
|
|
11152
|
+
const handoff = receipt.state === "COMMITTED" || recheckOnly ? {} : createHostRestartHandoff();
|
|
11153
|
+
const logPath = join(getConfigDir(), "operation_logs", `oysterun-continuity-recovery-${operation.operation_id}.log`);
|
|
11154
|
+
operation = writeUpdateOperationState({ ...operation, ...handoff, status: "restart_scheduled",
|
|
11155
|
+
phase: recheckOnly ? "Checking the already-running Host and restored Sessions" : receipt.state === "COMMITTED" ? "Finishing committed cleanup" : "Continuing the retained lifecycle operation",
|
|
11156
|
+
running: true, lifecycle_owner_pid: process.pid, runner_pid: null, requested_action: kind,
|
|
11157
|
+
runner_lease_expires_at: new Date(Date.now() + UPDATE_RESTART_RUNNER_LEASE_MS).toISOString(),
|
|
11158
|
+
operation_log_path: logPath }, { configDir: getConfigDir() });
|
|
11159
|
+
const args = [runner.runnerScript, "--transaction-kind", receipt.transaction_kind === "restart_only" ? "restart_only" : "update_latest_and_restart",
|
|
11160
|
+
"--operation-id", operation.operation_id, "--active-package-root", REPO_ROOT, "--config-dir", getConfigDir(),
|
|
11161
|
+
"--node-bin", toolchain.node_path, "--npm-cli", toolchain.npm_cli_path,
|
|
11162
|
+
"--identity-digest", toolchain.toolchain_identity_digest, "--stack", getActiveStackName(), "--health-url", `http://127.0.0.1:${PORT}/health`,
|
|
11163
|
+
...(operation.result_path ? ["--result-path", operation.result_path] : []),
|
|
11164
|
+
...(receipt.restart_id ? ["--restart-id", receipt.restart_id] : []),
|
|
11165
|
+
...(recheckOnly ? ["--recheck-only", "1"] : kind === "restore_and_restart" ? ["--restore-before-restart", "1"] : []),
|
|
11166
|
+
...(kind === "restart_only" ? ["--recover-restart", "1"] : []),
|
|
11167
|
+
];
|
|
11168
|
+
setTimeout(() => {
|
|
11169
|
+
const current = readUpdateOperationState({ configDir: getConfigDir() });
|
|
11170
|
+
if (current.operation_id !== operation.operation_id || current.status !== "restart_scheduled") return;
|
|
11171
|
+
let fd;
|
|
11172
|
+
try {
|
|
11173
|
+
fd = openSync(logPath, "a");
|
|
11174
|
+
const child = spawn(toolchain.node_path, args, { cwd: REPO_ROOT, detached: true,
|
|
11175
|
+
env: buildNodeNpmChildEnvironment(toolchain, { ...process.env, OYSTERUN_LIFECYCLE_OPERATION_ID: operation.operation_id }),
|
|
11176
|
+
stdio: ["ignore", fd, fd] });
|
|
11177
|
+
child.once("error", (error) => recordContinuityRunnerHandoffFailure({ operationId: operation.operation_id, error }));
|
|
11178
|
+
child.unref();
|
|
11179
|
+
} catch (error) {
|
|
11180
|
+
recordContinuityRunnerHandoffFailure({ operationId: operation.operation_id, error });
|
|
11181
|
+
} finally { if (fd !== undefined) closeSync(fd); }
|
|
11182
|
+
}, receipt.state === "COMMITTED" || recheckOnly ? 0 : getHostRestartHandoffDelayMs(handoff.execute_not_before)).unref();
|
|
11183
|
+
return recheckOnly ? null : buildOysterunUpdateStatusPayload({ status: "restart_scheduled", source, ...handoff });
|
|
11184
|
+
}
|
|
11185
|
+
|
|
10383
11186
|
async function scheduleHostLifecycleTransaction({
|
|
10384
11187
|
kind,
|
|
10385
11188
|
source = "host_preferences",
|
|
10386
11189
|
redirectUrl = null,
|
|
11190
|
+
expectedCurrentVersion = null,
|
|
11191
|
+
expectedTargetVersion = null,
|
|
10387
11192
|
} = {}) {
|
|
10388
11193
|
if (kind === "restart_only") {
|
|
10389
11194
|
return scheduleHostRestart({
|
|
@@ -10398,13 +11203,13 @@ async function scheduleHostLifecycleTransaction({
|
|
|
10398
11203
|
if (kind === "update_latest_and_restart") {
|
|
10399
11204
|
return scheduleOysterunPackageUpdate({
|
|
10400
11205
|
channel: OYSTERUN_UPDATE_RELEASE_CHANNEL,
|
|
10401
|
-
source,
|
|
11206
|
+
source, expectedCurrentVersion, expectedTargetVersion,
|
|
10402
11207
|
});
|
|
10403
11208
|
}
|
|
10404
11209
|
if (kind === "update_beta_and_restart") {
|
|
10405
11210
|
return scheduleOysterunPackageUpdate({
|
|
10406
11211
|
channel: OYSTERUN_UPDATE_CHANNEL_BETA,
|
|
10407
|
-
source,
|
|
11212
|
+
source, expectedCurrentVersion, expectedTargetVersion,
|
|
10408
11213
|
});
|
|
10409
11214
|
}
|
|
10410
11215
|
if (kind === "repair_and_restart") {
|
|
@@ -10571,6 +11376,9 @@ function buildHostPreferencesPayload({ managedTunnelQuota = null } = {}) {
|
|
|
10571
11376
|
config.notification_sound_web_enabled !== false,
|
|
10572
11377
|
notification_sound_app_enabled:
|
|
10573
11378
|
config.notification_sound_app_enabled !== false,
|
|
11379
|
+
restore_previous_sessions_after_restart:
|
|
11380
|
+
config.restore_previous_sessions_after_restart !== false,
|
|
11381
|
+
desired_open_restore: buildDesiredOpenRestoreStatusPayload(),
|
|
10574
11382
|
daily_telemetry_enabled: config.daily_telemetry_enabled === true,
|
|
10575
11383
|
daily_telemetry_consent_recorded_at:
|
|
10576
11384
|
normalizeString(config.daily_telemetry_consent_recorded_at),
|
|
@@ -11246,6 +12054,10 @@ function buildHistoryResumeDisposition(record, matrixTranscriptAvailable) {
|
|
|
11246
12054
|
resume_identity_kind: resumeIdentityKind,
|
|
11247
12055
|
resume_unavailable_reason: reason,
|
|
11248
12056
|
});
|
|
12057
|
+
// agent_folder_missing is disabled because the saved AgentFolder no longer exists.
|
|
12058
|
+
// Recheck on each availability read; a previous restore failure is not a veto.
|
|
12059
|
+
const folder = readAgentFolderAvailability(record.agent_folder);
|
|
12060
|
+
if (!folder.available) return unavailable(folder.reason);
|
|
11249
12061
|
if (!matrixTranscriptAvailable) {
|
|
11250
12062
|
return unavailable("matrix_timeline_missing");
|
|
11251
12063
|
}
|
|
@@ -11821,6 +12633,7 @@ function mergeConfigObjects(base = {}, overlay = {}) {
|
|
|
11821
12633
|
}
|
|
11822
12634
|
|
|
11823
12635
|
function persistAgentConfigUpdates(folder, body = {}) {
|
|
12636
|
+
requireAgentFolder(folder);
|
|
11824
12637
|
const layers = readAgentConfigLayers(folder);
|
|
11825
12638
|
const sharedConfig = readWritableAgentConfig(
|
|
11826
12639
|
layers.configPath,
|
|
@@ -12314,6 +13127,47 @@ function sessionRefResolutionResponseFields(resolution) {
|
|
|
12314
13127
|
};
|
|
12315
13128
|
}
|
|
12316
13129
|
|
|
13130
|
+
const historyTranscriptPager = createHistoryTranscriptPager();
|
|
13131
|
+
|
|
13132
|
+
function resolveTranscriptTargetOrRespond({ url, claims, res }) {
|
|
13133
|
+
try {
|
|
13134
|
+
return resolveSessionTranscriptTarget({
|
|
13135
|
+
sessionId: url.searchParams.get("session_id"),
|
|
13136
|
+
sessionRef: url.searchParams.get("session_ref"),
|
|
13137
|
+
requestedAgentId: normalizeString(url.searchParams.get("agent_id")),
|
|
13138
|
+
runtimeOnly: isRuntimeCapabilityOnly(claims),
|
|
13139
|
+
verifyRuntimeActor: () => requireRuntimeCapability(claims, res, { scope: "session:read_transcript" }),
|
|
13140
|
+
verifyLiveTarget: (sessionId, agentId) => requireRuntimeCapability(claims, res, { scope: "session:read_transcript", sessionId, agentId }),
|
|
13141
|
+
resolveLiveRef: (sessionRef) => {
|
|
13142
|
+
if (!sessionManager.getSession(sessionRef) && !sessionManager.list().some((entry) => normalizeSessionName(entry.sessionName) === normalizeSessionName(sessionRef))) {
|
|
13143
|
+
throw Object.assign(new Error("Session reference not found among live Sessions; stopped/history transcript targets require exact --session-id"), { status: 404, reason: "history_session_ref_requires_exact_id" });
|
|
13144
|
+
}
|
|
13145
|
+
return resolveLiveSessionReferenceOrRespond({ sessionRef, res });
|
|
13146
|
+
},
|
|
13147
|
+
getLive: (id) => sessionManager.getSession(id),
|
|
13148
|
+
getHistory: getSessionHistory,
|
|
13149
|
+
authorizeAgent: (id) => requireAgentCapability(claims, id, "can_chat", res),
|
|
13150
|
+
});
|
|
13151
|
+
} catch (error) {
|
|
13152
|
+
respond(res, error.status || 503, { error: error.message, reason: error.reason || "session_history_metadata_unavailable" });
|
|
13153
|
+
return null;
|
|
13154
|
+
}
|
|
13155
|
+
}
|
|
13156
|
+
|
|
13157
|
+
function readTranscriptTarget(target, binding, read) {
|
|
13158
|
+
return target.target_state === "stopped"
|
|
13159
|
+
? withRouteCMatrixTranscriptReadOnly({ binding }, read)
|
|
13160
|
+
: read();
|
|
13161
|
+
}
|
|
13162
|
+
|
|
13163
|
+
function respondTranscriptSourceError(res, error) {
|
|
13164
|
+
if (!["MATRIX_TRANSCRIPT_STORAGE_UNAVAILABLE", "MATRIX_TRANSCRIPT_ROOM_UNAVAILABLE", "MATRIX_TRANSCRIPT_BINDING_MISMATCH"].includes(error.code)) return false;
|
|
13165
|
+
respond(res, error.code === "MATRIX_TRANSCRIPT_BINDING_MISMATCH" ? 403 : 503, { error: error.message,
|
|
13166
|
+
reason: error.code === "MATRIX_TRANSCRIPT_BINDING_MISMATCH" ? "matrix_transcript_binding_owner_mismatch" : error.code === "MATRIX_TRANSCRIPT_ROOM_UNAVAILABLE" ? "matrix_transcript_room_unavailable" : "matrix_transcript_storage_unavailable",
|
|
13167
|
+
committed_transcript_truth: "matrix_room_timeline", host_db_transcript_product_truth: false });
|
|
13168
|
+
return true;
|
|
13169
|
+
}
|
|
13170
|
+
|
|
12317
13171
|
function resolveLiveSessionReferenceOrRespond({ sessionId, sessionRef, res }) {
|
|
12318
13172
|
const normalizedSessionId = normalizeString(sessionId);
|
|
12319
13173
|
if (normalizedSessionId) {
|
|
@@ -14423,6 +15277,8 @@ const ROUTEC_REQUEST_TRAFFIC_SENSITIVE_QUERY_KEYS = [
|
|
|
14423
15277
|
const ROUTEC_REQUEST_TRAFFIC_LOG_BATCH_MAX_RECORDS = 50;
|
|
14424
15278
|
const ROUTEC_REQUEST_TRAFFIC_LOG_FLUSH_DELAY_MS = 250;
|
|
14425
15279
|
const routeCRequestTrafficLogQueues = new Map();
|
|
15280
|
+
const routeCRequestDelayWindowController =
|
|
15281
|
+
createRouteCRequestDelayWindowController();
|
|
14426
15282
|
|
|
14427
15283
|
function classifyRouteCRequestTrafficEndpoint(method, requestPath) {
|
|
14428
15284
|
if (requestPath === "/_matrix/client/v3/sync") return "matrix_sync";
|
|
@@ -14571,15 +15427,20 @@ function isRouteCChatLivenessDiagnosticsEnabled() {
|
|
|
14571
15427
|
return readConfig().debug_routec_chat_liveness_diagnostics_enabled === true;
|
|
14572
15428
|
}
|
|
14573
15429
|
|
|
15430
|
+
function syncRouteCEventLoopDelayWindow(logPath) {
|
|
15431
|
+
return routeCRequestDelayWindowController.sync(logPath);
|
|
15432
|
+
}
|
|
15433
|
+
|
|
14574
15434
|
function attachRouteCRequestTrafficLogger(req, res, url, requestPath) {
|
|
14575
|
-
|
|
14576
|
-
const logPath =
|
|
14577
|
-
process.env.OYSTERUN_ROUTEC_REQUEST_TRAFFIC_LOG_PATH
|
|
14578
|
-
|
|
14579
|
-
|
|
15435
|
+
const diagnosticsEnabled = isRouteCChatLivenessDiagnosticsEnabled();
|
|
15436
|
+
const logPath = diagnosticsEnabled
|
|
15437
|
+
? normalizeString(process.env.OYSTERUN_ROUTEC_REQUEST_TRAFFIC_LOG_PATH)
|
|
15438
|
+
: "";
|
|
15439
|
+
const eventLoopDelayWindow = syncRouteCEventLoopDelayWindow(logPath);
|
|
15440
|
+
if (!eventLoopDelayWindow) return;
|
|
14580
15441
|
|
|
14581
15442
|
const startedAt = new Date();
|
|
14582
|
-
const
|
|
15443
|
+
const startedPerformanceMs = performance.now();
|
|
14583
15444
|
const startedCpuUsage = process.cpuUsage();
|
|
14584
15445
|
const startedEventLoopUtilization = performance.eventLoopUtilization();
|
|
14585
15446
|
const requestId = randomUUID();
|
|
@@ -14618,7 +15479,9 @@ function attachRouteCRequestTrafficLogger(req, res, url, requestPath) {
|
|
|
14618
15479
|
event,
|
|
14619
15480
|
started_at: startedAt.toISOString(),
|
|
14620
15481
|
ended_at: endedAt.toISOString(),
|
|
14621
|
-
|
|
15482
|
+
request_elapsed_ms: Number(
|
|
15483
|
+
(performance.now() - startedPerformanceMs).toFixed(3)
|
|
15484
|
+
),
|
|
14622
15485
|
method: req.method,
|
|
14623
15486
|
endpoint,
|
|
14624
15487
|
path: requestPath,
|
|
@@ -14635,6 +15498,8 @@ function attachRouteCRequestTrafficLogger(req, res, url, requestPath) {
|
|
|
14635
15498
|
active_ms: Number(eventLoopUtilization.active.toFixed(3)),
|
|
14636
15499
|
utilization: Number(eventLoopUtilization.utilization.toFixed(6)),
|
|
14637
15500
|
},
|
|
15501
|
+
event_loop_delay:
|
|
15502
|
+
routeCRequestDelayWindowController.snapshot(eventLoopDelayWindow),
|
|
14638
15503
|
raw_request_body_logged: false,
|
|
14639
15504
|
raw_response_body_logged: false,
|
|
14640
15505
|
stack_name: getActiveStackName(),
|
|
@@ -14751,8 +15616,28 @@ const server = createServer(async (req, res) => {
|
|
|
14751
15616
|
});
|
|
14752
15617
|
}
|
|
14753
15618
|
|
|
14754
|
-
//
|
|
14755
|
-
|
|
15619
|
+
// Shared Sessions/Setup source is mounted by React in the web-chat document.
|
|
15620
|
+
// Missing/stale aliases retain original normalization and warning copy.
|
|
15621
|
+
const legacySessionSurfaceRedirect = req.method === "GET"
|
|
15622
|
+
? buildLegacySessionSurfaceRedirectLocation(url) : null;
|
|
15623
|
+
if (legacySessionSurfaceRedirect) {
|
|
15624
|
+
const legacySessionId = normalizeString(url.searchParams.get("session_id"));
|
|
15625
|
+
if (sessionManager.getSession(legacySessionId)) {
|
|
15626
|
+
return redirect(res, 303, legacySessionSurfaceRedirect, {
|
|
15627
|
+
"Cache-Control": "no-store",
|
|
15628
|
+
"X-Oysterun-Legacy-Session-Surface": "clean-session-workspace",
|
|
15629
|
+
});
|
|
15630
|
+
}
|
|
15631
|
+
}
|
|
15632
|
+
if (req.method === "GET" && /^\/app\/(?:sessions\/?|session-setup\/?|agent-profile\/?|session-loop\/?|settings\/?|scheduler\/?|terminal\/?|host-preferences\/voice-input-usage\/?)$/.test(path)) {
|
|
15633
|
+
return serveWebChatAppDocument(req, res);
|
|
15634
|
+
}
|
|
15635
|
+
|
|
15636
|
+
if (
|
|
15637
|
+
req.method === "GET" &&
|
|
15638
|
+
(isCleanSessionChatAppShellPath(path) ||
|
|
15639
|
+
isCleanSessionMigratedSurfaceAppShellPath(path))
|
|
15640
|
+
) {
|
|
14756
15641
|
const cleanChatClaims = getCleanSessionChatAppShellClaims(req);
|
|
14757
15642
|
if (!cleanChatClaims) {
|
|
14758
15643
|
return serveDashboardHtml(req, res, {
|
|
@@ -14760,7 +15645,7 @@ const server = createServer(async (req, res) => {
|
|
|
14760
15645
|
"X-Oysterun-Clean-Chat-Shell": "dashboard-login",
|
|
14761
15646
|
});
|
|
14762
15647
|
}
|
|
14763
|
-
const cleanChatSessionId =
|
|
15648
|
+
const cleanChatSessionId = getCleanSessionAppShellSessionId(path);
|
|
14764
15649
|
if (!cleanChatSessionId || !sessionManager.getSession(cleanChatSessionId)) {
|
|
14765
15650
|
return redirect(
|
|
14766
15651
|
res,
|
|
@@ -14776,11 +15661,43 @@ const server = createServer(async (req, res) => {
|
|
|
14776
15661
|
return serveWebChatAppDocument(req, res);
|
|
14777
15662
|
}
|
|
14778
15663
|
|
|
15664
|
+
// ── GET migrated standalone browse utilities (one web-chat document) ──
|
|
15665
|
+
if (req.method === "GET" && isStandaloneMigratedBrowseAppShellPath(path)) {
|
|
15666
|
+
const standaloneBrowseClaims = getCleanSessionChatAppShellClaims(req);
|
|
15667
|
+
if (!standaloneBrowseClaims) {
|
|
15668
|
+
return serveDashboardHtml(req, res, {
|
|
15669
|
+
"X-Oysterun-Standalone-Browse-Auth-Guard": "dashboard-login-required",
|
|
15670
|
+
"X-Oysterun-Standalone-Browse-Shell": "dashboard-login",
|
|
15671
|
+
});
|
|
15672
|
+
}
|
|
15673
|
+
return serveWebChatAppDocument(req, res);
|
|
15674
|
+
}
|
|
15675
|
+
|
|
15676
|
+
// ── GET standalone migrated product surfaces (one Route C document) ──
|
|
15677
|
+
if (req.method === "GET" && isStandaloneMigratedSurfaceAppShellPath(path)) {
|
|
15678
|
+
const standaloneClaims = getCleanSessionChatAppShellClaims(req);
|
|
15679
|
+
if (!standaloneClaims) {
|
|
15680
|
+
return serveDashboardHtml(req, res, {
|
|
15681
|
+
"X-Oysterun-Standalone-Surface-Auth-Guard": "dashboard-login-required",
|
|
15682
|
+
"X-Oysterun-Standalone-Surface-Shell": "dashboard-login",
|
|
15683
|
+
});
|
|
15684
|
+
}
|
|
15685
|
+
return serveWebChatAppDocument(req, res);
|
|
15686
|
+
}
|
|
15687
|
+
|
|
14779
15688
|
// ── GET /.well-known/matrix/client (exact public Route C Host-origin discovery only) ──
|
|
14780
15689
|
if (req.method === "GET" && path === "/.well-known/matrix/client") {
|
|
14781
15690
|
return serveRouteCWellKnownMatrixClient(req, res);
|
|
14782
15691
|
}
|
|
14783
15692
|
|
|
15693
|
+
// /app is the stable alias for the new frontend Sessions list.
|
|
15694
|
+
if (req.method === "GET" && (path === "/app" || path === "/app/")) {
|
|
15695
|
+
return redirect(res, 308, `/app/sessions${url.search}`, {
|
|
15696
|
+
"Cache-Control": "no-store",
|
|
15697
|
+
"X-Oysterun-Sessions-Root-Alias": "app-sessions",
|
|
15698
|
+
});
|
|
15699
|
+
}
|
|
15700
|
+
|
|
14784
15701
|
// ── GET /app* (serves dashboard HTML, no auth — login page is in the HTML) ──
|
|
14785
15702
|
if (
|
|
14786
15703
|
req.method === "GET" &&
|
|
@@ -15105,15 +16022,23 @@ const server = createServer(async (req, res) => {
|
|
|
15105
16022
|
});
|
|
15106
16023
|
}
|
|
15107
16024
|
|
|
15108
|
-
// ── POST /routec/matrix/host-scoped-cinny-session-bootstrap (
|
|
16025
|
+
// ── POST /routec/matrix/host-scoped-cinny-session-bootstrap (authenticated exact target) ──
|
|
15109
16026
|
if (
|
|
15110
16027
|
req.method === "POST" &&
|
|
15111
16028
|
path === "/routec/matrix/host-scoped-cinny-session-bootstrap"
|
|
15112
16029
|
) {
|
|
16030
|
+
const claims = authenticate(req, res);
|
|
16031
|
+
if (!claims) return;
|
|
16032
|
+
// Runtime capability markers are verified by their owning capability
|
|
16033
|
+
// routes; they are not a login identity for Matrix credential issuance.
|
|
16034
|
+
if (claims._runtimeCapabilityAuth) {
|
|
16035
|
+
return respond(res, 401, { error: "Authentication required" });
|
|
16036
|
+
}
|
|
15113
16037
|
const body = await readBody(req);
|
|
15114
16038
|
return await routeCMatrixFacade.handleHostScopedCinnySessionBootstrap({
|
|
15115
16039
|
req,
|
|
15116
16040
|
res,
|
|
16041
|
+
claims,
|
|
15117
16042
|
body,
|
|
15118
16043
|
respond,
|
|
15119
16044
|
});
|
|
@@ -15377,6 +16302,27 @@ const server = createServer(async (req, res) => {
|
|
|
15377
16302
|
}
|
|
15378
16303
|
}
|
|
15379
16304
|
|
|
16305
|
+
if (req.method === "GET" && path === "/admin/lifecycle-qualification") {
|
|
16306
|
+
const operationId = url.searchParams.get("operation_id");
|
|
16307
|
+
const bearer = String(req.headers.authorization || "").replace(/^Bearer /, "");
|
|
16308
|
+
if (!verifyContinuityProbeGrant(getConfigDir(), operationId, bearer)) {
|
|
16309
|
+
return respond(res, 403, { error: "Operation observation grant required" });
|
|
16310
|
+
}
|
|
16311
|
+
try {
|
|
16312
|
+
const receipt = readContinuity(continuityReceiptPath(getConfigDir(), operationId), operationId);
|
|
16313
|
+
const observation = buildHostContinuityObservation(receipt.baseline);
|
|
16314
|
+
res.setHeader("Cache-Control", "no-store");
|
|
16315
|
+
return respond(res, 200, {
|
|
16316
|
+
operation_id: operationId, request_nonce: url.searchParams.get("nonce"),
|
|
16317
|
+
...observation, baseline: undefined,
|
|
16318
|
+
});
|
|
16319
|
+
} catch (error) {
|
|
16320
|
+
return respond(res, 409, { operation_id: operationId, request_nonce: url.searchParams.get("nonce"),
|
|
16321
|
+
error: sanitizeLifecycleDiagnostic(error.message), code: error.code || "continuity_observation_failed",
|
|
16322
|
+
lifecycle_diagnostic: error.lifecycle_diagnostic || null });
|
|
16323
|
+
}
|
|
16324
|
+
}
|
|
16325
|
+
|
|
15380
16326
|
if (req.method === "POST" && path === "/admin/restart-prepare") {
|
|
15381
16327
|
const serviceControlClaims = authenticateLocalServiceControlRequest(req);
|
|
15382
16328
|
let restartPrepareClaims = serviceControlClaims;
|
|
@@ -15433,6 +16379,8 @@ const server = createServer(async (req, res) => {
|
|
|
15433
16379
|
await scheduleHostLifecycleTransaction({
|
|
15434
16380
|
kind: normalizeString(body.kind),
|
|
15435
16381
|
source: serviceControlClaims ? "terminal" : "host_preferences",
|
|
16382
|
+
expectedCurrentVersion: body.expected_current_version,
|
|
16383
|
+
expectedTargetVersion: body.expected_target_version,
|
|
15436
16384
|
redirectUrl,
|
|
15437
16385
|
})
|
|
15438
16386
|
);
|
|
@@ -16154,7 +17102,10 @@ const server = createServer(async (req, res) => {
|
|
|
16154
17102
|
})
|
|
16155
17103
|
);
|
|
16156
17104
|
} catch (err) {
|
|
16157
|
-
return respond(res, 409, {
|
|
17105
|
+
return respond(res, 409, {
|
|
17106
|
+
...buildOysterunUpdateStatusPayload(),
|
|
17107
|
+
error: sanitizeLifecycleDiagnostic(err.message), code: err.code || null,
|
|
17108
|
+
});
|
|
16158
17109
|
}
|
|
16159
17110
|
}
|
|
16160
17111
|
|
|
@@ -16197,6 +17148,8 @@ const server = createServer(async (req, res) => {
|
|
|
16197
17148
|
await scheduleHostLifecycleTransaction({
|
|
16198
17149
|
kind: "update_latest_and_restart",
|
|
16199
17150
|
source: "host_preferences",
|
|
17151
|
+
expectedCurrentVersion: body.expected_current_version,
|
|
17152
|
+
expectedTargetVersion: body.expected_target_version,
|
|
16200
17153
|
})
|
|
16201
17154
|
);
|
|
16202
17155
|
} catch (err) {
|
|
@@ -16221,13 +17174,15 @@ const server = createServer(async (req, res) => {
|
|
|
16221
17174
|
if (req.method === "POST" && path === "/admin/update-beta-run") {
|
|
16222
17175
|
if (!requireDashboardMode(claims, res)) return;
|
|
16223
17176
|
try {
|
|
16224
|
-
await readJsonBody(req);
|
|
17177
|
+
const body = await readJsonBody(req);
|
|
16225
17178
|
return respond(
|
|
16226
17179
|
res,
|
|
16227
17180
|
202,
|
|
16228
17181
|
await scheduleHostLifecycleTransaction({
|
|
16229
17182
|
kind: "update_beta_and_restart",
|
|
16230
17183
|
source: "host_preferences",
|
|
17184
|
+
expectedCurrentVersion: body.expected_current_version,
|
|
17185
|
+
expectedTargetVersion: body.expected_target_version,
|
|
16231
17186
|
})
|
|
16232
17187
|
);
|
|
16233
17188
|
} catch (err) {
|
|
@@ -17562,6 +18517,57 @@ const server = createServer(async (req, res) => {
|
|
|
17562
18517
|
}
|
|
17563
18518
|
}
|
|
17564
18519
|
|
|
18520
|
+
if (req.method === "POST" && path === "/sessions/restore-previous") {
|
|
18521
|
+
if (!requireDashboardMode(claims, res)) return;
|
|
18522
|
+
try {
|
|
18523
|
+
const rawBody = await readRawBody(req);
|
|
18524
|
+
let body = {};
|
|
18525
|
+
if (rawBody.length > 0) {
|
|
18526
|
+
try {
|
|
18527
|
+
body = JSON.parse(rawBody.toString());
|
|
18528
|
+
} catch {
|
|
18529
|
+
return respond(res, 400, { error: "Invalid JSON body" });
|
|
18530
|
+
}
|
|
18531
|
+
}
|
|
18532
|
+
const hasSessionId = Object.prototype.hasOwnProperty.call(
|
|
18533
|
+
body,
|
|
18534
|
+
"session_id"
|
|
18535
|
+
);
|
|
18536
|
+
const sessionId = normalizeString(body.session_id);
|
|
18537
|
+
if (hasSessionId && !sessionId) {
|
|
18538
|
+
return respond(res, 400, { error: "session_id must be a string" });
|
|
18539
|
+
}
|
|
18540
|
+
if (sessionId) {
|
|
18541
|
+
const desired = sessionManager.getDesiredOpenRestoreStatus();
|
|
18542
|
+
if (
|
|
18543
|
+
!desired.valid ||
|
|
18544
|
+
!desired.sessions.some((entry) => entry.session_id === sessionId)
|
|
18545
|
+
) {
|
|
18546
|
+
return respond(res, 404, {
|
|
18547
|
+
error: "Desired-open Session not found",
|
|
18548
|
+
session_id: sessionId,
|
|
18549
|
+
});
|
|
18550
|
+
}
|
|
18551
|
+
}
|
|
18552
|
+
const restore = await reconcileDesiredOpenSessions({
|
|
18553
|
+
trigger: sessionId
|
|
18554
|
+
? "manual_restore_previous_session"
|
|
18555
|
+
: "manual_restore_previous_sessions",
|
|
18556
|
+
sessionIds: sessionId ? [sessionId] : null,
|
|
18557
|
+
});
|
|
18558
|
+
return respond(res, 200, {
|
|
18559
|
+
status: "restore_previous_sessions_complete",
|
|
18560
|
+
session_id: sessionId,
|
|
18561
|
+
restore,
|
|
18562
|
+
});
|
|
18563
|
+
} catch (err) {
|
|
18564
|
+
return respond(res, 400, {
|
|
18565
|
+
error: err.message,
|
|
18566
|
+
restore: buildDesiredOpenRestoreStatusPayload(),
|
|
18567
|
+
});
|
|
18568
|
+
}
|
|
18569
|
+
}
|
|
18570
|
+
|
|
17565
18571
|
if (req.method === "POST" && path === "/telemetry/feature") {
|
|
17566
18572
|
if (!requireDashboardMode(claims, res)) return;
|
|
17567
18573
|
try {
|
|
@@ -17602,6 +18608,10 @@ const server = createServer(async (req, res) => {
|
|
|
17602
18608
|
body,
|
|
17603
18609
|
"daily_telemetry_enabled"
|
|
17604
18610
|
) &&
|
|
18611
|
+
!Object.prototype.hasOwnProperty.call(
|
|
18612
|
+
body,
|
|
18613
|
+
"restore_previous_sessions_after_restart"
|
|
18614
|
+
) &&
|
|
17605
18615
|
!Object.prototype.hasOwnProperty.call(body, "default_browse_path") &&
|
|
17606
18616
|
!Object.prototype.hasOwnProperty.call(body, "public_base_url") &&
|
|
17607
18617
|
!Object.prototype.hasOwnProperty.call(body, "direct_host_url") &&
|
|
@@ -17613,7 +18623,7 @@ const server = createServer(async (req, res) => {
|
|
|
17613
18623
|
) {
|
|
17614
18624
|
return respond(res, 400, {
|
|
17615
18625
|
error:
|
|
17616
|
-
"display_name, notification_sound_web_enabled, notification_sound_app_enabled, daily_telemetry_enabled, default_browse_path, direct_host_url, claude_command, codex_command, grok_command, session_defaults, or voice_input required",
|
|
18626
|
+
"display_name, notification_sound_web_enabled, notification_sound_app_enabled, restore_previous_sessions_after_restart, daily_telemetry_enabled, default_browse_path, direct_host_url, claude_command, codex_command, grok_command, session_defaults, or voice_input required",
|
|
17617
18627
|
});
|
|
17618
18628
|
}
|
|
17619
18629
|
|
|
@@ -17697,6 +18707,24 @@ const server = createServer(async (req, res) => {
|
|
|
17697
18707
|
}
|
|
17698
18708
|
}
|
|
17699
18709
|
|
|
18710
|
+
if (
|
|
18711
|
+
Object.prototype.hasOwnProperty.call(
|
|
18712
|
+
body,
|
|
18713
|
+
"restore_previous_sessions_after_restart"
|
|
18714
|
+
)
|
|
18715
|
+
) {
|
|
18716
|
+
if (
|
|
18717
|
+
typeof body.restore_previous_sessions_after_restart !== "boolean"
|
|
18718
|
+
) {
|
|
18719
|
+
return respond(res, 400, {
|
|
18720
|
+
error:
|
|
18721
|
+
"restore_previous_sessions_after_restart must be a boolean",
|
|
18722
|
+
});
|
|
18723
|
+
}
|
|
18724
|
+
updates.restore_previous_sessions_after_restart =
|
|
18725
|
+
body.restore_previous_sessions_after_restart;
|
|
18726
|
+
}
|
|
18727
|
+
|
|
17700
18728
|
if (Object.prototype.hasOwnProperty.call(body, "voice_input")) {
|
|
17701
18729
|
const requestedVoiceInput = body.voice_input;
|
|
17702
18730
|
if (
|
|
@@ -18031,6 +19059,20 @@ const server = createServer(async (req, res) => {
|
|
|
18031
19059
|
}
|
|
18032
19060
|
}
|
|
18033
19061
|
|
|
19062
|
+
if ((req.method === 'GET' && path === '/session/voice-input/receipt') ||
|
|
19063
|
+
(req.method === 'POST' && path === '/session/voice-input/release')) {
|
|
19064
|
+
const body = req.method === 'POST' ? await readJsonBody(req) : null;
|
|
19065
|
+
const expectedHost = normalizeString(body?.host_identity || url.searchParams.get('host_identity'));
|
|
19066
|
+
if (expectedHost && expectedHost !== readCloudIdentity()?.host_id) return respond(res,409,{error:'Recording belongs to a different Host.',code:'voice_input_host_mismatch'});
|
|
19067
|
+
const sessionId = normalizeString(body?.session_id || url.searchParams.get('session_id'));
|
|
19068
|
+
const target = sessionManager.getSession(sessionId) || getSessionHistory().find(row => row.session_id === sessionId);
|
|
19069
|
+
if (!target) return respond(res,404,{error:'Session not found'});
|
|
19070
|
+
if (!requireAgentCapability(claims, target.agentId || target.agent_id, 'can_chat', res)) return;
|
|
19071
|
+
const captureId = normalizeString(body?.capture_id || url.searchParams.get('capture_id'));
|
|
19072
|
+
if (!captureId || !/^[a-zA-Z0-9_-]{1,100}$/.test(captureId)) return respond(res,400,{error:'Invalid recording identity.'});
|
|
19073
|
+
return respond(res,200,req.method === 'POST' ? voiceInputReceipts.release(sessionId,captureId) : voiceInputReceipts.get(sessionId,captureId));
|
|
19074
|
+
}
|
|
19075
|
+
|
|
18034
19076
|
if (
|
|
18035
19077
|
req.method === "POST" &&
|
|
18036
19078
|
path === "/session/voice-input/transcribe"
|
|
@@ -18038,9 +19080,13 @@ const server = createServer(async (req, res) => {
|
|
|
18038
19080
|
let sessionId = null;
|
|
18039
19081
|
let singleFlightStarted = false;
|
|
18040
19082
|
let ownerCaptureBase = null;
|
|
19083
|
+
let receiptCaptureId = null;
|
|
19084
|
+
let receiptGeneration = 1;
|
|
19085
|
+
let providerStarted = false;
|
|
18041
19086
|
try {
|
|
18042
19087
|
const { fields, file } = await parseVoiceInputMultipart(req);
|
|
18043
19088
|
sessionId = normalizeString(fields.session_id);
|
|
19089
|
+
if (fields.host_identity && fields.host_identity !== readCloudIdentity()?.host_id) return respond(res,409,{error:'Recording belongs to a different Host.',code:'voice_input_host_mismatch'});
|
|
18044
19090
|
const session = requireSessionCapability(
|
|
18045
19091
|
claims,
|
|
18046
19092
|
sessionId,
|
|
@@ -18058,13 +19104,6 @@ const server = createServer(async (req, res) => {
|
|
|
18058
19104
|
code: "voice_input_unavailable",
|
|
18059
19105
|
});
|
|
18060
19106
|
}
|
|
18061
|
-
if (!voiceInputSingleFlight.begin(sessionId)) {
|
|
18062
|
-
return respond(res, 409, {
|
|
18063
|
-
error: "Voice Input transcription is already in progress",
|
|
18064
|
-
code: "voice_input_in_flight",
|
|
18065
|
-
});
|
|
18066
|
-
}
|
|
18067
|
-
singleFlightStarted = true;
|
|
18068
19107
|
const metadata = validateVoiceInputMetadata({
|
|
18069
19108
|
captureId: fields.capture_id,
|
|
18070
19109
|
recordingId: fields.recording_id,
|
|
@@ -18096,6 +19135,17 @@ const server = createServer(async (req, res) => {
|
|
|
18096
19135
|
code: "voice_input_sha_mismatch",
|
|
18097
19136
|
});
|
|
18098
19137
|
}
|
|
19138
|
+
receiptCaptureId = metadata.captureId;
|
|
19139
|
+
receiptGeneration = Number(fields.attempt_generation || 1);
|
|
19140
|
+
const receipt = voiceInputReceipts.begin(sessionId, receiptCaptureId,
|
|
19141
|
+
voiceInputFingerprint({ ...metadata, mimeType: resolvedFile.mimeType, size: file.audio.length, audioSha256: actualSha }),
|
|
19142
|
+
receiptGeneration, fields.explicit_retry === 'true', { recording_id: metadata.recordingId, segment_index: metadata.segmentIndex, duration_ms: metadata.durationMs, normalized_mime: resolvedFile.mimeType.split(';')[0].toLowerCase(), size_bytes: file.audio.length, audio_sha256: actualSha });
|
|
19143
|
+
if (receipt.state === 'succeeded') return respond(res, 200, receipt.result);
|
|
19144
|
+
if (receipt.state !== 'received') return respond(res, 409, { ...receipt, error: receipt.result?.error || 'The recording attempt is not ready to upload.', code: `voice_input_${receipt.state}` });
|
|
19145
|
+
if (!voiceInputSingleFlight.begin(sessionId)) return respond(res,409,{error:'Voice Input transcription is already in progress',code:'voice_input_in_flight'});
|
|
19146
|
+
singleFlightStarted = true;
|
|
19147
|
+
if (!voiceInputReceipts.start(sessionId, receiptCaptureId, receiptGeneration)) return respond(res,409,{error:'Recording was removed or already started.',code:'voice_input_receipt_changed'});
|
|
19148
|
+
providerStarted = true;
|
|
18099
19149
|
const privateSettings = readVoiceInputPrivateSettings(getConfigDir());
|
|
18100
19150
|
if (privateSettings.owner_capture_enabled) {
|
|
18101
19151
|
let recentContext = [];
|
|
@@ -18156,14 +19206,20 @@ const server = createServer(async (req, res) => {
|
|
|
18156
19206
|
}
|
|
18157
19207
|
: null
|
|
18158
19208
|
);
|
|
18159
|
-
|
|
19209
|
+
const response = {
|
|
18160
19210
|
status: "voice_input_transcribed",
|
|
18161
19211
|
session_id: session.id,
|
|
18162
19212
|
capture_id: metadata.captureId,
|
|
18163
19213
|
audio_sha256: actualSha,
|
|
18164
19214
|
...result,
|
|
18165
|
-
|
|
19215
|
+
attempt_generation: receiptGeneration,
|
|
19216
|
+
};
|
|
19217
|
+
if (!voiceInputReceipts.complete(sessionId, receiptCaptureId, receiptGeneration, 'succeeded', response)) return respond(res,409,{error:'Recording was removed.',code:'voice_input_released'});
|
|
19218
|
+
return respond(res,200,response);
|
|
18166
19219
|
} catch (err) {
|
|
19220
|
+
if (providerStarted) voiceInputReceipts.complete(sessionId, receiptCaptureId, receiptGeneration,
|
|
19221
|
+
err.providerStatus ? (err.providerStatus >= 400 && err.providerStatus < 500 && err.providerStatus !== 429 ? 'failed_terminal' : 'failed_retryable') : 'outcome_unknown',
|
|
19222
|
+
{ error: err.message, code: err.code || 'voice_input_transcription_failed' });
|
|
18167
19223
|
await uploadOwnerVoiceInputCaptureBestEffort(
|
|
18168
19224
|
ownerCaptureBase
|
|
18169
19225
|
? {
|
|
@@ -18180,7 +19236,7 @@ const server = createServer(async (req, res) => {
|
|
|
18180
19236
|
);
|
|
18181
19237
|
return respond(
|
|
18182
19238
|
res,
|
|
18183
|
-
err.code === "voice_input_provider_timeout" ? 504 : 400,
|
|
19239
|
+
err.code === "voice_input_provider_timeout" ? 504 : String(err.code).includes("receipt") || String(err.code).includes("attempt") || err.code === "voice_input_released" ? 409 : 400,
|
|
18184
19240
|
{
|
|
18185
19241
|
error: err.message,
|
|
18186
19242
|
code: err.code || "voice_input_transcription_failed",
|
|
@@ -18471,7 +19527,7 @@ const server = createServer(async (req, res) => {
|
|
|
18471
19527
|
});
|
|
18472
19528
|
} catch (err) {
|
|
18473
19529
|
return respond(res, 404, {
|
|
18474
|
-
|
|
19530
|
+
...agentFolderErrorResponse(err),
|
|
18475
19531
|
});
|
|
18476
19532
|
}
|
|
18477
19533
|
|
|
@@ -18546,7 +19602,7 @@ const server = createServer(async (req, res) => {
|
|
|
18546
19602
|
}
|
|
18547
19603
|
} catch (err) {
|
|
18548
19604
|
return respond(res, 400, {
|
|
18549
|
-
|
|
19605
|
+
...agentFolderErrorResponse(err, folder),
|
|
18550
19606
|
config_mutated: false,
|
|
18551
19607
|
});
|
|
18552
19608
|
}
|
|
@@ -18899,6 +19955,53 @@ const server = createServer(async (req, res) => {
|
|
|
18899
19955
|
}
|
|
18900
19956
|
}
|
|
18901
19957
|
|
|
19958
|
+
const sessionLoopRecoveryMatch = path.match(
|
|
19959
|
+
/^\/session\/loops\/([^/]+)\/recovery$/
|
|
19960
|
+
);
|
|
19961
|
+
if (sessionLoopRecoveryMatch && req.method === "POST") {
|
|
19962
|
+
const scheduleId = decodeURIComponent(sessionLoopRecoveryMatch[1]);
|
|
19963
|
+
const body = await readBody(req);
|
|
19964
|
+
const sessionTarget = resolveLiveSessionReferenceOrRespond({
|
|
19965
|
+
sessionId: body.session_id,
|
|
19966
|
+
sessionRef: body.session_ref,
|
|
19967
|
+
res,
|
|
19968
|
+
});
|
|
19969
|
+
if (!sessionTarget) return;
|
|
19970
|
+
const session = requireSessionCapabilityOrRuntimeScope(
|
|
19971
|
+
claims,
|
|
19972
|
+
sessionTarget.sessionId,
|
|
19973
|
+
"can_chat",
|
|
19974
|
+
"session:send",
|
|
19975
|
+
res
|
|
19976
|
+
);
|
|
19977
|
+
if (!session) return;
|
|
19978
|
+
try {
|
|
19979
|
+
const result = schedulerService.resolveSessionLoopIncidentForGui({
|
|
19980
|
+
hostSessionId: session.id,
|
|
19981
|
+
agentId: session.agentId,
|
|
19982
|
+
agentFolder: session.cwd,
|
|
19983
|
+
scheduleId,
|
|
19984
|
+
action: body.action,
|
|
19985
|
+
});
|
|
19986
|
+
const feedback = await writeLoopFeedbackOrThrow({
|
|
19987
|
+
session,
|
|
19988
|
+
action: `recovery ${result.action}`,
|
|
19989
|
+
schedule: result.schedule,
|
|
19990
|
+
});
|
|
19991
|
+
return respond(res, 200, {
|
|
19992
|
+
...result,
|
|
19993
|
+
matrix_feedback: feedback,
|
|
19994
|
+
...sessionRefResolutionResponseFields(sessionTarget.resolution),
|
|
19995
|
+
});
|
|
19996
|
+
} catch (err) {
|
|
19997
|
+
return respond(
|
|
19998
|
+
res,
|
|
19999
|
+
getSessionLoopCrudErrorStatus(err),
|
|
20000
|
+
serializeSessionLoopCrudError(err)
|
|
20001
|
+
);
|
|
20002
|
+
}
|
|
20003
|
+
}
|
|
20004
|
+
|
|
18902
20005
|
const sessionLoopMatch = path.match(/^\/session\/loops\/([^/]+)$/);
|
|
18903
20006
|
if (
|
|
18904
20007
|
sessionLoopMatch &&
|
|
@@ -18974,6 +20077,17 @@ const server = createServer(async (req, res) => {
|
|
|
18974
20077
|
...sessionRefResolutionResponseFields(sessionTarget.resolution),
|
|
18975
20078
|
});
|
|
18976
20079
|
}
|
|
20080
|
+
const definitionFieldsPresent = [
|
|
20081
|
+
"interval",
|
|
20082
|
+
"command_text",
|
|
20083
|
+
"prompt_text",
|
|
20084
|
+
"prompt",
|
|
20085
|
+
"start_at",
|
|
20086
|
+
"end_at",
|
|
20087
|
+
].some((key) => Object.prototype.hasOwnProperty.call(body, key));
|
|
20088
|
+
const runtimeOnly =
|
|
20089
|
+
body.runtime_only === true ||
|
|
20090
|
+
(typeof desiredEnabled === "boolean" && !definitionFieldsPresent);
|
|
18977
20091
|
const result = schedulerService.updateSessionLoopScheduleForGui({
|
|
18978
20092
|
hostSessionId: session.id,
|
|
18979
20093
|
agentId: session.agentId,
|
|
@@ -18984,7 +20098,7 @@ const server = createServer(async (req, res) => {
|
|
|
18984
20098
|
startAt: body.start_at ?? null,
|
|
18985
20099
|
endAt: body.end_at ?? null,
|
|
18986
20100
|
enabled: desiredEnabled,
|
|
18987
|
-
runtimeOnly
|
|
20101
|
+
runtimeOnly,
|
|
18988
20102
|
});
|
|
18989
20103
|
const feedback = await writeLoopFeedbackOrThrow({
|
|
18990
20104
|
session,
|
|
@@ -19335,6 +20449,7 @@ const server = createServer(async (req, res) => {
|
|
|
19335
20449
|
|
|
19336
20450
|
// ── POST /session/start ────────────────────────────────
|
|
19337
20451
|
if (req.method === "POST" && path === "/session/start") {
|
|
20452
|
+
try { assertSessionRosterMutationAvailable(); } catch (error) { return respond(res, 409, { error: error.message, code: error.code }); }
|
|
19338
20453
|
const body = await readBody(req);
|
|
19339
20454
|
if (rejectRetiredCapabilitySettingInput(res, body)) return;
|
|
19340
20455
|
const { agent_id, agent_folder, interface_type, session_name } = body;
|
|
@@ -19350,6 +20465,12 @@ const server = createServer(async (req, res) => {
|
|
|
19350
20465
|
res
|
|
19351
20466
|
);
|
|
19352
20467
|
if (!auth) return;
|
|
20468
|
+
let sessionTags;
|
|
20469
|
+
try {
|
|
20470
|
+
sessionTags = resolveSessionStartTags(body, auth);
|
|
20471
|
+
} catch (err) {
|
|
20472
|
+
return respond(res, 400, { error: err.message, code: err.code });
|
|
20473
|
+
}
|
|
19353
20474
|
|
|
19354
20475
|
let resolvedFolder;
|
|
19355
20476
|
try {
|
|
@@ -19555,6 +20676,7 @@ const server = createServer(async (req, res) => {
|
|
|
19555
20676
|
}
|
|
19556
20677
|
let session;
|
|
19557
20678
|
try {
|
|
20679
|
+
assertSessionRosterMutationAvailable();
|
|
19558
20680
|
session = sessionManager.start({
|
|
19559
20681
|
agentId: agent_id,
|
|
19560
20682
|
cwd: resolvedFolder,
|
|
@@ -19565,6 +20687,9 @@ const server = createServer(async (req, res) => {
|
|
|
19565
20687
|
providerStartupResolvedCommand:
|
|
19566
20688
|
startupPreflight.diagnostic?.resolved_command || null,
|
|
19567
20689
|
sessionName: resolvedSessionName,
|
|
20690
|
+
tags: sessionTags,
|
|
20691
|
+
parentSessionId: auth.actorSession?.id || null,
|
|
20692
|
+
deferTagsPersistence: true,
|
|
19568
20693
|
provider: resolved.runtime.provider,
|
|
19569
20694
|
model: resolved.runtime.model,
|
|
19570
20695
|
reasoningEffort: resolved.runtime.reasoningEffort,
|
|
@@ -19720,11 +20845,13 @@ const server = createServer(async (req, res) => {
|
|
|
19720
20845
|
recordTelemetryCounter("session_started_count");
|
|
19721
20846
|
recordTelemetryFeature("sessions");
|
|
19722
20847
|
recordTelemetryFeature("chat");
|
|
20848
|
+
const persistedTags = canonicalSessionTags(session);
|
|
19723
20849
|
|
|
19724
|
-
|
|
20850
|
+
const response = {
|
|
19725
20851
|
success: true,
|
|
19726
20852
|
session_id: session.id,
|
|
19727
20853
|
session_name: session.sessionName,
|
|
20854
|
+
tags: persistedTags,
|
|
19728
20855
|
agent_id,
|
|
19729
20856
|
provider: session.provider || "claude",
|
|
19730
20857
|
permission_mode: session.permissionMode ?? null,
|
|
@@ -19800,7 +20927,10 @@ const server = createServer(async (req, res) => {
|
|
|
19800
20927
|
bookkeeping_warning: bookkeepingWarnings.length
|
|
19801
20928
|
? bookkeepingWarnings.join("; ")
|
|
19802
20929
|
: null,
|
|
19803
|
-
}
|
|
20930
|
+
};
|
|
20931
|
+
commitSuccessfulSessionTags(session);
|
|
20932
|
+
refreshSessionBookkeepingWarning(response, session);
|
|
20933
|
+
return respond(res, 200, response);
|
|
19804
20934
|
}
|
|
19805
20935
|
|
|
19806
20936
|
// ── P203 callback get/list/cancel ──────────────────────
|
|
@@ -19859,7 +20989,7 @@ const server = createServer(async (req, res) => {
|
|
|
19859
20989
|
});
|
|
19860
20990
|
return respond(res, 200, { status: "callbacks", ...page });
|
|
19861
20991
|
}
|
|
19862
|
-
const receipt = callbackHostIntegration.cancel(
|
|
20992
|
+
const receipt = await callbackHostIntegration.cancel(
|
|
19863
20993
|
callbackBody?.callback_id,
|
|
19864
20994
|
{ relatedSessionId }
|
|
19865
20995
|
);
|
|
@@ -20596,6 +21726,7 @@ const server = createServer(async (req, res) => {
|
|
|
20596
21726
|
|
|
20597
21727
|
// ── POST /session/stop ─────────────────────────────────
|
|
20598
21728
|
if (req.method === "POST" && path === "/session/stop") {
|
|
21729
|
+
try { assertSessionRosterMutationAvailable(); } catch (error) { return respond(res, 409, { error: error.message, code: error.code }); }
|
|
20599
21730
|
const body = await readBody(req);
|
|
20600
21731
|
const { force } = body;
|
|
20601
21732
|
const sessionTarget = resolveLiveSessionReferenceOrRespond({
|
|
@@ -20654,7 +21785,9 @@ const server = createServer(async (req, res) => {
|
|
|
20654
21785
|
try {
|
|
20655
21786
|
if (force) {
|
|
20656
21787
|
await sessionManager.killSession(session.id);
|
|
21788
|
+
sessionManager.removeDesiredOpenSession(session.id);
|
|
20657
21789
|
} else {
|
|
21790
|
+
assertSessionRosterMutationAvailable();
|
|
20658
21791
|
await sessionManager.stopSession(session.id);
|
|
20659
21792
|
}
|
|
20660
21793
|
sessionLoopRuntimeCleanup =
|
|
@@ -20712,6 +21845,8 @@ const server = createServer(async (req, res) => {
|
|
|
20712
21845
|
|
|
20713
21846
|
recordTelemetryCounter("session_stopped_count");
|
|
20714
21847
|
recordTelemetryFeature("sessions");
|
|
21848
|
+
sessionCollectionAdmittedSessionIds.delete(session.id);
|
|
21849
|
+
commitSessionCollectionChange(["stop", "history"], [session.id]);
|
|
20715
21850
|
return respond(res, 200, {
|
|
20716
21851
|
status: force ? "killed" : "stopped",
|
|
20717
21852
|
session_id: session.id,
|
|
@@ -20759,6 +21894,9 @@ const server = createServer(async (req, res) => {
|
|
|
20759
21894
|
}
|
|
20760
21895
|
const { session: renamedSession, changed } =
|
|
20761
21896
|
sessionManager.renameSession(session.id, resolvedSessionName);
|
|
21897
|
+
if (changed) {
|
|
21898
|
+
commitSessionCollectionChange(["rename"], [renamedSession.id]);
|
|
21899
|
+
}
|
|
20762
21900
|
return respond(res, 200, {
|
|
20763
21901
|
renamed: changed,
|
|
20764
21902
|
session_id: renamedSession.id,
|
|
@@ -21020,7 +22158,7 @@ const server = createServer(async (req, res) => {
|
|
|
21020
22158
|
);
|
|
21021
22159
|
if (!session) return;
|
|
21022
22160
|
|
|
21023
|
-
const message = sessionManager.cancelOutboxMessage(
|
|
22161
|
+
const message = await sessionManager.cancelOutboxMessage(
|
|
21024
22162
|
session.id,
|
|
21025
22163
|
message_id
|
|
21026
22164
|
);
|
|
@@ -21128,6 +22266,7 @@ const server = createServer(async (req, res) => {
|
|
|
21128
22266
|
|
|
21129
22267
|
// ── POST /session/restart ──────────────────────────────
|
|
21130
22268
|
if (req.method === "POST" && path === "/session/restart") {
|
|
22269
|
+
try { assertSessionRosterMutationAvailable(); } catch (error) { return respond(res, 409, { error: error.message, code: error.code }); }
|
|
21131
22270
|
const body = await readBody(req);
|
|
21132
22271
|
const {
|
|
21133
22272
|
provider,
|
|
@@ -21316,6 +22455,7 @@ const server = createServer(async (req, res) => {
|
|
|
21316
22455
|
|
|
21317
22456
|
let restarted;
|
|
21318
22457
|
try {
|
|
22458
|
+
assertSessionRosterMutationAvailable();
|
|
21319
22459
|
restarted = await sessionManager.restartSession(
|
|
21320
22460
|
currentSession.id,
|
|
21321
22461
|
overrides
|
|
@@ -21364,9 +22504,11 @@ const server = createServer(async (req, res) => {
|
|
|
21364
22504
|
recordTelemetryCounter("session_restarted_count");
|
|
21365
22505
|
recordTelemetryFeature("sessions");
|
|
21366
22506
|
recordTelemetryFeature("chat");
|
|
22507
|
+
admitSessionCollectionSession(restarted.id, ["restore"], { required: true });
|
|
21367
22508
|
return respond(res, 200, {
|
|
21368
22509
|
session_id: restarted.id,
|
|
21369
22510
|
session_name: restarted.sessionName || null,
|
|
22511
|
+
tags: canonicalSessionTags(restarted),
|
|
21370
22512
|
agent_id: restarted.agentId,
|
|
21371
22513
|
provider: restarted.provider || "claude",
|
|
21372
22514
|
model: restarted.model || null,
|
|
@@ -21470,6 +22612,7 @@ const server = createServer(async (req, res) => {
|
|
|
21470
22612
|
agent_id: session.agentId,
|
|
21471
22613
|
session_id: session.id,
|
|
21472
22614
|
session_name: session.sessionName || null,
|
|
22615
|
+
tags: canonicalSessionTags(session),
|
|
21473
22616
|
provider: session.provider || "claude",
|
|
21474
22617
|
model: session.model || null,
|
|
21475
22618
|
reasoning_effort: session.reasoningEffort || null,
|
|
@@ -21540,6 +22683,13 @@ const server = createServer(async (req, res) => {
|
|
|
21540
22683
|
const hasNotificationRequest = hasNotificationConfigRequest(body);
|
|
21541
22684
|
const hasAllowedPathsRequest =
|
|
21542
22685
|
hasSessionProfileAllowedPathsRequest(body);
|
|
22686
|
+
const hasTagsRequest = Object.prototype.hasOwnProperty.call(
|
|
22687
|
+
body,
|
|
22688
|
+
"tags"
|
|
22689
|
+
);
|
|
22690
|
+
const nextTags = hasTagsRequest
|
|
22691
|
+
? normalizeSessionTags(body.tags)
|
|
22692
|
+
: null;
|
|
21543
22693
|
const telegramConfigRequest = normalizeTelegramConfigPayload(body);
|
|
21544
22694
|
const hasTelegramRequest = hasTelegramConfigUpdate(telegramConfigRequest);
|
|
21545
22695
|
if (
|
|
@@ -21548,10 +22698,11 @@ const server = createServer(async (req, res) => {
|
|
|
21548
22698
|
!hasWebsitePasswordRequest &&
|
|
21549
22699
|
!hasNotificationRequest &&
|
|
21550
22700
|
!hasAllowedPathsRequest &&
|
|
22701
|
+
!hasTagsRequest &&
|
|
21551
22702
|
!hasTelegramRequest
|
|
21552
22703
|
) {
|
|
21553
22704
|
throw new Error(
|
|
21554
|
-
"web_access, web_enabled, web_password, notifications, allowed_paths, or telegram settings required"
|
|
22705
|
+
"web_access, web_enabled, web_password, notifications, allowed_paths, tags, or telegram settings required"
|
|
21555
22706
|
);
|
|
21556
22707
|
}
|
|
21557
22708
|
const persistSharedConfig =
|
|
@@ -21692,7 +22843,10 @@ const server = createServer(async (req, res) => {
|
|
|
21692
22843
|
session.notificationConfigUpdatedAt = new Date().toISOString();
|
|
21693
22844
|
}
|
|
21694
22845
|
if (hasAllowedPathsRequest) {
|
|
21695
|
-
|
|
22846
|
+
sessionManager.updateWorkspacePolicy(
|
|
22847
|
+
session.id,
|
|
22848
|
+
resolved.runtime.workspacePolicy
|
|
22849
|
+
);
|
|
21696
22850
|
}
|
|
21697
22851
|
if (hasTelegramRequest) {
|
|
21698
22852
|
session.telegramConfig = buildResolvedSessionTelegramConfig(
|
|
@@ -21714,7 +22868,10 @@ const server = createServer(async (req, res) => {
|
|
|
21714
22868
|
});
|
|
21715
22869
|
}
|
|
21716
22870
|
if (hasAllowedPathsRequest) {
|
|
21717
|
-
|
|
22871
|
+
sessionManager.updateWorkspacePolicy(
|
|
22872
|
+
session.id,
|
|
22873
|
+
nextWorkspacePolicy
|
|
22874
|
+
);
|
|
21718
22875
|
}
|
|
21719
22876
|
applySessionTelegramConfigRequest(session, telegramConfigRequest);
|
|
21720
22877
|
if (hasTelegramRequest) {
|
|
@@ -21747,13 +22904,14 @@ const server = createServer(async (req, res) => {
|
|
|
21747
22904
|
if (hasTelegramRequest) {
|
|
21748
22905
|
recordTelemetryFeature("telegram_settings");
|
|
21749
22906
|
}
|
|
21750
|
-
|
|
22907
|
+
const response = {
|
|
21751
22908
|
status: "session_profile_config_updated",
|
|
21752
22909
|
session_id: session.id,
|
|
21753
22910
|
agent_id: session.agentId,
|
|
21754
22911
|
provider: session.provider || "claude",
|
|
21755
22912
|
model: session.model || null,
|
|
21756
22913
|
cwd: session.cwd || null,
|
|
22914
|
+
tags: hasTagsRequest ? nextTags : canonicalSessionTags(session),
|
|
21757
22915
|
web_access: nextWebAccess,
|
|
21758
22916
|
web_enabled: nextWebEnabled,
|
|
21759
22917
|
website,
|
|
@@ -21791,10 +22949,13 @@ const server = createServer(async (req, res) => {
|
|
|
21791
22949
|
private_config_mutated: localConfigMutated,
|
|
21792
22950
|
private_local_material_serialized: localConfigMutated,
|
|
21793
22951
|
...sessionRefResolutionResponseFields(sessionTarget.resolution),
|
|
21794
|
-
}
|
|
22952
|
+
};
|
|
22953
|
+
if (hasTagsRequest) saveSuccessfulSessionTags(session, nextTags);
|
|
22954
|
+
return respond(res, 200, response);
|
|
21795
22955
|
} catch (err) {
|
|
21796
22956
|
return respond(res, 400, {
|
|
21797
22957
|
error: err.message,
|
|
22958
|
+
code: err.code || "session_profile_config_invalid",
|
|
21798
22959
|
session_profile_config: true,
|
|
21799
22960
|
config_mutated: false,
|
|
21800
22961
|
});
|
|
@@ -22052,36 +23213,11 @@ const server = createServer(async (req, res) => {
|
|
|
22052
23213
|
|
|
22053
23214
|
// ── GET /session/messages/search?session_id=…&q=… ──
|
|
22054
23215
|
if (req.method === "GET" && path === "/session/messages/search") {
|
|
22055
|
-
const sessionTarget =
|
|
22056
|
-
sessionId: url.searchParams.get("session_id"),
|
|
22057
|
-
sessionRef: url.searchParams.get("session_ref"),
|
|
22058
|
-
res,
|
|
22059
|
-
});
|
|
23216
|
+
const sessionTarget = resolveTranscriptTargetOrRespond({ url, claims, res });
|
|
22060
23217
|
if (!sessionTarget) return;
|
|
22061
23218
|
const session_id = sessionTarget.sessionId;
|
|
22062
|
-
const
|
|
22063
|
-
|
|
22064
|
-
return respond(res, 404, { error: "Session not found" });
|
|
22065
|
-
}
|
|
22066
|
-
const requestedAgentId = normalizeString(url.searchParams.get("agent_id"));
|
|
22067
|
-
if (requestedAgentId && requestedAgentId !== targetSession.agentId) {
|
|
22068
|
-
return respond(res, 403, {
|
|
22069
|
-
error: "Matrix search target does not match session agent",
|
|
22070
|
-
reason: "target_session_agent_mismatch",
|
|
22071
|
-
});
|
|
22072
|
-
}
|
|
22073
|
-
const agent_id = requestedAgentId || targetSession.agentId;
|
|
22074
|
-
let runtimeAuth = null;
|
|
22075
|
-
if (isRuntimeCapabilityOnly(claims)) {
|
|
22076
|
-
runtimeAuth = requireRuntimeCapability(claims, res, {
|
|
22077
|
-
scope: "session:read_transcript",
|
|
22078
|
-
sessionId: session_id,
|
|
22079
|
-
agentId: agent_id,
|
|
22080
|
-
});
|
|
22081
|
-
if (!runtimeAuth) return;
|
|
22082
|
-
} else if (!requireAgentCapability(claims, agent_id, "can_chat", res)) {
|
|
22083
|
-
return;
|
|
22084
|
-
}
|
|
23219
|
+
const agent_id = sessionTarget.agentId;
|
|
23220
|
+
const runtimeAuth = sessionTarget.runtimeAuth;
|
|
22085
23221
|
|
|
22086
23222
|
const searchQuery = normalizeString(
|
|
22087
23223
|
url.searchParams.get("q") || url.searchParams.get("query")
|
|
@@ -22121,7 +23257,7 @@ const server = createServer(async (req, res) => {
|
|
|
22121
23257
|
}
|
|
22122
23258
|
throw err;
|
|
22123
23259
|
}
|
|
22124
|
-
if (binding.host_agent_id !== agent_id) {
|
|
23260
|
+
if (binding.host_agent_id !== agent_id || binding.host_session_id !== session_id) {
|
|
22125
23261
|
return respond(res, 403, {
|
|
22126
23262
|
error: "Matrix search binding does not match agent",
|
|
22127
23263
|
reason: "matrix_search_binding_agent_mismatch",
|
|
@@ -22150,14 +23286,15 @@ const server = createServer(async (req, res) => {
|
|
|
22150
23286
|
|
|
22151
23287
|
let matrixSearch;
|
|
22152
23288
|
try {
|
|
22153
|
-
matrixSearch = searchRouteCMatrixRoomEvents({
|
|
23289
|
+
matrixSearch = readTranscriptTarget(sessionTarget, binding, () => searchRouteCMatrixRoomEvents({
|
|
22154
23290
|
binding,
|
|
22155
23291
|
searchTerm: searchQuery,
|
|
22156
23292
|
limit,
|
|
22157
23293
|
nextBatchToken: backendNextBatch,
|
|
22158
23294
|
senderFilter,
|
|
22159
|
-
});
|
|
23295
|
+
}));
|
|
22160
23296
|
} catch (err) {
|
|
23297
|
+
if (respondTranscriptSourceError(res, err)) return;
|
|
22161
23298
|
if (Number.isInteger(err?.status) && err?.body) {
|
|
22162
23299
|
return respond(res, err.status, err.body);
|
|
22163
23300
|
}
|
|
@@ -22235,41 +23372,17 @@ const server = createServer(async (req, res) => {
|
|
|
22235
23372
|
pagination_contract: "opaque_next_batch_v1",
|
|
22236
23373
|
raw_matrix_access_token_required: false,
|
|
22237
23374
|
...sessionRefResolutionResponseFields(sessionTarget.resolution),
|
|
23375
|
+
...sessionTranscriptTargetFields(sessionTarget),
|
|
22238
23376
|
});
|
|
22239
23377
|
}
|
|
22240
23378
|
|
|
22241
23379
|
// ── GET /session/messages-around?session_id=…&event_id=… ──
|
|
22242
23380
|
if (req.method === "GET" && path === "/session/messages-around") {
|
|
22243
|
-
const sessionTarget =
|
|
22244
|
-
sessionId: url.searchParams.get("session_id"),
|
|
22245
|
-
sessionRef: url.searchParams.get("session_ref"),
|
|
22246
|
-
res,
|
|
22247
|
-
});
|
|
23381
|
+
const sessionTarget = resolveTranscriptTargetOrRespond({ url, claims, res });
|
|
22248
23382
|
if (!sessionTarget) return;
|
|
22249
23383
|
const session_id = sessionTarget.sessionId;
|
|
22250
|
-
const
|
|
22251
|
-
|
|
22252
|
-
return respond(res, 404, { error: "Session not found" });
|
|
22253
|
-
}
|
|
22254
|
-
const requestedAgentId = normalizeString(url.searchParams.get("agent_id"));
|
|
22255
|
-
if (requestedAgentId && requestedAgentId !== targetSession.agentId) {
|
|
22256
|
-
return respond(res, 403, {
|
|
22257
|
-
error: "Matrix exact context target does not match session agent",
|
|
22258
|
-
reason: "target_session_agent_mismatch",
|
|
22259
|
-
});
|
|
22260
|
-
}
|
|
22261
|
-
const agent_id = requestedAgentId || targetSession.agentId;
|
|
22262
|
-
let runtimeAuth = null;
|
|
22263
|
-
if (isRuntimeCapabilityOnly(claims)) {
|
|
22264
|
-
runtimeAuth = requireRuntimeCapability(claims, res, {
|
|
22265
|
-
scope: "session:read_transcript",
|
|
22266
|
-
sessionId: session_id,
|
|
22267
|
-
agentId: agent_id,
|
|
22268
|
-
});
|
|
22269
|
-
if (!runtimeAuth) return;
|
|
22270
|
-
} else if (!requireAgentCapability(claims, agent_id, "can_chat", res)) {
|
|
22271
|
-
return;
|
|
22272
|
-
}
|
|
23384
|
+
const agent_id = sessionTarget.agentId;
|
|
23385
|
+
const runtimeAuth = sessionTarget.runtimeAuth;
|
|
22273
23386
|
|
|
22274
23387
|
const eventId = normalizeString(url.searchParams.get("event_id"));
|
|
22275
23388
|
if (!eventId) {
|
|
@@ -22331,7 +23444,7 @@ const server = createServer(async (req, res) => {
|
|
|
22331
23444
|
}
|
|
22332
23445
|
throw err;
|
|
22333
23446
|
}
|
|
22334
|
-
if (binding.host_agent_id !== agent_id) {
|
|
23447
|
+
if (binding.host_agent_id !== agent_id || binding.host_session_id !== session_id) {
|
|
22335
23448
|
return respond(res, 403, {
|
|
22336
23449
|
error: "Matrix exact context binding does not match agent",
|
|
22337
23450
|
reason: "matrix_context_binding_agent_mismatch",
|
|
@@ -22342,13 +23455,14 @@ const server = createServer(async (req, res) => {
|
|
|
22342
23455
|
|
|
22343
23456
|
let exactContext;
|
|
22344
23457
|
try {
|
|
22345
|
-
exactContext = readMatrixTranscriptContext({
|
|
23458
|
+
exactContext = readTranscriptTarget(sessionTarget, binding, () => readMatrixTranscriptContext({
|
|
22346
23459
|
binding,
|
|
22347
23460
|
eventId,
|
|
22348
23461
|
before,
|
|
22349
23462
|
after,
|
|
22350
|
-
});
|
|
23463
|
+
}));
|
|
22351
23464
|
} catch (err) {
|
|
23465
|
+
if (respondTranscriptSourceError(res, err)) return;
|
|
22352
23466
|
const projectionInvalid =
|
|
22353
23467
|
err?.code === "MATRIX_CONTEXT_PROJECTION_INVALID";
|
|
22354
23468
|
return respond(res, projectionInvalid ? 502 : 503, {
|
|
@@ -22437,62 +23551,20 @@ const server = createServer(async (req, res) => {
|
|
|
22437
23551
|
exact_context_read: exactContext?.context || null,
|
|
22438
23552
|
raw_matrix_access_token_required: false,
|
|
22439
23553
|
...sessionRefResolutionResponseFields(sessionTarget.resolution),
|
|
23554
|
+
...sessionTranscriptTargetFields(sessionTarget),
|
|
22440
23555
|
});
|
|
22441
23556
|
}
|
|
22442
23557
|
|
|
22443
23558
|
// ── GET /session/messages?session_id=…&agent_id=… ──────
|
|
22444
23559
|
if (req.method === "GET" && path === "/session/messages") {
|
|
22445
|
-
const sessionTarget =
|
|
22446
|
-
sessionId: url.searchParams.get("session_id"),
|
|
22447
|
-
sessionRef: url.searchParams.get("session_ref"),
|
|
22448
|
-
res,
|
|
22449
|
-
});
|
|
23560
|
+
const sessionTarget = resolveTranscriptTargetOrRespond({ url, claims, res });
|
|
22450
23561
|
if (!sessionTarget) return;
|
|
22451
23562
|
const session_id = sessionTarget.sessionId;
|
|
23563
|
+
const agent_id = sessionTarget.agentId;
|
|
22452
23564
|
const recentTailPresent = url.searchParams.has("recent_tail");
|
|
22453
23565
|
const rawRecentTail = normalizeString(url.searchParams.get("recent_tail"));
|
|
22454
|
-
if (recentTailPresent && rawRecentTail !== "true") {
|
|
22455
|
-
return respond(res, 400, {
|
|
22456
|
-
error: "recent_tail query param must be true when present",
|
|
22457
|
-
});
|
|
22458
|
-
}
|
|
23566
|
+
if (recentTailPresent && rawRecentTail !== "true") return respond(res, 400, { error: "recent_tail query param must be true when present" });
|
|
22459
23567
|
const recentTailRequested = rawRecentTail === "true";
|
|
22460
|
-
let targetSession = null;
|
|
22461
|
-
if (recentTailRequested) {
|
|
22462
|
-
targetSession = sessionManager.getSession(session_id);
|
|
22463
|
-
if (!targetSession) {
|
|
22464
|
-
return respond(res, 404, { error: "Session not found" });
|
|
22465
|
-
}
|
|
22466
|
-
}
|
|
22467
|
-
const requestedAgentId = normalizeString(
|
|
22468
|
-
url.searchParams.get("agent_id")
|
|
22469
|
-
);
|
|
22470
|
-
if (
|
|
22471
|
-
recentTailRequested &&
|
|
22472
|
-
requestedAgentId &&
|
|
22473
|
-
requestedAgentId !== targetSession.agentId
|
|
22474
|
-
) {
|
|
22475
|
-
return respond(res, 403, {
|
|
22476
|
-
error: "Recent Matrix transcript target does not match session agent",
|
|
22477
|
-
committed_transcript_truth: "matrix_room_timeline",
|
|
22478
|
-
host_db_transcript_product_truth: false,
|
|
22479
|
-
});
|
|
22480
|
-
}
|
|
22481
|
-
const agent_id =
|
|
22482
|
-
requestedAgentId || (recentTailRequested ? targetSession.agentId : "");
|
|
22483
|
-
if (!agent_id) {
|
|
22484
|
-
return respond(res, 400, { error: "agent_id query param required" });
|
|
22485
|
-
}
|
|
22486
|
-
if (isRuntimeCapabilityOnly(claims)) {
|
|
22487
|
-
const auth = requireRuntimeCapability(claims, res, {
|
|
22488
|
-
scope: "session:read_transcript",
|
|
22489
|
-
sessionId: session_id,
|
|
22490
|
-
agentId: agent_id,
|
|
22491
|
-
});
|
|
22492
|
-
if (!auth) return;
|
|
22493
|
-
} else if (!requireAgentCapability(claims, agent_id, "can_chat", res)) {
|
|
22494
|
-
return;
|
|
22495
|
-
}
|
|
22496
23568
|
|
|
22497
23569
|
let limit;
|
|
22498
23570
|
let afterSeq;
|
|
@@ -22510,29 +23582,35 @@ const server = createServer(async (req, res) => {
|
|
|
22510
23582
|
let syncResult;
|
|
22511
23583
|
try {
|
|
22512
23584
|
const binding = requireRouteCMatrixRoomBinding(session_id);
|
|
22513
|
-
if (binding.host_agent_id !== agent_id) {
|
|
23585
|
+
if (binding.host_agent_id !== agent_id || binding.host_session_id !== session_id) {
|
|
22514
23586
|
return respond(res, 403, {
|
|
22515
23587
|
error: "Matrix transcript read binding does not match agent",
|
|
22516
23588
|
committed_transcript_truth: "matrix_room_timeline",
|
|
22517
23589
|
host_db_transcript_product_truth: false,
|
|
22518
23590
|
});
|
|
22519
23591
|
}
|
|
22520
|
-
syncResult =
|
|
22521
|
-
|
|
22522
|
-
|
|
22523
|
-
|
|
22524
|
-
|
|
22525
|
-
|
|
22526
|
-
|
|
23592
|
+
syncResult = readTranscriptTarget(sessionTarget, binding, () => {
|
|
23593
|
+
if (recentTailRequested) return readMatrixTranscriptRecentTail({ binding, limit });
|
|
23594
|
+
if (sessionTarget.target_state === "stopped") return historyTranscriptPager.read({
|
|
23595
|
+
binding, rawLimit: url.searchParams.get("limit"), cursor: url.searchParams.get("next_batch"),
|
|
23596
|
+
afterSeqPresent: url.searchParams.has("after_seq"),
|
|
23597
|
+
readAfter: (options) => readMatrixTranscriptMessagesAfter({ ...options, includeReadCheckpoint: true }),
|
|
23598
|
+
});
|
|
23599
|
+
if (url.searchParams.has("next_batch")) throw Object.assign(new Error("Live messages use after_seq, not history next_batch"), { status: 400, reason: "live_messages_cursor_incompatible" });
|
|
23600
|
+
return readMatrixTranscriptMessagesAfter({ binding, afterSeq, limit });
|
|
23601
|
+
});
|
|
22527
23602
|
} catch (err) {
|
|
23603
|
+
if (respondTranscriptSourceError(res, err)) return;
|
|
23604
|
+
if (err.status && err.reason) return respond(res, err.status, { error: err.message, reason: err.reason });
|
|
22528
23605
|
if (isRouteCMatrixBindingNotFoundError(err)) {
|
|
22529
23606
|
return respond(res, 404, {
|
|
22530
23607
|
error: "Matrix transcript read binding not available for session",
|
|
23608
|
+
reason: "matrix_transcript_binding_unavailable",
|
|
22531
23609
|
committed_transcript_truth: "matrix_room_timeline",
|
|
22532
23610
|
host_db_transcript_product_truth: false,
|
|
22533
23611
|
});
|
|
22534
23612
|
}
|
|
22535
|
-
|
|
23613
|
+
return respond(res, 503, { error: "Matrix transcript storage read failed", reason: "matrix_transcript_storage_unavailable", retryable: true });
|
|
22536
23614
|
}
|
|
22537
23615
|
|
|
22538
23616
|
return respond(res, 200, {
|
|
@@ -22550,7 +23628,7 @@ const server = createServer(async (req, res) => {
|
|
|
22550
23628
|
one_bounded_matrix_tail_read: true,
|
|
22551
23629
|
legacy_session_history_used: false,
|
|
22552
23630
|
}
|
|
22553
|
-
: { sync: syncResult.sync }),
|
|
23631
|
+
: { sync: syncResult.sync, ...(syncResult.page ? { page: syncResult.page, next_batch: syncResult.next_batch, pagination_contract: syncResult.pagination_contract } : {}) }),
|
|
22554
23632
|
committed_transcript_truth: "matrix_room_timeline",
|
|
22555
23633
|
host_db_transcript_product_truth: false,
|
|
22556
23634
|
transcript_surface_classification: "product_matrix_path",
|
|
@@ -22559,9 +23637,168 @@ const server = createServer(async (req, res) => {
|
|
|
22559
23637
|
product_legacy_transcript_caller_allowed: false,
|
|
22560
23638
|
matrix_transcript_read: syncResult.matrix_transcript_read,
|
|
22561
23639
|
...sessionRefResolutionResponseFields(sessionTarget.resolution),
|
|
23640
|
+
...sessionTranscriptTargetFields(sessionTarget),
|
|
22562
23641
|
});
|
|
22563
23642
|
}
|
|
22564
23643
|
|
|
23644
|
+
// ── POST /sessions/completed-message-read-visit ────────
|
|
23645
|
+
if (
|
|
23646
|
+
req.method === "POST" &&
|
|
23647
|
+
path === "/sessions/completed-message-read-visit"
|
|
23648
|
+
) {
|
|
23649
|
+
const body = await readBody(req);
|
|
23650
|
+
const sessionId = normalizeString(body.session_id);
|
|
23651
|
+
const matrixRoomId = normalizeString(body.matrix_room_id);
|
|
23652
|
+
const documentVisitId = normalizeString(body.document_visit_id);
|
|
23653
|
+
const collectionId = normalizeString(body.collection_id);
|
|
23654
|
+
const collectionRevision = Number(body.collection_revision);
|
|
23655
|
+
if (!sessionId || !matrixRoomId || !documentVisitId ||
|
|
23656
|
+
!collectionId ||
|
|
23657
|
+
!Number.isSafeInteger(collectionRevision)) {
|
|
23658
|
+
return respond(res, 400, {
|
|
23659
|
+
error:
|
|
23660
|
+
"P250 visit requires Session, room, document and collection identity",
|
|
23661
|
+
code: "completed_session_unread_visit_identity_required",
|
|
23662
|
+
});
|
|
23663
|
+
}
|
|
23664
|
+
const session = requireSessionCapability(
|
|
23665
|
+
claims,
|
|
23666
|
+
sessionId,
|
|
23667
|
+
"can_chat",
|
|
23668
|
+
res
|
|
23669
|
+
);
|
|
23670
|
+
if (!session) return;
|
|
23671
|
+
const binding = getRouteCMatrixRoomBinding(sessionId);
|
|
23672
|
+
const collection = sessionCollectionService.snapshot({ view: "running" });
|
|
23673
|
+
if (
|
|
23674
|
+
!binding ||
|
|
23675
|
+
binding.matrix_room_id !== matrixRoomId ||
|
|
23676
|
+
binding.host_agent_id !== session.agentId ||
|
|
23677
|
+
collection.collection_id !== collectionId ||
|
|
23678
|
+
collection.revision !== collectionRevision ||
|
|
23679
|
+
!collection.rows.some((row) => row.session_id === sessionId)
|
|
23680
|
+
) {
|
|
23681
|
+
return respond(res, 409, {
|
|
23682
|
+
error: "P250 visit binding or collection generation is stale",
|
|
23683
|
+
code: "completed_session_unread_visit_generation_mismatch",
|
|
23684
|
+
});
|
|
23685
|
+
}
|
|
23686
|
+
try {
|
|
23687
|
+
const visit = beginCompletedSessionChatVisit(
|
|
23688
|
+
openRouteCMatrixSQLiteStore({
|
|
23689
|
+
jsonStoragePath: getRouteCMatrixStoragePath(),
|
|
23690
|
+
}),
|
|
23691
|
+
{
|
|
23692
|
+
hostUserId: HOST_OWNER_READ_CURSOR_USER_ID,
|
|
23693
|
+
hostSessionId: sessionId,
|
|
23694
|
+
matrixRoomId,
|
|
23695
|
+
documentVisitId,
|
|
23696
|
+
hostGeneration: collection.collection_id,
|
|
23697
|
+
collectionRevision: collection.revision,
|
|
23698
|
+
bindingGeneration: binding.updated_at,
|
|
23699
|
+
authScope: normalizeString(claims.user_id) || "dashboard-admin",
|
|
23700
|
+
}
|
|
23701
|
+
);
|
|
23702
|
+
return respond(res, 200, {
|
|
23703
|
+
...visit,
|
|
23704
|
+
collection_id: collection.collection_id,
|
|
23705
|
+
collection_revision: collection.revision,
|
|
23706
|
+
host_validated_binding: true,
|
|
23707
|
+
});
|
|
23708
|
+
} catch (err) {
|
|
23709
|
+
return respond(res, 409, {
|
|
23710
|
+
error: err instanceof Error ? err.message : String(err),
|
|
23711
|
+
code:
|
|
23712
|
+
normalizeString(err?.code) ||
|
|
23713
|
+
"completed_session_unread_visit_rejected",
|
|
23714
|
+
});
|
|
23715
|
+
}
|
|
23716
|
+
}
|
|
23717
|
+
|
|
23718
|
+
// ── POST /sessions/completed-message-mark-unread ──────
|
|
23719
|
+
if (
|
|
23720
|
+
req.method === "POST" &&
|
|
23721
|
+
path === "/sessions/completed-message-mark-unread"
|
|
23722
|
+
) {
|
|
23723
|
+
const body = await readBody(req);
|
|
23724
|
+
const sessionId = normalizeString(body.session_id);
|
|
23725
|
+
const matrixRoomId = normalizeString(body.matrix_room_id);
|
|
23726
|
+
const collectionId = normalizeString(body.collection_id);
|
|
23727
|
+
const collectionRevision = Number(body.collection_revision);
|
|
23728
|
+
if (
|
|
23729
|
+
!sessionId ||
|
|
23730
|
+
!matrixRoomId ||
|
|
23731
|
+
!collectionId ||
|
|
23732
|
+
!Number.isSafeInteger(collectionRevision)
|
|
23733
|
+
) {
|
|
23734
|
+
return respond(res, 400, {
|
|
23735
|
+
error:
|
|
23736
|
+
"Mark as unread requires Session, room, and collection identity",
|
|
23737
|
+
code: "completed_session_mark_unread_identity_required",
|
|
23738
|
+
});
|
|
23739
|
+
}
|
|
23740
|
+
const session = requireSessionCapability(
|
|
23741
|
+
claims,
|
|
23742
|
+
sessionId,
|
|
23743
|
+
"can_chat",
|
|
23744
|
+
res
|
|
23745
|
+
);
|
|
23746
|
+
if (!session) return;
|
|
23747
|
+
const binding = getRouteCMatrixRoomBinding(sessionId);
|
|
23748
|
+
const collection = sessionCollectionService.snapshot({ view: "running" });
|
|
23749
|
+
if (
|
|
23750
|
+
!binding ||
|
|
23751
|
+
binding.matrix_room_id !== matrixRoomId ||
|
|
23752
|
+
binding.host_agent_id !== session.agentId ||
|
|
23753
|
+
collection.collection_id !== collectionId ||
|
|
23754
|
+
collection.revision !== collectionRevision ||
|
|
23755
|
+
!collection.rows.some((row) => row.session_id === sessionId)
|
|
23756
|
+
) {
|
|
23757
|
+
return respond(res, 409, {
|
|
23758
|
+
error: "Mark as unread binding or live Session collection is stale",
|
|
23759
|
+
code: "completed_session_mark_unread_generation_mismatch",
|
|
23760
|
+
});
|
|
23761
|
+
}
|
|
23762
|
+
try {
|
|
23763
|
+
const liveBindings = sessionListProjectionService.liveBindings(
|
|
23764
|
+
sessionManager.list()
|
|
23765
|
+
);
|
|
23766
|
+
const result = markLatestCompletedSessionUnread(
|
|
23767
|
+
openRouteCMatrixSQLiteStore({
|
|
23768
|
+
jsonStoragePath: getRouteCMatrixStoragePath(),
|
|
23769
|
+
}),
|
|
23770
|
+
{
|
|
23771
|
+
hostUserId: HOST_OWNER_READ_CURSOR_USER_ID,
|
|
23772
|
+
hostSessionId: sessionId,
|
|
23773
|
+
matrixRoomId,
|
|
23774
|
+
bindings: liveBindings,
|
|
23775
|
+
unreadScope: "live",
|
|
23776
|
+
includeUnreadProjection: true,
|
|
23777
|
+
}
|
|
23778
|
+
);
|
|
23779
|
+
return respond(res, 200, {
|
|
23780
|
+
...result,
|
|
23781
|
+
collection_id: collection.collection_id,
|
|
23782
|
+
collection_revision: collection.revision,
|
|
23783
|
+
host_validated_binding: true,
|
|
23784
|
+
host_validated_latest_successful_final: true,
|
|
23785
|
+
});
|
|
23786
|
+
} catch (err) {
|
|
23787
|
+
const code =
|
|
23788
|
+
normalizeString(err?.code) ||
|
|
23789
|
+
"completed_session_mark_unread_rejected";
|
|
23790
|
+
const status =
|
|
23791
|
+
code === "completed_session_unread_binding_mismatch" ||
|
|
23792
|
+
code === "completed_session_unread_room_not_found"
|
|
23793
|
+
? 403
|
|
23794
|
+
: 409;
|
|
23795
|
+
return respond(res, status, {
|
|
23796
|
+
error: err instanceof Error ? err.message : String(err),
|
|
23797
|
+
code,
|
|
23798
|
+
});
|
|
23799
|
+
}
|
|
23800
|
+
}
|
|
23801
|
+
|
|
22565
23802
|
// ── POST /sessions/completed-message-read-ack ──────────
|
|
22566
23803
|
if (
|
|
22567
23804
|
req.method === "POST" &&
|
|
@@ -22585,10 +23822,13 @@ const server = createServer(async (req, res) => {
|
|
|
22585
23822
|
);
|
|
22586
23823
|
if (!session) return;
|
|
22587
23824
|
const binding = getRouteCMatrixRoomBinding(sessionId);
|
|
23825
|
+
const collection = sessionCollectionService.snapshot({ view: "running" });
|
|
22588
23826
|
if (
|
|
22589
23827
|
!binding ||
|
|
22590
23828
|
binding.matrix_room_id !== matrixRoomId ||
|
|
22591
|
-
binding.host_agent_id !== session.agentId
|
|
23829
|
+
binding.host_agent_id !== session.agentId ||
|
|
23830
|
+
collection.collection_id !== normalizeString(body.collection_id) ||
|
|
23831
|
+
collection.revision !== Number(body.collection_revision)
|
|
22592
23832
|
) {
|
|
22593
23833
|
return respond(res, 403, {
|
|
22594
23834
|
error: "Completed-message acknowledgement binding does not match the current Chat",
|
|
@@ -22597,18 +23837,38 @@ const server = createServer(async (req, res) => {
|
|
|
22597
23837
|
}
|
|
22598
23838
|
if (
|
|
22599
23839
|
body.acknowledgement_contract !==
|
|
22600
|
-
"routec.
|
|
23840
|
+
"routec.p250_latest_timeline_read_ack.v1" ||
|
|
22601
23841
|
body.materially_rendered !== true ||
|
|
22602
|
-
body.
|
|
23842
|
+
body.latest_timeline_loaded !== true ||
|
|
22603
23843
|
body.document_visible !== true ||
|
|
22604
|
-
body.document_focused !== true
|
|
23844
|
+
body.document_focused !== true ||
|
|
23845
|
+
!normalizeString(body.visit_proof) ||
|
|
23846
|
+
!normalizeString(body.host_generation) ||
|
|
23847
|
+
!normalizeString(body.binding_generation)
|
|
22605
23848
|
) {
|
|
22606
23849
|
return respond(res, 400, {
|
|
22607
|
-
error:
|
|
22608
|
-
|
|
23850
|
+
error:
|
|
23851
|
+
"Completed-message acknowledgement requires a qualified P250 visit and latest timeline proof",
|
|
23852
|
+
code: "completed_session_unread_readiness_proof_required",
|
|
23853
|
+
});
|
|
23854
|
+
}
|
|
23855
|
+
const unreadScope = normalizeString(body.unread_scope);
|
|
23856
|
+
const includeUnreadProjection = body.include_unread_projection === true;
|
|
23857
|
+
if (
|
|
23858
|
+
(unreadScope && unreadScope !== "live") ||
|
|
23859
|
+
(body.include_unread_projection !== undefined &&
|
|
23860
|
+
typeof body.include_unread_projection !== "boolean") ||
|
|
23861
|
+
(includeUnreadProjection && unreadScope !== "live")
|
|
23862
|
+
) {
|
|
23863
|
+
return respond(res, 400, {
|
|
23864
|
+
error: "Completed-message acknowledgement unread projection requires unread_scope live",
|
|
23865
|
+
code: "invalid_session_unread_scope",
|
|
22609
23866
|
});
|
|
22610
23867
|
}
|
|
22611
23868
|
try {
|
|
23869
|
+
const liveBindings = unreadScope === "live"
|
|
23870
|
+
? sessionListProjectionService.liveBindings(sessionManager.list())
|
|
23871
|
+
: null;
|
|
22612
23872
|
const acknowledgement = acknowledgeCompletedSessionRead(
|
|
22613
23873
|
openRouteCMatrixSQLiteStore({
|
|
22614
23874
|
jsonStoragePath: getRouteCMatrixStoragePath(),
|
|
@@ -22618,12 +23878,21 @@ const server = createServer(async (req, res) => {
|
|
|
22618
23878
|
hostSessionId: sessionId,
|
|
22619
23879
|
matrixRoomId,
|
|
22620
23880
|
eventId,
|
|
23881
|
+
bindings: liveBindings,
|
|
23882
|
+
unreadScope: unreadScope || null,
|
|
23883
|
+
includeUnreadProjection,
|
|
23884
|
+
visitProof: body.visit_proof,
|
|
23885
|
+
hostGeneration: body.host_generation,
|
|
23886
|
+
collectionRevision: Number(body.collection_revision),
|
|
23887
|
+
bindingGeneration: body.binding_generation,
|
|
23888
|
+
authScope: normalizeString(claims.user_id) || "dashboard-admin",
|
|
22621
23889
|
}
|
|
22622
23890
|
);
|
|
22623
23891
|
return respond(res, 200, {
|
|
22624
23892
|
...acknowledgement,
|
|
22625
|
-
|
|
22626
|
-
|
|
23893
|
+
collection_id: collection.collection_id,
|
|
23894
|
+
collection_revision: collection.revision,
|
|
23895
|
+
acknowledgement_contract: "routec.p250_latest_timeline_read_ack.v1",
|
|
22627
23896
|
host_validated_binding: true,
|
|
22628
23897
|
host_validated_latest_successful_final: true,
|
|
22629
23898
|
});
|
|
@@ -22641,15 +23910,41 @@ const server = createServer(async (req, res) => {
|
|
|
22641
23910
|
}
|
|
22642
23911
|
}
|
|
22643
23912
|
|
|
23913
|
+
// ── GET /sessions/collection ───────────────────────────
|
|
23914
|
+
if (req.method === "GET" && path === "/sessions/collection") {
|
|
23915
|
+
if (!requireDashboardMode(claims, res)) return;
|
|
23916
|
+
const view = url.searchParams.get("view") === "running" ? "running" : "all";
|
|
23917
|
+
commitSessionCollectionChange(["read_reconcile"]);
|
|
23918
|
+
const sinceText = url.searchParams.get("since_revision");
|
|
23919
|
+
if (sinceText === null) {
|
|
23920
|
+
return respond(res, 200, sessionCollectionService.snapshot({ view }));
|
|
23921
|
+
}
|
|
23922
|
+
const sinceRevision = Number(sinceText);
|
|
23923
|
+
if (!Number.isSafeInteger(sinceRevision) || sinceRevision < 0) {
|
|
23924
|
+
return respond(res, 400, {
|
|
23925
|
+
error: "since_revision must be a non-negative integer",
|
|
23926
|
+
code: "invalid_session_collection_revision",
|
|
23927
|
+
});
|
|
23928
|
+
}
|
|
23929
|
+
return respond(res, 200, sessionCollectionService.delta({
|
|
23930
|
+
sinceRevision,
|
|
23931
|
+
collectionId: normalizeString(url.searchParams.get("collection_id")),
|
|
23932
|
+
view,
|
|
23933
|
+
}));
|
|
23934
|
+
}
|
|
23935
|
+
|
|
22644
23936
|
// ── GET /sessions/previews ─────────────────────────────
|
|
22645
23937
|
if (req.method === "GET" && path === "/sessions/previews") {
|
|
22646
23938
|
if (!requireDashboardMode(claims, res)) return;
|
|
22647
23939
|
const liveSessions = sessionManager.list();
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
|
|
22652
|
-
)
|
|
23940
|
+
// P236 U1 / docs/routec/dev/session_list_progressive_loading.md:
|
|
23941
|
+
// Dashboard uses /sessions?include_previews=0 plus History/catalog.
|
|
23942
|
+
// Chat needs only live names first, then one P032 decoration batch;
|
|
23943
|
+
// never reconnect this metadata response to historical decoration reads.
|
|
23944
|
+
if (url.searchParams.get("include_session_switcher") === "1" &&
|
|
23945
|
+
url.searchParams.get("view") === "metadata") {
|
|
23946
|
+
return respond(res, 200, sessionListProjectionService.metadata(liveSessions));
|
|
23947
|
+
}
|
|
22653
23948
|
const entriesBySessionId = new Map();
|
|
22654
23949
|
for (const session of liveSessions) {
|
|
22655
23950
|
const sessionId = session.id || session.sessionId;
|
|
@@ -22662,8 +23957,6 @@ const server = createServer(async (req, res) => {
|
|
|
22662
23957
|
if (
|
|
22663
23958
|
record.session_id &&
|
|
22664
23959
|
record.agent_id &&
|
|
22665
|
-
record.agent_folder &&
|
|
22666
|
-
existsSync(record.agent_folder) &&
|
|
22667
23960
|
!entriesBySessionId.has(record.session_id)
|
|
22668
23961
|
) {
|
|
22669
23962
|
entriesBySessionId.set(record.session_id, {
|
|
@@ -22673,71 +23966,20 @@ const server = createServer(async (req, res) => {
|
|
|
22673
23966
|
}
|
|
22674
23967
|
}
|
|
22675
23968
|
|
|
22676
|
-
const
|
|
22677
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
bindingBySessionId.set(entry.sessionId, binding);
|
|
22683
|
-
if (!seenRoomIds.has(binding.matrix_room_id)) {
|
|
22684
|
-
seenRoomIds.add(binding.matrix_room_id);
|
|
22685
|
-
bindings.push(binding);
|
|
22686
|
-
}
|
|
23969
|
+
const unreadScope = normalizeString(url.searchParams.get("unread_scope"));
|
|
23970
|
+
if (unreadScope && unreadScope !== "live") {
|
|
23971
|
+
return respond(res, 400, {
|
|
23972
|
+
error: "unread_scope must be live when provided",
|
|
23973
|
+
code: "invalid_session_unread_scope",
|
|
23974
|
+
});
|
|
22687
23975
|
}
|
|
22688
|
-
const
|
|
22689
|
-
|
|
22690
|
-
|
|
22691
|
-
|
|
22692
|
-
|
|
22693
|
-
|
|
22694
|
-
|
|
22695
|
-
bindings,
|
|
22696
|
-
hostUserId: HOST_OWNER_READ_CURSOR_USER_ID,
|
|
22697
|
-
}
|
|
22698
|
-
);
|
|
22699
|
-
const previews = [];
|
|
22700
|
-
for (const entry of entriesBySessionId.values()) {
|
|
22701
|
-
const binding = bindingBySessionId.get(entry.sessionId) || null;
|
|
22702
|
-
if (!binding) {
|
|
22703
|
-
previews.push({
|
|
22704
|
-
session_id: entry.sessionId,
|
|
22705
|
-
status: "no_matrix_binding",
|
|
22706
|
-
last_message_preview: null,
|
|
22707
|
-
latest_committed_seq: 0,
|
|
22708
|
-
latest_completed_event_id: null,
|
|
22709
|
-
latest_completed_stream_seq: 0,
|
|
22710
|
-
read_through_stream_seq: 0,
|
|
22711
|
-
has_unread_completed_message: false,
|
|
22712
|
-
cache_status: "not_applicable",
|
|
22713
|
-
error_code: null,
|
|
22714
|
-
});
|
|
22715
|
-
continue;
|
|
22716
|
-
}
|
|
22717
|
-
const result = resultByRoomId.get(binding.matrix_room_id);
|
|
22718
|
-
const unread =
|
|
22719
|
-
unreadProjection.resultBySessionId.get(entry.sessionId) || {};
|
|
22720
|
-
const isLiveSession = liveSessionIds.has(entry.sessionId);
|
|
22721
|
-
previews.push({
|
|
22722
|
-
session_id: entry.sessionId,
|
|
22723
|
-
status: result.status,
|
|
22724
|
-
last_message_preview: result.last_message_preview,
|
|
22725
|
-
latest_committed_seq: result.latest_committed_seq,
|
|
22726
|
-
latest_completed_event_id:
|
|
22727
|
-
unread.latest_completed_event_id || null,
|
|
22728
|
-
latest_completed_stream_seq:
|
|
22729
|
-
Number(unread.latest_completed_stream_seq) || 0,
|
|
22730
|
-
read_through_stream_seq:
|
|
22731
|
-
Number(unread.read_through_stream_seq) || 0,
|
|
22732
|
-
has_unread_completed_message:
|
|
22733
|
-
isLiveSession && unread.has_unread_completed_message === true,
|
|
22734
|
-
cache_status: result.cache_status,
|
|
22735
|
-
error_code: result.error_code,
|
|
22736
|
-
});
|
|
22737
|
-
}
|
|
22738
|
-
const liveUnreadSessionCount = previews.filter(
|
|
22739
|
-
(preview) => preview.has_unread_completed_message === true
|
|
22740
|
-
).length;
|
|
23976
|
+
const { previews, sessionRows, uniqueRoomCount,
|
|
23977
|
+
unreadSchemaVersion, liveUnreadSessionCount } =
|
|
23978
|
+
sessionListProjectionService.decorate(
|
|
23979
|
+
[...entriesBySessionId.values()],
|
|
23980
|
+
liveSessions,
|
|
23981
|
+
{ unreadScope: unreadScope || "all" }
|
|
23982
|
+
);
|
|
22741
23983
|
const includeSessionSwitcher =
|
|
22742
23984
|
url.searchParams.get("include_session_switcher") === "1";
|
|
22743
23985
|
if (!includeSessionSwitcher) {
|
|
@@ -22745,10 +23987,10 @@ const server = createServer(async (req, res) => {
|
|
|
22745
23987
|
status: "session_list_previews",
|
|
22746
23988
|
previews,
|
|
22747
23989
|
session_count: entriesBySessionId.size,
|
|
22748
|
-
unique_room_count:
|
|
23990
|
+
unique_room_count: uniqueRoomCount,
|
|
22749
23991
|
unread_session_count: liveUnreadSessionCount,
|
|
22750
23992
|
completed_session_unread_schema_version:
|
|
22751
|
-
|
|
23993
|
+
unreadSchemaVersion,
|
|
22752
23994
|
dedicated_preview_query: true,
|
|
22753
23995
|
full_transcript_pagination_used: false,
|
|
22754
23996
|
room_aggregate_query_used: false,
|
|
@@ -22757,23 +23999,15 @@ const server = createServer(async (req, res) => {
|
|
|
22757
23999
|
committed_transcript_truth: "matrix_room_timeline",
|
|
22758
24000
|
});
|
|
22759
24001
|
}
|
|
22760
|
-
const previewBySessionId = new Map(
|
|
22761
|
-
previews.map((preview) => [preview.session_id, preview])
|
|
22762
|
-
);
|
|
22763
|
-
const sessionRows = buildSessionSwitcherRows({
|
|
22764
|
-
liveSessions,
|
|
22765
|
-
previewBySessionId,
|
|
22766
|
-
bindingBySessionId,
|
|
22767
|
-
});
|
|
22768
24002
|
return respond(res, 200, {
|
|
22769
24003
|
status: "session_list_previews",
|
|
22770
24004
|
previews,
|
|
22771
24005
|
session_rows: sessionRows,
|
|
22772
24006
|
session_count: sessionRows.length,
|
|
22773
|
-
unique_room_count:
|
|
24007
|
+
unique_room_count: uniqueRoomCount,
|
|
22774
24008
|
unread_session_count: liveUnreadSessionCount,
|
|
22775
24009
|
completed_session_unread_schema_version:
|
|
22776
|
-
|
|
24010
|
+
unreadSchemaVersion,
|
|
22777
24011
|
dedicated_preview_query: true,
|
|
22778
24012
|
full_transcript_pagination_used: false,
|
|
22779
24013
|
room_aggregate_query_used: false,
|
|
@@ -22785,28 +24019,65 @@ const server = createServer(async (req, res) => {
|
|
|
22785
24019
|
session_switcher_projection_batched: true,
|
|
22786
24020
|
session_switcher_per_row_http_requests: 0,
|
|
22787
24021
|
session_switcher_matrix_query_count: 0,
|
|
24022
|
+
...(unreadScope === "live"
|
|
24023
|
+
? {
|
|
24024
|
+
unread_scope: "live",
|
|
24025
|
+
unread_projection_contract:
|
|
24026
|
+
"routec.p236_live_completed_session_unread_projection.v1",
|
|
24027
|
+
}
|
|
24028
|
+
: {}),
|
|
22788
24029
|
});
|
|
22789
24030
|
}
|
|
22790
24031
|
|
|
22791
24032
|
// ── GET /sessions/history ──────────────────────────────
|
|
22792
24033
|
if (req.method === "GET" && path === "/sessions/history") {
|
|
22793
24034
|
const includePreviews = shouldIncludeSessionListPreviews(url);
|
|
22794
|
-
const
|
|
22795
|
-
|
|
22796
|
-
|
|
22797
|
-
|
|
22798
|
-
|
|
22799
|
-
|
|
22800
|
-
|
|
22801
|
-
|
|
22802
|
-
|
|
22803
|
-
|
|
22804
|
-
|
|
22805
|
-
|
|
22806
|
-
|
|
24035
|
+
const canonicalHistoryBySessionId = new Map(
|
|
24036
|
+
buildCanonicalSessionCollectionRows()
|
|
24037
|
+
.filter((row) => row.kind === "history")
|
|
24038
|
+
.map((row) => [row.session_id, row])
|
|
24039
|
+
);
|
|
24040
|
+
const eligible = getSessionHistory();
|
|
24041
|
+
const previews = includePreviews ? sessionListProjectionService.previewOnly(
|
|
24042
|
+
eligible.map((s) => ({ sessionId: s.session_id, agentId: s.agent_id }))
|
|
24043
|
+
) : null;
|
|
24044
|
+
const history = eligible.map((s) => ({
|
|
24045
|
+
...s,
|
|
24046
|
+
restore_failed:
|
|
24047
|
+
canonicalHistoryBySessionId.get(s.session_id)?.restore_failed === true,
|
|
24048
|
+
restore_failed_reason:
|
|
24049
|
+
canonicalHistoryBySessionId.get(s.session_id)?.restore_failed_reason || null,
|
|
24050
|
+
manual_start_supported:
|
|
24051
|
+
canonicalHistoryBySessionId.get(s.session_id)?.manual_start_supported === true,
|
|
24052
|
+
manual_start_setup_reason:
|
|
24053
|
+
canonicalHistoryBySessionId.get(s.session_id)?.manual_start_setup_reason || null,
|
|
24054
|
+
resume_supported:
|
|
24055
|
+
canonicalHistoryBySessionId.get(s.session_id)?.resume_supported === true,
|
|
24056
|
+
resume_unavailable_reason:
|
|
24057
|
+
canonicalHistoryBySessionId.get(s.session_id)?.resume_unavailable_reason || null,
|
|
24058
|
+
...(includePreviews ? { last_message_preview: previews.get(s.session_id) ?? null } : {}),
|
|
24059
|
+
}));
|
|
22807
24060
|
return respond(res, 200, { sessions: history });
|
|
22808
24061
|
}
|
|
22809
24062
|
|
|
24063
|
+
// ── GET /sessions/tags/recent ──────────────────────────
|
|
24064
|
+
if (req.method === "GET" && path === "/sessions/tags/recent") {
|
|
24065
|
+
if (!requireDashboardMode(claims, res)) return;
|
|
24066
|
+
try {
|
|
24067
|
+
return respond(res, 200, {
|
|
24068
|
+
status: "recent_session_tags",
|
|
24069
|
+
tags: new RecentSessionTagsStore(getConfigDir()).read(),
|
|
24070
|
+
bounded: true,
|
|
24071
|
+
host_local: true,
|
|
24072
|
+
});
|
|
24073
|
+
} catch (err) {
|
|
24074
|
+
return respond(res, 500, {
|
|
24075
|
+
error: err.message || String(err),
|
|
24076
|
+
code: "recent_session_tags_unavailable",
|
|
24077
|
+
});
|
|
24078
|
+
}
|
|
24079
|
+
}
|
|
24080
|
+
|
|
22810
24081
|
// ── GET /sessions/default-name?agent_id=…&mode=branch&source_session_id=… ──
|
|
22811
24082
|
// Returns the counter-based `${parent}-branch-N` default a subsequent
|
|
22812
24083
|
// `/sessions/resume` branch would resolve, WITHOUT bumping the counter.
|
|
@@ -22944,9 +24215,13 @@ const server = createServer(async (req, res) => {
|
|
|
22944
24215
|
|
|
22945
24216
|
const transcripts = [];
|
|
22946
24217
|
const resumeDispositions = [];
|
|
22947
|
-
|
|
22948
|
-
|
|
22949
|
-
|
|
24218
|
+
const records = getSessionHistory().filter(
|
|
24219
|
+
(record) => record.agent_id === agent_id && record.session_id
|
|
24220
|
+
);
|
|
24221
|
+
// R3 batches only the binding read; P210 timeline replay remains truth.
|
|
24222
|
+
const bindings = getRouteCMatrixRoomBindings(records.map((r) => r.session_id));
|
|
24223
|
+
for (const record of records) {
|
|
24224
|
+
const binding = bindings.get(record.session_id);
|
|
22950
24225
|
let matrixTranscriptAvailable = false;
|
|
22951
24226
|
let transcript = null;
|
|
22952
24227
|
try {
|
|
@@ -22994,6 +24269,7 @@ const server = createServer(async (req, res) => {
|
|
|
22994
24269
|
|
|
22995
24270
|
// ── POST /sessions/resume ──────────────────────────────
|
|
22996
24271
|
if (req.method === "POST" && path === "/sessions/resume") {
|
|
24272
|
+
try { assertSessionRosterMutationAvailable(); } catch (error) { return respond(res, 409, { error: error.message, code: error.code }); }
|
|
22997
24273
|
const body = await readBody(req);
|
|
22998
24274
|
if (rejectRetiredCapabilitySettingInput(res, body)) return;
|
|
22999
24275
|
const { source_session_id, agent_id, agent_folder, session_name } = body;
|
|
@@ -23028,6 +24304,14 @@ const server = createServer(async (req, res) => {
|
|
|
23028
24304
|
error: "Resume source agent does not match requested agent",
|
|
23029
24305
|
});
|
|
23030
24306
|
}
|
|
24307
|
+
let childTags;
|
|
24308
|
+
try {
|
|
24309
|
+
childTags = Object.prototype.hasOwnProperty.call(body, "tags")
|
|
24310
|
+
? normalizeSessionTags(body.tags)
|
|
24311
|
+
: normalizeSessionTags([sourceRecord.session_name]);
|
|
24312
|
+
} catch (err) {
|
|
24313
|
+
return respond(res, 400, { error: err.message, code: err.code });
|
|
24314
|
+
}
|
|
23031
24315
|
|
|
23032
24316
|
if (!sourceRecord.agent_folder) {
|
|
23033
24317
|
return respond(res, 400, {
|
|
@@ -23202,11 +24486,14 @@ const server = createServer(async (req, res) => {
|
|
|
23202
24486
|
});
|
|
23203
24487
|
let session;
|
|
23204
24488
|
try {
|
|
24489
|
+
assertSessionRosterMutationAvailable();
|
|
23205
24490
|
session = sessionManager.start({
|
|
23206
24491
|
agentId: agent_id,
|
|
23207
24492
|
cwd: resolvedFolder,
|
|
23208
24493
|
sessionId: childSessionId,
|
|
23209
24494
|
sessionName: resolvedSessionName,
|
|
24495
|
+
tags: childTags,
|
|
24496
|
+
deferTagsPersistence: true,
|
|
23210
24497
|
provider: resolved.runtime.provider,
|
|
23211
24498
|
parentSessionId: sourceSessionId,
|
|
23212
24499
|
resumeSessionId: forkSourceProviderMetadata.providerResumeId,
|
|
@@ -23341,11 +24628,13 @@ const server = createServer(async (req, res) => {
|
|
|
23341
24628
|
recordTelemetryCounter("session_branch_resumed_count");
|
|
23342
24629
|
recordTelemetryFeature("sessions");
|
|
23343
24630
|
recordTelemetryFeature("chat");
|
|
23344
|
-
|
|
24631
|
+
const persistedTags = canonicalSessionTags(session);
|
|
24632
|
+
const response = {
|
|
23345
24633
|
success: true,
|
|
23346
24634
|
session_id: session.id,
|
|
23347
24635
|
session_name: session.sessionName,
|
|
23348
24636
|
parent_session_id: sourceSessionId,
|
|
24637
|
+
tags: persistedTags,
|
|
23349
24638
|
agent_id,
|
|
23350
24639
|
provider: session.provider || "claude",
|
|
23351
24640
|
permission_mode: session.permissionMode ?? null,
|
|
@@ -23412,7 +24701,10 @@ const server = createServer(async (req, res) => {
|
|
|
23412
24701
|
bookkeeping_warning: bookkeepingWarnings.length
|
|
23413
24702
|
? bookkeepingWarnings.join("; ")
|
|
23414
24703
|
: null,
|
|
23415
|
-
}
|
|
24704
|
+
};
|
|
24705
|
+
commitSuccessfulSessionTags(session, { changeKinds: ["branch_resume", "create"] });
|
|
24706
|
+
refreshSessionBookkeepingWarning(response, session);
|
|
24707
|
+
return respond(res, 200, response);
|
|
23416
24708
|
}
|
|
23417
24709
|
|
|
23418
24710
|
// ── POST /session/resume ───────────────────────────────
|
|
@@ -23420,6 +24712,7 @@ const server = createServer(async (req, res) => {
|
|
|
23420
24712
|
// therefore the Route C Matrix room binding key. Branch/fork creation stays
|
|
23421
24713
|
// on /sessions/resume.
|
|
23422
24714
|
if (req.method === "POST" && path === "/session/resume") {
|
|
24715
|
+
try { assertSessionRosterMutationAvailable(); } catch (error) { return respond(res, 409, { error: error.message, code: error.code }); }
|
|
23423
24716
|
const body = await readBody(req);
|
|
23424
24717
|
if (rejectRetiredCapabilitySettingInput(res, body)) return;
|
|
23425
24718
|
const {
|
|
@@ -23482,6 +24775,14 @@ const server = createServer(async (req, res) => {
|
|
|
23482
24775
|
error: "Resume source agent does not match requested agent",
|
|
23483
24776
|
});
|
|
23484
24777
|
}
|
|
24778
|
+
let resumedTags;
|
|
24779
|
+
try {
|
|
24780
|
+
resumedTags = Object.prototype.hasOwnProperty.call(body, "tags")
|
|
24781
|
+
? normalizeSessionTags(body.tags)
|
|
24782
|
+
: canonicalSessionTags(sourceRecord);
|
|
24783
|
+
} catch (err) {
|
|
24784
|
+
return respond(res, 400, { error: err.message, code: err.code });
|
|
24785
|
+
}
|
|
23485
24786
|
|
|
23486
24787
|
if (!sourceRecord.agent_folder) {
|
|
23487
24788
|
return respond(res, 400, {
|
|
@@ -23492,13 +24793,13 @@ const server = createServer(async (req, res) => {
|
|
|
23492
24793
|
let sourceCanonicalFolder;
|
|
23493
24794
|
let requestedCanonicalFolder;
|
|
23494
24795
|
try {
|
|
23495
|
-
sourceCanonicalFolder =
|
|
24796
|
+
sourceCanonicalFolder = requireAgentFolder(sourceRecord.agent_folder);
|
|
23496
24797
|
requestedCanonicalFolder = agent_folder
|
|
23497
24798
|
? realpathSync(agent_folder)
|
|
23498
24799
|
: sourceCanonicalFolder;
|
|
23499
24800
|
} catch (err) {
|
|
23500
24801
|
return respond(res, 400, {
|
|
23501
|
-
|
|
24802
|
+
...agentFolderErrorResponse(err),
|
|
23502
24803
|
});
|
|
23503
24804
|
}
|
|
23504
24805
|
if (sourceCanonicalFolder !== requestedCanonicalFolder) {
|
|
@@ -23697,6 +24998,12 @@ const server = createServer(async (req, res) => {
|
|
|
23697
24998
|
});
|
|
23698
24999
|
}
|
|
23699
25000
|
|
|
25001
|
+
// Recheck after all asynchronous preparation, before provider construction.
|
|
25002
|
+
try {
|
|
25003
|
+
requireAgentFolder(resolvedFolder);
|
|
25004
|
+
} catch (err) {
|
|
25005
|
+
return respond(res, 400, agentFolderErrorResponse(err));
|
|
25006
|
+
}
|
|
23700
25007
|
const sessionRuntimeCapabilities =
|
|
23701
25008
|
resolveRuntimeCapabilitiesForSessionStart(resolved.config, body);
|
|
23702
25009
|
const runtimeCapabilityGrant = buildLiveSessionRuntimeCapabilityGrant({
|
|
@@ -23706,11 +25013,14 @@ const server = createServer(async (req, res) => {
|
|
|
23706
25013
|
});
|
|
23707
25014
|
let session;
|
|
23708
25015
|
try {
|
|
25016
|
+
assertSessionRosterMutationAvailable();
|
|
23709
25017
|
session = sessionManager.start({
|
|
23710
25018
|
agentId: agent_id,
|
|
23711
25019
|
cwd: resolvedFolder,
|
|
23712
25020
|
sessionId: sourceSessionId,
|
|
23713
25021
|
sessionName: resolvedSessionName,
|
|
25022
|
+
tags: resumedTags,
|
|
25023
|
+
deferTagsPersistence: true,
|
|
23714
25024
|
provider: resolved.runtime.provider,
|
|
23715
25025
|
parentSessionId: sourceRecord.parent_session_id || null,
|
|
23716
25026
|
resumeSessionId: sourceProviderMetadata.providerResumeId,
|
|
@@ -23847,12 +25157,14 @@ const server = createServer(async (req, res) => {
|
|
|
23847
25157
|
recordTelemetryCounter("session_resumed_count");
|
|
23848
25158
|
recordTelemetryFeature("sessions");
|
|
23849
25159
|
recordTelemetryFeature("chat");
|
|
23850
|
-
|
|
25160
|
+
const persistedTags = canonicalSessionTags(session);
|
|
25161
|
+
const response = {
|
|
23851
25162
|
success: true,
|
|
23852
25163
|
session_id: session.id,
|
|
23853
25164
|
source_session_id: sourceSessionId,
|
|
23854
25165
|
session_name: session.sessionName,
|
|
23855
25166
|
parent_session_id: session.parentSessionId || null,
|
|
25167
|
+
tags: persistedTags,
|
|
23856
25168
|
agent_id,
|
|
23857
25169
|
provider: session.provider || "claude",
|
|
23858
25170
|
permission_mode: session.permissionMode ?? null,
|
|
@@ -23919,13 +25231,26 @@ const server = createServer(async (req, res) => {
|
|
|
23919
25231
|
bookkeeping_warning: bookkeepingWarnings.length
|
|
23920
25232
|
? bookkeepingWarnings.join("; ")
|
|
23921
25233
|
: null,
|
|
25234
|
+
};
|
|
25235
|
+
commitSuccessfulSessionTags(session, {
|
|
25236
|
+
promote: Object.prototype.hasOwnProperty.call(body, "tags"),
|
|
25237
|
+
restoreReady: true,
|
|
25238
|
+
changeKinds: ["resume"],
|
|
23922
25239
|
});
|
|
25240
|
+
refreshSessionBookkeepingWarning(response, session);
|
|
25241
|
+
return respond(res, 200, response);
|
|
23923
25242
|
}
|
|
23924
25243
|
|
|
23925
25244
|
// ── GET /sessions ──────────────────────────────────────
|
|
23926
25245
|
if (req.method === "GET" && path === "/sessions") {
|
|
23927
25246
|
const includePreviews = shouldIncludeSessionListPreviews(url);
|
|
23928
|
-
const allSessions = sessionManager.list()
|
|
25247
|
+
const allSessions = sessionManager.list().filter((session) => {
|
|
25248
|
+
const sessionId = session.id || session.sessionId;
|
|
25249
|
+
return (
|
|
25250
|
+
sessionManager.getDesiredOpenRestoreOutcome(sessionId)?.state !==
|
|
25251
|
+
"failed"
|
|
25252
|
+
);
|
|
25253
|
+
});
|
|
23929
25254
|
let accessible;
|
|
23930
25255
|
if (isRuntimeCapabilityOnly(claims)) {
|
|
23931
25256
|
const auth = requireRuntimeCapability(claims, res, {
|
|
@@ -23938,7 +25263,11 @@ const server = createServer(async (req, res) => {
|
|
|
23938
25263
|
} else {
|
|
23939
25264
|
accessible = allSessions.filter((s) => claims.agent_ids.includes(s.agentId));
|
|
23940
25265
|
}
|
|
25266
|
+
const previews = includePreviews ? sessionListProjectionService.previewOnly(
|
|
25267
|
+
accessible.map((s) => ({ sessionId: s.id || s.sessionId, agentId: s.agentId }))
|
|
25268
|
+
) : null;
|
|
23941
25269
|
return respond(res, 200, {
|
|
25270
|
+
desired_open_restore: buildDesiredOpenRestoreStatusPayload(),
|
|
23942
25271
|
sessions: accessible.map((session) => {
|
|
23943
25272
|
const sessionId = session.id || session.sessionId;
|
|
23944
25273
|
const liveSession = sessionId
|
|
@@ -23976,11 +25305,7 @@ const server = createServer(async (req, res) => {
|
|
|
23976
25305
|
),
|
|
23977
25306
|
};
|
|
23978
25307
|
if (includePreviews) {
|
|
23979
|
-
row.lastMessagePreview =
|
|
23980
|
-
session.cwd,
|
|
23981
|
-
session.agentId,
|
|
23982
|
-
sessionId
|
|
23983
|
-
);
|
|
25308
|
+
row.lastMessagePreview = previews.get(sessionId) ?? null;
|
|
23984
25309
|
}
|
|
23985
25310
|
return row;
|
|
23986
25311
|
}),
|
|
@@ -24027,6 +25352,7 @@ const server = createServer(async (req, res) => {
|
|
|
24027
25352
|
|
|
24028
25353
|
const notificationWss = new WebSocketServer({ noServer: true });
|
|
24029
25354
|
const mailNotificationWss = new WebSocketServer({ noServer: true });
|
|
25355
|
+
const sessionCollectionWss = new WebSocketServer({ noServer: true });
|
|
24030
25356
|
const wss = new WebSocketServer({ noServer: true });
|
|
24031
25357
|
|
|
24032
25358
|
server.on("upgrade", (req, socket, head) => {
|
|
@@ -24043,6 +25369,12 @@ server.on("upgrade", (req, socket, head) => {
|
|
|
24043
25369
|
});
|
|
24044
25370
|
return;
|
|
24045
25371
|
}
|
|
25372
|
+
if (url.pathname === "/sessions/collection/stream") {
|
|
25373
|
+
sessionCollectionWss.handleUpgrade(req, socket, head, (ws) => {
|
|
25374
|
+
sessionCollectionWss.emit("connection", ws, req);
|
|
25375
|
+
});
|
|
25376
|
+
return;
|
|
25377
|
+
}
|
|
24046
25378
|
if (url.pathname === "/session/stream") {
|
|
24047
25379
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
24048
25380
|
wss.emit("connection", ws, req);
|
|
@@ -24134,6 +25466,27 @@ mailNotificationWss.on("connection", (ws, req) => {
|
|
|
24134
25466
|
});
|
|
24135
25467
|
});
|
|
24136
25468
|
|
|
25469
|
+
sessionCollectionWss.on("connection", (ws, req) => {
|
|
25470
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
25471
|
+
const claims = authenticateDashboardNotificationWebSocket(req, url);
|
|
25472
|
+
if (!claims?._dashboardAuth) {
|
|
25473
|
+
ws.send(JSON.stringify({ type: "error", error: "Dashboard authentication required" }));
|
|
25474
|
+
ws.close(4001, "Authentication failed");
|
|
25475
|
+
return;
|
|
25476
|
+
}
|
|
25477
|
+
commitSessionCollectionChange(["feed_handshake"]);
|
|
25478
|
+
sessionCollectionSubscribers.add(ws);
|
|
25479
|
+
const diagnostics = sessionCollectionService.diagnostics();
|
|
25480
|
+
ws.send(JSON.stringify({
|
|
25481
|
+
type: "sessions.collection.status",
|
|
25482
|
+
collection_id: diagnostics.collection_id,
|
|
25483
|
+
revision: diagnostics.revision,
|
|
25484
|
+
active: true,
|
|
25485
|
+
}));
|
|
25486
|
+
ws.on("close", () => sessionCollectionSubscribers.delete(ws));
|
|
25487
|
+
ws.on("error", () => sessionCollectionSubscribers.delete(ws));
|
|
25488
|
+
});
|
|
25489
|
+
|
|
24137
25490
|
wss.on("connection", (ws, req) => {
|
|
24138
25491
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
24139
25492
|
const sessionId = url.searchParams.get("session_id");
|
|
@@ -24795,6 +26148,262 @@ async function restoreTelegramListenersAfterHostRestart({
|
|
|
24795
26148
|
};
|
|
24796
26149
|
}
|
|
24797
26150
|
|
|
26151
|
+
function waitForDesiredOpenSessionTerminal(
|
|
26152
|
+
sessionId,
|
|
26153
|
+
{ timeoutMs = DESIRED_OPEN_RESTORE_SESSION_TIMEOUT_MS } = {}
|
|
26154
|
+
) {
|
|
26155
|
+
const session = sessionManager.getSession(sessionId);
|
|
26156
|
+
if (!session) {
|
|
26157
|
+
return Promise.resolve({ state: "failed", reason: "provider_runtime_exited" });
|
|
26158
|
+
}
|
|
26159
|
+
if (session._ready === true) return Promise.resolve({ state: "ready" });
|
|
26160
|
+
if (timeoutMs === 0) {
|
|
26161
|
+
return Promise.resolve({ state: "failed", reason: "restore_timeout" });
|
|
26162
|
+
}
|
|
26163
|
+
return new Promise((resolve) => {
|
|
26164
|
+
const timer = setTimeout(
|
|
26165
|
+
() => finish({ state: "failed", reason: "restore_timeout" }),
|
|
26166
|
+
timeoutMs
|
|
26167
|
+
);
|
|
26168
|
+
timer.unref?.();
|
|
26169
|
+
function finish(outcome) {
|
|
26170
|
+
clearTimeout(timer);
|
|
26171
|
+
session.off("event", onEvent);
|
|
26172
|
+
session.off("error", onError);
|
|
26173
|
+
session.off("exit", onExit);
|
|
26174
|
+
resolve(outcome);
|
|
26175
|
+
}
|
|
26176
|
+
function onEvent(event) {
|
|
26177
|
+
if (event?.type === "session.ready") finish({ state: "ready" });
|
|
26178
|
+
}
|
|
26179
|
+
function onError() {
|
|
26180
|
+
const outcome = sessionManager.getDesiredOpenRestoreOutcome(sessionId);
|
|
26181
|
+
finish({
|
|
26182
|
+
state: "failed",
|
|
26183
|
+
reason: outcome?.reason || "provider_runtime_restore_failed",
|
|
26184
|
+
});
|
|
26185
|
+
}
|
|
26186
|
+
function onExit() {
|
|
26187
|
+
finish({ state: "failed", reason: "provider_runtime_exited" });
|
|
26188
|
+
}
|
|
26189
|
+
session.on("event", onEvent);
|
|
26190
|
+
session.on("error", onError);
|
|
26191
|
+
session.on("exit", onExit);
|
|
26192
|
+
});
|
|
26193
|
+
}
|
|
26194
|
+
|
|
26195
|
+
async function executeDesiredOpenReconciliation({
|
|
26196
|
+
trigger,
|
|
26197
|
+
sessionIds = null,
|
|
26198
|
+
timeoutMs = DESIRED_OPEN_RESTORE_SESSION_TIMEOUT_MS,
|
|
26199
|
+
}) {
|
|
26200
|
+
const reconciliationId = randomUUID();
|
|
26201
|
+
const startedAt = new Date().toISOString();
|
|
26202
|
+
const desired = sessionManager.getDesiredOpenRestoreStatus();
|
|
26203
|
+
lastDesiredOpenRestoreStatus = {
|
|
26204
|
+
status: "restoring",
|
|
26205
|
+
trigger,
|
|
26206
|
+
reconciliation_id: reconciliationId,
|
|
26207
|
+
running: true,
|
|
26208
|
+
started_at: startedAt,
|
|
26209
|
+
completed_at: null,
|
|
26210
|
+
};
|
|
26211
|
+
if (!desired.valid) {
|
|
26212
|
+
lastDesiredOpenRestoreStatus = {
|
|
26213
|
+
...lastDesiredOpenRestoreStatus,
|
|
26214
|
+
status: desired.status,
|
|
26215
|
+
running: false,
|
|
26216
|
+
completed_at: new Date().toISOString(),
|
|
26217
|
+
};
|
|
26218
|
+
return buildDesiredOpenRestoreStatusPayload();
|
|
26219
|
+
}
|
|
26220
|
+
|
|
26221
|
+
const selectedIds = Array.isArray(sessionIds) ? new Set(sessionIds) : null;
|
|
26222
|
+
const queue = desired.sessions.filter(
|
|
26223
|
+
(entry) =>
|
|
26224
|
+
entry.state !== "failed" &&
|
|
26225
|
+
(!selectedIds || selectedIds.has(entry.session_id))
|
|
26226
|
+
);
|
|
26227
|
+
const aggregate = { restored: [], skipped: [], failed: [] };
|
|
26228
|
+
let nextIndex = 0;
|
|
26229
|
+
const worker = async () => {
|
|
26230
|
+
while (nextIndex < queue.length) {
|
|
26231
|
+
const record = queue[nextIndex];
|
|
26232
|
+
nextIndex += 1;
|
|
26233
|
+
const result = sessionManager.restoreDesiredOpenSessions({
|
|
26234
|
+
reconciliationId,
|
|
26235
|
+
sessionIds: [record.session_id],
|
|
26236
|
+
buildRuntimeCapabilityGrant: ({ sessionId, agentId, capabilities }) =>
|
|
26237
|
+
buildLiveSessionRuntimeCapabilityGrant({
|
|
26238
|
+
sessionId,
|
|
26239
|
+
agentId,
|
|
26240
|
+
capabilities,
|
|
26241
|
+
}),
|
|
26242
|
+
});
|
|
26243
|
+
aggregate.failed.push(...result.failed);
|
|
26244
|
+
for (const skippedEntry of result.skipped) {
|
|
26245
|
+
const liveSession = sessionManager.getSession(skippedEntry.session_id);
|
|
26246
|
+
if (
|
|
26247
|
+
skippedEntry.reason === "already_live" &&
|
|
26248
|
+
liveSession?._ready !== true
|
|
26249
|
+
) {
|
|
26250
|
+
const terminal = await waitForDesiredOpenSessionTerminal(
|
|
26251
|
+
skippedEntry.session_id,
|
|
26252
|
+
{ timeoutMs }
|
|
26253
|
+
);
|
|
26254
|
+
if (terminal.state === "failed") {
|
|
26255
|
+
const failure = {
|
|
26256
|
+
session_id: skippedEntry.session_id,
|
|
26257
|
+
agent_id: record.agent_id,
|
|
26258
|
+
provider: record.provider,
|
|
26259
|
+
reason: terminal.reason,
|
|
26260
|
+
};
|
|
26261
|
+
sessionManager.markDesiredOpenRestoreFailure(
|
|
26262
|
+
skippedEntry.session_id,
|
|
26263
|
+
{ ...failure, reconciliationId }
|
|
26264
|
+
);
|
|
26265
|
+
aggregate.failed.push(failure);
|
|
26266
|
+
continue;
|
|
26267
|
+
}
|
|
26268
|
+
}
|
|
26269
|
+
aggregate.skipped.push(skippedEntry);
|
|
26270
|
+
}
|
|
26271
|
+
for (const restoredEntry of result.restored) {
|
|
26272
|
+
const terminal = await waitForDesiredOpenSessionTerminal(
|
|
26273
|
+
restoredEntry.session_id,
|
|
26274
|
+
{ timeoutMs }
|
|
26275
|
+
);
|
|
26276
|
+
if (terminal.state === "failed") {
|
|
26277
|
+
const failure = {
|
|
26278
|
+
session_id: restoredEntry.session_id,
|
|
26279
|
+
agent_id: record.agent_id,
|
|
26280
|
+
provider: record.provider,
|
|
26281
|
+
reason: terminal.reason,
|
|
26282
|
+
};
|
|
26283
|
+
sessionManager.markDesiredOpenRestoreFailure(
|
|
26284
|
+
restoredEntry.session_id,
|
|
26285
|
+
{
|
|
26286
|
+
...failure,
|
|
26287
|
+
reconciliationId,
|
|
26288
|
+
}
|
|
26289
|
+
);
|
|
26290
|
+
aggregate.failed.push(failure);
|
|
26291
|
+
} else {
|
|
26292
|
+
aggregate.restored.push(restoredEntry);
|
|
26293
|
+
}
|
|
26294
|
+
}
|
|
26295
|
+
}
|
|
26296
|
+
};
|
|
26297
|
+
await Promise.all(
|
|
26298
|
+
Array.from(
|
|
26299
|
+
{ length: Math.min(DESIRED_OPEN_RESTORE_MAX_CONCURRENCY, queue.length) },
|
|
26300
|
+
() => worker()
|
|
26301
|
+
)
|
|
26302
|
+
);
|
|
26303
|
+
|
|
26304
|
+
if (aggregate.restored.length > 0) {
|
|
26305
|
+
await restoreTelegramListenersAfterHostRestart({
|
|
26306
|
+
snapshot: {},
|
|
26307
|
+
sessionRestore: aggregate,
|
|
26308
|
+
});
|
|
26309
|
+
for (const entry of aggregate.restored) {
|
|
26310
|
+
const session = sessionManager.getSession(entry.session_id);
|
|
26311
|
+
if (!session?.cwd) continue;
|
|
26312
|
+
try {
|
|
26313
|
+
const resolved = resolveAgentRuntimeConfig(session.cwd);
|
|
26314
|
+
session.notificationConfig = buildResolvedSessionNotificationConfig(
|
|
26315
|
+
resolved.layers
|
|
26316
|
+
);
|
|
26317
|
+
session.notificationConfigUpdatedAt = new Date().toISOString();
|
|
26318
|
+
} catch (err) {
|
|
26319
|
+
session.bookkeepingWarnings = [
|
|
26320
|
+
...(session.bookkeepingWarnings || []),
|
|
26321
|
+
`desired_open_restore.notification_config_failed: ${err.message}`,
|
|
26322
|
+
];
|
|
26323
|
+
}
|
|
26324
|
+
}
|
|
26325
|
+
}
|
|
26326
|
+
|
|
26327
|
+
const failedCount = aggregate.failed.length;
|
|
26328
|
+
for (const failure of aggregate.failed) {
|
|
26329
|
+
if (!failure.session_id) continue;
|
|
26330
|
+
const finalizedFailure = sessionManager.finalizeDesiredOpenRestoreFailure(
|
|
26331
|
+
failure.session_id,
|
|
26332
|
+
failure
|
|
26333
|
+
);
|
|
26334
|
+
try {
|
|
26335
|
+
updateSessionRecord(failure.session_id, {
|
|
26336
|
+
restore_failed_reason: finalizedFailure.reason,
|
|
26337
|
+
restore_failed_at: new Date().toISOString(),
|
|
26338
|
+
});
|
|
26339
|
+
} catch (err) {
|
|
26340
|
+
console.warn(
|
|
26341
|
+
`[desired-open-restore] Could not persist failure history ${failure.session_id}: ${err.message}`
|
|
26342
|
+
);
|
|
26343
|
+
}
|
|
26344
|
+
sessionCollectionAdmittedSessionIds.delete(failure.session_id);
|
|
26345
|
+
if (
|
|
26346
|
+
failure.reason === "restore_timeout" &&
|
|
26347
|
+
sessionManager.getSession(failure.session_id)
|
|
26348
|
+
) {
|
|
26349
|
+
try {
|
|
26350
|
+
await sessionManager.killSession(failure.session_id);
|
|
26351
|
+
} catch (err) {
|
|
26352
|
+
console.warn(
|
|
26353
|
+
`[desired-open-restore] Could not stop timed-out runtime ${failure.session_id}: ${err.message}`
|
|
26354
|
+
);
|
|
26355
|
+
}
|
|
26356
|
+
}
|
|
26357
|
+
}
|
|
26358
|
+
commitSessionCollectionChange(
|
|
26359
|
+
failedCount > 0
|
|
26360
|
+
? ["restore", "restore_failed_history"]
|
|
26361
|
+
: ["restore"],
|
|
26362
|
+
queue.map((entry) => entry.session_id)
|
|
26363
|
+
);
|
|
26364
|
+
lastDesiredOpenRestoreStatus = {
|
|
26365
|
+
...lastDesiredOpenRestoreStatus,
|
|
26366
|
+
status: failedCount > 0 ? "completed_with_failures" : "completed",
|
|
26367
|
+
running: false,
|
|
26368
|
+
completed_at: new Date().toISOString(),
|
|
26369
|
+
counts: {
|
|
26370
|
+
desired: queue.length,
|
|
26371
|
+
restored: aggregate.restored.length,
|
|
26372
|
+
skipped: aggregate.skipped.length,
|
|
26373
|
+
failed: failedCount,
|
|
26374
|
+
},
|
|
26375
|
+
result: aggregate,
|
|
26376
|
+
};
|
|
26377
|
+
return buildDesiredOpenRestoreStatusPayload();
|
|
26378
|
+
}
|
|
26379
|
+
|
|
26380
|
+
async function reconcileDesiredOpenSessions({
|
|
26381
|
+
trigger,
|
|
26382
|
+
sessionIds = null,
|
|
26383
|
+
timeoutMs = DESIRED_OPEN_RESTORE_SESSION_TIMEOUT_MS,
|
|
26384
|
+
}) {
|
|
26385
|
+
while (desiredOpenReconciliationPromise) {
|
|
26386
|
+
try {
|
|
26387
|
+
await desiredOpenReconciliationPromise;
|
|
26388
|
+
} catch {
|
|
26389
|
+
// A queued exact retry owns a new reconciliation after the prior result.
|
|
26390
|
+
}
|
|
26391
|
+
}
|
|
26392
|
+
const reconciliation = executeDesiredOpenReconciliation({
|
|
26393
|
+
trigger,
|
|
26394
|
+
sessionIds,
|
|
26395
|
+
timeoutMs,
|
|
26396
|
+
});
|
|
26397
|
+
desiredOpenReconciliationPromise = reconciliation;
|
|
26398
|
+
try {
|
|
26399
|
+
return await reconciliation;
|
|
26400
|
+
} finally {
|
|
26401
|
+
if (desiredOpenReconciliationPromise === reconciliation) {
|
|
26402
|
+
desiredOpenReconciliationPromise = null;
|
|
26403
|
+
}
|
|
26404
|
+
}
|
|
26405
|
+
}
|
|
26406
|
+
|
|
24798
26407
|
async function reconcileHostRestartRestoreOnBoot() {
|
|
24799
26408
|
const existing = readHostRestartRestoreState({ configDir: getConfigDir() });
|
|
24800
26409
|
const activeStack = getActiveStackName();
|
|
@@ -24808,6 +26417,30 @@ async function reconcileHostRestartRestoreOnBoot() {
|
|
|
24808
26417
|
consumed: false,
|
|
24809
26418
|
restart_restore: buildHostRestartRestoreSummary(existing),
|
|
24810
26419
|
};
|
|
26420
|
+
if (readConfig().restore_previous_sessions_after_restart !== false) {
|
|
26421
|
+
void reconcileDesiredOpenSessions({ trigger: "host_boot" }).catch(
|
|
26422
|
+
(err) => {
|
|
26423
|
+
lastDesiredOpenRestoreStatus = {
|
|
26424
|
+
status: "failed",
|
|
26425
|
+
trigger: "host_boot",
|
|
26426
|
+
reconciliation_id: null,
|
|
26427
|
+
running: false,
|
|
26428
|
+
started_at: null,
|
|
26429
|
+
completed_at: new Date().toISOString(),
|
|
26430
|
+
error: err.message,
|
|
26431
|
+
};
|
|
26432
|
+
}
|
|
26433
|
+
);
|
|
26434
|
+
} else {
|
|
26435
|
+
lastDesiredOpenRestoreStatus = {
|
|
26436
|
+
status: "disabled_by_preference",
|
|
26437
|
+
trigger: "host_boot",
|
|
26438
|
+
reconciliation_id: null,
|
|
26439
|
+
running: false,
|
|
26440
|
+
started_at: null,
|
|
26441
|
+
completed_at: new Date().toISOString(),
|
|
26442
|
+
};
|
|
26443
|
+
}
|
|
24811
26444
|
return lastHostRestartRestoreStatus;
|
|
24812
26445
|
}
|
|
24813
26446
|
const snapshot = existing.runtime_snapshot || {};
|
|
@@ -24837,6 +26470,7 @@ async function reconcileHostRestartRestoreOnBoot() {
|
|
|
24837
26470
|
restoredSessionIds: sessionRestore.restored.map(
|
|
24838
26471
|
(entry) => entry.session_id
|
|
24839
26472
|
),
|
|
26473
|
+
loopRestorePolicy: existing.loop_restore_policy || "default_off",
|
|
24840
26474
|
}
|
|
24841
26475
|
);
|
|
24842
26476
|
const schedulerRestore = schedulerService.markHostRestartInterruptedRuns({
|
|
@@ -24904,6 +26538,16 @@ async function reconcileHostRestartRestoreOnBoot() {
|
|
|
24904
26538
|
}
|
|
24905
26539
|
|
|
24906
26540
|
await reconcileHostRestartRestoreOnBoot();
|
|
26541
|
+
try {
|
|
26542
|
+
const callbackReconciliation = await callbackHostIntegration.reconcilePending({
|
|
26543
|
+
pageSize: 100,
|
|
26544
|
+
});
|
|
26545
|
+
console.log(
|
|
26546
|
+
`[p238-callback] startup reconciliation considered ${callbackReconciliation.considered_count} callback(s)`
|
|
26547
|
+
);
|
|
26548
|
+
} catch (err) {
|
|
26549
|
+
console.warn(`[p238-callback] startup reconciliation failed: ${err.message || err}`);
|
|
26550
|
+
}
|
|
24907
26551
|
removeObsoleteAgentRegistry(getConfigDir());
|
|
24908
26552
|
browserRootProjectIndex.start();
|
|
24909
26553
|
schedulerRunner.start();
|
|
@@ -25030,6 +26674,12 @@ server.listen(PORT, () => {
|
|
|
25030
26674
|
`[oysterun-host] Runtime metadata write failed: ${err.message}`
|
|
25031
26675
|
);
|
|
25032
26676
|
}
|
|
26677
|
+
// A retained route-pending incident must not require a human login to begin
|
|
26678
|
+
// its read-only requalification. The external coordinator owns completion.
|
|
26679
|
+
setImmediate(() => {
|
|
26680
|
+
try { maybeCloseSupersededOysterunUpdateRecovery(); }
|
|
26681
|
+
catch (error) { console.warn("[lifecycle] Retained operation observation failed:", sanitizeLifecycleDiagnostic(error.message)); }
|
|
26682
|
+
});
|
|
25033
26683
|
console.log(`[oysterun-host] Listening on http://localhost:${PORT}`);
|
|
25034
26684
|
console.log(
|
|
25035
26685
|
`[oysterun-host] WebSocket at ws://localhost:${PORT}/session/stream`
|