@milpa/comal 0.2.1 → 0.2.3
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 +1 -1
- package/backend/contracts/remote-commands-v1.json +0 -12
- package/backend/contracts/remote-relay-answers-v1.json +209 -0
- package/package.json +3 -1
- package/skills/mv3/SKILL.md +44 -0
- package/skills/mv3/skill.json +4 -0
- package/src/agent.mjs +299 -32
- package/src/auto-guard.mjs +40 -3
- package/src/autocompact.mjs +233 -0
- package/src/bash-timeouts.mjs +29 -0
- package/src/child-env.mjs +23 -0
- package/src/cli.mjs +232 -63
- package/src/code-run-artifact.mjs +30 -0
- package/src/code-run-journal.mjs +135 -0
- package/src/code-run-session-proof.mjs +50 -0
- package/src/code-run-session.mjs +97 -0
- package/src/code-run-worker.mjs +20 -4
- package/src/code-run.mjs +49 -14
- package/src/codex-provider.mjs +4 -4
- package/src/compaction-notice.mjs +22 -0
- package/src/compaction.mjs +162 -23
- package/src/connection-settings.mjs +36 -4
- package/src/context-pressure.mjs +5 -4
- package/src/context.mjs +27 -22
- package/src/execution-metrics.mjs +36 -5
- package/src/file-recovery-proof.mjs +9 -1
- package/src/file-recovery.mjs +36 -3
- package/src/gemini-provider.mjs +5 -3
- package/src/git-integration-operations.mjs +2 -1
- package/src/git-plumbing.mjs +2 -1
- package/src/git-source.mjs +2 -1
- package/src/git-workspaces.mjs +2 -1
- package/src/house.mjs +13 -1
- package/src/i18n/cli-es.mjs +12 -1
- package/src/i18n/es.mjs +133 -2
- package/src/judges.mjs +2 -1
- package/src/mcp.mjs +2 -1
- package/src/model-controls.mjs +16 -2
- package/src/model-deadline.mjs +2 -2
- package/src/openai-provider.mjs +8 -4
- package/src/operations.mjs +17 -2
- package/src/package-identity.mjs +68 -0
- package/src/peer-probe.mjs +16 -0
- package/src/permissions.mjs +99 -6
- package/src/plugin-catalog.mjs +19 -4
- package/src/plugin-config.mjs +18 -1
- package/src/plugin-controls.mjs +31 -8
- package/src/plugin-trust.mjs +15 -6
- package/src/plugins/chrome-lab/bridge.mjs +19 -6
- package/src/plugins/chrome-lab/client.mjs +242 -12
- package/src/plugins/chrome-lab/index.mjs +106 -24
- package/src/plugins/chrome-lab/legacy-fill.mjs +25 -0
- package/src/plugins/chrome-lab/native-reply.mjs +31 -0
- package/src/plugins/omarchy/catalog.mjs +96 -0
- package/src/plugins/omarchy/environment.mjs +78 -0
- package/src/plugins/omarchy/guide.mjs +54 -0
- package/src/plugins/omarchy/index.mjs +213 -0
- package/src/plugins/omarchy/runner.mjs +62 -0
- package/src/plugins/omarchy/text.mjs +26 -0
- package/src/plugins/omarchy/tiers.mjs +157 -0
- package/src/plugins/open-relay/actions.mjs +212 -16
- package/src/plugins/open-relay/client.mjs +93 -35
- package/src/plugins/open-relay/decision.mjs +89 -0
- package/src/plugins/open-relay/dossier.mjs +23 -5
- package/src/plugins/open-relay/index.mjs +68 -28
- package/src/plugins/open-relay/lookup.mjs +41 -4
- package/src/plugins/open-relay/notices.mjs +48 -4
- package/src/plugins/open-relay/panel.mjs +59 -0
- package/src/plugins/open-relay/watch.mjs +198 -85
- package/src/plugins.mjs +12 -1
- package/src/process-lock.mjs +20 -12
- package/src/provider-auth.mjs +6 -3
- package/src/provider-profiles.mjs +13 -3
- package/src/provider.mjs +175 -30
- package/src/read-only-check.mjs +12 -4
- package/src/relay/broker.mjs +160 -0
- package/src/relay/cards.mjs +702 -0
- package/src/relay/flow.mjs +886 -0
- package/src/relay/follow.mjs +119 -0
- package/src/relay/git.mjs +23 -0
- package/src/relay/hub-client.mjs +163 -0
- package/src/relay/state.mjs +55 -0
- package/src/remote-control.mjs +115 -12
- package/src/remote-relay.mjs +468 -0
- package/src/session-bus.mjs +13 -3
- package/src/session-provider-settings.mjs +2 -2
- package/src/skill-runtime.mjs +8 -6
- package/src/source-ownership.mjs +3 -1
- package/src/swarm-view.mjs +53 -0
- package/src/tool-manifest.mjs +71 -0
- package/src/tool-packs.mjs +22 -6
- package/src/tool-schema.mjs +14 -2
- package/src/tools.mjs +96 -27
- package/src/tui/connection-transition.mjs +10 -0
- package/src/tui/controller.mjs +35 -9
- package/src/tui/discovery.mjs +13 -8
- package/src/tui/inbound-blocks.mjs +5 -1
- package/src/tui/screen.mjs +78 -15
- package/src/tui/swarm-panel.mjs +217 -0
- package/src/turn-timing.mjs +82 -0
- package/src/usage-accounting.mjs +12 -1
- package/src/vision.mjs +6 -1
- package/src/visual/chrome.mjs +379 -0
- package/src/visual/fingerprint.mjs +30 -0
- package/src/visual/fixtures.mjs +201 -0
- package/src/visual/gate.mjs +92 -0
- package/src/visual/judge.mjs +114 -0
- package/src/visual/ledger.mjs +160 -0
- package/src/visual/measure.mjs +145 -0
- package/src/visual/names.mjs +7 -0
- package/src/visual/operations.mjs +211 -0
- package/src/visual/pixel-diff.mjs +96 -0
- package/src/visual/png.mjs +103 -0
- package/src/visual/registry.mjs +265 -0
- package/src/visual/report.mjs +64 -0
- package/src/visual/verify.mjs +292 -0
- package/src/work-assertions.mjs +15 -4
- package/src/work-plan.mjs +166 -30
- package/src/work-state.mjs +4 -4
- package/src/workflows/chrome-task-adoption.mjs +7 -3
- package/src/workspace-serve.mjs +59 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {readFileSync,readdirSync,statSync} from 'node:fs';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {createRequire} from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {digest} from './operations.mjs';
|
|
7
|
+
|
|
8
|
+
// Fingerprint actual installed source/VM artifacts, not just dependency versions.
|
|
9
|
+
export function codeRunArtifactIdentity(configuration) {
|
|
10
|
+
const files=[],seen=new Set(),require=createRequire(import.meta.url);
|
|
11
|
+
const walk=(directory,prefix)=>{
|
|
12
|
+
for(const name of readdirSync(directory).sort()) {
|
|
13
|
+
if(name==='node_modules')continue;
|
|
14
|
+
const target=path.join(directory,name),label=prefix+'/'+name,info=statSync(target);
|
|
15
|
+
if(info.isDirectory())walk(target,label);
|
|
16
|
+
else if(info.isFile())files.push([label,createHash('sha256').update(readFileSync(target)).digest('hex')]);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const dependency=(name,resolver)=>{
|
|
20
|
+
let base=path.dirname(resolver.resolve(name)),pkg;
|
|
21
|
+
for(;;){try{pkg=JSON.parse(readFileSync(path.join(base,'package.json'),'utf8'));if(pkg.name===name)break;}catch{}const parent=path.dirname(base);if(parent===base)throw Error('Cannot locate installed VM package: '+name);base=parent;}
|
|
22
|
+
if(seen.has(base))return;seen.add(base);walk(base,pkg.name+'@'+pkg.version);
|
|
23
|
+
for(const child of Object.keys(pkg.dependencies||{}).sort())dependency(child,createRequire(path.join(base,'package.json')));
|
|
24
|
+
};
|
|
25
|
+
walk(fileURLToPath(new URL('.',import.meta.url)),'src');
|
|
26
|
+
dependency('quickjs-emscripten',require);
|
|
27
|
+
dependency('ajv',require);
|
|
28
|
+
const identity={schema:'comal-code-run-artifact/1',node:process.version,platform:process.platform,arch:process.arch,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,configuration,files};
|
|
29
|
+
return {digest:digest(identity),identity};
|
|
30
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { randomUUID, randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { digest } from './operations.mjs';
|
|
4
|
+
import { SessionStore } from './sessions.mjs';
|
|
5
|
+
import { auditExecution } from './audit.mjs';
|
|
6
|
+
import { acquireProcessLock } from './process-lock.mjs';
|
|
7
|
+
|
|
8
|
+
const VERSION=1;
|
|
9
|
+
export const CODE_RUN_JOURNAL_MAX_ATTEMPTS=5;
|
|
10
|
+
const fail=(code,message)=>Object.assign(new Error(message),{code:`CODE_RUN_JOURNAL_${code}`});
|
|
11
|
+
const uuid=value=>/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value);
|
|
12
|
+
const engine=()=>digest({version:VERSION,node:process.version,platform:process.platform,arch:process.arch,
|
|
13
|
+
timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
14
|
+
files:['code-run.mjs','code-run-worker.mjs','code-run-journal.mjs'].map(file=>readFileSync(new URL(file,import.meta.url),'utf8'))});
|
|
15
|
+
|
|
16
|
+
// Host-only experimental resource. No model argument chooses a journal or grants replay safety.
|
|
17
|
+
// The native book is the only store: results reference existing complete House receipts.
|
|
18
|
+
export class CodeRunJournal {
|
|
19
|
+
#store; #sessionId; #id; #safe; #busy=false; #compatibility;
|
|
20
|
+
constructor({store,sessionId,id=randomUUID(),replaySafe=[],compatibility=null}) {
|
|
21
|
+
if(!(store instanceof SessionStore)||!uuid(id)||!Array.isArray(replaySafe)||replaySafe.some(n=>typeof n!=='string')) throw fail('HOST','A native store, session, UUID and host replay-safe operation list are required.');
|
|
22
|
+
store.file(sessionId);
|
|
23
|
+
if(compatibility!==null&&!/^[a-f0-9]{64}$/.test(compatibility))throw fail('HOST','Compatibility needs a pinned host profile identity.');
|
|
24
|
+
this.#store=store;this.#sessionId=sessionId;this.#id=id;this.#safe=[...new Set(replaySafe)].sort();this.#compatibility=compatibility;
|
|
25
|
+
}
|
|
26
|
+
get id(){return this.#id;}
|
|
27
|
+
async open({sourceRef,catalogue,limits,actor,sessionId,recordWork,signal}) {
|
|
28
|
+
if(this.#busy)throw fail('BUSY','This journal already owns a runtime.');
|
|
29
|
+
if(sessionId!==this.#sessionId||!actor?.id||typeof recordWork!=='function')throw fail('OWNER','The journal needs its original host-bound session and actor.');
|
|
30
|
+
this.#busy=true;let lock;
|
|
31
|
+
try {
|
|
32
|
+
lock=await acquireProcessLock(`${this.#store.file(sessionId)}.code-run-${this.#id}.lock`,{validate:()=>this.#store.validateForRecovery(sessionId)});
|
|
33
|
+
const load=async()=>{
|
|
34
|
+
const book=await this.#store.load(sessionId);
|
|
35
|
+
if(!book.journalAudit.verified||auditExecution(book).violations.length)throw fail('EVIDENCE','The native execution book does not verify.');
|
|
36
|
+
return book;
|
|
37
|
+
};
|
|
38
|
+
let book=await load();
|
|
39
|
+
const owned=row=>row.journal_id===this.#id&&row.type.startsWith('code_run_journal_');
|
|
40
|
+
const rows=book.rows.filter(owned),headers=rows.filter(r=>r.type==='code_run_journal_start');
|
|
41
|
+
const binding={version:VERSION,source_ref:sourceRef,catalogue,limits,actor,session_id:sessionId,house:this.#store.cwd,engine:engine(),replay_safe:this.#safe,...(this.#compatibility?{compatibility:this.#compatibility}:{})};
|
|
42
|
+
const fingerprint=digest(binding);
|
|
43
|
+
if(headers.length>1||rows.length&&!headers.length)throw fail('EVIDENCE','Journal birth is missing or duplicated.');
|
|
44
|
+
let header=headers[0];
|
|
45
|
+
if(header) {
|
|
46
|
+
if(header.fingerprint!==fingerprint||digest(header.binding)!==fingerprint||rows.some(r=>r.session_id!==sessionId||r.house!==this.#store.cwd||digest(r.actor)!==digest(actor)))throw fail('DRIFT','Source, runtime, catalogue, actor or replay policy changed; start a new journal.');
|
|
47
|
+
if(!Number.isSafeInteger(header.clock_ms)||!Number.isSafeInteger(header.deadline_ms)||header.deadline_ms!==header.clock_ms+limits.wallMs||!Number.isInteger(header.seed)||header.seed<0||header.seed>0xffffffff)throw fail('EVIDENCE','Invalid deterministic start data.');
|
|
48
|
+
} else {
|
|
49
|
+
const clock=Date.now();
|
|
50
|
+
header={type:'code_run_journal_start',journal_id:this.#id,binding,fingerprint,clock_ms:clock,deadline_ms:clock+limits.wallMs,seed:randomBytes(4).readUInt32LE()};
|
|
51
|
+
await recordWork(header);book=await load();header=book.rows.find(owned);
|
|
52
|
+
if(!header)throw fail('EVIDENCE','Journal birth was not retained by the native recorder.');
|
|
53
|
+
}
|
|
54
|
+
const check=()=>{signal?.throwIfAborted();if(Date.now()>=header.deadline_ms)throw fail('EXPIRED','The original journal deadline expired; no new observation was started.');};
|
|
55
|
+
check();
|
|
56
|
+
const slots=new Map(),results=new Map();
|
|
57
|
+
for(const row of book.rows.filter(owned)) {
|
|
58
|
+
if(row.type==='code_run_journal_call') {
|
|
59
|
+
if(!Number.isInteger(row.index)||row.index<1||row.index>limits.calls||!uuid(row.attempt_id)||row.call_id!==`${this.#id}:${row.attempt_id}:${row.index}`||!row.descriptor||row.descriptor_digest!==digest(row.descriptor))throw fail('EVIDENCE','Invalid journal call identity.');
|
|
60
|
+
const parent=book.rows.find(r=>r.type==='code_run_source'&&r.invocation_id===row.invocation_id&&r.run_id===this.#id&&r.attempt_id===row.attempt_id);
|
|
61
|
+
const proposal=book.rows.find(r=>r.type==='house_proposal'&&r.invocation_id===row.invocation_id);
|
|
62
|
+
if(!parent||parent.source_ref!==sourceRef||digest(parent.source)!==sourceRef||proposal?.operation!=='CodeRun'||proposal.arguments_digest!==digest({code:parent.source}))throw fail('EVIDENCE','The call is not bound to its original native script invocation.');
|
|
63
|
+
const prior=slots.get(row.index);
|
|
64
|
+
const logical=descriptor=>{const {dispatch_contract_digest,...rest}=descriptor;return digest(rest);};
|
|
65
|
+
if(prior&&logical(prior[0].descriptor)!==logical(row.descriptor))throw fail('EVIDENCE','Journal call identity was reused for different arguments.');
|
|
66
|
+
if(!prior)slots.set(row.index,[]);
|
|
67
|
+
// Arrays retain every uncertain attempt; none is silently discarded.
|
|
68
|
+
slots.get(row.index).push(row);
|
|
69
|
+
}
|
|
70
|
+
if(row.type==='code_run_journal_result') {
|
|
71
|
+
if(results.has(row.index))throw fail('EVIDENCE','Duplicate journal result.');
|
|
72
|
+
results.set(row.index,row);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const prefix=slots.size;
|
|
76
|
+
if([...slots.keys()].some(i=>i<1||i>prefix)||[...results.keys()].some(i=>!slots.has(i)))throw fail('EVIDENCE','The saved call prefix is incomplete.');
|
|
77
|
+
const receiptFor=(intent,currentBook)=>{
|
|
78
|
+
const receipts=currentBook.rows.filter(r=>r.type==='house_receipt'&&r.call_id===intent.call_id);
|
|
79
|
+
if(receipts.length>1)throw fail('EVIDENCE','An attempt has multiple native receipts.');
|
|
80
|
+
const receipt=receipts[0];
|
|
81
|
+
if(!receipt)return null;
|
|
82
|
+
const d=intent.descriptor;
|
|
83
|
+
if(receipt.status!=='returned')throw fail('UNCERTAIN','A saved call failed or was refused; automatic recovery is unavailable.');
|
|
84
|
+
if(d.dispatch_contract_digest&&!this.#compatibility)throw fail('EVIDENCE','A changed dispatch contract requires a pinned host compatibility proof.');
|
|
85
|
+
if(receipt.parent_invocation_id!==intent.invocation_id||receipt.operation!==d.name||receipt.arguments_digest!==d.arguments_digest||receipt.contract_digest!==(d.dispatch_contract_digest||d.contract_digest)||digest(receipt.actor)!==digest(actor)||receipt.session_id!==sessionId||receipt.house!==this.#store.cwd||receipt.output_truncated||typeof receipt.output!=='string'||digest(receipt.output)!==receipt.output_digest||!receipt.timestamp)throw fail('EVIDENCE','The complete native receipt does not match the original call.');
|
|
86
|
+
return receipt;
|
|
87
|
+
};
|
|
88
|
+
const saved=new Map();
|
|
89
|
+
for(const [index,intents] of slots) {
|
|
90
|
+
const receipts=intents.map(i=>receiptFor(i,book)).filter(Boolean),reference=results.get(index);
|
|
91
|
+
if(receipts.length>1)throw fail('EVIDENCE','Multiple completed attempts cannot become one observation.');
|
|
92
|
+
const receipt=receipts[0];
|
|
93
|
+
if(reference&&(!receipt||reference.receipt_invocation_id!==receipt.invocation_id||reference.receipt_digest!==receipt.audit.sha256))throw fail('EVIDENCE','The journal result reference is missing or mismatched.');
|
|
94
|
+
if(receipt)saved.set(index,{receipt,referenced:Boolean(reference)});
|
|
95
|
+
else if(!this.#safe.includes(intents[0].descriptor.name))throw fail('UNCERTAIN','An unreceipted call may have partially run; this host did not authorize read-only replay.');
|
|
96
|
+
}
|
|
97
|
+
let cursor=0;
|
|
98
|
+
if(rows.filter(r=>r.type==='code_run_journal_attempt').length>=CODE_RUN_JOURNAL_MAX_ATTEMPTS)throw fail('ATTEMPTS','The original journal exhausted its five runtime attempts.');
|
|
99
|
+
const attemptId=randomUUID(),reused=[];
|
|
100
|
+
const event=event=>recordWork({...event,journal_id:this.#id,attempt_id:attemptId});
|
|
101
|
+
await event({type:'code_run_journal_attempt'});check();
|
|
102
|
+
const reference=async(index,receipt)=>event({type:'code_run_journal_result',index,receipt_invocation_id:receipt.invocation_id,receipt_digest:receipt.audit.sha256});
|
|
103
|
+
const runtime={
|
|
104
|
+
runId:this.#id,attemptId,determinism:{clock_ms:header.clock_ms,seed:header.seed},
|
|
105
|
+
deadlineMs:header.deadline_ms,reused,
|
|
106
|
+
call:async(index,name,args,contractDigest,invoke,dispatchDigest=contractDigest)=>{
|
|
107
|
+
check();
|
|
108
|
+
if(index!==cursor+1)throw fail('DIVERGED','The script call order diverged.');
|
|
109
|
+
if(dispatchDigest!==contractDigest&&!this.#compatibility)throw fail('HOST','Dispatch rebinding needs a pinned host profile.');
|
|
110
|
+
const descriptor={name,arguments_digest:digest(args),contract_digest:contractDigest,...(dispatchDigest!==contractDigest?{dispatch_contract_digest:dispatchDigest}:{})};
|
|
111
|
+
const prior=slots.get(index)?.[0].descriptor;
|
|
112
|
+
if(prior&&(prior.name!==name||prior.arguments_digest!==descriptor.arguments_digest||prior.contract_digest!==contractDigest))throw fail('DIVERGED','The script arguments or tool diverged before the saved prefix was consumed.');
|
|
113
|
+
const old=saved.get(index);
|
|
114
|
+
if(old) {
|
|
115
|
+
if(!old.referenced){await reference(index,old.receipt);old.referenced=true;}
|
|
116
|
+
const observation={call_index:index,operation:name,call_id:old.receipt.call_id,contract_digest:old.receipt.contract_digest,receipt_invocation_id:old.receipt.invocation_id,receipt_digest:old.receipt.audit.sha256,observed_at:old.receipt.timestamp,current:false};
|
|
117
|
+
await event({type:'code_run_journal_reuse',index,...observation});check();
|
|
118
|
+
reused.push(observation);cursor=index;
|
|
119
|
+
return {result:old.receipt.output,...observation,reused:true};
|
|
120
|
+
}
|
|
121
|
+
const callId=`${this.#id}:${attemptId}:${index}`;
|
|
122
|
+
await event({type:'code_run_journal_call',index,call_id:callId,descriptor,descriptor_digest:digest(descriptor)});check();
|
|
123
|
+
const result=await invoke(callId);
|
|
124
|
+
const current=await load(),intent=current.rows.find(r=>r.type==='code_run_journal_call'&&r.call_id===callId),receipt=intent&&receiptFor(intent,current);
|
|
125
|
+
if(!receipt||receipt.output!==result)throw fail('EVIDENCE','A complete matching native result is required before delivery to the VM.');
|
|
126
|
+
await reference(index,receipt);check();cursor=index;
|
|
127
|
+
return {result,call_id:callId,contract_digest:dispatchDigest,observed_at:receipt.timestamp,reused:false};
|
|
128
|
+
},
|
|
129
|
+
finish:()=>{check();if(cursor<prefix)throw fail('DIVERGED','The script ended before consuming its original call prefix.');},
|
|
130
|
+
close:async()=>{await lock.release();this.#busy=false;}
|
|
131
|
+
};
|
|
132
|
+
return runtime;
|
|
133
|
+
}catch(error){try{await lock?.release();}finally{this.#busy=false;}throw error;}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {digest} from './operations.mjs';
|
|
2
|
+
|
|
3
|
+
// Incarnation IDs remain in native contracts. Only this compatibility comparison
|
|
4
|
+
// removes them; an independently pinned implementation/configuration must match too.
|
|
5
|
+
export function codeRunContractShape(value) {
|
|
6
|
+
const {revision,contract_digest,...contract}=value;
|
|
7
|
+
if(contract.origin){const {generation,...origin}=contract.origin;contract.origin=origin;}
|
|
8
|
+
return contract;
|
|
9
|
+
}
|
|
10
|
+
export function codeRunSessionPlan(rows,p,index=rows.length) {
|
|
11
|
+
const prefix=rows.slice(0,index),birth=prefix.find(r=>r.type==='code_run_session_bound'&&r.invocation_id===p.invocation_id);
|
|
12
|
+
if(!birth||birth.source!=='code_run_session_host'||p.operation!=='CodeRun'||p.parent_invocation_id!==null||birth.input_digest!==p.arguments_digest||digest(birth.input)!==p.arguments_digest||birth.session_id!==p.session_id||birth.house!==p.house||digest(birth.actor)!==digest(p.actor))return null;
|
|
13
|
+
const profile=prefix.find(r=>r.type==='code_run_session_profile'&&r.profile_digest===birth.profile_digest);
|
|
14
|
+
const rootContract=prefix.findLast(r=>r.type==='house_contract'&&r.contract_digest===p.contract_digest)?.contract;
|
|
15
|
+
if(!profile||digest(profile.identity)!==birth.profile_digest||rootContract?.revision!=='code-run-session/1:'+birth.profile_digest||rootContract.origin!==null||rootContract.permission!=='CodeRun'||!rootContract.steps)return null;
|
|
16
|
+
const preparations=prefix.filter(r=>r.type==='code_run_session_recovery'&&r.original_invocation_id===p.invocation_id&&r.journal_id===birth.journal_id&&r.profile_digest===birth.profile_digest);
|
|
17
|
+
for(const prepared of preparations.toReversed()) {
|
|
18
|
+
if(prepared.source!=='code_run_session_host')return null;
|
|
19
|
+
const result=prefix.findLast(r=>r.type==='house_receipt'&&r.call_id===prepared.call_id&&r.operation==='CodeRun'&&digest(r.actor)===digest(p.actor)&&r.session_id===p.session_id&&r.house===p.house);
|
|
20
|
+
const bound=result&&prefix.find(r=>r.type==='code_run_session_bound'&&r.invocation_id===result.invocation_id);
|
|
21
|
+
if(!result||result.arguments_digest!==p.arguments_digest||prepared.input_digest!==p.arguments_digest)continue;
|
|
22
|
+
const continuationContract=prefix.findLast(r=>r.type==='house_contract'&&r.contract_digest===result.contract_digest)?.contract;
|
|
23
|
+
if(continuationContract?.revision!=='code-run-session/1:'+birth.profile_digest||continuationContract.origin!==null||!continuationContract.steps||continuationContract.permission!=='CodeRun')return null;
|
|
24
|
+
if(bound&&(bound.original_invocation_id!==p.invocation_id||bound.journal_id!==birth.journal_id||bound.profile_digest!==birth.profile_digest||bound.input_digest!==p.arguments_digest||digest(bound.input)!==p.arguments_digest))continue;
|
|
25
|
+
if(!bound&&(result.status==='returned'||prefix.some(r=>r.type==='house_proposal'&&r.parent_invocation_id===result.invocation_id)))continue;
|
|
26
|
+
if(digest(birth.catalogue.map(codeRunContractShape))!==digest((bound?.catalogue||prepared.catalogue).map(codeRunContractShape)))return null;
|
|
27
|
+
const roots=[p.invocation_id,...prefix.filter(r=>r.type==='code_run_session_bound'&&r.original_invocation_id===p.invocation_id&&r.journal_id===birth.journal_id&&r.profile_digest===birth.profile_digest).map(r=>r.invocation_id)];
|
|
28
|
+
for(const member of prefix.filter(r=>r.type==='code_run_session_bound'&&roots.includes(r.invocation_id)))if(member.source!=='code_run_session_host'||member.house!==p.house||member.session_id!==p.session_id||digest(member.actor)!==digest(p.actor)||member.input_digest!==p.arguments_digest||digest(member.input)!==p.arguments_digest||digest(member.catalogue.map(codeRunContractShape))!==digest(birth.catalogue.map(codeRunContractShape)))return null;
|
|
29
|
+
const descendants=new Set(roots);
|
|
30
|
+
for(const row of prefix)if(row.type==='house_proposal'&&descendants.has(row.parent_invocation_id))descendants.add(row.invocation_id);
|
|
31
|
+
const proposed=prefix.filter(r=>r.type==='house_proposal'&&descendants.has(r.invocation_id)&&!roots.includes(r.invocation_id));
|
|
32
|
+
for(const child of proposed) {
|
|
33
|
+
const contract=prefix.findLast(r=>r.type==='house_contract'&&r.contract_digest===child.contract_digest)?.contract;
|
|
34
|
+
const owner=prefix.find(r=>r.type==='code_run_session_bound'&&r.invocation_id===child.parent_invocation_id);
|
|
35
|
+
if(!contract||contract.steps||!['none','read'].includes(contract.effects?.workspace)||!['none','read'].includes(contract.effects?.external)||contract.effects?.authority!=='none'||!owner?.catalogue.some(c=>c.name===contract.name&&c.contract_digest===child.contract_digest))return null;
|
|
36
|
+
}
|
|
37
|
+
const final=result.status==='returned'?prefix.findLast(r=>r.type==='code_run_result'&&r.invocation_id===result.invocation_id&&r.source_ref===digest(birth.input.code)&&r.status==='returned'):null;
|
|
38
|
+
if(result.status==='returned'&&(!final||JSON.stringify(JSON.parse(result.output).output)!==JSON.stringify(final.output)))return null;
|
|
39
|
+
return {status:result.status==='returned'?'returned':'refused',journal_id:birth.journal_id,birth_ref:prefix.indexOf(birth),preparation_ref:prefix.indexOf(prepared),continuation_ref:prefix.indexOf(result),
|
|
40
|
+
output:result.status==='returned'?result.output:undefined,error:result.status==='returned'?undefined:JSON.stringify({code:'HOUSE_RECOVERY_CONTINUE',original_handler_return:'unavailable',continuation_status:result.status,reason:result.error,asked:'unjudged'}),
|
|
41
|
+
roots,descendants:[...descendants].filter(id=>!roots.includes(id))};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
export function codeRunSessionRecoveryValid(rows,index,row) {
|
|
46
|
+
const r=row.recovery,root=rows[r.root_ref],plan=root&&codeRunSessionPlan(rows,root,index);
|
|
47
|
+
if(!plan||r.plan_digest!==digest(plan)||!['code_run_journal','code_run_journal_read'].includes(r.kind))return false;
|
|
48
|
+
if(r.kind==='code_run_journal_read')return plan.descendants.includes(row.invocation_id)&&row.status==='refused'&&row.code==='HOUSE_RECOVERY_READ_RETRY'&&row.effects_observed==='read_only'&&row.observations?.length===0;
|
|
49
|
+
return plan.roots.includes(row.invocation_id)&&row.status===plan.status&&row.asked==='unjudged'&&row.observations?.length===0&&(plan.status==='returned'?row.output===plan.output&&row.output_digest===digest(plan.output)&&row.output_truncated===false&&row.judgment_scope==='reconstructed_read_only_script_response'&&row.authority==='reconstructed_governed_continuation'&&row.disposition==='serve':row.code==='HOUSE_RECOVERY_CONTINUE'&&row.error===plan.error&&row.effects_observed==='read_only');
|
|
50
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
2
|
+
import {randomUUID} from 'node:crypto';
|
|
3
|
+
import {digest} from './operations.mjs';
|
|
4
|
+
import {CodeRunJournal,CODE_RUN_JOURNAL_MAX_ATTEMPTS} from './code-run-journal.mjs';
|
|
5
|
+
import {codeRunDefinition,codeRunEligible,runCode,CODE_RUN_LIMITS} from './code-run.mjs';
|
|
6
|
+
import {codeRunArtifactIdentity} from './code-run-artifact.mjs';
|
|
7
|
+
import {codeRunContractShape,codeRunSessionPlan} from './code-run-session-proof.mjs';
|
|
8
|
+
import {auditExecution} from './audit.mjs';
|
|
9
|
+
import {processDead,processIdentity} from './process-lock.mjs';
|
|
10
|
+
import {recoveryReceiptValid} from './file-recovery-proof.mjs';
|
|
11
|
+
|
|
12
|
+
const fail=(code,message)=>Object.assign(new Error(message),{code});
|
|
13
|
+
|
|
14
|
+
// Startup-only native experiment. Current registry generations are never rewritten.
|
|
15
|
+
export function installCodeRunSession({tools,store,house,sessionId,actor,toolNames,replaySafe=[],configuration={},onCheckpoint=async()=>{},strategy='journal'}) {
|
|
16
|
+
if(!Array.isArray(toolNames)||!toolNames.length||toolNames.length>8||toolNames.some(n=>typeof n!=='string')||replaySafe.some(n=>!toolNames.includes(n)))throw Error('Select one to eight explicit observation names.');
|
|
17
|
+
if(!['journal','restart'].includes(strategy)||strategy==='restart'&&toolNames.some(n=>!replaySafe.includes(n)))throw Error('Whole-script comparison retry requires explicit replay safety for every selected observation.');
|
|
18
|
+
const artifact=codeRunArtifactIdentity({configuration,strategy,toolNames:[...toolNames].sort(),replaySafe:[...replaySafe].sort()});
|
|
19
|
+
const context=new AsyncLocalStorage();
|
|
20
|
+
const id=()=>typeof sessionId==='function'?sessionId():sessionId;
|
|
21
|
+
const catalogue=()=>{
|
|
22
|
+
const visible=tools.executor.catalogue().filter(c=>toolNames.includes(c.name));
|
|
23
|
+
if(visible.length!==toolNames.length||visible.some(c=>!codeRunEligible(c)))throw fail('CODE_RUN_SESSION_SCOPE','The current caller does not expose the complete selected read-only surface.');
|
|
24
|
+
for(const c of visible) {
|
|
25
|
+
if(c.origin===null&&!tools.isNativeOperation(c.name))throw fail('CODE_RUN_SESSION_SCOPE','Only native workspace operations are supported by this profile.');
|
|
26
|
+
if(c.origin&&(c.origin.plugin!=='chrome-lab'||c.origin.module_sha256!==artifact.identity.files.find(([name])=>name==='src/plugins/chrome-lab/index.mjs')?.[1]))throw fail('CODE_RUN_SESSION_SCOPE','The selected plugin implementation is outside the pinned native artifact.');
|
|
27
|
+
}
|
|
28
|
+
return visible.sort((a,b)=>a.name.localeCompare(b.name,'en'));
|
|
29
|
+
};
|
|
30
|
+
const verifyBook=book=>{if(!book.journalAudit.verified||auditExecution(book,house.stateContext()).violations.length)throw fail('AUDIT_INVALID','The session recovery book does not verify.');};
|
|
31
|
+
house.registry.replace(['CodeRun'],[{...codeRunDefinition,description:codeRunDefinition.description+' This host manages sequential read-only recovery. Recovery call_index is the 1-based tool-call order, not an output-field index. historical_reused identifies a dated prior observation; observed_this_attempt identifies a new observation in this execution, never proof of current or atomic DOM state.',revision:'code-run-session/1:'+artifact.digest,run:async(input,options)=>{
|
|
32
|
+
const sid=id(),book=await store.load(sid);verifyBook(book);
|
|
33
|
+
if(options.sessionId!==sid||digest(options.actor)!==digest(actor))throw fail('CODE_RUN_SESSION_OWNER','Session recovery needs its original host actor.');
|
|
34
|
+
const selected=catalogue(),resume=context.getStore();
|
|
35
|
+
const original=resume?.birth;
|
|
36
|
+
if(original&&(original.profile_digest!==artifact.digest||digest(original.catalogue.map(codeRunContractShape))!==digest(selected.map(codeRunContractShape))))throw fail('CODE_RUN_SESSION_DRIFT','Source, installed VM, plugin configuration or observation contracts changed.');
|
|
37
|
+
if(original&&(Date.now()>=original.deadline_ms||book.rows.filter(r=>r.type==='code_run_session_recovery'&&r.original_invocation_id===resume.original.invocation_id).length>=CODE_RUN_JOURNAL_MAX_ATTEMPTS))throw fail('CODE_RUN_SESSION_BUDGET','The original read-only execution deadline or five-attempt budget is exhausted.');
|
|
38
|
+
if(!book.rows.some(r=>r.type==='code_run_session_profile'&&r.profile_digest===artifact.digest))await options.recordWork({type:'code_run_session_profile',source:'code_run_session_host',profile_digest:artifact.digest,identity:artifact.identity});
|
|
39
|
+
const journalId=original?.journal_id||randomUUID();
|
|
40
|
+
const deadline=original?.deadline_ms||Date.now()+CODE_RUN_LIMITS.wallMs;
|
|
41
|
+
await options.recordWork({type:'code_run_session_bound',source:'code_run_session_host',profile_digest:artifact.digest,journal_id:journalId,input, input_digest:digest(input),deadline_ms:deadline,catalogue:selected,...(resume?{original_invocation_id:resume.original.invocation_id}:{})});
|
|
42
|
+
const journal=strategy==='journal'?new CodeRunJournal({store,sessionId:sid,id:journalId,replaySafe,compatibility:artifact.digest}):undefined;
|
|
43
|
+
// Enforce the original deadline during execution too, including the comparison
|
|
44
|
+
// strategy which has no journal timer of its own. A fresh VM gets no new budget.
|
|
45
|
+
const budget=new AbortController(),timer=setTimeout(()=>budget.abort(fail('CODE_RUN_SESSION_BUDGET','The original read-only execution deadline expired.')),Math.max(0,deadline-Date.now()));
|
|
46
|
+
const signal=options.signal?AbortSignal.any([options.signal,budget.signal]):budget.signal;
|
|
47
|
+
try {return await runCode(input,{...options,signal,codeRunJournal:journal,
|
|
48
|
+
recordWork:async event=>{await options.recordWork(event);await onCheckpoint(event);},
|
|
49
|
+
onCodeRunObservation:onCheckpoint,
|
|
50
|
+
// VM metadata stays byte-identical; actual dispatch still uses this invocation's
|
|
51
|
+
// fresh House snapshot and records its real current contract digest.
|
|
52
|
+
catalogue:strategy==='journal'?(original?.catalogue||selected):selected,
|
|
53
|
+
codeRunDispatchContracts:Object.fromEntries(selected.map(c=>[c.name,c.contract_digest]))});
|
|
54
|
+
} finally {clearTimeout(timer);}
|
|
55
|
+
}}]);
|
|
56
|
+
|
|
57
|
+
const recover=async({signal}={})=>store.workTransaction(id(),async()=>{
|
|
58
|
+
let book=await store.load(id());verifyBook(book);
|
|
59
|
+
const roots=book.rows.filter(r=>r.type==='code_run_session_bound'&&!r.original_invocation_id);
|
|
60
|
+
for(const birth of roots) {
|
|
61
|
+
book=await store.load(id());verifyBook(book);
|
|
62
|
+
const original=book.rows.find(r=>r.type==='house_proposal'&&r.invocation_id===birth.invocation_id);
|
|
63
|
+
if(!original||original.parent_invocation_id!==null||book.rows.some(r=>r.type==='house_receipt'&&r.invocation_id===original.invocation_id))continue;
|
|
64
|
+
if(!processDead(original.execution_process))throw fail('CODE_RUN_SESSION_OWNER','The original CodeRun process is not proven dead.');
|
|
65
|
+
let plan=codeRunSessionPlan(book.rows,original);
|
|
66
|
+
if(!plan) {
|
|
67
|
+
const current=catalogue();
|
|
68
|
+
if(birth.profile_digest!==artifact.digest||digest(birth.catalogue.map(codeRunContractShape))!==digest(current.map(codeRunContractShape)))throw fail('CODE_RUN_SESSION_DRIFT','Recovery implementation/configuration/contracts differ; no observation was dispatched.');
|
|
69
|
+
const callId='code-run-recovery-'+randomUUID();
|
|
70
|
+
await store.append(id(),{type:'code_run_session_recovery',source:'code_run_session_host',original_invocation_id:original.invocation_id,journal_id:birth.journal_id,profile_digest:artifact.digest,input_digest:original.arguments_digest,catalogue:current,call_id:callId});
|
|
71
|
+
try{await context.run({birth,original},()=>tools.execute('CodeRun',birth.input,{callId,signal}));}
|
|
72
|
+
catch(error){if(signal?.aborted)throw error;/* A durable refusal is delivered below, never converted to success. */}
|
|
73
|
+
book=await store.load(id());verifyBook(book);plan=codeRunSessionPlan(book.rows,original);
|
|
74
|
+
}
|
|
75
|
+
if(!plan)throw fail('HOUSE_RECOVERY_UNCERTAIN','No complete compatible CodeRun continuation outcome is available.');
|
|
76
|
+
const targets=book.rows.filter(r=>r.type==='house_proposal'&&[...plan.roots,...plan.descendants].includes(r.invocation_id)).toReversed();
|
|
77
|
+
for(const proposal of targets) {
|
|
78
|
+
book=await store.load(id());
|
|
79
|
+
if(book.rows.some(r=>r.type==='house_receipt'&&r.invocation_id===proposal.invocation_id))continue;
|
|
80
|
+
if(!processDead(proposal.execution_process))throw fail('CODE_RUN_SESSION_OWNER','A related original process is not proven dead.');
|
|
81
|
+
const {audit:ignored,type,timestamp,...base}=proposal,isRoot=plan.roots.includes(proposal.invocation_id);
|
|
82
|
+
const recovery={version:1,kind:isRoot?'code_run_journal':'code_run_journal_read',proposal_ref:book.rows.indexOf(proposal),root_ref:book.rows.findIndex(r=>r.type==='house_proposal'&&r.invocation_id===original.invocation_id),original_process:proposal.execution_process,process_state:'dead',recovered_by:{actor,process:processIdentity},plan_digest:digest(plan)};
|
|
83
|
+
// Reloaded row objects are not reference-equal to the earlier target list.
|
|
84
|
+
recovery.proposal_ref=book.rows.findIndex(r=>r.type==='house_proposal'&&r.invocation_id===proposal.invocation_id);
|
|
85
|
+
const contract=book.rows.findLast(r=>r.type==='house_contract'&&r.contract_digest===proposal.contract_digest)?.contract;
|
|
86
|
+
const returned=isRoot&&plan.status==='returned';
|
|
87
|
+
const closure={...base,type:'house_receipt',source:'recovery_host',recovery,status:returned?'returned':'refused',observations:[],asked:'unjudged',
|
|
88
|
+
...(returned?{output:plan.output,output_digest:digest(plan.output),output_truncated:false,effects:contract.effects,authority:'reconstructed_governed_continuation',judgment_scope:'reconstructed_read_only_script_response',disposition:'serve'}:
|
|
89
|
+
{code:isRoot?'HOUSE_RECOVERY_CONTINUE':'HOUSE_RECOVERY_READ_RETRY',error:isRoot?plan.error:'Original read ended without a durable return; a separate governed continuation is recorded.',effects_observed:'read_only'})};
|
|
90
|
+
if(!recoveryReceiptValid(book.rows,book.rows.length,closure))throw fail('AUDIT_INVALID','The CodeRun recovery linkage does not verify.');
|
|
91
|
+
await store.append(id(),closure);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
tools.codeRunSessionRecovery={recover,profile:artifact.digest};
|
|
96
|
+
return tools.codeRunSessionRecovery;
|
|
97
|
+
}
|
package/src/code-run-worker.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
2
|
import { getQuickJS } from 'quickjs-emscripten';
|
|
3
3
|
|
|
4
|
-
const { code, catalogue, limits } = workerData;
|
|
4
|
+
const { code, catalogue, limits, determinism } = workerData;
|
|
5
5
|
const module = await getQuickJS(), runtime = module.newRuntime();
|
|
6
6
|
runtime.setMemoryLimit(limits.memoryBytes); runtime.setMaxStackSize(limits.stackBytes);
|
|
7
7
|
let cpuLeft = limits.cpuMs, activeStart = performance.now(), stopped = false, sequence = 0, emitted = 2;
|
|
@@ -13,7 +13,10 @@ const expose = (name, fn) => { const handle = vm.newFunction(name, fn); vm.setPr
|
|
|
13
13
|
const jsonHandle = value => vm.unwrapResult(enter(() => vm.evalCode(`JSON.parse(${JSON.stringify(JSON.stringify(value))})`)));
|
|
14
14
|
function fail(error) {
|
|
15
15
|
if (stopped) return; stopped = true;
|
|
16
|
-
|
|
16
|
+
let message = error.message || String(error);
|
|
17
|
+
// Gate «extensión de bóveda»: QuickJS solo dice «not a function» (p. ej. `tools.searchTools(...)` antes
|
|
18
|
+
// de existir el alias); el modelo necesita saber qué hay para no repetir.
|
|
19
|
+
if (/not a function/.test(message)) message += ' — tools only has the visible tool names (tools.Read(args)), tools.call(name,args), tools.searchTools(query) and tools.describeTool(name); searchTools/describeTool also exist as globals.';
|
|
17
20
|
const code = error.code || (cpuLeft <= 0 || /interrupted/.test(message) ? 'CODE_RUN_CPU_LIMIT' : /out of memory|allocation failed/i.test(message) ? 'CODE_RUN_MEMORY_LIMIT' : /stack overflow/i.test(message) ? 'CODE_RUN_STACK_LIMIT' : /output byte limit/.test(message) ? 'CODE_RUN_OUTPUT_LIMIT' : /call\/concurrency limit/.test(message) ? 'CODE_RUN_CALL_LIMIT' : /argument byte limit/.test(message) ? 'CODE_RUN_ARGUMENT_LIMIT' : /outside the read-only caller catalogue/.test(message) ? 'CODE_RUN_TOOL_UNAVAILABLE' : 'CODE_RUN_SCRIPT');
|
|
18
21
|
parentPort.postMessage({ type: 'error', code, message, output });
|
|
19
22
|
}
|
|
@@ -34,14 +37,27 @@ expose('__call', (nameHandle, argsHandle) => {
|
|
|
34
37
|
});
|
|
35
38
|
let script;
|
|
36
39
|
try {
|
|
40
|
+
if(determinism)vm.unwrapResult(enter(()=>vm.evalCode(`(function(){
|
|
41
|
+
const NativeDate=globalThis.Date,clock=${JSON.stringify(determinism.clock_ms)};
|
|
42
|
+
function StableDate(...args){
|
|
43
|
+
if(!new.target)return new NativeDate(clock).toString();
|
|
44
|
+
return Reflect.construct(NativeDate,args.length?args:[clock],new.target===StableDate?NativeDate:new.target);
|
|
45
|
+
}
|
|
46
|
+
StableDate.prototype=NativeDate.prototype;
|
|
47
|
+
Object.defineProperty(StableDate.prototype,'constructor',{value:StableDate,writable:false,configurable:false});
|
|
48
|
+
Object.defineProperties(StableDate,{now:{value:()=>clock},parse:{value:NativeDate.parse},UTC:{value:NativeDate.UTC}});
|
|
49
|
+
Object.defineProperty(globalThis,'Date',{value:StableDate,writable:false,configurable:false});
|
|
50
|
+
let seed=${JSON.stringify(determinism.seed)};const imul=Math.imul;
|
|
51
|
+
Object.defineProperty(Math,'random',{value:()=>{seed=(imul(1664525,seed)+1013904223)>>>0;return seed/4294967296;},writable:false,configurable:false});
|
|
52
|
+
})();`))).dispose();
|
|
37
53
|
// All metadata enters as JSON. Bindings expose only VM values, never native
|
|
38
54
|
// host objects or host function constructors. No module loader is installed.
|
|
39
55
|
vm.unwrapResult(enter(() => vm.evalCode(`
|
|
40
56
|
const ALL_TOOLS = Object.freeze(${JSON.stringify(catalogue)}.map(t => Object.freeze(t)));
|
|
41
57
|
const __allowed = new Set(ALL_TOOLS.map(t => t.name));
|
|
42
|
-
const tools = Object.freeze({...Object.fromEntries(ALL_TOOLS.map(t => [t.name, args => __call(t.name, JSON.stringify(args ?? {})).then(JSON.parse)])), call: (name,args) => __call(String(name),JSON.stringify(args ?? {})).then(JSON.parse)});
|
|
58
|
+
const tools = Object.freeze({...Object.fromEntries(ALL_TOOLS.map(t => [t.name, args => __call(t.name, JSON.stringify(args ?? {})).then(JSON.parse)])), call: (name,args) => __call(String(name),JSON.stringify(args ?? {})).then(JSON.parse), searchTools, describeTool});
|
|
43
59
|
function text(value) { const encoded = JSON.stringify(value); __emit(encoded === undefined ? 'null' : encoded); }
|
|
44
|
-
function searchTools(query = '') { return ALL_TOOLS.filter(t => (t.name+' '+t.description).toLowerCase().includes(
|
|
60
|
+
function searchTools(query = '') { const q = String(query && typeof query === 'object' ? (query.query ?? '') : query).toLowerCase(); return ALL_TOOLS.filter(t => (t.name+' '+t.description).toLowerCase().includes(q)).map(t => ({name:t.name,description:t.description})); }
|
|
45
61
|
function describeTool(name) { return ALL_TOOLS.find(t => t.name === name) || null; }
|
|
46
62
|
Object.freeze(tools); Object.freeze(searchTools); Object.freeze(describeTool);
|
|
47
63
|
`))).dispose();
|
package/src/code-run.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { digest } from './operations.mjs';
|
|
4
4
|
import { chromeLabObservation } from './read-only-check.mjs';
|
|
5
|
+
import { CodeRunJournal } from './code-run-journal.mjs';
|
|
5
6
|
|
|
6
7
|
export const CODE_RUN_LIMITS = Object.freeze({ sourceBytes: 32768, outputBytes: 16384, resultBytes: 1048576, argumentBytes: 65536, calls: 32, concurrency: 4, wallMs: 30000, cpuMs: 2000, memoryBytes: 33554432, stackBytes: 262144, cleanupMs: 2000 });
|
|
7
8
|
const ceiling = { ...CODE_RUN_LIMITS, wallMs: 60000, cpuMs: 5000, memoryBytes: 67108864 };
|
|
@@ -29,47 +30,70 @@ export async function runCode({ code }, options = {}) {
|
|
|
29
30
|
signal?.throwIfAborted();
|
|
30
31
|
const catalogue = options.catalogue.filter(codeRunEligible).map(row => ({ name: row.name, description: row.description, input_schema: row.input_schema, effects: row.effects, contract_digest: row.contract_digest }));
|
|
31
32
|
const visible = new Map(catalogue.map(row => [row.name, row]));
|
|
32
|
-
|
|
33
|
+
let runId = randomUUID(), journal;
|
|
34
|
+
const runtimeToken=randomUUID(),sourceRef = digest(code), controller = new AbortController();
|
|
33
35
|
const pending = new Map(), calls = []; let total = 0, closed = false, worker, timer;
|
|
34
36
|
if (activeRuns.size >= CODE_RUN_MAX_WORKERS) throw failure('CODE_RUN_BUSY', 'Two CodeRun runtimes are already active; wait for completion before proposing another run.');
|
|
35
|
-
activeRuns.add(
|
|
36
|
-
try {
|
|
37
|
-
|
|
37
|
+
activeRuns.add(runtimeToken);
|
|
38
|
+
try {
|
|
39
|
+
if(options.codeRunJournal) {
|
|
40
|
+
if(!(options.codeRunJournal instanceof CodeRunJournal))throw failure('CODE_RUN_JOURNAL_HOST','A native host journal is required.');
|
|
41
|
+
journal=await options.codeRunJournal.open({sourceRef,catalogue,limits,actor:options.actor,sessionId:options.sessionId,recordWork,signal});
|
|
42
|
+
runId=journal.runId;
|
|
43
|
+
}
|
|
44
|
+
await recordWork({ type: 'code_run_source', run_id: runId, ...(journal?{attempt_id:journal.attemptId}:{}),source_ref: sourceRef, source: code, limits, max_workers: CODE_RUN_MAX_WORKERS, catalogue: catalogue.map(({name,contract_digest}) => ({name,contract_digest})) });
|
|
45
|
+
} catch(error) { activeRuns.delete(runtimeToken);await journal?.close();throw error; }
|
|
38
46
|
let rejectRun;
|
|
39
47
|
const abort = () => { const error = signal?.reason instanceof Error ? signal.reason : failure('CODE_RUN_ABORTED', 'CodeRun was cancelled.'); controller.abort(error); rejectRun?.(error); };
|
|
40
48
|
signal?.addEventListener('abort', abort, { once: true });
|
|
41
49
|
let selected = [], runError;
|
|
42
50
|
try {
|
|
43
51
|
signal?.throwIfAborted();
|
|
44
|
-
worker = new Worker(new URL('./code-run-worker.mjs', import.meta.url), { workerData: { code, catalogue, limits }, resourceLimits: { maxOldGenerationSizeMb: 96, stackSizeMb: 4 }, execArgv: [] });
|
|
52
|
+
worker = new Worker(new URL('./code-run-worker.mjs', import.meta.url), { workerData: { code, catalogue, limits, ...(journal?{determinism:journal.determinism}: {}) }, resourceLimits: { maxOldGenerationSizeMb: 96, stackSizeMb: 4 }, execArgv: [] });
|
|
45
53
|
selected = await new Promise((resolve, reject) => {
|
|
46
54
|
rejectRun = reject;
|
|
47
|
-
timer = setTimeout(() => { const e = failure('CODE_RUN_TIMEOUT', 'CodeRun wall deadline exceeded.'); controller.abort(e); reject(e); }, limits.wallMs);
|
|
55
|
+
timer = setTimeout(() => { const e = failure('CODE_RUN_TIMEOUT', 'CodeRun wall deadline exceeded.'); controller.abort(e); reject(e); }, journal?Math.max(1,journal.deadlineMs-Date.now()):limits.wallMs);
|
|
48
56
|
worker.on('error', reject);
|
|
49
57
|
worker.on('exit', exit => { if (!closed && exit !== 0) reject(failure('CODE_RUN_WORKER', `CodeRun worker exited (${exit}).`)); });
|
|
50
58
|
worker.on('message', message => {
|
|
51
59
|
if (closed) return;
|
|
52
60
|
if (message.type === 'output') { selected.push(message.value); return; }
|
|
53
|
-
if (message.type === 'done') { resolve(message.output);
|
|
61
|
+
if (message.type === 'done') { try{journal?.finish();resolve(message.output);}catch(error){reject(error);}return; }
|
|
54
62
|
if (message.type === 'error') { selected = message.output || []; reject(failure(message.code || 'CODE_RUN_SCRIPT', message.message)); return; }
|
|
55
63
|
if (message.type !== 'call') return;
|
|
56
64
|
const { id, name, args } = message;
|
|
57
65
|
const answer = (ok, value, errorCode) => { if (!closed) worker.postMessage({ type: 'result', id, ok, value, code: errorCode }); };
|
|
58
66
|
if (controller.signal.aborted) { answer(false, 'CodeRun is stopping.', 'CODE_RUN_ABORTED'); return; }
|
|
59
67
|
if (!visible.has(name)) { answer(false, 'Tool is outside the read-only caller catalogue.', 'CODE_RUN_TOOL_UNAVAILABLE'); return; }
|
|
68
|
+
if(journal&&pending.size) {
|
|
69
|
+
const error=failure('CODE_RUN_JOURNAL_CONCURRENCY','The experimental journal accepts sequential awaited calls only.');
|
|
70
|
+
controller.abort(error);reject(error);return;
|
|
71
|
+
}
|
|
60
72
|
if (++total > limits.calls || pending.size >= limits.concurrency) { answer(false, 'CodeRun tool call/concurrency limit exceeded.', 'CODE_RUN_CALL_LIMIT'); return; }
|
|
61
73
|
if (Buffer.byteLength(JSON.stringify(args) ?? '') > limits.argumentBytes) { answer(false, 'CodeRun tool arguments exceed their byte limit.', 'CODE_RUN_ARGUMENT_LIMIT'); return; }
|
|
62
|
-
const callId =
|
|
74
|
+
const callId = journal?`${runId}:${journal.attemptId}:${id}`:`${runId}:${id}`, row = { call_id: callId, ...(journal?{call_index:id}:{}),operation: name, contract_digest: visible.get(name).contract_digest, arguments_digest: digest(args), status: 'started' }; calls.push(row);
|
|
63
75
|
const work = (async () => {
|
|
64
76
|
try {
|
|
65
77
|
// This callback runs in the parent operation's async context; House
|
|
66
78
|
// revalidates contracts, visibility and authority at the effect frontier.
|
|
67
|
-
const
|
|
68
|
-
|
|
79
|
+
const governed=id=>invoke(name,args,{signal:controller.signal,callId:id,modelProposed:true});
|
|
80
|
+
const observation=journal?await journal.call(id,name,args,visible.get(name).contract_digest,governed,options.codeRunDispatchContracts?.[name]||visible.get(name).contract_digest):{result:await governed(callId),call_id:callId};
|
|
81
|
+
const result=observation.result;
|
|
82
|
+
row.call_id=observation.call_id;row.status = observation.reused?'historical_reused':'returned'; row.output_ref = digest(result);
|
|
83
|
+
if(observation.contract_digest&&observation.contract_digest!==row.contract_digest) {
|
|
84
|
+
row.vm_contract_digest=row.contract_digest;row.contract_digest=observation.contract_digest;
|
|
85
|
+
}
|
|
86
|
+
if(observation.observed_at)row.observed_at=observation.observed_at;
|
|
87
|
+
if(observation.reused)Object.assign(row,{observed_at:observation.observed_at,receipt_invocation_id:observation.receipt_invocation_id,current:false});
|
|
69
88
|
const encoded = JSON.stringify(result);
|
|
70
89
|
if (Buffer.byteLength(encoded ?? '') > limits.resultBytes) throw failure('CODE_RUN_RESULT_LIMIT', 'Tool observation exceeds the VM transfer limit; the complete House receipt remains recorded.');
|
|
90
|
+
await options.onCodeRunObservation?.({type:'code_run_observation',index:id,name,call_id:row.call_id,reused:Boolean(observation.reused)});
|
|
71
91
|
answer(true, encoded ?? 'null');
|
|
72
|
-
} catch (error) {
|
|
92
|
+
} catch (error) {
|
|
93
|
+
row.status = row.status === 'returned' ? 'returned_untransferred' : 'failed_or_refused'; row.code = error.code;
|
|
94
|
+
if(error.code?.startsWith('CODE_RUN_JOURNAL_')){controller.abort(error);reject(error);}
|
|
95
|
+
answer(false, error.message, error.code);
|
|
96
|
+
}
|
|
73
97
|
})();
|
|
74
98
|
pending.set(id, work); work.finally(() => pending.delete(id));
|
|
75
99
|
});
|
|
@@ -84,14 +108,25 @@ export async function runCode({ code }, options = {}) {
|
|
|
84
108
|
const settled = await Promise.race([Promise.allSettled([...pending.values()]).then(() => true), new Promise(resolve => { cleanupTimer = setTimeout(() => resolve(false), limits.cleanupMs); })]); clearTimeout(cleanupTimer);
|
|
85
109
|
if (!settled) runError = failure('CODE_RUN_CLEANUP_PENDING', 'A host tool ignored cancellation; its outcome remains unknown/partial. Pending call IDs are recorded. Do not replay this script.');
|
|
86
110
|
}
|
|
87
|
-
activeRuns.delete(
|
|
111
|
+
activeRuns.delete(runtimeToken);
|
|
88
112
|
}
|
|
89
113
|
const outputRef = digest(selected);
|
|
90
|
-
const
|
|
114
|
+
const recovery=journal?{journal_id:runId,attempt_id:journal.attemptId,scope:'historical_observation_reconstruction',reused_observations:structuredClone(journal.reused)}:null;
|
|
115
|
+
const outcome = { run_id: runId, source_ref: sourceRef, output_ref: outputRef, output: selected, status: runError ? 'failed' : 'returned', code: runError?.code, error: runError?.message, calls: structuredClone(calls), pending_calls: [...pending.keys()].map(id => journal?`${runId}:${journal.attemptId}:${id}`:`${runId}:${id}`), replayed: Boolean(journal?.reused.length),...(recovery?{recovery}:{}) };
|
|
116
|
+
try {
|
|
91
117
|
await recordWork({ type: 'code_run_result', ...outcome });
|
|
92
118
|
if (runError) {
|
|
93
119
|
const details = { ...outcome, message: runError.message, next_action: 'Inspect this run and individual House receipts using the recorded call IDs. Preserve completed observations. Do not replay the script automatically; pending host calls may still have unknown/partial outcomes.' };
|
|
94
120
|
const error = failure(runError.code || 'CODE_RUN_SCRIPT', JSON.stringify(details)); error.run_id = runId; error.details = details; throw error;
|
|
95
121
|
}
|
|
96
|
-
|
|
122
|
+
const modelRecovery=recovery?{scope:recovery.scope,
|
|
123
|
+
reused_observations:recovery.reused_observations.map(({call_index,operation,observed_at,current,receipt_invocation_id})=>({call_index,operation,observed_at,current,receipt_invocation_id})),
|
|
124
|
+
call_observations:calls.map(row=>({call_index:row.call_index,operation:row.operation,state:row.status==='returned'?'observed_this_attempt':row.status,observed_at:row.observed_at}))}:null;
|
|
125
|
+
return JSON.stringify({ run_id: runId, output: selected, source_ref: sourceRef, output_ref: outputRef, calls: calls.length,...(modelRecovery?{recovery:modelRecovery}:{}) });
|
|
126
|
+
} finally {
|
|
127
|
+
if(journal) {
|
|
128
|
+
if(pending.size)void Promise.allSettled([...pending.values()]).then(()=>journal.close()).catch(()=>{});
|
|
129
|
+
else await journal.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
97
132
|
}
|
package/src/codex-provider.mjs
CHANGED
|
@@ -176,7 +176,7 @@ export class CodexProvider {
|
|
|
176
176
|
async getVisionCapability({ signal } = {}) { const state = await this.discoverModelCapabilities({ signal }); return { supported: state.capabilities.vision === true, source: this.vision !== 'auto' ? 'explicit_user_configuration' : 'codex_models', model: this.model, imageTokens: this.imageTokens }; }
|
|
177
177
|
getCapabilities() { return this.modelCapabilities?.model === this.model ? { ...this.modelCapabilities.capabilities, vision: this.modelCapabilities.capabilities.vision === true } : { text: true, tools: true, ...this.capabilitiesConfigured, vision: this.vision === 'on', video: false, web_search: false }; }
|
|
178
178
|
analyzeMedia(request) { return analyzeImages(this, request); }
|
|
179
|
-
async complete({ system, messages, tools = [], schemaCatalog = [], contextIdentity, signal, onText = () => {}, onProgress = () => {}, purpose = 'worker', maxTokens = this.maxTokens }) {
|
|
179
|
+
async complete({ system, messages, tools = [], schemaCatalog = [], contextIdentity, signal, onText = () => {}, onProgress = () => {}, onReasoning = () => {}, onToolCall = () => {}, purpose = 'worker', maxTokens = this.maxTokens }) {
|
|
180
180
|
if (this.capabilitiesConfigured.tools === false && tools.length) throw fail('The Codex profile does not enable tools; the request was not sent.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
|
|
181
181
|
await this.discoverModel({ signal });
|
|
182
182
|
const accountingRequest={system,messages,tools,maxTokens},identity={...accountingIdentity(this),projection_epoch:contextIdentity?.projection_epoch ?? null};
|
|
@@ -205,15 +205,15 @@ export class CodexProvider {
|
|
|
205
205
|
let result, textChars = 0, reasoningChars = 0, argumentChars = 0, toolName;
|
|
206
206
|
for await (const event of sseEvents(framed.body)) {
|
|
207
207
|
if (event.type === 'response.output_text.delta') { onText(event.delta || ''); textChars += (event.delta || '').length; deadline.touch(); }
|
|
208
|
-
else if (event.type === 'response.reasoning_summary_text.delta') { reasoningChars += (event.delta || '').length; deadline.touch(); }
|
|
208
|
+
else if (event.type === 'response.reasoning_summary_text.delta') { reasoningChars += (event.delta || '').length; deadline.touch(); onReasoning(); }
|
|
209
209
|
else if (event.type === 'response.function_call_arguments.delta') { argumentChars += (event.delta || '').length; deadline.touch(); }
|
|
210
|
-
else if (event.type === 'response.output_item.added') { toolName = event.item?.type === 'function_call' && typeof event.item.name === 'string' ? event.item.name : undefined; }
|
|
210
|
+
else if (event.type === 'response.output_item.added') { if (event.item?.type === 'function_call') onToolCall(); toolName = event.item?.type === 'function_call' && typeof event.item.name === 'string' ? event.item.name : undefined; }
|
|
211
211
|
else if (event.type === 'response.completed') result = normalized(event.response);
|
|
212
212
|
else if (['response.failed', 'response.incomplete', 'error'].includes(event.type)) { const failure = responseFailure(event); if (failure.code === 'OUTPUT_LENGTH') failure.details = { ...failure.details, text_chars: textChars, reasoning_chars: reasoningChars, tool_argument_chars: argumentChars, ...(toolName ? { tool_name: toolName } : {}) }; throw failure; }
|
|
213
213
|
onProgress({ text_chars: textChars, reasoning_chars: reasoningChars, tool_argument_chars: argumentChars, elapsed_ms: deadline.elapsedMs(), purpose });
|
|
214
214
|
}
|
|
215
215
|
deadline.signal.throwIfAborted();
|
|
216
|
-
if (!result) throw fail('Codex streaming was interrupted; no tools were executed.', '
|
|
216
|
+
if (!result) throw fail('Codex streaming was interrupted; no tools were executed.', 'MODEL_STREAM_ERROR');
|
|
217
217
|
return account(result);
|
|
218
218
|
} catch (error) { if(error.usage_accounting)error.usage_accounting=bindUsageAccounting(error.usage_accounting,accountingRequest,identity,body);throw deadline.error(error); } finally { deadline.close(); }
|
|
219
219
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// One-line notices for automatic compactions. Pure presentation: no files, no
|
|
2
|
+
// provider, no authority; safe to import from the TUI.
|
|
3
|
+
import { t, registerMessages } from './i18n/index.mjs';
|
|
4
|
+
const kilo = value => !Number.isFinite(value) ? '?' : value >= 10000 ? `${Math.round(value / 1000)}k` : String(Math.round(value));
|
|
5
|
+
const TRIGGER_TEXT = {
|
|
6
|
+
soft_turn_end: 'Context compacted at the end of the turn · ~{v0} → ~{v1} tokens · compaction window {v2}',
|
|
7
|
+
idle_cache_expired: 'Context compacted after {v3} min idle (prompt cache expired) · ~{v0} → ~{v1} tokens · compaction window {v2}',
|
|
8
|
+
hard: 'Context compacted at the hard limit · ~{v0} → ~{v1} tokens · compaction window {v2}',
|
|
9
|
+
recovery: 'Context compacted to recover a context-length rejection · ~{v0} → ~{v1} tokens',
|
|
10
|
+
manual: 'Context compacted on request · ~{v0} → ~{v1} tokens',
|
|
11
|
+
};
|
|
12
|
+
registerMessages({
|
|
13
|
+
[TRIGGER_TEXT.soft_turn_end]: 'Contexto compactado al terminar el turno · ~{v0} → ~{v1} tokens · ventana de compactación {v2}',
|
|
14
|
+
[TRIGGER_TEXT.idle_cache_expired]: 'Contexto compactado tras {v3} min de inactividad (caché expirada) · ~{v0} → ~{v1} tokens · ventana de compactación {v2}',
|
|
15
|
+
[TRIGGER_TEXT.hard]: 'Contexto compactado en el límite duro · ~{v0} → ~{v1} tokens · ventana de compactación {v2}',
|
|
16
|
+
[TRIGGER_TEXT.recovery]: 'Contexto compactado para recuperar un rechazo por longitud · ~{v0} → ~{v1} tokens',
|
|
17
|
+
[TRIGGER_TEXT.manual]: 'Contexto compactado a pedido · ~{v0} → ~{v1} tokens',
|
|
18
|
+
});
|
|
19
|
+
export function compactionNotice(event) {
|
|
20
|
+
const template = TRIGGER_TEXT[event.compaction_trigger]; if (!template) return null;
|
|
21
|
+
return t(template, { v0: kilo(event.before_tokens), v1: kilo(event.after_tokens), v2: kilo(event.compaction?.compaction_window), v3: Math.round((event.compaction?.idle_ms || 0) / 60000) });
|
|
22
|
+
}
|