oysterun 1.6.0-beta.2 → 1.6.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/oysterun.mjs +64 -17
- package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
- package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
- package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
- package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
- package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
- package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
- package/dev/client/connectivity/managed-host-browser.mjs +10 -0
- package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
- package/dev/client/connectivity/session-transition-state.mjs +18 -6
- package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
- package/dev/client/web/index.html +5367 -609
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
- package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
- package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
- package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
- package/dev/client/web-chat/dist/index.html +1 -2
- package/docs/routec/dev/cloud_deplyments.md +5 -0
- package/docs/routec/dev/npm_native_dependency_install.md +6 -2
- package/docs/routec/dev/npmjs_publish.md +55 -19
- package/docs/routec/dev/p262_setup_input_file.md +117 -0
- package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
- package/docs/routec/host_setup.md +39 -10
- package/docs/routec/oysterun_operation_interface_spec.md +78 -2
- package/docs/routec/provider_contract.md +35 -5
- package/host-service/README.md +36 -0
- package/host-service/adapters/claude-code-adapter.mjs +48 -5
- package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
- package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
- package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
- package/host-service/agent-config.mjs +16 -5
- package/host-service/agent-folder-availability.mjs +41 -0
- package/host-service/apns-complete-message-dispatcher.mjs +1 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
- package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
- package/host-service/callback-durable-store.mjs +354 -47
- package/host-service/callback-foundation.mjs +21 -1
- package/host-service/callback-host-integration.mjs +345 -30
- package/host-service/claude-acp-model-discovery.mjs +23 -18
- package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
- package/host-service/cli/installer-recovery-guidance.mjs +20 -0
- package/host-service/cli/prepare-restart-restore.mjs +2 -0
- package/host-service/cli/product-cli.mjs +34 -33
- package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
- package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
- package/host-service/cli/run-host-lifecycle.mjs +2 -0
- package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
- package/host-service/codex-model-discovery.mjs +23 -18
- package/host-service/complete-cli-uninstall.mjs +176 -0
- package/host-service/complete-message-notification-predicate.mjs +8 -6
- package/host-service/complete-message-notification-title.mjs +4 -0
- package/host-service/completed-session-unread.mjs +425 -42
- package/host-service/completed-turn-navigation.mjs +444 -144
- package/host-service/completed-turn-notification-observer.mjs +116 -0
- package/host-service/completed-turn-service.mjs +445 -0
- package/host-service/config.mjs +115 -9
- package/host-service/desired-open-session-state.mjs +331 -0
- package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
- package/host-service/frp-agent.mjs +9 -9
- package/host-service/grok-acp-model-discovery.mjs +31 -33
- package/host-service/host-lifecycle-recovery.mjs +37 -0
- package/host-service/host-port-resolution.mjs +98 -0
- package/host-service/host-runtime-diagnostics.mjs +18 -1
- package/host-service/initial-preflight-recovery.mjs +71 -0
- package/host-service/installer-recovery-guidance.mjs +85 -0
- package/host-service/installer-reinstall-bindings.mjs +49 -0
- package/host-service/installer-reinstall-target.mjs +49 -0
- package/host-service/legacy-continuity-observation.mjs +33 -0
- package/host-service/lifecycle-compat-cli.mjs +13 -0
- package/host-service/lifecycle-compat-preload.mjs +89 -0
- package/host-service/lifecycle-compatibility.mjs +164 -0
- package/host-service/lifecycle-continuity-assets.mjs +222 -0
- package/host-service/lifecycle-continuity-probe.mjs +97 -0
- package/host-service/lifecycle-continuity-projection.mjs +29 -0
- package/host-service/lifecycle-continuity-runner.mjs +192 -0
- package/host-service/lifecycle-continuity.mjs +493 -0
- package/host-service/lifecycle-service-render-bundle.mjs +120 -0
- package/host-service/lifecycle-user-path.mjs +34 -0
- package/host-service/link-annotations.mjs +10 -0
- package/host-service/matrix-credential-issuer.mjs +32 -1
- package/host-service/matrix-event-correlator.mjs +166 -27
- package/host-service/matrix-facade.mjs +96 -19
- package/host-service/matrix-room-binding.mjs +24 -0
- package/host-service/matrix-transcript-read-adapter.mjs +17 -3
- package/host-service/model-discovery-guardian.mjs +40 -0
- package/host-service/model-discovery-process.mjs +141 -0
- package/host-service/onboard-lifecycle-status.mjs +28 -0
- package/host-service/onboard.mjs +479 -0
- package/host-service/package.json +2 -1
- package/host-service/portable-scheduler-definitions.mjs +28 -0
- package/host-service/product-runtime-host-origin.mjs +22 -0
- package/host-service/provider-model-params.mjs +14 -17
- package/host-service/provider-model-refresh-runner.mjs +124 -169
- package/host-service/provider-model-refresh-schedule.mjs +11 -0
- package/host-service/provider-resume-contract.mjs +9 -3
- package/host-service/restart-restore-state.mjs +25 -0
- package/host-service/retained-host-state-holder.mjs +57 -0
- package/host-service/routec-matrix-sqlite-store.mjs +155 -10
- package/host-service/routec-matrix-storage-adapter.mjs +208 -25
- package/host-service/routec-matrix-storage-paths.mjs +12 -0
- package/host-service/routec-request-delay-window.mjs +55 -0
- package/host-service/scheduler-acp-provider-runner.mjs +2 -1
- package/host-service/scheduler-direct-provider-runner.mjs +71 -9
- package/host-service/scheduler-run-timeout-policy.mjs +85 -0
- package/host-service/scheduler-service.mjs +96 -9
- package/host-service/scheduler-store.mjs +9 -2
- package/host-service/server.mjs +2202 -552
- package/host-service/service-definition-migration.mjs +10 -3
- package/host-service/session-collection-service.mjs +320 -0
- package/host-service/session-history.mjs +99 -15
- package/host-service/session-list-preview-service.mjs +87 -12
- package/host-service/session-list-projection-service.mjs +182 -0
- package/host-service/session-loop-definitions.mjs +702 -75
- package/host-service/session-manager.mjs +473 -38
- package/host-service/session-switcher-projection.mjs +11 -3
- package/host-service/session-tags.mjs +451 -0
- package/host-service/session-transcript-target.mjs +103 -0
- package/host-service/setup-input-file.mjs +91 -0
- package/host-service/setup-provider-model-defaults.mjs +53 -0
- package/host-service/setup.mjs +133 -101
- package/host-service/stopped-user-host-package.mjs +60 -0
- package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
- package/host-service/update-reminder-state.mjs +7 -0
- package/host-service/voice-input-receipt-store.mjs +60 -0
- package/package.json +1 -1
- package/static/oysterun-social-preview.png +0 -0
- package/static/site/assets/oysterun-social-preview.png +0 -0
- package/static/site/blog/en/index.html +17 -0
- package/static/site/blog/generated_manifest.json +9 -9
- package/static/site/blog/index.html +18 -0
- package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
- package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
- package/static/site/blog/zh/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
- package/static/site/docs/en/index.html +17 -0
- package/static/site/docs/generated_manifest.json +60 -60
- package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
- package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
- package/static/site/docs/index.html +18 -0
- package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
- package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
- package/static/site/docs/read-large-tool-results/en/index.html +17 -0
- package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
- package/static/site/docs/send-images-and-media/en/index.html +17 -0
- package/static/site/docs/send-images-and-media/zh/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
- package/static/site/docs/understand-agent-activity/en/index.html +19 -2
- package/static/site/docs/understand-agent-activity/en/index.md +2 -2
- package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
- package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
- package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
- package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
- package/static/site/docs/zh/index.html +17 -0
- package/static/site/en/index.html +18 -1
- package/static/site/en/index.md +1 -1
- package/static/site/faq/en/index.html +17 -0
- package/static/site/faq/index.html +18 -0
- package/static/site/faq/zh/index.html +17 -0
- package/static/site/getting-started/en/index.html +23 -1
- package/static/site/getting-started/en/index.md +7 -1
- package/static/site/getting-started/index.html +18 -0
- package/static/site/getting-started/zh/index.html +23 -1
- package/static/site/getting-started/zh/index.md +7 -1
- package/static/site/index.html +18 -0
- package/static/site/install.sh +676 -127
- package/static/site/overview/en/index.html +18 -1
- package/static/site/overview/en/index.md +2 -2
- package/static/site/overview/index.html +18 -0
- package/static/site/overview/zh/index.html +18 -1
- package/static/site/overview/zh/index.md +2 -2
- package/static/site/privacy/en/index.html +17 -0
- package/static/site/privacy/index.html +18 -0
- package/static/site/privacy/zh/index.html +17 -0
- package/static/site/zh/index.html +18 -1
- package/static/site/zh/index.md +1 -1
- package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
- package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
- package/static/site_source/downloads/install.sh +676 -127
- package/static/site_source/manifests/host_install.json +2 -10
- package/static/site_source/pages/getting-started/en.html +6 -1
- package/static/site_source/pages/getting-started/zh.html +6 -1
- package/static/site_source/pages/home/en.html +1 -1
- package/static/site_source/pages/home/zh.html +1 -1
- package/static/site_source/pages/home_main.html +3 -2
- package/static/site_source/pages/overview/en.html +1 -1
- package/static/site_source/pages/overview/zh.html +1 -1
- package/tool_scripts/install_oysterun_update.mjs +78 -13
- package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
- package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
- package/tool_scripts/oysterun_release_service.sh +5 -0
- package/tool_scripts/oysterun_service_common.sh +59 -24
- package/tool_scripts/oysterun_setup_input.sh +97 -0
- package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
- package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
- package/tool_scripts/resolve_user_host_port.mjs +15 -0
- package/tool_scripts/restart_oysterun.sh +8 -2
- package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
- package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
- package/tool_scripts/sync_host_stack_config.mjs +9 -6
- package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
- package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
- package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
- package/dev/client/web-chat/dist/assets/web-c70YaskK.js +0 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// Durable continuation of the existing Host Preferences lifecycle runner.
|
|
2
|
+
// Service mutation remains owned by its canonical service-command primitive.
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, renameSync } from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import {
|
|
8
|
+
advanceContinuity, continuityDigest, continuityFailure, continuityReceiptPath, readContinuity,
|
|
9
|
+
recordContinuityRecovery, writeContinuityAtomic, writeContinuityBytesAtomic, recordContinuityRestartAttempt,
|
|
10
|
+
qualifyContinuityHost, qualifyContinuitySessions,
|
|
11
|
+
bindLegacyContinuityConnectionMode,
|
|
12
|
+
} from "../host-service/lifecycle-continuity.mjs";
|
|
13
|
+
import {
|
|
14
|
+
finalizeContinuityAssets, restoreContinuityAssets, validateContinuityResources, verifyContinuityAssets, digestContinuityPackage,
|
|
15
|
+
} from "../host-service/lifecycle-continuity-assets.mjs";
|
|
16
|
+
import { qualifyLifecycleContinuity } from "../host-service/lifecycle-continuity-runner.mjs";
|
|
17
|
+
import { fetchContinuityObservation, ensureContinuityProbeGrant } from "../host-service/lifecycle-continuity-probe.mjs";
|
|
18
|
+
import { verifyPersistedNodeNpmRuntimeIdentity, resolveNodeNpmRuntimeIdentity } from "../host-service/node-npm-runtime-identity.mjs";
|
|
19
|
+
import { readUpdateOperationState, writeUpdateOperationState, UPDATE_RESTART_RUNNER_LEASE_MS } from "../host-service/update-reminder-state.mjs";
|
|
20
|
+
import { readHostRestartRestoreState } from "../host-service/restart-restore-state.mjs";
|
|
21
|
+
import { releaseHostRestartLoginGate } from "../host-service/host-restart-login-gate.mjs";
|
|
22
|
+
import { projectContinuityOperation } from "../host-service/lifecycle-continuity-projection.mjs";
|
|
23
|
+
import { sanitizeLifecycleDiagnostic } from "../host-service/lifecycle-diagnostics.mjs";
|
|
24
|
+
|
|
25
|
+
import { prepareCompatibility, readCompatibility } from "../host-service/lifecycle-compatibility.mjs";
|
|
26
|
+
import { renderServiceDefinition } from "../host-service/service-definition-migration.mjs";
|
|
27
|
+
import { bindLegacyObservationConnectionMode } from "../host-service/legacy-continuity-observation.mjs";
|
|
28
|
+
|
|
29
|
+
export function inspectContinuityListener(port) {
|
|
30
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw continuityFailure("host_qualification", "port", "The retained Host port is invalid.");
|
|
31
|
+
const result = spawnSync(process.platform === "darwin" ? "/usr/sbin/lsof" : "lsof",
|
|
32
|
+
["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"], { encoding: "utf8" });
|
|
33
|
+
if (result.error || ![0, 1].includes(result.status)) throw continuityFailure("host_qualification", "listener_observation", "Cannot inspect the exact Host listener.");
|
|
34
|
+
return [...new Set(result.stdout.trim().split(/\s+/).filter(Boolean).map(Number))];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function confirmStoppedContinuityService(receipt) {
|
|
38
|
+
if (inspectContinuityListener(receipt.baseline.host.port).length !== 0 || process.platform !== "darwin") return false;
|
|
39
|
+
const stack = receipt.baseline.host.stack;
|
|
40
|
+
if (!/^[A-Za-z0-9_-]+$/.test(stack || "")) return false;
|
|
41
|
+
const label = stack === "production" ? "com.oysterun.host" : `com.oysterun.host.${stack}`;
|
|
42
|
+
const result = spawnSync("/bin/launchctl", ["print", `gui/${process.getuid()}/${label}`], { encoding: "utf8" });
|
|
43
|
+
// launchctl's explicit service-not-found result, not a network timeout.
|
|
44
|
+
return !result.error && result.status !== 0 && /Could not find service|service not found/i.test(`${result.stderr}\n${result.stdout}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function serviceDefinitionMatchesRetainedDigest(path, retainedDigest) {
|
|
48
|
+
if (retainedDigest === "absent") return !existsSync(path);
|
|
49
|
+
return Boolean(retainedDigest) && existsSync(path) &&
|
|
50
|
+
continuityDigest(readFileSync(path)) === retainedDigest;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function acquireRunner(path, operationId) {
|
|
54
|
+
const lock = `${path}.runner`;
|
|
55
|
+
const claim = `${lock}.claim-${randomUUID()}`;
|
|
56
|
+
mkdirSync(claim, { mode: 0o700 });
|
|
57
|
+
writeContinuityAtomic(join(claim, "owner.json"), { operation_id: operationId, pid: process.pid });
|
|
58
|
+
try { renameSync(claim, lock); } catch (error) {
|
|
59
|
+
rmSync(claim, { recursive: true, force: true });
|
|
60
|
+
if (!["EEXIST", "ENOTEMPTY"].includes(error.code)) throw error;
|
|
61
|
+
const owner = JSON.parse(readFileSync(join(lock, "owner.json"), "utf8"));
|
|
62
|
+
if (owner.operation_id !== operationId || !Number.isInteger(owner.pid)) throw continuityFailure("runner", "runner_lock", "Runner ownership is ambiguous.");
|
|
63
|
+
let alive = true;
|
|
64
|
+
try { process.kill(owner.pid, 0); } catch (error) { if (error.code === "ESRCH") alive = false; else throw error; }
|
|
65
|
+
if (alive) throw continuityFailure("runner", "runner_active", "This operation already has a live external runner.");
|
|
66
|
+
rmSync(lock, { recursive: true });
|
|
67
|
+
return acquireRunner(path, operationId);
|
|
68
|
+
}
|
|
69
|
+
return () => rmSync(lock, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function runContinuityTransaction(args, overrides = {}) {
|
|
73
|
+
const operation = readUpdateOperationState({ configDir: args.config_dir });
|
|
74
|
+
const path = args.continuity_path || operation.continuity_path;
|
|
75
|
+
if (!path || operation.operation_id !== args.operation_id ||
|
|
76
|
+
path !== continuityReceiptPath(args.config_dir, args.operation_id)) throw continuityFailure("runner", "operation_binding", "Lifecycle operation changed before continuation.");
|
|
77
|
+
const unlock = acquireRunner(path, args.operation_id);
|
|
78
|
+
try { return await continueTransaction(args, overrides, path); }
|
|
79
|
+
catch (error) {
|
|
80
|
+
const current = readUpdateOperationState({ configDir: args.config_dir });
|
|
81
|
+
const receipt = readContinuity(path, args.operation_id);
|
|
82
|
+
if (current.operation_id === args.operation_id && !["COMMITTED", "CLEANED", "ROLLED_BACK_HEALTHY"].includes(receipt.state)) {
|
|
83
|
+
const failed = recordContinuityRecovery(path, args.operation_id, error);
|
|
84
|
+
writeUpdateOperationState({ ...current,
|
|
85
|
+
status: failed.transaction_kind === "restart_only" ? "restart_recovery_required" : "update_recovery_required",
|
|
86
|
+
running: false, lifecycle_owner_pid: null, runner_pid: null, runner_lease_expires_at: null,
|
|
87
|
+
continuity_state: failed.state, restart_attempted: failed.restart_attempted,
|
|
88
|
+
root_failure: { ...failed.first_failure, safe_message: failed.first_failure.message },
|
|
89
|
+
error: sanitizeLifecycleDiagnostic(error.message), error_redacted: true,
|
|
90
|
+
}, { configDir: args.config_dir });
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
} finally { unlock(); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function continueTransaction(args, overrides, path) {
|
|
97
|
+
const get = () => readContinuity(path, args.operation_id);
|
|
98
|
+
const baseline = get().baseline.host;
|
|
99
|
+
if (baseline.config_dir !== resolve(args.config_dir) || baseline.stack !== args.stack ||
|
|
100
|
+
(baseline.package_root && baseline.package_root !== resolve(args.active_package_root)) ||
|
|
101
|
+
new URL(args.health_url).origin !== `http://127.0.0.1:${baseline.port}`) throw continuityFailure("runner", "exact_geometry", "Runner arguments differ from the retained Host geometry.");
|
|
102
|
+
const restartOnly = get().transaction_kind === "restart_only";
|
|
103
|
+
const observeRaw = overrides.observeContinuity || fetchContinuityObservation;
|
|
104
|
+
const listeners = overrides.inspectListener || inspectContinuityListener;
|
|
105
|
+
const readBoundHealth = async () => {
|
|
106
|
+
if (overrides.readPreRestartHealth) return overrides.readPreRestartHealth({ healthUrl: args.health_url });
|
|
107
|
+
const response = await fetch(args.health_url, {
|
|
108
|
+
headers: { Accept: "application/json" }, signal: AbortSignal.timeout(10_000),
|
|
109
|
+
redirect: "error", cache: "no-store",
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) throw continuityFailure("host_qualification", "legacy_health_status", "Legacy Host health did not return success.");
|
|
112
|
+
return response.json();
|
|
113
|
+
};
|
|
114
|
+
const observe = async (options) => bindLegacyObservationConnectionMode({
|
|
115
|
+
receipt: get(), observation: await observeRaw(options),
|
|
116
|
+
inspectListener: listeners, readHealth: readBoundHealth,
|
|
117
|
+
});
|
|
118
|
+
const recovering = get().state === "RECOVERY_REQUIRED" || args.restore_before_restart === "1";
|
|
119
|
+
const identity = recovering
|
|
120
|
+
? resolveNodeNpmRuntimeIdentity({ nodeBin: args.node_bin, npmCli: args.npm_cli })
|
|
121
|
+
: verifyPersistedNodeNpmRuntimeIdentity({ configDir: args.config_dir, nodeBin: args.node_bin, npmCli: args.npm_cli });
|
|
122
|
+
if (identity.toolchain_identity_digest !== args.identity_digest || identity.toolchain_identity_digest !== get().source.runtime_identity_digest) throw continuityFailure("restart_preflight", "runtime_identity", "Retained Node/npm identity changed.");
|
|
123
|
+
ensureContinuityProbeGrant(args.config_dir, args.operation_id);
|
|
124
|
+
const patch = (values) => {
|
|
125
|
+
const current = readUpdateOperationState({ configDir: args.config_dir });
|
|
126
|
+
if (current.operation_id !== args.operation_id) throw continuityFailure("runner", "operation_binding", "Lifecycle operation changed while running.");
|
|
127
|
+
const receipt = get();
|
|
128
|
+
if (["CLEANED", "ROLLED_BACK_HEALTHY"].includes(receipt.state) && values.running === true) {
|
|
129
|
+
return writeUpdateOperationState(projectContinuityOperation(current, receipt), { configDir: args.config_dir });
|
|
130
|
+
}
|
|
131
|
+
return writeUpdateOperationState({ ...current, ...values,
|
|
132
|
+
restart_attempted: receipt.restart_attempted,
|
|
133
|
+
continuity_state: receipt.state,
|
|
134
|
+
...(receipt.first_failure ? { root_failure: { ...receipt.first_failure,
|
|
135
|
+
safe_message: receipt.first_failure.message, observed_at: receipt.first_failure.at } } : {}),
|
|
136
|
+
error_redacted: true }, { configDir: args.config_dir });
|
|
137
|
+
};
|
|
138
|
+
const phase = (stage) => patch({ status: "restart_runner_started", running: true, runner_pid: process.pid,
|
|
139
|
+
phase: stage === "HOST_QUALIFIED" ? "Verifying Host identity"
|
|
140
|
+
: stage === "SESSIONS_QUALIFIED" ? "Verifying every Session and committed history"
|
|
141
|
+
: "Verifying the existing authenticated Host URL",
|
|
142
|
+
runner_lease_expires_at: new Date(Date.now() + UPDATE_RESTART_RUNNER_LEASE_MS).toISOString() });
|
|
143
|
+
const runService = async (command, options = {}) => {
|
|
144
|
+
if (["COMMITTED", "CLEANED", "ROLLED_BACK_HEALTHY"].includes(get().state)) {
|
|
145
|
+
throw continuityFailure("recovery", "terminal_operation", "A completed operation cannot mutate the Host again.");
|
|
146
|
+
}
|
|
147
|
+
if (args.recheck_only === "1") throw continuityFailure("recovery", "read_only_recheck", "Requalification cannot restart or restore the Host.");
|
|
148
|
+
if (overrides.runService) return overrides.runService(command, options);
|
|
149
|
+
if (!overrides.runCanonicalService) throw continuityFailure("runner", "canonical_service", "Canonical service command primitive is unavailable.");
|
|
150
|
+
if (["service:start", "service:restart"].includes(command)) {
|
|
151
|
+
const ps = spawnSync("/bin/ps", ["-axo", "pid=,command="], { encoding: "utf8" });
|
|
152
|
+
if (ps.status !== 0 || ps.stdout.split("\n").some((line) =>
|
|
153
|
+
["synapse_homeserver", "artifacts/spike0", "routec_spike0"].every((part) => line.includes(part)))) throw continuityFailure("restart_preflight", "legacy_cleanup_scope", "Canonical start would include legacy cleanup outside this operation; stop for review.");
|
|
154
|
+
}
|
|
155
|
+
return overrides.runCanonicalService({ identity, activeRoot: resolve(args.active_package_root), stack: args.stack,
|
|
156
|
+
command, operationId: args.operation_id, continuityPath: path, ...options });
|
|
157
|
+
};
|
|
158
|
+
const bindLegacyConnectionMode = async () => {
|
|
159
|
+
const retained = get();
|
|
160
|
+
if (Object.hasOwn(retained.baseline.host, "connection_mode")) return retained;
|
|
161
|
+
const origin = new URL(args.health_url).origin;
|
|
162
|
+
const observation = await observe({
|
|
163
|
+
configDir: args.config_dir,
|
|
164
|
+
operationId: args.operation_id,
|
|
165
|
+
origin,
|
|
166
|
+
});
|
|
167
|
+
const pids = listeners(retained.baseline.host.port);
|
|
168
|
+
const expected = retained.baseline.host;
|
|
169
|
+
for (const key of ["device_id", "config_dir", "browser_root", "host_folder", "port", "stack", "managed_endpoint", "direct_host_url"]) {
|
|
170
|
+
if (observation.host?.[key] !== expected[key]) {
|
|
171
|
+
throw continuityFailure("preparation", `legacy_host_${key}`, `Legacy Host ${key} changed before route-mode binding.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (pids.length !== 1 || pids[0] !== expected.pid || observation.host?.pid !== expected.pid) {
|
|
175
|
+
throw continuityFailure("preparation", "legacy_sole_listener", "Legacy route mode requires the retained original Host as sole listener.");
|
|
176
|
+
}
|
|
177
|
+
let health;
|
|
178
|
+
if (overrides.readPreRestartHealth) {
|
|
179
|
+
health = await overrides.readPreRestartHealth({ healthUrl: args.health_url });
|
|
180
|
+
} else {
|
|
181
|
+
const response = await fetch(args.health_url, {
|
|
182
|
+
headers: { Accept: "application/json" },
|
|
183
|
+
signal: AbortSignal.timeout(10_000),
|
|
184
|
+
redirect: "error",
|
|
185
|
+
cache: "no-store",
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) throw continuityFailure("preparation", "legacy_health_status", "Legacy Host health did not return success.");
|
|
188
|
+
health = await response.json();
|
|
189
|
+
}
|
|
190
|
+
if (!["ok", "degraded"].includes(health?.status) ||
|
|
191
|
+
health.device_id !== expected.device_id ||
|
|
192
|
+
!["cloud", "direct"].includes(health.connection_mode)) {
|
|
193
|
+
throw continuityFailure("preparation", "legacy_health_identity", "Legacy Host health cannot bind the retained route mode.");
|
|
194
|
+
}
|
|
195
|
+
return bindLegacyContinuityConnectionMode(path, args.operation_id, health.connection_mode, {
|
|
196
|
+
pid: expected.pid,
|
|
197
|
+
device_id: health.device_id,
|
|
198
|
+
sole_listener: true,
|
|
199
|
+
observed_at: new Date().toISOString(),
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const qualify = async (rollback, requireRestore) => {
|
|
203
|
+
const r = get();
|
|
204
|
+
const bound = rollback ? r.source : r.target;
|
|
205
|
+
const checkedObserve = async (options) => {
|
|
206
|
+
const observation = await observe(options);
|
|
207
|
+
const pids = listeners(r.baseline.host.port);
|
|
208
|
+
if (pids.length !== 1 || pids[0] !== observation.host.pid) throw continuityFailure("host_qualification", "sole_listener", "The intended Host is not the sole listener on its retained port.");
|
|
209
|
+
return observation;
|
|
210
|
+
};
|
|
211
|
+
return qualifyLifecycleContinuity({ path, operationId: args.operation_id, configDir: args.config_dir,
|
|
212
|
+
healthUrl: args.health_url, version: bound.version,
|
|
213
|
+
definitionDigest: rollback && r.compatibility && r.paired_restore_completed_at
|
|
214
|
+
? readCompatibility(r.compatibility.descriptor_path).descriptor.definition_sha256
|
|
215
|
+
: bound.service_definition_sha256 || r.baseline.host.service_definition_sha256,
|
|
216
|
+
rollback, requireRestore, restartId: r.restart_id || args.restart_id,
|
|
217
|
+
observe: checkedObserve, timeoutMs: overrides.qualificationTimeoutMs ?? 180_000, onStage: phase });
|
|
218
|
+
};
|
|
219
|
+
const terminal = (status, receipt) => {
|
|
220
|
+
const rolledBack = receipt.state === "ROLLED_BACK_HEALTHY";
|
|
221
|
+
const result = {
|
|
222
|
+
status, operation_id: args.operation_id, continuity_path: path,
|
|
223
|
+
completed_at: new Date().toISOString(), observed_version: rolledBack ? receipt.source.version : receipt.target.version,
|
|
224
|
+
rollback_available: false, rollback_completed: rolledBack && !restartOnly, restart_attempted: receipt.restart_attempted,
|
|
225
|
+
operation_cleanup_completed: receipt.state === "CLEANED", root_failure: receipt.first_failure,
|
|
226
|
+
retained_recovery_assets: receipt.state !== "CLEANED", error_redacted: true,
|
|
227
|
+
};
|
|
228
|
+
if (args.result_path) writeContinuityAtomic(args.result_path, result);
|
|
229
|
+
patch({ status, phase: rolledBack ? restartOnly ? "Restart recovered; original Sessions, history and route verified"
|
|
230
|
+
: "Previous version, Sessions and route restored; recovery evidence retained"
|
|
231
|
+
: restartOnly ? "Restarted; all Sessions, history and route verified" : "Updated; all Sessions, history and route verified",
|
|
232
|
+
running: false, final_observed_version: result.observed_version, terminal_result_status: status,
|
|
233
|
+
operation_cleanup_completed: result.operation_cleanup_completed, rollback_completed: result.rollback_completed,
|
|
234
|
+
service_status: "healthy", session_restore_status: "qualified_without_replay",
|
|
235
|
+
restart_restore_status: receipt.recovery_qualification?.sessions?.restore_required === false ? "not_required" : "consumed",
|
|
236
|
+
runner_pid: null, runner_lease_expires_at: null, next_safe_action: null,
|
|
237
|
+
recovery_required_at: null, recovery_reason: null, recovery_boundary: null,
|
|
238
|
+
error: rolledBack ? receipt.first_failure?.message : null,
|
|
239
|
+
...(rolledBack ? { recovery_result: { result: "passed", code: "prior_state_qualified",
|
|
240
|
+
safe_message: restartOnly ? "Restart recovered with every retained Session, history and authenticated Host route verified."
|
|
241
|
+
: "Previous package, service, every Session, history and authenticated Host route restored and verified." },
|
|
242
|
+
rollback_result: { result: restartOnly ? "not_required" : "passed", code: restartOnly ? "same_package_restart_recovered" : "paired_source_restored",
|
|
243
|
+
safe_message: restartOnly ? "The original package remains installed." : "The receipt-bound prior package and service definition are restored." } } : {}),
|
|
244
|
+
cleanup_result: { result: receipt.state === "CLEANED" ? "passed" : "incomplete",
|
|
245
|
+
code: receipt.state === "CLEANED" ? "operation_root_removed" : "operation_root_retained",
|
|
246
|
+
safe_message: receipt.state === "CLEANED" ? "Committed operation resources were removed." : "Recovery assets remain retained." },
|
|
247
|
+
});
|
|
248
|
+
// Publish the same terminal wording and separate login evidence as status/CLI.
|
|
249
|
+
const current = readUpdateOperationState({ configDir: args.config_dir });
|
|
250
|
+
writeUpdateOperationState(projectContinuityOperation(current, receipt), { configDir: args.config_dir });
|
|
251
|
+
return result;
|
|
252
|
+
};
|
|
253
|
+
const finalize = () => terminal(restartOnly ? "restart_complete" : "update_complete",
|
|
254
|
+
finalizeContinuityAssets({ path, operationId: args.operation_id, configDir: args.config_dir, activePackageRoot: args.active_package_root }));
|
|
255
|
+
if (["COMMITTED", "CLEANED"].includes(get().state) &&
|
|
256
|
+
(args.restore_before_restart === "1" || args.rollback_before_restart === "1")) throw continuityFailure("recovery", "already_committed", "Rollback is closed after COMMITTED; use the explicit finalize command for cleanup.");
|
|
257
|
+
if (["COMMITTED", "CLEANED"].includes(get().state)) return finalize();
|
|
258
|
+
if (get().state === "ROLLED_BACK_HEALTHY") return terminal(restartOnly ? "restart_complete" : "rolled_back", get());
|
|
259
|
+
|
|
260
|
+
if (args.recheck_only === "1") {
|
|
261
|
+
const r = get();
|
|
262
|
+
const live = await observe({ configDir: args.config_dir, operationId: args.operation_id, origin: new URL(args.health_url).origin });
|
|
263
|
+
const pids = listeners(r.baseline.host.port);
|
|
264
|
+
const unchanged = !r.restart_attempted && !r.restart_id && live.host.pid === r.baseline.host.pid;
|
|
265
|
+
if (r.state !== "RECOVERY_REQUIRED" || pids.length !== 1 || pids[0] !== live.host.pid ||
|
|
266
|
+
live.host.version !== r.source.version || (!unchanged && !r.paired_restore_completed_at) ||
|
|
267
|
+
digestContinuityPackage(args.active_package_root) !== r.source.package_tree_sha256 ||
|
|
268
|
+
!serviceDefinitionMatchesRetainedDigest(r.baseline.host.service_definition_path, r.source.service_definition_sha256)) {
|
|
269
|
+
throw continuityFailure("recovery", "recheck_identity", "The existing Host is not a proved unchanged or already-restored source; recovery remains protected.");
|
|
270
|
+
}
|
|
271
|
+
if (!unchanged) verifyContinuityAssets({ path, operationId: args.operation_id,
|
|
272
|
+
configDir: args.config_dir, activePackageRoot: args.active_package_root });
|
|
273
|
+
const proof = await qualify(true, !unchanged);
|
|
274
|
+
return terminal(restartOnly ? "restart_complete" : "rolled_back",
|
|
275
|
+
recordContinuityRecovery(path, args.operation_id, new Error(r.first_failure?.message || "Update did not complete"), proof));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (restartOnly && args.recover_restart === "1" && get().state === "RECOVERY_REQUIRED") {
|
|
279
|
+
try {
|
|
280
|
+
const r = get();
|
|
281
|
+
if (digestContinuityPackage(args.active_package_root) !== r.source.package_tree_sha256 ||
|
|
282
|
+
!serviceDefinitionMatchesRetainedDigest(
|
|
283
|
+
r.baseline.host.service_definition_path,
|
|
284
|
+
r.source.service_definition_sha256
|
|
285
|
+
)) {
|
|
286
|
+
throw continuityFailure("restart_recovery", "original_package_definition", "Restart recovery requires the exact original package and service definition; retain the original Session snapshot.");
|
|
287
|
+
}
|
|
288
|
+
const binding = r.restart_only_snapshot;
|
|
289
|
+
if (binding?.path !== join(resolve(args.config_dir), "restart-restore-state.json") ||
|
|
290
|
+
binding.saved_path !== `${path}.restore` || continuityDigest(readFileSync(binding.saved_path)) !== binding.sha256) throw continuityFailure("restart_recovery", "fresh_restore_binding", "The original fresh restart snapshot is unavailable or changed.");
|
|
291
|
+
const saved = JSON.parse(readFileSync(binding.saved_path, "utf8"));
|
|
292
|
+
if (saved.state !== "prepared" || saved.restart_id !== r.restart_id) throw continuityFailure("restart_recovery", "fresh_restore_identity", "Restart recovery cannot substitute another Session snapshot.");
|
|
293
|
+
writeContinuityBytesAtomic(binding.path, readFileSync(binding.saved_path), { mode: binding.mode ?? 0o600 });
|
|
294
|
+
await runService("service:restart");
|
|
295
|
+
const proof = await qualify(true, true);
|
|
296
|
+
return terminal("restart_complete", recordContinuityRecovery(path, args.operation_id,
|
|
297
|
+
new Error(r.first_failure.message), proof));
|
|
298
|
+
} catch (error) {
|
|
299
|
+
recordContinuityRecovery(path, args.operation_id, error);
|
|
300
|
+
patch({ status: "restart_recovery_required", phase: "Restart recovery failed; original evidence retained",
|
|
301
|
+
running: false, runner_pid: null, runner_lease_expires_at: null, operation_cleanup_completed: false,
|
|
302
|
+
error: sanitizeLifecycleDiagnostic(error.message), recovery_boundary: error.code || "restart_recovery_failed" });
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let failure;
|
|
308
|
+
try {
|
|
309
|
+
if (args.restore_before_restart === "1" || args.rollback_before_restart === "1" || get().state === "RECOVERY_REQUIRED") {
|
|
310
|
+
throw continuityFailure("recovery", "recovery_requested", "Continue the retained operation's prior-state recovery.");
|
|
311
|
+
}
|
|
312
|
+
if (restartOnly && get().state === "PREPARED") advanceContinuity(path, args.operation_id, "PACKAGE_SWITCHED", { package_unchanged: true });
|
|
313
|
+
if (get().state === "PREPARED") throw continuityFailure("recovery", "activation_incomplete", "Package activation was interrupted before its durable boundary.");
|
|
314
|
+
if (get().state === "PACKAGE_SWITCHED") {
|
|
315
|
+
await bindLegacyConnectionMode();
|
|
316
|
+
const r = get();
|
|
317
|
+
if (!r.restart_attempted) {
|
|
318
|
+
const restore = readHostRestartRestoreState({ configDir: args.config_dir });
|
|
319
|
+
if (restore?.state !== "prepared" || restore.restart_id !== (r.restart_id || args.restart_id) || restore.stack_name !== args.stack) throw continuityFailure("restart_preflight", "fresh_restore_receipt", "The exact fresh prepared Session receipt is missing.");
|
|
320
|
+
if (overrides.beforeForwardRestart) await overrides.beforeForwardRestart(r);
|
|
321
|
+
await runService("service:restart", restartOnly ? {} : {
|
|
322
|
+
expectedRetainedCurrentHostCwd: join(r.resources.previous_package_root, "host-service"),
|
|
323
|
+
expectedTargetServiceDefinitionSha256: r.target.service_definition_sha256,
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
// Crash after the canonical stop/start marker: observe the replacement.
|
|
327
|
+
// Never repeat the forward restart to paper over an unknown outcome.
|
|
328
|
+
const observation = await observe({ configDir: args.config_dir, operationId: args.operation_id, origin: new URL(args.health_url).origin });
|
|
329
|
+
if (observation.host.pid === r.baseline.host.pid || observation.host.version !== r.target.version) throw continuityFailure("restart", "interrupted_restart", "Interrupted restart has no proven replacement Host.");
|
|
330
|
+
}
|
|
331
|
+
advanceContinuity(path, args.operation_id, "SERVICE_RESTARTED", { canonical_restart_completed: true });
|
|
332
|
+
}
|
|
333
|
+
const finalQualification = await qualify(false, true);
|
|
334
|
+
// A continuation can requalify an already-written TUNNEL_QUALIFIED
|
|
335
|
+
// boundary. Preserve the original transition and durably bind the fresh
|
|
336
|
+
// observation used by this commit, rather than discarding that evidence.
|
|
337
|
+
advanceContinuity(path, args.operation_id, "COMMITTED", { final_qualification: finalQualification });
|
|
338
|
+
return finalize();
|
|
339
|
+
} catch (error) {
|
|
340
|
+
// Failure during cleanup is a committed cleanup failure, never rollback.
|
|
341
|
+
if (["COMMITTED", "CLEANED"].includes(get().state)) {
|
|
342
|
+
patch({ status: "restart_recovery_required", phase: "Qualification committed; cleanup requires continuation",
|
|
343
|
+
running: false, operation_cleanup_completed: false, runner_pid: null, runner_lease_expires_at: null,
|
|
344
|
+
error: sanitizeLifecycleDiagnostic(error.message), next_safe_action: "complete_update_and_restart" });
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
failure = error;
|
|
348
|
+
recordContinuityRecovery(path, args.operation_id, error);
|
|
349
|
+
}
|
|
350
|
+
patch({ status: restartOnly ? "restart_recovery_required" : "post_restart_health_failed",
|
|
351
|
+
phase: restartOnly ? "Restart qualification failed; recovery required" : "Target qualification failed; restoring the previous version",
|
|
352
|
+
running: !restartOnly, error: sanitizeLifecycleDiagnostic(failure.message) });
|
|
353
|
+
if (restartOnly) {
|
|
354
|
+
// A same-package restart has no alternate package to exchange. Preserve its
|
|
355
|
+
// fresh restore authority and report the exact cause for canonical recovery.
|
|
356
|
+
patch({ running: false, runner_pid: null, runner_lease_expires_at: null,
|
|
357
|
+
recovery_required_at: new Date().toISOString(), recovery_boundary: "restart_qualification_incomplete",
|
|
358
|
+
recovery_reason: get().first_failure.message, next_safe_action: null, operation_cleanup_completed: false });
|
|
359
|
+
throw failure;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const r = get();
|
|
363
|
+
const pids = listeners(r.baseline.host.port);
|
|
364
|
+
if (pids.length > 1) throw continuityFailure("recovery", "ambiguous_listener", "Recovery found more than one Host listener.");
|
|
365
|
+
const originalStillLive = pids.length === 1 && pids[0] === r.baseline.host.pid;
|
|
366
|
+
if (!r.source.definition_snapshot_digest && !r.restart_attempted && originalStillLive &&
|
|
367
|
+
r.source.package_tree_sha256 && digestContinuityPackage(args.active_package_root) === r.source.package_tree_sha256 &&
|
|
368
|
+
continuityDigest(readFileSync(r.baseline.host.service_definition_path)) === r.baseline.host.service_definition_sha256) {
|
|
369
|
+
// PREPARED can be interrupted before the installer retains any assets.
|
|
370
|
+
// Nothing is exchanged here: prove the complete unchanged prior state.
|
|
371
|
+
return terminal("rolled_back", recordContinuityRecovery(path, args.operation_id, failure, await qualify(true, false)));
|
|
372
|
+
}
|
|
373
|
+
validateContinuityResources(r, args.config_dir, args.active_package_root);
|
|
374
|
+
prepareCompatibility({ path, operationId: args.operation_id, render: renderServiceDefinition });
|
|
375
|
+
verifyContinuityAssets({ path, operationId: args.operation_id, configDir: args.config_dir, activePackageRoot: args.active_package_root });
|
|
376
|
+
const restartPrior = !originalStillLive || existsSync(r.resources.previous_package_root);
|
|
377
|
+
if (pids.length && !originalStillLive) {
|
|
378
|
+
const live = await observe({ configDir: args.config_dir, operationId: args.operation_id, origin: new URL(args.health_url).origin });
|
|
379
|
+
if (r.paired_restore_completed_at && live.host.pid === pids[0] && live.host.version === r.source.version &&
|
|
380
|
+
digestContinuityPackage(args.active_package_root) === r.source.package_tree_sha256) {
|
|
381
|
+
// Asset restoration does not prove provider restoration. Retry only
|
|
382
|
+
// positively failed/exited retained runtimes, never a merely missing
|
|
383
|
+
// or still-starting provider. Healthy route-only recovery stays read-only.
|
|
384
|
+
const missing = r.baseline.sessions.filter((row) => !live.sessions.some((seen) => seen.session_id === row.session_id));
|
|
385
|
+
const failedIds = new Set((live.restore_result?.session_restore?.failed || []).map((row) => row.session_id));
|
|
386
|
+
const desired = live.desired_session_restore;
|
|
387
|
+
const exited = desired?.valid === true ? desired.sessions.filter((row) => row.state === "failed") : [];
|
|
388
|
+
const retryProviders = missing.length > 0 && live.restart_state === "consumed" && live.restart_id === r.restart_id &&
|
|
389
|
+
missing.every((row) => failedIds.has(row.session_id) || exited.some((seen) =>
|
|
390
|
+
seen.session_id === row.session_id && ["agent_id", "agent_folder", "provider"].every((key) => seen[key] === row[key])));
|
|
391
|
+
if (!retryProviders) return terminal("rolled_back", recordContinuityRecovery(path, args.operation_id, failure, await qualify(true, true)));
|
|
392
|
+
qualifyContinuityHost(r.baseline.host, live.host, { version: r.source.version,
|
|
393
|
+
runtimeIdentityDigest: r.source.runtime_identity_digest,
|
|
394
|
+
definitionDigest: r.compatibility ? readCompatibility(r.compatibility.descriptor_path).descriptor.definition_sha256 : r.source.service_definition_sha256 });
|
|
395
|
+
if ([...live.sessions, ...(desired?.valid === true ? desired.sessions : [])].some((row) => !r.baseline.sessions.some((expected) => expected.session_id === row.session_id))) {
|
|
396
|
+
throw continuityFailure("recovery", "unexpected_live_session", "An unretained Session appeared; preserve the live Host and stop for a fresh recovery review.");
|
|
397
|
+
}
|
|
398
|
+
qualifyContinuitySessions(r.baseline.sessions.filter((row) => live.sessions.some((seen) => seen.session_id === row.session_id)), live.sessions, null, { requireRestore: false, path, operationId: args.operation_id });
|
|
399
|
+
await runService("service:stop");
|
|
400
|
+
restoreContinuityAssets({ path, operationId: args.operation_id, configDir: args.config_dir, activePackageRoot: args.active_package_root });
|
|
401
|
+
await runService("service:start");
|
|
402
|
+
releaseHostRestartLoginGate({ configDir: args.config_dir, gateId: args.login_gate_id || null,
|
|
403
|
+
restartId: r.restart_id || args.restart_id, reason: "prior_providers_retried_for_qualification" });
|
|
404
|
+
return terminal("rolled_back", recordContinuityRecovery(path, args.operation_id, failure, await qualify(true, true)));
|
|
405
|
+
}
|
|
406
|
+
if (live.host.pid !== pids[0] || live.host.version !== r.target.version) throw continuityFailure("recovery", "target_process_identity", "Recovery cannot bind the live process to this target.");
|
|
407
|
+
if (live.sessions.some((row) => !r.baseline.sessions.some((expected) => expected.session_id === row.session_id))) throw continuityFailure("recovery", "unexpected_live_session", "An unretained Session appeared; preserve the live Host and stop for a fresh recovery review.");
|
|
408
|
+
await runService("service:stop");
|
|
409
|
+
}
|
|
410
|
+
if (originalStillLive && restartPrior) {
|
|
411
|
+
// The retained source inode is now outside activeRoot. A copied package
|
|
412
|
+
// cannot move the running process's cwd back; restart the exact prior
|
|
413
|
+
// Host so the next canonical lifecycle preflight remains usable.
|
|
414
|
+
const live = await observe({ configDir: args.config_dir, operationId: args.operation_id, origin: new URL(args.health_url).origin });
|
|
415
|
+
if (live.sessions.some((row) => !r.baseline.sessions.some((expected) => expected.session_id === row.session_id))) throw continuityFailure("recovery", "unexpected_live_session", "An unretained Session appeared; preserve the live Host and stop for a fresh recovery review.");
|
|
416
|
+
recordContinuityRestartAttempt(path, args.operation_id);
|
|
417
|
+
await runService("service:stop");
|
|
418
|
+
}
|
|
419
|
+
if (!pids.length && !(overrides.confirmStoppedService || confirmStoppedContinuityService)(r)) {
|
|
420
|
+
throw continuityFailure("recovery", "service_stop_unproven", "No listener is present; confirm the exact service job is stopped before offline paired recovery.");
|
|
421
|
+
}
|
|
422
|
+
restoreContinuityAssets({ path, operationId: args.operation_id, configDir: args.config_dir, activePackageRoot: args.active_package_root });
|
|
423
|
+
if (restartPrior) await runService("service:start");
|
|
424
|
+
releaseHostRestartLoginGate({ configDir: args.config_dir, gateId: args.login_gate_id || null,
|
|
425
|
+
restartId: get().restart_id || args.restart_id, reason: "prior_host_available_for_qualification" });
|
|
426
|
+
const proof = await qualify(true, restartPrior);
|
|
427
|
+
const recovered = recordContinuityRecovery(path, args.operation_id, failure, proof);
|
|
428
|
+
return terminal("rolled_back", recovered);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
recordContinuityRecovery(path, args.operation_id, failure);
|
|
431
|
+
patch({ status: "rollback_failed", phase: "Recovery incomplete; retained assets require canonical continuation",
|
|
432
|
+
running: false, terminal_result_status: "rollback_failed", runner_pid: null, runner_lease_expires_at: null,
|
|
433
|
+
rollback_completed: false, operation_cleanup_completed: false, next_safe_action: "restore_and_restart",
|
|
434
|
+
recovery_required_at: new Date().toISOString(), recovery_boundary: error.code || "paired_recovery_incomplete",
|
|
435
|
+
recovery_reason: sanitizeLifecycleDiagnostic(error.message), error: sanitizeLifecycleDiagnostic(error.message) });
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -7,10 +7,12 @@ export const npmRequiredToolScripts = Object.freeze([
|
|
|
7
7
|
"restart_oysterun.sh",
|
|
8
8
|
"stop_oysterun.sh",
|
|
9
9
|
"run_oysterun_host.sh",
|
|
10
|
+
"oysterun_setup_input.sh",
|
|
10
11
|
"oysterun_service_common.sh",
|
|
11
12
|
"preflight_oysterun_restart.sh",
|
|
12
13
|
"stack_dashboard_credentials.mjs",
|
|
13
14
|
"sync_host_stack_config.mjs",
|
|
15
|
+
"resolve_user_host_port.mjs",
|
|
14
16
|
"routec_stack_readiness.mjs",
|
|
15
17
|
"oysterun_npm_package_manifest.mjs",
|
|
16
18
|
"oysterun_release_receipt_lib.mjs",
|
|
@@ -19,10 +21,14 @@ export const npmRequiredToolScripts = Object.freeze([
|
|
|
19
21
|
"verify_oysterun_runtime.mjs",
|
|
20
22
|
"install_oysterun_update.mjs",
|
|
21
23
|
"oysterun_update_restart_runner.mjs",
|
|
24
|
+
"oysterun_continuity_transaction.mjs",
|
|
25
|
+
"test_environment_remote_cloud_deprovision.mjs",
|
|
26
|
+
"p233_legacy_bootstrap.mjs",
|
|
22
27
|
]);
|
|
23
28
|
|
|
24
29
|
export const npmDocsToInclude = Object.freeze([
|
|
25
30
|
"docs/routec/dev/npmjs_publish.md",
|
|
31
|
+
"docs/routec/dev/p262_setup_input_file.md",
|
|
26
32
|
"docs/routec/dev/npm_native_dependency_install.md",
|
|
27
33
|
"docs/routec/dev/chat_local_link_annotation_pipeline.md",
|
|
28
34
|
"docs/routec/host_setup.md",
|
|
@@ -73,11 +79,34 @@ export const npmRequiredPackagePaths = Object.freeze([
|
|
|
73
79
|
"package.json",
|
|
74
80
|
"README.md",
|
|
75
81
|
"bin/oysterun.mjs",
|
|
82
|
+
"host-service/complete-cli-uninstall.mjs",
|
|
76
83
|
"host-service/server.mjs",
|
|
84
|
+
"host-service/onboard.mjs",
|
|
85
|
+
"host-service/host-port-resolution.mjs",
|
|
86
|
+
"host-service/installer-recovery-guidance.mjs",
|
|
87
|
+
"host-service/installer-reinstall-bindings.mjs",
|
|
88
|
+
"host-service/routec-matrix-storage-paths.mjs",
|
|
89
|
+
"host-service/legacy-continuity-observation.mjs",
|
|
90
|
+
"host-service/retained-host-state-holder.mjs",
|
|
91
|
+
"host-service/stopped-user-host-package.mjs",
|
|
92
|
+
"host-service/cli/installer-recovery-guidance.mjs",
|
|
93
|
+
"host-service/cli/assert-installer-reinstall-target.mjs",
|
|
94
|
+
"host-service/cli/recover-installer-lifecycle.mjs",
|
|
95
|
+
"tool_scripts/resolve_user_host_port.mjs",
|
|
77
96
|
"host-service/node-npm-runtime-identity.mjs",
|
|
78
97
|
"host-service/host-lifecycle-recovery.mjs",
|
|
79
98
|
"host-service/lifecycle-diagnostics.mjs",
|
|
80
99
|
"host-service/service-definition-migration.mjs",
|
|
100
|
+
"host-service/lifecycle-continuity.mjs",
|
|
101
|
+
"host-service/lifecycle-user-path.mjs",
|
|
102
|
+
"host-service/lifecycle-compatibility.mjs",
|
|
103
|
+
"host-service/lifecycle-compat-preload.mjs",
|
|
104
|
+
"host-service/lifecycle-compat-cli.mjs",
|
|
105
|
+
"host-service/lifecycle-continuity-assets.mjs",
|
|
106
|
+
"host-service/lifecycle-continuity-probe.mjs",
|
|
107
|
+
"host-service/lifecycle-continuity-projection.mjs",
|
|
108
|
+
"host-service/lifecycle-continuity-runner.mjs",
|
|
109
|
+
"host-service/lifecycle-service-render-bundle.mjs",
|
|
81
110
|
"host-service/cli/assert-lifecycle-operation-available.mjs",
|
|
82
111
|
"host-service/cli/run-host-lifecycle.mjs",
|
|
83
112
|
"host-service/connectivity/data-channel-adapter.mjs",
|
|
@@ -97,5 +126,8 @@ export const npmRequiredPackagePaths = Object.freeze([
|
|
|
97
126
|
"tool_scripts/verify_oysterun_runtime.mjs",
|
|
98
127
|
"tool_scripts/install_oysterun_update.mjs",
|
|
99
128
|
"tool_scripts/oysterun_update_restart_runner.mjs",
|
|
129
|
+
"tool_scripts/oysterun_continuity_transaction.mjs",
|
|
130
|
+
"tool_scripts/test_environment_remote_cloud_deprovision.mjs",
|
|
131
|
+
"tool_scripts/p233_legacy_bootstrap.mjs",
|
|
100
132
|
"tool_scripts/preflight_oysterun_restart.sh",
|
|
101
133
|
]);
|
|
@@ -53,6 +53,11 @@ run_host_lifecycle_request() {
|
|
|
53
53
|
fi
|
|
54
54
|
configure_stack_runtime
|
|
55
55
|
require_host_port_configured
|
|
56
|
+
if [[ "${STACK_NAME}" == production ]]; then
|
|
57
|
+
local bound_pid=""
|
|
58
|
+
bound_pid="$(resolve_bound_user_host_pid)"
|
|
59
|
+
ensure_port_listener_matches_expected_process "${HOST_PORT}" "${bound_pid}"
|
|
60
|
+
fi
|
|
56
61
|
local bootstrap_node=""
|
|
57
62
|
local node_bin=""
|
|
58
63
|
local npm_cli=""
|