@awak-app/simy-cli 0.7.46 → 0.7.48
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/package.json +2 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.mcp.json +11 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +5 -3
- package/src/codex-plugins/simy/native/app-tools.mjs +45 -0
- package/src/codex-plugins/simy/native/bridge.mjs +156 -0
- package/src/codex-plugins/simy/native/guidance-intent.mjs +30 -0
- package/src/codex-plugins/simy/native/launch +36 -0
- package/src/codex-plugins/simy/native/server.mjs +26 -0
- package/src/codex-plugins/simy/native/submission-receipt.mjs +9 -0
- package/src/delivery-hooks/supervisor.js +28 -3
- package/src/desktop-host.js +185 -13
- package/src/session-supervision/claude-sessions.js +25 -14
- package/src/session-supervision/claude-stream-owner.js +266 -0
- package/src/session-supervision/claude-stream-presentation.js +27 -0
- package/src/session-supervision/claude-stream-transport.js +188 -0
- package/src/session-supervision/codex-app-server.js +30 -8
- package/src/session-supervision/codex-app-tools-connection.js +30 -0
- package/src/session-supervision/conversation-presentation.js +2 -0
- package/src/session-supervision/index.js +5 -1
- package/src/session-supervision/native-handoff.js +69 -13
- package/src/session-supervision/native-turn-observer.js +196 -0
- package/src/session-supervision/purpose-verification.js +18 -7
- package/src/session-supervision/session-guidance.js +160 -0
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/review-worker.js +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.48",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
|
|
44
44
|
"check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
|
|
45
45
|
"check:sqm": "node --check ./src/sqm/provider-plugin-repair.js && node --check ./src/sqm/provider-plugin-status.js && node --check ./src/sqm/advisory-guidance.js && node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
|
46
|
-
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/claude-delivery-fault-live-e2e.mjs && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js && node --check ./scripts/codex-plugin-runtime-e2e.js",
|
|
46
|
+
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/claude-stream-acceptance.mjs && node --check ./scripts/claude-delivery-fault-live-e2e.mjs && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js && node --check ./scripts/codex-plugin-runtime-e2e.js",
|
|
47
47
|
"check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/connection-diagnostics.js && node --check ./src/launch-authorization.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/delivery-hooks/recovery.js && node --check ./src/delivery-hooks/supervisor-owner.js && node --check ./src/delivery-hooks/supervisor-watchdog.js && node --check ./src/delivery-hooks/supervisor-service.js && node --check ./scripts/supervisor-watchdog-live-e2e.js && node --check ./src/codex-plugins/simy/hooks/recovery-incident.mjs && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
|
|
48
48
|
"check:package": "node ./scripts/check-package-contents.js",
|
|
49
49
|
"build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
|
package/src/cli-contract.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./durable-local-task-steps-contract.js";
|
|
15
15
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
16
16
|
|
|
17
|
-
export const CLI_VERSION = "0.7.
|
|
17
|
+
export const CLI_VERSION = "0.7.48";
|
|
18
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
19
19
|
|
|
20
20
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"simy_native": {
|
|
4
|
+
"command": "./native/launch",
|
|
5
|
+
"args": ["./native/server.mjs"],
|
|
6
|
+
"cwd": ".",
|
|
7
|
+
"env_vars": ["CODEX_APP_TOOLS_PIPE_PATH", "CODEX_MCP_NODE_PATH", "CODEX_BROWSER_USE_NODE_PATH", "CODEX_ELECTRON_RESOURCES_PATH", "CODEX_CLI_PATH", "SIMY_HOME", "HOME", "PATH"],
|
|
8
|
+
"startup_timeout_sec": 10
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {guidancePending} from '../native/guidance-intent.mjs';
|
|
2
3
|
import { decideLoop, compactWorkHistory } from './loop-kernel.mjs';
|
|
3
4
|
import { completionContractIdentity } from './purpose-completion-contract.mjs';
|
|
4
5
|
import { captureDispatchProvenance } from './update-dispatch-identity.mjs';
|
|
@@ -1674,10 +1675,10 @@ export function externalReviewIsCurrent(managed,review,now) {
|
|
|
1674
1675
|
const refs=[...(review.criteria||[]).filter(row=>row.passed),...(review.rules||[]).filter(row=>row.status==='satisfied'),...(review.workUnits||[])].flatMap(row=>row.evidenceRefs||[]).filter(link=>/^[a-f0-9]{64}$/.test(link.ref));
|
|
1675
1676
|
// A flexible semantic verdict refers to its actual inspection, including
|
|
1676
1677
|
// external observations even when no authored citation was supplied.
|
|
1677
|
-
if (review.semanticInspection?.source
|
|
1678
|
+
if (['controller_actual_read_set', 'controller_conversation_answer'].includes(review.semanticInspection?.source)) refs.push(...(review.semanticInspection.observationRefs || []).map(ref => ({ ref })));
|
|
1678
1679
|
const ledger=managed.purpose.evidence;
|
|
1679
1680
|
const observations=[...(ledger?.observations||[]),...(ledger?.reviewObservationActionId===(review.decisionId||managed.decision.id) ? ledger.reviewObservations||[] : [])];
|
|
1680
|
-
const inspected = new Set(review.semanticInspection?.source
|
|
1681
|
+
const inspected = new Set(['controller_actual_read_set', 'controller_conversation_answer'].includes(review.semanticInspection?.source) ? review.semanticInspection.observationRefs || [] : []);
|
|
1681
1682
|
return refs.filter(link => inspected.has(link.ref) || observations.some(item => item.id === link.ref))
|
|
1682
1683
|
.every(link=>observations.some(item=>item.id===link.ref && item.status==='observed' && currentPurposeObservation(item,now,review.observationWindow)));
|
|
1683
1684
|
|
|
@@ -1799,7 +1800,7 @@ export async function settlePurposeVerification({ root, provider, session, expec
|
|
|
1799
1800
|
} else if (pending.length && pending.every(c => c.check.type === 'manual')) {
|
|
1800
1801
|
run.state = 'verification_pending'; run.reason = 'Only explicitly human acceptance remains.'; state.phase = 'waiting';
|
|
1801
1802
|
run.checkpoint = { status: 'completed', summary: review.summary }; managed.autoRecovery = false;
|
|
1802
|
-
} else review = { ...review, passed: false, needsHuman: false, findings: ['Local acceptance changed during review; investigate and recheck.'] };
|
|
1803
|
+
} else if (review.passed || review.needsHuman) review = { ...review, passed: false, needsHuman: false, findings: ['Local acceptance changed during review; investigate and recheck.'] };
|
|
1803
1804
|
}
|
|
1804
1805
|
state.review={...review,decisionId:managed.decision.id};
|
|
1805
1806
|
if (!review.passed && !review.needsHuman && openFindings.some(item => item.observation)) {
|
|
@@ -2461,6 +2462,7 @@ function recordManagedOutcomeProgress(run, now, assessmentAvailable = true) {
|
|
|
2461
2462
|
// Manual/legacy runs have no marker and retain their existing controls.
|
|
2462
2463
|
export const ownerRenewalPending = record => ['awaiting_human', 'prepared', 'gate_committed'].includes(record?.run?.managed?.ownerRenewal?.status);
|
|
2463
2464
|
export function automaticContinuationAllowed(record) {
|
|
2465
|
+
if (guidancePending(record)) return false;
|
|
2464
2466
|
if (ownerRenewalPending(record) || record?.run?.managed?.ownerRenewal?.needsReadOnlyReassessment === true) return false;
|
|
2465
2467
|
if (record?.providerArchived === true || record?.run?.managed?.purpose && record.autoContinue === false) return false;
|
|
2466
2468
|
const epoch = record?.run?.managed?.autoContinueEpoch;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
import {createInterface} from 'node:readline';
|
|
3
|
+
const fail=code=>Object.assign(new Error(code),{code});
|
|
4
|
+
export async function openAppTools({record,signal,node,server,pipe}) {
|
|
5
|
+
const p=spawn(node,[server],{env:{...process.env,CODEX_APP_TOOLS_PIPE_PATH:pipe},stdio:['pipe','pipe','ignore']});
|
|
6
|
+
let sequence=0,closed=false;const pending=new Map();const lines=createInterface({input:p.stdout});
|
|
7
|
+
const close=()=>{if(closed)return;closed=true;signal?.removeEventListener('abort',close);for(const q of pending.values())q.reject(fail(q.write?'native_submission_unknown':'native_tools_closed'));pending.clear();lines.close();p.stdin.end();p.kill('SIGTERM');};
|
|
8
|
+
p.on('error',close);p.on('exit',close);p.stdin.on('error',close);signal?.addEventListener('abort',close,{once:true});
|
|
9
|
+
lines.on('line',line=>{let d;try{d=JSON.parse(line);}catch{close();return;}const q=pending.get(d.id);if(!q)return;pending.delete(d.id);d.error?q.reject(fail(q.write?'native_submission_unknown':'native_tools_request_failed')):q.resolve(d.result);});
|
|
10
|
+
const rpc=(method,params,write=false)=>new Promise((resolve,reject)=>{
|
|
11
|
+
if(closed||signal?.aborted){reject(fail('native_tools_closed'));return;}const id=++sequence;
|
|
12
|
+
const timer=setTimeout(()=>{pending.delete(id);reject(fail(write?'native_submission_unknown':'native_tools_timeout'));},15000);
|
|
13
|
+
pending.set(id,{write,resolve:r=>{clearTimeout(timer);resolve(r)},reject:e=>{clearTimeout(timer);reject(e)}});
|
|
14
|
+
p.stdin.write(JSON.stringify({jsonrpc:'2.0',id,method,params})+'\n');
|
|
15
|
+
});
|
|
16
|
+
try{
|
|
17
|
+
await rpc('initialize',{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'simy-original-continuation',version:'1.0.0'}});
|
|
18
|
+
p.stdin.write(JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})+'\n');
|
|
19
|
+
const list=await rpc('tools/list',{});const names=new Set(list.tools.map(t=>t.name));
|
|
20
|
+
if(!names.has('read_thread')||!names.has('send_message_to_thread'))throw fail('native_tools_unsupported');
|
|
21
|
+
const call=async(name,args,write=false)=>{
|
|
22
|
+
const result=await rpc('tools/call',{name,arguments:args,_meta:{'openai/threadId':record.session}},write);
|
|
23
|
+
if(result.isError)throw fail(write?'native_submission_unknown':result.content?.some(c=>c.type==='text'&&c.text.includes('exceeds the size limit'))?'native_tools_response_too_large':'native_tools_request_failed');
|
|
24
|
+
const text=result.content?.find(c=>c.type==='text')?.text;if(!text)throw fail(write?'native_submission_unknown':'native_tools_invalid_response');
|
|
25
|
+
return JSON.parse(text);
|
|
26
|
+
};
|
|
27
|
+
const read=async(cursor,includeOutputs=false)=>{
|
|
28
|
+
const r=await call('read_thread',{threadId:record.session,hostId:'local',turnLimit:1,includeOutputs,maxOutputCharsPerItem:16000,...(cursor?{cursor}:{})});
|
|
29
|
+
if(r.thread?.id!==record.session||r.thread?.hostId!=='local'||r.thread?.cwd!==record.cwd)throw fail('native_tools_owner_mismatch');return r;
|
|
30
|
+
};
|
|
31
|
+
return {transport:'codex_app_tools',close,
|
|
32
|
+
async request(method,params){
|
|
33
|
+
if(params.threadId!==record.session)throw fail('native_tools_target_mismatch');
|
|
34
|
+
const r=await read(params.cursor,method==='thread/turns/list');
|
|
35
|
+
if(method==='thread/read')return {thread:{...r.thread,status:{type:r.thread.status?.type==='active'?'active':r.thread.status?.type==='idle'?'idle':'unknown'}}};
|
|
36
|
+
if(method==='thread/turns/list')return {data:r.turns,nextCursor:r.page?.nextCursor};
|
|
37
|
+
throw fail('native_method_unsupported');
|
|
38
|
+
},
|
|
39
|
+
async submitOriginal({text,beforeSubmit}){
|
|
40
|
+
const r=await read();if(r.thread.status?.type!=='idle')throw fail('native_thread_busy');
|
|
41
|
+
await beforeSubmit();signal?.throwIfAborted();
|
|
42
|
+
return call('send_message_to_thread',{threadId:record.session,hostId:'local',prompt:text},true);
|
|
43
|
+
}};
|
|
44
|
+
}catch(e){close();throw e;}
|
|
45
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {assertGuidanceIntent, guidancePending} from './guidance-intent.mjs';
|
|
2
|
+
import {createServer, createConnection} from 'node:net';
|
|
3
|
+
import {mkdir, stat, readFile, writeFile, rename, chmod, rm, readdir} from 'node:fs/promises';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
import {randomUUID, createHash} from 'node:crypto';
|
|
6
|
+
import {readRecord, updateRecord, automaticContinuationAllowed, hasPendingUserStop, managedBudgetReason} from '../hooks/delivery-loop.mjs';
|
|
7
|
+
import {openAppTools} from './app-tools.mjs';
|
|
8
|
+
|
|
9
|
+
const fail = code => Object.assign(new Error(code), {code});
|
|
10
|
+
export const nativeIntentId = r => createHash('sha256').update(JSON.stringify([r.run.id, r.autoContinueEpoch, r.run.controlRevision, r.run.revision, r.run.managed.decision?.id])).digest('hex');
|
|
11
|
+
const directory = root => join(root, 'native-app-bridge');
|
|
12
|
+
// This is an app-launched transport, never a supervisor or a new task owner.
|
|
13
|
+
// Every write requires an already persisted, exact original-task send intent.
|
|
14
|
+
export async function startNativeBridge({root, node, server, pipe, openClient = openAppTools}) {
|
|
15
|
+
const pipeStat = await stat(pipe);
|
|
16
|
+
if (!pipeStat.isSocket() || pipeStat.uid !== process.getuid()) throw fail('native_tools_owner_mismatch');
|
|
17
|
+
const dir = directory(root), id = randomUUID(), token = randomUUID();
|
|
18
|
+
await mkdir(dir, {recursive: true, mode: 0o700});
|
|
19
|
+
const socket = join(dir, id + '.sock'), peers = new Set();
|
|
20
|
+
const listener = createServer(peer => {
|
|
21
|
+
peers.add(peer); peer.setEncoding('utf8'); peer.setTimeout(60000, () => peer.destroy());
|
|
22
|
+
let buffer = '', busy = false, authorizationReply = null;
|
|
23
|
+
peer.on('error', () => {}); peer.on('close', () => peers.delete(peer));
|
|
24
|
+
peer.on('data', chunk => {
|
|
25
|
+
buffer += chunk;
|
|
26
|
+
if (buffer.length > 1024 * 1024) {peer.destroy(); return;}
|
|
27
|
+
const end = buffer.indexOf('\n'); if (end < 0) return;
|
|
28
|
+
if (buffer.indexOf('\n', end + 1) !== -1) {peer.destroy(); return;}
|
|
29
|
+
const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
|
|
30
|
+
if (busy) {if (authorizationReply) authorizationReply(line); else peer.destroy(); return;}
|
|
31
|
+
busy = true;
|
|
32
|
+
void (async () => {
|
|
33
|
+
let client;
|
|
34
|
+
try {
|
|
35
|
+
const q = JSON.parse(line);
|
|
36
|
+
if (q.token !== token || !['thread/read', 'thread/turns/list', 'submit', 'submit_guidance'].includes(q.method)) throw fail('native_tools_bridge_denied');
|
|
37
|
+
// Old hosts cannot answer the final controller challenge. Fail closed
|
|
38
|
+
// rather than silently accepting their earlier authorization.
|
|
39
|
+
if (q.method === 'submit_guidance' && q.guidanceAuthorization !== 1) throw fail('native_guidance_unavailable');
|
|
40
|
+
const authorizeGuidance = () => new Promise((resolve, reject) => {
|
|
41
|
+
const challenge = randomUUID();
|
|
42
|
+
const finish = error => {
|
|
43
|
+
clearTimeout(timer); authorizationReply = null; peer.removeListener('close', lost);
|
|
44
|
+
error ? reject(error) : resolve();
|
|
45
|
+
};
|
|
46
|
+
const lost = () => finish(fail('native_guidance_changed'));
|
|
47
|
+
const timer = setTimeout(lost, 15000);
|
|
48
|
+
peer.once('close', lost);
|
|
49
|
+
authorizationReply = line => {
|
|
50
|
+
try {
|
|
51
|
+
const ack = JSON.parse(line);
|
|
52
|
+
if (ack.token !== token || ack.method !== 'authorize_guidance' || ack.challenge !== challenge ||
|
|
53
|
+
ack.guidanceId !== q.guidanceId || ack.authorized !== true) throw fail('native_guidance_changed');
|
|
54
|
+
finish();
|
|
55
|
+
} catch {finish(fail('native_guidance_changed'));}
|
|
56
|
+
};
|
|
57
|
+
if (peer.destroyed) {lost(); return;}
|
|
58
|
+
peer.write(JSON.stringify({kind: 'guidance_authorization_required', guidanceId: q.guidanceId, challenge}) + '\n');
|
|
59
|
+
});
|
|
60
|
+
const r = await readRecord(join(root, 'delivery-hooks'), 'codex', q.session);
|
|
61
|
+
if (!r?.run?.managed || r.run.id !== q.runId || r.run.managed.taskId !== q.taskId || r.autoContinueEpoch !== q.epoch || !(q.guidanceId ? guidancePending(r) && r.run.managed.userGuidance.id === q.guidanceId : automaticContinuationAllowed(r))) throw fail('native_tools_attachment_changed');
|
|
62
|
+
client = await openClient({record: r, node, server, pipe});
|
|
63
|
+
const result = q.method === 'submit_guidance' ? await client.submitOriginal({text: q.text, beforeSubmit: async () => {
|
|
64
|
+
await updateRecord(join(root, 'delivery-hooks'), 'codex', q.session, async fresh => {
|
|
65
|
+
const intent = assertGuidanceIntent(fresh, q.guidanceId, q.text);
|
|
66
|
+
if (intent.phase !== 'submitting' || intent.bridgeAttemptedAt || hasPendingUserStop(join(root, 'delivery-hooks'), fresh) || managedBudgetReason(fresh.run)) throw fail('native_guidance_changed');
|
|
67
|
+
// Hold the exact intent lock while asking its original controller
|
|
68
|
+
// to recheck account/org/provider authority at the provider boundary.
|
|
69
|
+
await authorizeGuidance();
|
|
70
|
+
assertGuidanceIntent(fresh, q.guidanceId, q.text);
|
|
71
|
+
if (peer.destroyed || hasPendingUserStop(join(root, 'delivery-hooks'), fresh) || managedBudgetReason(fresh.run)) throw fail('native_guidance_changed');
|
|
72
|
+
intent.bridgeAttemptedAt = Date.now();
|
|
73
|
+
return fresh;
|
|
74
|
+
});
|
|
75
|
+
}}) : q.method === 'submit' ? await client.submitOriginal({text: q.text, beforeSubmit: async () => {
|
|
76
|
+
const fresh = await readRecord(join(root, 'delivery-hooks'), 'codex', q.session), m = fresh?.run?.managed, h = m?.nativeHandoff;
|
|
77
|
+
if (fresh?.run?.id !== q.runId || m?.taskId !== q.taskId || fresh.autoContinueEpoch !== q.epoch || !automaticContinuationAllowed(fresh) || hasPendingUserStop(join(root, 'delivery-hooks'), fresh) || fresh.run.stopReason || fresh.run.state !== 'recovery_required' || m.pendingApproval || m.resumeLease || m.liveWorkspaceLease || m.outcomeAudit?.lease || m.providerWarning || m.usageBlocked || m.maxTokens || m.understandingQuestion || m.purpose?.nativeBoundary || managedBudgetReason(fresh.run) || ['verifying', 'understanding', 'needs_clarification', 'action_association'].includes(m.phase) || m.executionUncertain || m.nativeWait || m.userQuestion || m.authenticationWait || m.dependencyWait || h?.id !== q.handoffId || h?.id !== nativeIntentId(fresh) || h.phase !== 'submitting' || h.text !== q.text) throw fail('native_tools_intent_changed');
|
|
78
|
+
}}) : await client.request(q.method, {threadId: q.session, cursor: q.cursor});
|
|
79
|
+
peer.end(JSON.stringify({ok: true, result, guidanceAuthorization: 1}) + '\n');
|
|
80
|
+
} catch (error) {peer.end(JSON.stringify({ok: false, code: error.code || 'native_tools_request_failed'}) + '\n');}
|
|
81
|
+
finally {client?.close();}
|
|
82
|
+
})();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
await new Promise((resolve, reject) => {listener.once('error', reject); listener.listen(socket, resolve);});
|
|
86
|
+
await chmod(socket, 0o600);
|
|
87
|
+
const target = join(dir, id + '.json'), temp = target + '.tmp';
|
|
88
|
+
await writeFile(temp, JSON.stringify({schema: 1, id, pid: process.pid, socket, token, startedAt: Date.now()}), {mode: 0o600});
|
|
89
|
+
await rename(temp, target);
|
|
90
|
+
return {id, async close() {
|
|
91
|
+
for (const peer of peers) peer.destroy();
|
|
92
|
+
await new Promise(resolve => listener.close(resolve));
|
|
93
|
+
await rm(socket, {force: true});
|
|
94
|
+
try {if (JSON.parse(await readFile(target, 'utf8')).id === id) await rm(target);} catch (e) {if (e.code !== 'ENOENT') throw e;}
|
|
95
|
+
}};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function openNativeBridge(options) {
|
|
99
|
+
let names;
|
|
100
|
+
try {names = (await readdir(directory(options.root))).filter(n => /^[0-9a-f-]{36}\.json$/.test(n)).sort();}
|
|
101
|
+
catch (e) {if (e.code === 'ENOENT') return null; throw e;}
|
|
102
|
+
if (!names.length) return null;
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
let client;
|
|
105
|
+
try {
|
|
106
|
+
const a = JSON.parse(await readFile(join(directory(options.root), name), 'utf8'));
|
|
107
|
+
client = await openBridgeInstance(options, a);
|
|
108
|
+
await client.request('thread/read', {threadId: options.record.session});
|
|
109
|
+
return client;
|
|
110
|
+
} catch (e) {client?.close(); if (options.signal?.aborted) throw e;}
|
|
111
|
+
}
|
|
112
|
+
throw fail('native_tools_bridge_unavailable');
|
|
113
|
+
}
|
|
114
|
+
async function openBridgeInstance({root, record, signal, guidanceId: optionsGuidanceId}, a) {
|
|
115
|
+
if (a.schema !== 1 || typeof a.token !== 'string' || a.socket !== join(directory(root), a.id + '.sock')) throw fail('native_tools_bridge_invalid');
|
|
116
|
+
const s = await stat(a.socket).catch(() => {throw fail('native_tools_bridge_unavailable');});
|
|
117
|
+
if (!s.isSocket() || s.uid !== process.getuid() || (s.mode & 0o077)) throw fail('native_tools_owner_mismatch');
|
|
118
|
+
const peers = new Set(); let closed = false, guidanceAuthorization = false;
|
|
119
|
+
const call = (method, extra = {}, authorizeGuidance) => new Promise((resolve, reject) => {
|
|
120
|
+
if (closed || signal?.aborted) {reject(fail('native_tools_closed')); return;}
|
|
121
|
+
const peer = createConnection(a.socket); peers.add(peer); let buffer = '', sent = false, settled = false, challenged = false;
|
|
122
|
+
const finish = (error, result) => {if (settled) return; settled = true; clearTimeout(timer); peers.delete(peer); peer.destroy(); error ? reject(error) : resolve(result);};
|
|
123
|
+
const lost = () => finish(fail(sent && ['submit', 'submit_guidance'].includes(method) ? 'native_submission_unknown' : 'native_tools_bridge_unavailable'));
|
|
124
|
+
const timer = setTimeout(lost, 45000);
|
|
125
|
+
peer.setEncoding('utf8'); peer.on('error', lost); peer.on('close', lost);
|
|
126
|
+
peer.on('connect', () => {sent = true; peer.write(JSON.stringify({token: a.token, session: record.session, runId: record.run.id, taskId: record.run.managed.taskId, epoch: record.autoContinueEpoch, method, ...(method === 'submit_guidance' ? {guidanceAuthorization: 1} : {}), ...(optionsGuidanceId ? {guidanceId: optionsGuidanceId} : {}), ...extra}) + '\n');});
|
|
127
|
+
peer.on('data', chunk => {
|
|
128
|
+
buffer += chunk; if (buffer.length > 16 * 1024 * 1024) {lost(); return;}
|
|
129
|
+
const end = buffer.indexOf('\n'); if (end < 0) return;
|
|
130
|
+
const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
|
|
131
|
+
try {
|
|
132
|
+
const r = JSON.parse(line);
|
|
133
|
+
if (r.kind === 'guidance_authorization_required') {
|
|
134
|
+
if (method !== 'submit_guidance' || challenged || !authorizeGuidance || r.guidanceId !== optionsGuidanceId ||
|
|
135
|
+
typeof r.challenge !== 'string' || !/^[0-9a-f-]{36}$/i.test(r.challenge)) {lost(); return;}
|
|
136
|
+
challenged = true;
|
|
137
|
+
const acknowledge = authorized => {
|
|
138
|
+
if (!settled && !peer.destroyed) peer.write(JSON.stringify({token: a.token, method: 'authorize_guidance',
|
|
139
|
+
guidanceId: optionsGuidanceId, challenge: r.challenge, authorized}) + '\n');
|
|
140
|
+
};
|
|
141
|
+
Promise.resolve().then(authorizeGuidance).then(() => {signal?.throwIfAborted(); acknowledge(true);}, () => acknowledge(false)).catch(() => acknowledge(false));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (r.guidanceAuthorization === 1) guidanceAuthorization = true;
|
|
145
|
+
finish(r.ok ? null : fail(r.code), r.result);
|
|
146
|
+
} catch {lost();}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
const close = () => {closed = true; signal?.removeEventListener('abort', close); for (const peer of peers) peer.destroy();};
|
|
150
|
+
signal?.addEventListener('abort', close, {once: true});
|
|
151
|
+
return {transport: 'codex_app_tools', close, get guidanceAuthorization() {return guidanceAuthorization;},
|
|
152
|
+
request(method, params) {if (params.threadId !== record.session) throw fail('native_tools_target_mismatch'); return call(method, {cursor: params.cursor});},
|
|
153
|
+
async submitGuidance({text, beforeSubmit}) {if (!optionsGuidanceId || !guidanceAuthorization) throw fail('native_guidance_unavailable'); await beforeSubmit(); signal?.throwIfAborted(); return call('submit_guidance', {text}, beforeSubmit);},
|
|
154
|
+
async submitOriginal({text, beforeSubmit}) {await beforeSubmit(); signal?.throwIfAborted(); const fresh = await readRecord(join(root, 'delivery-hooks'), 'codex', record.session); return call('submit', {text, handoffId: fresh?.run?.managed?.nativeHandoff?.id});},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const guidancePending = record => ['prepared', 'submitting', 'submitted', 'unknown'].includes(record?.run?.managed?.userGuidance?.phase);
|
|
4
|
+
export const guidanceDigest = text => createHash('sha256').update(text).digest('hex');
|
|
5
|
+
export function guidanceTarget(record) {
|
|
6
|
+
return {provider: record?.provider, session: record?.session, cwd: record?.cwd,
|
|
7
|
+
runId: record?.run?.id, taskId: record?.run?.managed?.taskId, epoch: record?.autoContinueEpoch,
|
|
8
|
+
revision: (record?.run?.revision || 0) + (record?.run?.controlRevision || 0)};
|
|
9
|
+
}
|
|
10
|
+
export function guidanceAllowed(record, {pendingId} = {}) {
|
|
11
|
+
const m = record?.run?.managed;
|
|
12
|
+
return record?.provider === 'codex' && record.autoContinue === true && !!record.autoContinueEpoch &&
|
|
13
|
+
!record.providerArchived && !record.parentSession && record.run?.state === 'recovery_required' && !record.run.stopReason &&
|
|
14
|
+
m?.purpose?.enabled === true && m.resumeChannel === 'codex_app_server' && (!m.autoContinueEpoch || m.autoContinueEpoch === record.autoContinueEpoch) &&
|
|
15
|
+
!m.pendingApproval && !m.userQuestion && !m.understandingQuestion && !m.nativeWait && !m.dependencyWait && !m.authenticationWait &&
|
|
16
|
+
!m.resumeLease && !m.liveWorkspaceLease && !m.outcomeAudit?.lease && !m.executionUncertain && !m.providerWarning &&
|
|
17
|
+
!m.usageBlocked && !m.maxTokens && !m.purpose?.nativeBoundary &&
|
|
18
|
+
!['verifying', 'understanding', 'needs_clarification', 'action_association'].includes(m.phase) &&
|
|
19
|
+
(!m.nativeHandoff || ['finished', 'cancelled'].includes(m.nativeHandoff.phase)) &&
|
|
20
|
+
(!guidancePending(record) || pendingId === m.userGuidance.id);
|
|
21
|
+
}
|
|
22
|
+
export function assertGuidanceIntent(record, id, text) {
|
|
23
|
+
const intent = record?.run?.managed?.userGuidance;
|
|
24
|
+
if (!intent || intent.id !== id || !guidanceAllowed(record, {pendingId: id}) ||
|
|
25
|
+
JSON.stringify(intent.target) !== JSON.stringify(guidanceTarget(record)) ||
|
|
26
|
+
intent.text !== text || intent.digest !== guidanceDigest(text)) {
|
|
27
|
+
throw Object.assign(new Error('The original guidance target or send intent changed.'), {code: 'native_guidance_changed'});
|
|
28
|
+
}
|
|
29
|
+
return intent;
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
if [ "$#" -lt 1 ]; then
|
|
5
|
+
printf '%s\n' 'Codex app tools could not start: missing MCP server path.' >&2
|
|
6
|
+
exit 64
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
PATH="${PATH:-/usr/bin:/bin}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
|
10
|
+
export PATH
|
|
11
|
+
|
|
12
|
+
cache_root=${XDG_CACHE_HOME:-${HOME:-}/.cache}
|
|
13
|
+
codex_resources=
|
|
14
|
+
case "${CODEX_CLI_PATH:-}" in
|
|
15
|
+
*/*) codex_resources=${CODEX_CLI_PATH%/*} ;;
|
|
16
|
+
esac
|
|
17
|
+
for candidate in \
|
|
18
|
+
"${CODEX_MCP_NODE_PATH:-}" \
|
|
19
|
+
"${CODEX_BROWSER_USE_NODE_PATH:-}" \
|
|
20
|
+
"${CODEX_ELECTRON_RESOURCES_PATH:-}/cua_node/bin/node" \
|
|
21
|
+
"$codex_resources/cua_node/bin/node" \
|
|
22
|
+
"$cache_root/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node"
|
|
23
|
+
do
|
|
24
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
25
|
+
exec "$candidate" "$@"
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
if node_path=$(command -v node 2>/dev/null); then
|
|
30
|
+
case "$node_path" in
|
|
31
|
+
/*) exec "$node_path" "$@" ;;
|
|
32
|
+
esac
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
printf '%s\n' 'Codex app tools could not find a Node runtime. Reinstall or update Codex.' >&2
|
|
36
|
+
exit 127
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {createInterface} from 'node:readline';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import {join, dirname} from 'node:path';
|
|
4
|
+
import {startNativeBridge} from './bridge.mjs';
|
|
5
|
+
|
|
6
|
+
// Codex launches this ordinary stdio MCP server with its own Node runtime and
|
|
7
|
+
// fresh app-tools pipe. No saved pipe, daemon launch or app modification needed.
|
|
8
|
+
let bridge, reason = 'native_tools_attachment_missing';
|
|
9
|
+
try {
|
|
10
|
+
const resources = process.env.CODEX_ELECTRON_RESOURCES_PATH || dirname(dirname(dirname(process.execPath)));
|
|
11
|
+
bridge = await startNativeBridge({root: process.env.SIMY_HOME || join(homedir(), '.simy'), node: process.execPath,
|
|
12
|
+
server: join(resources, 'plugins/openai-bundled/plugins/codex-app-tools/server.mjs'), pipe: process.env.CODEX_APP_TOOLS_PIPE_PATH});
|
|
13
|
+
} catch (e) {reason = e.code || reason;}
|
|
14
|
+
const lines = createInterface({input: process.stdin});
|
|
15
|
+
for await (const line of lines) {
|
|
16
|
+
let q; try {q = JSON.parse(line);} catch {continue;}
|
|
17
|
+
if (q.id === undefined) continue;
|
|
18
|
+
let result;
|
|
19
|
+
if (q.method === 'initialize') result = {protocolVersion: q.params.protocolVersion, capabilities: {tools: {}}, serverInfo: {name: 'simy-native-continuation', version: '1.0.0'}};
|
|
20
|
+
else if (q.method === 'tools/list') result = {tools: [{name: 'native_connection_status', description: 'Read SIMY native Codex transport availability. Connection availability is not execution or completion proof.', inputSchema: {type: 'object', properties: {}, additionalProperties: false}, annotations: {readOnlyHint: true}}]};
|
|
21
|
+
else if (q.method === 'tools/call' && q.params?.name === 'native_connection_status') result = {content: [{type: 'text', text: JSON.stringify({connected: !!bridge, reason: bridge ? 'app_launched_transport' : reason})}]};
|
|
22
|
+
else if (q.method === 'ping') result = {};
|
|
23
|
+
else {process.stdout.write(JSON.stringify({jsonrpc: '2.0', id: q.id, error: {code: -32601, message: 'Method not found'}}) + '\n'); continue;}
|
|
24
|
+
process.stdout.write(JSON.stringify({jsonrpc: '2.0', id: q.id, result}) + '\n');
|
|
25
|
+
}
|
|
26
|
+
await bridge?.close();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Exact tool output, original sender, complete text and XML escaping are all
|
|
2
|
+
// required. A quoted message or worker assertion is never a send receipt.
|
|
3
|
+
export function matchesNativeDelegation(turn, session, text) {
|
|
4
|
+
const escaped = text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
5
|
+
const expected = `<codex_delegation>\n <source_thread_id>${session}</source_thread_id>\n <input>${escaped}</input>\n</codex_delegation>`;
|
|
6
|
+
return turn.items?.some(item => item.type === 'functionCallOutput' && item.namespace === 'codex_app' &&
|
|
7
|
+
item.name === 'send_message_to_thread' && !item.output?.truncated &&
|
|
8
|
+
(typeof item.output === 'string' ? item.output : item.output?.text) === expected) === true;
|
|
9
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {assertNativeObservationScope} from '../session-supervision/native-turn-observer.js';
|
|
1
2
|
import { captureDispatchProvenance } from '../update-detachment/provenance.js';
|
|
2
3
|
import { captureDispatchOwner, assertTaskAdmission, dispatchWithFence, queueDispatchReceipt, confirmDispatch, flushDispatchIntake, rootFromDelivery, observeDispatchEvent } from '../update-detachment/dispatch-gate.js';
|
|
3
4
|
import { bindCompletionArtifacts } from '../session-supervision/completion-artifacts.js';
|
|
@@ -29,7 +30,7 @@ import { createTaskPool } from '../codex-plugins/simy/hooks/loop-kernel.mjs';
|
|
|
29
30
|
const MAX_OUTPUT = 64 * 1024;
|
|
30
31
|
const SAFE_PROVIDER_CODES = new Set(['authentication_required', 'identity', 'permission_narrowed', 'permission_restore', 'turn_failed', 'checkpoint',
|
|
31
32
|
'closed', 'timeout', 'unsupported', 'unavailable', 'manual_provider_action', 'checkpoint_rejected', 'provider_approval', 'provider_question', 'usage_unavailable',
|
|
32
|
-
'state_busy', 'dispatch_detached', 'owner_fenced', 'task_admission_blocked', 'task_admission_identity_required',
|
|
33
|
+
'state_busy', 'review_context_changed', 'review_history_changed', 'cancelled', 'dispatch_detached', 'owner_fenced', 'task_admission_blocked', 'task_admission_identity_required',
|
|
33
34
|
'task_identity_conflict', 'attempt_already_recorded', 'identity_unavailable']);
|
|
34
35
|
function safeProviderCode(code) { return SAFE_PROVIDER_CODES.has(code) ? code : 'provider_error'; }
|
|
35
36
|
function providerFailureReason(code) {
|
|
@@ -39,6 +40,7 @@ function providerFailureReason(code) {
|
|
|
39
40
|
if (code === 'checkpoint' || code === 'checkpoint_rejected') return 'Codex returned an invalid managed checkpoint; inspect its plan before resuming.';
|
|
40
41
|
if (code === 'turn_failed') return 'The Codex turn did not complete; inspect the original conversation.';
|
|
41
42
|
if (code === 'state_busy') return 'The local dispatch state is busy; reconcile its lock before another attempt.';
|
|
43
|
+
if (code === 'review_context_changed' || code === 'review_history_changed') return 'The reviewed conversation or captured results changed. Preserve the prior verdict and inspect the latest settled result before another independent review.';
|
|
42
44
|
if (['dispatch_detached', 'owner_fenced', 'task_admission_blocked', 'task_admission_identity_required', 'task_identity_conflict', 'identity_unavailable'].includes(code)) return `Local dispatch admission requires reconciliation (${code}); preserve the existing task and provider authority.`;
|
|
43
45
|
return 'Codex recovery process failed; inspect its checkpoint and original session before the next bounded recovery.';
|
|
44
46
|
}
|
|
@@ -416,8 +418,14 @@ export async function tickManagedTasks({ root, webOrigin, now = Date.now(), exec
|
|
|
416
418
|
});
|
|
417
419
|
},
|
|
418
420
|
onEvidence: async evidence => {
|
|
419
|
-
await persistReceipt(current => {
|
|
421
|
+
await persistReceipt(async current => {
|
|
420
422
|
if (current?.run?.managed?.resumeLease?.id !== leaseId || current.run.managed.decision?.id !== evidence.actionId) throw new Error('Evidence capture lease changed.');
|
|
423
|
+
if ((evidence.observations || []).some(item => item.kind === 'codex_native_history')) {
|
|
424
|
+
await authorizeClaim();
|
|
425
|
+
assertNativeObservationScope(evidence, current, root);
|
|
426
|
+
for (const item of evidence.observations.filter(item => item.kind === 'codex_native_history'))
|
|
427
|
+
await requireProviderLink({root, provider: current.provider, expectedEpoch: item.providerEpoch});
|
|
428
|
+
}
|
|
421
429
|
if(evidence.workerContext)current.run.managed.executionAttempt.workerContext=structuredClone(evidence.workerContext);
|
|
422
430
|
if (searchEnabled(current.run.managed)) retainControllerEvidence(current.run.managed, evidence, currentTime());
|
|
423
431
|
return current;
|
|
@@ -683,6 +691,23 @@ export function createManagedSupervisor(options) {
|
|
|
683
691
|
// Continue actual task work and its result checks. Starting an unrelated
|
|
684
692
|
// periodic audit here could acquire a lease that prevents that work.
|
|
685
693
|
if (!running) running = (async () => {
|
|
694
|
+
// Share cancellation discovery within this offered batch, never its
|
|
695
|
+
// authority: reconciliation still reads each current record. Long-lived
|
|
696
|
+
// queued jobs refresh at the desktop's 3s maintenance cadence; new ticks
|
|
697
|
+
// also replace their queued observations. Failed reads remain retryable.
|
|
698
|
+
let cancellationCandidates, cancellationReadAt;
|
|
699
|
+
const readCancellationRecords = () => {
|
|
700
|
+
const age = Date.now() - cancellationReadAt;
|
|
701
|
+
if (!cancellationCandidates || !(age >= 0 && age < 3000)) {
|
|
702
|
+
cancellationReadAt = Date.now();
|
|
703
|
+
const pending = Promise.resolve()
|
|
704
|
+
.then(() => options.readCancellationRecords ? options.readCancellationRecords() : listRecords(options.root))
|
|
705
|
+
.then(records => records.filter(record => record.run?.managed?.nativeHandoff));
|
|
706
|
+
cancellationCandidates = pending;
|
|
707
|
+
void pending.catch(() => { if (cancellationCandidates === pending) cancellationCandidates = null; });
|
|
708
|
+
}
|
|
709
|
+
return cancellationCandidates;
|
|
710
|
+
};
|
|
686
711
|
for (const record of await readCandidates()) {
|
|
687
712
|
if (closed || await options.shouldYield?.()) break;
|
|
688
713
|
if (!record.run?.managed) continue;
|
|
@@ -699,7 +724,7 @@ export function createManagedSupervisor(options) {
|
|
|
699
724
|
return rows.filter(row => row?.provider === record.provider && row.session === record.session &&
|
|
700
725
|
row.run?.id === record.run.id && row.run?.managed?.taskId === record.run.managed.taskId);
|
|
701
726
|
};
|
|
702
|
-
await tickManagedTasks({ ...options, readRecords, signal: controller.signal });
|
|
727
|
+
await tickManagedTasks({ ...options, readRecords, readCancellationRecords, signal: controller.signal });
|
|
703
728
|
});
|
|
704
729
|
}
|
|
705
730
|
})().finally(() => { running = null; });
|