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.
Files changed (70) hide show
  1. package/README.md +84 -41
  2. package/lib/runtime-closure-policy.json +2 -2
  3. package/package.json +10 -4
  4. package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
  5. package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
  6. package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
  7. package/template/coding-task-manager/lib/client-diag-handler.js +20 -0
  8. package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
  9. package/template/coding-task-manager/lib/codex-local-links.js +267 -0
  10. package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
  11. package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
  12. package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
  13. package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
  14. package/template/coding-task-manager/lib/feature-manifest.js +0 -2
  15. package/template/coding-task-manager/lib/harness-history-transition.js +38 -11
  16. package/template/coding-task-manager/lib/headless-term-service.js +5 -6
  17. package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
  18. package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
  19. package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
  20. package/template/coding-task-manager/lib/lead-model-switch.js +8 -0
  21. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
  22. package/template/coding-task-manager/lib/login-tripwire.js +17 -0
  23. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +4 -0
  24. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
  25. package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
  26. package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
  27. package/template/coding-task-manager/lib/native-history-control.js +60 -12
  28. package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
  29. package/template/coding-task-manager/lib/raw-history-block.js +3 -1
  30. package/template/coding-task-manager/lib/read-pool-client.js +31 -22
  31. package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
  32. package/template/coding-task-manager/lib/remote-command-runtime.js +5 -4
  33. package/template/coding-task-manager/lib/root-prompt-admission.js +10 -9
  34. package/template/coding-task-manager/lib/root-prompt-flow.js +8 -5
  35. package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
  36. package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
  37. package/template/coding-task-manager/lib/session-create/provider-process.js +4 -2
  38. package/template/coding-task-manager/lib/session-lifecycle.js +2 -50
  39. package/template/coding-task-manager/lib/session-model-action-preconditions.js +59 -3
  40. package/template/coding-task-manager/lib/session-resource-sampler.js +48 -0
  41. package/template/coding-task-manager/lib/session-status.js +2 -2
  42. package/template/coding-task-manager/lib/typing-interaction-trace.js +44 -2
  43. package/template/coding-task-manager/lib/typing-worker-stages.js +148 -1
  44. package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
  45. package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
  46. package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
  47. package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
  48. package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
  49. package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
  50. package/template/coding-task-manager/lib/ws/input.js +1 -0
  51. package/template/coding-task-manager/protocol/message-table.json +0 -10
  52. package/template/coding-task-manager/protocol/messages.js +0 -10
  53. package/template/coding-task-manager/protocol/ws-schema.json +0 -20
  54. package/template/coding-task-manager/protocol/ws-schema.md +0 -2
  55. package/template/coding-task-manager/public/css/app.css +0 -1
  56. package/template/coding-task-manager/public/index.html +3 -17
  57. package/template/coding-task-manager/public/js/client-health-core.js +81 -0
  58. package/template/coding-task-manager/public/js/client-health-main.js +61 -0
  59. package/template/coding-task-manager/public/js/prompt-submission-client.js +5 -1
  60. package/template/coding-task-manager/public/js/protocol.js +0 -10
  61. package/template/coding-task-manager/public/js/row-join.js +14 -1
  62. package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
  63. package/template/coding-task-manager/public/js/terminal-history-viewport.js +0 -2
  64. package/template/coding-task-manager/server.js +62 -186
  65. package/template/package-lock.json +2 -2
  66. package/template/package.json +1 -1
  67. package/template/wall-e/coding/host-capabilities.js +2 -2
  68. package/template/wall-e/fly.toml +1 -1
  69. package/template/wall-e/tools/local-tools.js +6 -1
  70. package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // WP-1 (docs/codex-lead-routed-switch-history-handoff-2026-10-21.html §1, §7 first row, §8 WP-1):
5
+ // the §1 census, promoted from a scratch script to the tool the report quotes.
6
+ //
7
+ // WHY IT EXISTS. The naive census calls the scanner's inner function and counts record TYPES, which
8
+ // reported "no new record types" for 0.163.0 while CTM refused 265 of 505 real sessions because of
9
+ // the KEYS inside a known type (`thread_settings_applied`). This tool calls the PRODUCTION entry
10
+ // point instead: readCodexHistoryTransitionSnapshot = owned-rollout resolution + the native prefix
11
+ // reader + the scanner; when the scan completes it also runs the planner (inspectTransition), so the
12
+ // first refusal of the REAL chain is what gets reported. See §1 "The naive count is wrong".
13
+ //
14
+ // OQ-1 (the census harness, not production, showed 65 `canonical_history_path_unavailable`): this
15
+ // tool derives each session's storage root from the session's OWN jsonl_path -- the directory whose
16
+ // last `sessions` segment holds the rollout -- instead of assuming one ~/.codex home for every row.
17
+ // The corresponding root receipt is built the same way CTM's launch path builds it
18
+ // (lib/codex-paths.js codexSessionsRootReceipt), so what is measured here is what production reads.
19
+ //
20
+ // READ-ONLY. The database is opened `readonly` + `fileMustExist` and is never written; no file is
21
+ // created anywhere; no port is contacted.
22
+ //
23
+ // usage:
24
+ // node bin/census-codex-lead-history.js [--since YYYY-MM-DD] [--limit N] [--only <ctm-session-prefix>]
25
+ // [--db <path to core.db>] [--explain <ctm-session-prefix>]
26
+ //
27
+ // --since oldest modified_at to include (default 2026-10-01, the §1 population)
28
+ // --limit newest sessions to scan (default 1000: above the 505 of §1, so a full table by default)
29
+ // --only only sessions whose ctm_session_id starts with this prefix
30
+ // --explain also print the snapshot's summary fields for the matching session(s), and for a
31
+ // `thread_settings_applied` refusal the lineage set vs the failing record's thread_id (OQ-3)
32
+ //
33
+ // exit 0 the table printed · 1 usage error · 2 the database could not be opened read-only.
34
+
35
+ const fs = require('node:fs');
36
+ const os = require('node:os');
37
+ const path = require('node:path');
38
+ const Database = require('better-sqlite3');
39
+
40
+ const op = require('../lib/history-transition-read-op');
41
+ const { readNativeHistoryPrefix } = require('../lib/codex-native-history-prefix');
42
+ const { inspectTransition } = require('../lib/harness-history-transition');
43
+ const codexPaths = require('../lib/codex-paths');
44
+ const { CODEX_CONTROL_ADAPTER, CODEX_COMPACTION_SCHEMA_RECEIPT } = require('../lib/codex-harness-qualification');
45
+
46
+ const DEFAULT_DB = path.join(os.homedir(), '.walle', 'data', 'core.db');
47
+ const DEFAULT_SINCE = '2026-10-01';
48
+ const DEFAULT_LIMIT = 1000;
49
+ const EXAMPLES_PER_REASON = 4;
50
+ const EXPLAIN_GENERATION = 'census-codex-lead-history';
51
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
52
+
53
+ // The §1 target recipe: a routed DeepSeek V4.1 Flash target (any provider id other than the
54
+ // source's; the planner only compares them) with the
55
+ // qualified harness version and the qualified compaction receipt. Per-session fields (approval
56
+ // policy, sandbox policy, cyber access program) are copied from the scanned snapshot.
57
+ const TARGET_RECIPE_BASE = Object.freeze({
58
+ providerId: 'census-routed-target',
59
+ routeId: 'census-routed-target',
60
+ origin: 'routed-proxy',
61
+ harness: 'codex-cli',
62
+ controlAdapterVersion: CODEX_CONTROL_ADAPTER,
63
+ harnessVersion: '0.163.0',
64
+ wireModelId: 'deepseek-v4p1-flash',
65
+ contextTokens: 1049000,
66
+ usableContextTokens: 996550,
67
+ nativeCompactThresholdTokens: 944100,
68
+ plannedRequestOutputTokens: 65536,
69
+ compactHash: null,
70
+ compactHashStatus: 'known-absent-with-native-schema-receipt',
71
+ compactHashSchemaReceipt: CODEX_COMPACTION_SCHEMA_RECEIPT,
72
+ catalogSha256: '0'.repeat(64),
73
+ });
74
+
75
+ // One row per CTM session, newest agent_sessions row first (§1: "the newest row per session").
76
+ const SESSION_QUERY = `SELECT ctm_session_id, agent_session_id, jsonl_path, max(modified_at) AS modified_at
77
+ FROM agent_sessions
78
+ WHERE lower(provider) = 'codex' AND ctm_session_id IS NOT NULL AND ctm_session_id <> ''
79
+ AND jsonl_path IS NOT NULL AND modified_at > ?
80
+ GROUP BY ctm_session_id
81
+ ORDER BY modified_at DESC
82
+ LIMIT ?`;
83
+
84
+ class UsageError extends Error {}
85
+
86
+ function usage() {
87
+ return [
88
+ 'usage: node bin/census-codex-lead-history.js [--since YYYY-MM-DD] [--limit N] [--only <ctm-session-prefix>] [--db <path>] [--explain <ctm-session-prefix>]',
89
+ ' --since <date> oldest modified_at to include (default 2026-10-01)',
90
+ ' --limit <n> newest sessions to scan (default 1000)',
91
+ ' --only <prefix> only sessions whose ctm_session_id starts with this prefix',
92
+ ' --db <path> core.db to read (default ~/.walle/data/core.db, opened read-only)',
93
+ ' --explain <prefix> print the snapshot summary (and, for a thread_settings_applied refusal,',
94
+ ' the lineage set vs the failing record thread_id) for matching session(s)',
95
+ ].join('\n');
96
+ }
97
+
98
+ function parseArgs(argv) {
99
+ const parsed = { help: false, since: DEFAULT_SINCE, limit: DEFAULT_LIMIT, only: null, db: DEFAULT_DB, explain: null };
100
+ for (let index = 0; index < argv.length; index += 1) {
101
+ const arg = argv[index];
102
+ if (arg === '--help' || arg === '-h') { parsed.help = true; continue; }
103
+ if (['--since', '--limit', '--only', '--db', '--explain'].includes(arg)) {
104
+ const value = argv[index + 1];
105
+ if (value === undefined || value.startsWith('--')) throw new UsageError(`${arg} needs a value`);
106
+ index += 1;
107
+ if (arg === '--since') parsed.since = value;
108
+ else if (arg === '--limit') parsed.limit = value;
109
+ else if (arg === '--only') parsed.only = value;
110
+ else if (arg === '--explain') parsed.explain = value;
111
+ else parsed.db = value;
112
+ continue;
113
+ }
114
+ throw new UsageError(`unknown option ${arg}`);
115
+ }
116
+ if (!DATE_RE.test(parsed.since)) throw new UsageError(`--since must be YYYY-MM-DD, got ${parsed.since}`);
117
+ const limit = Number(parsed.limit);
118
+ if (!Number.isSafeInteger(limit) || limit <= 0) throw new UsageError(`--limit must be a positive integer, got ${parsed.limit}`);
119
+ parsed.limit = limit;
120
+ return parsed;
121
+ }
122
+
123
+ function openReadOnly(dbPath) {
124
+ if (!fs.existsSync(dbPath)) throw new Error(`${dbPath} does not exist`);
125
+ const stat = fs.statSync(dbPath);
126
+ if (!stat.isFile()) throw new Error(`${dbPath} is not a file`);
127
+ return new Database(dbPath, { readonly: true, fileMustExist: true });
128
+ }
129
+
130
+ // OQ-1: the storage root of THIS session, taken from its own rollout path. Codex writes
131
+ // <codexHome>/sessions/<date>/rollout-<ts>-<uuid>.jsonl, so the codex home is the parent of the
132
+ // `sessions` segment; the receipt is the same object the launch path captures. A jsonl_path with no
133
+ // `sessions` segment is not a Codex rollout path this tool can speak about -- reported, not guessed.
134
+ function storageRootForSession(jsonlPath) {
135
+ const segments = path.resolve(jsonlPath).split(path.sep);
136
+ const index = segments.lastIndexOf('sessions');
137
+ if (index <= 0) return { codexHome: null, sessionsDir: null, receipt: null };
138
+ const codexHome = segments.slice(0, index).join(path.sep) || path.sep;
139
+ const sessionsDir = segments.slice(0, index + 1).join(path.sep);
140
+ return { codexHome, sessionsDir, receipt: codexPaths.codexSessionsRootReceipt({ codexHomePath: codexHome, sessionsDir }) };
141
+ }
142
+
143
+ // The §1 methodology, one session at a time: the production read path, then the planner. Keeping
144
+ // only the key (never the snapshot) is what lets this run over every session in the window.
145
+ function scanSession(row) {
146
+ const storage = storageRootForSession(row.jsonl_path);
147
+ if (!storage.receipt) {
148
+ return { key: `scan:${storage.codexHome ? 'history_storage_root_unavailable' : 'census_jsonl_path_unexpected'}`, storage };
149
+ }
150
+ let snapshot;
151
+ try {
152
+ snapshot = op.readCodexHistoryTransitionSnapshot({
153
+ db: row.db,
154
+ ctmSessionId: row.ctm_session_id,
155
+ sourceThreadId: row.agent_session_id,
156
+ processGeneration: 0,
157
+ storageRootReceipt: storage.receipt,
158
+ maxBytes: op.MAX_SCAN_BYTES,
159
+ });
160
+ } catch (error) {
161
+ // expected: a rollout deleted or replaced mid-scan is reported as a throw, never as a pass
162
+ return { key: `scan:THROW:${String(error?.message || error).slice(0, 60)}`, storage, snapshot: null };
163
+ }
164
+ if (!snapshot.complete) return { key: `scan:${snapshot.reason}`, storage, snapshot };
165
+ const previousSettings = snapshot.previousSettings || {};
166
+ const decision = inspectTransition({
167
+ sourceRecipe: {
168
+ providerId: previousSettings.modelProvider || 'openai',
169
+ origin: 'native-codex',
170
+ harness: 'codex-cli',
171
+ controlAdapterVersion: CODEX_CONTROL_ADAPTER,
172
+ programVersion: previousSettings.programVersion,
173
+ cyberAccessProgram: previousSettings.cyberAccessProgram,
174
+ approvalPolicy: previousSettings.approvalPolicy,
175
+ sandboxPolicy: previousSettings.sandboxPolicy,
176
+ wireModelId: previousSettings.model,
177
+ providerThreadId: row.agent_session_id,
178
+ },
179
+ targetRecipe: {
180
+ ...TARGET_RECIPE_BASE,
181
+ approvalPolicy: previousSettings.approvalPolicy,
182
+ sandboxPolicy: previousSettings.sandboxPolicy,
183
+ cyberAccessProgram: previousSettings.cyberAccessProgram,
184
+ },
185
+ snapshot,
186
+ });
187
+ return { key: decision.action === 'unsupported' ? `plan:${decision.reason}` : `OK:${decision.action}`, storage, snapshot };
188
+ }
189
+
190
+ function prefixed(value) {
191
+ return String(value || '').slice(0, 8);
192
+ }
193
+
194
+ function printTable(rows, tallies, examples, { since, limit }) {
195
+ const lines = [];
196
+ lines.push(`sessions ${rows.length} (since ${since}, limit ${limit})`); const ordered = [...tallies.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
197
+ const width = ordered.reduce((max, [reason]) => Math.max(max, reason.length), 6);
198
+ for (const [reason, count] of ordered) {
199
+ lines.push(`${String(count).padStart(4)} ${reason.padEnd(width)} ${examples.get(reason).join(', ')}`);
200
+ }
201
+ if (!ordered.length) lines.push(' (no sessions matched)');
202
+ lines.push(`total ${[...tallies.values()].reduce((sum, count) => sum + count, 0)}`);
203
+ process.stdout.write(`${lines.join('\n')}\n`);
204
+ }
205
+
206
+ // OQ-3: the refusal the scanner returns for a `thread_settings_applied` record is the generic
207
+ // `unsupported_native_history_event`, so the failing RECORD has to be located. The scanner stops at
208
+ // its first refusal, and summarizing a slice that ends after the last `compacted` record uses the
209
+ // same start index as the full list -- so "does this slice refuse" is monotone there, and a binary
210
+ // search finds the refusing index in O(log n) scans instead of one scan per record.
211
+ function firstRefusalIndex(records, { threadId, lineageIds }) {
212
+ const lastCompaction = records.findLastIndex((record) => record?.type === 'compacted');
213
+ const startIndex = lastCompaction >= 0 ? lastCompaction : 0;
214
+ let low = startIndex + 1;
215
+ let high = records.length;
216
+ let found = -1;
217
+ while (low <= high) {
218
+ const mid = (low + high) >> 1;
219
+ const summary = op.summarizeCodexHistoryRecords(records.slice(0, mid), {
220
+ threadId, generation: EXPLAIN_GENERATION, lineageThreadIds: lineageIds,
221
+ });
222
+ if (summary.complete) low = mid + 1;
223
+ else { found = mid - 1; high = mid - 1; }
224
+ }
225
+ return found;
226
+ }
227
+
228
+ function describeRefusal(root, threadId) {
229
+ const lines = [];
230
+ const prefix = readNativeHistoryPrefix(root.jsonl_path, { expectedThreadId: threadId, storageRoots: [root.storage.receipt.canonicalPath] });
231
+ if (!prefix.ok) {
232
+ lines.push(` raw prefix read refused: ${prefix.reason}`);
233
+ return lines;
234
+ }
235
+ lines.push(` lineage ids (${prefix.lineageIds.length}): ${prefix.lineageIds.join(', ')}`);
236
+ const index = firstRefusalIndex(prefix.records, { threadId, lineageIds: prefix.lineageIds });
237
+ if (index < 0) {
238
+ lines.push(' no refusing record found by re-summarizing prefixes (the refusal may be outside the scanned window)');
239
+ } else {
240
+ const record = prefix.records[index];
241
+ const payload = record?.payload && typeof record.payload === 'object' ? record.payload : {};
242
+ lines.push(` first refusing record: index ${index}, type=${record?.type}, payload.type=${payload.type}`);
243
+ if ('thread_id' in payload) {
244
+ const owned = new Set(prefix.lineageIds.map((id) => String(id).toLowerCase()));
245
+ lines.push(` payload.thread_id=${payload.thread_id} in lineage=${owned.has(String(payload.thread_id).toLowerCase())}`);
246
+ }
247
+ }
248
+ // OQ-3 verbatim: every thread_settings_applied record's thread_id against the computed lineage,
249
+ // with its key set, so "the keys are in the D-1 set, so the thread_id must be outside it" is
250
+ // answerable from this output alone.
251
+ const settingsRecords = prefix.records
252
+ .map((record, at) => ({ record, at }))
253
+ .filter(({ record }) => record?.type === 'event_msg' && record.payload?.type === 'thread_settings_applied');
254
+ const owned = new Set(prefix.lineageIds.map((id) => String(id).toLowerCase()));
255
+ lines.push(` thread_settings_applied records: ${settingsRecords.length}`);
256
+ for (const { record, at } of settingsRecords.slice(0, 8)) {
257
+ const payload = record.payload;
258
+ const keys = payload.thread_settings && typeof payload.thread_settings === 'object' ? Object.keys(payload.thread_settings) : [];
259
+ lines.push(` index ${at}: thread_id=${payload.thread_id ?? '(absent)'} in lineage=${payload.thread_id === undefined || payload.thread_id === null ? 'n/a' : owned.has(String(payload.thread_id).toLowerCase())} settings_keys=${keys.length}`);
260
+ }
261
+ return lines;
262
+ }
263
+
264
+ function explainSessions(rows, db) {
265
+ for (const row of rows) {
266
+ process.stdout.write(`\nexplain ${row.ctm_session_id}\n`);
267
+ process.stdout.write(` agent_session_id: ${row.agent_session_id}\n`);
268
+ process.stdout.write(` jsonl_path: ${row.jsonl_path}\n`);
269
+ const storage = storageRootForSession(row.jsonl_path);
270
+ process.stdout.write(` codex_home: ${storage.codexHome || '(no sessions segment)'}\n`);
271
+ process.stdout.write(` storage_root: ${storage.receipt ? storage.receipt.canonicalPath : '(no receipt)'}`
272
+ + `${storage.receipt ? ` fingerprint=${storage.receipt.fingerprint.slice(0, 12)}` : ''}\n`);
273
+ const result = scanSession({ ...row, db });
274
+ process.stdout.write(` verdict: ${result.key}\n`);
275
+ process.stdout.write(` snapshot: ${result.snapshot ? JSON.stringify(result.snapshot, null, 2).split('\n').join('\n ') : '(not captured: the scan never ran or threw)'}\n`);
276
+ if (result.key === 'scan:unsupported_native_history_event' && storage.receipt) {
277
+ for (const line of describeRefusal({ jsonl_path: row.jsonl_path, storage }, row.agent_session_id)) {
278
+ process.stdout.write(`${line}\n`);
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ function run(argv) {
285
+ const args = parseArgs(argv);
286
+ if (args.help) {
287
+ process.stdout.write(`${usage()}\n`);
288
+ return 0;
289
+ }
290
+ const db = openReadOnly(args.db);
291
+ try {
292
+ const rows = db.prepare(SESSION_QUERY).all(args.since, args.limit)
293
+ .filter((row) => !args.only || String(row.ctm_session_id).startsWith(args.only));
294
+ const tallies = new Map();
295
+ const examples = new Map();
296
+ for (const row of rows) {
297
+ const result = scanSession({ ...row, db });
298
+ tallies.set(result.key, (tallies.get(result.key) || 0) + 1);
299
+ const list = examples.get(result.key) || [];
300
+ if (list.length < EXAMPLES_PER_REASON) list.push(prefixed(row.ctm_session_id));
301
+ examples.set(result.key, list);
302
+ }
303
+ printTable(rows, tallies, examples, args);
304
+ if (args.explain) {
305
+ const matching = rows.filter((row) => String(row.ctm_session_id).startsWith(args.explain));
306
+ if (!matching.length) process.stdout.write(`\nexplain ${args.explain}: no matching session\n`);
307
+ else explainSessions(matching, db);
308
+ }
309
+ } finally {
310
+ db.close();
311
+ }
312
+ return 0;
313
+ }
314
+
315
+ if (require.main === module) {
316
+ try {
317
+ process.exitCode = run(process.argv.slice(2));
318
+ } catch (error) {
319
+ if (error instanceof UsageError) process.stderr.write(`census-codex-lead-history: ${error.message}\n${usage()}\n`);
320
+ else process.stderr.write(`census-codex-lead-history: ${error?.stack || error}\n`);
321
+ process.exitCode = error instanceof UsageError ? 1 : 2;
322
+ }
323
+ }
324
+
325
+ module.exports = { run, parseArgs, storageRootForSession, scanSession, firstRefusalIndex };
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // bin/seam-replay-codex.js -- the Codex lane of the reader seam replay harness (design
5
+ // seam-proof-r3-codex-render-parity-2026-10-20, WP-0 / §6).
6
+ //
7
+ // Offline and read-only. For every Codex CTM session that still has a painted-row store, replay the
8
+ // RECORDED live screen (a store `seal` record's `ref.rows`) against CTM's own render of the session's
9
+ // rollouts AS THEY STOOD AT THE SEAL TIME, through the shipped join, and print COUNTS as JSON:
10
+ //
11
+ // sessions Codex sessions (core.db: ctm_sessions.provider = 'codex') with at least one rollout on
12
+ // disk AND a store directory, that contributed at least one taken seal.
13
+ // screens seals that reached the join: the rollout rendered rows at the seal's cut, and the screen
14
+ // held MIN_LIVE_ROWS non-blank rows.
15
+ // unreplayable seals whose render threw, produced no rows, or whose screen was too small to be one.
16
+ // byKind what findJoin decided: proven / overlap / none.
17
+ // why for a `none`, landingDiag's own refusal (`noknown` / `cover` / `loss` / `cut` / ...).
18
+ // dup3 `none` screens whose live rows repeat >= DUP3_MIN transcript-window rows (the visibly
19
+ // duplicated population the design measures).
20
+ // headerRows screens whose first live row is Codex's own `› ` prompt header (WP-3 changes what
21
+ // the join does with it; this counts how often it is there).
22
+ // hiddenTextRowsNotOnScreen proofs whose rows [cut, end) hold an evidence row with a cut word the
23
+ // live screen does not show. Must be 0: it is the join's own safety claim.
24
+ //
25
+ // The render is the SHIPPED lib/raw-codex-history-window.js `getRawCodexHistoryWindow`, fed every
26
+ // rollout of the session with a `fileOps` whose `fstatSync` caps `size` at the seal's cut, so the
27
+ // window is what the rollout held at that moment. Nothing is ever written: no file is opened for
28
+ // writing, no copy of a rollout is made, and the core.db handle is opened read-only and closed before
29
+ // the replay starts.
30
+ //
31
+ // PRIVACY (hard, the same rule as bin/seam-replay.js): never prints row text, transcript lines, file
32
+ // paths, file contents or a full session id. The report is JSON numbers, enums and the fixed key names
33
+ // below -- nothing else.
34
+
35
+ const fs = require('node:fs');
36
+ const path = require('node:path');
37
+ const os = require('node:os');
38
+ const Database = require('better-sqlite3');
39
+ const Raw = require('../lib/raw-codex-history-window.js');
40
+ const RowJoin = require('../public/js/row-join.js');
41
+ // The store's PURE reader only (`parseBuffer` verifies every line's CRC and stops at the first bad
42
+ // one). `createPaintedRowStore` is never called from here, so nothing is written, pruned or swept.
43
+ const { parseBuffer } = require('../lib/painted-row-store.js');
44
+
45
+ const DEFAULT_PER = 40; // newest N seals per session (the same default as the Claude lane)
46
+ const MAX_ROWS = 400; // rows the renderer may return (design §6: 400 / 1 MiB)
47
+ const MAX_BYTES = 1 << 20;
48
+ const SCAN_CHUNK_BYTES = 1 << 24; // 16 MiB: rollouts can be large, so the timestamp scan is chunked
49
+ const STAMP_SCAN_BYTES = 120; // the `"timestamp"` field must be inside a line's first 120 bytes
50
+ const MIN_LIVE_ROWS = 3; // a screen with fewer non-blank live rows is not a screen
51
+ const DUP3_MIN = 3; // `landingDiag().dup >= 3` is the visibly-duplicated population
52
+ const SID_PREFIX = 8; // never used for printing a full id; kept for callers that need one
53
+ // 46 bytes, well over the 32 the cursor signer demands. Local to this script: a cursor is never
54
+ // round-tripped here (each seal starts a fresh `older` read), so the value only has to be constant.
55
+ const CURSOR_SECRET = 'seam-replay-codex-offline-replay-cursor-secret';
56
+ const STRETCH_FILE_RE = /^stretch-\d+\.jsonl$/; // one stretch of a painted-row store session
57
+ const CODEX_PROMPT_HEADER = /^› /u; // Codex's own presentation-only prompt header row
58
+ // The Codex sessions and their rollouts. Read-only; `jsonl_path <> ''` skips a session with no file.
59
+ const CODEX_SESSIONS_SQL = "select a.ctm_session_id sid, a.jsonl_path p from agent_sessions a "
60
+ + "join ctm_sessions c on c.id = a.ctm_session_id "
61
+ + "where c.provider = 'codex' and a.provider = 'codex' and a.jsonl_path <> '' order by a.jsonl_path";
62
+
63
+ const defaultStore = () => path.join(os.homedir(), '.walle', 'data', 'painted-rows');
64
+ const defaultCoreDb = () => path.join(os.homedir(), '.walle', 'data', 'core.db');
65
+
66
+ // --- text handling ---------------------------------------------------------
67
+
68
+ // The Claude lane's `strip` (bin/seam-replay.js), which is byte for byte RowJoin.stripForCompare:
69
+ // SGR/CSI removed, U+00A0 folded to a space, trailing blanks trimmed. Delegating to the join's own
70
+ // copy keeps one truth (and findJoin strips every row it is handed with it anyway).
71
+ function strip(text) {
72
+ return RowJoin.stripForCompare(text);
73
+ }
74
+
75
+ function inc(counts, key) {
76
+ counts[key] = (counts[key] || 0) + 1;
77
+ }
78
+
79
+ // --- core.db (read-only) ---------------------------------------------------
80
+
81
+ // Codex CTM sessions with at least one rollout still on disk: sid -> [rollout path], in path order.
82
+ // The DB handle is opened read-only (`fileMustExist`) and closed before anything else runs; a session
83
+ // whose rollout file has gone is dropped here rather than counted.
84
+ function codexSessions(coreDb) {
85
+ const db = new Database(coreDb, { readonly: true, fileMustExist: true });
86
+ let pairs = [];
87
+ try {
88
+ pairs = db.prepare(CODEX_SESSIONS_SQL).all();
89
+ } finally {
90
+ db.close();
91
+ }
92
+ const bySid = new Map();
93
+ for (const row of pairs) {
94
+ const file = String(row.p == null ? '' : row.p);
95
+ if (!file || !fs.existsSync(file)) continue;
96
+ const sid = String(row.sid == null ? '' : row.sid);
97
+ const list = bySid.get(sid);
98
+ if (list) list.push(file);
99
+ else bySid.set(sid, [file]);
100
+ }
101
+ return bySid;
102
+ }
103
+
104
+ // --- the recorded screens --------------------------------------------------
105
+
106
+ // Every `seal` record of one session directory, in file order (`stretch-*.jsonl` sorted by name, so
107
+ // the oldest stretch first). Each line's CRC is verified by the store's own reader; a file that is
108
+ // missing, unreadable or torn past its first bad line contributes only what it verified.
109
+ function readSeals(dir) {
110
+ let names;
111
+ try {
112
+ names = fs.readdirSync(dir);
113
+ } catch {
114
+ return []; /* expected: the session directory went away between the store scan and the read */
115
+ }
116
+ const seals = [];
117
+ for (const name of names.filter((n) => STRETCH_FILE_RE.test(n)).sort()) {
118
+ let buffer;
119
+ try {
120
+ buffer = fs.readFileSync(path.join(dir, name));
121
+ } catch {
122
+ continue; /* expected: a stretch file unreadable between the listing and the read */
123
+ }
124
+ for (const object of parseBuffer(buffer).objects) {
125
+ if (object.op !== 'seal' || !object.ref || !Array.isArray(object.ref.rows)) continue;
126
+ if (typeof object.at !== 'number') continue;
127
+ seals.push(object);
128
+ }
129
+ }
130
+ return seals;
131
+ }
132
+
133
+ // The timestamp marks of one rollout: `[parsedMs, byteOffset]` for every line whose first
134
+ // STAMP_SCAN_BYTES bytes hold a `"timestamp":"..."` field, in file order, plus the file size. Read in
135
+ // SCAN_CHUNK_BYTES chunks with a carry, because a rollout can hold more than one chunk and the scan is
136
+ // per line, not per chunk. Read-only: the file is opened 'r' and closed before returning.
137
+ function timestampMarks(file) {
138
+ const fd = fs.openSync(file, 'r');
139
+ try {
140
+ const size = fs.fstatSync(fd).size;
141
+ const marks = [];
142
+ let carry = Buffer.alloc(0);
143
+ let base = 0;
144
+ for (let offset = 0; offset < size; offset += SCAN_CHUNK_BYTES) {
145
+ const chunk = Buffer.alloc(Math.min(SCAN_CHUNK_BYTES, size - offset));
146
+ fs.readSync(fd, chunk, 0, chunk.length, offset);
147
+ const buffer = carry.length ? Buffer.concat([carry, chunk]) : chunk;
148
+ let position = 0;
149
+ for (;;) {
150
+ const newline = buffer.indexOf(10, position);
151
+ if (newline < 0) break;
152
+ const line = buffer.subarray(position, Math.min(newline, position + STAMP_SCAN_BYTES)).toString('utf8');
153
+ const match = line.match(/"timestamp":"([^"]+)"/);
154
+ if (match) marks.push([Date.parse(match[1]), base + position]);
155
+ position = newline + 1;
156
+ }
157
+ carry = buffer.subarray(position);
158
+ base += position;
159
+ }
160
+ return { size, marks };
161
+ } finally {
162
+ fs.closeSync(fd);
163
+ }
164
+ }
165
+
166
+ // The byte offset a seal's view of a rollout ends at: the offset of the FIRST line whose timestamp is
167
+ // after the seal, else the whole file (the rollout never went further than the seal).
168
+ function cutFor(marks, size, at) {
169
+ for (const [ms, offset] of marks) if (ms > at) return offset;
170
+ return size;
171
+ }
172
+
173
+ // Is `sid` a session the store still has a directory for? A store without one is simply not replayed.
174
+ function hasStoreDir(store, sid) {
175
+ try {
176
+ return fs.statSync(path.join(store, sid)).isDirectory();
177
+ } catch {
178
+ return false; /* expected: no store directory for this session (or it is not a directory) */
179
+ }
180
+ }
181
+
182
+ // --- the join --------------------------------------------------------------
183
+
184
+ // The safety count (design §7): does a proof's hidden tail hold an EVIDENCE row with a cut word the
185
+ // live screen does not show? Rows, not words; nothing is printed.
186
+ function hidesEvidenceRow(rows, kinds, live, cut) {
187
+ const liveWords = new Set();
188
+ for (const row of live) for (const word of RowJoin.cutWordsOf(row)) liveWords.add(word);
189
+ for (let i = Math.max(0, cut); i < rows.length; i += 1) {
190
+ if (kinds && kinds[i] !== 'text' && kinds[i] !== 'prompt') continue;
191
+ if (RowJoin.cutWordsOf(strip(rows[i])).some((word) => !liveWords.has(word))) return true;
192
+ }
193
+ return false;
194
+ }
195
+
196
+ // Replay ONE seal: render the session's rollouts as they stood at `seal.at`, join the recorded screen
197
+ // against them, and tally. Mutates `report`; returns nothing. Counts and enums only, ever.
198
+ function replaySeal(report, sid, seal, scans) {
199
+ const cutOf = new Map();
200
+ for (const scan of scans) cutOf.set(scan.file, cutFor(scan.marks, scan.size, seal.at));
201
+ const sources = [];
202
+ for (const scan of scans) {
203
+ const cut = cutOf.get(scan.file);
204
+ if (cut > 0) sources.push({ jsonl_path: scan.file, provider: 'codex', size: cut, offset: cut });
205
+ }
206
+ // No rollout held a byte at the seal time: there is no screen to replay and no join to make.
207
+ if (!sources.length) return;
208
+ // The cut is a `fstat` size cap, nothing more: the window reads the real files (read-only) and stops
209
+ // at the sealed size. `fd` -> path is remembered so a copied stat can be capped per source.
210
+ const fdPath = new Map();
211
+ const fileOps = {
212
+ ...fs,
213
+ openSync(file, ...args) {
214
+ const fd = fs.openSync(file, ...args);
215
+ fdPath.set(fd, String(file));
216
+ return fd;
217
+ },
218
+ fstatSync(fd) {
219
+ const stat = fs.fstatSync(fd);
220
+ const cut = cutOf.get(fdPath.get(fd));
221
+ return Object.assign(Object.create(Object.getPrototypeOf(stat)), stat, {
222
+ size: cut === undefined ? stat.size : Math.min(stat.size, cut),
223
+ });
224
+ },
225
+ };
226
+ let win;
227
+ try {
228
+ win = Raw.getRawCodexHistoryWindow(sources, {
229
+ sessionId: sid, provider: 'codex', cols: seal.ref.cols, maxRows: MAX_ROWS, maxBytes: MAX_BYTES, cursorSecret: CURSOR_SECRET,
230
+ }, fileOps);
231
+ } catch {
232
+ report.unreplayable += 1; /* expected: a rollout shape the renderer refuses (counted, never printed) */
233
+ return;
234
+ }
235
+ const items = win.items || [];
236
+ const rows = win.rows || items.map((item) => item.content);
237
+ if (!rows.length) {
238
+ report.unreplayable += 1; /* expected: a render that produced no rows (counted, never printed) */
239
+ return;
240
+ }
241
+ const kinds = items.length === rows.length ? items.map((item) => item.kind) : undefined;
242
+ const live = seal.ref.rows.slice(seal.ref.pinned || 0).map(strip);
243
+ if (live.filter((row) => row.trim()).length < MIN_LIVE_ROWS) {
244
+ report.unreplayable += 1; /* expected: a screen too small to be one (counted, never printed) */
245
+ return;
246
+ }
247
+ report.screens += 1;
248
+ const found = RowJoin.findJoin({ transcriptRows: rows, transcriptKinds: kinds, painted: { from: 0, rows: [] }, liveRows: live, appBelow: 0 });
249
+ inc(report.byKind, found.kind);
250
+ if (found.kind === 'none') {
251
+ const diag = RowJoin.landingDiag(rows, live, kinds);
252
+ inc(report.why, diag.why);
253
+ if (diag.dup >= DUP3_MIN) report.dup3 += 1;
254
+ }
255
+ if (CODEX_PROMPT_HEADER.test(live[0] || '')) report.headerRows += 1;
256
+ if (found.kind !== 'none' && hidesEvidenceRow(rows, kinds, live, found.cut)) report.hiddenTextRowsNotOnScreen += 1;
257
+ }
258
+
259
+ // --- the run ---------------------------------------------------------------
260
+
261
+ // The whole `--codex` mode (design §6). Returns the report object; the counts are the only thing an
262
+ // operator reads, and the caller prints it as JSON. `opts`:
263
+ // per newest N seals per session (default DEFAULT_PER)
264
+ // until an ISO time (or epoch ms) -- seals at or after it are not taken; null = no limit
265
+ // store painted-rows root (default ~/.walle/data/painted-rows)
266
+ // coreDb CTM core database (default ~/.walle/data/core.db), opened READ-ONLY
267
+ function runCodex(opts) {
268
+ const options = opts || {};
269
+ const per = Number(options.per) > 0 ? Math.floor(Number(options.per)) : DEFAULT_PER;
270
+ const until = options.until === null || options.until === undefined || options.until === ''
271
+ ? null
272
+ : (typeof options.until === 'number' ? options.until : Date.parse(String(options.until)));
273
+ if (until !== null && !Number.isFinite(until)) throw new TypeError('--until must be an ISO time');
274
+ const report = {
275
+ mode: 'codex',
276
+ until: until === null ? null : new Date(until).toISOString(),
277
+ per,
278
+ sessions: 0,
279
+ screens: 0,
280
+ unreplayable: 0,
281
+ byKind: { proven: 0, overlap: 0, none: 0 },
282
+ why: {},
283
+ dup3: 0,
284
+ headerRows: 0,
285
+ hiddenTextRowsNotOnScreen: 0,
286
+ };
287
+ const store = options.store ? String(options.store) : defaultStore();
288
+ for (const [sid, paths] of codexSessions(options.coreDb ? String(options.coreDb) : defaultCoreDb())) {
289
+ if (!hasStoreDir(store, sid)) continue;
290
+ const seals = readSeals(path.join(store, sid))
291
+ .filter((seal) => until === null || seal.at <= until)
292
+ .slice(-per);
293
+ if (!seals.length) continue;
294
+ report.sessions += 1;
295
+ const scans = paths.map((file) => Object.assign({ file }, timestampMarks(file)));
296
+ for (const seal of seals) replaySeal(report, sid, seal, scans);
297
+ }
298
+ return report;
299
+ }
300
+
301
+ module.exports = { runCodex, defaultStore, defaultCoreDb, DEFAULT_PER, MAX_ROWS, MAX_BYTES, SID_PREFIX };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const os = require('node:os');
4
+
3
5
  // GAP-2: the throttle below used to dedupe purely by `event`, regardless of
4
6
  // which client sent it — two viewers on the same session probing within 1s
5
7
  // of each other would see one silently dropped. For terminal-authority this
@@ -47,6 +49,23 @@ const MAX_DIAG_THROTTLE_KEYS = 512;
47
49
  // 24-key cut silently dropped its trailing distPx/exitBy/provider fields.
48
50
  const MAX_GENERIC_DETAIL_KEYS = 32;
49
51
 
52
+ // WP-B C-4: the host context AT RECEIPT of a client stall row, so a client-side frame width can be
53
+ // read against what the machine was doing at that moment (a 35s frame during a load-40 host is not
54
+ // the same evidence as the same frame on an idle one) without correlating a second log line. The
55
+ // row's own client clock cannot be trusted, so `receivedAt` is the server's. Best effort: a platform
56
+ // that refuses any of these calls records `host: null` rather than dropping the diagnostic.
57
+ function clientDiagHostSnapshot() {
58
+ try {
59
+ const cpus = os.cpus();
60
+ return {
61
+ loadPerCore: Math.round((Number(os.loadavg()[0]) / (cpus.length || 1)) * 100) / 100,
62
+ freeMemPct: Math.round((os.freemem() / os.totalmem()) * 1000) / 10,
63
+ serverRssMB: Math.round(process.memoryUsage().rss / 1048576),
64
+ receivedAt: Date.now(),
65
+ };
66
+ } catch { /* expected: host sampling is advisory; its failure must not drop the client row */ return null; }
67
+ }
68
+
50
69
  function createClientDiagHandler(deps = {}) {
51
70
  const getSession = deps.getSession || (() => null);
52
71
  const isSubscribed = typeof deps.isSubscribed === 'function' ? deps.isSubscribed : () => false;
@@ -129,6 +148,7 @@ function createClientDiagHandler(deps = {}) {
129
148
  entries: routed.entries,
130
149
  count: routed.entries.length,
131
150
  maxDurMs: routed.top.dur,
151
+ host: clientDiagHostSnapshot(),
132
152
  });
133
153
  return;
134
154
  }