@awak-app/simy-cli 0.7.16 → 0.7.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -26
- package/package.json +3 -2
- package/src/agent.js +128 -7
- package/src/bounded-local-task-subtask-pool.js +32 -6
- package/src/bounded-local-task-subtasks-contract.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +2 -2
- package/src/codex-plugins/simy/hooks/README.md +8 -3
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +261 -67
- package/src/codex-plugins/simy/hooks/hooks.json +1 -1
- package/src/codex-plugins/simy/hooks/loop-kernel/conformance.json +136 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/index.mjs +42 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/outcome-review.mjs +31 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/pin.json +6 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/policy.json +141 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/task-pool.mjs +34 -0
- package/src/codex-plugins/simy/hooks/loop-kernel/work-frontier.mjs +132 -0
- package/src/codex-plugins/simy/hooks/loop-kernel.mjs +6 -0
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +15 -3
- package/src/codex-plugins/simy/hooks/purpose-completion-contract.mjs +10 -0
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +8 -6
- package/src/codex-plugins/simy/hooks/result-cycle.mjs +32 -6
- package/src/codex-plugins/simy/hooks/work-frontier.mjs +2 -0
- package/src/codex-plugins/simy/skills/diagnose/SKILL.md +10 -4
- package/src/coding-run-recovery.js +4 -2
- package/src/delivery-hooks/claude-workflow.md +10 -7
- package/src/delivery-hooks/command.js +45 -10
- package/src/delivery-hooks/consult.js +60 -0
- package/src/delivery-hooks/diagnosis.js +2 -2
- package/src/delivery-hooks/legacy-hook-routing.js +46 -8
- package/src/delivery-hooks/local-runtime.js +50 -1
- package/src/delivery-hooks/local-supervisor.js +1 -1
- package/src/delivery-hooks/repair-continuation.js +80 -0
- package/src/delivery-hooks/repair.js +164 -0
- package/src/delivery-hooks/supervisor-service.js +3 -1
- package/src/delivery-hooks/supervisor.js +88 -12
- package/src/desktop-access.js +2 -1
- package/src/desktop-host.js +62 -2
- package/src/durable-local-task-worker.js +372 -58
- package/src/execution-capability-contract.js +15 -1
- package/src/index.js +3 -3
- package/src/local-photos-read.swift +33 -0
- package/src/local-photos.js +47 -0
- package/src/local-task-attachment-store.js +31 -0
- package/src/local-task-destination.js +174 -0
- package/src/local-task.js +119 -7
- package/src/native-delegation-contract.js +68 -0
- package/src/native-provider-identity.js +60 -0
- package/src/orchestrator/budget.js +22 -11
- package/src/orchestrator/independent-audit.js +9 -18
- package/src/orchestrator/loop.js +27 -59
- package/src/orchestrator/recovery.js +32 -2
- package/src/provider-delegation.js +336 -0
- package/src/runner.js +2 -1
- package/src/session-supervision/artifact-observer.js +138 -0
- package/src/session-supervision/claude-sessions.js +5 -3
- package/src/session-supervision/codex-app-server.js +20 -5
- package/src/session-supervision/completion-artifacts.js +44 -0
- package/src/session-supervision/consistency-auditor.js +8 -11
- package/src/session-supervision/continuation-analysis.js +46 -28
- package/src/session-supervision/discovery-window.js +1 -1
- package/src/session-supervision/final-response-recovery.js +5 -3
- package/src/session-supervision/index.js +25 -8
- package/src/session-supervision/purpose-recovery.js +12 -3
- package/src/session-supervision/purpose-verification.js +9 -57
- package/src/session-supervision/recovery-frontier.js +55 -0
- package/src/session-supervision/remote-hosts.js +1 -1
- package/src/session-supervision/stop-notifications.js +8 -8
- package/src/sqm/agent-skill.js +1 -1
- package/src/sqm/checkers.js +12 -6
- package/src/sqm/claude-plugin-update.js +22 -8
- package/src/sqm/claude-plugin.js +4 -0
- package/src/sqm/local-tests.js +2 -2
- package/src/sqm/plugin-cache.js +67 -0
- package/src/sqm/proof.js +25 -0
- package/src/sqm/report.js +1 -0
- package/src/sqm/workflow-onboarding.js +3 -39
- package/src/sqm/workflow-update.js +17 -6
- package/src/team-status-fence-store.js +32 -0
- package/src/team-threads.js +478 -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(
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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 =
|
|
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
|
-
|
|
598
|
-
async
|
|
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
|
|
package/src/desktop-access.js
CHANGED
|
@@ -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' });
|