@awak-app/simy-cli 0.5.71 → 0.5.73
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 +101 -3
- package/package.json +2 -2
- package/src/agent.js +205 -12
- package/src/backend-executable.js +45 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +22 -0
- package/src/codex-plugins/simy/hooks/continuation-policy.mjs +27 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +180 -31
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +129 -0
- package/src/coding-run-recovery.js +88 -0
- package/src/coding-run-store.js +2 -2
- package/src/continuation-policy.js +2 -0
- package/src/delivery-hooks/command.js +11 -5
- package/src/delivery-hooks/desktop.js +12 -3
- package/src/delivery-hooks/supervisor.js +17 -12
- package/src/desktop-host.js +19 -8
- package/src/desktop-sqm-settings.js +24 -7
- package/src/device-signing-identity.js +65 -0
- package/src/diagnostic-log.js +68 -0
- package/src/doctor.js +55 -0
- package/src/index.js +18 -1
- package/src/local-task.js +68 -3
- package/src/orchestrator/budget.js +55 -4
- package/src/orchestrator/charter-validation.js +33 -0
- package/src/orchestrator/completion-contract.js +1 -1
- package/src/orchestrator/contract.js +2 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +21 -3
- package/src/orchestrator/evidence.js +20 -19
- package/src/orchestrator/independent-audit.js +9 -0
- package/src/orchestrator/instruction.js +6 -1
- package/src/orchestrator/loop.js +66 -41
- package/src/orchestrator/outcome-monitor.js +95 -44
- package/src/orchestrator/outcome-progress.js +2 -0
- package/src/orchestrator/recovery.js +3 -0
- package/src/purpose-local-proof.js +380 -0
- package/src/runner.js +227 -19
- package/src/runtime-diagnostics.js +77 -0
- package/src/session-supervision/codex-app-server.js +3 -2
- package/src/session-supervision/index.js +16 -4
- package/src/sqm/bundle-store.js +14 -7
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +42 -4
- package/src/sqm/diagnostics.js +23 -0
- package/src/sqm/evidence-client.js +24 -7
- package/src/sqm/history-store.js +21 -5
- package/src/sqm/index.js +116 -17
- package/src/sqm/proof.js +4 -23
- package/src/sqm/report.js +9 -2
- package/src/sqm/request-recovery.js +82 -0
- package/src/sqm/review-worker.js +97 -15
- package/src/sqm/session.js +33 -3
- package/src/startup-preflight.js +44 -0
package/README.md
CHANGED
|
@@ -2,6 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
Local execution agent for SIMY Local Tasks and Agentic Loop runs.
|
|
4
4
|
|
|
5
|
+
`simy --version` prints the installed CLI version without starting an agent or
|
|
6
|
+
checking for updates. `simy doctor` performs read-only local connection and
|
|
7
|
+
provider login checks and prints a JSON diagnostic report: installed/running
|
|
8
|
+
versions, stale discovery records, session expiry, provider readiness, and recent
|
|
9
|
+
structured SQM failures. It does not run a model, renew login, or restart tasks.
|
|
10
|
+
Provider login is separate from model access; unverified model access remains
|
|
11
|
+
`unknown`. Diagnostic journals retain stage/code/HTTP status only, use private
|
|
12
|
+
files scoped to the Web/account/organization, rotate each writer's four 64 KiB
|
|
13
|
+
segments, and keep lifetime failure counts after event rotation.
|
|
14
|
+
|
|
15
|
+
The organization menu's **Allow Codex / Claude fallback** setting defaults to
|
|
16
|
+
off and grants only read-only SQM analysis permission to use the other signed-in
|
|
17
|
+
local provider. It is saved per Web/account/organization. Switching requires an
|
|
18
|
+
explicit native model rejection or unavailable executable, a server that supports
|
|
19
|
+
actual-provider provenance, and fresh actor/settings/lease checks. Authentication,
|
|
20
|
+
quota, malformed output, arbitrary failures and coding tasks do not switch engines.
|
|
21
|
+
Both providers share one analysis deadline. A deterministic provider failure pauses
|
|
22
|
+
new claims and retains failed history until environment/settings change or **Retry
|
|
23
|
+
analysis** is selected.
|
|
24
|
+
|
|
25
|
+
Agentic Loop preflight accepts the task's existing requirement, acceptance
|
|
26
|
+
criteria, required checks and charter context alongside the repository/provider.
|
|
27
|
+
Its `startup` summary lists all blockers, unverified conditions and recovery
|
|
28
|
+
actions together. Environment-only calls remain supported. Task checks reuse the
|
|
29
|
+
execution charter policy; model entitlement, external test resources and assurance
|
|
30
|
+
selection evidence are not guessed from a successful login or recommendation.
|
|
31
|
+
|
|
32
|
+
### Explicit progress continuation
|
|
33
|
+
|
|
34
|
+
Existing tasks use **Bounded** continuation. Select **Progress** before starting
|
|
35
|
+
an Agentic Loop or Local Task, or when enrolling an App session, to treat the
|
|
36
|
+
ordinary token/attempt/time limits as warning thresholds. New independently
|
|
37
|
+
verified acceptance criteria permit further work within finite hard ceilings;
|
|
38
|
+
repeated work without new proof pauses the same task. Approval requests, explicit
|
|
39
|
+
stops and authorization failures still pause execution. Provider text claiming
|
|
40
|
+
success does not count as verification.
|
|
41
|
+
|
|
42
|
+
Local Tasks retain valid output after a soft token warning and stop at their
|
|
43
|
+
explicit hard ceiling (at most 10 million reported provider tokens). The Agentic
|
|
44
|
+
controller limits a whole progress run to its approved allocation, at most
|
|
45
|
+
15 million tokens, and persists the allocation with canonical receipts. Missing
|
|
46
|
+
reliable usage is charged conservatively against the reserved allocation. The
|
|
47
|
+
Web controller supports this mode for the connected local Codex/Claude engines;
|
|
48
|
+
unsupported cloud execution is refused before dispatch.
|
|
49
|
+
|
|
50
|
+
In the App, pause a managed Delivery task before changing its continuation
|
|
51
|
+
policy or extending its bounds. Extension keeps usage, task identity, acceptance
|
|
52
|
+
and approvals, and requires a separate resume action. Delivery hard ceilings are
|
|
53
|
+
24 hours and 50 continuations; host-observed Codex usage can also impose a token
|
|
54
|
+
ceiling. A Claude live-hook session without reliable token telemetry explicitly
|
|
55
|
+
uses time/continuation bounds instead of inventing usage.
|
|
56
|
+
|
|
57
|
+
For interrupted Agentic work, `GET /v1/agentic-loop/:id/recovery` provides a
|
|
58
|
+
read-only objective and checkout fingerprint. Reconcile an uncertain side effect
|
|
59
|
+
with `POST` to that endpoint (`action: reconcile`, authoritative
|
|
60
|
+
`operation_status`, `observation`, `evidence_refs`) before resuming. A paused
|
|
61
|
+
progress run accepts explicit increases through
|
|
62
|
+
`POST /v1/agentic-loop/:id/continuation-ceilings`; it never resets already used
|
|
63
|
+
tokens or automatically resumes. Both routes require the original authorized
|
|
64
|
+
account, organization and device throughout the request.
|
|
65
|
+
|
|
5
66
|
## Apple Silicon Mac app
|
|
6
67
|
|
|
7
68
|
The macOS installer includes a native status window and menu bar app. Open
|
|
@@ -30,9 +91,20 @@ current analysis finishes and submits its result. The toggle does not grant SQM
|
|
|
30
91
|
subscription or organization permissions. First-time repository history discovery
|
|
31
92
|
still starts from the web workspace.
|
|
32
93
|
|
|
33
|
-
**
|
|
34
|
-
|
|
35
|
-
|
|
94
|
+
**Quality checks** shows local analysis and check history separately from ordinary
|
|
95
|
+
**Tasks**. Recent records survive restart and are scoped to the Web environment,
|
|
96
|
+
account, and organization. Failed checks retain safe diagnostic codes, stages,
|
|
97
|
+
HTTP status, and request identifiers when available. The web SQM workspace
|
|
98
|
+
retains the authoritative analysis history. Persistent provider login, model or
|
|
99
|
+
access errors pause new local claims with a visible recovery hint; refreshing
|
|
100
|
+
after reconnecting permits another probe, with bounded automatic cooldowns.
|
|
101
|
+
|
|
102
|
+
Detached daemon and desktop-host diagnostics are saved to
|
|
103
|
+
`$SIMY_HOME/logs/daemon.log` and `$SIMY_HOME/logs/desktop-host.log`
|
|
104
|
+
(`SIMY_HOME` defaults to `~/.simy`). Each log is limited to 1 MiB plus two
|
|
105
|
+
backups; credentials, Cookie headers and URL queries are redacted. Installer
|
|
106
|
+
handoff diagnostics remain in `~/.simy/installer/desktop-update.log`.
|
|
107
|
+
`simy --version` or `simy -v` prints the package version without starting work.
|
|
36
108
|
|
|
37
109
|
Closing the window keeps the agent running. Open the app again or choose
|
|
38
110
|
**Show SIMY** from its menu bar icon to return. **Quit SIMY** asks before
|
|
@@ -187,6 +259,29 @@ that task.
|
|
|
187
259
|
|
|
188
260
|
## Local Tasks
|
|
189
261
|
|
|
262
|
+
Purpose runs may opt into independent local evidence by freezing user-approved
|
|
263
|
+
`verification_recipes` in the server claim. `capabilities.purpose_verification`
|
|
264
|
+
advertises the device's Ed25519 public key; the server pins that key for the
|
|
265
|
+
attempt. Older clients without this capability cannot supply this evidence.
|
|
266
|
+
`POST /v1/local-tasks/:id/verify-purpose` with `{}` checks the original plan only
|
|
267
|
+
after the task succeeds. GET only reads the cached `task.host_verification`.
|
|
268
|
+
File recipes independently check bounded regular files and optional JSON format;
|
|
269
|
+
command recipes execute approved structured argv for Node tests or package
|
|
270
|
+
`test`, `build`, `check`, and `lint` scripts. Commands require write permission,
|
|
271
|
+
share a five-minute deadline, and retain only output hashes and exit status.
|
|
272
|
+
Optional `protected_paths` bind check definitions to their pre-execution hashes.
|
|
273
|
+
|
|
274
|
+
A receipt binds the actor, device, attempt, objective, recipe plan, local task,
|
|
275
|
+
and before/after repository identity. Changed repositories, stopped or uncertain
|
|
276
|
+
tasks, unknown prior check effects, and changed keys fail closed. Fresh POSTs are
|
|
277
|
+
idempotent; an expired confirmed receipt may be refreshed explicitly on the same
|
|
278
|
+
task, with at most three check rounds and no new Provider invocation. Checks
|
|
279
|
+
never upload source files or raw command output. Exit zero proves the specified
|
|
280
|
+
command's result; it does not prove product acceptance or that a package script
|
|
281
|
+
has no external effects. This host evidence boundary does not isolate a malicious
|
|
282
|
+
process running as the same OS user.
|
|
283
|
+
|
|
284
|
+
|
|
190
285
|
CLI API contract v9 promotes non-coding work to a first-class **Local Task**
|
|
191
286
|
runtime. A Local Task is separate from Agentic Loop: it runs one bounded Codex
|
|
192
287
|
or Claude Code session without Git branches, pull requests, audits, or automatic
|
|
@@ -411,6 +506,9 @@ SQM deliberately has two identity-bound stages:
|
|
|
411
506
|
The full-check summary reports applicable modules, evaluated rules, scenario
|
|
412
507
|
statuses and blocking/advisory scope. `passed` with `no_applicable_rules` or zero
|
|
413
508
|
scenario evaluations does not establish feature or historical incident coverage.
|
|
509
|
+
An empty system/repository bundle adds `no_system_knowledge` and an explicit
|
|
510
|
+
zero-case warning; knowledge that exists but does not match this change uses a
|
|
511
|
+
separate warning. Both retain the existing `passed` outcome.
|
|
414
512
|
Scenario statuses come from deterministic rule evaluation, not browser/provider
|
|
415
513
|
execution. Keep concrete regression and delivery acceptance evidence separately.
|
|
416
514
|
If knowledge is unavailable, the command returns exit 2 with the service cause
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.73",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
|
|
43
43
|
"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",
|
|
44
44
|
"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",
|
|
45
|
-
"check:syntax": "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/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",
|
|
45
|
+
"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/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",
|
|
46
46
|
"check:package": "node ./scripts/check-package-contents.js",
|
|
47
47
|
"build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
|
|
48
48
|
"build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
|
package/src/agent.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { normalizePurposeVerification, assertPurposeActor, preparePurposeVerification, verifyPurposeLocalTask, purposeSigningKeyPath, purposeVerificationCapability, purposeWorkspaceBusy } from "./purpose-local-proof.js";
|
|
2
|
+
import { startupRequirementChecks, summarizeStartupChecks } from "./startup-preflight.js";
|
|
1
3
|
import { BROWSER_HANDOFF_TRANSPORT, browserHandoffPage, decryptBrowserHandoff } from "./browser-handoff.js";
|
|
2
4
|
import { createAuthorizationWindow } from "./browser-authorization.js";
|
|
3
5
|
import { createSqmHistoryStore, projectSqmHistory, sqmHistoryScope } from "./sqm/history-store.js";
|
|
4
6
|
import { createDesktopAccessPolicy, desktopAccessReady, requireDesktopAccess } from './desktop-access.js';
|
|
5
|
-
import { readSqmEnabled, writeSqmEnabled } from "./desktop-sqm-settings.js";
|
|
7
|
+
import { readSqmEnabled, writeSqmEnabled, readSqmSettings, writeSqmEngineFallback } from "./desktop-sqm-settings.js";
|
|
6
8
|
import { createServer } from "node:http";
|
|
7
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
8
10
|
import { createReadStream } from "node:fs";
|
|
@@ -14,10 +16,13 @@ import {
|
|
|
14
16
|
continueLocalCodingRunAfterRepositoryApproval,
|
|
15
17
|
createRun,
|
|
16
18
|
increaseLocalCodingRunProviderTokenBudget,
|
|
19
|
+
extendLocalCodingRunCeilings,
|
|
17
20
|
LocalRunRegistry,
|
|
18
21
|
markCodingRunMonitorFailure,
|
|
19
22
|
isLocalRepositoryApprovalPending,
|
|
20
23
|
pauseLocalCodingRun,
|
|
24
|
+
prepareLocalCodingRunRecovery,
|
|
25
|
+
reconcileLocalCodingRun,
|
|
21
26
|
queueLocalCodingGuidance,
|
|
22
27
|
recheckLocalCodingRun,
|
|
23
28
|
publishRestoredLocalCodingRun,
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
toCompatibleCodingLoopState,
|
|
31
36
|
toLedgerSnapshot,
|
|
32
37
|
} from "./runner.js";
|
|
38
|
+
import { assertRecoveryActor } from "./coding-run-recovery.js";
|
|
33
39
|
import { codingRunsRoot, loadCodingRunSnapshots, saveCodingRunSnapshot } from "./coding-run-store.js";
|
|
34
40
|
import {
|
|
35
41
|
defaultSessionRoot,
|
|
@@ -196,7 +202,15 @@ export async function startAgent({
|
|
|
196
202
|
// as its device (see /v1/auth/complete), so keep it after it is cleared.
|
|
197
203
|
let reclaimableSession = null;
|
|
198
204
|
if (session && sessionRequiresWebAuthorization(session, apiOrigin)) session = null;
|
|
199
|
-
|
|
205
|
+
const purposeKeyPath = dependencies.purposeSigningKeyPath || purposeSigningKeyPath(sessionRoot);
|
|
206
|
+
let purposeSigner = null;
|
|
207
|
+
const refreshPurposeSigner = async () => {
|
|
208
|
+
try { purposeSigner = await purposeVerificationCapability(purposeKeyPath, { create: true }); }
|
|
209
|
+
catch { purposeSigner = null; } // Invalid/private-key state fails closed without blocking ordinary tasks.
|
|
210
|
+
};
|
|
211
|
+
await refreshPurposeSigner();
|
|
212
|
+
let sqmEngineSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
213
|
+
let sqmEnabled = sqmEngineSettings.enabled;
|
|
200
214
|
let organizationItems = [];
|
|
201
215
|
let organizationState = "unknown";
|
|
202
216
|
let organizationRefresh = null;
|
|
@@ -246,11 +260,13 @@ export async function startAgent({
|
|
|
246
260
|
);
|
|
247
261
|
let availableCapabilities;
|
|
248
262
|
let providerStatuses = { codex: { state: "unknown" }, claude: { state: "unknown" } };
|
|
263
|
+
const providerRecoveryRevisions = { codex: 0, claude: 0 };
|
|
249
264
|
const refreshAvailableCapabilities = () => {
|
|
250
265
|
const features = contractSourceCapabilities.features;
|
|
251
266
|
availableCapabilities = withCliContract({
|
|
252
267
|
...contractSourceCapabilities,
|
|
253
|
-
...(
|
|
268
|
+
...(purposeSigner ? { purpose_verification: purposeSigner } : {}),
|
|
269
|
+
...(features ? { features: { ...features, purpose_host_verification: Boolean(purposeSigner) && isSessionValid(session, Date.now(), apiOrigin), ...(Object.hasOwn(features, "sqm_local_review")
|
|
254
270
|
? { sqm_local_review: features.sqm_local_review === true && sqmEnabled && !pendingOrganization && desktopWorkAllowed() } : {}) } } : {}),
|
|
255
271
|
...(providerStatuses ? { provider_statuses: providerStatuses } : {}),
|
|
256
272
|
}, installMode, {
|
|
@@ -423,6 +439,7 @@ export async function startAgent({
|
|
|
423
439
|
providerStatuses = { ...providerStatuses, [provider]: next };
|
|
424
440
|
refreshAvailableCapabilities();
|
|
425
441
|
if (previous[provider]?.version !== next.version || previous[provider]?.state !== next.state) {
|
|
442
|
+
providerRecoveryRevisions[provider] += 1;
|
|
426
443
|
notifyDesktopStatusChange();
|
|
427
444
|
}
|
|
428
445
|
}));
|
|
@@ -574,6 +591,9 @@ export async function startAgent({
|
|
|
574
591
|
const localRun = registry.get(snapshot.id);
|
|
575
592
|
localRun.session = session;
|
|
576
593
|
localRun.apiOrigin = apiOrigin;
|
|
594
|
+
// A newer ledger is not permission to replace an active local operation.
|
|
595
|
+
// Keep the owner, child, checkpoint and approval state until it settles.
|
|
596
|
+
if (localRun.operation || localRun.child) continue;
|
|
577
597
|
const remoteTime = Date.parse(snapshot.updated_at || "") || 0;
|
|
578
598
|
const localTime = Date.parse(localRun.snapshot.updated_at || "") || 0;
|
|
579
599
|
if (remoteTime > localTime) {
|
|
@@ -872,17 +892,40 @@ export async function startAgent({
|
|
|
872
892
|
sqmLocalReviewWorker = createWorker({
|
|
873
893
|
api: sqmApi,
|
|
874
894
|
beginWork: () => acquireUpdateWork(updateManager),
|
|
875
|
-
runReview: async (request, provider) => {
|
|
895
|
+
runReview: async (request, provider, context = {}) => {
|
|
876
896
|
const scope = await authorizeDesktopWork();
|
|
877
|
-
const
|
|
878
|
-
|
|
897
|
+
const actor = JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]);
|
|
898
|
+
const settings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
899
|
+
const engineSettings = JSON.stringify([settings.enable_engine_fallback, settings.allowed_fallback_providers]);
|
|
900
|
+
const beforeDispatch = async () => {
|
|
901
|
+
await authorizeDesktopWork(scope);
|
|
902
|
+
const current = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
903
|
+
if (actor !== JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]) ||
|
|
904
|
+
engineSettings !== JSON.stringify([current.enable_engine_fallback, current.allowed_fallback_providers])) {
|
|
905
|
+
throw Object.assign(new Error("SQM actor or fallback settings changed."), { code: "desktop_access_required" });
|
|
906
|
+
}
|
|
907
|
+
await context.beforeLeaseDispatch?.();
|
|
908
|
+
await authorizeDesktopWork(scope);
|
|
909
|
+
const finalSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
910
|
+
if (actor !== JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]) ||
|
|
911
|
+
engineSettings !== JSON.stringify([finalSettings.enable_engine_fallback, finalSettings.allowed_fallback_providers])) {
|
|
912
|
+
throw Object.assign(new Error("SQM actor or fallback settings changed."), { code: "desktop_access_required" });
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
return (dependencies.runSqmLocalReview || ((value, selected, options) => runStructuredProviderReview(selected, value, options)))(request, provider, {
|
|
916
|
+
beforeDispatch, readOnlyAnalysis: true, enableEngineFallback: settings.enable_engine_fallback,
|
|
917
|
+
allowedFallbackProviders: settings.allowed_fallback_providers, providerReportingSupported: context.providerReportingSupported === true,
|
|
918
|
+
});
|
|
879
919
|
},
|
|
920
|
+
getEnvironmentKey: () => JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id,
|
|
921
|
+
session?.expires_at, sqmEnabled, sqmEngineSettings, ["codex", "claude"].map(provider => [providerStatuses[provider]?.state, providerStatuses[provider]?.version])]),
|
|
880
922
|
onChange: notifyDesktopStatusChange,
|
|
881
923
|
getWorkspaceUrl: () => {
|
|
882
924
|
const slug = desktopAccess.snapshot().organizationSlug;
|
|
883
925
|
return slug ? new URL(`/org/${encodeURIComponent(slug)}/quality-monitor/`, apiOrigin).toString() : null;
|
|
884
926
|
},
|
|
885
927
|
getHistoryScope: () => sqmHistoryScope(apiOrigin, session),
|
|
928
|
+
getRecoveryKey: (provider) => providerRecoveryRevisions[provider] ?? 0,
|
|
886
929
|
recordHistory: (scope, item) => sqmHistory.record(scope, item),
|
|
887
930
|
onHistoryError: () => { if (!quiet) console.error("Could not save the local SQM analysis history."); },
|
|
888
931
|
getProvider: () => !desktopWorkAllowed() || availableCapabilities?.features?.sqm_local_review !== true ? null : ["codex", "claude"].find(
|
|
@@ -1161,6 +1204,7 @@ export async function startAgent({
|
|
|
1161
1204
|
: indexedRepository?.local_path || null,
|
|
1162
1205
|
dependencies,
|
|
1163
1206
|
providerStatus: (await readProviderStatuses())[backend],
|
|
1207
|
+
request: body,
|
|
1164
1208
|
});
|
|
1165
1209
|
json(res, 200, environment);
|
|
1166
1210
|
return;
|
|
@@ -1268,7 +1312,8 @@ export async function startAgent({
|
|
|
1268
1312
|
authNonce = randomBytes(16).toString("base64url");
|
|
1269
1313
|
pendingOrganization = null;
|
|
1270
1314
|
desktopLoginPending = false;
|
|
1271
|
-
|
|
1315
|
+
sqmEngineSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
1316
|
+
sqmEnabled = sqmEngineSettings.enabled;
|
|
1272
1317
|
if (changingScope) sqmLocalReviewWorker?.resetHistory?.();
|
|
1273
1318
|
organizationState = "unknown";
|
|
1274
1319
|
organizationItems = [];
|
|
@@ -1318,6 +1363,35 @@ export async function startAgent({
|
|
|
1318
1363
|
.pipe(res);
|
|
1319
1364
|
return;
|
|
1320
1365
|
}
|
|
1366
|
+
const purposeVerifyMatch = url.pathname.match(/^\/v1\/local-tasks\/([^/]+)\/verify-purpose$/);
|
|
1367
|
+
if (req.method === "POST" && purposeVerifyMatch) {
|
|
1368
|
+
const actorScope = desktopScopeRevision();
|
|
1369
|
+
const actorSession = session;
|
|
1370
|
+
const task = localTaskRegistry.get(decodeURIComponent(purposeVerifyMatch[1]));
|
|
1371
|
+
if (!task) { json(res, 404, { error: "Local Task not found" }); return; }
|
|
1372
|
+
const body = await readBoundedJson(req, 1024).catch(() => { throw Object.assign(new Error("Purpose verification body must be an empty JSON object."), { code: "purpose_verification_plan_invalid", status: 400 }); });
|
|
1373
|
+
if (!body || Array.isArray(body) || typeof body !== "object" || Object.keys(body).length) {
|
|
1374
|
+
json(res, 400, { error: "Verification uses only the frozen task plan.", code: "purpose_verification_plan_invalid" }); return;
|
|
1375
|
+
}
|
|
1376
|
+
const selected = await ensureRepositoryIndexed(task.repository);
|
|
1377
|
+
const approvedInventoryPath = findRepository(repositoryInventory, task.repository)?.local_path;
|
|
1378
|
+
const authorize = async () => {
|
|
1379
|
+
if (actorScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin) || shuttingDown ||
|
|
1380
|
+
selected?.local_path !== task.workspace_path || findRepository(repositoryInventory, task.repository)?.local_path !== approvedInventoryPath ||
|
|
1381
|
+
registry.list().some(run => run.request?.repository === task.repository && (run.operation || run.child || ["queued", "dispatching", "coding"].includes(run.status))) ||
|
|
1382
|
+
localTaskRegistry.list().some(other => other !== task && other.workspace_path === task.workspace_path && ["queued", "running", "stopping"].includes(other.status))) {
|
|
1383
|
+
throw Object.assign(new Error("Purpose verification scope or workspace changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1384
|
+
}
|
|
1385
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
1386
|
+
if (actorScope !== desktopScopeRevision()) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1387
|
+
};
|
|
1388
|
+
await authorize();
|
|
1389
|
+
await withUpdateWork(updateManager, async () => verifyPurposeLocalTask(task, {
|
|
1390
|
+
session: actorSession, apiOrigin, keyPath: purposeKeyPath, authorize, persist: persistLocalTask,
|
|
1391
|
+
}));
|
|
1392
|
+
json(res, 200, { ok: true, task: localTaskSnapshot(task) });
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1321
1395
|
const localTaskStreamMatch = url.pathname.match(/^\/v1\/local-tasks\/([^/]+)\/stream$/);
|
|
1322
1396
|
if (req.method === "GET" && localTaskStreamMatch) {
|
|
1323
1397
|
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
@@ -1388,7 +1462,13 @@ export async function startAgent({
|
|
|
1388
1462
|
json(res, 401, { error: "simy session expired; run simy again" });
|
|
1389
1463
|
return;
|
|
1390
1464
|
}
|
|
1465
|
+
const purposeStartScope = desktopScopeRevision();
|
|
1391
1466
|
const { body, attachments } = await readCodingLoopStart(req);
|
|
1467
|
+
const purposeVerification = normalizePurposeVerification(body.purpose_verification);
|
|
1468
|
+
if (purposeVerification) {
|
|
1469
|
+
assertPurposeActor(purposeVerification, session, apiOrigin, null, String(req.headers["idempotency-key"] || body.client_request_id || "").trim());
|
|
1470
|
+
if (purposeStartScope !== desktopScopeRevision() || !purposeSigner || purposeSigner.key_id !== purposeVerification.key_id) throw Object.assign(new Error("Purpose verification signer or actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1471
|
+
}
|
|
1392
1472
|
const instruction = String(body.instruction || "").trim();
|
|
1393
1473
|
if (!instruction) {
|
|
1394
1474
|
json(res, 400, { error: "instruction is required" });
|
|
@@ -1507,11 +1587,14 @@ export async function startAgent({
|
|
|
1507
1587
|
repository,
|
|
1508
1588
|
expectedOutputs: body.expected_outputs,
|
|
1509
1589
|
providerTokenBudget: body.provider_token_budget,
|
|
1590
|
+
continuationPolicy: body.continuation_policy,
|
|
1591
|
+
providerTokenCeiling: body.provider_token_ceiling,
|
|
1510
1592
|
timeoutMs: body.timeout_ms,
|
|
1511
1593
|
attachmentManifest: preparedAttachments,
|
|
1512
1594
|
scenarioPack,
|
|
1513
1595
|
workflowObservationBinding,
|
|
1514
1596
|
workflowStartIntent,
|
|
1597
|
+
purposeVerification,
|
|
1515
1598
|
});
|
|
1516
1599
|
hashLocalTaskIdempotencyKey(idempotencyKey);
|
|
1517
1600
|
} catch (error) {
|
|
@@ -1633,20 +1716,36 @@ export async function startAgent({
|
|
|
1633
1716
|
expectedOutputs: body.expected_outputs,
|
|
1634
1717
|
scenarioPack,
|
|
1635
1718
|
providerTokenBudget: body.provider_token_budget,
|
|
1719
|
+
continuationPolicy: body.continuation_policy,
|
|
1720
|
+
providerTokenCeiling: body.provider_token_ceiling,
|
|
1636
1721
|
timeoutMs: body.timeout_ms,
|
|
1637
1722
|
idempotencyKeyHash: hashLocalTaskIdempotencyKey(idempotencyKey),
|
|
1638
1723
|
requestFingerprint,
|
|
1639
1724
|
workflowObservationBinding,
|
|
1640
1725
|
workflowStartIntent,
|
|
1726
|
+
purposeVerification,
|
|
1641
1727
|
});
|
|
1728
|
+
if (purposeVerification) await preparePurposeVerification(task, { session, apiOrigin, clientRequestId: idempotencyKey, keyPath: purposeKeyPath,
|
|
1729
|
+
authorize: async () => {
|
|
1730
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
1731
|
+
if (purposeStartScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1732
|
+
} });
|
|
1642
1733
|
await persistLocalTask(task);
|
|
1643
1734
|
localTaskRegistry.create(task);
|
|
1644
1735
|
requestClaim.complete(task);
|
|
1645
1736
|
requestClaimCompleted = true;
|
|
1646
1737
|
const runLocalTask =
|
|
1647
1738
|
dependencies.runLocalTask ||
|
|
1648
|
-
((localTask) => startLocalTask(localTask, { session, apiOrigin
|
|
1739
|
+
((localTask) => startLocalTask(localTask, { session, apiOrigin, beforeSpawn: async () => {
|
|
1740
|
+
if (purposeWorkspaceBusy(localTask.workspace_path)) throw Object.assign(new Error("Independent verification owns this workspace."), { code: "purpose_verification_workspace_busy" });
|
|
1741
|
+
if (purposeVerification) {
|
|
1742
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
1743
|
+
assertPurposeActor(purposeVerification, session, apiOrigin, localTask, idempotencyKey);
|
|
1744
|
+
if (purposeStartScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed" });
|
|
1745
|
+
}
|
|
1746
|
+
} }));
|
|
1649
1747
|
await authorizeDesktopWork(requestAccessScope);
|
|
1748
|
+
if (purposeVerification && (purposeStartScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin))) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1650
1749
|
task.operation_promise = Promise.resolve(runLocalTask(task))
|
|
1651
1750
|
.catch(async (error) => {
|
|
1652
1751
|
task.status = "failed";
|
|
@@ -1914,6 +2013,10 @@ export async function startAgent({
|
|
|
1914
2013
|
: indexedRepository?.local_path || null,
|
|
1915
2014
|
base_branch: typeof body.base_branch === "string" ? body.base_branch : "dev",
|
|
1916
2015
|
max_attempts: body.max_attempts,
|
|
2016
|
+
continuation_policy: body.continuation_policy,
|
|
2017
|
+
provider_token_ceiling: body.provider_token_ceiling,
|
|
2018
|
+
attempt_ceiling: body.attempt_ceiling,
|
|
2019
|
+
max_no_progress_attempts: body.max_no_progress_attempts,
|
|
1917
2020
|
retry_budget: body.retry_budget,
|
|
1918
2021
|
token_budget: body.provider_token_budget ?? body.token_budget,
|
|
1919
2022
|
ui_evidence_root:
|
|
@@ -2008,6 +2111,66 @@ export async function startAgent({
|
|
|
2008
2111
|
return;
|
|
2009
2112
|
}
|
|
2010
2113
|
|
|
2114
|
+
const recoveryMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/recovery$/);
|
|
2115
|
+
if (recoveryMatch && ["GET", "POST"].includes(req.method)) {
|
|
2116
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2117
|
+
json(res, 401, { error: "Restore the authorized SIMY session before recovering this run." }); return;
|
|
2118
|
+
}
|
|
2119
|
+
const run = registry.get(decodeURIComponent(recoveryMatch[1]));
|
|
2120
|
+
if (!run) { json(res, 404, { error: "run not found" }); return; }
|
|
2121
|
+
try {
|
|
2122
|
+
const recoveryScope = desktopScopeRevision();
|
|
2123
|
+
const authorizeRecovery = async () => {
|
|
2124
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
2125
|
+
if (recoveryScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2126
|
+
throw Object.assign(new Error('The authorized desktop session changed.'), { code: 'desktop_access_required' });
|
|
2127
|
+
}
|
|
2128
|
+
assertRecoveryActor({ ...run, session, apiOrigin });
|
|
2129
|
+
};
|
|
2130
|
+
assertRecoveryActor({ ...run, session, apiOrigin });
|
|
2131
|
+
if (req.method === "GET") {
|
|
2132
|
+
const recovery = await prepareLocalCodingRunRecovery(run);
|
|
2133
|
+
await authorizeRecovery();
|
|
2134
|
+
json(res, 200, { recovery });
|
|
2135
|
+
} else {
|
|
2136
|
+
const body = await readJson(req);
|
|
2137
|
+
await authorizeRecovery();
|
|
2138
|
+
if (body.action !== "reconcile") { json(res, 400, { error: "Choose reconcile after inspecting the interrupted operation." }); return; }
|
|
2139
|
+
const recovery = await reconcileLocalCodingRun(run, {
|
|
2140
|
+
operationStatus: body.operation_status, observation: body.observation, evidenceRefs: body.evidence_refs, authorize: authorizeRecovery,
|
|
2141
|
+
});
|
|
2142
|
+
json(res, 200, { recovery, run_id: run.id, resumed: false });
|
|
2143
|
+
}
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
json(res, 409, { error: error instanceof Error ? error.message : "Recovery could not be verified.", code: error.code || "recovery_rejected" });
|
|
2146
|
+
}
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const ceilingMatch = agenticLoopPath.match(/^\/v1\/agentic-loop\/([^/]+)\/continuation-ceilings$/);
|
|
2151
|
+
if (req.method === "POST" && ceilingMatch) {
|
|
2152
|
+
if (!isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2153
|
+
json(res, 401, { error: "Restore the authorized SIMY session before extending this run." }); return;
|
|
2154
|
+
}
|
|
2155
|
+
const run = registry.get(decodeURIComponent(ceilingMatch[1]));
|
|
2156
|
+
if (!run) { json(res, 404, { error: "run not found" }); return; }
|
|
2157
|
+
try {
|
|
2158
|
+
const recoveryScope = desktopScopeRevision();
|
|
2159
|
+
assertRecoveryActor({ ...run, session, apiOrigin });
|
|
2160
|
+
const body = await readJson(req);
|
|
2161
|
+
await authorizeDesktopWork(requestAccessScope);
|
|
2162
|
+
if (recoveryScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2163
|
+
throw Object.assign(new Error('The authorized desktop session changed.'), { code: 'desktop_access_required' });
|
|
2164
|
+
}
|
|
2165
|
+
assertRecoveryActor({ ...run, session, apiOrigin });
|
|
2166
|
+
const budget = await extendLocalCodingRunCeilings(run, body);
|
|
2167
|
+
json(res, 200, { run_id: run.id, budget, resumed: false });
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
json(res, 409, { error: error instanceof Error ? error.message : "Continuation ceilings could not be extended.", code: error.code || "continuation_ceiling_rejected" });
|
|
2170
|
+
}
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2011
2174
|
const providerBudgetMatch = agenticLoopPath.match(
|
|
2012
2175
|
/^\/v1\/agentic-loop\/([^/]+)\/provider-token-budget$/,
|
|
2013
2176
|
);
|
|
@@ -2210,6 +2373,7 @@ export async function startAgent({
|
|
|
2210
2373
|
json(res, 400, localTaskFileErrorPayload(err));
|
|
2211
2374
|
return;
|
|
2212
2375
|
}
|
|
2376
|
+
if (String(err?.code || "").startsWith("purpose_verification_")) { json(res, err.status || 409, { error: err.message, code: err.code }); return; }
|
|
2213
2377
|
json(res, 500, { error: err instanceof Error ? err.message : "internal error" });
|
|
2214
2378
|
}
|
|
2215
2379
|
});
|
|
@@ -2327,9 +2491,12 @@ export async function startAgent({
|
|
|
2327
2491
|
tasks: desktopTaskStatus([registry, directRegistry, localTaskRegistry]),
|
|
2328
2492
|
organizations: { state: organizationState, items: organizationItems, selectedId: session?.organization_id ?? null,
|
|
2329
2493
|
switching: Boolean(pendingOrganization), canSwitch: auth.state === "signed_in" && !organizationBusy() },
|
|
2330
|
-
sqm: { enabled: sqmEnabled,
|
|
2494
|
+
sqm: { enabled: sqmEnabled, engineFallbackEnabled: sqmEngineSettings.enable_engine_fallback,
|
|
2495
|
+
allowedFallbackProviders: sqmEngineSettings.allowed_fallback_providers,
|
|
2496
|
+
paused: sqmLocalReviewWorker?.snapshot?.().paused === true, pauseReason: sqmLocalReviewWorker?.snapshot?.().pause_reason ?? null, canEnable: auth.state === "signed_in" && Boolean(session?.account_id && session?.organization_id) && !pendingOrganization && desktopWorkAllowed(),
|
|
2331
2497
|
activeJobId: sqmLocalReviewWorker?.snapshot?.().active_job_id ?? null,
|
|
2332
2498
|
issue: sqmLocalReviewWorker?.snapshot?.().issue ?? null,
|
|
2499
|
+
providerPause: sqmLocalReviewWorker?.snapshot?.().provider_pause ?? null,
|
|
2333
2500
|
providerReady: [codex, claude].some(provider => provider?.state === "ready") },
|
|
2334
2501
|
...projectSqmHistory(await historyForCurrentScope(), sqmLocalReviewWorker?.desktopTasks?.()),
|
|
2335
2502
|
update: updateManager?.snapshot?.() ?? null,
|
|
@@ -2378,10 +2545,22 @@ export async function startAgent({
|
|
|
2378
2545
|
await writeSqmEnabled(sessionRoot, apiOrigin, settingsSession, enabled);
|
|
2379
2546
|
if (session !== settingsSession) return;
|
|
2380
2547
|
sqmEnabled = enabled;
|
|
2548
|
+
sqmEngineSettings = { ...sqmEngineSettings, enabled };
|
|
2549
|
+
sqmLocalReviewWorker?.retry?.();
|
|
2381
2550
|
refreshAvailableCapabilities();
|
|
2382
2551
|
notifyDesktopStatusChange();
|
|
2383
2552
|
void heartbeat().catch(error => reportHeartbeatError(error, quiet));
|
|
2384
2553
|
},
|
|
2554
|
+
async setSqmEngineFallback({ enabled, allowedProviders }) {
|
|
2555
|
+
await authorizeDesktopWork();
|
|
2556
|
+
if (!isSessionValid(session, Date.now(), apiOrigin) || pendingOrganization) throw new Error("Sign in to an organization first.");
|
|
2557
|
+
const settingsSession = session;
|
|
2558
|
+
await writeSqmEngineFallback(sessionRoot, apiOrigin, settingsSession, { enabled, allowedProviders });
|
|
2559
|
+
if (session !== settingsSession) return;
|
|
2560
|
+
sqmEngineSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
2561
|
+
sqmLocalReviewWorker?.retry?.(); notifyDesktopStatusChange();
|
|
2562
|
+
},
|
|
2563
|
+
async retrySqmEnvironment() { await authorizeDesktopWork(); sqmLocalReviewWorker?.retry?.(); notifyDesktopStatusChange(); },
|
|
2385
2564
|
getDesktopStatus,
|
|
2386
2565
|
getDesktopTask: async (key) => {
|
|
2387
2566
|
if (key?.startsWith("sqm:") || key?.startsWith("sqm-check:")) {
|
|
@@ -2400,7 +2579,16 @@ export async function startAgent({
|
|
|
2400
2579
|
return getDesktopStatus();
|
|
2401
2580
|
},
|
|
2402
2581
|
refreshCodexStatus: () => codexStatus({ force: true }),
|
|
2403
|
-
refreshProviderStatus: () =>
|
|
2582
|
+
refreshProviderStatus: async () => {
|
|
2583
|
+
const statuses = await readProviderStatuses({ force: true });
|
|
2584
|
+
// Explicit reconnect/refresh permits one recovery attempt. Routine status
|
|
2585
|
+
// polling must not defeat the worker's persistent-failure cooldown.
|
|
2586
|
+
for (const provider of ["codex", "claude"]) {
|
|
2587
|
+
if (statuses[provider]?.state === "ready") providerRecoveryRevisions[provider] += 1;
|
|
2588
|
+
}
|
|
2589
|
+
notifyDesktopStatusChange();
|
|
2590
|
+
return statuses;
|
|
2591
|
+
},
|
|
2404
2592
|
workspace,
|
|
2405
2593
|
repositoryScanRoot,
|
|
2406
2594
|
get capabilities() {
|
|
@@ -2818,6 +3006,7 @@ function capabilitiesFromInspections(codex, claude) {
|
|
|
2818
3006
|
local_task_streaming: true,
|
|
2819
3007
|
local_task_idempotency: true,
|
|
2820
3008
|
local_task_model: true,
|
|
3009
|
+
progress_continuation_policy: true,
|
|
2821
3010
|
local_task_reconciliation: true,
|
|
2822
3011
|
local_task_control_plane: true,
|
|
2823
3012
|
pipeline_local_task_artifacts: true,
|
|
@@ -2849,6 +3038,7 @@ async function inspectCodingLoopEnvironment({
|
|
|
2849
3038
|
localPath,
|
|
2850
3039
|
dependencies,
|
|
2851
3040
|
providerStatus,
|
|
3041
|
+
request = {},
|
|
2852
3042
|
}) {
|
|
2853
3043
|
const checks = [
|
|
2854
3044
|
{
|
|
@@ -2909,13 +3099,16 @@ async function inspectCodingLoopEnvironment({
|
|
|
2909
3099
|
if (inspection.compatible) {
|
|
2910
3100
|
checks.push({
|
|
2911
3101
|
key: "provider_auth",
|
|
2912
|
-
status: authFailure ? "failed" : "passed",
|
|
3102
|
+
status: authFailure ? (providerStatus?.state === "signed_out" ? "failed" : "unknown") : "passed",
|
|
2913
3103
|
summary: authFailure?.summary || `${backend === "claude" ? "Claude Code" : "Codex"} is signed in.`,
|
|
2914
3104
|
...(authFailure ? { recovery: authFailure.recovery } : {}),
|
|
2915
3105
|
});
|
|
2916
3106
|
}
|
|
3107
|
+
checks.push(...startupRequirementChecks(request));
|
|
3108
|
+
const startup = summarizeStartupChecks(checks);
|
|
2917
3109
|
return {
|
|
2918
|
-
ready: Boolean(repositoryPath && inspection.compatible && !authFailure),
|
|
3110
|
+
ready: Boolean(repositoryPath && inspection.compatible && !authFailure && startup.blocker_keys.length === 0),
|
|
3111
|
+
startup,
|
|
2919
3112
|
repository,
|
|
2920
3113
|
backend,
|
|
2921
3114
|
execution_target: executionTarget,
|