@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 +48 -0
- package/package.json +2 -2
- package/src/agent.js +93 -45
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +110 -30
- package/src/codex-plugins/simy/hooks/workspace-lease.mjs +32 -5
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +4 -2
- package/src/coding-run-recovery.js +49 -0
- package/src/coding-run-store.js +9 -2
- package/src/connection-diagnostics.js +77 -0
- package/src/connection-discovery.js +161 -0
- package/src/delivery-hooks/command.js +5 -5
- package/src/delivery-hooks/desktop.js +5 -2
- package/src/delivery-hooks/supervisor.js +4 -3
- package/src/index.js +21 -0
- package/src/launch-authorization.js +80 -0
- package/src/orchestrator/evidence.js +10 -2
- package/src/orchestrator/loop.js +99 -44
- package/src/orchestrator/risk.js +17 -2
- package/src/runner.js +63 -27
- package/src/session-supervision/codex-app-server.js +72 -8
- package/src/session-supervision/continuation-analysis.js +46 -9
- package/src/session-supervision/index.js +70 -17
- package/src/session-supervision/statistics.js +138 -60
- package/src/sqm/failure-circuit.js +105 -0
- package/src/sqm/review-worker.js +18 -4
- package/src/sqm/session.js +9 -2
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.
|
|
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,
|
|
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
|
-
|
|
1982
|
-
|
|
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
|
+
}
|
package/src/cli-contract.js
CHANGED
|
@@ -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.
|
|
16
|
+
export const CLI_VERSION = "0.6.12";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|