create-walle 0.10.0 → 0.10.1
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 +84 -41
- package/lib/runtime-closure-policy.json +2 -2
- package/package.json +10 -4
- package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
- package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
- package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
- package/template/coding-task-manager/lib/client-diag-handler.js +20 -0
- package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
- package/template/coding-task-manager/lib/codex-local-links.js +267 -0
- package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
- package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
- package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
- package/template/coding-task-manager/lib/feature-manifest.js +0 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +38 -11
- package/template/coding-task-manager/lib/headless-term-service.js +5 -6
- package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
- package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
- package/template/coding-task-manager/lib/lead-model-switch.js +8 -0
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
- package/template/coding-task-manager/lib/login-tripwire.js +17 -0
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +4 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
- package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
- package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
- package/template/coding-task-manager/lib/native-history-control.js +60 -12
- package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
- package/template/coding-task-manager/lib/raw-history-block.js +3 -1
- package/template/coding-task-manager/lib/read-pool-client.js +31 -22
- package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
- package/template/coding-task-manager/lib/remote-command-runtime.js +5 -4
- package/template/coding-task-manager/lib/root-prompt-admission.js +10 -9
- package/template/coding-task-manager/lib/root-prompt-flow.js +8 -5
- package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
- package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
- package/template/coding-task-manager/lib/session-create/provider-process.js +4 -2
- package/template/coding-task-manager/lib/session-lifecycle.js +2 -50
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +59 -3
- package/template/coding-task-manager/lib/session-resource-sampler.js +48 -0
- package/template/coding-task-manager/lib/session-status.js +2 -2
- package/template/coding-task-manager/lib/typing-interaction-trace.js +44 -2
- package/template/coding-task-manager/lib/typing-worker-stages.js +148 -1
- package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
- package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
- package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
- package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
- package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
- package/template/coding-task-manager/lib/ws/input.js +1 -0
- package/template/coding-task-manager/protocol/message-table.json +0 -10
- package/template/coding-task-manager/protocol/messages.js +0 -10
- package/template/coding-task-manager/protocol/ws-schema.json +0 -20
- package/template/coding-task-manager/protocol/ws-schema.md +0 -2
- package/template/coding-task-manager/public/css/app.css +0 -1
- package/template/coding-task-manager/public/index.html +3 -17
- package/template/coding-task-manager/public/js/client-health-core.js +81 -0
- package/template/coding-task-manager/public/js/client-health-main.js +61 -0
- package/template/coding-task-manager/public/js/prompt-submission-client.js +5 -1
- package/template/coding-task-manager/public/js/protocol.js +0 -10
- package/template/coding-task-manager/public/js/row-join.js +14 -1
- package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
- package/template/coding-task-manager/public/js/terminal-history-viewport.js +0 -2
- package/template/coding-task-manager/server.js +62 -186
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/wall-e/coding/host-capabilities.js +2 -2
- package/template/wall-e/fly.toml +1 -1
- package/template/wall-e/tools/local-tools.js +6 -1
- package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* WP-2 D-5/D-6 (docs/codex-lead-routed-switch-history-handoff-2026-10-21.html §6.3, §5 D-5/D-6).
|
|
5
|
+
*
|
|
6
|
+
* A GPT Codex lead whose last compaction holds an OpenAI-encrypted `compaction` item cannot carry
|
|
7
|
+
* that ciphertext to a routed target model. The user's decision (2026-10-21) is that the SOURCE GPT
|
|
8
|
+
* model writes a plaintext handoff summary before the switch.
|
|
9
|
+
*
|
|
10
|
+
* The summary is produced by an EPHEMERAL fork of the source thread, so:
|
|
11
|
+
* - the user's live source PTY and source thread are never touched (anti-goal: never submit into
|
|
12
|
+
* the live source session);
|
|
13
|
+
* - no rollout file is persisted for the fork (measured on Codex 0.163.0: the rollout-file set is
|
|
14
|
+
* byte-identical before and after);
|
|
15
|
+
* - the turn is a real model turn on the user's own ChatGPT account (risk R-1: it spends quota and
|
|
16
|
+
* can take up to HANDOFF_DEADLINE_MS, which is why the caller shows a "compacting" phase).
|
|
17
|
+
*
|
|
18
|
+
* This module owns the child process, the app-server JSON-RPC sequence, and the text validation. It
|
|
19
|
+
* writes nothing anywhere: the returned text travels in memory to lib/native-history-control.js,
|
|
20
|
+
* which injects it into the forked target as one developer message.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { spawn } = require('node:child_process');
|
|
24
|
+
const crypto = require('node:crypto');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
// The ONE process-launch/RPC implementation. `allowTurn: true` is what makes the handoff turn legal:
|
|
27
|
+
// every fork and readback path keeps the fail-closed `native_fork_emitted_model_turn` guard.
|
|
28
|
+
const { createBoundedRpc, terminateChild } = require('./native-history-control');
|
|
29
|
+
|
|
30
|
+
// Codex rust-v0.163.0 codex-rs/prompts/templates/compact/prompt.md, verbatim (Apache-2.0; the file
|
|
31
|
+
// carries the upstream copyright header). Kept byte-identical on purpose: the source model must be
|
|
32
|
+
// asked the same question Codex asks its own compactor, or the summary is a different artifact.
|
|
33
|
+
const CODEX_COMPACT_PROMPT = 'You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.\n'
|
|
34
|
+
+ '\n'
|
|
35
|
+
+ 'Include:\n'
|
|
36
|
+
+ '- Current progress and key decisions made\n'
|
|
37
|
+
+ '- Important context, constraints, or user preferences\n'
|
|
38
|
+
+ '- What remains to be done (clear next steps)\n'
|
|
39
|
+
+ '- Any critical data, examples, or references needed to continue\n'
|
|
40
|
+
+ '\n'
|
|
41
|
+
+ 'Be concise, structured, and focused on helping the next LLM seamlessly continue the work.\n';
|
|
42
|
+
// The CTM addition (§6.3): what a provider switch additionally needs that a same-provider compaction
|
|
43
|
+
// does not. It is not a tool-using turn -- the fork is read-only and every tool is refused.
|
|
44
|
+
const CTM_HANDOFF_ADDITION = '\n\nAlso include, verbatim and complete: every file path you changed or must change, exact error strings and test names still failing, commands that must run next, and the single next action. Do not call tools. Reply with the summary only.';
|
|
45
|
+
const HANDOFF_PROMPT = `${CODEX_COMPACT_PROMPT}${CTM_HANDOFF_ADDITION}`;
|
|
46
|
+
|
|
47
|
+
const HANDOFF_MIN_CHARS = 200;
|
|
48
|
+
const HANDOFF_MAX_CHARS = 32_768;
|
|
49
|
+
const HANDOFF_DEADLINE_MS = 240_000;
|
|
50
|
+
const HANDOFF_APP_SERVER_ARGS = Object.freeze(['app-server', '-c', 'approval_policy="never"',
|
|
51
|
+
'-c', 'sandbox_mode="read-only"']);
|
|
52
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
53
|
+
const MARKER_PREFIX = '<ctm_handoff';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ask the SOURCE model for the plaintext handoff that will cover its sealed compaction.
|
|
57
|
+
*
|
|
58
|
+
* @param {{executablePath:string, homeDir:string, codexHome:string, workingDirectory:string, tmpDir?:string|null}} source
|
|
59
|
+
* @param {{sourceThreadId:string, lastTurnId:string}} checkpoint
|
|
60
|
+
* @param {{signal?:AbortSignal, timeoutMs?:number}} [opts]
|
|
61
|
+
* @returns {Promise<{ok:true, text:string, textSha256:string, turnId:string, model:string|null}
|
|
62
|
+
* | {ok:false, reason:'handoff_fork_failed'|'handoff_turn_failed'|'handoff_empty'|'handoff_too_long'
|
|
63
|
+
* |'handoff_marker_in_text'|'handoff_timeout'|'transition_cancelled'}>}
|
|
64
|
+
*/
|
|
65
|
+
async function writeSourceHandoff(source, checkpoint, { signal, timeoutMs } = {}) {
|
|
66
|
+
if (signal?.aborted) return failure('transition_cancelled');
|
|
67
|
+
const sourceThreadId = nonempty(checkpoint?.sourceThreadId);
|
|
68
|
+
const lastTurnId = nonempty(checkpoint?.lastTurnId);
|
|
69
|
+
const executablePath = nonempty(source?.executablePath);
|
|
70
|
+
if (![executablePath, source?.homeDir, source?.codexHome, source?.workingDirectory]
|
|
71
|
+
.every((value) => typeof value === 'string' && path.isAbsolute(value))
|
|
72
|
+
|| (source?.tmpDir != null && !path.isAbsolute(source.tmpDir))
|
|
73
|
+
|| !UUID_RE.test(sourceThreadId) || !lastTurnId) return failure('handoff_fork_failed');
|
|
74
|
+
const deadlineMs = Number.isSafeInteger(timeoutMs) && timeoutMs > 0
|
|
75
|
+
? Math.min(timeoutMs, HANDOFF_DEADLINE_MS) : HANDOFF_DEADLINE_MS;
|
|
76
|
+
const state = { turnId: '', completed: null, agentText: '', onComplete: null };
|
|
77
|
+
let stage = 'fork';
|
|
78
|
+
let child = null;
|
|
79
|
+
let rpc = null;
|
|
80
|
+
try {
|
|
81
|
+
child = spawn(executablePath, [...HANDOFF_APP_SERVER_ARGS], { cwd: source.workingDirectory,
|
|
82
|
+
env: handoffEnv(source), stdio: ['pipe', 'pipe', 'pipe'] });
|
|
83
|
+
child.stderr.resume();
|
|
84
|
+
rpc = createBoundedRpc(child, { signal, timeoutMs: deadlineMs, allowTurn: true,
|
|
85
|
+
onNotification: (method, params) => observe(state, method, params) });
|
|
86
|
+
const initialized = await rpc.request('initialize', {
|
|
87
|
+
clientInfo: { name: 'ctm-source-handoff', version: '1' }, capabilities: {},
|
|
88
|
+
});
|
|
89
|
+
if (!initialized || rpc.closed) return failure('handoff_fork_failed');
|
|
90
|
+
await rpc.notify('initialized', {});
|
|
91
|
+
const forked = await rpc.request('thread/fork', { threadId: sourceThreadId, lastTurnId,
|
|
92
|
+
ephemeral: true, excludeTurns: true });
|
|
93
|
+
const ephemeralId = nonempty(forked?.thread?.id || forked?.threadId || forked?.thread_id);
|
|
94
|
+
if (!UUID_RE.test(ephemeralId) || ephemeralId === sourceThreadId) return failure('handoff_fork_failed');
|
|
95
|
+
stage = 'turn';
|
|
96
|
+
const started = await rpc.request('turn/start', { threadId: ephemeralId,
|
|
97
|
+
input: [{ type: 'text', text: HANDOFF_PROMPT }] });
|
|
98
|
+
state.turnId = nonempty(started?.turn?.id || started?.turnId || started?.id);
|
|
99
|
+
if (!state.turnId) return failure('handoff_turn_failed');
|
|
100
|
+
const turn = await waitForTurn(state, signal, deadlineMs);
|
|
101
|
+
if (signal?.aborted) return failure('transition_cancelled');
|
|
102
|
+
if (!turn || turn.timedOut) return failure(turn ? 'handoff_timeout' : 'handoff_turn_failed');
|
|
103
|
+
if (nonempty(turn.status) !== 'completed') return failure('handoff_turn_failed');
|
|
104
|
+
const text = (agentMessageText(turn) || state.agentText).trim();
|
|
105
|
+
if (text.length < HANDOFF_MIN_CHARS) return failure('handoff_empty');
|
|
106
|
+
if (text.length > HANDOFF_MAX_CHARS) return failure('handoff_too_long');
|
|
107
|
+
// The target's own readback looks for this prefix; a summary that already contains one could
|
|
108
|
+
// forge coverage for an unrelated sealed item.
|
|
109
|
+
if (text.includes(MARKER_PREFIX)) return failure('handoff_marker_in_text');
|
|
110
|
+
return { ok: true, text, textSha256: sha256(text), turnId: state.turnId,
|
|
111
|
+
model: nonempty(turn.model || turn.modelId) || null };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (signal?.aborted || error?.code === 'transition_cancelled') return failure('transition_cancelled');
|
|
114
|
+
if (error?.code === 'native_rpc_deadline') return failure('handoff_timeout');
|
|
115
|
+
return failure(stage === 'turn' ? 'handoff_turn_failed' : 'handoff_fork_failed');
|
|
116
|
+
} finally {
|
|
117
|
+
// The ephemeral fork must never outlive this call, on any path -- including a refusal or an abort.
|
|
118
|
+
if (rpc) await rpc.close();
|
|
119
|
+
else await terminateChild(child);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function handoffEnv(source) {
|
|
124
|
+
const env = { PATH: process.env.PATH || '/usr/bin:/bin', HOME: source.homeDir,
|
|
125
|
+
CODEX_HOME: source.codexHome, LANG: process.env.LANG || 'C',
|
|
126
|
+
CTM_PROCESS_ROLE: 'ctm-source-handoff' };
|
|
127
|
+
if (source.tmpDir) env.TMPDIR = source.tmpDir;
|
|
128
|
+
return env;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The single notification sink: turn/completed carries the finished turn (with its items), and
|
|
132
|
+
// item/completed is the fallback if a Codex version reports the agent message only there.
|
|
133
|
+
function observe(state, method, params) {
|
|
134
|
+
if (method === 'item/completed') {
|
|
135
|
+
const item = params?.item;
|
|
136
|
+
if (nonempty(item?.type) === 'agentMessage' && typeof item.text === 'string'
|
|
137
|
+
&& (!state.turnId || nonempty(params?.turnId || item.turn_id) === state.turnId)) {
|
|
138
|
+
state.agentText = item.text;
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (method !== 'turn/completed') return;
|
|
143
|
+
const turn = params?.turn;
|
|
144
|
+
if (state.turnId && nonempty(turn?.id) !== state.turnId) return;
|
|
145
|
+
state.completed = isRecord(turn) ? turn : null;
|
|
146
|
+
const settle = state.onComplete;
|
|
147
|
+
state.onComplete = null;
|
|
148
|
+
if (settle) settle(state.completed);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function waitForTurn(state, signal, deadlineMs) {
|
|
152
|
+
if (state.completed) return Promise.resolve(state.completed);
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
let settled = false;
|
|
155
|
+
const timer = setTimeout(() => finish({ timedOut: true }), deadlineMs);
|
|
156
|
+
const onAbort = () => finish(null);
|
|
157
|
+
function finish(value) {
|
|
158
|
+
if (settled) return;
|
|
159
|
+
settled = true;
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
state.onComplete = null;
|
|
162
|
+
signal?.removeEventListener('abort', onAbort);
|
|
163
|
+
resolve(value);
|
|
164
|
+
}
|
|
165
|
+
state.onComplete = finish;
|
|
166
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
167
|
+
if (state.completed) finish(state.completed);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function agentMessageText(turn) {
|
|
172
|
+
if (!Array.isArray(turn?.items)) return '';
|
|
173
|
+
for (let index = turn.items.length - 1; index >= 0; index -= 1) {
|
|
174
|
+
const item = turn.items[index];
|
|
175
|
+
if (nonempty(item?.type) === 'agentMessage' && typeof item.text === 'string') return item.text;
|
|
176
|
+
}
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function failure(reason) { return { ok: false, reason }; }
|
|
181
|
+
function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
|
|
182
|
+
function nonempty(value) { return typeof value === 'string' && value.trim() ? value.trim() : ''; }
|
|
183
|
+
function isRecord(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
|
|
184
|
+
|
|
185
|
+
module.exports = { writeSourceHandoff, HANDOFF_PROMPT, HANDOFF_MIN_CHARS, HANDOFF_MAX_CHARS,
|
|
186
|
+
HANDOFF_DEADLINE_MS };
|
|
@@ -937,12 +937,34 @@ class CtmDbOwnerWorkerClient {
|
|
|
937
937
|
}).catch(() => {});
|
|
938
938
|
}
|
|
939
939
|
|
|
940
|
+
// WP-A P-2 (freeze-probes): the up-to-`limit` OLDEST client requests still outstanding on this
|
|
941
|
+
// worker, each { op, ageMs, started } -- what a memory step should be read against. One pass over
|
|
942
|
+
// the Map with a bounded insert (never a sort of the whole pending set: this runs on the sampler path).
|
|
943
|
+
_oldestPendingOps(limit = 5) {
|
|
944
|
+
const now = Date.now();
|
|
945
|
+
const oldest = [];
|
|
946
|
+
for (const pending of this._pending.values()) {
|
|
947
|
+
const entry = {
|
|
948
|
+
op: String(pending?.op || 'unknown'),
|
|
949
|
+
ageMs: Math.max(0, now - (Number(pending?.enqueuedAt) || now)),
|
|
950
|
+
started: Number(pending?.startedAt) > 0,
|
|
951
|
+
};
|
|
952
|
+
let at = oldest.length;
|
|
953
|
+
while (at > 0 && oldest[at - 1].ageMs < entry.ageMs) at -= 1;
|
|
954
|
+
if (at >= limit) continue;
|
|
955
|
+
oldest.splice(at, 0, entry);
|
|
956
|
+
if (oldest.length > limit) oldest.pop();
|
|
957
|
+
}
|
|
958
|
+
return oldest;
|
|
959
|
+
}
|
|
960
|
+
|
|
940
961
|
getStatus() {
|
|
941
962
|
const s = {
|
|
942
963
|
...this._status,
|
|
943
964
|
ready: this._ready,
|
|
944
965
|
closed: this._closed,
|
|
945
966
|
pendingClientRequests: this._pending.size,
|
|
967
|
+
pendingOps: this._oldestPendingOps(),
|
|
946
968
|
degraded: this.isDegraded(),
|
|
947
969
|
degradedUntil: this._degradedUntil,
|
|
948
970
|
consecutiveTimeouts: this._consecutiveTimeouts,
|
|
@@ -612,8 +612,6 @@ const FEATURES = [
|
|
|
612
612
|
{ id: "ctm_ctm_fp_memo", env: "CTM_FP_MEMO", owner: "ctm-core-team", stage: "stable", defaultOn: true, since: "2026-08-24", expires: null },
|
|
613
613
|
// !== '0' comparison found; 1 site(s); first: lib/ws/state-sync.js:470
|
|
614
614
|
{ id: "ctm_ctm_frame_nack", env: "CTM_FRAME_NACK", owner: "ctm-core-team", stage: "stable", defaultOn: true, since: "2026-08-24", expires: null },
|
|
615
|
-
// !== '0' comparison found; 1 site(s); first: server.js:36110
|
|
616
|
-
{ id: "ctm_ctm_freeze_park_giant", env: "CTM_FREEZE_PARK_GIANT", owner: "ctm-core-team", stage: "stable", defaultOn: true, since: "2026-08-24", expires: null },
|
|
617
615
|
// numeric-tunable suffix/read shape; 1 site(s); first: lib/perf-tracker.js:829
|
|
618
616
|
{ id: "ctm_ctm_freeze_probe_ms", env: "CTM_FREEZE_PROBE_MS", owner: "ctm-core-team", stage: "ops", tunable: true, default: 0, since: "2026-08-24", expires: null, note: "numeric tunable" },
|
|
619
617
|
// !== '0' comparison found; 1 site(s); first: server.js:445
|
|
@@ -5,7 +5,13 @@ const KNOWN_ORIGINS = new Set([...NATIVE_ORIGINS, 'routed-proxy', 'portable-tran
|
|
|
5
5
|
// S-3: the adapter/schema ids and the version membership test come from the one qualified table.
|
|
6
6
|
const { CODEX_CONTROL_ADAPTER, CODEX_COMPACTION_SCHEMA_RECEIPT, CODEX_PRE_TURN_COMPACTION_SCHEMA,
|
|
7
7
|
isQualifiedCodexVersion } = require('./codex-harness-qualification');
|
|
8
|
-
|
|
8
|
+
// S-4: the ONE portable-kind set. `native-media` is a message whose only non-text content parts are
|
|
9
|
+
// input_image parts (D-2); it is portable only to a Codex target, which is the planner's job below.
|
|
10
|
+
const PORTABLE_HISTORY_KINDS = new Set(['text', 'tool-pair', 'native-world-state', 'native-lifecycle', 'native-media']);
|
|
11
|
+
// D-5/D-6 (docs/codex-lead-routed-switch-history-handoff-2026-10-21.html §6.2): the plaintext handoff
|
|
12
|
+
// the source model writes is injected into the forked target, so it consumes target context that the
|
|
13
|
+
// source estimate does not count. 32 KiB of summary text is 8_192 tokens at four characters each.
|
|
14
|
+
const HANDOFF_TOKEN_RESERVE = 8_192;
|
|
9
15
|
|
|
10
16
|
function inspectTransition({ sourceRecipe, targetRecipe, snapshot, signal } = {}) {
|
|
11
17
|
if (signal?.aborted) return refuse('transition_cancelled');
|
|
@@ -17,9 +23,11 @@ function inspectTransition({ sourceRecipe, targetRecipe, snapshot, signal } = {}
|
|
|
17
23
|
const targetProviderId = nonempty(targetRecipe.providerId);
|
|
18
24
|
const origin = nonempty(sourceRecipe.origin);
|
|
19
25
|
const codexHistorySource = origin === 'native-codex' || sourceRecipe.harness === 'codex-cli';
|
|
26
|
+
// D-3: qualification is per record (D-1) plus the installed binary that performs the fork. The
|
|
27
|
+
// rollout's CREATOR version (session_meta.cli_version, written once at creation) is not the writer
|
|
28
|
+
// of its later records, so it is not a gate here.
|
|
20
29
|
const qualifiedCodexHistorySource = codexHistorySource
|
|
21
|
-
&& sourceRecipe.controlAdapterVersion === CODEX_CONTROL_ADAPTER
|
|
22
|
-
&& isQualifiedCodexVersion(sourceRecipe.programVersion);
|
|
30
|
+
&& sourceRecipe.controlAdapterVersion === CODEX_CONTROL_ADAPTER;
|
|
23
31
|
if (!origin) return refuse('source_origin_unknown');
|
|
24
32
|
if (!KNOWN_ORIGINS.has(origin)) return refuse('source_origin_unsupported');
|
|
25
33
|
if (!sourceProviderId || !targetProviderId) return refuse('provider_identity_unknown');
|
|
@@ -31,7 +39,7 @@ function inspectTransition({ sourceRecipe, targetRecipe, snapshot, signal } = {}
|
|
|
31
39
|
if (nativeSource && sourceProviderId === targetProviderId) {
|
|
32
40
|
return {
|
|
33
41
|
action: 'reuse', reason: 'same_provider_native_history', sourceCheckpoint,
|
|
34
|
-
estimateTokens, targetBudgetTokens: null, evidenceKind,
|
|
42
|
+
estimateTokens, targetBudgetTokens: null, evidenceKind, handoff: null,
|
|
35
43
|
};
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -40,12 +48,8 @@ function inspectTransition({ sourceRecipe, targetRecipe, snapshot, signal } = {}
|
|
|
40
48
|
if (!sourceCheckpoint) return refuse('source_checkpoint_unknown');
|
|
41
49
|
if (codexHistorySource && sourceProviderId !== targetProviderId
|
|
42
50
|
&& (!qualifiedCodexHistorySource
|
|
43
|
-
|| !isQualifiedCodexVersion(sourceRecipe.programVersion)
|
|
44
51
|
|| sourceRecipe.cyberAccessProgram !== null
|
|
45
52
|
|| snapshot.previousSettings?.cyberAccessProgram !== null
|
|
46
|
-
|| (snapshot.previousSettings?.programVersion !== undefined
|
|
47
|
-
&& snapshot.previousSettings?.programVersion !== null
|
|
48
|
-
&& !isQualifiedCodexVersion(snapshot.previousSettings.programVersion))
|
|
49
53
|
|| targetRecipe.harness !== 'codex-cli' || !isQualifiedCodexVersion(targetRecipe.harnessVersion))) {
|
|
50
54
|
return refuse('native_fork_adapter_unqualified', { sourceCheckpoint });
|
|
51
55
|
}
|
|
@@ -71,7 +75,27 @@ function inspectTransition({ sourceRecipe, targetRecipe, snapshot, signal } = {}
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
if (snapshot.opaque === true || has(snapshot.historyKinds, 'opaque')) return refuse('opaque_history_not_portable', { sourceCheckpoint });
|
|
74
|
-
|
|
78
|
+
// D-2: native media is portable exactly when the target is Codex, which strips images the target
|
|
79
|
+
// cannot accept (core/src/context_manager/normalize.rs:346 @ rust-v0.163.0). A non-Codex target
|
|
80
|
+
// gets a plain refusal instead of a silent rewrite of the source history.
|
|
81
|
+
if (has(snapshot.historyKinds, 'native-media') && targetRecipe.harness !== 'codex-cli') {
|
|
82
|
+
return refuse('history_media_not_portable', { sourceCheckpoint });
|
|
83
|
+
}
|
|
84
|
+
// D-5/D-6: a sealed `compaction` item is carried by a plaintext handoff the SOURCE model writes
|
|
85
|
+
// before the switch. Exactly one sealed digest can be covered, and only the native openai source
|
|
86
|
+
// (whose own model can be asked to write it) can produce one; everything else keeps a named refusal.
|
|
87
|
+
const sealed = (Array.isArray(snapshot.encryptedCompactions) ? snapshot.encryptedCompactions : [])
|
|
88
|
+
.filter((sha) => typeof sha === 'string' && /^[a-f0-9]{64}$/.test(sha));
|
|
89
|
+
if (sealed.length > 1) return refuse('encrypted_compaction_multiple', { sourceCheckpoint });
|
|
90
|
+
const handoff = sealed.length === 1 && snapshot.handoffCovers !== true
|
|
91
|
+
? (qualifiedCodexHistorySource && origin === 'native-codex' && sourceProviderId === 'openai'
|
|
92
|
+
? { required: true, compactionSha256: sealed[0] } : null)
|
|
93
|
+
: null;
|
|
94
|
+
if (sealed.length === 1 && snapshot.handoffCovers !== true && !handoff) {
|
|
95
|
+
return refuse('encrypted_compaction_handoff_unavailable', { sourceCheckpoint });
|
|
96
|
+
}
|
|
97
|
+
// D-4: a plaintext compaction is portable. Codex fork rebuilds the window from
|
|
98
|
+
// replacement_history, and no test, doc or incident ever justified the blanket refusal.
|
|
75
99
|
if (snapshot.encrypted === true || has(snapshot.historyKinds, 'encrypted')) return refuse('encrypted_history_not_portable', { sourceCheckpoint });
|
|
76
100
|
if (!Array.isArray(snapshot.historyKinds) || snapshot.historyKinds.length === 0
|
|
77
101
|
|| snapshot.historyKinds.some((kind) => !PORTABLE_HISTORY_KINDS.has(kind))) {
|
|
@@ -126,7 +150,10 @@ function inspectTransition({ sourceRecipe, targetRecipe, snapshot, signal } = {}
|
|
|
126
150
|
: null;
|
|
127
151
|
const evidence = { sourceCheckpoint, estimateTokens, targetBudgetTokens, evidenceKind,
|
|
128
152
|
...(preTurnCompaction ? { preTurnCompaction } : {}) };
|
|
129
|
-
|
|
153
|
+
// The injected handoff is target context the source estimate cannot see, so the comparison
|
|
154
|
+
// reserves it while every reported evidence field keeps the measured source value.
|
|
155
|
+
const budgetTokens = handoff?.required ? estimateTokens + HANDOFF_TOKEN_RESERVE : estimateTokens;
|
|
156
|
+
if (budgetTokens <= targetBudgetTokens) return { action: 'reuse', reason: 'history_fits_target', ...evidence, handoff };
|
|
130
157
|
|
|
131
158
|
// Phase A has no qualified source-owned compact adapter. Native API acknowledgements alone
|
|
132
159
|
// are not durable checkpoint receipts, so a recipe flag cannot authorize mutation here.
|
|
@@ -191,4 +218,4 @@ function refuse(reason, details = {}) {
|
|
|
191
218
|
return { action: 'unsupported', reason, ...details };
|
|
192
219
|
}
|
|
193
220
|
|
|
194
|
-
module.exports = { inspectTransition, computeTargetBudget };
|
|
221
|
+
module.exports = { inspectTransition, computeTargetBudget, PORTABLE_HISTORY_KINDS, HANDOFF_TOKEN_RESERVE };
|
|
@@ -1345,6 +1345,8 @@ async function handleMessage(msg) {
|
|
|
1345
1345
|
// WP-0b (§6): the xterm write callback for THIS feed -- exactly where the brief says
|
|
1346
1346
|
// `mirror-written` belongs.
|
|
1347
1347
|
_noteWorkerTypingStagePending(msg.sessionId, entry, 'mirror-written');
|
|
1348
|
+
// WP-3 (D-C2): per-key shadow echo matchers -- see lib/typing-worker-stages.js.
|
|
1349
|
+
_typingWorkerStages.matchEchoPredictions(postTransport, msg.sessionId, entry, msg.data);
|
|
1348
1350
|
entry.state.liveOutputSeq += 1;
|
|
1349
1351
|
entry.state.liveBytesSinceSeed += byteCount;
|
|
1350
1352
|
if (entry.state.pendingInputSeq > entry.state.echoedThroughInputSeq) {
|
|
@@ -1470,12 +1472,9 @@ async function handleMessage(msg) {
|
|
|
1470
1472
|
// every keystroke (HeadlessTerminalRouter.postMessage, lib/session-host-manager.js
|
|
1471
1473
|
// :1815-1834, forwards by sessionId to the pool host when one owns the session, else the
|
|
1472
1474
|
// legacy worker thread) -- carry the pending typing-interaction ids here, not on 'feed'.
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
);
|
|
1477
|
-
entry.state.typingIdsAwaitingFeed = true;
|
|
1478
|
-
}
|
|
1475
|
+
// WP-3 (D-C2): the pending-id merge above moved into armEchoPrediction, which also arms
|
|
1476
|
+
// this keystroke's shadow echo prediction when the message carries one.
|
|
1477
|
+
_typingWorkerStages.armEchoPrediction(entry, msg);
|
|
1479
1478
|
break;
|
|
1480
1479
|
}
|
|
1481
1480
|
|