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,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
|
}
|