@awak-app/simy-cli 0.6.10 → 0.6.12

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/README.md CHANGED
@@ -22,6 +22,18 @@ Both providers share one analysis deadline. A deterministic provider failure pau
22
22
  new claims and retains failed history until environment/settings change or **Retry
23
23
  analysis** is selected.
24
24
 
25
+ After **6 consecutive SQM analysis failures**, this machine stops claiming new
26
+ SQM analyses for the current Web/account/organization. Codex and Claude share the
27
+ counter; a completed analysis resets it. Empty polls and user cancellations do
28
+ not change it. The pause and partial failure count survive restart, sign-in and
29
+ provider changes. A review interrupted by a host exit counts once on restart;
30
+ graceful cancellation clears that unfinished marker without changing the count.
31
+ Reconnecting or selecting Retry cannot clear this pause.
32
+ Installing and running a **newer SIMY version** resets the counter and resumes
33
+ claims automatically, subject to normal organization access and analysis settings.
34
+ The native organization menu shows when an update is required. This pause does
35
+ not change server analysis consent or stop ordinary implementation tasks.
36
+
25
37
  Agentic Loop preflight accepts the task's existing requirement, acceptance
26
38
  criteria, required checks and charter context alongside the repository/provider.
27
39
  Its `startup` summary lists all blockers, unverified conditions and recovery
@@ -41,6 +53,16 @@ remain available for history and usage warnings. The Web controller supports
41
53
  connected local Codex/Claude engines; unsupported cloud execution is refused
42
54
  before dispatch.
43
55
 
56
+ A completed Agentic worker's structured result is saved privately before evidence
57
+ collection and the final observation flush. If the controller restarts after that save,
58
+ the authorized daemon recovers the same attempt and collects fresh evidence
59
+ without dispatching the completed worker again. Collector read failures retry up
60
+ to three times; exhausted reads retain the result for an explicit continuation.
61
+ A result receipt does not prove acceptance. Stop, changed identity or objective,
62
+ and an already consumed remote result prevent automatic recovery. An unfinished
63
+ worker or a missing, invalid or redacted structured result still requires
64
+ reconciliation; no unknown external operation is automatically replayed.
65
+
44
66
  For interrupted Agentic work, `GET /v1/agentic-loop/:id/recovery` provides a
45
67
  read-only objective and checkout fingerprint. Reconcile an uncertain side effect
46
68
  with `POST` to that endpoint (`action: reconcile`, authoritative
@@ -794,3 +816,29 @@ Only summary metadata is retained, without analysis prompts, source inputs,
794
816
  model responses or credentials. Historical summaries are not a replacement for
795
817
  signed proof files or delivery acceptance. Records from versions that kept
796
818
  analysis only in memory cannot be recovered after that process exits.
819
+
820
+
821
+ ### Reusing a running service and launch recovery
822
+
823
+ `simy` and `simy --daemon` first inspect the private discovery record and verify
824
+ its live PID, instance, Web origin, CLI/API version and saved account,
825
+ organization and device. A compatible background service is reused: the entry
826
+ prints its existing address and opens SIMY Web (or its sign-in page). The local
827
+ interactive console is only created when this entry owns a new service.
828
+ SQM authorization also reuses that service and leaves it running when SQM exits.
829
+ Unreachable or incompatible live services produce diagnostics instead of a
830
+ second daemon. A stale record with a dead PID permits normal startup.
831
+
832
+ Web-led launches can omit their temporary challenge. The daemon requests a
833
+ credential for the existing queued run using its authenticated device. An
834
+ explicitly expired challenge can be renewed once; consumed, invalid or
835
+ cross-scope credentials cannot. Refresh the original run/device before retrying
836
+ a consumed credential because execution may have started before its ledger
837
+ status was uploaded. This recovery requires the companion Web
838
+ `/api/local-cli/challenges/issue` endpoint; existing valid challenges continue
839
+ to work against older Web versions.
840
+
841
+ Scope mismatch diagnostics identify the changed account, organization, device
842
+ or Web host and tell the caller to restore the original identity. Launch and
843
+ recovery operations do not switch identity automatically. The private
844
+ `/v1/connection` endpoint rejects browser requests and never returns tokens.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
44
44
  "check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
45
45
  "check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js",
46
- "check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
46
+ "check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/connection-diagnostics.js && node --check ./src/launch-authorization.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
47
47
  "check:package": "node ./scripts/check-package-contents.js",
48
48
  "build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
49
49
  "build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
package/src/agent.js CHANGED
@@ -1,8 +1,11 @@
1
+ import { assertConnectionScope, connectionScope, connectionDiagnostic } from "./connection-diagnostics.js";
2
+ import { authorizeLaunch } from "./launch-authorization.js";
1
3
  import { normalizePurposeVerification, assertPurposeActor, preparePurposeVerification, verifyPurposeLocalTask, purposeSigningKeyPath, purposeVerificationCapability, purposeWorkspaceBusy } from "./purpose-local-proof.js";
2
4
  import { startupRequirementChecks, summarizeStartupChecks } from "./startup-preflight.js";
3
5
  import { BROWSER_HANDOFF_TRANSPORT, browserHandoffPage, decryptBrowserHandoff } from "./browser-handoff.js";
4
6
  import { createAuthorizationWindow } from "./browser-authorization.js";
5
7
  import { createSqmHistoryStore, projectSqmHistory, sqmHistoryScope } from "./sqm/history-store.js";
8
+ import { createSqmFailureCircuit, createSqmFailureCircuitStore } from "./sqm/failure-circuit.js";
6
9
  import { createDesktopAccessPolicy, desktopAccessReady, requireDesktopAccess } from './desktop-access.js';
7
10
  import { readSqmEnabled, writeSqmEnabled, readSqmSettings, writeSqmEngineFallback } from "./desktop-sqm-settings.js";
8
11
  import { createServer } from "node:http";
@@ -23,6 +26,7 @@ import {
23
26
  pauseLocalCodingRun,
24
27
  prepareLocalCodingRunRecovery,
25
28
  reconcileLocalCodingRun,
29
+ recoverCompletedLocalCodingRun,
26
30
  queueLocalCodingGuidance,
27
31
  recheckLocalCodingRun,
28
32
  publishRestoredLocalCodingRun,
@@ -35,7 +39,7 @@ import {
35
39
  toCompatibleCodingLoopState,
36
40
  toLedgerSnapshot,
37
41
  } from "./runner.js";
38
- import { assertRecoveryActor } from "./coding-run-recovery.js";
42
+ import { assertRecoveryActor, preserveCompletedWorkerResult } from "./coding-run-recovery.js";
39
43
  import { codingRunsRoot, loadCodingRunSnapshots, saveCodingRunSnapshot } from "./coding-run-store.js";
40
44
  import {
41
45
  defaultSessionRoot,
@@ -538,7 +542,7 @@ export async function startAgent({
538
542
  })();
539
543
  return organizationRefresh;
540
544
  };
541
- const requestDesktopLogin = (organizationId = null, { passive = false } = {}) => {
545
+ const requestDesktopLogin = (organizationId = null, { passive = false, reauthorize = false } = {}) => {
542
546
  const authorization = authorizationWindow.snapshot();
543
547
  if (authorization.state === "timed_out") {
544
548
  if (passive) return null;
@@ -566,7 +570,7 @@ export async function startAgent({
566
570
  refreshAvailableCapabilities();
567
571
  notifyDesktopStatusChange();
568
572
  } else if (isSessionValid(session, Date.now(), apiOrigin)) {
569
- if (!enforceDesktopAccess || desktopAccess.snapshot().state !== 'signed_out') return null;
573
+ if (!reauthorize && (!enforceDesktopAccess || desktopAccess.snapshot().state !== 'signed_out')) return null;
570
574
  if (!passive || !loginUrl) {
571
575
  authNonce = randomBytes(16).toString("base64url");
572
576
  loginUrl = null;
@@ -635,7 +639,7 @@ export async function startAgent({
635
639
  if (remoteTime > localTime) {
636
640
  const repository = findRepository(repositoryInventory, snapshot.charter?.repository);
637
641
  const remoteRun = restoreRun({
638
- snapshot, session, apiOrigin,
642
+ snapshot: preserveCompletedWorkerResult(localRun.snapshot, snapshot), session, apiOrigin,
639
643
  localPath: repository?.local_path || null,
640
644
  localStateRoot: codingRunStateRoot, sqmHistoryRoot: sessionRoot,
641
645
  });
@@ -647,6 +651,7 @@ export async function startAgent({
647
651
  completedAt: remoteRun.completedAt,
648
652
  restoredFromState: remoteRun.restoredFromState,
649
653
  restoredObjectiveUnverified: remoteRun.restoredObjectiveUnverified,
654
+ restoredCompletedWorkerResult: remoteRun.restoredCompletedWorkerResult,
650
655
  });
651
656
  await saveCodingRunSnapshot(codingRunStateRoot, localRun.snapshot);
652
657
  await publishRestoredLocalCodingRun(localRun);
@@ -673,6 +678,23 @@ export async function startAgent({
673
678
  if (restored) notifyDesktopStatusChange();
674
679
  };
675
680
 
681
+ const recoverCompletedCodingRuns = async () => {
682
+ if (!daemon || shuttingDown || !desktopWorkAllowed() || !isSessionValid(session, Date.now(), apiOrigin)) return;
683
+ const requestedSession = session;
684
+ for (const run of registry.list()) {
685
+ if (!run.restoredCompletedWorkerResult || run.operation || run.child) continue;
686
+ try {
687
+ const scope = await authorizeDesktopWork();
688
+ if (shuttingDown || session !== requestedSession || !desktopWorkAllowed()) return;
689
+ assertRecoveryActor(run);
690
+ await authorizeDesktopWork(scope);
691
+ // Claim synchronously before starting the asynchronous controller. The
692
+ // normal process lease still protects this run against another daemon.
693
+ void recoverCompletedLocalCodingRun(run, dependencies.runOptions).catch(error => reportStartupConnectionError(error, quiet));
694
+ } catch (error) { reportStartupConnectionError(error, quiet); }
695
+ }
696
+ };
697
+
676
698
  const restoreLocalCodingRuns = async () => {
677
699
  if (!desktopWorkAllowed()) return;
678
700
  const requestedSession = session;
@@ -822,6 +844,7 @@ export async function startAgent({
822
844
  try {
823
845
  await restoreLocalCodingRuns();
824
846
  await restoreRemoteRuns();
847
+ await recoverCompletedCodingRuns();
825
848
  startCodingRunMonitor();
826
849
  } catch (error) {
827
850
  reportStartupConnectionError(error, quiet);
@@ -933,6 +956,7 @@ export async function startAgent({
933
956
  dependencies.createSqmLocalReviewWorker || createSqmLocalReviewWorker;
934
957
  sqmLocalReviewWorker = createWorker({
935
958
  api: sqmApi,
959
+ failureCircuit: createSqmFailureCircuit({ version: CLI_VERSION, store: createSqmFailureCircuitStore({ root: sessionRoot }) }),
936
960
  beginWork: () => acquireUpdateWork(updateManager),
937
961
  runReview: async (request, provider, context = {}) => {
938
962
  const scope = await authorizeDesktopWork();
@@ -1005,14 +1029,17 @@ export async function startAgent({
1005
1029
  );
1006
1030
  };
1007
1031
 
1032
+ let connectionDiscovery = null;
1033
+ const launchingRuns = new Set();
1008
1034
  const server = createServer(async (req, res) => {
1035
+ let reservedLaunch = null;
1009
1036
  try {
1010
1037
  const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
1011
1038
  const localHandoff = url.pathname === "/v1/auth/browser-complete";
1012
1039
  const localOrigin = `http://127.0.0.1:${port}`;
1013
1040
  if (localHandoff ? (req.headers.origin !== localOrigin || req.headers.host !== `127.0.0.1:${port}`)
1014
1041
  : !isRequestOriginAllowed(req.headers.origin, apiOrigin)) {
1015
- json(res, 403, { error: "request origin does not match the configured SIMY Web host" });
1042
+ json(res, 403, connectionDiagnostic("connection_scope_changed", "web_origin"));
1016
1043
  return;
1017
1044
  }
1018
1045
  applyCors(req, res, apiOrigin);
@@ -1028,7 +1055,7 @@ export async function startAgent({
1028
1055
  return;
1029
1056
  }
1030
1057
  const agenticLoopPath = canonicalAgenticLoopPath(url.pathname);
1031
- const desktopRecoveryRoute = ['/v1/health', '/v1/capabilities', '/v1/auth/complete', '/v1/auth/browser-complete'].includes(url.pathname) ||
1058
+ const desktopRecoveryRoute = ['/v1/health', '/v1/connection', '/v1/capabilities', '/v1/auth/complete', '/v1/auth/browser-complete'].includes(url.pathname) ||
1032
1059
  req.method === 'POST' && (/^\/v1\/local-tasks\/[^/]+\/control$/.test(url.pathname) || /^\/v1\/agentic-loop\/[^/]+\/control$/.test(agenticLoopPath));
1033
1060
  let requestAccessScope;
1034
1061
  if (enforceDesktopAccess && !desktopRecoveryRoute) {
@@ -1053,6 +1080,28 @@ export async function startAgent({
1053
1080
  res.once("finish", releaseUpdateWork);
1054
1081
  res.once("close", releaseUpdateWork);
1055
1082
  }
1083
+ if (["GET", "POST"].includes(req.method) && url.pathname === "/v1/connection") {
1084
+ // Private local entry points only. Browser origins must not read account
1085
+ // bindings, instance identifiers or the pending authorization nonce.
1086
+ if (req.headers.origin !== undefined || req.headers.host !== `127.0.0.1:${port}`) {
1087
+ json(res, 403, { error: "Local connection discovery only.", code: "local_connection_only" }); return;
1088
+ }
1089
+ let authorizationUrl;
1090
+ if (req.method === "POST") {
1091
+ const body = await readJson(req);
1092
+ if (!connectionDiscovery?.instanceId || body.instance_id !== connectionDiscovery.instanceId) {
1093
+ json(res, 409, connectionDiagnostic("connection_service_mismatch")); return;
1094
+ }
1095
+ try { assertConnectionScope(body.scope, connectionScope(session || reclaimableSession, apiOrigin)); }
1096
+ catch (error) { json(res, 409, { error: error.message, code: error.code, field: error.field, recovery: error.recovery }); return; }
1097
+ authorizationUrl = requestDesktopLogin(null, { reauthorize: body.reauthorize === true });
1098
+ } else authorizationUrl = requestDesktopLogin(null, { passive: true });
1099
+ json(res, 200, { ok: true, cli_version: CLI_VERSION, api_contract_version: CLI_API_CONTRACT_VERSION,
1100
+ daemon, web_origin: apiOrigin, instance_id: connectionDiscovery?.instanceId || null, pid: process.pid,
1101
+ scope: connectionScope(session || reclaimableSession, apiOrigin), session_valid: isSessionValid(session, Date.now(), apiOrigin),
1102
+ login_url: authorizationUrl });
1103
+ return;
1104
+ }
1056
1105
  if (req.method === "GET" && url.pathname === "/v1/health") {
1057
1106
  const sessionValid = isSessionValid(session, Date.now(), apiOrigin);
1058
1107
  const connection = connectionStatus.snapshot({ authenticated: sessionValid });
@@ -1965,25 +2014,23 @@ export async function startAgent({
1965
2014
  return;
1966
2015
  }
1967
2016
  }
1968
- const verified = await verifyLaunchChallenge({
1969
- apiOrigin,
1970
- apiBaseUrl: session.api_base_url,
1971
- token: session.token,
1972
- runId: String(body.run_id || ""),
1973
- challenge: String(body.challenge || ""),
1974
- });
1975
- if (!verified.ok) {
1976
- json(res, 403, { error: verified.error || "launch challenge rejected" });
1977
- return;
1978
- }
1979
2017
  const runId = String(body.run_id || "");
1980
- if (!runId) {
1981
- json(res, 400, { error: "run_id is required" });
1982
- return;
1983
- }
2018
+ if (!runId) { json(res, 400, { error: "run_id is required" }); return; }
2019
+ const launchSession = session;
2020
+ try { assertConnectionScope(body.connection_scope, connectionScope(launchSession, apiOrigin)); }
2021
+ catch (error) { json(res, 409, { error: error.message, code: error.code, field: error.field, recovery: error.recovery }); return; }
2022
+ if (launchingRuns.has(runId)) { json(res, 409, { error: "This launch is still being checked. Refresh its status before retrying.", code: "launch_in_progress" }); return; }
2023
+ launchingRuns.add(runId);
2024
+ reservedLaunch = runId;
2025
+ const verified = await authorizeLaunch({ apiOrigin, apiBaseUrl: launchSession.api_base_url,
2026
+ token: launchSession.token, runId, challenge: String(body.challenge || ""),
2027
+ fetchImpl: dependencies.fetch || globalThis.fetch });
2028
+ if (session !== launchSession || !isSessionValid(session, Date.now(), apiOrigin)) {
2029
+ json(res, 409, changedLaunchSession(launchSession, session, apiOrigin)); return;
2030
+ }
2031
+ if (!verified.ok) { json(res, verified.status, connectionDiagnostic(verified.code)); return; }
1984
2032
  if (registry.has(runId)) {
1985
- json(res, 409, { error: "agentic loop run already exists" });
1986
- return;
2033
+ json(res, 409, { error: "agentic loop run already exists" }); return;
1987
2034
  }
1988
2035
  let executionTarget;
1989
2036
  try {
@@ -2047,6 +2094,10 @@ export async function startAgent({
2047
2094
  });
2048
2095
  return;
2049
2096
  }
2097
+ if (session !== launchSession || !isSessionValid(session, Date.now(), apiOrigin)) {
2098
+ await cleanupRunAttachments(stagedAttachments);
2099
+ json(res, 409, changedLaunchSession(launchSession, session, apiOrigin)); return;
2100
+ }
2050
2101
  const run = createRun({
2051
2102
  runId,
2052
2103
  request: {
@@ -2114,6 +2165,9 @@ export async function startAgent({
2114
2165
  throw error;
2115
2166
  }
2116
2167
  await authorizeDesktopWork(requestAccessScope);
2168
+ if (session !== launchSession || !isSessionValid(session, Date.now(), apiOrigin)) {
2169
+ json(res, 409, changedLaunchSession(launchSession, session, apiOrigin)); return;
2170
+ }
2117
2171
  void startLocalCodingRun(run, dependencies.runOptions);
2118
2172
  json(res, 202, { ok: true, run_id: run.id });
2119
2173
  return;
@@ -2194,7 +2248,7 @@ export async function startAgent({
2194
2248
  json(res, 200, { recovery, run_id: run.id, resumed: false });
2195
2249
  }
2196
2250
  } catch (error) {
2197
- json(res, 409, { error: error instanceof Error ? error.message : "Recovery could not be verified.", code: error.code || "recovery_rejected" });
2251
+ json(res, 409, { error: error instanceof Error ? error.message : "Recovery could not be verified.", code: error.code || "recovery_rejected", ...(error.field ? { field: error.field, recovery: error.recovery } : {}) });
2198
2252
  }
2199
2253
  return;
2200
2254
  }
@@ -2427,13 +2481,16 @@ export async function startAgent({
2427
2481
  }
2428
2482
  if (String(err?.code || "").startsWith("purpose_verification_")) { json(res, err.status || 409, { error: err.message, code: err.code }); return; }
2429
2483
  json(res, 500, { error: err instanceof Error ? err.message : "internal error" });
2484
+ } finally {
2485
+ // An aborted HTTP response must not leave a permanent launch reservation.
2486
+ // Keep the reservation until all asynchronous preparation has settled.
2487
+ if (reservedLaunch) launchingRuns.delete(reservedLaunch);
2430
2488
  }
2431
2489
  });
2432
2490
 
2433
2491
  await new Promise((resolve) => server.listen(requestedPort, "127.0.0.1", resolve));
2434
2492
  const address = server.address();
2435
2493
  port = typeof address === "object" && address ? address.port : requestedPort;
2436
- let connectionDiscovery = null;
2437
2494
  try {
2438
2495
  connectionDiscovery = await (
2439
2496
  dependencies.publishConnectionDiscovery || publishConnectionDiscovery
@@ -2463,6 +2520,7 @@ export async function startAgent({
2463
2520
  await restoreLocalCodingRuns();
2464
2521
  if (shuttingDown) return;
2465
2522
  await restoreRemoteRuns();
2523
+ await recoverCompletedCodingRuns();
2466
2524
  startCodingRunMonitor();
2467
2525
  } catch (error) {
2468
2526
  reportStartupConnectionError(error, quiet);
@@ -2571,6 +2629,9 @@ export async function startAgent({
2571
2629
  activeJobId: sqmLocalReviewWorker?.snapshot?.().active_job_id ?? null,
2572
2630
  issue: sqmLocalReviewWorker?.snapshot?.().issue ?? null,
2573
2631
  providerPause: sqmLocalReviewWorker?.snapshot?.().provider_pause ?? null,
2632
+ consecutiveFailures: sqmLocalReviewWorker?.snapshot?.().consecutive_failures ?? 0,
2633
+ pausedUntilUpdate: sqmLocalReviewWorker?.snapshot?.().paused_until_update === true,
2634
+ pausedAtVersion: sqmLocalReviewWorker?.snapshot?.().paused_at_version ?? null,
2574
2635
  providerReady: [codex, claude].some(provider => provider?.state === "ready") },
2575
2636
  ...projectSqmHistory(await historyForCurrentScope(), sqmLocalReviewWorker?.desktopTasks?.()),
2576
2637
  update: updateManager?.snapshot?.() ?? null,
@@ -3112,6 +3173,7 @@ function capabilitiesFromInspections(codex, claude) {
3112
3173
  workflow_terminal_contract: true,
3113
3174
  workflow_deliverable_upload: true,
3114
3175
  sqm_local_review: codex.compatible || claude.compatible,
3176
+ sqm_failure_pause_until_update: true,
3115
3177
  codex_work_pattern_source: codex.compatible || claude.compatible,
3116
3178
  },
3117
3179
  session_ttl_hours: 48,
@@ -3265,26 +3327,6 @@ function backendPreflightSummary(backend, inspection) {
3265
3327
  return `${label} is not available. Install or update it: ${inspection.update_command}`;
3266
3328
  }
3267
3329
 
3268
- async function verifyLaunchChallenge({ apiOrigin, apiBaseUrl, token, runId, challenge }) {
3269
- try {
3270
- const response = await fetch(
3271
- webApiUrl("challenges/verify", { webOrigin: apiOrigin, apiBaseUrl }),
3272
- {
3273
- method: "POST",
3274
- headers: webApiHeaders(
3275
- { token },
3276
- { "Content-Type": "application/json" },
3277
- ),
3278
- body: JSON.stringify({ run_id: runId, challenge }),
3279
- },
3280
- );
3281
- if (response.ok) return { ok: true };
3282
- const payload = await response.json().catch(() => null);
3283
- return { ok: false, error: webApiErrorMessage(payload, response.status) };
3284
- } catch (err) {
3285
- return { ok: false, error: err instanceof Error ? err.message : "challenge check failed" };
3286
- }
3287
- }
3288
3330
 
3289
3331
  function canonicalAgenticLoopPath(pathname) {
3290
3332
  return pathname.replace(/^\/v1\/coding-loop(?=\/|$)/, "/v1/agentic-loop");
@@ -3546,3 +3588,9 @@ function providerAuthenticationFailure(provider, status) {
3546
3588
  }),
3547
3589
  };
3548
3590
  }
3591
+
3592
+ function changedLaunchSession(previous, current, origin) {
3593
+ try { assertConnectionScope(connectionScope(previous, origin), connectionScope(current, origin)); }
3594
+ catch (error) { return { error: error.message, code: error.code, field: error.field, recovery: error.recovery }; }
3595
+ return connectionDiagnostic("connection_session_changed");
3596
+ }
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.6.10";
16
+ export const CLI_VERSION = "0.6.12";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(