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.
Files changed (242) hide show
  1. package/bin/oysterun.mjs +64 -17
  2. package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
  3. package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
  4. package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
  5. package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
  6. package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
  7. package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
  8. package/dev/client/connectivity/managed-host-browser.mjs +10 -0
  9. package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
  10. package/dev/client/connectivity/session-transition-state.mjs +18 -6
  11. package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
  12. package/dev/client/web/index.html +5367 -609
  13. package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
  14. package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
  15. package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
  16. package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
  17. package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
  18. package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
  19. package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
  20. package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
  21. package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
  22. package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
  23. package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
  24. package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
  25. package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
  26. package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
  27. package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
  28. package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
  29. package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
  30. package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
  31. package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
  32. package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
  33. package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
  34. package/dev/client/web-chat/dist/index.html +1 -2
  35. package/docs/routec/dev/cloud_deplyments.md +5 -0
  36. package/docs/routec/dev/npm_native_dependency_install.md +6 -2
  37. package/docs/routec/dev/npmjs_publish.md +55 -19
  38. package/docs/routec/dev/p262_setup_input_file.md +117 -0
  39. package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
  40. package/docs/routec/host_setup.md +39 -10
  41. package/docs/routec/oysterun_operation_interface_spec.md +78 -2
  42. package/docs/routec/provider_contract.md +35 -5
  43. package/host-service/README.md +36 -0
  44. package/host-service/adapters/claude-code-adapter.mjs +48 -5
  45. package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
  46. package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
  47. package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
  48. package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
  49. package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
  50. package/host-service/agent-config.mjs +16 -5
  51. package/host-service/agent-folder-availability.mjs +41 -0
  52. package/host-service/apns-complete-message-dispatcher.mjs +1 -0
  53. package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
  54. package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
  55. package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
  56. package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
  57. package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
  58. package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
  59. package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
  60. package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
  61. package/host-service/callback-durable-store.mjs +354 -47
  62. package/host-service/callback-foundation.mjs +21 -1
  63. package/host-service/callback-host-integration.mjs +345 -30
  64. package/host-service/claude-acp-model-discovery.mjs +23 -18
  65. package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
  66. package/host-service/cli/installer-recovery-guidance.mjs +20 -0
  67. package/host-service/cli/prepare-restart-restore.mjs +2 -0
  68. package/host-service/cli/product-cli.mjs +34 -33
  69. package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
  70. package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
  71. package/host-service/cli/run-host-lifecycle.mjs +2 -0
  72. package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
  73. package/host-service/codex-model-discovery.mjs +23 -18
  74. package/host-service/complete-cli-uninstall.mjs +176 -0
  75. package/host-service/complete-message-notification-predicate.mjs +8 -6
  76. package/host-service/complete-message-notification-title.mjs +4 -0
  77. package/host-service/completed-session-unread.mjs +425 -42
  78. package/host-service/completed-turn-navigation.mjs +444 -144
  79. package/host-service/completed-turn-notification-observer.mjs +116 -0
  80. package/host-service/completed-turn-service.mjs +445 -0
  81. package/host-service/config.mjs +115 -9
  82. package/host-service/desired-open-session-state.mjs +331 -0
  83. package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
  84. package/host-service/frp-agent.mjs +9 -9
  85. package/host-service/grok-acp-model-discovery.mjs +31 -33
  86. package/host-service/host-lifecycle-recovery.mjs +37 -0
  87. package/host-service/host-port-resolution.mjs +98 -0
  88. package/host-service/host-runtime-diagnostics.mjs +18 -1
  89. package/host-service/initial-preflight-recovery.mjs +71 -0
  90. package/host-service/installer-recovery-guidance.mjs +85 -0
  91. package/host-service/installer-reinstall-bindings.mjs +49 -0
  92. package/host-service/installer-reinstall-target.mjs +49 -0
  93. package/host-service/legacy-continuity-observation.mjs +33 -0
  94. package/host-service/lifecycle-compat-cli.mjs +13 -0
  95. package/host-service/lifecycle-compat-preload.mjs +89 -0
  96. package/host-service/lifecycle-compatibility.mjs +164 -0
  97. package/host-service/lifecycle-continuity-assets.mjs +222 -0
  98. package/host-service/lifecycle-continuity-probe.mjs +97 -0
  99. package/host-service/lifecycle-continuity-projection.mjs +29 -0
  100. package/host-service/lifecycle-continuity-runner.mjs +192 -0
  101. package/host-service/lifecycle-continuity.mjs +493 -0
  102. package/host-service/lifecycle-service-render-bundle.mjs +120 -0
  103. package/host-service/lifecycle-user-path.mjs +34 -0
  104. package/host-service/link-annotations.mjs +10 -0
  105. package/host-service/matrix-credential-issuer.mjs +32 -1
  106. package/host-service/matrix-event-correlator.mjs +166 -27
  107. package/host-service/matrix-facade.mjs +96 -19
  108. package/host-service/matrix-room-binding.mjs +24 -0
  109. package/host-service/matrix-transcript-read-adapter.mjs +17 -3
  110. package/host-service/model-discovery-guardian.mjs +40 -0
  111. package/host-service/model-discovery-process.mjs +141 -0
  112. package/host-service/onboard-lifecycle-status.mjs +28 -0
  113. package/host-service/onboard.mjs +479 -0
  114. package/host-service/package.json +2 -1
  115. package/host-service/portable-scheduler-definitions.mjs +28 -0
  116. package/host-service/product-runtime-host-origin.mjs +22 -0
  117. package/host-service/provider-model-params.mjs +14 -17
  118. package/host-service/provider-model-refresh-runner.mjs +124 -169
  119. package/host-service/provider-model-refresh-schedule.mjs +11 -0
  120. package/host-service/provider-resume-contract.mjs +9 -3
  121. package/host-service/restart-restore-state.mjs +25 -0
  122. package/host-service/retained-host-state-holder.mjs +57 -0
  123. package/host-service/routec-matrix-sqlite-store.mjs +155 -10
  124. package/host-service/routec-matrix-storage-adapter.mjs +208 -25
  125. package/host-service/routec-matrix-storage-paths.mjs +12 -0
  126. package/host-service/routec-request-delay-window.mjs +55 -0
  127. package/host-service/scheduler-acp-provider-runner.mjs +2 -1
  128. package/host-service/scheduler-direct-provider-runner.mjs +71 -9
  129. package/host-service/scheduler-run-timeout-policy.mjs +85 -0
  130. package/host-service/scheduler-service.mjs +96 -9
  131. package/host-service/scheduler-store.mjs +9 -2
  132. package/host-service/server.mjs +2202 -552
  133. package/host-service/service-definition-migration.mjs +10 -3
  134. package/host-service/session-collection-service.mjs +320 -0
  135. package/host-service/session-history.mjs +99 -15
  136. package/host-service/session-list-preview-service.mjs +87 -12
  137. package/host-service/session-list-projection-service.mjs +182 -0
  138. package/host-service/session-loop-definitions.mjs +702 -75
  139. package/host-service/session-manager.mjs +473 -38
  140. package/host-service/session-switcher-projection.mjs +11 -3
  141. package/host-service/session-tags.mjs +451 -0
  142. package/host-service/session-transcript-target.mjs +103 -0
  143. package/host-service/setup-input-file.mjs +91 -0
  144. package/host-service/setup-provider-model-defaults.mjs +53 -0
  145. package/host-service/setup.mjs +133 -101
  146. package/host-service/stopped-user-host-package.mjs +60 -0
  147. package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
  148. package/host-service/update-reminder-state.mjs +7 -0
  149. package/host-service/voice-input-receipt-store.mjs +60 -0
  150. package/package.json +1 -1
  151. package/static/oysterun-social-preview.png +0 -0
  152. package/static/site/assets/oysterun-social-preview.png +0 -0
  153. package/static/site/blog/en/index.html +17 -0
  154. package/static/site/blog/generated_manifest.json +9 -9
  155. package/static/site/blog/index.html +18 -0
  156. package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
  157. package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
  158. package/static/site/blog/zh/index.html +17 -0
  159. package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
  160. package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
  161. package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
  162. package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
  163. package/static/site/docs/en/index.html +17 -0
  164. package/static/site/docs/generated_manifest.json +60 -60
  165. package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
  166. package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
  167. package/static/site/docs/index.html +18 -0
  168. package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
  169. package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
  170. package/static/site/docs/read-large-tool-results/en/index.html +17 -0
  171. package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
  172. package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
  173. package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
  174. package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
  175. package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
  176. package/static/site/docs/send-images-and-media/en/index.html +17 -0
  177. package/static/site/docs/send-images-and-media/zh/index.html +17 -0
  178. package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
  179. package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
  180. package/static/site/docs/understand-agent-activity/en/index.html +19 -2
  181. package/static/site/docs/understand-agent-activity/en/index.md +2 -2
  182. package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
  183. package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
  184. package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
  185. package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
  186. package/static/site/docs/zh/index.html +17 -0
  187. package/static/site/en/index.html +18 -1
  188. package/static/site/en/index.md +1 -1
  189. package/static/site/faq/en/index.html +17 -0
  190. package/static/site/faq/index.html +18 -0
  191. package/static/site/faq/zh/index.html +17 -0
  192. package/static/site/getting-started/en/index.html +23 -1
  193. package/static/site/getting-started/en/index.md +7 -1
  194. package/static/site/getting-started/index.html +18 -0
  195. package/static/site/getting-started/zh/index.html +23 -1
  196. package/static/site/getting-started/zh/index.md +7 -1
  197. package/static/site/index.html +18 -0
  198. package/static/site/install.sh +676 -127
  199. package/static/site/overview/en/index.html +18 -1
  200. package/static/site/overview/en/index.md +2 -2
  201. package/static/site/overview/index.html +18 -0
  202. package/static/site/overview/zh/index.html +18 -1
  203. package/static/site/overview/zh/index.md +2 -2
  204. package/static/site/privacy/en/index.html +17 -0
  205. package/static/site/privacy/index.html +18 -0
  206. package/static/site/privacy/zh/index.html +17 -0
  207. package/static/site/zh/index.html +18 -1
  208. package/static/site/zh/index.md +1 -1
  209. package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
  210. package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
  211. package/static/site_source/downloads/install.sh +676 -127
  212. package/static/site_source/manifests/host_install.json +2 -10
  213. package/static/site_source/pages/getting-started/en.html +6 -1
  214. package/static/site_source/pages/getting-started/zh.html +6 -1
  215. package/static/site_source/pages/home/en.html +1 -1
  216. package/static/site_source/pages/home/zh.html +1 -1
  217. package/static/site_source/pages/home_main.html +3 -2
  218. package/static/site_source/pages/overview/en.html +1 -1
  219. package/static/site_source/pages/overview/zh.html +1 -1
  220. package/tool_scripts/install_oysterun_update.mjs +78 -13
  221. package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
  222. package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
  223. package/tool_scripts/oysterun_release_service.sh +5 -0
  224. package/tool_scripts/oysterun_service_common.sh +59 -24
  225. package/tool_scripts/oysterun_setup_input.sh +97 -0
  226. package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
  227. package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
  228. package/tool_scripts/resolve_user_host_port.mjs +15 -0
  229. package/tool_scripts/restart_oysterun.sh +8 -2
  230. package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
  231. package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
  232. package/tool_scripts/sync_host_stack_config.mjs +9 -6
  233. package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
  234. package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
  235. package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
  236. package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
  237. package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
  238. package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
  239. package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
  240. package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
  241. package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
  242. package/dev/client/web-chat/dist/assets/web-c70YaskK.js +0 -1
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "crypto";
2
2
  import { EventEmitter } from "events";
3
+ import { appendTaskProviderLifecycleDiagnostic } from "./host-runtime-diagnostics.mjs";
3
4
  import {
4
5
  chmodSync,
5
6
  copyFileSync,
@@ -19,7 +20,8 @@ import { DebugFixtureProviderAdapter } from "./adapters/debug-fixture-provider-a
19
20
  import { DebugLargeToolSpilloverProviderAdapter } from "./adapters/debug-large-tool-spillover-provider-adapter.mjs";
20
21
  import { DebugP135CodexReplayProviderAdapter } from "./adapters/debug-p135-codex-replay-provider-adapter.mjs";
21
22
  import { DebugRouteCStructuralReplayProviderAdapter } from "./adapters/debug-routec-structural-replay-provider-adapter.mjs";
22
- import { readConfig } from "./config.mjs";
23
+ import { getConfigDir, readConfig } from "./config.mjs";
24
+ import { DesiredOpenSessionStateOwner } from "./desired-open-session-state.mjs";
23
25
  import {
24
26
  createProviderUnavailableError,
25
27
  getConfiguredProviderCommand,
@@ -30,6 +32,7 @@ import {
30
32
  saveSessionRecord,
31
33
  updateSessionRecord,
32
34
  } from "./session-history.mjs";
35
+ import { canonicalSessionTags, normalizeSessionTags } from "./session-tags.mjs";
33
36
  import {
34
37
  HOST_SESSION_RESUME_IDENTITY_KIND,
35
38
  getAdapterResumeIdentityKind,
@@ -53,6 +56,7 @@ import {
53
56
  import { normalizeAssetReadablePaths } from "./workspace-policy.mjs";
54
57
  import {
55
58
  blockRouteCHost2IntakeProviderDelivery,
59
+ cancelRouteCHost2IntakeForMatrixEvent,
56
60
  claimRouteCHost2IntakeForProviderDelivery,
57
61
  getRouteCHost2IntakeProof,
58
62
  markRouteCHost2IntakeAgentTurnStarted,
@@ -846,6 +850,19 @@ export class SessionManager extends EventEmitter {
846
850
  DEFAULT_CLAUDE_CLAIMED_TURN_NO_OUTPUT_TIMEOUT_MS,
847
851
  "claudeClaimedTurnNoOutputTimeoutMs"
848
852
  );
853
+ const desiredOpenStackName =
854
+ options.desiredOpenStackName ||
855
+ process.env.OYSTERUN_ROUTEC_STACK_NAME ||
856
+ process.env.OYSTERUN_STACK_NAME ||
857
+ process.env.OYSTERUN_STACK ||
858
+ "default";
859
+ this.desiredOpenStateOwner =
860
+ options.desiredOpenStateOwner ||
861
+ new DesiredOpenSessionStateOwner({
862
+ configDir: options.configDir || getConfigDir(),
863
+ stackName: desiredOpenStackName,
864
+ });
865
+ this.desiredOpenRestoreOutcomes = new Map();
849
866
  /** @type {Map<string, import("./adapters/claude-code-adapter.mjs").ClaudeCodeSession>} sessionId → Session */
850
867
  this.sessions = new Map();
851
868
  /** @type {Map<string, Set<string>>} agentId → sessionIds */
@@ -877,6 +894,168 @@ export class SessionManager extends EventEmitter {
877
894
  return session.providerResumeId || session.resumeSessionId || null;
878
895
  }
879
896
 
897
+ buildDesiredOpenSessionRecord(sessionId, session) {
898
+ return {
899
+ session_id: sessionId,
900
+ session_name: session.sessionName || sessionId,
901
+ tags: canonicalSessionTags(session),
902
+ agent_id: session.agentId,
903
+ agent_folder: session.cwd,
904
+ provider: session.provider || session.adapterId || "claude",
905
+ model: session.model ?? null,
906
+ fixture_resume_generation:
907
+ session.resumeIdentityKind === HOST_SESSION_RESUME_IDENTITY_KIND
908
+ ? normalizeFixtureResumeGeneration(session.fixtureResumeGeneration)
909
+ : null,
910
+ reasoning_effort: session.reasoningEffort ?? null,
911
+ reasoning_effort_source: session.reasoningEffortSource ?? null,
912
+ permission_mode: session.permissionMode ?? null,
913
+ approval_policy: session.approvalPolicy ?? null,
914
+ sandbox_mode: session.sandboxMode ?? null,
915
+ dangerous_mode: session.dangerousMode === true,
916
+ allow_dangerously_skip_permissions:
917
+ session.allowDangerouslySkipPermissions === true,
918
+ search_enabled: session.searchEnabled === true,
919
+ image_input_enabled: session.imageInputEnabled === true,
920
+ provider_resume_id: this.getEffectiveProviderResumeId(session),
921
+ provider_thread_id: session.threadId || null,
922
+ provider_transport: session.transport || null,
923
+ resume_identity_kind: session.resumeIdentityKind || null,
924
+ runtime_capabilities: cloneJsonObject(session.runtimeCapabilities) || {},
925
+ workspace_policy: cloneJsonObject(session.workspacePolicy),
926
+ asset_readable_paths: Array.isArray(session.assetReadablePaths)
927
+ ? [...session.assetReadablePaths]
928
+ : [],
929
+ started_at: session.historyCreatedAt || session.startedAt || null,
930
+ last_active_at:
931
+ session.lastActiveAt ||
932
+ session.historyCreatedAt ||
933
+ session.startedAt ||
934
+ null,
935
+ };
936
+ }
937
+
938
+ syncDesiredOpenSession(session) {
939
+ const state = this.desiredOpenStateOwner.upsert(
940
+ this.buildDesiredOpenSessionRecord(session.id, session)
941
+ );
942
+ const existing = this.desiredOpenRestoreOutcomes.get(session.id);
943
+ if (!existing || existing.state !== "failed") {
944
+ this.desiredOpenRestoreOutcomes.set(session.id, {
945
+ session_id: session.id,
946
+ state: session._ready === true ? "ready" : "restoring",
947
+ reason: null,
948
+ });
949
+ }
950
+ return state;
951
+ }
952
+
953
+ getDesiredOpenRestoreStatus() {
954
+ const desired = this.desiredOpenStateOwner.read();
955
+ if (!desired.valid) {
956
+ return {
957
+ status: desired.status,
958
+ valid: false,
959
+ sessions: [],
960
+ };
961
+ }
962
+ const sessions = desired.state.sessions.map((record) => ({
963
+ session_id: record.session_id,
964
+ session_name: record.session_name,
965
+ agent_id: record.agent_id,
966
+ agent_folder: record.agent_folder,
967
+ provider: record.provider,
968
+ ...(this.desiredOpenRestoreOutcomes.get(record.session_id) || {
969
+ state: record.restore_failed_reason ? "failed" : "pending",
970
+ reason: record.restore_failed_reason || null,
971
+ ...(record.restore_failed_at
972
+ ? { restore_failed_at: record.restore_failed_at }
973
+ : {}),
974
+ }),
975
+ }));
976
+ const desiredIds = new Set(sessions.map((entry) => entry.session_id));
977
+ for (const outcome of this.desiredOpenRestoreOutcomes.values()) {
978
+ if (
979
+ outcome.state === "failed" &&
980
+ outcome.session_id &&
981
+ !desiredIds.has(outcome.session_id)
982
+ ) {
983
+ sessions.push({ ...outcome });
984
+ }
985
+ }
986
+ return {
987
+ status: "available",
988
+ valid: true,
989
+ sessions,
990
+ };
991
+ }
992
+
993
+ getDesiredOpenRestoreOutcome(sessionId) {
994
+ return this.desiredOpenRestoreOutcomes.get(sessionId) || null;
995
+ }
996
+
997
+ markDesiredOpenRestoreReady(sessionId) {
998
+ if (!this.desiredOpenRestoreOutcomes.has(sessionId)) return null;
999
+ const outcome = {
1000
+ session_id: sessionId,
1001
+ state: "ready",
1002
+ reason: null,
1003
+ };
1004
+ this.desiredOpenRestoreOutcomes.set(sessionId, outcome);
1005
+ return outcome;
1006
+ }
1007
+
1008
+ finalizeDesiredOpenRestoreFailure(sessionId, details = {}) {
1009
+ const desired = this.desiredOpenStateOwner.read();
1010
+ const record = desired.valid
1011
+ ? desired.state.sessions.find((entry) => entry.session_id === sessionId)
1012
+ : null;
1013
+ const existing = this.desiredOpenRestoreOutcomes.get(sessionId) || {};
1014
+ const outcome = {
1015
+ ...(record || {}),
1016
+ ...existing,
1017
+ ...details,
1018
+ session_id: sessionId,
1019
+ state: "failed",
1020
+ reason:
1021
+ details.reason || existing.reason || "provider_runtime_restore_failed",
1022
+ };
1023
+ if (record) {
1024
+ this.desiredOpenStateOwner.upsert({
1025
+ ...record,
1026
+ restore_failed_reason: outcome.reason,
1027
+ restore_failed_at: new Date().toISOString(),
1028
+ });
1029
+ }
1030
+ this.desiredOpenRestoreOutcomes.set(sessionId, outcome);
1031
+ return outcome;
1032
+ }
1033
+
1034
+ markDesiredOpenRestoreFailure(
1035
+ sessionId,
1036
+ { reason, reconciliationId = null, ...details } = {}
1037
+ ) {
1038
+ const existing = this.desiredOpenRestoreOutcomes.get(sessionId);
1039
+ if (
1040
+ existing?.state === "failed" &&
1041
+ existing.reason === "restore_timeout" &&
1042
+ reason !== "restore_timeout" &&
1043
+ !reconciliationId
1044
+ ) {
1045
+ return existing;
1046
+ }
1047
+ const outcome = {
1048
+ session_id: sessionId,
1049
+ ...details,
1050
+ state: "failed",
1051
+ reason: reason || "provider_runtime_restore_failed",
1052
+ reconciliation_id:
1053
+ reconciliationId || existing?.reconciliation_id || null,
1054
+ };
1055
+ this.desiredOpenRestoreOutcomes.set(sessionId, outcome);
1056
+ return outcome;
1057
+ }
1058
+
880
1059
  getProviderTrustedFolderStatus({ cwd, provider, env = process.env }) {
881
1060
  return this.providerTrust.getStatus({ cwd, provider, env });
882
1061
  }
@@ -1531,10 +1710,23 @@ export class SessionManager extends EventEmitter {
1531
1710
  if (session._claimedProviderTurnNoOutputTimer) {
1532
1711
  clearTimeout(session._claimedProviderTurnNoOutputTimer);
1533
1712
  }
1713
+ const message = messageId ? this.getOutboxMessage(session, messageId) : null;
1714
+ // A primary-timer reset on a still-claimed silent prompt keeps the existing
1715
+ // failsafe. Terminal/visible completion and re-arm clear both handles.
1716
+ const clearFailsafe = resetState || !messageId ||
1717
+ message?.claimedClaudeFinalEventSeen === true ||
1718
+ ["failed", "canceled", "interrupted", "completed"].includes(message?.state);
1719
+ if (clearFailsafe && session._claimedProviderTurnNoOutputFailsafeTimer) {
1720
+ clearTimeout(session._claimedProviderTurnNoOutputFailsafeTimer);
1721
+ }
1722
+ appendTaskProviderLifecycleDiagnostic("claude_no_output_timer_clear", {
1723
+ session_id: session.id, message_id: session._claimedProviderTurnNoOutputMessageId,
1724
+ state: messageId ? this.getOutboxMessage(session, messageId)?.state : null,
1725
+ });
1534
1726
  session._claimedProviderTurnNoOutputTimer = null;
1727
+ if (clearFailsafe) session._claimedProviderTurnNoOutputFailsafeTimer = null;
1535
1728
  session._claimedProviderTurnNoOutputMessageId = null;
1536
1729
  session._claimedProviderTurnNoOutputDeadlineAt = null;
1537
- const message = messageId ? this.getOutboxMessage(session, messageId) : null;
1538
1730
  if (message) {
1539
1731
  message.claimedClaudeNoOutputDeadlineAt = null;
1540
1732
  }
@@ -2001,7 +2193,9 @@ export class SessionManager extends EventEmitter {
2001
2193
  if (
2002
2194
  message.state === "queued" ||
2003
2195
  message.state === "failed" ||
2004
- message.state === "canceled"
2196
+ message.state === "canceled" ||
2197
+ message.state === "interrupted" ||
2198
+ message.state === "completed"
2005
2199
  ) {
2006
2200
  return false;
2007
2201
  }
@@ -2530,6 +2724,10 @@ export class SessionManager extends EventEmitter {
2530
2724
  ) {
2531
2725
  return;
2532
2726
  }
2727
+ appendTaskProviderLifecycleDiagnostic("claude_no_output_timer_fire", {
2728
+ session_id: liveSession.id, message_id: messageId,
2729
+ state: targetMessage.state, generation,
2730
+ });
2533
2731
  this.enforceClaimedClaudeNoOutputMessage(liveSession, targetMessage, {
2534
2732
  nowMs: Date.now(),
2535
2733
  phase: "claimed_provider_turn_timeout",
@@ -2548,6 +2746,10 @@ export class SessionManager extends EventEmitter {
2548
2746
  Math.min(50, Math.max(1, this.claudeClaimedTurnNoOutputTimeoutMs))
2549
2747
  );
2550
2748
  failsafeTimer.unref?.();
2749
+ session._claimedProviderTurnNoOutputFailsafeTimer = failsafeTimer;
2750
+ appendTaskProviderLifecycleDiagnostic("claude_no_output_timer_arm", {
2751
+ session_id: session.id, message_id: messageId, state: message.state, generation,
2752
+ });
2551
2753
  }
2552
2754
 
2553
2755
  providerControlKindFromPendingRequest(pendingRequest) {
@@ -2727,6 +2929,7 @@ export class SessionManager extends EventEmitter {
2727
2929
  semantic_body: `Cancel ${outcome} for queued message ${message.id}.`,
2728
2930
  control_outcome_id: controlOutcomeId,
2729
2931
  control_outcome: outcome,
2932
+ cancel_outcome: outcome === "accepted" ? "accepted" : outcome,
2730
2933
  outcome,
2731
2934
  replay_policy: "always",
2732
2935
  })
@@ -2842,17 +3045,69 @@ export class SessionManager extends EventEmitter {
2842
3045
  return message;
2843
3046
  }
2844
3047
 
2845
- cancelOutboxMessage(sessionId, messageId) {
3048
+ async cancelOutboxMessage(sessionId, messageId) {
2846
3049
  const session = this.requireSession(sessionId);
2847
3050
  const message = this.getOutboxMessage(session, messageId);
2848
3051
  if (!message) {
2849
3052
  throw new Error(`Outbox message ${messageId} not found`);
2850
3053
  }
3054
+ const delivery = message.routeCMatrixDelivery || null;
3055
+ const providerStarted =
3056
+ delivery?.host2_intake_proof?.agent_turn_started === true ||
3057
+ delivery?.agent_turn_started === true ||
3058
+ message.state === "running";
3059
+ if (providerStarted || !["queued", "dispatching"].includes(message.state)) {
3060
+ throw new Error(`Outbox message ${messageId} is not cancelable`);
3061
+ }
3062
+ const dispatchAdapter = message.state === "dispatching"
3063
+ ? this.getAdapter(session.provider || session.adapterId || "claude")
3064
+ : null;
2851
3065
  if (
2852
- message.id === session._activeOutboxMessageId ||
2853
- message.state !== "queued"
3066
+ message.state === "dispatching" &&
3067
+ typeof dispatchAdapter?.cancelPendingMessage !== "function"
2854
3068
  ) {
2855
- throw new Error(`Outbox message ${messageId} is not cancelable`);
3069
+ throw new Error(
3070
+ `Provider ${session.provider || session.adapterId || "unknown"} cannot prove pre-start cancellation`
3071
+ );
3072
+ }
3073
+ if (message.state === "dispatching") {
3074
+ const adapterCancellation = await dispatchAdapter.cancelPendingMessage(
3075
+ session,
3076
+ message.id
3077
+ );
3078
+ if (adapterCancellation?.status === "provider_turn_started") {
3079
+ throw new Error(`Outbox message ${messageId} is too late to cancel`);
3080
+ }
3081
+ if (adapterCancellation?.status === "provider_acceptance_unknown") {
3082
+ const error = new Error(
3083
+ `Outbox message ${messageId} provider acceptance is unknown`,
3084
+ );
3085
+ error.code = "PROVIDER_ACCEPTANCE_UNKNOWN";
3086
+ error.providerAcceptance = "unknown";
3087
+ throw error;
3088
+ }
3089
+ if (adapterCancellation?.status !== "pending_message_cancelled") {
3090
+ throw new Error(
3091
+ `Provider ${session.provider || session.adapterId || "unknown"} did not prove pre-start cancellation`
3092
+ );
3093
+ }
3094
+ }
3095
+ if (delivery?.source_user_event_id) {
3096
+ const cancellation = cancelRouteCHost2IntakeForMatrixEvent({
3097
+ hostSessionId: session.id,
3098
+ matrixRoomId: delivery.matrix_room_id,
3099
+ serverEventId: delivery.source_user_event_id,
3100
+ });
3101
+ if (cancellation.status !== 200) {
3102
+ throw new Error(`Outbox message ${messageId} is too late to cancel`);
3103
+ }
3104
+ delivery.host2_intake_proof = cancellation.body?.proof || delivery.host2_intake_proof;
3105
+ }
3106
+ if (message.state === "dispatching") {
3107
+ session._activeOutboxMessageId = null;
3108
+ session._activeOutboxProgressSeen = false;
3109
+ session._activeOutboxProviderVisibleEventSeen = false;
3110
+ session._activeOutboxToolProgressSeen = false;
2856
3111
  }
2857
3112
  this.emitOutboxCancelControlEvent(session, message, {
2858
3113
  semanticType: "control.request",
@@ -2865,9 +3120,23 @@ export class SessionManager extends EventEmitter {
2865
3120
  outcome: "accepted",
2866
3121
  });
2867
3122
  this.emitDeliveryStateNotice(session);
3123
+ this.drainOutbox(session);
2868
3124
  return message;
2869
3125
  }
2870
3126
 
3127
+ async reconcileProviderMessageCancellation(sessionId, payload) {
3128
+ const session = this.requireSession(sessionId);
3129
+ const adapter = this.getAdapter(session.provider || session.adapterId || "claude");
3130
+ if (typeof adapter?.reconcilePendingMessageCancellation !== "function") {
3131
+ return {
3132
+ status: "provider_acceptance_unknown",
3133
+ message_id: payload?.messageId || null,
3134
+ reason: "provider_cancellation_reconciliation_unsupported",
3135
+ };
3136
+ }
3137
+ return adapter.reconcilePendingMessageCancellation(session, payload);
3138
+ }
3139
+
2871
3140
  handleOysterunProviderSkillInstallSlashCommand(
2872
3141
  session,
2873
3142
  userId,
@@ -3085,6 +3354,7 @@ export class SessionManager extends EventEmitter {
3085
3354
  }
3086
3355
 
3087
3356
  canDrainOutbox(session) {
3357
+ if (session._stopping === true) return false;
3088
3358
  const providerId = session.provider || session.adapterId || "claude";
3089
3359
  const allowFreshClaudeStartupDispatch =
3090
3360
  providerId === "claude" &&
@@ -3192,12 +3462,17 @@ export class SessionManager extends EventEmitter {
3192
3462
  }
3193
3463
  this.armClaimedClaudeNoOutputTimer(session, message);
3194
3464
  adapter.sendMessage(session, {
3465
+ messageId: message.id,
3195
3466
  userId: message.userId,
3196
3467
  nickname: message.nickname,
3197
3468
  text: message.providerText,
3198
3469
  rawText: message.rawText,
3199
3470
  isSlashCommand: message.isSlashCommand,
3200
3471
  slashCommandName: message.slashCommandName,
3472
+ reconcileProviderAcceptance:
3473
+ message.routeCMatrixDelivery?.delivery_kind === "callback_result" &&
3474
+ message.routeCMatrixDelivery?.session_correlation?.reconcile_provider_acceptance === true,
3475
+ providerTurnId: message.providerTurnId,
3201
3476
  });
3202
3477
  this.emit(
3203
3478
  "runtimeEvent",
@@ -3887,6 +4162,8 @@ export class SessionManager extends EventEmitter {
3887
4162
  provider = "claude",
3888
4163
  sessionId,
3889
4164
  sessionName,
4165
+ tags = [],
4166
+ deferTagsPersistence = false,
3890
4167
  parentSessionId = null,
3891
4168
  resumeSessionId,
3892
4169
  fork = false,
@@ -4071,6 +4348,8 @@ export class SessionManager extends EventEmitter {
4071
4348
  session.sandboxMode = session.sandboxMode ?? sandboxMode ?? null;
4072
4349
  session.dangerousMode = dangerousMode === true;
4073
4350
  session.sessionName = resolvedSessionName;
4351
+ session.tags = normalizeSessionTags(tags);
4352
+ session.tagsPersistenceDeferred = deferTagsPersistence === true;
4074
4353
  session.startRequestedAt = new Date().toISOString();
4075
4354
  session.historyCreatedAt =
4076
4355
  session.providerReadinessDeferred === true
@@ -4123,6 +4402,7 @@ export class SessionManager extends EventEmitter {
4123
4402
  session_id: session.id,
4124
4403
  session_name: session.sessionName,
4125
4404
  parent_session_id: session.parentSessionId,
4405
+ ...(session.tagsPersistenceDeferred ? {} : { tags: session.tags }),
4126
4406
  agent_id: agentId,
4127
4407
  agent_folder: cwd,
4128
4408
  runtime: session.provider,
@@ -4138,6 +4418,8 @@ export class SessionManager extends EventEmitter {
4138
4418
  fixture_resume_generation: session.fixtureResumeGeneration,
4139
4419
  created_at: session.historyCreatedAt,
4140
4420
  last_active_at: lastActiveAt,
4421
+ restore_failed_reason: null,
4422
+ restore_failed_at: null,
4141
4423
  });
4142
4424
 
4143
4425
  const buildHistoryMetadataSnapshot = () =>
@@ -4164,6 +4446,7 @@ export class SessionManager extends EventEmitter {
4164
4446
  session.historyCreatedAt ||= new Date().toISOString();
4165
4447
  session.lastActiveAt ||= session.historyCreatedAt;
4166
4448
  saveSessionRecord(buildHistoryRecord());
4449
+ this.syncDesiredOpenSession(session);
4167
4450
  session.historyRecordId = session.id;
4168
4451
  session.historyCommitted = true;
4169
4452
  session.historyMetadataSnapshot = buildHistoryMetadataSnapshot();
@@ -4190,6 +4473,16 @@ export class SessionManager extends EventEmitter {
4190
4473
  }
4191
4474
  this.handleOutboxRuntimeEvent(session, event);
4192
4475
  this.syncSessionRuntimeState(session, event);
4476
+ if (
4477
+ event.type === "session.ready" &&
4478
+ this.desiredOpenRestoreOutcomes.get(session.id)?.state !== "failed"
4479
+ ) {
4480
+ this.desiredOpenRestoreOutcomes.set(session.id, {
4481
+ session_id: session.id,
4482
+ state: "ready",
4483
+ reason: null,
4484
+ });
4485
+ }
4193
4486
  if (event.type === "session.ready" && !session.historyCommitted) {
4194
4487
  try {
4195
4488
  commitHistoryRecord();
@@ -4225,6 +4518,7 @@ export class SessionManager extends EventEmitter {
4225
4518
  buildHistoryRecord(new Date().toISOString())
4226
4519
  );
4227
4520
  session.historyMetadataSnapshot = nextHistoryMetadata;
4521
+ this.syncDesiredOpenSession(session);
4228
4522
  } catch (err) {
4229
4523
  recordBookkeepingWarning("history.runtime_sync_failed", err);
4230
4524
  }
@@ -4235,6 +4529,19 @@ export class SessionManager extends EventEmitter {
4235
4529
  }
4236
4530
  });
4237
4531
  session.on("error", (err) => {
4532
+ if (this.desiredOpenRestoreOutcomes.has(session.id) &&
4533
+ err.codexRequestOutcome !== "pre_acceptance_rejected") {
4534
+ const authFailure = this.classifyProviderAuthFailure(session, null, {
4535
+ type: "runtime.error",
4536
+ error: err.message || String(err),
4537
+ });
4538
+ this.markDesiredOpenRestoreFailure(session.id, {
4539
+ reason: authFailure
4540
+ ? "provider_login_required"
4541
+ : "provider_runtime_restore_failed",
4542
+ error: err.message || String(err),
4543
+ });
4544
+ }
4238
4545
  if (this.shouldSuppressExitDerivedRuntimeError(session, err)) {
4239
4546
  return;
4240
4547
  }
@@ -4280,6 +4587,12 @@ export class SessionManager extends EventEmitter {
4280
4587
  this.handleOutboxExit(session);
4281
4588
  this.sessions.delete(id);
4282
4589
  this.removeAgentSession(agentId, id);
4590
+ if (this.desiredOpenRestoreOutcomes.has(id)) {
4591
+ this.markDesiredOpenRestoreFailure(id, {
4592
+ reason: "provider_runtime_exited",
4593
+ exit_code: code ?? null,
4594
+ });
4595
+ }
4283
4596
  const providerExitEvent = session._lastProviderExitEvent || null;
4284
4597
  try {
4285
4598
  if (session.historyCommitted) {
@@ -4620,6 +4933,7 @@ export class SessionManager extends EventEmitter {
4620
4933
  session.runtimeCapabilities = JSON.parse(
4621
4934
  JSON.stringify(runtimeCapabilities)
4622
4935
  );
4936
+ this.syncDesiredOpenSession(session);
4623
4937
  this.emit(
4624
4938
  "runtimeEvent",
4625
4939
  session.agentId,
@@ -4638,6 +4952,13 @@ export class SessionManager extends EventEmitter {
4638
4952
  return session.runtimeCapabilities;
4639
4953
  }
4640
4954
 
4955
+ updateWorkspacePolicy(sessionId, workspacePolicy) {
4956
+ const session = this.requireSession(sessionId);
4957
+ session.workspacePolicy = cloneJsonObject(workspacePolicy);
4958
+ this.syncDesiredOpenSession(session);
4959
+ return session.workspacePolicy;
4960
+ }
4961
+
4641
4962
  renameSession(sessionId, sessionName) {
4642
4963
  const session = this.requireSession(sessionId);
4643
4964
  const resolvedSessionName = normalizeSessionName(sessionName);
@@ -4661,6 +4982,7 @@ export class SessionManager extends EventEmitter {
4661
4982
  last_active_at: new Date().toISOString(),
4662
4983
  });
4663
4984
  session.sessionName = resolvedSessionName;
4985
+ this.syncDesiredOpenSession(session);
4664
4986
  this.emit(
4665
4987
  "runtimeEvent",
4666
4988
  session.agentId,
@@ -4830,6 +5152,19 @@ export class SessionManager extends EventEmitter {
4830
5152
 
4831
5153
  async stopSession(sessionId) {
4832
5154
  const session = this.requireSession(sessionId);
5155
+ // Stop must leave a durable visible cancellation for never-started user
5156
+ // prompts, while callback results retain their own recovery contract.
5157
+ session._stopping = true;
5158
+ const discarded = [];
5159
+ for (const message of session._outbox || []) {
5160
+ if (message.state !== "queued" ||
5161
+ message.routeCMatrixDelivery?.delivery_kind === "callback_result") continue;
5162
+ await this.cancelOutboxMessage(session.id, message.id);
5163
+ discarded.push(message.id);
5164
+ }
5165
+ appendTaskProviderLifecycleDiagnostic("session_stop_queued_prompts", {
5166
+ session_id: session.id, discarded_message_ids: discarded,
5167
+ });
4833
5168
  const agentId = session.agentId;
4834
5169
  const adapter = this.getAdapter(
4835
5170
  session.provider || session.adapterId || "claude"
@@ -4851,6 +5186,14 @@ export class SessionManager extends EventEmitter {
4851
5186
  adapter.killSession(session);
4852
5187
  await this.waitForSessionExit(session, 5000);
4853
5188
  }
5189
+ this.desiredOpenStateOwner.remove(sessionId);
5190
+ this.desiredOpenRestoreOutcomes.delete(sessionId);
5191
+ }
5192
+
5193
+ removeDesiredOpenSession(sessionId) {
5194
+ const state = this.desiredOpenStateOwner.remove(sessionId);
5195
+ this.desiredOpenRestoreOutcomes.delete(sessionId);
5196
+ return state;
4854
5197
  }
4855
5198
 
4856
5199
  /**
@@ -4934,6 +5277,7 @@ export class SessionManager extends EventEmitter {
4934
5277
  provider: nextProvider,
4935
5278
  sessionId: nextSessionId,
4936
5279
  sessionName: overrides.sessionName ?? session.sessionName,
5280
+ tags: overrides.tags ?? session.tags,
4937
5281
  resumeSessionId: providerResumeId,
4938
5282
  resume: providerResumeId !== undefined,
4939
5283
  model:
@@ -4976,52 +5320,120 @@ export class SessionManager extends EventEmitter {
4976
5320
 
4977
5321
  snapshotHostRestartState() {
4978
5322
  return [...this.sessions.entries()].map(([sessionId, session]) => ({
4979
- session_id: sessionId,
4980
- session_name: session.sessionName || null,
4981
- agent_id: session.agentId,
4982
- agent_folder: session.cwd,
4983
- provider: session.provider || "claude",
4984
- model: session.model ?? null,
4985
- fixture_resume_generation:
4986
- session.resumeIdentityKind === HOST_SESSION_RESUME_IDENTITY_KIND
4987
- ? normalizeFixtureResumeGeneration(session.fixtureResumeGeneration)
4988
- : null,
4989
- reasoning_effort: session.reasoningEffort ?? null,
4990
- permission_mode: session.permissionMode ?? null,
4991
- approval_policy: session.approvalPolicy ?? null,
4992
- sandbox_mode: session.sandboxMode ?? null,
4993
- dangerous_mode: session.dangerousMode === true,
4994
- provider_resume_id: this.getEffectiveProviderResumeId(session),
4995
- provider_thread_id: session.threadId || null,
4996
- provider_transport: session.transport || null,
4997
- runtime_capabilities: cloneJsonObject(session.runtimeCapabilities) || {},
5323
+ ...this.buildDesiredOpenSessionRecord(sessionId, session),
4998
5324
  runtime_capability_grant: cloneJsonObject(session.runtimeCapabilityGrant),
4999
- reasoning_effort_source: session.reasoningEffortSource || null,
5000
5325
  native: cloneJsonObject(session.native) || session.native || null,
5001
- workspace_policy: cloneJsonObject(session.workspacePolicy),
5002
- asset_readable_paths: Array.isArray(session.assetReadablePaths)
5003
- ? [...session.assetReadablePaths]
5004
- : [],
5005
5326
  notification_config: serializeHostRestartNotificationConfig(
5006
5327
  session.notificationConfig
5007
5328
  ),
5008
5329
  notification_config_updated_at: normalizeHostRestartString(
5009
5330
  session.notificationConfigUpdatedAt
5010
5331
  ),
5011
- search_enabled: session.searchEnabled === true,
5012
- image_input_enabled: session.imageInputEnabled === true,
5013
- allow_dangerously_skip_permissions:
5014
- session.allowDangerouslySkipPermissions === true,
5015
5332
  alive_before_restart: session.alive === true,
5016
5333
  ready_before_restart: session._ready === true,
5017
- started_at: session.historyCreatedAt || session.startedAt || null,
5018
- last_active_at:
5019
- session.lastActiveAt || session.historyCreatedAt || session.startedAt || null,
5020
5334
  restore_policy: "provider_runtime_resume_on_boot",
5021
5335
  prompt_replay: false,
5022
5336
  }));
5023
5337
  }
5024
5338
 
5339
+ restoreDesiredOpenSessions({
5340
+ buildRuntimeCapabilityGrant = null,
5341
+ reconciliationId = null,
5342
+ sessionIds = null,
5343
+ } = {}) {
5344
+ const desired = this.desiredOpenStateOwner.read();
5345
+ if (!desired.valid) {
5346
+ return {
5347
+ restored: [],
5348
+ skipped: [],
5349
+ failed: [{ session_id: null, reason: desired.status }],
5350
+ };
5351
+ }
5352
+ const selectedIds = Array.isArray(sessionIds) ? new Set(sessionIds) : null;
5353
+ const eligible = [];
5354
+ const failed = [];
5355
+ for (const record of desired.state.sessions) {
5356
+ if (selectedIds && !selectedIds.has(record.session_id)) continue;
5357
+ if (record.restore_failed_reason) {
5358
+ this.desiredOpenRestoreOutcomes.set(record.session_id, {
5359
+ ...record,
5360
+ session_id: record.session_id,
5361
+ state: "failed",
5362
+ reason: record.restore_failed_reason,
5363
+ });
5364
+ continue;
5365
+ }
5366
+ this.desiredOpenRestoreOutcomes.set(record.session_id, {
5367
+ session_id: record.session_id,
5368
+ state: "restoring",
5369
+ reason: null,
5370
+ reconciliation_id: reconciliationId,
5371
+ });
5372
+ if (!existsSync(record.agent_folder)) {
5373
+ const failure = {
5374
+ session_id: record.session_id,
5375
+ agent_id: record.agent_id,
5376
+ reason: "agent_folder_missing",
5377
+ };
5378
+ failed.push(failure);
5379
+ this.desiredOpenRestoreOutcomes.set(record.session_id, {
5380
+ ...record,
5381
+ ...failure,
5382
+ state: "failed",
5383
+ reconciliation_id: reconciliationId,
5384
+ });
5385
+ continue;
5386
+ }
5387
+ eligible.push({
5388
+ ...record,
5389
+ alive_before_restart: true,
5390
+ ready_before_restart: true,
5391
+ prompt_replay: false,
5392
+ });
5393
+ }
5394
+ const result = this.restoreHostRestartState(eligible, {
5395
+ restartId: reconciliationId || `desired-open-${randomUUID()}`,
5396
+ buildRuntimeCapabilityGrant,
5397
+ });
5398
+ for (const entry of result.restored) {
5399
+ const session = this.getSession(entry.session_id);
5400
+ this.desiredOpenRestoreOutcomes.set(entry.session_id, {
5401
+ ...desired.state.sessions.find(
5402
+ (record) => record.session_id === entry.session_id
5403
+ ),
5404
+ session_id: entry.session_id,
5405
+ state: session?._ready === true ? "ready" : "restoring",
5406
+ reason: null,
5407
+ reconciliation_id: reconciliationId,
5408
+ });
5409
+ }
5410
+ for (const entry of result.skipped) {
5411
+ const session = this.getSession(entry.session_id);
5412
+ this.desiredOpenRestoreOutcomes.set(entry.session_id, {
5413
+ ...desired.state.sessions.find(
5414
+ (record) => record.session_id === entry.session_id
5415
+ ),
5416
+ ...entry,
5417
+ state:
5418
+ entry.reason === "already_live" && session?._ready !== true
5419
+ ? "restoring"
5420
+ : "ready",
5421
+ reconciliation_id: reconciliationId,
5422
+ });
5423
+ }
5424
+ for (const entry of result.failed) {
5425
+ this.desiredOpenRestoreOutcomes.set(entry.session_id, {
5426
+ ...desired.state.sessions.find(
5427
+ (record) => record.session_id === entry.session_id
5428
+ ),
5429
+ ...entry,
5430
+ state: "failed",
5431
+ reconciliation_id: reconciliationId,
5432
+ });
5433
+ }
5434
+ return { ...result, failed: [...failed, ...result.failed] };
5435
+ }
5436
+
5025
5437
  restoreHostRestartState(
5026
5438
  sessions = [],
5027
5439
  { restartId, restoredAt, buildRuntimeCapabilityGrant = null } = {}
@@ -5051,6 +5463,14 @@ export class SessionManager extends EventEmitter {
5051
5463
  });
5052
5464
  continue;
5053
5465
  }
5466
+ if (!existsSync(agentFolder)) {
5467
+ failed.push({
5468
+ session_id: id,
5469
+ agent_id: agentId,
5470
+ reason: "agent_folder_missing",
5471
+ });
5472
+ continue;
5473
+ }
5054
5474
  const wasLiveOrReady =
5055
5475
  record.alive_before_restart === true ||
5056
5476
  record.ready_before_restart === true;
@@ -5143,6 +5563,7 @@ export class SessionManager extends EventEmitter {
5143
5563
  provider,
5144
5564
  sessionId: id,
5145
5565
  sessionName: record.session_name || id,
5566
+ tags: canonicalSessionTags(record),
5146
5567
  resumeSessionId: providerResumeId,
5147
5568
  resume: true,
5148
5569
  fork: false,
@@ -5247,6 +5668,7 @@ export class SessionManager extends EventEmitter {
5247
5668
  session_id: id,
5248
5669
  session_name: session.sessionName,
5249
5670
  parent_session_id: session.parentSessionId || null,
5671
+ tags: canonicalSessionTags(session),
5250
5672
  agent_id: agentId,
5251
5673
  agent_folder: session.cwd,
5252
5674
  runtime: session.provider,
@@ -5273,6 +5695,18 @@ export class SessionManager extends EventEmitter {
5273
5695
  prompt_replay: false,
5274
5696
  });
5275
5697
  }
5698
+ for (const failure of failed) {
5699
+ if (!failure.session_id) continue;
5700
+ const record = sessions.find(
5701
+ (entry) => entry?.session_id === failure.session_id
5702
+ );
5703
+ this.finalizeDesiredOpenRestoreFailure(failure.session_id, {
5704
+ ...(record || {}),
5705
+ ...failure,
5706
+ reason: failure.reason,
5707
+ reconciliation_id: restartId || null,
5708
+ });
5709
+ }
5276
5710
  return { restored, skipped, failed };
5277
5711
  }
5278
5712
 
@@ -5303,6 +5737,7 @@ export class SessionManager extends EventEmitter {
5303
5737
  return [...this.sessions.entries()].map(([id, s]) => ({
5304
5738
  sessionId: id,
5305
5739
  sessionName: s.sessionName || null,
5740
+ tags: canonicalSessionTags(s),
5306
5741
  agentId: s.agentId,
5307
5742
  provider: s.provider || "claude",
5308
5743
  cwd: s.cwd,