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
@@ -13,6 +13,7 @@ import {
13
13
  } from "fs";
14
14
  import { basename, dirname, join } from "path";
15
15
  import { homedir } from "os";
16
+ import { routeCMatrixJsonStoragePath } from "./routec-matrix-storage-paths.mjs";
16
17
  import {
17
18
  getRouteCMatrixActorByKey,
18
19
  getRouteCMatrixActorByUserId,
@@ -26,14 +27,20 @@ import {
26
27
  } from "./tool-event-transfer-projection.mjs";
27
28
  import { ROUTEC_TOOL_EVENT_DETAIL_SELECTED_DETAIL_LIMIT_BYTES } from "./tool-event-detail-store.mjs";
28
29
  import { readCompletedTurnFinalMessageNeighborsFromSQLite } from "./completed-turn-navigation.mjs";
30
+ import {
31
+ decorateMatrixEventsWithCompletedTurnProjection,
32
+ MATRIX_COMPLETED_TURN_PROJECTION_KEY,
33
+ } from "./completed-turn-service.mjs";
29
34
  import {
30
35
  MATRIX_SQLITE_SCHEMA_VERSION,
36
+ routeCMatrixSQLiteRoomReadContextDB,
31
37
  exportSQLiteStoreObject,
32
38
  getLastRouteCMatrixSQLiteMigrationProof,
33
39
  getRouteCMatrixSQLiteStorageHealth,
34
40
  getRouteCMatrixSQLiteStoragePath,
35
41
  insertRouteCMatrixSQLiteTxnMapping,
36
42
  openRouteCMatrixSQLiteStore,
43
+ openRouteCMatrixSQLiteReadOnlyStore,
37
44
  readRouteCMatrixSQLiteContiguousSemanticEvents,
38
45
  readRouteCMatrixSQLiteContextWindow,
39
46
  readRouteCMatrixSQLiteCurrentStateSince,
@@ -45,6 +52,7 @@ import {
45
52
  readRouteCMatrixSQLiteMessagesWindow,
46
53
  readRouteCMatrixSQLiteLatestPreviewRows,
47
54
  readRouteCMatrixSQLiteLatestTimelineSeq,
55
+ readRouteCMatrixSQLiteLatestTimelineSeqs,
48
56
  readRouteCMatrixSQLiteNextStreamSeq,
49
57
  readRouteCMatrixSQLiteRoom,
50
58
  readRouteCMatrixSQLiteStateEvent,
@@ -904,16 +912,8 @@ function isObject(value) {
904
912
  return value !== null && typeof value === "object" && !Array.isArray(value);
905
913
  }
906
914
 
907
- function stackRootFromConfigDir() {
908
- return basename(CONFIG_DIR) === "host" ? dirname(CONFIG_DIR) : CONFIG_DIR;
909
- }
910
-
911
915
  export function getRouteCMatrixStoragePath() {
912
- const configured = process.env.OYSTERUN_ROUTEC_MATRIX_STORAGE_PATH;
913
- if (typeof configured === "string" && configured.trim()) {
914
- return configured.trim();
915
- }
916
- return join(stackRootFromConfigDir(), "matrix", "homeserver.db");
916
+ return routeCMatrixJsonStoragePath(CONFIG_DIR, process.env.OYSTERUN_ROUTEC_MATRIX_STORAGE_PATH);
917
917
  }
918
918
 
919
919
  export function getRouteCMatrixStorageDeltaPath(
@@ -2292,7 +2292,37 @@ function ensureRoomInStore(store, binding) {
2292
2292
  return room;
2293
2293
  }
2294
2294
 
2295
+ const transcriptReadOnlyScope = new AsyncLocalStorage();
2296
+
2297
+ // Synchronous adapter consumers share one read-only DB and one bound room.
2298
+ // No cached writable opener, schema initializer, or room creator is used.
2299
+ export function withRouteCMatrixTranscriptReadOnly({ binding }, read) {
2300
+ requireBinding(binding);
2301
+ const db = openRouteCMatrixSQLiteReadOnlyStore({ jsonStoragePath: getRouteCMatrixStoragePath() });
2302
+ try {
2303
+ const room = readRouteCMatrixSQLiteRoom(db, { roomId: binding.matrix_room_id });
2304
+ if (!room) {
2305
+ const error = new Error("Durable Matrix transcript room is unavailable");
2306
+ error.code = "MATRIX_TRANSCRIPT_ROOM_UNAVAILABLE";
2307
+ throw error;
2308
+ }
2309
+ if (room.host_session_id !== binding.host_session_id || room.host_agent_id !== binding.host_agent_id) {
2310
+ const error = new Error("Durable Matrix transcript room ownership does not match binding");
2311
+ error.code = "MATRIX_TRANSCRIPT_BINDING_MISMATCH";
2312
+ throw error;
2313
+ }
2314
+ validateBoundRoomRecord(room, binding);
2315
+ return transcriptReadOnlyScope.run({ db, roomId: binding.matrix_room_id }, () => {
2316
+ const result = read();
2317
+ if (result?.then) throw new Error("Transcript read-only scope requires synchronous reads");
2318
+ return result;
2319
+ });
2320
+ } finally { db.close(); }
2321
+ }
2322
+
2295
2323
  function openRouteCMatrixAdapterSQLiteStore() {
2324
+ const scope = transcriptReadOnlyScope.getStore();
2325
+ if (scope) return scope.db;
2296
2326
  return openRouteCMatrixSQLiteStore({
2297
2327
  jsonStoragePath: getRouteCMatrixStoragePath(),
2298
2328
  });
@@ -2426,12 +2456,14 @@ export function getRouteCMatrixRoomTimelineReplaySourceProof({
2426
2456
  };
2427
2457
  }
2428
2458
 
2429
- export function readRouteCMatrixRoomLatestPreviewCheckpoint({ binding }) {
2459
+ export function readRouteCMatrixRoomLatestPreviewCheckpoint({ binding, readContext = null }) {
2430
2460
  requireBinding(binding);
2431
- const db = openRouteCMatrixAdapterSQLiteStore();
2461
+ const db = readContext === null ? openRouteCMatrixAdapterSQLiteStore()
2462
+ : routeCMatrixSQLiteRoomReadContextDB(readContext, binding.matrix_room_id);
2432
2463
  return {
2433
2464
  latest_committed_seq: readRouteCMatrixSQLiteLatestTimelineSeq(db, {
2434
2465
  roomId: binding.matrix_room_id,
2466
+ readContext,
2435
2467
  }),
2436
2468
  dedicated_preview_query: true,
2437
2469
  full_transcript_pagination_used: false,
@@ -2440,11 +2472,29 @@ export function readRouteCMatrixRoomLatestPreviewCheckpoint({ binding }) {
2440
2472
  };
2441
2473
  }
2442
2474
 
2443
- export function readRouteCMatrixRoomLatestPreviewRows({ binding, limit = 20 }) {
2444
- requireBinding(binding);
2475
+ export function readRouteCMatrixRoomLatestPreviewCheckpoints({ bindings }) {
2476
+ const selected = [];
2477
+ const seen = new Set();
2478
+ for (const binding of Array.isArray(bindings) ? bindings : []) {
2479
+ requireBinding(binding);
2480
+ if (!seen.has(binding.matrix_room_id)) {
2481
+ seen.add(binding.matrix_room_id);
2482
+ selected.push(binding);
2483
+ }
2484
+ }
2445
2485
  const db = openRouteCMatrixAdapterSQLiteStore();
2486
+ return readRouteCMatrixSQLiteLatestTimelineSeqs(db, {
2487
+ roomIds: selected.map((binding) => binding.matrix_room_id),
2488
+ });
2489
+ }
2490
+
2491
+ export function readRouteCMatrixRoomLatestPreviewRows({ binding, limit = 20, readContext = null }) {
2492
+ requireBinding(binding);
2493
+ const db = readContext === null ? openRouteCMatrixAdapterSQLiteStore()
2494
+ : routeCMatrixSQLiteRoomReadContextDB(readContext, binding.matrix_room_id);
2446
2495
  const rows = readRouteCMatrixSQLiteLatestPreviewRows(db, {
2447
2496
  roomId: binding.matrix_room_id,
2497
+ readContext,
2448
2498
  limit,
2449
2499
  });
2450
2500
  return {
@@ -2477,7 +2527,11 @@ export function readRouteCMatrixRoomTimelinePreview({
2477
2527
  });
2478
2528
  const countLimited = candidates.length > normalizedLimit;
2479
2529
  const preBudgetChunk = candidates.slice(0, normalizedLimit).reverse();
2480
- const preBudgetClientEvents = preBudgetChunk.map(eventForClientMatrixRead);
2530
+ const preBudgetClientEvents = eventsForClientMatrixRead(
2531
+ db,
2532
+ preBudgetChunk,
2533
+ binding
2534
+ );
2481
2535
  const budgeted = selectNewestClientEventsWithinTransferBudget({
2482
2536
  rawEvents: preBudgetChunk,
2483
2537
  clientEvents: preBudgetClientEvents,
@@ -3335,6 +3389,100 @@ function eventForClientMatrixRead(event) {
3335
3389
  return clientEvent;
3336
3390
  }
3337
3391
 
3392
+ function eventsForClientMatrixRead(db, events, binding) {
3393
+ const clientEvents = events.map(eventForClientMatrixRead);
3394
+ return decorateMatrixEventsWithCompletedTurnProjection(db, clientEvents, {
3395
+ roomId: binding.matrix_room_id,
3396
+ hostSessionId: binding.host_session_id,
3397
+ getSession: (sessionId) =>
3398
+ sessionId === binding.host_session_id
3399
+ ? {
3400
+ id: binding.host_session_id,
3401
+ sessionName: binding.host_session_name,
3402
+ }
3403
+ : null,
3404
+ });
3405
+ }
3406
+
3407
+ function parseMatrixObject(value) {
3408
+ if (isObject(value)) return value;
3409
+ if (typeof value !== "string") return {};
3410
+ try {
3411
+ const parsed = JSON.parse(value);
3412
+ return isObject(parsed) ? parsed : {};
3413
+ } catch {
3414
+ return {};
3415
+ }
3416
+ }
3417
+
3418
+ function completedAssistantRecoveryEventIds(clientEvents) {
3419
+ const loadedEventIds = new Set(
3420
+ clientEvents
3421
+ .map((event) => normalizeNonEmptyString(event?.event_id))
3422
+ .filter(Boolean)
3423
+ );
3424
+ const recoveryEventIds = [];
3425
+ for (const event of clientEvents) {
3426
+ const unsigned = parseMatrixObject(event?.unsigned);
3427
+ const projection = unsigned[MATRIX_COMPLETED_TURN_PROJECTION_KEY];
3428
+ if (
3429
+ !isObject(projection) ||
3430
+ projection.schema !== "local_turn_completion.v1" ||
3431
+ projection.successful !== true ||
3432
+ projection.conflict === true
3433
+ ) {
3434
+ continue;
3435
+ }
3436
+ const eventId = normalizeNonEmptyString(projection.final_assistant_event_id);
3437
+ if (!eventId || loadedEventIds.has(eventId)) continue;
3438
+ loadedEventIds.add(eventId);
3439
+ recoveryEventIds.push(eventId);
3440
+ }
3441
+ return recoveryEventIds;
3442
+ }
3443
+
3444
+ function recoverCompletedAssistantSyncCompanions({
3445
+ db,
3446
+ binding,
3447
+ rawEvents,
3448
+ clientEvents,
3449
+ }) {
3450
+ const recoveredRawEvents = [];
3451
+ for (const eventId of completedAssistantRecoveryEventIds(clientEvents)) {
3452
+ const event = readRouteCMatrixSQLiteEvent(db, {
3453
+ roomId: binding.matrix_room_id,
3454
+ eventId,
3455
+ });
3456
+ const content = parseMatrixObject(event?.content_json ?? event?.content);
3457
+ const semantic = content[OYSTERUN_SEMANTIC_NAMESPACE];
3458
+ if (
3459
+ !event ||
3460
+ event.type !== "m.room.message" ||
3461
+ !isObject(semantic) ||
3462
+ semantic.semantic_type !== "message.assistant" ||
3463
+ content.delta === true
3464
+ ) {
3465
+ continue;
3466
+ }
3467
+ recoveredRawEvents.push(event);
3468
+ }
3469
+ if (recoveredRawEvents.length === 0) {
3470
+ return { rawEvents, clientEvents, recoveredCount: 0 };
3471
+ }
3472
+ const byEventId = new Map();
3473
+ for (const event of [...rawEvents, ...recoveredRawEvents]) {
3474
+ byEventId.set(event.event_id, event);
3475
+ }
3476
+ const mergedRawEvents = [...byEventId.values()].sort(
3477
+ (left, right) => eventStreamSeq(left) - eventStreamSeq(right)
3478
+ );
3479
+ return {
3480
+ rawEvents: mergedRawEvents,
3481
+ clientEvents: eventsForClientMatrixRead(db, mergedRawEvents, binding),
3482
+ recoveredCount: mergedRawEvents.length - rawEvents.length,
3483
+ };
3484
+ }
3485
+
3338
3486
  function summarizeClientTransferBytes(clientEvents) {
3339
3487
  const eventByteCounts = clientEvents
3340
3488
  .map((event) => safeJsonByteLength(event))
@@ -4199,6 +4347,8 @@ function selectMessagesChunk({ events, startSeq, toSeq, direction, limit }) {
4199
4347
  }
4200
4348
 
4201
4349
  function applyMessagesTransferBudgetToWindow({
4350
+ db,
4351
+ binding,
4202
4352
  chunk,
4203
4353
  countLimited,
4204
4354
  startSeq,
@@ -4206,7 +4356,7 @@ function applyMessagesTransferBudgetToWindow({
4206
4356
  direction,
4207
4357
  budgetBytes = P135_MATRIX_CLIENT_TRANSFER_BUDGET_BYTES,
4208
4358
  }) {
4209
- const clientEvents = chunk.map(eventForClientMatrixRead);
4359
+ const clientEvents = eventsForClientMatrixRead(db, chunk, binding);
4210
4360
  const selectedRaw = [];
4211
4361
  const selectedClient = [];
4212
4362
  const selectedByteCounts = [];
@@ -4392,6 +4542,10 @@ function clientTransferBudgetProof({
4392
4542
  messages_gap_fill_direction: timeline.limited ? "b" : null,
4393
4543
  p117_current_since_long_poll_preserved: true,
4394
4544
  p131_projection_boundary_preserved: true,
4545
+ completed_assistant_recovery_count:
4546
+ transferBudget?.completedAssistantRecoveryCount ?? 0,
4547
+ completed_assistant_recovery_contract:
4548
+ "successful_completed_turn_final_assistant_companion_v1",
4395
4549
  p82_large_tool_spillover_boundary_preserved: true,
4396
4550
  foundation_pass_claimed: false,
4397
4551
  };
@@ -4461,15 +4615,41 @@ function buildSyncBody({
4461
4615
  sinceSeq,
4462
4616
  limited: sqliteTimeline.limited,
4463
4617
  });
4464
- const initialClientTimelineEvents = initialTimeline.events.map(eventForClient);
4465
- const {
4466
- timeline,
4467
- clientEvents: clientTimelineEvents,
4468
- transferBudget,
4469
- } = applyTransferBudgetToSyncTimeline({
4618
+ const initialClientTimelineEvents = eventsForClientMatrixRead(
4619
+ db,
4620
+ initialTimeline.events,
4621
+ binding
4622
+ );
4623
+ const budgetedTimeline = applyTransferBudgetToSyncTimeline({
4470
4624
  timeline: initialTimeline,
4471
4625
  clientEvents: initialClientTimelineEvents,
4472
4626
  });
4627
+ const completionRecovery = recoverCompletedAssistantSyncCompanions({
4628
+ db,
4629
+ binding,
4630
+ rawEvents: budgetedTimeline.timeline.events,
4631
+ clientEvents: budgetedTimeline.clientEvents,
4632
+ });
4633
+ const timeline =
4634
+ completionRecovery.recoveredCount > 0
4635
+ ? {
4636
+ ...budgetedTimeline.timeline,
4637
+ events: completionRecovery.rawEvents,
4638
+ oldestReturnedSeq: eventSeqOrNull(completionRecovery.rawEvents[0]),
4639
+ newestReturnedSeq: eventSeqOrNull(
4640
+ completionRecovery.rawEvents.at(-1)
4641
+ ),
4642
+ prevBatchSeq:
4643
+ eventSeqOrNull(completionRecovery.rawEvents[0]) ??
4644
+ budgetedTimeline.timeline.prevBatchSeq,
4645
+ budgetMode: `${budgetedTimeline.timeline.budgetMode}_completed_assistant_recovery`,
4646
+ }
4647
+ : budgetedTimeline.timeline;
4648
+ const clientTimelineEvents = completionRecovery.clientEvents;
4649
+ const transferBudget = {
4650
+ ...budgetedTimeline.transferBudget,
4651
+ completedAssistantRecoveryCount: completionRecovery.recoveredCount,
4652
+ };
4473
4653
  const memberships = roomMembershipEvents(binding, room);
4474
4654
  const stateEvents = sinceSeq === null
4475
4655
  ? roomStateEvents(binding, room)
@@ -4595,7 +4775,7 @@ function readEvent({ binding, eventId }) {
4595
4775
  if (!event) {
4596
4776
  return null;
4597
4777
  }
4598
- return eventForClient(event);
4778
+ return eventsForClientMatrixRead(db, [event], binding)[0];
4599
4779
  }
4600
4780
 
4601
4781
  export function readRouteCMatrixEvent({ binding, eventId }) {
@@ -4630,6 +4810,8 @@ export function readRouteCMatrixRoomMessages({
4630
4810
  limit: normalizedLimit,
4631
4811
  });
4632
4812
  const selected = applyMessagesTransferBudgetToWindow({
4813
+ db,
4814
+ binding,
4633
4815
  chunk: window.chunk,
4634
4816
  countLimited: window.countLimited,
4635
4817
  startSeq,
@@ -4771,9 +4953,9 @@ export function readRouteCMatrixRoomContext({
4771
4953
  start,
4772
4954
  end,
4773
4955
  state: roomStateEvents(binding, room),
4774
- events_before: beforeRaw.map(eventForClientMatrixRead),
4775
- events_after: afterRaw.map(eventForClientMatrixRead),
4776
- event: eventForClientMatrixRead(targetEvent),
4956
+ events_before: eventsForClientMatrixRead(db, beforeRaw, binding),
4957
+ events_after: eventsForClientMatrixRead(db, afterRaw, binding),
4958
+ event: eventsForClientMatrixRead(db, [targetEvent], binding)[0],
4777
4959
  ...(diagnosticsEnabled
4778
4960
  ? {
4779
4961
  routec_context_focus_proof: {
@@ -4914,6 +5096,7 @@ export async function handleRouteCMatrixStorageRequest({
4914
5096
  }) {
4915
5097
  const effectiveBinding = binding || {
4916
5098
  host_session_id: tokenRecord?.host_session_id,
5099
+ host_session_name: tokenRecord?.host_session_name,
4917
5100
  host_agent_id: tokenRecord?.host_agent_id,
4918
5101
  matrix_room_id: tokenRecord?.matrix_room_id,
4919
5102
  matrix_user_id: tokenRecord?.matrix_user_id,
@@ -0,0 +1,12 @@
1
+ import { basename, dirname, join } from "node:path";
2
+
3
+ // Pure path contract shared by storage and lifecycle ownership inspection.
4
+ export function routeCMatrixJsonStoragePath(configDir, configuredPath) {
5
+ if (typeof configuredPath === "string" && configuredPath.trim()) return configuredPath.trim();
6
+ const stackRoot = basename(configDir) === "host" ? dirname(configDir) : configDir;
7
+ return join(stackRoot, "matrix", "homeserver.db");
8
+ }
9
+
10
+ export function routeCMatrixSQLiteStoragePath(jsonStoragePath) {
11
+ return join(dirname(jsonStoragePath), "homeserver.sqlite");
12
+ }
@@ -0,0 +1,55 @@
1
+ import { monitorEventLoopDelay, performance } from "node:perf_hooks";
2
+
3
+ export function createRouteCRequestDelayWindowController({
4
+ createMonitor = () => monitorEventLoopDelay({ resolution: 10 }),
5
+ now = () => new Date(),
6
+ performanceNow = () => performance.now(),
7
+ } = {}) {
8
+ let activeWindow = null;
9
+
10
+ function sync(logPath) {
11
+ if (!logPath) {
12
+ activeWindow?.monitor.disable();
13
+ activeWindow = null;
14
+ return null;
15
+ }
16
+ if (activeWindow?.logPath === logPath) return activeWindow;
17
+
18
+ activeWindow?.monitor.disable();
19
+ const monitor = createMonitor();
20
+ monitor.enable();
21
+ activeWindow = {
22
+ logPath,
23
+ monitor,
24
+ startedAt: now(),
25
+ startedPerformanceMs: performanceNow(),
26
+ };
27
+ return activeWindow;
28
+ }
29
+
30
+ function snapshot(window = activeWindow) {
31
+ if (!window) return null;
32
+ const count = window.monitor.count;
33
+ const milliseconds = (nanoseconds) =>
34
+ count > 0 && Number.isFinite(nanoseconds)
35
+ ? Number((nanoseconds / 1_000_000).toFixed(3))
36
+ : null;
37
+ return {
38
+ monitor: "node_perf_hooks_monitor_event_loop_delay",
39
+ resolution_ms: 10,
40
+ window_started_at: window.startedAt.toISOString(),
41
+ window_elapsed_ms: Number(
42
+ (performanceNow() - window.startedPerformanceMs).toFixed(3)
43
+ ),
44
+ samples: count,
45
+ min_ms: milliseconds(window.monitor.min),
46
+ max_ms: milliseconds(window.monitor.max),
47
+ mean_ms: milliseconds(window.monitor.mean),
48
+ p50_ms: milliseconds(window.monitor.percentile(50)),
49
+ p95_ms: milliseconds(window.monitor.percentile(95)),
50
+ p99_ms: milliseconds(window.monitor.percentile(99)),
51
+ };
52
+ }
53
+
54
+ return { sync, snapshot };
55
+ }
@@ -2,8 +2,9 @@ import { randomUUID } from "crypto";
2
2
  import { ClaudeCodeAdapter } from "./adapters/claude-code-adapter.mjs";
3
3
  import { readConfig } from "./config.mjs";
4
4
  import { getConfiguredProviderCommand } from "./provider-registry.mjs";
5
+ import { DEFAULT_SCHEDULER_RUN_TIMEOUT_SECONDS } from "./scheduler-run-timeout-policy.mjs";
5
6
 
6
- const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
7
+ const DEFAULT_TIMEOUT_MS = DEFAULT_SCHEDULER_RUN_TIMEOUT_SECONDS * 1000;
7
8
  const DEFAULT_GRACEFUL_EXIT_MS = 5_000;
8
9
  const DEFAULT_FORCED_EXIT_MS = 2_000;
9
10
  const MAX_CAPTURE_CHARS = 24_000;
@@ -8,8 +8,12 @@ import {
8
8
  getConfiguredProviderCommand,
9
9
  isConfiguredCommandAvailable,
10
10
  } from "./provider-registry.mjs";
11
+ import { DEFAULT_SCHEDULER_RUN_TIMEOUT_SECONDS } from "./scheduler-run-timeout-policy.mjs";
11
12
 
12
- const DEFAULT_SCHEDULER_DIRECT_RUN_TIMEOUT_MS = 30 * 60 * 1000;
13
+ const DEFAULT_SCHEDULER_DIRECT_RUN_TIMEOUT_MS =
14
+ DEFAULT_SCHEDULER_RUN_TIMEOUT_SECONDS * 1000;
15
+ const DEFAULT_GRACEFUL_EXIT_MS = 5_000;
16
+ const DEFAULT_FORCED_EXIT_MS = 2_000;
13
17
  const MAX_CAPTURE_CHARS = 24_000;
14
18
  const MAX_SUMMARY_CHARS = 700;
15
19
 
@@ -208,6 +212,9 @@ export async function runSchedulerDirectProviderCommand({
208
212
  redactionValues = [],
209
213
  spawnFn = spawn,
210
214
  timeoutMs = DEFAULT_SCHEDULER_DIRECT_RUN_TIMEOUT_MS,
215
+ gracefulExitMs = DEFAULT_GRACEFUL_EXIT_MS,
216
+ forcedExitMs = DEFAULT_FORCED_EXIT_MS,
217
+ signal: abortSignal = null,
211
218
  } = {}) {
212
219
  const provider = requireString(runtime?.provider, "provider");
213
220
  const resolvedCwd = requireString(cwd, "cwd");
@@ -227,12 +234,15 @@ export async function runSchedulerDirectProviderCommand({
227
234
  let stdoutTruncated = false;
228
235
  let stderrTruncated = false;
229
236
  let settled = false;
237
+ let terminalReason = null;
238
+ let childExited = false;
230
239
  let timeout = null;
231
240
  let child;
232
241
  const finish = (result) => {
233
242
  if (settled) return;
234
243
  settled = true;
235
244
  if (timeout) clearTimeout(timeout);
245
+ abortSignal?.removeEventListener?.("abort", onAbort);
236
246
  const safeStdout = redactText(stdout, redactionTokens);
237
247
  const safeStderr = redactText(stderr, redactionTokens);
238
248
  resolve({
@@ -248,6 +258,11 @@ export async function runSchedulerDirectProviderCommand({
248
258
  stdout_truncated: stdoutTruncated,
249
259
  stderr_truncated: stderrTruncated,
250
260
  redacted_sensitive_env: redactionTokens.length > 0,
261
+ lifecycle_state: result.ok
262
+ ? "succeeded"
263
+ : result.error_summary || result.signal || "failed",
264
+ provider_process_exited: childExited,
265
+ provider_force_killed: result.provider_force_killed === true,
251
266
  log_summary: buildSummary({
252
267
  commandLabel,
253
268
  exitCode: result.exit_code,
@@ -257,6 +272,50 @@ export async function runSchedulerDirectProviderCommand({
257
272
  }),
258
273
  });
259
274
  };
275
+ const waitForExit = (waitMs) => {
276
+ if (childExited) return Promise.resolve(true);
277
+ return new Promise((resolve) => {
278
+ let done = false;
279
+ const complete = (value) => {
280
+ if (done) return;
281
+ done = true;
282
+ clearTimeout(waitTimer);
283
+ child?.off?.("close", onExit);
284
+ child?.off?.("exit", onExit);
285
+ resolve(value);
286
+ };
287
+ const onExit = () => complete(true);
288
+ const waitTimer = setTimeout(() => complete(childExited), waitMs);
289
+ child?.on?.("close", onExit);
290
+ child?.on?.("exit", onExit);
291
+ });
292
+ };
293
+ const terminate = async (reason) => {
294
+ if (settled || terminalReason) return;
295
+ terminalReason = reason;
296
+ child?.kill?.("SIGTERM");
297
+ let forceKilled = false;
298
+ let exited = await waitForExit(gracefulExitMs);
299
+ if (!exited) {
300
+ forceKilled = true;
301
+ child?.kill?.("SIGKILL");
302
+ exited = await waitForExit(forcedExitMs);
303
+ }
304
+ finish({
305
+ ok: false,
306
+ exit_code: null,
307
+ signal: reason === "timeout" ? "timeout" : "cancelled",
308
+ error_summary: exited
309
+ ? reason === "timeout"
310
+ ? "scheduler_provider_run_timed_out"
311
+ : "scheduler_provider_run_cancelled"
312
+ : "scheduler_provider_cleanup_failed",
313
+ provider_force_killed: forceKilled,
314
+ });
315
+ };
316
+ const onAbort = () => {
317
+ void terminate("cancelled");
318
+ };
260
319
  try {
261
320
  child = spawnFn(command, args, {
262
321
  cwd: resolvedCwd,
@@ -290,6 +349,8 @@ export async function runSchedulerDirectProviderCommand({
290
349
  reject(err);
291
350
  });
292
351
  child.on("close", (code, signal) => {
352
+ childExited = true;
353
+ if (terminalReason) return;
293
354
  finish({
294
355
  ok: code === 0,
295
356
  exit_code: typeof code === "number" ? code : null,
@@ -297,21 +358,22 @@ export async function runSchedulerDirectProviderCommand({
297
358
  });
298
359
  });
299
360
  child.on("exit", (code, signal) => {
361
+ childExited = true;
362
+ if (terminalReason) return;
300
363
  finish({
301
364
  ok: code === 0,
302
365
  exit_code: typeof code === "number" ? code : null,
303
366
  signal: signal || null,
304
367
  });
305
368
  });
306
- if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
369
+ if (abortSignal?.aborted) {
370
+ onAbort();
371
+ } else {
372
+ abortSignal?.addEventListener?.("abort", onAbort, { once: true });
373
+ }
374
+ if (!terminalReason && Number.isFinite(timeoutMs) && timeoutMs > 0) {
307
375
  timeout = setTimeout(() => {
308
- if (settled) return;
309
- child.kill?.("SIGTERM");
310
- finish({
311
- ok: false,
312
- exit_code: null,
313
- signal: "timeout",
314
- });
376
+ void terminate("timeout");
315
377
  }, timeoutMs);
316
378
  timeout.unref?.();
317
379
  }
@@ -0,0 +1,85 @@
1
+ export const DEFAULT_SCHEDULER_RUN_TIMEOUT_SECONDS = 60 * 60;
2
+ export const MIN_SCHEDULER_RUN_TIMEOUT_SECONDS = 60;
3
+ export const MAX_SCHEDULER_RUN_TIMEOUT_SECONDS = 24 * 60 * 60;
4
+
5
+ function contractError(code, message) {
6
+ const error = new Error(message);
7
+ error.code = code;
8
+ return error;
9
+ }
10
+
11
+ export function normalizeSchedulerRunTimeoutSeconds(
12
+ value,
13
+ { fieldName = "run_timeout_seconds" } = {}
14
+ ) {
15
+ if (!Number.isInteger(value)) {
16
+ throw contractError(
17
+ "scheduler_run_timeout_invalid",
18
+ `${fieldName} must be a whole number of seconds`
19
+ );
20
+ }
21
+ if (
22
+ value < MIN_SCHEDULER_RUN_TIMEOUT_SECONDS ||
23
+ value > MAX_SCHEDULER_RUN_TIMEOUT_SECONDS
24
+ ) {
25
+ throw contractError(
26
+ "scheduler_run_timeout_out_of_range",
27
+ `${fieldName} must be between ${MIN_SCHEDULER_RUN_TIMEOUT_SECONDS} and ${MAX_SCHEDULER_RUN_TIMEOUT_SECONDS} seconds`
28
+ );
29
+ }
30
+ return value;
31
+ }
32
+
33
+ export function resolveSchedulerRunTimeout(
34
+ value,
35
+ { present = value !== undefined, source = null } = {}
36
+ ) {
37
+ if (!present) {
38
+ return {
39
+ seconds: DEFAULT_SCHEDULER_RUN_TIMEOUT_SECONDS,
40
+ source: source || "default_missing",
41
+ explicit: false,
42
+ };
43
+ }
44
+ const seconds = normalizeSchedulerRunTimeoutSeconds(value);
45
+ const normalizedSource = ["default", "default_missing"].includes(source)
46
+ ? source
47
+ : "explicit";
48
+ return {
49
+ seconds,
50
+ source: normalizedSource,
51
+ explicit: normalizedSource === "explicit",
52
+ };
53
+ }
54
+
55
+ export function parseSchedulerRunTimeoutDuration(value) {
56
+ if (typeof value !== "string" || !value.trim()) {
57
+ throw contractError(
58
+ "scheduler_run_timeout_invalid",
59
+ "--run-timeout requires a finite duration such as 15m, 2h, or 3600s"
60
+ );
61
+ }
62
+ const normalized = value.trim().toLowerCase();
63
+ if (["unlimited", "none", "infinite", "infinity", "no-limit"].includes(normalized)) {
64
+ throw contractError(
65
+ "scheduler_run_timeout_unlimited_forbidden",
66
+ "Scheduler run timeout must be finite; unlimited is not supported"
67
+ );
68
+ }
69
+ const match = normalized.match(/^([0-9]+)(s|m|h)$/);
70
+ if (!match) {
71
+ throw contractError(
72
+ "scheduler_run_timeout_invalid",
73
+ "--run-timeout must be a whole-number duration using s, m, or h"
74
+ );
75
+ }
76
+ const amount = Number(match[1]);
77
+ const multiplier = match[2] === "h" ? 3600 : match[2] === "m" ? 60 : 1;
78
+ return normalizeSchedulerRunTimeoutSeconds(amount * multiplier, {
79
+ fieldName: "--run-timeout",
80
+ });
81
+ }
82
+
83
+ export function schedulerRunTimeoutMilliseconds(value, options = {}) {
84
+ return resolveSchedulerRunTimeout(value, options).seconds * 1000;
85
+ }