throughline 0.3.24 → 0.4.0
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/.claude/commands/tl.md +6 -21
- package/.codex-sidecar.yml +62 -0
- package/CHANGELOG.md +632 -0
- package/README.ja.md +71 -46
- package/README.md +420 -76
- package/bin/throughline.mjs +169 -7
- package/codex/skills/throughline/SKILL.md +157 -0
- package/codex/skills/throughline/agents/openai.yaml +7 -0
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +159 -0
- package/docs/L1_L2_L3_REDESIGN.md +415 -0
- package/docs/PUBLIC_RELEASE_PLAN.md +185 -0
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +286 -0
- package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
- package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
- package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
- package/docs/archive/CONCEPT.md +476 -0
- package/docs/archive/EXPERIMENT.md +371 -0
- package/docs/archive/README.md +22 -0
- package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
- package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
- package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
- package/docs/throughline-handoff-context.example.json +57 -0
- package/docs/throughline-rollback-context-trim-insight.md +455 -0
- package/package.json +6 -2
- package/src/baton.mjs +17 -45
- package/src/baton.test.mjs +4 -41
- package/src/cli/codex-capture.mjs +95 -0
- package/src/cli/codex-handoff-model-smoke.mjs +292 -0
- package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
- package/src/cli/codex-handoff-smoke.mjs +163 -0
- package/src/cli/codex-handoff-smoke.test.mjs +149 -0
- package/src/cli/codex-handoff-start.mjs +291 -0
- package/src/cli/codex-handoff-start.test.mjs +194 -0
- package/src/cli/codex-hook.mjs +276 -0
- package/src/cli/codex-hook.test.mjs +293 -0
- package/src/cli/codex-host-primitive-audit.mjs +110 -0
- package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
- package/src/cli/codex-restore-smoke.mjs +357 -0
- package/src/cli/codex-restore-source-audit.mjs +304 -0
- package/src/cli/codex-resume.mjs +138 -0
- package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
- package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
- package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
- package/src/cli/codex-sidecar-dry-run.mjs +85 -0
- package/src/cli/codex-summarize.mjs +224 -0
- package/src/cli/codex-threads.mjs +89 -0
- package/src/cli/codex-visibility-smoke.mjs +196 -0
- package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
- package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
- package/src/cli/doctor.mjs +503 -1
- package/src/cli/doctor.test.mjs +542 -3
- package/src/cli/handoff-preview.mjs +78 -0
- package/src/cli/help.test.mjs +64 -0
- package/src/cli/install.mjs +226 -3
- package/src/cli/install.test.mjs +205 -4
- package/src/cli/trim.mjs +564 -0
- package/src/codex-app-server.mjs +1816 -0
- package/src/codex-app-server.test.mjs +512 -0
- package/src/codex-auto-refresh.mjs +194 -0
- package/src/codex-auto-refresh.test.mjs +182 -0
- package/src/codex-capture.mjs +235 -0
- package/src/codex-capture.test.mjs +393 -0
- package/src/codex-handoff-model-smoke.mjs +114 -0
- package/src/codex-handoff-model-smoke.test.mjs +89 -0
- package/src/codex-handoff-smoke.mjs +124 -0
- package/src/codex-handoff-smoke.test.mjs +103 -0
- package/src/codex-handoff.mjs +331 -0
- package/src/codex-handoff.test.mjs +220 -0
- package/src/codex-host-primitive-audit.mjs +374 -0
- package/src/codex-host-primitive-audit.test.mjs +208 -0
- package/src/codex-restore-smoke.test.mjs +639 -0
- package/src/codex-restore-source-audit.mjs +1348 -0
- package/src/codex-restore-source-audit.test.mjs +623 -0
- package/src/codex-resume.test.mjs +242 -0
- package/src/codex-rollout-memory.mjs +711 -0
- package/src/codex-rollout-memory.test.mjs +610 -0
- package/src/codex-sidecar-cli.test.mjs +75 -0
- package/src/codex-sidecar.mjs +246 -0
- package/src/codex-sidecar.test.mjs +172 -0
- package/src/codex-summarize.test.mjs +143 -0
- package/src/codex-thread-identity.mjs +23 -0
- package/src/codex-thread-index.mjs +173 -0
- package/src/codex-thread-index.test.mjs +164 -0
- package/src/codex-usage.mjs +110 -0
- package/src/codex-usage.test.mjs +140 -0
- package/src/codex-visibility-smoke.test.mjs +222 -0
- package/src/codex-vscode-restore-smoke.mjs +206 -0
- package/src/codex-vscode-restore-smoke.test.mjs +325 -0
- package/src/codex-vscode-rollback-smoke.mjs +90 -0
- package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
- package/src/db-schema.test.mjs +96 -0
- package/src/db.mjs +14 -1
- package/src/haiku-summarizer.mjs +267 -26
- package/src/haiku-summarizer.test.mjs +282 -0
- package/src/handoff-preview.test.mjs +108 -0
- package/src/handoff-record.mjs +294 -0
- package/src/handoff-record.test.mjs +226 -0
- package/src/hook-entrypoints.test.mjs +286 -0
- package/src/package-files.test.mjs +19 -0
- package/src/prompt-submit.mjs +9 -6
- package/src/resume-context.mjs +58 -171
- package/src/resume-context.test.mjs +177 -0
- package/src/session-start.mjs +85 -26
- package/src/state-file.mjs +50 -6
- package/src/state-file.test.mjs +50 -0
- package/src/token-monitor.mjs +14 -10
- package/src/token-monitor.test.mjs +27 -0
- package/src/trim-cli.test.mjs +1584 -0
- package/src/trim-model.mjs +584 -0
- package/src/trim-model.test.mjs +568 -0
- package/src/turn-processor.mjs +17 -10
- package/src/vscode-task.mjs +33 -10
- package/src/vscode-task.test.mjs +19 -9
- package/src/cli/save-inflight.mjs +0 -81
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { runCodexThreadRestoreSmoke } from '../codex-app-server.mjs';
|
|
2
|
+
import { buildCodexRolloutTrimSource } from '../codex-rollout-memory.mjs';
|
|
3
|
+
import { resolveCodexThreadIdentity } from '../codex-thread-identity.mjs';
|
|
4
|
+
|
|
5
|
+
const CODEX_RESTORE_SMOKE_ENV = 'THROUGHLINE_EXPERIMENTAL_CODEX_RESTORE_SMOKE';
|
|
6
|
+
|
|
7
|
+
function parseArgs(args) {
|
|
8
|
+
const out = {
|
|
9
|
+
codexThreadId: null,
|
|
10
|
+
json: false,
|
|
11
|
+
codexAppServerBin: null,
|
|
12
|
+
timeoutMs: 60_000,
|
|
13
|
+
requestTimeoutMs: 20_000,
|
|
14
|
+
cycles: 2,
|
|
15
|
+
turnsListLimit: 200,
|
|
16
|
+
maxTurnsListPages: 50,
|
|
17
|
+
inspectRiskyRollout: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
const arg = args[i];
|
|
22
|
+
if (arg === '--codex-thread-id') {
|
|
23
|
+
const value = args[++i];
|
|
24
|
+
if (!value || value.startsWith('-')) {
|
|
25
|
+
throw new Error('--codex-thread-id requires a thread id');
|
|
26
|
+
}
|
|
27
|
+
out.codexThreadId = value;
|
|
28
|
+
} else if (arg === '--codex-app-server-bin') {
|
|
29
|
+
const value = args[++i];
|
|
30
|
+
if (!value || value.startsWith('-')) {
|
|
31
|
+
throw new Error('--codex-app-server-bin requires a command path');
|
|
32
|
+
}
|
|
33
|
+
out.codexAppServerBin = value;
|
|
34
|
+
} else if (arg === '--timeout-ms') {
|
|
35
|
+
const value = Number(args[++i]);
|
|
36
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
37
|
+
throw new Error('--timeout-ms must be a positive integer');
|
|
38
|
+
}
|
|
39
|
+
out.timeoutMs = value;
|
|
40
|
+
} else if (arg === '--request-timeout-ms') {
|
|
41
|
+
const value = Number(args[++i]);
|
|
42
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
43
|
+
throw new Error('--request-timeout-ms must be a positive integer');
|
|
44
|
+
}
|
|
45
|
+
out.requestTimeoutMs = value;
|
|
46
|
+
} else if (arg === '--cycles') {
|
|
47
|
+
const value = Number(args[++i]);
|
|
48
|
+
if (!Number.isInteger(value) || value < 2) {
|
|
49
|
+
throw new Error('--cycles must be an integer >= 2');
|
|
50
|
+
}
|
|
51
|
+
out.cycles = value;
|
|
52
|
+
} else if (arg === '--turns-list-limit') {
|
|
53
|
+
const value = Number(args[++i]);
|
|
54
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
55
|
+
throw new Error('--turns-list-limit must be a positive integer');
|
|
56
|
+
}
|
|
57
|
+
out.turnsListLimit = value;
|
|
58
|
+
} else if (arg === '--max-turns-list-pages') {
|
|
59
|
+
const value = Number(args[++i]);
|
|
60
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
61
|
+
throw new Error('--max-turns-list-pages must be a positive integer');
|
|
62
|
+
}
|
|
63
|
+
out.maxTurnsListPages = value;
|
|
64
|
+
} else if (arg === '--inspect-risky-rollout') {
|
|
65
|
+
out.inspectRiskyRollout = true;
|
|
66
|
+
} else if (arg === '--json') {
|
|
67
|
+
out.json = true;
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderTextResult(result) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
lines.push('throughline codex restore smoke');
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push(` status: ${result.status}`);
|
|
81
|
+
lines.push(` reason: ${result.reason}`);
|
|
82
|
+
lines.push(` thread: ${result.threadId ?? 'unknown'}`);
|
|
83
|
+
lines.push(` rollout: ${result.rolloutPath ?? 'unknown'}`);
|
|
84
|
+
lines.push(` proof scope: ${result.proofScope ?? 'none'}`);
|
|
85
|
+
lines.push(` restart safe: ${result.restartSafe ? 'yes' : 'no'}`);
|
|
86
|
+
if (result.restoreSafety) {
|
|
87
|
+
lines.push(` restore safety: ${result.restoreSafety.status}`);
|
|
88
|
+
}
|
|
89
|
+
if (result.restoreSafetyRiskInspected) {
|
|
90
|
+
lines.push(' restore safety risk inspected read-only: yes');
|
|
91
|
+
}
|
|
92
|
+
if (Number.isInteger(result.expectedTurns)) {
|
|
93
|
+
lines.push(` expected turns: ${result.expectedTurns}`);
|
|
94
|
+
}
|
|
95
|
+
const aggregateTextMatchSummary = formatResponseTextMatchSummary(result.restoreTextMatchCheck);
|
|
96
|
+
if (aggregateTextMatchSummary) {
|
|
97
|
+
lines.push(` restore text match check: ${aggregateTextMatchSummary}`);
|
|
98
|
+
}
|
|
99
|
+
for (const observation of result.observations ?? []) {
|
|
100
|
+
lines.push(
|
|
101
|
+
` cycle ${observation.cycle}: read=${observation.readTurns ?? 'unknown'} resume=${
|
|
102
|
+
observation.resumedTurns ?? 'unknown'
|
|
103
|
+
} turns/list=${observation.turnsListTurns ?? 'unknown'} check=${
|
|
104
|
+
observation.turnCountCheck?.status ?? 'unknown'
|
|
105
|
+
}`,
|
|
106
|
+
);
|
|
107
|
+
const textMatchSummary = formatResponseTextMatchSummary(observation.responseTextMatches);
|
|
108
|
+
if (textMatchSummary) {
|
|
109
|
+
lines.push(` response text matches: ${textMatchSummary}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatResponseTextMatchSummary(responseTextMatches) {
|
|
116
|
+
if (!responseTextMatches) return null;
|
|
117
|
+
const status = responseTextMatches.status ?? 'unknown';
|
|
118
|
+
const matchedNeedles = responseTextMatches.matchedNeedles ?? [];
|
|
119
|
+
const matchingSources = [
|
|
120
|
+
...new Set(
|
|
121
|
+
(responseTextMatches.sources ?? [])
|
|
122
|
+
.filter(
|
|
123
|
+
(source) =>
|
|
124
|
+
(source.matches ?? []).length > 0 || (source.matchedNeedleIds ?? []).length > 0,
|
|
125
|
+
)
|
|
126
|
+
.map((source) => source.source ?? source.id)
|
|
127
|
+
.filter(Boolean),
|
|
128
|
+
),
|
|
129
|
+
];
|
|
130
|
+
const parts = [status];
|
|
131
|
+
if (matchedNeedles.length > 0) {
|
|
132
|
+
parts.push(`${matchedNeedles.length} needle${matchedNeedles.length === 1 ? '' : 's'}`);
|
|
133
|
+
}
|
|
134
|
+
if (matchingSources.length > 0) {
|
|
135
|
+
parts.push(`sources=${matchingSources.join(',')}`);
|
|
136
|
+
}
|
|
137
|
+
const pathsBySource = (responseTextMatches.sources ?? []).map((source) => {
|
|
138
|
+
const paths =
|
|
139
|
+
source.samplePaths ??
|
|
140
|
+
[
|
|
141
|
+
...new Set(
|
|
142
|
+
(source.matches ?? []).flatMap((match) =>
|
|
143
|
+
(match.locations ?? []).map((location) => location.path),
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
];
|
|
147
|
+
return {
|
|
148
|
+
source: source.source ?? source.id,
|
|
149
|
+
paths,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
const firstPathPerSource = pathsBySource
|
|
153
|
+
.filter((entry) => entry.paths.length > 0)
|
|
154
|
+
.map((entry) => `${entry.source}:${entry.paths[0]}`);
|
|
155
|
+
const extraPaths = pathsBySource.flatMap((entry) =>
|
|
156
|
+
entry.paths.slice(1, 3).map((path) => `${entry.source}:${path}`),
|
|
157
|
+
);
|
|
158
|
+
const samplePaths = [...firstPathPerSource, ...extraPaths].slice(0, 6);
|
|
159
|
+
if (samplePaths.length > 0) {
|
|
160
|
+
parts.push(`paths=${samplePaths.join(';')}`);
|
|
161
|
+
}
|
|
162
|
+
const locationKinds = [
|
|
163
|
+
...new Set(
|
|
164
|
+
(responseTextMatches.sources ?? []).flatMap((source) => {
|
|
165
|
+
if (source.locationKinds) return source.locationKinds;
|
|
166
|
+
return (source.matches ?? []).flatMap((match) =>
|
|
167
|
+
(match.locations ?? []).map((location) => location.kind).filter(Boolean),
|
|
168
|
+
);
|
|
169
|
+
}),
|
|
170
|
+
),
|
|
171
|
+
];
|
|
172
|
+
if (locationKinds.length > 0) {
|
|
173
|
+
parts.push(`kinds=${locationKinds.join(',')}`);
|
|
174
|
+
}
|
|
175
|
+
const locationRisks = [
|
|
176
|
+
...new Set(
|
|
177
|
+
(responseTextMatches.sources ?? []).flatMap((source) => {
|
|
178
|
+
if (source.locationRisks) return source.locationRisks;
|
|
179
|
+
return (source.matches ?? []).flatMap((match) =>
|
|
180
|
+
(match.locations ?? []).map((location) => location.risk).filter(Boolean),
|
|
181
|
+
);
|
|
182
|
+
}),
|
|
183
|
+
),
|
|
184
|
+
];
|
|
185
|
+
if (locationRisks.length > 0) {
|
|
186
|
+
parts.push(`risks=${locationRisks.join(',')}`);
|
|
187
|
+
}
|
|
188
|
+
const blockingKinds = [
|
|
189
|
+
...new Set(
|
|
190
|
+
responseTextMatches.blockingKinds ??
|
|
191
|
+
(responseTextMatches.sources ?? []).flatMap((source) => {
|
|
192
|
+
if (source.blockingKinds) return source.blockingKinds;
|
|
193
|
+
return (source.matches ?? []).flatMap((match) =>
|
|
194
|
+
(match.locations ?? [])
|
|
195
|
+
.filter((location) => location.blockingCandidate)
|
|
196
|
+
.map((location) => location.kind),
|
|
197
|
+
);
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
];
|
|
201
|
+
if (responseTextMatches.hasBlockingCandidates === false) {
|
|
202
|
+
parts.push('blocking-candidates=no');
|
|
203
|
+
} else if (blockingKinds.length > 0) {
|
|
204
|
+
parts.push(`blocking-candidates=${blockingKinds.join(',')}`);
|
|
205
|
+
}
|
|
206
|
+
if (responseTextMatches.reason) {
|
|
207
|
+
parts.push(`reason=${responseTextMatches.reason}`);
|
|
208
|
+
}
|
|
209
|
+
return parts.join(' ');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function run(args) {
|
|
213
|
+
let parsed;
|
|
214
|
+
try {
|
|
215
|
+
parsed = parseArgs(args);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
218
|
+
process.stderr.write(`[codex-restore-smoke] ${msg}\n`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
parsed = {
|
|
223
|
+
...parsed,
|
|
224
|
+
...resolveCodexThreadIdentity({ host: 'codex', codexThreadId: parsed.codexThreadId }, process.env),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (process.env[CODEX_RESTORE_SMOKE_ENV] !== '1') {
|
|
228
|
+
const result = {
|
|
229
|
+
status: 'refused',
|
|
230
|
+
reason: 'experimental_env_required',
|
|
231
|
+
requiredEnv: `${CODEX_RESTORE_SMOKE_ENV}=1`,
|
|
232
|
+
proofScope: 'none',
|
|
233
|
+
restartSafe: false,
|
|
234
|
+
};
|
|
235
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
236
|
+
else process.stderr.write(`[codex-restore-smoke] ${result.reason}\n`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!parsed.codexThreadId) {
|
|
241
|
+
const result = {
|
|
242
|
+
status: 'refused',
|
|
243
|
+
reason: 'codex_thread_id_required',
|
|
244
|
+
proofScope: 'none',
|
|
245
|
+
restartSafe: false,
|
|
246
|
+
};
|
|
247
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
248
|
+
else process.stderr.write(`[codex-restore-smoke] ${result.reason}\n`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const trimSource = buildCodexRolloutTrimSource({
|
|
253
|
+
threadId: parsed.codexThreadId,
|
|
254
|
+
projectPath: process.cwd(),
|
|
255
|
+
sourceReason:
|
|
256
|
+
parsed.codexThreadIdSource && parsed.codexThreadIdSource.startsWith('env:')
|
|
257
|
+
? 'env_codex_thread_rollout'
|
|
258
|
+
: 'explicit_codex_thread_rollout',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!trimSource) {
|
|
262
|
+
const result = {
|
|
263
|
+
status: 'refused',
|
|
264
|
+
reason: 'codex_rollout_source_required',
|
|
265
|
+
threadId: parsed.codexThreadId,
|
|
266
|
+
proofScope: 'none',
|
|
267
|
+
restartSafe: false,
|
|
268
|
+
};
|
|
269
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
270
|
+
else process.stderr.write(`[codex-restore-smoke] ${result.reason}\n`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (trimSource.restoreSafety?.status === 'risk' && !parsed.inspectRiskyRollout) {
|
|
275
|
+
const result = {
|
|
276
|
+
status: 'refused',
|
|
277
|
+
reason: 'restore_safety_risk',
|
|
278
|
+
threadId: parsed.codexThreadId,
|
|
279
|
+
rolloutPath: trimSource.rolloutPath,
|
|
280
|
+
restoreSafety: trimSource.restoreSafety,
|
|
281
|
+
proofScope: 'none',
|
|
282
|
+
restartSafe: false,
|
|
283
|
+
};
|
|
284
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
285
|
+
else process.stdout.write(renderTextResult(result) + '\n');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const command = parsed.codexAppServerBin ?? process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex';
|
|
290
|
+
let result;
|
|
291
|
+
try {
|
|
292
|
+
result = await runCodexThreadRestoreSmoke({
|
|
293
|
+
threadId: parsed.codexThreadId,
|
|
294
|
+
cwd: process.cwd(),
|
|
295
|
+
expectedTurns: trimSource.capturedTurns,
|
|
296
|
+
restoreTextNeedles: buildRestoreTextNeedles(trimSource.restoreSafety),
|
|
297
|
+
command,
|
|
298
|
+
timeoutMs: parsed.timeoutMs,
|
|
299
|
+
requestTimeoutMs: parsed.requestTimeoutMs,
|
|
300
|
+
cycles: parsed.cycles,
|
|
301
|
+
turnsListLimit: parsed.turnsListLimit,
|
|
302
|
+
maxTurnsListPages: parsed.maxTurnsListPages,
|
|
303
|
+
});
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
306
|
+
const errorResult = {
|
|
307
|
+
status: 'app-server-restore-smoke-error',
|
|
308
|
+
reason: 'app_server_restore_request_failed',
|
|
309
|
+
error: msg,
|
|
310
|
+
threadId: parsed.codexThreadId,
|
|
311
|
+
rolloutPath: trimSource.rolloutPath,
|
|
312
|
+
restoreSafety: trimSource.restoreSafety,
|
|
313
|
+
proofScope: 'app_server_process_restart_only',
|
|
314
|
+
restartSafe: false,
|
|
315
|
+
};
|
|
316
|
+
if (parsed.json) process.stdout.write(JSON.stringify(errorResult, null, 2) + '\n');
|
|
317
|
+
else process.stdout.write(renderTextResult(errorResult) + '\n');
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
const payload = {
|
|
321
|
+
...result,
|
|
322
|
+
rolloutPath: trimSource.rolloutPath,
|
|
323
|
+
restoreSafety: trimSource.restoreSafety,
|
|
324
|
+
restoreSafetyRiskInspected: trimSource.restoreSafety?.status === 'risk' && parsed.inspectRiskyRollout,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
if (parsed.json) process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
328
|
+
else process.stdout.write(renderTextResult(payload) + '\n');
|
|
329
|
+
process.exit(payload.status === 'app-server-restart-stable' && !payload.restoreSafetyRiskInspected ? 0 : 1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildRestoreTextNeedles(restoreSafety) {
|
|
333
|
+
const entries = restoreSafety?.retainedTexts ?? [];
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
const needles = [];
|
|
336
|
+
let index = 1;
|
|
337
|
+
for (const entry of entries) {
|
|
338
|
+
const value = normalizeRestoreTextNeedle(entry.textPreview);
|
|
339
|
+
if (value.length < 20 || seen.has(value)) continue;
|
|
340
|
+
seen.add(value);
|
|
341
|
+
needles.push({
|
|
342
|
+
id: `retained_rollback_text_${index++}`,
|
|
343
|
+
textPreview: entry.textPreview,
|
|
344
|
+
value,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return needles;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function normalizeRestoreTextNeedle(value) {
|
|
351
|
+
return String(value ?? '').replace(' [truncated]', '').replace(/\s+/g, ' ').trim();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export const _internal = {
|
|
355
|
+
parseArgs,
|
|
356
|
+
formatResponseTextMatchSummary,
|
|
357
|
+
};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { runCodexRestoreSourceAudit } from '../codex-restore-source-audit.mjs';
|
|
2
|
+
import { defaultCodexHome } from '../codex-thread-index.mjs';
|
|
3
|
+
import { resolveCodexThreadIdentity } from '../codex-thread-identity.mjs';
|
|
4
|
+
|
|
5
|
+
function parseArgs(args) {
|
|
6
|
+
const out = {
|
|
7
|
+
codexThreadId: null,
|
|
8
|
+
codexHome: defaultCodexHome(),
|
|
9
|
+
json: false,
|
|
10
|
+
vscodeStorageRoots: null,
|
|
11
|
+
vscodeExtensionRoots: null,
|
|
12
|
+
vscodeSettingsRoots: null,
|
|
13
|
+
vscodeLogRoots: null,
|
|
14
|
+
maxStorageFiles: 5000,
|
|
15
|
+
maxStorageFileBytes: 2 * 1024 * 1024,
|
|
16
|
+
maxStorageMatches: 50,
|
|
17
|
+
maxExtensionFiles: 5000,
|
|
18
|
+
maxExtensionFileBytes: 2 * 1024 * 1024,
|
|
19
|
+
maxExtensionMatches: 100,
|
|
20
|
+
maxExtensionSourceSnippets: 40,
|
|
21
|
+
maxSettingsFiles: 100,
|
|
22
|
+
maxSettingsFileBytes: 256 * 1024,
|
|
23
|
+
maxSettingsMatches: 20,
|
|
24
|
+
maxLogFiles: 2000,
|
|
25
|
+
maxLogFileBytes: 2 * 1024 * 1024,
|
|
26
|
+
maxLogMatches: 50,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg === '--codex-thread-id') {
|
|
32
|
+
const value = args[++i];
|
|
33
|
+
if (!value || value.startsWith('-')) throw new Error('--codex-thread-id requires a thread id');
|
|
34
|
+
out.codexThreadId = value;
|
|
35
|
+
} else if (arg === '--codex-home') {
|
|
36
|
+
const value = args[++i];
|
|
37
|
+
if (!value || value.startsWith('-')) throw new Error('--codex-home requires a path');
|
|
38
|
+
out.codexHome = value;
|
|
39
|
+
} else if (arg === '--vscode-storage-root') {
|
|
40
|
+
const value = args[++i];
|
|
41
|
+
if (!value || value.startsWith('-')) throw new Error('--vscode-storage-root requires a path');
|
|
42
|
+
out.vscodeStorageRoots = [...(out.vscodeStorageRoots ?? []), value];
|
|
43
|
+
} else if (arg === '--vscode-extension-root') {
|
|
44
|
+
const value = args[++i];
|
|
45
|
+
if (!value || value.startsWith('-')) throw new Error('--vscode-extension-root requires a path');
|
|
46
|
+
out.vscodeExtensionRoots = [...(out.vscodeExtensionRoots ?? []), value];
|
|
47
|
+
} else if (arg === '--vscode-settings-root') {
|
|
48
|
+
const value = args[++i];
|
|
49
|
+
if (!value || value.startsWith('-')) throw new Error('--vscode-settings-root requires a path');
|
|
50
|
+
out.vscodeSettingsRoots = [...(out.vscodeSettingsRoots ?? []), value];
|
|
51
|
+
} else if (arg === '--vscode-log-root') {
|
|
52
|
+
const value = args[++i];
|
|
53
|
+
if (!value || value.startsWith('-')) throw new Error('--vscode-log-root requires a path');
|
|
54
|
+
out.vscodeLogRoots = [...(out.vscodeLogRoots ?? []), value];
|
|
55
|
+
} else if (arg === '--max-storage-files') {
|
|
56
|
+
const value = Number(args[++i]);
|
|
57
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
58
|
+
throw new Error('--max-storage-files must be a non-negative integer');
|
|
59
|
+
}
|
|
60
|
+
out.maxStorageFiles = value;
|
|
61
|
+
} else if (arg === '--max-storage-file-bytes') {
|
|
62
|
+
const value = Number(args[++i]);
|
|
63
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
64
|
+
throw new Error('--max-storage-file-bytes must be a positive integer');
|
|
65
|
+
}
|
|
66
|
+
out.maxStorageFileBytes = value;
|
|
67
|
+
} else if (arg === '--max-storage-matches') {
|
|
68
|
+
const value = Number(args[++i]);
|
|
69
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
70
|
+
throw new Error('--max-storage-matches must be a non-negative integer');
|
|
71
|
+
}
|
|
72
|
+
out.maxStorageMatches = value;
|
|
73
|
+
} else if (arg === '--max-extension-files') {
|
|
74
|
+
const value = Number(args[++i]);
|
|
75
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
76
|
+
throw new Error('--max-extension-files must be a non-negative integer');
|
|
77
|
+
}
|
|
78
|
+
out.maxExtensionFiles = value;
|
|
79
|
+
} else if (arg === '--max-extension-file-bytes') {
|
|
80
|
+
const value = Number(args[++i]);
|
|
81
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
82
|
+
throw new Error('--max-extension-file-bytes must be a positive integer');
|
|
83
|
+
}
|
|
84
|
+
out.maxExtensionFileBytes = value;
|
|
85
|
+
} else if (arg === '--max-extension-matches') {
|
|
86
|
+
const value = Number(args[++i]);
|
|
87
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
88
|
+
throw new Error('--max-extension-matches must be a non-negative integer');
|
|
89
|
+
}
|
|
90
|
+
out.maxExtensionMatches = value;
|
|
91
|
+
} else if (arg === '--max-extension-source-snippets') {
|
|
92
|
+
const value = Number(args[++i]);
|
|
93
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
94
|
+
throw new Error('--max-extension-source-snippets must be a non-negative integer');
|
|
95
|
+
}
|
|
96
|
+
out.maxExtensionSourceSnippets = value;
|
|
97
|
+
} else if (arg === '--max-settings-files') {
|
|
98
|
+
const value = Number(args[++i]);
|
|
99
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
100
|
+
throw new Error('--max-settings-files must be a non-negative integer');
|
|
101
|
+
}
|
|
102
|
+
out.maxSettingsFiles = value;
|
|
103
|
+
} else if (arg === '--max-settings-file-bytes') {
|
|
104
|
+
const value = Number(args[++i]);
|
|
105
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
106
|
+
throw new Error('--max-settings-file-bytes must be a positive integer');
|
|
107
|
+
}
|
|
108
|
+
out.maxSettingsFileBytes = value;
|
|
109
|
+
} else if (arg === '--max-settings-matches') {
|
|
110
|
+
const value = Number(args[++i]);
|
|
111
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
112
|
+
throw new Error('--max-settings-matches must be a non-negative integer');
|
|
113
|
+
}
|
|
114
|
+
out.maxSettingsMatches = value;
|
|
115
|
+
} else if (arg === '--max-log-files') {
|
|
116
|
+
const value = Number(args[++i]);
|
|
117
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
118
|
+
throw new Error('--max-log-files must be a non-negative integer');
|
|
119
|
+
}
|
|
120
|
+
out.maxLogFiles = value;
|
|
121
|
+
} else if (arg === '--max-log-file-bytes') {
|
|
122
|
+
const value = Number(args[++i]);
|
|
123
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
124
|
+
throw new Error('--max-log-file-bytes must be a positive integer');
|
|
125
|
+
}
|
|
126
|
+
out.maxLogFileBytes = value;
|
|
127
|
+
} else if (arg === '--max-log-matches') {
|
|
128
|
+
const value = Number(args[++i]);
|
|
129
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
130
|
+
throw new Error('--max-log-matches must be a non-negative integer');
|
|
131
|
+
}
|
|
132
|
+
out.maxLogMatches = value;
|
|
133
|
+
} else if (arg === '--json') {
|
|
134
|
+
out.json = true;
|
|
135
|
+
} else {
|
|
136
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderTextResult(result) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push('throughline codex restore source audit');
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(` status: ${result.status}`);
|
|
148
|
+
lines.push(` reason: ${result.reason}`);
|
|
149
|
+
lines.push(` thread: ${result.threadId ?? 'unknown'}`);
|
|
150
|
+
lines.push(` proof scope: ${result.proofScope ?? 'none'}`);
|
|
151
|
+
lines.push(` restart safe: ${result.restartSafe ? 'yes' : 'no'}`);
|
|
152
|
+
if (result.rollout) {
|
|
153
|
+
lines.push(` rollout: ${result.rollout.path}`);
|
|
154
|
+
lines.push(` restore safety: ${result.rollout.restoreSafety?.status ?? 'unknown'}`);
|
|
155
|
+
}
|
|
156
|
+
if (result.summary) {
|
|
157
|
+
lines.push(` session index has thread: ${result.summary.sessionIndexContainsThreadId ? 'yes' : 'no'}`);
|
|
158
|
+
lines.push(` Codex state thread rows: ${result.summary.codexStateThreadMatches}`);
|
|
159
|
+
lines.push(` Codex state conclusion: ${result.summary.codexStateConclusion}`);
|
|
160
|
+
lines.push(` VS Code storage searched: ${result.summary.vscodeStorageSearched ? 'yes' : 'no'}`);
|
|
161
|
+
lines.push(` VS Code storage matches: ${result.summary.vscodeStorageMatches}`);
|
|
162
|
+
lines.push(` VS Code storage sqlite DBs: ${result.summary.vscodeStorageSqliteDatabases ?? 0}`);
|
|
163
|
+
lines.push(
|
|
164
|
+
` VS Code storage sqlite matches: ${result.summary.vscodeStorageSqliteDatabaseMatches ?? 0}`,
|
|
165
|
+
);
|
|
166
|
+
lines.push(` VS Code extension searched: ${result.summary.vscodeExtensionSearched ? 'yes' : 'no'}`);
|
|
167
|
+
lines.push(` VS Code extension matches: ${result.summary.vscodeExtensionMatches}`);
|
|
168
|
+
lines.push(` VS Code extension source snippets: ${result.summary.vscodeExtensionSourceSnippetCount}`);
|
|
169
|
+
lines.push(` VS Code extension conclusion: ${result.summary.vscodeExtensionConclusion}`);
|
|
170
|
+
lines.push(
|
|
171
|
+
` VS Code reconnect resume via rollout path: ${
|
|
172
|
+
result.summary.vscodeExtensionSourceFacts?.reconnectResumeViaAppServerRolloutPath ? 'yes' : 'no'
|
|
173
|
+
}`,
|
|
174
|
+
);
|
|
175
|
+
lines.push(` VS Code settings searched: ${result.summary.vscodeSettingsSearched ? 'yes' : 'no'}`);
|
|
176
|
+
lines.push(
|
|
177
|
+
` VS Code follow-up queue default: ${
|
|
178
|
+
result.summary.vscodeExtensionFollowUpQueueModeDefault?.values?.join(', ') || 'unknown'
|
|
179
|
+
}`,
|
|
180
|
+
);
|
|
181
|
+
lines.push(
|
|
182
|
+
` VS Code follow-up queue setting: ${
|
|
183
|
+
result.summary.vscodeSettingsFollowUpQueueMode?.status ?? 'unknown'
|
|
184
|
+
}`,
|
|
185
|
+
);
|
|
186
|
+
lines.push(` VS Code logs searched: ${result.summary.vscodeLogSearched ? 'yes' : 'no'}`);
|
|
187
|
+
lines.push(` VS Code log matches: ${result.summary.vscodeLogMatches}`);
|
|
188
|
+
lines.push(` VS Code log thread id matches: ${result.summary.vscodeLogThreadIdMatches ?? 0}`);
|
|
189
|
+
lines.push(` VS Code log retained text matches: ${result.summary.vscodeLogRetainedTextMatches ?? 0}`);
|
|
190
|
+
lines.push(` VS Code log patch apply failures: ${result.summary.vscodeLogPatchApplyFailures ?? 0}`);
|
|
191
|
+
if (
|
|
192
|
+
result.summary.vscodeLogPatchApplyFailureFirstTimestamp ||
|
|
193
|
+
result.summary.vscodeLogPatchApplyFailureLastTimestamp
|
|
194
|
+
) {
|
|
195
|
+
lines.push(
|
|
196
|
+
` VS Code log patch apply failure window: ${
|
|
197
|
+
result.summary.vscodeLogPatchApplyFailureFirstTimestamp ?? 'unknown'
|
|
198
|
+
} -> ${result.summary.vscodeLogPatchApplyFailureLastTimestamp ?? 'unknown'}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
lines.push(
|
|
202
|
+
` VS Code log thread stream signals: ${
|
|
203
|
+
result.summary.vscodeLogThreadStreamStateSignals ?? 0
|
|
204
|
+
}`,
|
|
205
|
+
);
|
|
206
|
+
lines.push(
|
|
207
|
+
` VS Code thread stream patch path: ${
|
|
208
|
+
result.summary.vscodeThreadStreamPatchApplyPathPresent ? 'yes' : 'no'
|
|
209
|
+
}`,
|
|
210
|
+
);
|
|
211
|
+
lines.push(
|
|
212
|
+
` VS Code thread stream patch failure signal: ${
|
|
213
|
+
result.summary.vscodeThreadStreamPatchFailureSignal ? 'yes' : 'no'
|
|
214
|
+
}`,
|
|
215
|
+
);
|
|
216
|
+
lines.push(
|
|
217
|
+
` VS Code rollback projection candidate: ${
|
|
218
|
+
result.summary.vscodeRollbackNonResurrectionProjectionPathPresent ? 'yes' : 'no'
|
|
219
|
+
}`,
|
|
220
|
+
);
|
|
221
|
+
const projectionCandidates = result.summary.vscodeRollbackNonResurrectionProjectionCandidates ?? [];
|
|
222
|
+
if (projectionCandidates.length > 0) {
|
|
223
|
+
lines.push(` VS Code rollback projection candidates: ${projectionCandidates.join(', ')}`);
|
|
224
|
+
}
|
|
225
|
+
const signals = result.summary.vscodeExtensionRestorePathSignals;
|
|
226
|
+
if (signals) {
|
|
227
|
+
lines.push(
|
|
228
|
+
` VS Code extension app-server restore signals: ${
|
|
229
|
+
signals.hasAppServerRestoreSignals ? 'yes' : 'no'
|
|
230
|
+
}`,
|
|
231
|
+
);
|
|
232
|
+
lines.push(
|
|
233
|
+
` VS Code extension webview persistence signals: ${
|
|
234
|
+
signals.hasWebviewPersistenceSignals ? 'yes' : 'no'
|
|
235
|
+
}`,
|
|
236
|
+
);
|
|
237
|
+
lines.push(
|
|
238
|
+
` VS Code extension follow-up queue signals: ${
|
|
239
|
+
signals.hasFollowUpQueueSignals ? 'yes' : 'no'
|
|
240
|
+
}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return lines.join('\n');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function run(args) {
|
|
248
|
+
let parsed;
|
|
249
|
+
try {
|
|
250
|
+
parsed = parseArgs(args);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
253
|
+
process.stderr.write(`[codex-restore-source-audit] ${msg}\n`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
parsed = {
|
|
258
|
+
...parsed,
|
|
259
|
+
...resolveCodexThreadIdentity({ host: 'codex', codexThreadId: parsed.codexThreadId }, process.env),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (!parsed.codexThreadId) {
|
|
263
|
+
const result = {
|
|
264
|
+
status: 'refused',
|
|
265
|
+
reason: 'codex_thread_id_required',
|
|
266
|
+
proofScope: 'none',
|
|
267
|
+
restartSafe: false,
|
|
268
|
+
};
|
|
269
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
270
|
+
else process.stderr.write(`[codex-restore-source-audit] ${result.reason}\n`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = runCodexRestoreSourceAudit({
|
|
275
|
+
threadId: parsed.codexThreadId,
|
|
276
|
+
codexHome: parsed.codexHome,
|
|
277
|
+
projectPath: process.cwd(),
|
|
278
|
+
vscodeStorageRoots: parsed.vscodeStorageRoots ?? undefined,
|
|
279
|
+
vscodeExtensionRoots: parsed.vscodeExtensionRoots ?? undefined,
|
|
280
|
+
maxStorageFiles: parsed.maxStorageFiles,
|
|
281
|
+
maxStorageFileBytes: parsed.maxStorageFileBytes,
|
|
282
|
+
maxStorageMatches: parsed.maxStorageMatches,
|
|
283
|
+
maxExtensionFiles: parsed.maxExtensionFiles,
|
|
284
|
+
maxExtensionFileBytes: parsed.maxExtensionFileBytes,
|
|
285
|
+
maxExtensionMatches: parsed.maxExtensionMatches,
|
|
286
|
+
maxExtensionSourceSnippets: parsed.maxExtensionSourceSnippets,
|
|
287
|
+
vscodeSettingsRoots: parsed.vscodeSettingsRoots ?? undefined,
|
|
288
|
+
vscodeLogRoots: parsed.vscodeLogRoots ?? undefined,
|
|
289
|
+
maxSettingsFiles: parsed.maxSettingsFiles,
|
|
290
|
+
maxSettingsFileBytes: parsed.maxSettingsFileBytes,
|
|
291
|
+
maxSettingsMatches: parsed.maxSettingsMatches,
|
|
292
|
+
maxLogFiles: parsed.maxLogFiles,
|
|
293
|
+
maxLogFileBytes: parsed.maxLogFileBytes,
|
|
294
|
+
maxLogMatches: parsed.maxLogMatches,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
298
|
+
else process.stdout.write(renderTextResult(result) + '\n');
|
|
299
|
+
process.exit(result.status === 'restore-source-audit-complete' ? 0 : 1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const _internal = {
|
|
303
|
+
parseArgs,
|
|
304
|
+
};
|