@awak-app/simy-cli 0.7.16 → 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 (77) hide show
  1. package/README.md +180 -26
  2. package/package.json +3 -2
  3. package/src/agent.js +128 -7
  4. package/src/bounded-local-task-subtask-pool.js +32 -6
  5. package/src/bounded-local-task-subtasks-contract.js +1 -1
  6. package/src/cli-contract.js +1 -1
  7. package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
  8. package/src/codex-plugins/simy/hooks/README.md +8 -3
  9. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +261 -67
  10. package/src/codex-plugins/simy/hooks/hooks.json +1 -1
  11. package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
  12. package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
  13. package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
  14. package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
  15. package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
  16. package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
  17. package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
  18. package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
  19. package/src/codex-plugins/simy/hooks/outcome-progress.mjs +15 -3
  20. package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
  21. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +8 -6
  22. package/src/codex-plugins/simy/hooks/result-cycle.mjs +32 -6
  23. package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
  24. package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
  25. package/src/coding-run-recovery.js +4 -2
  26. package/src/delivery-hooks/claude-workflow.md +10 -7
  27. package/src/delivery-hooks/command.js +45 -10
  28. package/src/delivery-hooks/consult.js +60 -0
  29. package/src/delivery-hooks/diagnosis.js +2 -2
  30. package/src/delivery-hooks/legacy-hook-routing.js +46 -8
  31. package/src/delivery-hooks/local-runtime.js +50 -1
  32. package/src/delivery-hooks/local-supervisor.js +1 -1
  33. package/src/delivery-hooks/repair-continuation.js +80 -0
  34. package/src/delivery-hooks/repair.js +164 -0
  35. package/src/delivery-hooks/supervisor-service.js +3 -1
  36. package/src/delivery-hooks/supervisor.js +88 -12
  37. package/src/desktop-access.js +2 -1
  38. package/src/desktop-host.js +35 -2
  39. package/src/durable-local-task-worker.js +372 -58
  40. package/src/execution-capability-contract.js +15 -1
  41. package/src/index.js +3 -3
  42. package/src/local-photos-read.swift +33 -0
  43. package/src/local-photos.js +47 -0
  44. package/src/local-task-attachment-store.js +31 -0
  45. package/src/local-task-destination.js +174 -0
  46. package/src/local-task.js +119 -7
  47. package/src/native-delegation-contract.js +68 -0
  48. package/src/native-provider-identity.js +60 -0
  49. package/src/orchestrator/budget.js +22 -11
  50. package/src/orchestrator/independent-audit.js +9 -18
  51. package/src/orchestrator/loop.js +27 -59
  52. package/src/orchestrator/recovery.js +32 -2
  53. package/src/provider-delegation.js +336 -0
  54. package/src/runner.js +2 -1
  55. package/src/session-supervision/artifact-observer.js +138 -0
  56. package/src/session-supervision/codex-app-server.js +19 -5
  57. package/src/session-supervision/completion-artifacts.js +44 -0
  58. package/src/session-supervision/consistency-auditor.js +8 -11
  59. package/src/session-supervision/continuation-analysis.js +46 -28
  60. package/src/session-supervision/final-response-recovery.js +5 -3
  61. package/src/session-supervision/index.js +22 -5
  62. package/src/session-supervision/purpose-recovery.js +12 -3
  63. package/src/session-supervision/purpose-verification.js +9 -57
  64. package/src/session-supervision/recovery-frontier.js +55 -0
  65. package/src/session-supervision/stop-notifications.js +8 -8
  66. package/src/sqm/agent-skill.js +1 -1
  67. package/src/sqm/checkers.js +12 -6
  68. package/src/sqm/claude-plugin-update.js +22 -8
  69. package/src/sqm/claude-plugin.js +4 -0
  70. package/src/sqm/local-tests.js +2 -2
  71. package/src/sqm/plugin-cache.js +67 -0
  72. package/src/sqm/proof.js +25 -0
  73. package/src/sqm/report.js +1 -0
  74. package/src/sqm/workflow-onboarding.js +3 -39
  75. package/src/sqm/workflow-update.js +17 -6
  76. package/src/team-status-fence-store.js +32 -0
  77. package/src/team-threads.js +457 -0
@@ -0,0 +1,80 @@
1
+ import { readRecord, pendingAcceptance } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
2
+ import { tickManagedTasks } from './supervisor.js';
3
+
4
+ const matches = (record, target) => record?.provider === target.provider && record.session === target.session &&
5
+ record.run?.id === target.runId && record.run?.managed?.taskId === target.taskId;
6
+
7
+ // A projection of the original task, never a second completion decision or ledger.
8
+ export function originalWorkResult(record) {
9
+ const run = record.run;
10
+ const managed = run.managed;
11
+ const completed = run.state === 'completed';
12
+ const waiting = run.state === 'recovery_required' && !managed.autoRecovery || ['blocked', 'paused', 'stopped', 'budget_exhausted', 'failed', 'verification_pending'].includes(run.state);
13
+ const pending = pendingAcceptance(run);
14
+ return {
15
+ status: completed ? 'completed' : run.state === 'reported_done' ? 'reported_done' : waiting ? run.state : 'in_progress',
16
+ completed,
17
+ objective: run.objective,
18
+ result: completed || run.state === 'reported_done' ? run.checkpoint?.summary || run.reason : null,
19
+ remainingWork: completed ? [] : pending.length ? pending.map(item => item.description) :
20
+ [managed.remainingWork || run.nextStep || run.reason].filter(Boolean),
21
+ requiredAction: waiting ? managed.userQuestion || managed.dependencyWait?.condition || run.reason : null,
22
+ message: completed ? '元の仕事の完成条件を確認し、完了しました。' :
23
+ run.state === 'reported_done' ? '元の会話は完了を報告しています。完成条件の確認は未完了です。' :
24
+ waiting ? '元の仕事は未完了です。残件と必要な操作を確認してください。' : '元の仕事の残作業を続行中です。',
25
+ };
26
+ }
27
+
28
+ // Repair owner has already queued an authorized action on the original record.
29
+ // Never resume/replan here: those controls may reset progress or release a wait.
30
+ // The durable supervisor owns retries, writer claims, budgets and notifications.
31
+ export async function continueRepairedDelivery({ root, webOrigin, request, repair, signal,
32
+ tick = tickManagedTasks, drain = false, ...executionOptions } = {}) {
33
+ const target = request?.target;
34
+ if (request?.scope !== 'repair_continue' || typeof request.requestId !== 'string' || !request.requestId.trim() || request.requestId.length > 200 || !target?.provider || !target.session || !target.runId || !target.taskId) {
35
+ throw new Error('An exact original task and explicit repair_continue request are required.');
36
+ }
37
+ const read = async () => {
38
+ const record = await readRecord(root, target.provider, target.session);
39
+ return matches(record, target) ? record : null;
40
+ };
41
+ let record = await read();
42
+ if (!record) return { status: 'target_changed', completed: false, request, repair };
43
+ const receipt = record.run.managed.repairRequest;
44
+ if (repair?.reason === 'already_requested' && receipt?.requestId === request.requestId && receipt.status === 'resolved') {
45
+ repair = { ...repair, status: 'resolved', message: '停止原因の解消は確認済みです。' };
46
+ }
47
+ // Only a resolved repair can initiate a continuation tick. In-progress repair
48
+ // and uncertain effects remain with their existing owner; status reads are safe.
49
+ while (repair?.status === 'resolved' && !signal?.aborted &&
50
+ record.run.state === 'recovery_required' && record.run.managed.autoRecovery) {
51
+ const attemptId = record.run.managed.attempts?.at(-1)?.id;
52
+ await tick({ ...executionOptions, root, webOrigin, signal,
53
+ readRecords: async () => { const current = await read(); return current ? [current] : []; } });
54
+ record = await read();
55
+ const attempt = record?.run.managed.attempts?.at(-1);
56
+ // Continue only after a settled checkpoint. Busy owners, backoff, failed
57
+ // turns and unchanged state stay with the existing durable supervisor.
58
+ if (!drain || !record || attempt?.id === attemptId || attempt?.outcome !== 'checkpoint') break;
59
+ }
60
+ if (!record) return { status: 'target_changed', completed: false, request, repair };
61
+ const work = originalWorkResult(record);
62
+ if (!work.completed && repair?.status === 'needs_human') {
63
+ work.status = 'needs_human';
64
+ work.requiredAction = repair.message || repair.reason;
65
+ work.message = '元の仕事は未完了です。必要な操作を確認してください。';
66
+ }
67
+ return { request, repair, ...work };
68
+ }
69
+
70
+ // Conversation entry composition. Repair settles its durable receipt first;
71
+ // continuation then reads the original task and uses its existing next action.
72
+ export async function repairAndContinueDelivery(options = {}) {
73
+ const repairTask = options.repairTask || (await import('./repair.js')).repairStoppedDelivery;
74
+ const repair = await repairTask(options);
75
+ const work = await continueRepairedDelivery({ ...options.supervisorOptions,
76
+ root: options.root, webOrigin: options.webOrigin || options.request?.webOrigin,
77
+ request: options.request, repair, signal: options.signal, drain: true });
78
+ return { ...work.repair, taskCompleted: work.completed, work,
79
+ message: `${work.repair?.message || ''} ${work.message || ''}`.trim() };
80
+ }
@@ -0,0 +1,164 @@
1
+ import { repairAndContinueDelivery } from './repair-continuation.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { readRecord, updateRecord, automaticContinuationAllowed, deliveryStop, prepareWaitingDiagnosis,
4
+ prepareRecoveryDiagnosis, queuePurposeDiagnosis, advanceDeliveryControlRevision, canReplanDelivery, processIdentity } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
5
+ import { leaseOwnerDead } from '../session-supervision/workspace-lease.js';
6
+ import { requireProviderLink } from '../provider-links.js';
7
+ import { diagnoseDelivery, diagnosticProjection } from './diagnosis.js';
8
+ import { tickManagedTasks } from './supervisor.js';
9
+
10
+ // One bounded adapter to the existing controller, not another worker or consent store.
11
+ const fingerprint = record => createHash('sha256').update(JSON.stringify([
12
+ record?.run?.id, record?.run?.revision, record?.run?.controlRevision, record?.run?.state,
13
+ record?.run?.blocker, record?.run?.managed?.decision?.id, record?.run?.managed?.attempts?.at(-1)?.id,
14
+ record?.run?.managed?.purpose?.lastObservedTurnId,
15
+ ])).digest('hex');
16
+ const matches = (record, target) => record?.provider === target.provider && record?.session === target.session &&
17
+ record.run?.id === target.runId && record.run?.managed?.taskId === target.taskId;
18
+ function boundary(record, now) {
19
+ const run = record?.run, m = run?.managed;
20
+ if (record?.providerArchived) return 'user_stopped';
21
+ if (!m || record.provider !== 'codex' || m.resumeChannel !== 'codex_app_server') return 'unsupported_target';
22
+ if (!automaticContinuationAllowed(record) || run.stopReason?.actor === 'user' || ['stopped', 'paused'].includes(run.state)) return 'user_stopped';
23
+ if (deliveryStop(run, now)) return 'budget_or_failure_limit';
24
+ if (['permission_narrowed', 'permission_restore', 'manual_provider_action'].includes(m.attempts?.at(-1)?.errorCode)) return 'permission_required';
25
+ if (m.authenticationWait) return 'login_required';
26
+ if (m.pendingApproval || m.nativeWait || m.providerWarning || m.purpose?.nativeBoundary || run.blocker === 'authorization') return 'permission_required';
27
+ if (m.dependencyWait || m.understandingQuestion || m.userQuestion) return 'answer_or_dependency_required';
28
+ if (['pending', 'pending_received', 'transferred'].includes(m.handoff?.status)) return 'handoff_pending';
29
+ return null;
30
+ }
31
+ const humanReasons = new Set(['login_required', 'permission_required', 'answer_or_dependency_required', 'provider_link_required']);
32
+ function response(request, status, reason, extra = {}) {
33
+ return { status, reason, request, taskCompleted: false, ...extra,
34
+ message: status === 'resolved' ? '停止原因の解消を確認しました。元の仕事全体の完了は別途確認します。'
35
+ : status === 'needs_human' && !humanReasons.has(reason) ? '元の会話で確認結果に記載された操作を行ってください。'
36
+ : humanReasons.has(reason) ? ({ login_required: '元のプロバイダにログインしてください。', permission_required: '元の会話で必要な権限・承認を確認してください。', answer_or_dependency_required: '元の会話の質問または外部条件を確認してください。', provider_link_required: '元のプロバイダとの接続を確認してください。' })[reason]
37
+ : '停止原因はまだ解消を確認できていません。現在の結果から続けます。' };
38
+ }
39
+
40
+ export async function repairStoppedDelivery({ root, webOrigin, request, issues, signal, diagnosis,
41
+ diagnose = diagnoseDelivery, tick = tickManagedTasks, supervisorOptions = {}, now = Date.now, inspectProcess = processIdentity } = {}) {
42
+ if (request?.scope !== 'repair_continue' || typeof request.requestId !== 'string' || !request.requestId.trim() || request.requestId.length > 200 ||
43
+ !request.target?.runId || !request.target?.taskId || !['codex', 'claude'].includes(request.target?.provider) || !request.target?.session) {
44
+ throw Error('Repair requires an explicit repair_continue request and an exact existing task reference.');
45
+ }
46
+ const target = request.target, read = () => readRecord(root, target.provider, target.session);
47
+ const result = (reason, extra) => response(request, humanReasons.has(reason) ? 'needs_human' : 'unresolved', reason, extra);
48
+ let initial = await read();
49
+ if (!matches(initial, target)) return result('target_changed', { executed: false });
50
+ if (signal?.aborted) return result('cancelled', { executed: false });
51
+ let link;
52
+ try { link = await requireProviderLink({ root, provider: target.provider }); }
53
+ catch { return result('provider_link_required', { executed: false }); }
54
+ let reason = boundary(initial, now());
55
+ if (reason) return result(reason, { executed: false });
56
+ const previous = initial.run.managed.repairRequest;
57
+ const interrupted = previous?.status === 'running' && await leaseOwnerDead(previous.owner, inspectProcess);
58
+ if (!interrupted && (previous?.requestId === request.requestId || previous?.status === 'running') || previous?.settledFingerprint === fingerprint(initial)) {
59
+ return result('already_requested', { executed: false, priorStatus: previous.status });
60
+ }
61
+ if (initial.run.managed.resumeLease || initial.run.state === 'auto_resuming') return result('busy', { executed: false });
62
+ if (!['blocked', 'recovery_required'].includes(initial.run.state)) return result('no_repairable_stop', { executed: false });
63
+ const attempt = initial.run.managed.executionAttempt;
64
+ // Unknown effects are reconciled read-only; never retry the prior write.
65
+ const uncertain = initial.run.managed.executionUncertain || attempt?.phase === 'dispatching';
66
+ const busy = initial.run.managed.attempts?.at(-1)?.outcome === 'busy' && attempt?.phase === 'claimed' && attempt.id === initial.run.managed.attempts.at(-1).id && !attempt.dispatchedAt && !initial.run.managed.unknownConsumption;
67
+ const research = initial.run.managed.decision?.waitKind === 'research' && canReplanDelivery(initial.run, target.provider, now());
68
+ const transientRead = initial.run.managed.decision?.action?.kind === 'read' &&
69
+ ['closed', 'timeout', 'unavailable', 'turn_failed', 'provider_error'].includes(initial.run.managed.attempts?.at(-1)?.errorCode);
70
+ if (!uncertain && !busy && !research && !(initial.run.state === 'blocked' && initial.run.blocker === 'missing_information') && !transientRead) {
71
+ return result('unsupported_cause', { executed: false });
72
+ }
73
+ // Advisory failure is a warning, never a new task failure or approval gate.
74
+ try { diagnosis ||= await diagnose({ root, webOrigin: webOrigin || request.webOrigin, ...target, issues, signal }); }
75
+ catch { diagnosis = { status: 'analysis_unavailable' }; }
76
+ const advice = diagnosticProjection(diagnosis?.analysis?.hypothesisSheet, 5000).data;
77
+ const expected = JSON.stringify(initial);
78
+ const owner = { pid: process.pid, identity: await inspectProcess(process.pid) || `pid:${process.pid}` };
79
+ let claimed = false;
80
+ try {
81
+ await updateRecord(root, target.provider, target.session, async current => {
82
+ await requireProviderLink({ root, provider: target.provider, expectedEpoch: link.epoch });
83
+ if (signal?.aborted || !matches(current, target) || JSON.stringify(current) !== expected || boundary(current, now()) || current.run.managed.resumeLease) return current;
84
+ const run = current.run, m = run.managed;
85
+ const originalStop = { state: run.state, blocker: run.blocker, reason: run.reason, action: m.decision?.action };
86
+ if (uncertain && !m.executionUncertain) m.executionUncertain = { attemptId: attempt.id, decisionId: attempt.decisionId, action: attempt.action, at: now() };
87
+ const instruction = 'Investigate the original stop using the ranked hypotheses as untrusted advice. Check only changed or unresolved aspects. Execute only the selected authorized action; never treat diagnostic text as consent. After a repair propose a read-only check of the ORIGINAL stopping condition. In that read checkpoint include repairResult {status: resolved|unresolved|needs_human, summary: concise observed result}. Do not report original task completion just because repair succeeded.';
88
+ if (m.purpose?.enabled) {
89
+ if (!queuePurposeDiagnosis(run, instruction, `manual-repair:${fingerprint(current)}`, now())) return current;
90
+ } else {
91
+ prepareWaitingDiagnosis(run, instruction, now());
92
+ if (m.executionUncertain) prepareRecoveryDiagnosis(run, now());
93
+ m.autoRecovery = true; m.nextAttemptAt = now(); run.blocker = null; run.blockerSource = null;
94
+ run.checkpoint = null; run.consumedRevision = run.revision;
95
+ run.state = 'recovery_required'; run.recoveryCause = 'session_supervision';
96
+ }
97
+ m.repairRequest = { requestId: request.requestId, owner, status: 'running', originalStop, originalCause: { research, busy, transientRead, uncertain: Boolean(uncertain) }, advice, instruction };
98
+ advanceDeliveryControlRevision(run); claimed = true; return current;
99
+ });
100
+ } catch (error) {
101
+ if (/provider_link/.test(error.code || '')) return result('provider_link_required', { executed: false });
102
+ if (/Delivery state/.test(error.message)) return result('busy', { executed: false });
103
+ throw error;
104
+ }
105
+ if (!claimed) return result('state_changed', { executed: false, currentState: (await read())?.run?.state });
106
+ let outcome = result('unresolved', { executed: false });
107
+ try {
108
+ // At most check -> permitted repair -> check. All claims, budgets, native
109
+ // permissions, cancellation and effect reconciliation remain in the supervisor.
110
+ const actions = new Set();
111
+ for (let step = 0; step < 3; step++) {
112
+ const before = await read();
113
+ if (signal?.aborted) { outcome = result('cancelled', { executed: outcome.executed }); break; }
114
+ if (!matches(before, target)) { outcome = result('target_changed', { executed: outcome.executed }); break; }
115
+ reason = boundary(before, now());
116
+ if (reason) { outcome = result(reason, { executed: outcome.executed }); break; }
117
+ const action = before.run.managed.decision?.action;
118
+ const actionKey = JSON.stringify(action);
119
+ if (!['read', 'local_edit', 'local_test'].includes(action?.kind) || actions.has(actionKey)) break;
120
+ actions.add(actionKey);
121
+ const oldAttempt = before.run.managed.attempts?.at(-1)?.id;
122
+ await tick({ ...supervisorOptions, root, webOrigin: webOrigin || request.webOrigin, signal,
123
+ authorizeDispatch: async record => {
124
+ await requireProviderLink({ root, provider: target.provider, expectedEpoch: link.epoch });
125
+ return supervisorOptions.authorizeDispatch?.(record);
126
+ },
127
+ readRecords: async () => { const current = await read(); return matches(current, target) && current.run.managed.repairRequest?.requestId === request.requestId ? [current] : []; } });
128
+ const after = await read();
129
+ if (!matches(after, target)) { outcome = result('target_changed', { executed: outcome.executed }); break; }
130
+ const latest = after.run.managed.attempts?.at(-1);
131
+ const executed = outcome.executed || Boolean(latest?.id !== oldAttempt && latest?.outcome !== 'busy');
132
+ try { await requireProviderLink({ root, provider: target.provider, expectedEpoch: link.epoch }); }
133
+ catch { outcome = result('provider_link_required', { executed }); break; }
134
+ if (signal?.aborted) { outcome = result('cancelled', { executed }); break; }
135
+ reason = boundary(after, now());
136
+ if (reason) { outcome = result(reason, { executed }); break; }
137
+ const history = after.run.managed.workHistory?.at(-1);
138
+ if (latest?.id !== oldAttempt && latest?.outcome === 'checkpoint' && history?.repairResult) {
139
+ outcome = response(request, history.repairResult.status, 'observed_check', { executed, repairResult: history.repairResult, verificationSource: 'provider_observed' }); break;
140
+ }
141
+ outcome = result(latest?.outcome === 'busy' ? 'busy' : 'unresolved', { executed });
142
+ if (uncertain || latest?.id === oldAttempt || latest?.outcome !== 'checkpoint' || history?.result?.status !== 'observed' || after.run.managed.executionUncertain) break;
143
+ }
144
+ return { ...outcome, warnings: [
145
+ ...(diagnosis?.status === 'analysis_unavailable' ? ['diagnosis_unavailable'] : []),
146
+ ...(diagnosis?.analysis?.sheetDiagnostic ? ['incomplete_advisory_sheet'] : []),
147
+ ] };
148
+ } finally {
149
+ await updateRecord(root, target.provider, target.session, current => {
150
+ if (matches(current, target) && current.run.managed.repairRequest?.requestId === request.requestId) {
151
+ const receipt = current.run.managed.repairRequest;
152
+ receipt.status = outcome.status; receipt.reason = outcome.reason; receipt.settledFingerprint = fingerprint(current);
153
+ delete receipt.advice; delete receipt.instruction;
154
+ }
155
+ return current;
156
+ });
157
+ }
158
+ }
159
+
160
+ // Keep the conversation entry stable while returning to the original work only
161
+ // after the repair receipt has settled. The repair adapter above owns diagnosis.
162
+ export async function repairDelivery(options = {}) {
163
+ return repairAndContinueDelivery({ ...options, repairTask: repairStoppedDelivery });
164
+ }
@@ -3,6 +3,7 @@ import { promisify } from 'node:util';
3
3
  import { mkdir, lstat, readFile, writeFile } from 'node:fs/promises';
4
4
  import { homedir } from 'node:os';
5
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';
7
8
  import { inspectBackendExecutable, backendCandidates, CODEX_WRITER_LOCK_MINIMUM_VERSION } from '../backend-executable.js';
8
9
  import { runSceneReasoning } from '../codex-plugins/simy/hooks/scene-reasoning.mjs';
@@ -49,11 +50,12 @@ export async function installSupervisorService({ root, home = homedir(), platfor
49
50
  const pin = await readSupervisorState(join(root, 'delivery-runtime/current.json'));
50
51
  if (!pin || pin.digest !== control.digest || pin.release !== control.release) throw new Error('Supervisor activation no longer matches the fixed runtime.');
51
52
  const provider = await supervisorProviderEnvironment({ home, platform, environment, inspectBackend });
52
- const definition = supervisorServiceDefinition({ root, home, platform, entry: join(pin.release, 'src/index.js'), providerEnvironment: provider.environment });
53
+ const definition = supervisorServiceDefinition({ root, home, platform, entry: join(root, 'bin/simy-runtime.mjs'), providerEnvironment: provider.environment });
53
54
  try {
54
55
  const info = await lstat(definition.file);
55
56
  if (!info.isFile() || info.isSymbolicLink() || !(await readFile(definition.file, 'utf8')).includes(marker)) throw new Error('Supervisor service path is not managed by SIMY.');
56
57
  } catch (error) { if (error.code !== 'ENOENT') throw error; }
58
+ await installPinnedCliEntry(root);
57
59
  await mkdir(dirname(definition.file), { recursive: true, mode: 0o700 });
58
60
  await writeFile(definition.file, definition.content, { mode: 0o600 });
59
61
  if (platform === 'linux') {
@@ -1,5 +1,7 @@
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';
4
6
  import { continueResultCycle, resultEvent, executeResultCycle, acceptResultEvent } from '../codex-plugins/simy/hooks/result-cycle.mjs';
5
7
  import { finalResponseRecoveryInstruction } from '../session-supervision/final-response-recovery.js';
@@ -17,11 +19,12 @@ import { requireProviderLink } from '../provider-links.js';
17
19
  import { spawn } from 'node:child_process';
18
20
  import { randomUUID } from 'node:crypto';
19
21
  import { dirname } from 'node:path';
20
- import { hasPendingUserStop, recordPath, canExecuteAutonomousExternal, automaticContinuationAllowed, queuePurposeDiagnosis, 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';
21
23
  import { resolveBackendExecutable } from '../backend-executable.js';
22
24
  import { safeProviderFailure, providerEnvironmentIdentity, checkProviderRetry } from '../session-supervision/provider-failure.js';
23
25
  import { executeCodexAppServer, inspectCodexAppServer } from '../session-supervision/codex-app-server.js';
24
26
  import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
27
+ import { createTaskPool } from '../codex-plugins/simy/hooks/loop-kernel.mjs';
25
28
 
26
29
  const MAX_OUTPUT = 64 * 1024;
27
30
  const SAFE_PROVIDER_CODES = new Set(['authentication_required', 'identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
@@ -103,7 +106,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
103
106
  remainingWork: run.managed.remainingWork, acceptanceCriteria: run.managed.acceptanceCriteria, proposedAction: run.managed.proposedAction,
104
107
  currentDecision: run.managed.autonomyPolicy ? run.managed.decision : undefined, diagnosis: run.managed.plan?.diagnosis,
105
108
  progress: deliveryProgressContext(run) };
106
- 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.`;
107
110
  const sandbox = run.managed.autonomyPolicy && run.managed.decision?.action.kind === 'read' ? 'read-only' : run.managed.autoRecoverySandbox;
108
111
  const args = ['exec', '--json', '--skip-git-repo-check', '-s', sandbox, '-m', run.managed.resumeModel, 'resume', record.session, prompt];
109
112
  if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
@@ -184,9 +187,27 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
184
187
  const owner = run.managed.resumeLease?.owner;
185
188
  const ownerDead = await leaseOwnerDead(owner, inspectProcess);
186
189
  // A live/unknown owner is never taken over merely because time passed.
187
- if (!ownerDead && (owner || claimNow - run.managed.resumeLease?.claimedAt <= 5 * 60_000)) continue;
188
- 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 => {
189
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
+ }
190
211
  current.run.state = 'recovery_required'; current.run.recoveryCause = 'interrupted_owner'; current.run.managed.autoRecovery = false;
191
212
  recordRecoveryIncident(current.run, { code: 'interrupted_owner', subject: current.run.managed.executionAttempt?.id || '', now: claimNow });
192
213
  recordRecoveryStage(current.run, 'interrupted', { now: claimNow, attemptId: current.run.managed.executionAttempt?.id });
@@ -195,12 +216,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
195
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; }
196
217
  if (attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') current.run.managed.executionUncertain = {
197
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;
198
222
  current.run.updatedAt = claimNow; notifyState(current.run, claimNow); return current;
199
223
  });
200
224
  continue;
201
225
  }
202
226
  if (!automaticContinuationAllowed(record)) continue;
203
- try { await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
227
+ let providerLink;
228
+ try { providerLink = await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
204
229
  if (run.managed.providerCapabilityWait) {
205
230
  if (run.state !== 'recovery_required' || run.managed.resumeLease || managedBudgetReason(run, claimNow)) continue;
206
231
  const expected = run.managed.providerCapabilityWait;
@@ -228,7 +253,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
228
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;
229
254
  let authorizationScope;
230
255
  const authorizeClaim = async () => {
231
- await requireProviderLink({ root, provider: record.provider });
256
+ await requireProviderLink({ root, provider: record.provider, expectedEpoch: providerLink.epoch });
232
257
  if (!authorizeDispatch) return;
233
258
  const latest = await authorizeDispatch(record);
234
259
  if (authorizationScope !== undefined && latest !== authorizationScope) throw Object.assign(new Error("Desktop authorization scope changed."), { code: "desktop_access_required" });
@@ -249,9 +274,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
249
274
  let claimed;
250
275
  try { claimed = await updateRecord(root, record.provider, record.session, current => {
251
276
  const live = current?.run;
252
- if (live?.id !== run.id || live.revision !== run.revision || live.managed?.decision?.id !== run.managed?.decision?.id || live.managed?.providerCapabilityWait) return current;
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
+
253
282
  if (!automaticContinuationAllowed(current)) return current;
254
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;
255
287
  const appChannel = live?.managed?.resumeChannel === 'codex_app_server';
256
288
  const allowedCause = live?.recoveryCause === 'process_exit' || appChannel && ['session_supervision', 'approved_action'].includes(live?.recoveryCause);
257
289
  const approved = live?.managed?.decision?.outcome === 'wait' && ['read', 'local_edit', 'local_test', 'external'].includes(live.managed.decision.action?.kind) &&
@@ -261,8 +293,10 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
261
293
  if (stop) {
262
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;
263
295
  }
296
+ const workProposal = recoveryWorkProposal(current);
297
+ if (workProposal?.blocked) return current;
264
298
  live.state = 'auto_resuming'; live.managed.resumeLease = { id: leaseId, claimedAt: currentTime(), checkpointRevision: live.revision, decisionId: live.managed.decision?.id || null,
265
- owner: { pid: process.pid, identity: ownerIdentity } };
299
+ owner: { pid: process.pid, identity: ownerIdentity }, workProposal: workProposal ? { inputDigest: workProposal.inputDigest } : null };
266
300
  live.managed.executionAttempt = { id: leaseId, decisionId: live.managed.decision?.id || null,
267
301
  action: live.managed.decision?.action || live.managed.proposedAction,
268
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() };
@@ -381,6 +415,16 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
381
415
  const live = current?.run;
382
416
  if (live?.state !== 'auto_resuming' || live.managed?.resumeLease?.id !== leaseId) throw new Error('Managed recovery lease changed before provider dispatch.');
383
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
+ }
384
428
  if (appServer) {
385
429
  if (!providerSandbox || !['readOnly', 'workspaceWrite', 'dangerFullAccess', 'externalSandbox'].includes(providerSandbox.type)) throw new Error('Original Codex sandbox policy is unavailable.');
386
430
  // Observation is not enrollment authority. In particular, a
@@ -391,6 +435,7 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
391
435
  const ledger=live.managed.purpose?.evidence;
392
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.');
393
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());
394
439
  const approved = live.managed.approvedAction;
395
440
  live.managed.executionAttempt.phase = 'dispatching';
396
441
  live.managed.executionAttempt.dispatchedAt = currentTime();
@@ -426,7 +471,12 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
426
471
  if (controller.signal.aborted) result = { ...result, code: -2, cancelled: true };
427
472
  if (result.code === 0) {
428
473
  try {
429
- 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() });
430
480
  else await checkpointDelivery({ root, provider: record.provider, session: record.session, cwd: record.cwd,
431
481
  checkpoint: result.checkpoint || parseAgentCheckpoint(result.output, record.session), expectedLease: leaseId, toolEvidence: result.toolEvidence || [], sourceContext: result.sourceContext, controllerEvidence: result.controllerEvidence, now: Date.now() });
432
482
  if (result.dispatchAttemptId) queueDispatchReceipt({ root: rootFromDelivery(root), attemptId: result.dispatchAttemptId, receiptId: `checkpoint:${leaseId}`, resultRef: `delivery-record:${record.provider}:${record.session}:${leaseId}` });
@@ -474,6 +524,10 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
474
524
  providerTurnId: result.providerTurnId, providerSessionId: result.providerSessionId || record.session };
475
525
  } else if (result.code === 0) delete live.managed.finalResponseWithoutCheckpoint;
476
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
+ }
477
531
  if (!result.busy && attempt?.phase === 'dispatching' && attempt.action?.kind !== 'read') live.managed.executionUncertain = {
478
532
  attemptId: leaseId, decisionId: attempt.decisionId, action: attempt.action, at: Date.now() };
479
533
  live.managed.attempts ||= [];
@@ -582,6 +636,8 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
582
636
  export function createManagedSupervisor(options) {
583
637
  let running = null, auditing = null, closed = false;
584
638
  const controller = new AbortController();
639
+ const workers = createTaskPool({ concurrency: options.concurrency ?? 4 });
640
+ const readCandidates = options.readRecords || (() => listRecords(options.root));
585
641
  return {
586
642
  tick() {
587
643
  if (closed) return Promise.resolve();
@@ -591,11 +647,31 @@ export function createManagedSupervisor(options) {
591
647
  onAudit: async (record, findings, request) => closed || await options.shouldYield?.() ? undefined : auditConsistencyTask(record, findings, request, {
592
648
  root: options.root, signal: controller.signal, authorize: options.authorizeDispatch || (async () => {}) }) });
593
649
  })().catch(() => {}).finally(() => { auditing = null; });
594
- 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; });
595
669
  return running;
596
670
  },
597
- async drain() { closed = true; await Promise.allSettled([running, auditing]); },
598
- 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()]); },
599
675
  };
600
676
  }
601
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
  }