@awak-app/simy-cli 0.7.15 → 0.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +225 -98
  2. package/package.json +5 -3
  3. package/src/agent.js +129 -7
  4. package/src/backend-executable.js +7 -6
  5. package/src/bounded-local-task-subtask-pool.js +32 -6
  6. package/src/bounded-local-task-subtasks-contract.js +1 -1
  7. package/src/cli-contract.js +1 -1
  8. package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
  9. package/src/codex-plugins/simy/hooks/README.md +8 -3
  10. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +302 -78
  11. package/src/codex-plugins/simy/hooks/hooks.json +1 -1
  12. package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
  13. package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
  14. package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
  15. package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
  16. package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
  17. package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
  18. package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
  19. package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
  20. package/src/codex-plugins/simy/hooks/outcome-progress.mjs +16 -5
  21. package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
  22. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +12 -11
  23. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +28 -38
  24. package/src/codex-plugins/simy/hooks/purpose-rule-policy.mjs +1 -2
  25. package/src/codex-plugins/simy/hooks/purpose-rule-review.mjs +5 -63
  26. package/src/codex-plugins/simy/hooks/purpose-search.mjs +21 -4
  27. package/src/codex-plugins/simy/hooks/result-cycle.mjs +166 -0
  28. package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +98 -12
  29. package/src/codex-plugins/simy/hooks/user-experience.mjs +26 -0
  30. package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
  31. package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
  32. package/src/coding-run-recovery.js +4 -2
  33. package/src/delivery-hooks/claude-workflow.md +10 -7
  34. package/src/delivery-hooks/command.js +76 -10
  35. package/src/delivery-hooks/consistency-monitor.js +20 -4
  36. package/src/delivery-hooks/consult.js +60 -0
  37. package/src/delivery-hooks/diagnosis.js +2 -2
  38. package/src/delivery-hooks/legacy-hook-routing.js +46 -8
  39. package/src/delivery-hooks/local-runtime.js +81 -1
  40. package/src/delivery-hooks/local-supervisor.js +13 -4
  41. package/src/delivery-hooks/repair-continuation.js +80 -0
  42. package/src/delivery-hooks/repair.js +164 -0
  43. package/src/delivery-hooks/supervisor-handover.js +84 -0
  44. package/src/delivery-hooks/supervisor-owner.js +3 -2
  45. package/src/delivery-hooks/supervisor-service.js +38 -6
  46. package/src/delivery-hooks/supervisor-watchdog.js +15 -4
  47. package/src/delivery-hooks/supervisor.js +151 -18
  48. package/src/desktop-access.js +2 -1
  49. package/src/desktop-host.js +35 -2
  50. package/src/durable-local-task-worker.js +372 -58
  51. package/src/execution-capability-contract.js +15 -1
  52. package/src/index.js +3 -3
  53. package/src/local-photos-read.swift +33 -0
  54. package/src/local-photos.js +47 -0
  55. package/src/local-task-attachment-store.js +31 -0
  56. package/src/local-task-destination.js +174 -0
  57. package/src/local-task.js +119 -7
  58. package/src/native-delegation-contract.js +68 -0
  59. package/src/native-provider-identity.js +60 -0
  60. package/src/orchestrator/audit.js +24 -323
  61. package/src/orchestrator/budget.js +22 -11
  62. package/src/orchestrator/charter-validation.js +3 -41
  63. package/src/orchestrator/checklist-continuation.js +2 -14
  64. package/src/orchestrator/completion-contract.js +4 -49
  65. package/src/orchestrator/contract.js +5 -9
  66. package/src/orchestrator/delivery/WORKFLOW.md +30 -710
  67. package/src/orchestrator/delivery/references/acceptance-evidence.md +4 -216
  68. package/src/orchestrator/delivery/references/change-boundary-matrix.md +4 -103
  69. package/src/orchestrator/delivery/references/chrome-user-verification.md +5 -133
  70. package/src/orchestrator/delivery/references/cross-repository-control.md +4 -74
  71. package/src/orchestrator/delivery/references/delivery-levels.md +3 -117
  72. package/src/orchestrator/delivery/references/local-sqm-evidence.md +1 -1
  73. package/src/orchestrator/delivery/references/minimum-completion.md +4 -144
  74. package/src/orchestrator/delivery/references/pr-closure.md +4 -200
  75. package/src/orchestrator/delivery/references/shared-system-specifications.md +4 -161
  76. package/src/orchestrator/delivery/references/state-and-receipts.md +4 -245
  77. package/src/orchestrator/delivery/references/state-model.md +4 -185
  78. package/src/orchestrator/delivery/references/ux-specification.md +4 -95
  79. package/src/orchestrator/delivery-level.js +2 -10
  80. package/src/orchestrator/evidence.js +2 -3
  81. package/src/orchestrator/independent-audit.js +15 -164
  82. package/src/orchestrator/instruction.js +19 -254
  83. package/src/orchestrator/loop.js +35 -95
  84. package/src/orchestrator/outcome-monitor.js +21 -0
  85. package/src/orchestrator/pr-closure.js +2 -16
  86. package/src/orchestrator/recovery.js +32 -2
  87. package/src/orchestrator/ux-specification.js +3 -12
  88. package/src/provider-delegation.js +336 -0
  89. package/src/runner.js +5 -2
  90. package/src/session-supervision/acceptance-reads.js +34 -0
  91. package/src/session-supervision/artifact-observer.js +138 -0
  92. package/src/session-supervision/browser-cdp.js +59 -0
  93. package/src/session-supervision/browser-recovery.js +97 -0
  94. package/src/session-supervision/codex-app-server.js +61 -22
  95. package/src/session-supervision/completion-artifacts.js +44 -0
  96. package/src/session-supervision/consistency-auditor.js +8 -11
  97. package/src/session-supervision/continuation-analysis.js +47 -28
  98. package/src/session-supervision/final-response-recovery.js +5 -0
  99. package/src/session-supervision/index.js +22 -5
  100. package/src/session-supervision/interaction-proxy.js +250 -0
  101. package/src/session-supervision/interaction-task.js +189 -0
  102. package/src/session-supervision/native-interaction.js +102 -0
  103. package/src/session-supervision/provider-failure.js +40 -0
  104. package/src/session-supervision/purpose-instruction.js +14 -14
  105. package/src/session-supervision/purpose-recovery.js +32 -12
  106. package/src/session-supervision/purpose-verification.js +50 -216
  107. package/src/session-supervision/recovery-frontier.js +55 -0
  108. package/src/session-supervision/stop-notifications.js +8 -8
  109. package/src/sqm/agent-skill.js +1 -1
  110. package/src/sqm/checkers.js +12 -6
  111. package/src/sqm/claude-plugin-update.js +22 -8
  112. package/src/sqm/claude-plugin.js +6 -0
  113. package/src/sqm/local-tests.js +2 -1
  114. package/src/sqm/plugin-cache.js +67 -0
  115. package/src/sqm/proof.js +25 -0
  116. package/src/sqm/report.js +1 -0
  117. package/src/sqm/workflow-onboarding.js +3 -39
  118. package/src/sqm/workflow-update.js +17 -6
  119. package/src/startup-preflight.js +2 -20
  120. package/src/team-status-fence-store.js +32 -0
  121. package/src/team-threads.js +457 -0
@@ -2,8 +2,11 @@ import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import { mkdir, lstat, readFile, writeFile } from 'node:fs/promises';
4
4
  import { homedir } from 'node:os';
5
- import { dirname, join } from 'node:path';
5
+ import { dirname, join, isAbsolute, delimiter } from 'node:path';
6
+ import { installPinnedCliEntry } from './legacy-hook-routing.js';
6
7
  import { pinnedWorkflowSource } from './local-runtime.js';
8
+ import { inspectBackendExecutable, backendCandidates, CODEX_WRITER_LOCK_MINIMUM_VERSION } from '../backend-executable.js';
9
+ import { runSceneReasoning } from '../codex-plugins/simy/hooks/scene-reasoning.mjs';
7
10
  import { readSupervisorState } from './supervisor-owner.js';
8
11
 
9
12
  const exec = promisify(execFile);
@@ -12,9 +15,12 @@ const xml = text => String(text).replaceAll('&', '&amp;').replaceAll('<', '&lt;'
12
15
  const systemdArgument = text => `"${String(text).replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('$', () => '$$')}"`;
13
16
  const windowsArgument = text => `"${String(text).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
14
17
 
15
- export function supervisorServiceDefinition({ root, entry, node = process.execPath, platform = process.platform, home = homedir() }) {
18
+ export function supervisorServiceDefinition({ root, entry, node = process.execPath, platform = process.platform, home = homedir(), providerEnvironment = {} }) {
16
19
  if ([root, entry, node].some(value => typeof value !== 'string' || /[\r\n\0]/.test(value))) throw new Error('Invalid supervisor service path.');
17
- const args = [entry, 'delivery', 'runtime', 'supervisor-watch', '--root', root, '--no-auto-update'];
20
+ const environment = Object.fromEntries(Object.entries(providerEnvironment).filter(([key]) => ['PATH', 'SIMY_CODEX_BIN', 'SIMY_CODEX_EXPECTED_VERSION'].includes(key)));
21
+ if (Object.values(environment).some(value => typeof value !== 'string' || /[\r\n\0]/.test(value))) throw new Error('Invalid provider environment.');
22
+ const bootstrap = Object.keys(environment).length ? ['--import', `data:text/javascript,${encodeURIComponent(`Object.assign(process.env,${JSON.stringify(environment)})`)}`] : [];
23
+ const args = [...bootstrap, entry, 'delivery', 'runtime', 'supervisor-watch', '--root', root, '--no-auto-update'];
18
24
  if (platform === 'linux') {
19
25
  const name = 'simy-delivery-supervisor.service';
20
26
  return { name, file: join(home, '.config/systemd/user', name),
@@ -37,17 +43,19 @@ export function supervisorServiceDefinition({ root, entry, node = process.execPa
37
43
  // Explicit service installation only. Installing/updating a runtime alone
38
44
  // never enables this service or enrolls a conversation.
39
45
  export async function installSupervisorService({ root, home = homedir(), platform = process.platform,
40
- run = exec, inspectPin = pinnedWorkflowSource } = {}) {
46
+ run = exec, inspectPin = pinnedWorkflowSource, inspectBackend = inspectBackendExecutable, environment = process.env } = {}) {
41
47
  const control = await readSupervisorState(join(root, 'supervisor-runtime/control.json'));
42
48
  if (control?.version !== 1 || control.enabled !== true) throw new Error('Enable supervisor recovery explicitly before installing its OS service.');
43
49
  await inspectPin({ root });
44
50
  const pin = await readSupervisorState(join(root, 'delivery-runtime/current.json'));
45
51
  if (!pin || pin.digest !== control.digest || pin.release !== control.release) throw new Error('Supervisor activation no longer matches the fixed runtime.');
46
- const definition = supervisorServiceDefinition({ root, home, platform, entry: join(pin.release, 'src/index.js') });
52
+ const provider = await supervisorProviderEnvironment({ home, platform, environment, inspectBackend });
53
+ const definition = supervisorServiceDefinition({ root, home, platform, entry: join(root, 'bin/simy-runtime.mjs'), providerEnvironment: provider.environment });
47
54
  try {
48
55
  const info = await lstat(definition.file);
49
56
  if (!info.isFile() || info.isSymbolicLink() || !(await readFile(definition.file, 'utf8')).includes(marker)) throw new Error('Supervisor service path is not managed by SIMY.');
50
57
  } catch (error) { if (error.code !== 'ENOENT') throw error; }
58
+ await installPinnedCliEntry(root);
51
59
  await mkdir(dirname(definition.file), { recursive: true, mode: 0o700 });
52
60
  await writeFile(definition.file, definition.content, { mode: 0o600 });
53
61
  if (platform === 'linux') {
@@ -61,5 +69,29 @@ export async function installSupervisorService({ root, home = homedir(), platfor
61
69
  await run(scheduler, ['/Create', '/TN', definition.name, '/XML', definition.file, '/F']);
62
70
  await run(scheduler, ['/Run', '/TN', definition.name]);
63
71
  }
64
- return { ...definition, content: undefined, status: 'registered', scope: 'previously_enrolled_tasks_only' };
72
+ return { ...definition, content: undefined, status: 'registered', execution: 'awaiting_service_observation', provider: provider.inspection, scope: 'previously_enrolled_tasks_only' };
73
+ }
74
+
75
+
76
+ // Resolve and re-probe under exactly the environment passed to the service.
77
+ // This is a prerequisite observation, never proof of task execution/acceptance.
78
+ export async function supervisorProviderEnvironment({ home = homedir(), platform = process.platform, environment = process.env,
79
+ inspectBackend = inspectBackendExecutable, onStep } = {}) {
80
+ return runSceneReasoning({ home, platform, environment }, {
81
+ understand_scene: scene => ({ ...scene, candidates: backendCandidates('codex', { home, platform, environment }).filter(value => typeof value === 'string' && isAbsolute(value)) }),
82
+ derive_perspectives: async scene => ({ ...scene, inspection: await inspectBackend('codex', {
83
+ candidates: scene.candidates, minimumVersion: CODEX_WRITER_LOCK_MINIMUM_VERSION, discoveryOptions: { home, platform, environment },
84
+ }) }),
85
+ prioritize_options: scene => {
86
+ if (!scene.inspection.compatible || !isAbsolute(scene.inspection.executable || '')) throw new Error('No compatible absolute Codex executable for the supervisor service.');
87
+ const selected = scene.inspection.executable;
88
+ return { ...scene, selected, environment: { PATH: [dirname(process.execPath), dirname(selected), environment.PATH || environment.Path || ''].join(delimiter), SIMY_CODEX_BIN: selected, SIMY_CODEX_EXPECTED_VERSION: scene.inspection.installed_version } };
89
+ },
90
+ connect_to_next_check: async scene => {
91
+ const verified = await inspectBackend('codex', { candidates: [scene.selected], minimumVersion: CODEX_WRITER_LOCK_MINIMUM_VERSION,
92
+ discoveryOptions: { home, platform, environment: scene.environment }, executionEnvironment: scene.environment });
93
+ if (!verified.compatible || verified.executable !== scene.selected || verified.installed_version !== scene.inspection.installed_version) throw new Error('Supervisor provider changed during service-environment verification.');
94
+ return { environment: scene.environment, inspection: verified };
95
+ },
96
+ }, { onStep });
65
97
  }
@@ -5,6 +5,7 @@ import { setTimeout as delay } from 'node:timers/promises';
5
5
  import { acquireDesktopProcessLock, assertNoDesktopHostForOrigin } from '../desktop-process-lock.js';
6
6
  import { pinnedWorkflowSource } from './local-runtime.js';
7
7
  import { resolveWebOrigin } from '../web-origin.js';
8
+ import { completeSupervisorHandover } from './supervisor-handover.js';
8
9
  import { observeSupervisorOwner, supervisorProcessAlive, supervisorProcessIdentity,
9
10
  readSupervisorState, saveSupervisorState } from './supervisor-owner.js';
10
11
 
@@ -39,17 +40,18 @@ export async function supervisorWatchdogStatus({ root, now = Date.now(), observe
39
40
  export function createSupervisorWatchdog({ root, inspectPin = verifiedPin, readControl = () => readSupervisorState(controlPath(root)),
40
41
  observeOwner = observeSupervisorOwner, checkDesktop = assertNoDesktopHostForOrigin,
41
42
  launch = (bin, args, options) => spawn(bin, args, options), now = Date.now,
43
+ completeHandover = completeSupervisorHandover, watcherNonce,
42
44
  identity = supervisorProcessIdentity, save = state => saveSupervisorState(statePath(root), state) } = {}) {
43
45
  let child = null, childIdentity = null, launchTime = null, closed = false, running = null, lastExit = null;
44
- let pendingPin = null, lastWall = null, controlEpoch;
46
+ let pendingPin = null, lastWall = null, controlEpoch, handoverRetryAt = 0;
45
47
  const clock = () => Math.max(0, now());
46
48
  const tick = async () => {
47
49
  if (closed) return;
48
50
  const at = clock(), control = await readControl();
49
51
  controlEpoch = control?.epoch;
50
52
  const previous = pendingPin || await readSupervisorState(statePath(root));
51
- const state = { version: 1, at, action: 'observe', supervisor: null, lastExit,
52
- lastLaunchAt: launchTime ?? previous?.lastLaunchAt ?? null, epoch: previous?.epoch || null };
53
+ const state = { version: 1, handoverVersion: 1, watcherNonce, at, action: 'observe', supervisor: null, lastExit,
54
+ lastLaunchAt: launchTime ?? previous?.lastLaunchAt ?? null, epoch: control?.epoch || null };
53
55
  if (!control || control.version !== 1 || control.enabled !== true) { state.action = 'disabled'; await save(state); return state; }
54
56
  state.supervisor = await observeOwner(root, { now: at });
55
57
  if (state.supervisor.state === 'running') {
@@ -57,6 +59,15 @@ export function createSupervisorWatchdog({ root, inspectPin = verifiedPin, readC
57
59
  await save(state); return state;
58
60
  }
59
61
  if (!['missing', 'dead'].includes(state.supervisor.state)) { state.action = 'owner_unknown'; await save(state); return state; }
62
+ const retryAt = Math.max(handoverRetryAt, previous?.handoverRetryAt || 0);
63
+ if (retryAt > at) { state.action = 'handover_backoff'; state.handoverRetryAt = retryAt; await save(state); return state; }
64
+ try {
65
+ if (await completeHandover(root)) { state.action = 'handover_activated'; await save(state); return state; }
66
+ } catch (error) {
67
+ state.action = 'handover_wait'; state.reason = error.code === 'process_conflict' ? 'owner_not_released' : 'activation_failed';
68
+ handoverRetryAt = state.handoverRetryAt = at + 30_000;
69
+ await save(state); return state;
70
+ }
60
71
  if (child && child.exitCode === null && child.signalCode === null) { state.action = 'child_starting'; await save(state); return state; }
61
72
  // Persist the launch intent before spawn. On watchdog crash/restart this
62
73
  // gives the previous child time to publish its singleton owner receipt.
@@ -125,7 +136,7 @@ export async function watchSupervisor({ root, signal, once = false, wait = delay
125
136
  let watchdog, initialEpoch;
126
137
  try {
127
138
  initialEpoch = (await readSupervisorState(controlPath(root)))?.epoch;
128
- watchdog = createWatchdog({ root });
139
+ watchdog = createWatchdog({ root, watcherNonce: lock.nonce });
129
140
  do {
130
141
  if (signal?.aborted) break;
131
142
  await watchdog.tick();
@@ -1,6 +1,10 @@
1
1
  import { captureDispatchProvenance } from '../update-detachment/provenance.js';
2
2
  import { captureDispatchOwner, assertTaskAdmission, dispatchWithFence, queueDispatchReceipt, rootFromDelivery, observeDispatchEvent } from '../update-detachment/dispatch-gate.js';
3
+ import { bindCompletionArtifacts } from '../session-supervision/completion-artifacts.js';
4
+ import { recoveryWorkProposal, admitRecoveryWork } from '../session-supervision/recovery-frontier.js';
3
5
  import { safeCheckpointDiagnostic } from './checkpoint-diagnostic.js';
6
+ import { continueResultCycle, resultEvent, executeResultCycle, acceptResultEvent } from '../codex-plugins/simy/hooks/result-cycle.mjs';
7
+ import { finalResponseRecoveryInstruction } from '../session-supervision/final-response-recovery.js';
4
8
  import { recordHandoffStage } from '../codex-plugins/simy/hooks/consistency-handoff.mjs';
5
9
  import { retainControllerEvidence } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
6
10
  import { workspaceIdentity, reviewWorkspaceIdentity } from '../session-supervision/purpose-verification.js';
@@ -15,10 +19,12 @@ import { requireProviderLink } from '../provider-links.js';
15
19
  import { spawn } from 'node:child_process';
16
20
  import { randomUUID } from 'node:crypto';
17
21
  import { dirname } from 'node:path';
18
- import { hasPendingUserStop, recordPath, canExecuteAutonomousExternal, automaticContinuationAllowed, settlePurposeVerification, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
22
+ import { hasPendingUserStop, recordPath, canExecuteAutonomousExternal, automaticContinuationAllowed, queuePurposeDiagnosis, settlePurposeVerification, informationWaitInstruction, assessedCheckpointContract, checkpointDelivery, decideStop, deliveryProgressContext, deliveryStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, sharesTaskWorkspace, taskDirectory, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
19
23
  import { resolveBackendExecutable } from '../backend-executable.js';
20
- import { executeCodexAppServer } from '../session-supervision/codex-app-server.js';
24
+ import { safeProviderFailure, providerEnvironmentIdentity, checkProviderRetry } from '../session-supervision/provider-failure.js';
25
+ import { executeCodexAppServer, inspectCodexAppServer } from '../session-supervision/codex-app-server.js';
21
26
  import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
27
+ import { createTaskPool } from '../codex-plugins/simy/hooks/loop-kernel.mjs';
22
28
 
23
29
  const MAX_OUTPUT = 64 * 1024;
24
30
  const SAFE_PROVIDER_CODES = new Set(['authentication_required', 'identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
@@ -100,7 +106,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
100
106
  remainingWork: run.managed.remainingWork, acceptanceCriteria: run.managed.acceptanceCriteria, proposedAction: run.managed.proposedAction,
101
107
  currentDecision: run.managed.autonomyPolicy ? run.managed.decision : undefined, diagnosis: run.managed.plan?.diagnosis,
102
108
  progress: deliveryProgressContext(run) };
103
- const prompt = run.managed.autonomyPolicy ? `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nExecute ONLY currentDecision.action. Its expectedVerification and rollback apply to this turn. Stay within provider permissions. End with ONLY one JSON checkpoint. ${assessedCheckpointContract(run.managed.decision, run.managed)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\n${minimalSufficientChangeInstruction()}\nStay within the original task and provider permissions. End with ONLY one JSON object: {"status":"in_progress|completed|blocked|error","summary":"...","nextStep":"...","remainingWork":"...","proposedAction":{"kind":"read|local_edit|local_test|external|destructive|scope_change|unknown","scope":"...","summary":"..."},"evidence":[],"acceptanceMet":false}. For blocked include blocker (authorization, missing_information, external_dependency), and for authorization include approval {kind,scope,summary}. Do not claim completion without checking frozen acceptance. SIMY independently verifies supported criteria. Do not write SIMY state directly. If approval or input is needed, return blocked and stop.`;
109
+ const prompt = run.managed.autonomyPolicy ? `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\nExecute ONLY currentDecision.action. Its expectedVerification and rollback apply to this turn. Stay within provider permissions. End with ONLY one JSON checkpoint. ${assessedCheckpointContract(run.managed.decision, run.managed)} Do not write SIMY state directly.` : `Continue the existing SIMY managed task in this exact session. Context: ${JSON.stringify(context)}\n${minimalSufficientChangeInstruction()}\nStay within the original task and provider permissions. End with ONLY one JSON object: {"status":"in_progress|completed|blocked|error","summary":"...","nextStep":"...","remainingWork":"...","proposedAction":{"kind":"read|local_edit|local_test|external|destructive|scope_change|unknown","scope":"...","summary":"..."},"evidence":[],"acceptanceMet":false}. ${informationWaitInstruction} For blocked include blocker (authorization, missing_information, external_dependency), and for authorization include approval {kind,scope,summary}. Do not claim completion without checking frozen acceptance. SIMY independently verifies supported criteria. Do not write SIMY state directly. If approval or input is needed, return blocked and stop.`;
104
110
  const sandbox = run.managed.autonomyPolicy && run.managed.decision?.action.kind === 'read' ? 'read-only' : run.managed.autoRecoverySandbox;
105
111
  const args = ['exec', '--json', '--skip-git-repo-check', '-s', sandbox, '-m', run.managed.resumeModel, 'resume', record.session, prompt];
106
112
  if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
@@ -150,7 +156,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
150
156
  });
151
157
  }
152
158
 
153
- export async function tickManagedTasks({ root, webOrigin, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication } = {}) {
159
+ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), execute, inspectProcess = processIdentity, signal, beforeClaim, authorizeDispatch, readRecords, observeTurn, observeAuthentication, inspectProvider = inspectCodexAppServer, shouldYield } = {}) {
154
160
  const tickWallStart = Date.now();
155
161
  const currentTime = () => now + Math.max(0, Date.now() - tickWallStart);
156
162
  if (signal?.aborted) return;
@@ -160,7 +166,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
160
166
  if (signal?.aborted) return;
161
167
  const records = await readCandidates();
162
168
  for (let record of records) {
163
- if (signal?.aborted) break;
169
+ if (signal?.aborted || await shouldYield?.()) break;
164
170
  const claimNow = currentTime();
165
171
  if (!record.run?.managed) continue;
166
172
  record = await recoverOutcomeAuditLease(record, { root, inspectProcess });
@@ -181,9 +187,27 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
181
187
  const owner = run.managed.resumeLease?.owner;
182
188
  const ownerDead = await leaseOwnerDead(owner, inspectProcess);
183
189
  // A live/unknown owner is never taken over merely because time passed.
184
- if (!ownerDead && (owner || claimNow - run.managed.resumeLease?.claimedAt <= 5 * 60_000)) continue;
185
- await updateRecord(root, record.provider, record.session, current => {
190
+ const acceptedCheckpoint = run.managed.executionAttempt?.id === run.managed.resumeLease?.id &&
191
+ run.managed.executionAttempt?.phase === 'checkpointed' &&
192
+ run.managed.executionAttempt?.checkpointRevision === run.revision;
193
+ if (!ownerDead && (owner || !acceptedCheckpoint)) continue;
194
+ await updateRecord(root, record.provider, record.session, async current => {
186
195
  if (hasPendingUserStop(root, current) || current?.run?.state !== 'auto_resuming' || current.run.managed.resumeLease?.id !== run.managed.resumeLease?.id) return current;
196
+ const accepted = current.run.managed.executionAttempt;
197
+ if (accepted?.id === current.run.managed.resumeLease?.id && accepted.phase === 'checkpointed' &&
198
+ accepted.checkpointRevision === current.run.revision) {
199
+ // The checkpoint transaction survived but its host did not. Settle
200
+ // that checkpoint; never dispatch the already acknowledged action.
201
+ delete current.run.managed.resumeLease;
202
+ await verifyAutomaticCriteria(current, claimNow);
203
+ const decision = decideStop(current.run, claimNow);
204
+ current.run = decision.run;
205
+ if (current.run.state === 'active') {
206
+ current.run.state = 'recovery_required';
207
+ current.run.recoveryCause = current.run.managed.resumeChannel === 'codex_app_server' ? 'session_supervision' : 'process_exit';
208
+ }
209
+ current.run.updatedAt = claimNow; notifyState(current.run, claimNow); return current;
210
+ }
187
211
  current.run.state = 'recovery_required'; current.run.recoveryCause = 'interrupted_owner'; current.run.managed.autoRecovery = false;
188
212
  recordRecoveryIncident(current.run, { code: 'interrupted_owner', subject: current.run.managed.executionAttempt?.id || '', now: claimNow });
189
213
  recordRecoveryStage(current.run, 'interrupted', { now: claimNow, attemptId: current.run.managed.executionAttempt?.id });
@@ -192,12 +216,34 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
192
216
  if (attempt?.budgetReservation && attempt.phase === 'dispatching') { attempt.budgetReservation.state = 'unknown'; current.run.managed.unknownConsumption = true; if (current.run.managed.maxTokens) current.run.managed.usageBlocked = true; }
193
217
  if (attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') current.run.managed.executionUncertain = {
194
218
  attemptId: attempt.id, decisionId: attempt.decisionId, action: attempt.action, at: claimNow };
219
+ // Ownership is released only after the existing process-identity check
220
+ // proves the owner dead and uncertain effects have been retained.
221
+ delete current.run.managed.resumeLease;
195
222
  current.run.updatedAt = claimNow; notifyState(current.run, claimNow); return current;
196
223
  });
197
224
  continue;
198
225
  }
199
226
  if (!automaticContinuationAllowed(record)) continue;
200
- try { await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
227
+ let providerLink;
228
+ try { providerLink = await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
229
+ if (run.managed.providerCapabilityWait) {
230
+ if (run.state !== 'recovery_required' || run.managed.resumeLease || managedBudgetReason(run, claimNow)) continue;
231
+ const expected = run.managed.providerCapabilityWait;
232
+ let check;
233
+ try { check = await checkProviderRetry(expected, inspectProvider, { now: claimNow, signal }); } catch { continue; }
234
+ record = await updateRecord(root, record.provider, record.session, current => {
235
+ if (current?.run?.id !== run.id || !automaticContinuationAllowed(current) || current.run.state !== 'recovery_required' ||
236
+ current.run.managed.resumeLease || JSON.stringify(current.run.managed.providerCapabilityWait) !== JSON.stringify(expected)) return current;
237
+ if (check.retry) {
238
+ delete current.run.managed.providerCapabilityWait;
239
+ current.run.managed.autoRecovery = true; current.run.recoveryCause = 'session_supervision';
240
+ current.run.managed.nextAttemptAt = claimNow;
241
+ } else current.run.managed.providerCapabilityWait = check.wait;
242
+ return current;
243
+ });
244
+ run = record.run;
245
+ if (run.managed.providerCapabilityWait) continue;
246
+ }
201
247
  const appServer = run.managed.resumeChannel === 'codex_app_server';
202
248
  const causeAllowed = run.recoveryCause === 'process_exit' || appServer && ['session_supervision', 'approved_action'].includes(run.recoveryCause);
203
249
  const approvedWait = run.managed.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(run.managed.decision.action?.kind) &&
@@ -207,7 +253,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
207
253
  if (run.state !== 'recovery_required' || !causeAllowed || !run.managed.autoRecovery || run.managed.pendingApproval || run.managed.autonomyPolicy && run.managed.decision?.outcome === 'wait' && !approvedWait || appServer && run.managed.nextAttemptAt > claimNow) continue;
208
254
  let authorizationScope;
209
255
  const authorizeClaim = async () => {
210
- await requireProviderLink({ root, provider: record.provider });
256
+ await requireProviderLink({ root, provider: record.provider, expectedEpoch: providerLink.epoch });
211
257
  if (!authorizeDispatch) return;
212
258
  const latest = await authorizeDispatch(record);
213
259
  if (authorizationScope !== undefined && latest !== authorizationScope) throw Object.assign(new Error("Desktop authorization scope changed."), { code: "desktop_access_required" });
@@ -228,8 +274,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
228
274
  let claimed;
229
275
  try { claimed = await updateRecord(root, record.provider, record.session, current => {
230
276
  const live = current?.run;
277
+ // Re-read if the selected task or action changed while acquiring the state lock.
278
+ if (live?.id !== run.id || live.revision !== run.revision || live.controlRevision !== run.controlRevision ||
279
+ live.managed?.taskId !== run.managed?.taskId || live.managed?.decision?.id !== run.managed?.decision?.id ||
280
+ live.managed?.providerCapabilityWait) return current;
281
+
231
282
  if (!automaticContinuationAllowed(current)) return current;
232
283
  if (live?.managed?.outcomeAudit?.lease) return current;
284
+ if (current.autoContinueEpoch !== record.autoContinueEpoch ||
285
+ live.managed?.executionUncertain && live.managed.recoveryDiagnosisDecisionId !== live.managed.decision?.id ||
286
+ ['pending', 'transferred', 'pending_received'].includes(live.managed?.handoff?.status)) return current;
233
287
  const appChannel = live?.managed?.resumeChannel === 'codex_app_server';
234
288
  const allowedCause = live?.recoveryCause === 'process_exit' || appChannel && ['session_supervision', 'approved_action'].includes(live?.recoveryCause);
235
289
  const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(live.managed.decision.action?.kind) &&
@@ -239,12 +293,20 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
239
293
  if (stop) {
240
294
  live.state = stop.code === 'purpose_budget' ? 'budget_exhausted' : stop.code === 'failure_limit' ? 'failed' : 'paused'; live.reason = stop.reason; live.updatedAt = currentTime(); notifyState(live, live.updatedAt); return current;
241
295
  }
296
+ const workProposal = recoveryWorkProposal(current);
297
+ if (workProposal?.blocked) return current;
242
298
  live.state = 'auto_resuming'; live.managed.resumeLease = { id: leaseId, claimedAt: currentTime(), checkpointRevision: live.revision, decisionId: live.managed.decision?.id || null,
243
- owner: { pid: process.pid, identity: ownerIdentity } };
299
+ owner: { pid: process.pid, identity: ownerIdentity }, workProposal: workProposal ? { inputDigest: workProposal.inputDigest } : null };
244
300
  live.managed.executionAttempt = { id: leaseId, decisionId: live.managed.decision?.id || null,
245
301
  action: live.managed.decision?.action || live.managed.proposedAction,
246
302
  budgetReservation: { rootTaskId: live.managed.taskId, tokens: live.managed.maxTokens ? Math.max(0, live.managed.maxTokens - (live.managed.tokensUsed || 0)) : null, state: 'reserved', epoch: leaseId }, phase: 'claimed', claimedAt: currentTime(), lastActivityAt: currentTime() };
247
303
  live.managed.executionAttempt.recoveryIncidentId = live.managed.recovery?.currentId || null;
304
+ const pendingResult = live.resultCycle?.events.find(row => row.id === live.resultCycle.pendingId);
305
+ // Carry the actual preceding result into its dispatch receipt. A synthetic
306
+ // initial/condition event is needed only when no unconsumed result exists.
307
+ if (!pendingResult || pendingResult.dispatchAttemptId) acceptResultEvent(current, resultEvent(current, {
308
+ kind: 'condition_changed', origin: 'supervisor_claim',
309
+ sourceId: `dispatch:${live.managed.decision?.id}:${live.revision}`, now: currentTime() }));
248
310
  // Retain the repair action identity during independent verification.
249
311
  // Older receipts can still be executing after a verifier; bind those to
250
312
  // the next actual diagnosis/repair only after all lease/authority fences.
@@ -318,7 +380,8 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
318
380
  if (!result) {
319
381
  await authorizeClaim();
320
382
  const runner = execute || (appServer ? executeCodexAppServer : executeCodexResume);
321
- result = await runner(claimed, { signal: controller.signal, root, webOrigin, leaseId, currentTime, leaseMonitored: true,
383
+ result = await executeResultCycle({ record: claimed, attemptId: leaseId, execute: runner, persist: persistReceipt,
384
+ options: { signal: controller.signal, root, webOrigin, leaseId, currentTime, leaseMonitored: true,
322
385
  onStarted: async observation => {
323
386
  await persistReceipt(current => {
324
387
  const attempt = current?.run?.managed?.executionAttempt;
@@ -352,6 +415,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
352
415
  const live = current?.run;
353
416
  if (live?.state !== 'auto_resuming' || live.managed?.resumeLease?.id !== leaseId) throw new Error('Managed recovery lease changed before provider dispatch.');
354
417
  if (!automaticContinuationAllowed(current)) throw new Error('Automatic continuation was disabled before provider dispatch.');
418
+ if (live.managed.executionAttempt?.id !== leaseId || live.managed.executionAttempt.phase !== 'claimed') {
419
+ throw new Error('Managed attempt was already dispatched or replaced.');
420
+ }
421
+ if (current.autoContinueEpoch !== claimed.autoContinueEpoch ||
422
+ live.revision !== live.managed.resumeLease.checkpointRevision ||
423
+ (live.managed.decision?.id || null) !== live.managed.resumeLease.decisionId ||
424
+ ['pending', 'transferred', 'pending_received'].includes(live.managed.handoff?.status) ||
425
+ live.managed.executionUncertain && live.managed.recoveryDiagnosisDecisionId !== live.managed.decision?.id) {
426
+ throw new Error('Managed recovery authorization changed before provider dispatch.');
427
+ }
355
428
  if (appServer) {
356
429
  if (!providerSandbox || !['readOnly', 'workspaceWrite', 'dangerFullAccess', 'externalSandbox'].includes(providerSandbox.type)) throw new Error('Original Codex sandbox policy is unavailable.');
357
430
  // Observation is not enrollment authority. In particular, a
@@ -362,6 +435,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
362
435
  const ledger=live.managed.purpose?.evidence;
363
436
  if(selected?.dependsOn?.some(id=>!ledger?.units?.some(unit=>unit.id===id && unit.status==='verified' && unit.purposeRevision===ledger.purposeRevision))) throw new Error('Required dependency is not independently verified.');
364
437
  if (live.managed.decision?.autonomousExternal && !canExecuteAutonomousExternal(live.managed)) throw new Error('Autonomous external authority changed before dispatch.');
438
+ admitRecoveryWork(current, live.managed.resumeLease.workProposal, leaseId, currentTime());
365
439
  const approved = live.managed.approvedAction;
366
440
  live.managed.executionAttempt.phase = 'dispatching';
367
441
  live.managed.executionAttempt.dispatchedAt = currentTime();
@@ -376,7 +450,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
376
450
  return current;
377
451
  });
378
452
  await authorizeClaim();
379
- } });
453
+ } } });
380
454
  }
381
455
  }
382
456
  } catch (error) { result = error?.code === 'provider_link_disconnected'
@@ -384,15 +458,25 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
384
458
  : error?.code === 'desktop_access_required'
385
459
  ? { code: -3, busy: true, accessDenied: true }
386
460
  : { code: controller.signal.aborted ? -2 : -1, cancelled: controller.signal.aborted,
387
- providerTurnId: error?.providerTurnId, providerSessionId:error?.providerSessionId, question: error?.question, errorCode: error?.code || 'provider_error', restorationUnverified: error?.restorationUnverified === true }; }
461
+ providerTurnId: error?.providerTurnId, providerSessionId:error?.providerSessionId, question: error?.question,
462
+ finalResponseWithoutCheckpoint: error?.finalResponseWithoutCheckpoint,
463
+ errorCode: error?.code || 'provider_error', diagnostic: safeProviderFailure(error?.diagnostic), restorationUnverified: error?.restorationUnverified === true }; }
388
464
  try {
465
+ if (!dispatched && ['unsupported', 'unavailable'].includes(result.errorCode) && ['writer_lock', 'provider_available', 'service_identity', 'bounded_history', 'connector_inventory', 'connector_pagination', 'review_configuration', 'review_isolation'].includes(result.diagnostic?.capability)) {
466
+ try { result.providerEnvironment = await providerEnvironmentIdentity(inspectProvider); } catch { /* No guessed identity: normal bounded failure path remains. */ }
467
+ }
389
468
  await usageQueue;
390
469
  if (usageError) result = { ...result, code: -1, errorCode: 'usage_unavailable' };
391
470
  usage ||= result?.usage || null;
392
471
  if (controller.signal.aborted) result = { ...result, code: -2, cancelled: true };
393
472
  if (result.code === 0) {
394
473
  try {
395
- if (result.verification) await settlePurposeVerification({ root, provider: record.provider, session: record.session, expectedLease: leaseId, review: result.verification, verifySnapshot: current => reviewWorkspaceIdentity(current, root, result.verification, { signal: controller.signal }), now: Date.now() });
474
+ if (result.verification) await settlePurposeVerification({ root, provider: record.provider, session: record.session, expectedLease: leaseId, review: result.verification, verifySnapshot: async current => {
475
+ const refreshed = await bindCompletionArtifacts(current, result.verification, {
476
+ authorize: async () => { await assertActiveLease(root, record.provider, record.session, leaseId, controller.signal, currentTime); await authorizeClaim(); }, signal: controller.signal });
477
+ Object.assign(result.verification, refreshed);
478
+ return reviewWorkspaceIdentity(current, root, result.verification, { signal: controller.signal });
479
+ }, now: Date.now() });
396
480
  else await checkpointDelivery({ root, provider: record.provider, session: record.session, cwd: record.cwd,
397
481
  checkpoint: result.checkpoint || parseAgentCheckpoint(result.output, record.session), expectedLease: leaseId, toolEvidence: result.toolEvidence || [], sourceContext: result.sourceContext, controllerEvidence: result.controllerEvidence, now: Date.now() });
398
482
  if (result.dispatchAttemptId) queueDispatchReceipt({ root: rootFromDelivery(root), attemptId: result.dispatchAttemptId, receiptId: `checkpoint:${leaseId}`, resultRef: `delivery-record:${record.provider}:${record.session}:${leaseId}` });
@@ -435,7 +519,15 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
435
519
  }
436
520
  if (result.errorCode === 'checkpoint_rejected') live.managed.lastCheckpointRejection = { at: Date.now(), decisionId: claimed.run.managed.decision?.id, diagnostic: result.diagnostic };
437
521
  else if (result.code === 0) delete live.managed.lastCheckpointRejection;
522
+ if (result.errorCode === 'checkpoint' && result.finalResponseWithoutCheckpoint) {
523
+ live.managed.finalResponseWithoutCheckpoint = { ...result.finalResponseWithoutCheckpoint,
524
+ providerTurnId: result.providerTurnId, providerSessionId: result.providerSessionId || record.session };
525
+ } else if (result.code === 0) delete live.managed.finalResponseWithoutCheckpoint;
438
526
  const attempt = live.managed.executionAttempt;
527
+ const attemptedUnit = live.managed.purpose?.evidence?.units?.find(unit => unit.id === attempt?.decisionId);
528
+ if (attemptedUnit?.status === 'dispatched' && result.code !== 0) {
529
+ attemptedUnit.status = attempt.action?.kind === 'read' ? 'failed' : 'unknown';
530
+ }
439
531
  if (!result.busy && attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') live.managed.executionUncertain = {
440
532
  attemptId: leaseId, decisionId: attempt.decisionId, action: attempt.action, at: Date.now() };
441
533
  live.managed.attempts ||= [];
@@ -445,8 +537,9 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
445
537
  checkpointRevision: live.revision, outcome: result.busy ? 'busy' : result.cancelled ? 'cancelled' : result.code === 0 ? 'checkpoint' : 'failed',
446
538
  errorCode: result.code === 0 || result.busy || result.cancelled ? null : safeProviderCode(result.errorCode),
447
539
  ...(result.providerSessionId ? {providerSessionId:result.providerSessionId} : {}),
540
+ ...(safeProviderFailure(result.diagnostic) ? { diagnostic: safeProviderFailure(result.diagnostic) } : {}),
448
541
  restorationUnverified: result.restorationUnverified === true,
449
- reason: result.accessDenied ? 'Desktop organization access is required before another provider turn.' : result.busy ? 'Exact provider session currently has another writer.' : result.cancelled ? 'Managed recovery cancelled before completion.' : result.code === 0 ? 'Structured checkpoint recorded.' : 'Provider failed or returned an invalid checkpoint.',
542
+ reason: result.accessDenied ? 'Desktop organization access is required before another provider turn.' : result.busy ? 'Exact provider session currently has another writer.' : result.cancelled ? 'Managed recovery cancelled before completion.' : result.code === 0 ? 'Structured checkpoint recorded.' : result.diagnostic ? `Provider capability unavailable: ${safeProviderFailure(result.diagnostic)?.capability || 'unknown'}. Retry after its environment changes.` : 'Provider failed or returned an invalid checkpoint.',
450
543
  nextStep: live.nextStep });
451
544
  live.managed.attempts = live.managed.attempts.slice(-20);
452
545
  if (result.code !== 0) recordRecoveryStage(live, result.busy ? 'waiting' : 'interrupted', {
@@ -486,6 +579,10 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
486
579
  } else if (result.cancelled) {
487
580
  live.managed.autoRecovery = false; live.state = 'recovery_required'; live.recoveryCause = 'shutdown_cancelled';
488
581
  live.reason = 'Managed recovery was cancelled; inspect the original session before the next bounded recovery.';
582
+ } else if (result.providerEnvironment && !dispatched) {
583
+ live.managed.providerCapabilityWait = { diagnostic: safeProviderFailure(result.diagnostic), environmentIdentity: result.providerEnvironment.identity, nextCheckAt: currentTime() + 60000, checks: 0 };
584
+ live.managed.autoRecovery = true; live.state = 'recovery_required'; live.recoveryCause = 'session_supervision';
585
+ live.reason = 'Provider capability needs an environment change; bounded read-only checks continue without spending another provider turn.';
489
586
  } else if (result.code !== 0) {
490
587
  live.failures += 1; live.managed.autoRecovery = false;
491
588
  live.state = failureLimitReached(live) ? 'failed' : 'recovery_required';
@@ -493,6 +590,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
493
590
  live.reason = appServer ? providerFailureReason(safeProviderCode(result.errorCode)) +
494
591
  (result.restorationUnverified ? ' Original provider permissions also require inspection.' : '') :
495
592
  'Codex recovery process failed; inspect its checkpoint and original session before the next bounded recovery.';
593
+ // Recover the missing handoff with a read diagnosis, never by replaying
594
+ // the last action. The existing dispatcher rechecks leases and rights.
595
+ if (appServer && live.state !== 'failed' && automaticContinuationAllowed(current) &&
596
+ !live.managed.pendingApproval && !live.managed.nativeWait && !live.managed.purpose?.nativeBoundary &&
597
+ (result.errorCode === 'checkpoint' && result.finalResponseWithoutCheckpoint || result.errorCode === 'checkpoint_rejected')) {
598
+ await continueResultCycle(current, resultEvent(current, { kind: 'final_response', origin: 'executor_result',
599
+ sourceId: result.providerTurnId || leaseId, attemptId: leaseId, resultRef: `attempt:${leaseId}` }), {
600
+ selectNext: () => queuePurposeDiagnosis(live, finalResponseRecoveryInstruction,
601
+ `missing-handoff:${result.providerTurnId || leaseId}`, Date.now(), 'checkpoint') });
602
+ }
496
603
  } else if (live.revision <= run.revision) {
497
604
  live.failures += 1;
498
605
  live.reason = result.code === 0 ? 'Codex recovery returned without a checkpoint.' : 'Codex recovery failed; inspect the original session.';
@@ -514,6 +621,9 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
514
621
  current.run.reason = 'Verified checkpoint recorded; next bounded exact-session attempt is pending.';
515
622
  }
516
623
  }
624
+ if (result.code !== 0) await continueResultCycle(current, resultEvent(current, {
625
+ kind: 'failure', origin: 'executor_result', sourceId: result.providerTurnId || leaseId,
626
+ attemptId: leaseId, resultRef: `attempt:${leaseId}` }));
517
627
  current.run.updatedAt = Date.now(); notifyState(current.run, current.run.updatedAt); return current;
518
628
  });
519
629
  // Settling uncertainty must precede unlocking: otherwise another session
@@ -526,19 +636,42 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
526
636
  export function createManagedSupervisor(options) {
527
637
  let running = null, auditing = null, closed = false;
528
638
  const controller = new AbortController();
639
+ const workers = createTaskPool({ concurrency: options.concurrency ?? 4 });
640
+ const readCandidates = options.readRecords || (() => listRecords(options.root));
529
641
  return {
530
642
  tick() {
531
643
  if (closed) return Promise.resolve();
532
644
  if (!auditing) auditing = (async () => {
533
645
  const runtime = await desktopRuntimeStatus({ root: dirname(options.root) }).catch(() => ({ state: 'runtime_observation_failed' }));
534
646
  await runConsistencyMonitor({ root: options.root, runtime, readRecords: options.readRecords || (() => listRecords(options.root)),
535
- onAudit: (record, findings, request) => auditConsistencyTask(record, findings, request, {
647
+ onAudit: async (record, findings, request) => closed || await options.shouldYield?.() ? undefined : auditConsistencyTask(record, findings, request, {
536
648
  root: options.root, signal: controller.signal, authorize: options.authorizeDispatch || (async () => {}) }) });
537
649
  })().catch(() => {}).finally(() => { auditing = null; });
538
- if (!running) running = tickManagedTasks({ ...options, signal: controller.signal }).finally(() => { running = null; });
650
+ if (!running) running = (async () => {
651
+ for (const record of await readCandidates()) {
652
+ if (closed || await options.shouldYield?.()) break;
653
+ if (!record.run?.managed) continue;
654
+ const key = `${record.provider}:${record.session}`;
655
+ workers.offer(key, async () => {
656
+ if (closed || await options.shouldYield?.()) return;
657
+ // A queue entry is a wakeup, not authority or a saved action. Re-read
658
+ // consent, revisions and task identity at admission and each claim.
659
+ const readRecords = async () => {
660
+ const rows = options.readRecords ? await options.readRecords()
661
+ : [await readRecord(options.root, record.provider, record.session)];
662
+ return rows.filter(row => row?.provider === record.provider && row.session === record.session &&
663
+ row.run?.id === record.run.id && row.run?.managed?.taskId === record.run.managed.taskId);
664
+ };
665
+ await tickManagedTasks({ ...options, readRecords, signal: controller.signal });
666
+ });
667
+ }
668
+ })().finally(() => { running = null; });
539
669
  return running;
540
670
  },
541
- async close() { closed = true; controller.abort(); await Promise.allSettled([running, auditing]); },
671
+ status() { return workers.snapshot(); },
672
+ async idle() { await running; await workers.idle(); },
673
+ async drain() { closed = true; await Promise.allSettled([running, auditing, workers.stop()]); },
674
+ async close() { closed = true; controller.abort(); await Promise.allSettled([running, auditing, workers.stop()]); },
542
675
  };
543
676
  }
544
677
 
@@ -13,7 +13,8 @@ const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'authorization
13
13
  export function desktopActionAllowed(access, command) {
14
14
  return desktopAccessReady(access) || recoveryActions.has(command?.action) ||
15
15
  command?.action === 'session_set_auto_continue' && command.enabled === false ||
16
- command?.action === 'session_set_email_reports' && command.enabled === false;
16
+ command?.action === 'session_set_email_reports' && command.enabled === false ||
17
+ command?.action === 'team_thread_share' && command.enabled === false;
17
18
  }
18
19
  export function requireDesktopAccess(access) {
19
20
  if (!desktopAccessReady(access)) throw Object.assign(new Error('Sign in to an organization with SQM and Developer access before using SIMY.'), { code: 'desktop_access_required' });
@@ -5,6 +5,8 @@ import { createLedgerCoordinatorAdapters } from "./update-detachment/ledger-adap
5
5
  import { createUpdateCoordinator } from "./update-detachment/coordinator.js";
6
6
  import { sessionWorkState, compareSessionWork, reconcileFamilyCompletion, expireSessionWorkState } from './session-supervision/work-state.js';
7
7
  import { createPullRequestObserver } from './session-supervision/pull-requests.js';
8
+ import { createTeamThreads, teamThreadKey } from './team-threads.js';
9
+ import { createTeamStatusFenceStore } from './team-status-fence-store.js';
8
10
  import { sessionDecisionPresentation } from './session-supervision/decision-presentation.js';
9
11
  import { PROVIDER_LINKS, readProviderLinks, writeProviderLink, requireProviderLink } from "./provider-links.js";
10
12
  import { randomUUID } from "node:crypto";
@@ -221,6 +223,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
221
223
  let statisticsSelection = { key: null, generation: 0 };
222
224
  const stopNotifier = createStopNotifier({ emit: event => { if (!closed && !relay) emit(event); } });
223
225
  if (env.SIMY_REMOTE_CHILD !== "1" && process.platform !== "win32") remoteHosts = (dependencies.createRemoteHostManager || createRemoteHostManager)({ root, onChange: () => { if (!closed && !relay) setImmediate(() => { void publish().catch(() => {}); }); } });
226
+ let teamThreads;
224
227
  let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
225
228
  let nativeDevelopmentReports = { state: 'loading', enrollments: [] };
226
229
  let nativeDevelopmentReportsScope = null;
@@ -713,6 +716,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
713
716
  if (JSON.stringify(await readProviderLinks(root)) !== requestedLinks) { publishAgain = true; return; }
714
717
  if (connectionRevision !== requestedConnectionRevision || !sameAccess(sampledAccess, agent?.getDesktopAccess ? agent.getDesktopAccess() : await readAccess())) { publishAgain = true; return; }
715
718
  status.nativeDevelopmentReports = { state: 'loading', enrollments: [] };
719
+ status.teamThreads = teamThreads?.snapshot();
716
720
  if (!closed && !relay) emitStatus(status);
717
721
  return;
718
722
  }
@@ -764,6 +768,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
764
768
  status.sessionSupervision = { ...discovery, refreshing: Boolean(sessionRefresh), sessions: reconcileFamilyCompletion(sessions.map(candidate => ({ ...candidate,
765
769
  decisionRequest: sessionDecisionPresentation(candidate, runsBySession.get(`${candidate.provider}:${candidate.session}`)),
766
770
  workState: sessionWorkState(candidate, runsBySession.get(`${candidate.provider}:${candidate.session}`) || stateRunsBySession.get(`${candidate.provider}:${candidate.session}`), { provider: discovery.providers?.[candidate.provider] }) }))).sort(compareSessionWork) };
771
+ teamThreads?.tick(sessions);
772
+ status.teamThreads = teamThreads?.snapshot();
773
+ for (const session of status.sessionSupervision.sessions) {
774
+ const key = teamThreadKey(agent.getTeamThreadsScope?.() || {}, session);
775
+ const own = status.teamThreads?.own.find(item => item.threadKey === key);
776
+ session.teamSharing = { enabled: own?.enabled === true, pendingStop: status.teamThreads?.pendingStops.includes(key) === true };
777
+ }
767
778
  const latestAccess = await accessSnapshot();
768
779
  if (JSON.stringify(await readProviderLinks(root)) !== requestedLinks ||
769
780
  !desktopAccessReady(latestAccess) || accessScope(latestAccess) !== requestedScope ||
@@ -779,12 +790,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
779
790
  if (accessScope(latestConnectionStatus?.access) !== requestedScope || !desktopAccessReady(latestConnectionStatus?.access)) { publishAgain = true; return; }
780
791
  status = { ...structuredClone(latestConnectionStatus), deliveryHooks: status.deliveryHooks, sessionSupervision: status.sessionSupervision };
781
792
  }
793
+ status.teamThreads = teamThreads?.snapshot();
782
794
  if (!closed && !relay) emitStatus(status);
783
795
  }).finally(() => { publishing = null; if (publishAgain && !closed) { publishAgain = false; setImmediate(() => { void publish().catch(() => {}); }); } });
784
796
  return publishing;
785
797
  };
786
798
  const stopAgent = async () => {
787
799
  clearInterval(timer);
800
+ teamThreads?.close();
788
801
  pluginInspectionAbort.abort();
789
802
  activeStatisticsRead?.controller.abort();
790
803
  discoveryStopped = true;
@@ -843,6 +856,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
843
856
  },
844
857
  });
845
858
  if (env.SIMY_UPDATE_TRANSFER_TOKEN) await coordinator.reconnect();
859
+ teamThreads = createTeamThreads({
860
+ readScope: () => agent.getTeamThreadsScope?.() || {key: accessScope(currentAccess),allowed:false,authenticated:false},
861
+ request: (body,signal) => agent.requestTeamThreads(body,signal),
862
+ fenceStore: createTeamStatusFenceStore(root),
863
+ readContext: dependencies.readTeamThreadContext,
864
+ onChange: () => { if(!closed) setImmediate(() => {void publish().catch(() => {});}); },
865
+ });
846
866
  stopWorkflows = (dependencies.startWorkflowUpdates || startWorkflowUpdates)({ enabled: env.SIMY_AUTO_UPDATE !== "0" });
847
867
  updater.on?.("change", () => { void publish().catch(() => {}); });
848
868
  updater.start({ registry: agent.registry, coordinator, isDesktopIdle: () => !unfinishedDesktopWork,
@@ -1105,15 +1125,28 @@ export async function startDesktopHost({ input = process.stdin, output = process
1105
1125
  const commandScope = accessScope(access);
1106
1126
  if (!desktopActionAllowed(access, command)) throw Object.assign(new Error('Desktop access is required.'), { code: 'desktop_access_required' });
1107
1127
  const recoveryControl = ['provider_disconnect', 'session_unmonitor'].includes(action) ||
1108
- ['session_set_auto_continue', 'session_set_email_reports'].includes(action) && command.enabled === false;
1128
+ ['session_set_auto_continue', 'session_set_email_reports', 'team_thread_share'].includes(action) && command.enabled === false;
1109
1129
  if (['detach_update', 'update_reconnect'].includes(action)) requireDesktopAccess(access);
1110
1130
  else if (!recoveryControl && desktopAccessReady(access) && !desktopActionAllowed({ state: 'unavailable' }, command)) await authorizeWork();
1111
1131
 
1112
1132
  if (!coordinator.acceptsNewWork() && !recoveryControl && !["refresh", "reconnect", "task_detail", "check_update", "quit", "delivery_stop", "delivery_pause", "delivery_reject"].includes(action)) {
1113
1133
  throw new Error("The update connection requires recovery.");
1114
1134
  }
1135
+ if (['team_threads_refresh','team_thread_open','team_thread_share'].includes(action)) {
1136
+ let ok;
1137
+ if(action === 'team_threads_refresh') ok = await teamThreads.refresh({memberId:command.memberId});
1138
+ else if(action === 'team_thread_open') ok = await teamThreads.open({memberId:command.memberId,threadKey:command.threadKey});
1139
+ else {
1140
+ if (command.organizationId !== undefined && command.organizationId !== access.organizationId) throw new Error('Sharing organization changed. Confirm again.');
1141
+ const selected = sessionStatus.sessions.find(item => item.provider === command.provider && item.session === command.session);
1142
+ if(!selected) throw new Error('Refresh and select your conversation again.');
1143
+ ok = await teamThreads.setSharing(selected,command.enabled);
1144
+ }
1145
+ emit({type:'result',id:command.id,ok:ok===true}); void publish().catch(() => {}); return;
1146
+ }
1147
+
1115
1148
  let commandProviderLink;
1116
- if (command.provider && !["provider_connect", "provider_disconnect", "delivery_stop", "delivery_pause", "delivery_reject", "session_untrack", "session_unmonitor"].includes(action) &&
1149
+ if (command.provider && !["refresh", "provider_connect", "provider_disconnect", "delivery_stop", "delivery_pause", "delivery_reject", "session_untrack", "session_unmonitor"].includes(action) &&
1117
1150
  !(action === "session_set_auto_continue" && command.enabled === false)) {
1118
1151
  commandProviderLink = await requireProviderLink({ root: deliveryRoot, provider: command.provider });
1119
1152
  }