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
@@ -5,6 +5,8 @@ import {
5
5
  getConfigPath,
6
6
  readRawConfigSource,
7
7
  } from "../config.mjs";
8
+ import { parseSessionTagsCliValue } from "../session-tags.mjs";
9
+ import { parseSchedulerRunTimeoutDuration } from "../scheduler-run-timeout-policy.mjs";
8
10
 
9
11
  export const P86_CLI_CONTRACT = "p86_product_cli_module_action_v1";
10
12
  export const P86_CLI_PROFILE_CONTRACT = "p86_cli_profile_0600_token_fallback_v1";
@@ -610,10 +612,10 @@ async function readCurrentSessionMessagesAround({ options, env, fetchFn }) {
610
612
  session_id: resolvedSessionId,
611
613
  session_ref: body?.session_ref || sessionTarget.session_ref || null,
612
614
  agent_id: normalizeString(body?.agent_id) || agentId,
613
- cross_session_search: crossSessionContext,
614
- context_surface: crossSessionContext
615
+ cross_session_search: body?.cross_session_search ?? crossSessionContext,
616
+ context_surface: body?.context_surface || (crossSessionContext
615
617
  ? "session_messages_matrix_target_session"
616
- : "session_messages_matrix_current_session",
618
+ : "session_messages_matrix_current_session"),
617
619
  };
618
620
  }
619
621
 
@@ -671,6 +673,14 @@ function boolPayload(options, sourceKey, targetKey, payload) {
671
673
  }
672
674
  }
673
675
 
676
+ function applySessionTagsOptions(options, body) {
677
+ const hasTags = Object.prototype.hasOwnProperty.call(options, "tags");
678
+ const clearTags = parseBooleanOption(options, "clearTags", false);
679
+ if (hasTags && clearTags) fail("--tags and --clear-tags are mutually exclusive");
680
+ if (hasTags) body.tags = parseSessionTagsCliValue(options.tags);
681
+ if (clearTags) body.tags = [];
682
+ }
683
+
674
684
  function sessionStartPayload(options, env = process.env) {
675
685
  const body = {
676
686
  agent_id: requireAgentId(options, env),
@@ -706,6 +716,7 @@ function sessionStartPayload(options, env = process.env) {
706
716
  body.telegram_allowed_users =
707
717
  allowedUsers === "." ? ["."] : allowedUsers.split(",").map((entry) => entry.trim()).filter(Boolean);
708
718
  }
719
+ applySessionTagsOptions(options, body);
709
720
  return body;
710
721
  }
711
722
 
@@ -720,6 +731,7 @@ function commaListOption(options, ...keys) {
720
731
 
721
732
  function sessionProfileUpdatePayload(options, env = process.env) {
722
733
  const body = {};
734
+ applySessionTagsOptions(options, body);
723
735
  boolPayload(options, "notificationsEnabled", "notifications_enabled", body);
724
736
  boolPayload(options, "webEnabled", "web_enabled", body);
725
737
  const webAccess = optionalOption(options, "webAccess", "access");
@@ -868,40 +880,15 @@ function resolveSessionUrlIdFromStatus(status, sessionTarget) {
868
880
  return resolvedSessionId;
869
881
  }
870
882
 
871
- async function resolveSessionMessagesTarget({ options, env, fetchFn }) {
883
+ async function resolveSessionMessagesTarget({ options, env }) {
872
884
  const sessionTarget = requireSessionTarget(options, env);
885
+ // Host resolves durable target ownership. Explicit agent is only an assertion.
873
886
  const explicitAgentId = resolveAgentId(options, {});
874
- const envSessionId = normalizeString(env.OYSTERUN_SESSION_ID);
875
- const envAgentId = normalizeString(env.OYSTERUN_AGENT_ID);
876
- if (
877
- sessionTarget.session_id &&
878
- (explicitAgentId || (envAgentId && sessionTarget.session_id === envSessionId))
879
- ) {
880
- return {
881
- sessionTarget,
882
- sessionId: sessionTarget.session_id,
883
- agentId: explicitAgentId || envAgentId,
884
- };
885
- }
886
- const status = await resolveLiveSessionTargetStatus({
887
- options,
888
- env,
889
- fetchFn,
890
- sessionTarget,
891
- });
892
- const statusAgentId = normalizeString(status?.agent_id);
893
- if (explicitAgentId && statusAgentId && explicitAgentId !== statusAgentId) {
894
- fail("target session agent id does not match explicit --agent-id");
895
- }
896
- const agentId =
897
- explicitAgentId ||
898
- statusAgentId ||
899
- resolveAgentId(options, env);
900
- if (!agentId) fail("target session agent id is required");
887
+ const sameCurrent = sessionTarget.session_id === normalizeString(env.OYSTERUN_SESSION_ID);
901
888
  return {
902
889
  sessionTarget,
903
- sessionId: normalizeString(status?.session_id) || sessionTarget.session_id || null,
904
- agentId,
890
+ sessionId: sessionTarget.session_id || null,
891
+ agentId: explicitAgentId || (sameCurrent ? normalizeString(env.OYSTERUN_AGENT_ID) : ""),
905
892
  };
906
893
  }
907
894
 
@@ -1052,6 +1039,7 @@ async function runSessions({ action, rest, options, env, fetchFn }) {
1052
1039
  session_id: status.session_id || sessionTarget.session_id || null,
1053
1040
  session_ref: sessionTarget.session_ref || null,
1054
1041
  session_name: status.session_name || null,
1042
+ tags: Array.isArray(status.tags) ? status.tags : [],
1055
1043
  agent_id: status.agent_id || null,
1056
1044
  profile_config: status.profile_config || {},
1057
1045
  notification_settings: status.notification_settings || null,
@@ -1103,6 +1091,9 @@ async function runSessions({ action, rest, options, env, fetchFn }) {
1103
1091
  })).data;
1104
1092
  result.profile_config_update = profileResult;
1105
1093
  result.session_id = profileResult.session_id || result.session_id;
1094
+ result.tags = Array.isArray(profileResult.tags)
1095
+ ? profileResult.tags
1096
+ : [];
1106
1097
  }
1107
1098
  if (!sessionName && !hasSessionProfileUpdateBody(profileBody)) {
1108
1099
  fail("profile update requires at least one typed field");
@@ -1362,6 +1353,7 @@ async function runChat({ action, rest, options, env, fetchFn }) {
1362
1353
  agent_id: targetContext.agentId,
1363
1354
  limit: optionalOption(options, "limit"),
1364
1355
  after_seq: optionalOption(options, "afterSeq"),
1356
+ next_batch: optionalOption(options, "nextBatch"),
1365
1357
  })}`,
1366
1358
  options,
1367
1359
  env,
@@ -1471,6 +1463,15 @@ function schedulerPayload(options) {
1471
1463
  const weekdays = commaListOption(options, "weekdays", "selectedWeekdays");
1472
1464
  if (weekdays) body.weekdays = weekdays;
1473
1465
  boolPayload(options, "enabled", "enabled", body);
1466
+ if (Object.prototype.hasOwnProperty.call(options, "runTimeout")) {
1467
+ try {
1468
+ body.run_timeout_seconds = parseSchedulerRunTimeoutDuration(
1469
+ options.runTimeout
1470
+ );
1471
+ } catch (error) {
1472
+ fail(error.message);
1473
+ }
1474
+ }
1474
1475
  return body;
1475
1476
  }
1476
1477
 
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ // Offline compatibility entry for Hosts predating initial-preflight terminalization.
3
+ // The service must be stopped; this never owns a restore, install, or snapshot.
4
+ import { readHostRuntimeMetadata } from "../host-runtime-diagnostics.mjs";
5
+ import { processDefinitelyAbsent, reconcileFailedInitialPreflight } from "../initial-preflight-recovery.mjs";
6
+ import { isUpdateOperationBlocking } from "../update-reminder-state.mjs";
7
+ const args = process.argv.slice(2);
8
+ if (args.length !== 4 || args[0] !== "--config-dir" || args[2] !== "--operation-id") {
9
+ throw new Error("Required: --config-dir <exact stopped Host config> --operation-id <exact failed attempt>");
10
+ }
11
+ const configDir = args[1];
12
+ const runtime = readHostRuntimeMetadata(configDir);
13
+ if (!runtime || !processDefinitelyAbsent(runtime.pid)) {
14
+ throw new Error("Offline reconciliation requires affirmative stopped Host process evidence.");
15
+ }
16
+ const operation = reconcileFailedInitialPreflight({ configDir, currentVersion: runtime.package_version, expectedOperationId: args[3] });
17
+ if (isUpdateOperationBlocking(operation) || operation.status !== "update_check_failed" ||
18
+ operation.reconciliation_outcome !== "abandoned_initial_preflight_failed") throw new Error("Retained operation is not a proven abandoned initial-preflight failure.");
19
+ console.log(JSON.stringify({ operation_id: operation.operation_id, status: operation.status, reconciliation_outcome: operation.reconciliation_outcome }));
@@ -0,0 +1,24 @@
1
+ import { join, resolve } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { readUserHostPort } from "../host-port-resolution.mjs";
4
+ import { readUpdateOperationState, isUpdateOperationBlocking } from "../update-reminder-state.mjs";
5
+ import { continuityReceiptPath } from "../lifecycle-continuity.mjs";
6
+ import { runStagedLifecycleRecovery } from "./run-staged-lifecycle-recovery.mjs";
7
+ try {
8
+ const [configDir, activePackage] = process.argv.slice(2);
9
+ const port = readUserHostPort({ configPath: join(configDir, "config.json"), runtimePath: join(configDir, "run", "host-runtime.json"), configDir });
10
+ const operation = readUpdateOperationState({ configDir });
11
+ if (operation.continuity_path !== continuityReceiptPath(configDir, operation.operation_id)) throw new Error("Retained lifecycle has no exact continuity receipt; preserve it and use its reported recovery path.");
12
+ const receipt = JSON.parse(readFileSync(operation.continuity_path, "utf8"));
13
+ if (receipt.baseline?.host?.port !== port ||
14
+ resolve(receipt.baseline?.host?.config_dir || "") !== resolve(configDir) ||
15
+ resolve(receipt.baseline?.host?.package_root || "") !== resolve(activePackage) ||
16
+ resolve(receipt.resources?.active_package_root || "") !== resolve(activePackage)) {
17
+ throw new Error("Retained recovery differs from the selected Host port/config/package; its package was preserved.");
18
+ }
19
+ await runStagedLifecycleRecovery({ configDir, stack: "production", port: String(port) });
20
+ if (isUpdateOperationBlocking(readUpdateOperationState({ configDir }))) throw new Error("Retained lifecycle cleanup is still blocking; preserve its package and receipts.");
21
+ } catch (error) {
22
+ console.error(`Installer lifecycle recovery failed: ${error.message}`);
23
+ process.exitCode = 1;
24
+ }
@@ -61,6 +61,8 @@ function printBoundedStatus(payload) {
61
61
  const operation = payload.update_operation || payload.operation || {};
62
62
  const fields = {
63
63
  status: payload.status || operation.status || "unknown",
64
+ operation_status:
65
+ operation.status || payload.operation_status || null,
64
66
  operation_id: operation.operation_id || payload.operation_id || null,
65
67
  transaction_kind:
66
68
  operation.transaction_kind || payload.transaction_kind || null,
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import { buildHostLifecycleRecoveryPlan } from "../host-lifecycle-recovery.mjs";
8
+ import { readContinuity, continuityReceiptPath } from "../lifecycle-continuity.mjs";
9
+ import {
10
+ verifyPersistedNodeNpmRuntimeIdentityAuthoritatively,
11
+ } from "../node-npm-runtime-identity.mjs";
12
+ import {
13
+ readUpdateOperationState,
14
+ } from "../update-reminder-state.mjs";
15
+ import { runHostLifecycleTransaction } from "../../tool_scripts/oysterun_update_restart_runner.mjs";
16
+
17
+ function normalizeString(value) {
18
+ return typeof value === "string" && value.trim() ? value.trim() : null;
19
+ }
20
+
21
+ function isProcessRunning(pid) {
22
+ if (!Number.isInteger(pid) || pid <= 0) return false;
23
+ try {
24
+ process.kill(pid, 0);
25
+ return true;
26
+ } catch (error) {
27
+ if (error?.code === "ESRCH") return false;
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ function parseJsonObject(path, label) {
33
+ const value = JSON.parse(readFileSync(path, "utf8"));
34
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
35
+ throw new Error(`${label} must contain a JSON object`);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ export async function runStagedLifecycleRecovery({
41
+ configDir = process.env.OYSTERUN_CONFIG_DIR,
42
+ stack = process.env.OYSTERUN_RELEASE_STACK || "production",
43
+ port = process.env.OYSTERUN_PORT || null,
44
+ readOperation = readUpdateOperationState,
45
+ readReceipt = readContinuity,
46
+ buildRecoveryPlan = buildHostLifecycleRecoveryPlan,
47
+ verifyIdentity = verifyPersistedNodeNpmRuntimeIdentityAuthoritatively,
48
+ readPreparation = parseJsonObject,
49
+ runnerIsAlive = isProcessRunning,
50
+ runTransaction = runHostLifecycleTransaction,
51
+ } = {}) {
52
+ const exactConfigDir = normalizeString(configDir);
53
+ if (!exactConfigDir || resolve(exactConfigDir) !== exactConfigDir) {
54
+ throw new Error("OYSTERUN_CONFIG_DIR must be an absolute path");
55
+ }
56
+ const operation = readOperation({ configDir: exactConfigDir });
57
+ const expectedContinuityPath = continuityReceiptPath(
58
+ exactConfigDir,
59
+ operation.operation_id
60
+ );
61
+ if (
62
+ !operation.operation_id ||
63
+ operation.continuity_path !== expectedContinuityPath
64
+ ) {
65
+ throw new Error("retained lifecycle operation has no exact continuity binding");
66
+ }
67
+ const receipt = readReceipt(
68
+ operation.continuity_path,
69
+ operation.operation_id
70
+ );
71
+ const runnerAlive = runnerIsAlive(operation.runner_pid);
72
+ const recoveryPlan = buildRecoveryPlan({
73
+ operation,
74
+ continuityReceipt: receipt,
75
+ runnerAlive,
76
+ });
77
+ if (
78
+ recoveryPlan?.blocking !== true ||
79
+ recoveryPlan?.user_action?.kind !== "restore_and_restart" ||
80
+ operation.running === true ||
81
+ runnerAlive
82
+ ) {
83
+ throw new Error(
84
+ "retained lifecycle operation is not eligible for staged restore"
85
+ );
86
+ }
87
+ const baseline = receipt.baseline?.host || {};
88
+ const resources = receipt.resources || {};
89
+ const exactPort = Number(port || baseline.port);
90
+ if (
91
+ receipt.transaction_kind === "restart_only" ||
92
+ resolve(baseline.config_dir || "") !== exactConfigDir ||
93
+ baseline.stack !== stack ||
94
+ !Number.isInteger(exactPort) ||
95
+ exactPort !== baseline.port ||
96
+ resolve(resources.active_package_root || "") !==
97
+ resolve(baseline.package_root || "") ||
98
+ operation.result_path !== resources.result_path
99
+ ) {
100
+ throw new Error("staged recovery geometry differs from the retained Host");
101
+ }
102
+ const preparation = readPreparation(
103
+ operation.result_path,
104
+ "retained update preparation receipt"
105
+ );
106
+ if (
107
+ preparation.operation_id !== operation.operation_id ||
108
+ resolve(preparation.active_package_root || "") !==
109
+ resolve(resources.active_package_root)
110
+ ) {
111
+ throw new Error("retained preparation receipt differs from the operation");
112
+ }
113
+ const identity = verifyIdentity({
114
+ configDir: exactConfigDir,
115
+ });
116
+ if (
117
+ identity.toolchain_identity_digest !== receipt.source?.runtime_identity_digest
118
+ ) {
119
+ throw new Error("retained recovery Node/npm identity changed");
120
+ }
121
+ const result = await runTransaction({
122
+ transaction_kind: receipt.transaction_kind,
123
+ operation_id: operation.operation_id,
124
+ active_package_root: resources.active_package_root,
125
+ config_dir: exactConfigDir,
126
+ node_bin: identity.node_path,
127
+ npm_cli: identity.npm_cli_path,
128
+ identity_digest: identity.toolchain_identity_digest,
129
+ stack,
130
+ health_url: `http://127.0.0.1:${exactPort}/health`,
131
+ result_path: operation.result_path,
132
+ restart_id: receipt.restart_id,
133
+ login_gate_id: operation.login_gate_id || null,
134
+ continuity_path: operation.continuity_path,
135
+ restore_before_restart: "1",
136
+ });
137
+ return result;
138
+ }
139
+
140
+ async function main() {
141
+ const result = await runStagedLifecycleRecovery();
142
+ process.stdout.write(
143
+ `${JSON.stringify({
144
+ status: result?.status || "recovered",
145
+ operation_id: result?.operation_id || null,
146
+ operation_cleanup_completed:
147
+ result?.operation_cleanup_completed === true,
148
+ secret_fields_redacted: true,
149
+ }, null, 2)}\n`
150
+ );
151
+ }
152
+
153
+ if (
154
+ process.argv[1] &&
155
+ resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))
156
+ ) {
157
+ main().catch((error) => {
158
+ process.stderr.write(`[oysterun-recovery] ${error.message}\n`);
159
+ process.exitCode = 1;
160
+ });
161
+ }
@@ -1,4 +1,4 @@
1
- import { spawn } from "child_process";
1
+ import { ownDiscoveryProcess, spawnDiscoveryProcess, collectDiscoveryCleanup } from "./model-discovery-process.mjs";
2
2
  import { execFileSync } from "child_process";
3
3
  import { EventEmitter } from "events";
4
4
  import { existsSync } from "fs";
@@ -89,6 +89,7 @@ class JsonRpcLineClient extends EventEmitter {
89
89
  this.stdoutBuffer = "";
90
90
  this.nextId = 1;
91
91
  this.pending = new Map();
92
+ this.terminalError = null;
92
93
  proc.stdout.on("data", (chunk) => this.consumeStdout(String(chunk)));
93
94
  proc.stderr.on("data", (chunk) => {
94
95
  this.emit("stderr", String(chunk));
@@ -98,20 +99,16 @@ class JsonRpcLineClient extends EventEmitter {
98
99
  "codex_app_server_exit",
99
100
  `Codex app-server exited during discovery (code=${code}, signal=${signal || "none"})`
100
101
  );
101
- for (const pending of this.pending.values()) {
102
- pending.reject(error);
103
- }
104
- this.pending.clear();
102
+ this.terminalError ||= error;
103
+ this.failPending(this.terminalError);
105
104
  });
106
105
  proc.on("error", (err) => {
107
106
  const error = new CodexModelDiscoveryError(
108
107
  "codex_app_server_error",
109
108
  err.message
110
109
  );
111
- for (const pending of this.pending.values()) {
112
- pending.reject(error);
113
- }
114
- this.pending.clear();
110
+ this.terminalError ||= error;
111
+ this.failPending(this.terminalError);
115
112
  });
116
113
  }
117
114
 
@@ -169,9 +166,11 @@ class JsonRpcLineClient extends EventEmitter {
169
166
 
170
167
  request(method, params = {}) {
171
168
  const id = String(this.nextId++);
172
- this.writeJson({ jsonrpc: "2.0", id, method, params });
173
169
  return new Promise((resolve, reject) => {
170
+ if (this.terminalError) { reject(this.terminalError); return; }
174
171
  this.pending.set(id, { resolve, reject });
172
+ try { this.writeJson({ jsonrpc: "2.0", id, method, params }); }
173
+ catch (error) { this.pending.delete(id); reject(error); }
175
174
  });
176
175
  }
177
176
 
@@ -180,6 +179,7 @@ class JsonRpcLineClient extends EventEmitter {
180
179
  }
181
180
 
182
181
  writeJson(payload) {
182
+ if (this.terminalError) throw this.terminalError;
183
183
  if (!this.proc.stdin?.writable) {
184
184
  throw new CodexModelDiscoveryError(
185
185
  "codex_app_server_stdin_unavailable",
@@ -189,11 +189,6 @@ class JsonRpcLineClient extends EventEmitter {
189
189
  this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
190
190
  }
191
191
 
192
- close() {
193
- if (typeof this.proc.kill === "function") {
194
- this.proc.kill("SIGTERM");
195
- }
196
- }
197
192
  }
198
193
 
199
194
  function extractModelEntries(result) {
@@ -249,7 +244,7 @@ export async function discoverCodexProviderModels(options = {}) {
249
244
  ? options.timeoutMs
250
245
  : 5000;
251
246
  const maxPages = Number.isInteger(options.maxPages) ? options.maxPages : 20;
252
- const spawnFn = options.spawnFn || spawn;
247
+ const spawnFn = options.spawnFn || spawnDiscoveryProcess;
253
248
  const commandResolution = resolveCodexDiscoveryCommand(options);
254
249
  const proc = spawnFn(commandResolution.resolved_command, [
255
250
  "app-server",
@@ -258,10 +253,15 @@ export async function discoverCodexProviderModels(options = {}) {
258
253
  ], {
259
254
  cwd: options.cwd || process.cwd(),
260
255
  stdio: ["pipe", "pipe", "pipe"],
256
+ detached: true,
257
+ ...(options.signal ? { signal: options.signal } : {}),
261
258
  env: { ...process.env, ...(options.env || {}) },
262
259
  });
263
- const client = new JsonRpcLineClient(proc);
260
+ const ownedProcess = ownDiscoveryProcess(proc, options.processCleanup);
261
+ const processCleanup = {};
262
+ let discoveryError;
264
263
  try {
264
+ const client = new JsonRpcLineClient(proc);
265
265
  await withTimeout(
266
266
  client.request("initialize", {
267
267
  clientInfo: {
@@ -300,6 +300,7 @@ export async function discoverCodexProviderModels(options = {}) {
300
300
  rawModels.map(normalizeCodexDiscoveryModel)
301
301
  );
302
302
  return {
303
+ process_cleanup: processCleanup,
303
304
  provider: "codex",
304
305
  command_available: true,
305
306
  resolved_command_redacted: redactCommand(
@@ -315,7 +316,11 @@ export async function discoverCodexProviderModels(options = {}) {
315
316
  "codex_model_list_pagination_limit",
316
317
  `Codex model/list exceeded ${maxPages} pages`
317
318
  );
319
+ } catch (err) {
320
+ discoveryError = err;
321
+ throw err;
318
322
  } finally {
319
- client.close();
323
+ Object.assign(processCleanup, await collectDiscoveryCleanup(ownedProcess));
324
+ if (discoveryError) discoveryError.details = { ...discoveryError.details, process_cleanup: processCleanup };
320
325
  }
321
326
  }
@@ -0,0 +1,176 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { constants, accessSync, existsSync, lstatSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { userInfo } from "node:os";
6
+ import { resolveNodeNpmRuntimeIdentity, buildNodeNpmChildEnvironment, assertNodeNpmRuntimeIdentityMatches } from "./node-npm-runtime-identity.mjs";
7
+
8
+ function inside(parent, child) {
9
+ const r = relative(parent, child);
10
+ return !r || (!r.startsWith("..") && !isAbsolute(r));
11
+ }
12
+ function canonical(path) {
13
+ if (!isAbsolute(path) || /[\r\n\t\0]/u.test(path)) throw new Error("Uninstall requires exact absolute paths without control characters.");
14
+ if (existsSync(path)) return realpathSync(path);
15
+ return join(canonical(dirname(path)), path.slice(dirname(path).length + 1));
16
+ }
17
+ function checkedRun(spawn, command, args, options) {
18
+ const r = spawn(command, args, { encoding: "utf8", ...options });
19
+ if (r.error || r.status !== 0) throw new Error(`${command}: ${r.error?.message || String(r.stderr || r.stdout || `exit ${r.status}`).trim()}`);
20
+ return String(r.stdout || "").trim();
21
+ }
22
+ function writableRemoval(path, uid) {
23
+ if (!existsSync(path)) return;
24
+ if (statSync(path).uid !== uid) throw new Error(`Selected removal target is not owned by the running OS user: ${path}`);
25
+ accessSync(dirname(path), constants.W_OK | constants.X_OK);
26
+ }
27
+
28
+ // npm enables Node's compile cache even for --version. Keep this policy local
29
+ // to identity/resolver probes; lifecycle and the actual npm uninstall retain
30
+ // the caller's normal child environment.
31
+ export function buildUninstallProbeEnvironment(env) {
32
+ const probeEnv = { ...env, NODE_DISABLE_COMPILE_CACHE: "1" };
33
+ delete probeEnv.NODE_COMPILE_CACHE;
34
+ return probeEnv;
35
+ }
36
+
37
+ export function resolveLaunchingCli({ cliPath, env = process.env, spawn = spawnSync } = {}) {
38
+ const cli = realpathSync(cliPath);
39
+ const root = dirname(dirname(cli));
40
+ const prefix = dirname(dirname(dirname(root)));
41
+ if (root !== join(prefix, "lib", "node_modules", "oysterun") || cli !== join(root, "bin", "oysterun.mjs")) {
42
+ throw new Error("Complete uninstall requires the installed global Oysterun CLI, not a checkout or npm link. Run the exact installed prefix/bin/oysterun.");
43
+ }
44
+ const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
45
+ const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.oysterun;
46
+ if (manifest.name !== "oysterun" || bin !== "bin/oysterun.mjs" || canonical(join(prefix, "bin", "oysterun")) !== cli) {
47
+ throw new Error("Launching Oysterun package/bin identity disagrees; repair this exact global installation before uninstalling.");
48
+ }
49
+ const identity = resolveNodeNpmRuntimeIdentity({ nodeBin: process.execPath, env: buildUninstallProbeEnvironment(env), spawnSyncFn: spawn });
50
+ const childEnv = buildNodeNpmChildEnvironment(identity, env);
51
+ const probeEnv = buildUninstallProbeEnvironment(childEnv);
52
+ // The removal arguments remain unchanged. Read-only npm root must also avoid
53
+ // cache directory creation, log cleanup, timing output and update checks.
54
+ // /dev/null is a non-directory on the supported Unix global-install layout.
55
+ const npmArgs = ["--global", "--prefix", prefix, "--ignore-scripts", "--logs-max=0", "--no-audit", "--no-fund"];
56
+ const modules = checkedRun(spawn, identity.node_path, [identity.npm_cli_path, "root", ...npmArgs, "--cache=/dev/null", "--logs-dir=/dev/null", "--update-notifier=false", "--timing=false"], { env: probeEnv });
57
+ if (canonical(modules) !== canonical(join(prefix, "lib", "node_modules")) || canonical(join(modules, "oysterun")) !== root) {
58
+ throw new Error("Matching npm does not address the launching Oysterun prefix; no service or config was changed.");
59
+ }
60
+ const osUser = userInfo();
61
+ if (process.getuid() !== osUser.uid || process.geteuid() !== osUser.uid) throw new Error("Running OS user identity is ambiguous; run uninstall as the installation owner without escalation.");
62
+ writableRemoval(root, osUser.uid);
63
+ accessSync(join(prefix, "bin"), constants.W_OK | constants.X_OK);
64
+ return { cli, root, prefix, manifestSha256: createHash("sha256").update(readFileSync(join(root, "package.json"))).digest("hex"), bin: join(prefix, "bin", "oysterun"), identity, childEnv, probeEnv, user: { uid: osUser.uid, name: osUser.username }, npmArgs };
65
+ }
66
+
67
+ // This only reads the canonical service resolver; it never executes lifecycle.
68
+ export function resolveUninstallServices({ repoRoot, stack, env, spawn = spawnSync }) {
69
+ const stacks = stack === "all" ? ["production", "staging"] : [stack];
70
+ const source = `source "$1"; shift
71
+ for target in "$@"; do
72
+ STACK_NAME="$target"; configure_stack_runtime
73
+ printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' "$STACK_NAME" "$CONFIG_DIR" "$HOST_LABEL" "$HOST_PORT" "$HOST_PID_FILE" "$HOST_ORIGIN_FILE" "$HOME/Library/LaunchAgents/$HOST_LABEL.plist"
74
+ done`;
75
+ const raw = checkedRun(spawn, "/bin/bash", ["--noprofile", "--norc", "-eu", "-c", source, "--", join(repoRoot, "tool_scripts", "oysterun_service_common.sh"), ...stacks], { env, cwd: repoRoot });
76
+ const targets = raw.split("\n").map(line => {
77
+ const [name, config, label, port, pidFile, originFile, plist] = line.split("\t");
78
+ if (!name || !config || !label || !pidFile || !originFile || !plist) throw new Error("Canonical service resolver returned an incomplete uninstall target.");
79
+ return { name, config: canonical(config), label, port, pidFile, originFile, plist,
80
+ pid: existsSync(pidFile) ? Number(readFileSync(pidFile, "utf8").trim()) : null };
81
+ });
82
+ if (targets.length !== stacks.length || targets.some((t, i) => t.name !== stacks[i])) throw new Error("Canonical uninstall stack selection drifted.");
83
+ return targets;
84
+ }
85
+
86
+ export function preflightCompleteUninstall({ cliPath, configDir, stack, env = process.env, spawn = spawnSync, resolveServices = resolveUninstallServices }) {
87
+ const installation = resolveLaunchingCli({ cliPath, env, spawn });
88
+ const services = resolveServices({ repoRoot: installation.root, stack, env: { ...installation.probeEnv, OYSTERUN_NODE_BIN: installation.identity.node_path }, spawn });
89
+ const config = canonical(resolve(configDir));
90
+ if (!services.some(t => t.config === config)) throw new Error("Selected config does not match the selected service stack(s); set OYSTERUN_CONFIG_DIR and --stack/OYSTERUN_RELEASE_STACK to the same installation. No changes made.");
91
+ writableRemoval(config, installation.user.uid);
92
+ const protectedPaths = [installation.prefix, installation.root, installation.identity.node_realpath, installation.identity.npm_cli_realpath, canonical(userInfo().homedir)];
93
+ for (const filename of ["config.json", "session-history.json", "loops.json"]) {
94
+ const path = join(config, filename);
95
+ if (!existsSync(path)) continue;
96
+ const raw = JSON.parse(readFileSync(path, "utf8"));
97
+ const collect = value => {
98
+ if (!value || typeof value !== "object") return;
99
+ for (const [key, item] of Object.entries(value)) {
100
+ if (["default_browse_path", "agent_folder", "start_folder", "cwd"].includes(key) && typeof item === "string" && item) protectedPaths.push(canonical(resolve(item)));
101
+ else if (item && typeof item === "object") collect(item);
102
+ }
103
+ };
104
+ collect(raw);
105
+ }
106
+ if (protectedPaths.some(p => inside(config, p)) || protectedPaths.slice(5).some(p => inside(installation.root, p))) throw new Error("Config removal overlaps the CLI/toolchain, user home, Default Browse Root or Agent/project data. Move the protected data outside this config before complete uninstall; no changes made.");
107
+ for (const service of services) {
108
+ // all preserves both service selections; only the selected config must
109
+ // belong to the launching package, since only that config is deleted.
110
+ if (service.config === config) {
111
+ const runtimePath = join(dirname(service.pidFile), "host-runtime.json");
112
+ if (existsSync(runtimePath)) {
113
+ const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
114
+ if (canonical(runtime.config_dir) !== config || canonical(runtime.repo_root) !== installation.root || (runtime.stack_name ?? "production") !== service.name) throw new Error("Selected Host runtime belongs to another package/config/stack; no changes made.");
115
+ }
116
+ if (process.platform === "darwin" && existsSync(service.plist)) {
117
+ const plist = JSON.parse(checkedRun(spawn, "/usr/bin/plutil", ["-convert", "json", "-o", "-", service.plist], { env: installation.probeEnv }));
118
+ const args = plist.ProgramArguments || []; const pe = plist.EnvironmentVariables || {};
119
+ if (plist.Label !== service.label || canonical(args[0] || "") !== join(installation.root, "tool_scripts", "run_oysterun_host.sh") || args[args.indexOf("--stack") + 1] !== service.name ||
120
+ ![installation.root, join(installation.root, "host-service")].includes(canonical(plist.WorkingDirectory || "")) ||
121
+ (pe.OYSTERUN_CONFIG_DIR && canonical(pe.OYSTERUN_CONFIG_DIR) !== config) || (pe.HOME && canonical(pe.HOME) !== canonical(userInfo().homedir))) throw new Error("Selected service definition belongs to another package/config/user; no changes made.");
122
+ }
123
+ }
124
+ if (process.platform === "darwin") writableRemoval(service.plist, installation.user.uid);
125
+ }
126
+ return { ...installation, config, services, stack };
127
+ }
128
+
129
+ export function verifyUninstallServices(plan, { spawn = spawnSync } = {}) {
130
+ for (const t of plan.services) {
131
+ if (existsSync(t.pidFile) || existsSync(t.originFile) || (process.platform === "darwin" && existsSync(t.plist))) throw new Error(`Service state remains for ${t.name}.`);
132
+ if (Number.isInteger(t.pid) && t.pid > 0) {
133
+ try { process.kill(t.pid, 0); throw new Error(`Service PID ${t.pid} remains alive.`); }
134
+ catch (error) { if (error.code !== "ESRCH") throw error; }
135
+ }
136
+ }
137
+ const source = `source "$1"; shift
138
+ for target in "$@"; do
139
+ STACK_NAME="$target"; configure_stack_runtime
140
+ if is_macos && launch_service_is_loaded "$HOST_LABEL"; then echo "Service remains loaded: $HOST_LABEL" >&2; exit 1; fi
141
+ if [[ -n "$HOST_PORT" ]]; then owners="$(listener_pids_for_port "$HOST_PORT")"; [[ -z "$owners" ]] || { echo "Service port remains occupied: $HOST_PORT" >&2; exit 1; }; fi
142
+ done`;
143
+ checkedRun(spawn, "/bin/bash", ["--noprofile", "--norc", "-eu", "-c", source, "--", join(plan.root, "tool_scripts", "oysterun_service_common.sh"), ...plan.services.map(t => t.name)], { env: buildUninstallProbeEnvironment(plan.childEnv), cwd: plan.root });
144
+ }
145
+
146
+ export function executeCompleteUninstall(plan, { spawn = spawnSync, remove = rmSync, verifyServices = verifyUninstallServices, log = console.log } = {}) {
147
+ const steps = { service: "incomplete", services: Object.fromEntries(plan.services.map(t => [t.name, "incomplete"])), config: "incomplete", cli: "incomplete", userData: "preserved" };
148
+ let current = "service";
149
+ try {
150
+ for (const target of plan.services) {
151
+ checkedRun(spawn, "/bin/bash", [join(plan.root, "tool_scripts", "oysterun_release_service.sh"), "uninstall", "--stack", target.name], { env: plan.childEnv, cwd: plan.root, stdio: "inherit" });
152
+ verifyServices({ ...plan, services: [target] }, { spawn });
153
+ steps.services[target.name] = "removed/absent";
154
+ }
155
+ steps.service = "removed/absent";
156
+ current = "config";
157
+ remove(plan.config, { recursive: true, force: true });
158
+ if (existsSync(plan.config)) throw new Error("Selected config directory remains.");
159
+ steps.config = "removed/absent";
160
+ current = "cli";
161
+ // Recheck the exact installation after earlier work; never switch prefixes.
162
+ if (realpathSync(plan.cli) !== plan.cli || canonical(plan.bin) !== plan.cli || realpathSync(plan.root) !== plan.root) throw new Error("Launching CLI identity changed during cleanup.");
163
+ if (createHash("sha256").update(readFileSync(join(plan.root, "package.json"))).digest("hex") !== plan.manifestSha256) throw new Error("Launching package changed during cleanup.");
164
+ assertNodeNpmRuntimeIdentityMatches(plan.identity, resolveNodeNpmRuntimeIdentity({ nodeBin: plan.identity.node_path, npmCli: plan.identity.npm_cli_path, env: buildUninstallProbeEnvironment(plan.childEnv), spawnSyncFn: spawn }));
165
+ checkedRun(spawn, plan.identity.node_path, [plan.identity.npm_cli_path, "uninstall", ...plan.npmArgs, "oysterun"], { env: plan.childEnv, cwd: plan.prefix, stdio: "inherit" });
166
+ if (existsSync(plan.root) || existsSync(plan.bin) || (() => { try { lstatSync(plan.bin); return true; } catch (e) { if (e.code === "ENOENT") return false; throw e; } })()) throw new Error("npm completed but the exact Oysterun package/bin remains.");
167
+ steps.cli = "removed/absent";
168
+ log("Complete uninstall succeeded: service removed, configuration removed, CLI removed; user data preserved.");
169
+ return { exitCode: 0, steps };
170
+ } catch (error) {
171
+ log(`Complete uninstall failed at ${current}: ${error.message}`);
172
+ log(`Services: ${Object.entries(steps.services).map(([name, status]) => `${name}=${status}`).join("; ")}.`);
173
+ log(`Completed/incomplete steps: service=${steps.service}; config=${steps.config}; CLI=${steps.cli}; user data=${steps.userData}.`);
174
+ return { exitCode: 1, steps, error: error.message };
175
+ }
176
+ }