throughline 0.3.23 → 0.3.25
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-trim.md +42 -0
- package/.codex-sidecar.yml +62 -0
- package/CHANGELOG.md +583 -0
- package/README.ja.md +42 -5
- package/README.md +400 -23
- package/bin/throughline.mjs +168 -4
- package/codex/skills/throughline/SKILL.md +157 -0
- package/codex/skills/throughline/agents/openai.yaml +7 -0
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +146 -0
- package/docs/L1_L2_L3_REDESIGN.md +415 -0
- package/docs/PUBLIC_RELEASE_PLAN.md +184 -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/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 +227 -4
- package/src/cli/install.test.mjs +207 -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 +97 -0
- 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 +326 -0
- package/src/package-files.test.mjs +19 -0
- package/src/prompt-submit.mjs +9 -6
- package/src/resume-context.mjs +44 -140
- package/src/resume-context.test.mjs +172 -0
- package/src/session-start.mjs +8 -5
- 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 +94 -6
- package/src/vscode-task.test.mjs +186 -6
package/src/cli/trim.mjs
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { runCodexTrimExecution, runCodexTrimPreflight } from '../codex-app-server.mjs';
|
|
2
|
+
import {
|
|
3
|
+
buildCodexRolloutTrimSource,
|
|
4
|
+
parseCodexRolloutFile,
|
|
5
|
+
} from '../codex-rollout-memory.mjs';
|
|
6
|
+
import { resolveCodexThreadIdentity } from '../codex-thread-identity.mjs';
|
|
7
|
+
import { getDb } from '../db.mjs';
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_TRIM_KEEP_RECENT,
|
|
10
|
+
DEFAULT_TRIM_PREVIEW_MAX_CHARS,
|
|
11
|
+
buildTrimPlan,
|
|
12
|
+
renderTrimDryRunReport,
|
|
13
|
+
} from '../trim-model.mjs';
|
|
14
|
+
|
|
15
|
+
const DURABLE_ROLLOUT_READ_ATTEMPTS = 5;
|
|
16
|
+
const DURABLE_ROLLOUT_READ_DELAY_MS = 100;
|
|
17
|
+
|
|
18
|
+
async function readStdin() {
|
|
19
|
+
let raw = '';
|
|
20
|
+
await new Promise((resolve) => {
|
|
21
|
+
process.stdin.setEncoding('utf8');
|
|
22
|
+
process.stdin.on('data', (chunk) => {
|
|
23
|
+
raw += chunk;
|
|
24
|
+
});
|
|
25
|
+
process.stdin.on('end', resolve);
|
|
26
|
+
});
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseArgs(args) {
|
|
31
|
+
const out = {
|
|
32
|
+
dryRun: false,
|
|
33
|
+
json: false,
|
|
34
|
+
sessionId: null,
|
|
35
|
+
host: 'unknown',
|
|
36
|
+
keepRecent: DEFAULT_TRIM_KEEP_RECENT,
|
|
37
|
+
trimAll: false,
|
|
38
|
+
memoStdin: false,
|
|
39
|
+
codexThreadId: null,
|
|
40
|
+
previewMaxChars: DEFAULT_TRIM_PREVIEW_MAX_CHARS,
|
|
41
|
+
preflight: false,
|
|
42
|
+
execute: false,
|
|
43
|
+
codexAppServerBin: null,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
const arg = args[i];
|
|
48
|
+
if (arg === '--dry-run') {
|
|
49
|
+
out.dryRun = true;
|
|
50
|
+
} else if (arg === '--json') {
|
|
51
|
+
out.json = true;
|
|
52
|
+
} else if (arg === '--session') {
|
|
53
|
+
const value = args[++i];
|
|
54
|
+
if (!value || value.startsWith('-')) {
|
|
55
|
+
throw new Error('--session requires a session id');
|
|
56
|
+
}
|
|
57
|
+
out.sessionId = value;
|
|
58
|
+
} else if (arg === '--host') {
|
|
59
|
+
const value = args[++i];
|
|
60
|
+
if (!['claude', 'codex', 'unknown'].includes(value)) {
|
|
61
|
+
throw new Error('--host must be claude, codex, or unknown');
|
|
62
|
+
}
|
|
63
|
+
out.host = value;
|
|
64
|
+
} else if (arg === '--keep-recent') {
|
|
65
|
+
const value = args[++i];
|
|
66
|
+
const parsed = Number(value);
|
|
67
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
68
|
+
throw new Error('--keep-recent must be a non-negative integer');
|
|
69
|
+
}
|
|
70
|
+
out.keepRecent = parsed;
|
|
71
|
+
} else if (arg === '--all') {
|
|
72
|
+
out.trimAll = true;
|
|
73
|
+
} else if (arg === '--memo-stdin') {
|
|
74
|
+
out.memoStdin = true;
|
|
75
|
+
} else if (arg === '--preview-max-chars') {
|
|
76
|
+
const value = Number(args[++i]);
|
|
77
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
78
|
+
throw new Error('--preview-max-chars must be a positive integer');
|
|
79
|
+
}
|
|
80
|
+
out.previewMaxChars = value;
|
|
81
|
+
} else if (arg === '--codex-thread-id') {
|
|
82
|
+
const value = args[++i];
|
|
83
|
+
if (!value || value.startsWith('-')) {
|
|
84
|
+
throw new Error('--codex-thread-id requires a thread id');
|
|
85
|
+
}
|
|
86
|
+
out.codexThreadId = value;
|
|
87
|
+
} else if (arg === '--preflight') {
|
|
88
|
+
out.preflight = true;
|
|
89
|
+
} else if (arg === '--execute') {
|
|
90
|
+
out.execute = true;
|
|
91
|
+
} else if (arg === '--codex-app-server-bin') {
|
|
92
|
+
const value = args[++i];
|
|
93
|
+
if (!value || value.startsWith('-')) {
|
|
94
|
+
throw new Error('--codex-app-server-bin requires a command path');
|
|
95
|
+
}
|
|
96
|
+
out.codexAppServerBin = value;
|
|
97
|
+
} else if (!arg.startsWith('-') && !out.sessionId) {
|
|
98
|
+
out.sessionId = arg;
|
|
99
|
+
} else {
|
|
100
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function run(args) {
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = parseArgs(args);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
113
|
+
process.stderr.write(`[trim] ${msg}\n`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
parsed = {
|
|
118
|
+
...parsed,
|
|
119
|
+
...resolveCodexThreadIdentity(parsed, process.env),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const inflightMemo = parsed.memoStdin ? await readStdin() : null;
|
|
123
|
+
const db = getDb();
|
|
124
|
+
const trimSource =
|
|
125
|
+
parsed.host === 'codex' && parsed.codexThreadId
|
|
126
|
+
? buildCodexRolloutTrimSource({
|
|
127
|
+
threadId: parsed.codexThreadId,
|
|
128
|
+
projectPath: process.cwd(),
|
|
129
|
+
sourceReason:
|
|
130
|
+
parsed.codexThreadIdSource && parsed.codexThreadIdSource.startsWith('env:')
|
|
131
|
+
? 'env_codex_thread_rollout'
|
|
132
|
+
: 'explicit_codex_thread_rollout',
|
|
133
|
+
})
|
|
134
|
+
: null;
|
|
135
|
+
const plan = buildTrimPlan(db, {
|
|
136
|
+
sessionId: parsed.sessionId,
|
|
137
|
+
projectPath: process.cwd(),
|
|
138
|
+
host: parsed.host,
|
|
139
|
+
keepRecent: parsed.keepRecent,
|
|
140
|
+
trimAll: parsed.trimAll,
|
|
141
|
+
inflightMemo,
|
|
142
|
+
codexThreadId: parsed.codexThreadId,
|
|
143
|
+
codexThreadIdSource: parsed.codexThreadIdSource,
|
|
144
|
+
trimSource,
|
|
145
|
+
previewMaxChars: parsed.previewMaxChars,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!parsed.dryRun) {
|
|
149
|
+
if (parsed.preflight && parsed.execute) {
|
|
150
|
+
process.stderr.write('[trim] choose either --preflight or --execute, not both.\n');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!parsed.preflight && !parsed.execute) {
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
'[trim] automatic rollback/inject is not implemented yet. Re-run with --dry-run, --preflight, or guarded --execute.\n',
|
|
157
|
+
);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = parsed.preflight ? await runPreflight(parsed, plan) : await runExecute(parsed, plan);
|
|
162
|
+
if (parsed.json) {
|
|
163
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
164
|
+
} else {
|
|
165
|
+
process.stdout.write(renderTrimActionReport(result) + '\n');
|
|
166
|
+
}
|
|
167
|
+
process.exit(
|
|
168
|
+
result.status === 'preflight-ready' || result.status === 'execute-durable-verified'
|
|
169
|
+
? 0
|
|
170
|
+
: 1,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (parsed.json) {
|
|
175
|
+
process.stdout.write(JSON.stringify(plan, null, 2) + '\n');
|
|
176
|
+
} else {
|
|
177
|
+
process.stdout.write(renderTrimDryRunReport(plan) + '\n');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
process.exit(plan.status === 'unavailable' ? 1 : 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function runExecute(parsed, plan) {
|
|
184
|
+
const refusal = validateCodexAction(parsed, plan, 'execute');
|
|
185
|
+
if (refusal) return refusal;
|
|
186
|
+
|
|
187
|
+
if (!hasInjectableMemory(plan.memoryPreview)) {
|
|
188
|
+
return {
|
|
189
|
+
status: 'execute-refused',
|
|
190
|
+
reason: 'injectable_memory_required',
|
|
191
|
+
plan,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const command = parsed.codexAppServerBin ?? process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex';
|
|
196
|
+
const execution = await runCodexTrimExecution({
|
|
197
|
+
threadId: parsed.codexThreadId,
|
|
198
|
+
cwd: process.cwd(),
|
|
199
|
+
rollbackTurns: plan.trim.rollbackTurns,
|
|
200
|
+
memoryText: plan.memoryPreview.text,
|
|
201
|
+
expectedTurns: expectedCodexAppServerTurns(plan),
|
|
202
|
+
command,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (execution.status === 'refused') {
|
|
206
|
+
return {
|
|
207
|
+
status: 'execute-refused',
|
|
208
|
+
reason: execution.reason,
|
|
209
|
+
plan,
|
|
210
|
+
execution,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const classification = await classifyCodexExecutionResult({ plan, execution });
|
|
215
|
+
return {
|
|
216
|
+
status: classification.status,
|
|
217
|
+
reason: classification.reason,
|
|
218
|
+
durableVerification: classification.durableVerification,
|
|
219
|
+
plan: {
|
|
220
|
+
...plan,
|
|
221
|
+
mode: 'execute',
|
|
222
|
+
},
|
|
223
|
+
execution,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function classifyCodexExecutionResult({ plan, execution }) {
|
|
228
|
+
const visibilityStatus = execution.postInjectVisibilityCheck?.status ?? 'unchecked';
|
|
229
|
+
const restoreSafetyStatus = plan.trim?.restoreSafety?.status ?? 'unknown';
|
|
230
|
+
const initialRolloutStats = plan.trim?.rolloutStats ?? {};
|
|
231
|
+
const durableVerification = {
|
|
232
|
+
liveMutationSent: Boolean(execution.rollbackSent && execution.injectSent),
|
|
233
|
+
durableVerified: false,
|
|
234
|
+
postInjectVisibilityStatus: visibilityStatus,
|
|
235
|
+
restoreSafetyStatus,
|
|
236
|
+
rolloutPath: plan.trim?.rolloutPath ?? null,
|
|
237
|
+
rolloutChecked: false,
|
|
238
|
+
postExecuteRestoreSafetyStatus: null,
|
|
239
|
+
observedNewRollbackEvent: false,
|
|
240
|
+
observedInjectedMemory: false,
|
|
241
|
+
reasons: [],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (visibilityStatus !== 'match') {
|
|
245
|
+
durableVerification.reasons.push(
|
|
246
|
+
execution.postInjectVisibilityCheck?.reason ?? 'post_inject_visibility_unverified',
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!durableVerification.rolloutPath) {
|
|
251
|
+
durableVerification.reasons.push('rollout_path_unavailable_for_durable_verification');
|
|
252
|
+
if (visibilityStatus !== 'match') {
|
|
253
|
+
return {
|
|
254
|
+
status: 'execute-unverified',
|
|
255
|
+
reason: execution.postInjectVisibilityCheck?.reason ?? 'post_inject_visibility_unverified',
|
|
256
|
+
durableVerification,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
status: 'execute-sent-live-only',
|
|
261
|
+
reason: 'rollback_and_inject_sent_live_only',
|
|
262
|
+
durableVerification,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const evidence = await waitForDurableRolloutEvidence({
|
|
267
|
+
durableVerification,
|
|
268
|
+
initialRolloutStats,
|
|
269
|
+
attempts: DURABLE_ROLLOUT_READ_ATTEMPTS,
|
|
270
|
+
delayMs: DURABLE_ROLLOUT_READ_DELAY_MS,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (evidence.error) {
|
|
274
|
+
durableVerification.reasons.push('rollout_durable_verification_failed');
|
|
275
|
+
durableVerification.error = evidence.error;
|
|
276
|
+
return {
|
|
277
|
+
status: 'execute-unverified',
|
|
278
|
+
reason: 'rollout_durable_verification_failed',
|
|
279
|
+
durableVerification,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (visibilityStatus !== 'match') {
|
|
284
|
+
return {
|
|
285
|
+
status: 'execute-unverified',
|
|
286
|
+
reason: execution.postInjectVisibilityCheck?.reason ?? 'post_inject_visibility_unverified',
|
|
287
|
+
durableVerification,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!durableVerification.observedNewRollbackEvent) {
|
|
292
|
+
durableVerification.reasons.push('rollback_marker_not_observed_in_rollout');
|
|
293
|
+
return {
|
|
294
|
+
status: 'execute-unverified',
|
|
295
|
+
reason: 'rollback_marker_not_observed_in_rollout',
|
|
296
|
+
durableVerification,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!durableVerification.observedInjectedMemory) {
|
|
301
|
+
durableVerification.reasons.push('injected_memory_not_observed_in_rollout');
|
|
302
|
+
return {
|
|
303
|
+
status: 'execute-unverified',
|
|
304
|
+
reason: 'injected_memory_not_observed_in_rollout',
|
|
305
|
+
durableVerification,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
durableVerification.durableVerified = true;
|
|
310
|
+
durableVerification.reasons.push('rollout_durable_evidence_verified');
|
|
311
|
+
durableVerification.restoreSafetyStatus = durableVerification.postExecuteRestoreSafetyStatus;
|
|
312
|
+
return {
|
|
313
|
+
status: 'execute-durable-verified',
|
|
314
|
+
reason: 'rollback_and_inject_durable_verified',
|
|
315
|
+
durableVerification,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function waitForDurableRolloutEvidence({
|
|
320
|
+
durableVerification,
|
|
321
|
+
initialRolloutStats,
|
|
322
|
+
attempts,
|
|
323
|
+
delayMs,
|
|
324
|
+
}) {
|
|
325
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
326
|
+
if (attempt > 1 && delayMs > 0) {
|
|
327
|
+
await sleep(delayMs);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let parsedAfter;
|
|
331
|
+
try {
|
|
332
|
+
parsedAfter = parseCodexRolloutFile(durableVerification.rolloutPath);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
durableVerification.rolloutChecked = true;
|
|
335
|
+
return {
|
|
336
|
+
error: err instanceof Error ? err.message : String(err),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
durableVerification.rolloutChecked = true;
|
|
341
|
+
durableVerification.postExecuteRestoreSafetyStatus = parsedAfter.restoreSafety?.status ?? 'unknown';
|
|
342
|
+
durableVerification.observedNewRollbackEvent =
|
|
343
|
+
parsedAfter.stats.rollbackEvents > (initialRolloutStats.rollbackEvents ?? 0);
|
|
344
|
+
durableVerification.observedInjectedMemory =
|
|
345
|
+
parsedAfter.stats.injectedDeveloperMessages > (initialRolloutStats.injectedDeveloperMessages ?? 0);
|
|
346
|
+
|
|
347
|
+
if (
|
|
348
|
+
(durableVerification.observedNewRollbackEvent && durableVerification.observedInjectedMemory)
|
|
349
|
+
) {
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function runPreflight(parsed, plan) {
|
|
358
|
+
const refusal = validateCodexAction(parsed, plan, 'preflight');
|
|
359
|
+
if (refusal) return refusal;
|
|
360
|
+
|
|
361
|
+
const command = parsed.codexAppServerBin ?? process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex';
|
|
362
|
+
const preflight = await runCodexTrimPreflight({
|
|
363
|
+
threadId: parsed.codexThreadId,
|
|
364
|
+
cwd: process.cwd(),
|
|
365
|
+
rollbackTurns: plan.trim.rollbackTurns,
|
|
366
|
+
expectedTurns: expectedCodexAppServerTurns(plan),
|
|
367
|
+
command,
|
|
368
|
+
});
|
|
369
|
+
const turnCountStatus = preflight.turnCountCheck?.status;
|
|
370
|
+
if (turnCountStatus === 'mismatch' || turnCountStatus === 'unknown') {
|
|
371
|
+
return {
|
|
372
|
+
status: 'preflight-refused',
|
|
373
|
+
reason: preflight.turnCountCheck.reason,
|
|
374
|
+
plan,
|
|
375
|
+
preflight,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
status: 'preflight-ready',
|
|
381
|
+
reason: 'rollback_not_sent',
|
|
382
|
+
plan,
|
|
383
|
+
preflight,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validateCodexAction(parsed, plan, action) {
|
|
388
|
+
if (parsed.host !== 'codex') {
|
|
389
|
+
return {
|
|
390
|
+
status: `${action}-refused`,
|
|
391
|
+
reason: `${action}_requires_codex_host`,
|
|
392
|
+
plan,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!parsed.codexThreadId) {
|
|
397
|
+
return {
|
|
398
|
+
status: `${action}-refused`,
|
|
399
|
+
reason: 'codex_thread_id_required',
|
|
400
|
+
plan,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (plan.status === 'unavailable') {
|
|
405
|
+
return {
|
|
406
|
+
status: `${action}-refused`,
|
|
407
|
+
reason: plan.reason,
|
|
408
|
+
plan,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (plan.trim.rollbackTurns < 1) {
|
|
413
|
+
return {
|
|
414
|
+
status: `${action}-noop`,
|
|
415
|
+
reason: 'nothing_to_trim',
|
|
416
|
+
plan,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function sleep(ms) {
|
|
424
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function hasInjectableMemory(memoryPreview) {
|
|
428
|
+
const text = memoryPreview?.text;
|
|
429
|
+
return (
|
|
430
|
+
memoryPreview?.stats?.source === 'throughline-db' &&
|
|
431
|
+
typeof text === 'string' &&
|
|
432
|
+
text.trim().length > 0 &&
|
|
433
|
+
text !== '(no captured memory available)'
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function expectedCodexAppServerTurns(plan) {
|
|
438
|
+
return plan?.trim?.source === 'codex-rollout' ? plan.trim.capturedTurns : null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function renderTrimActionReport(result) {
|
|
442
|
+
const lines = [];
|
|
443
|
+
lines.push(result.status.startsWith('execute-') ? '## Throughline Trim Execute' : '## Throughline Trim Preflight');
|
|
444
|
+
lines.push('');
|
|
445
|
+
lines.push(`Status: ${result.status}`);
|
|
446
|
+
if (result.reason) lines.push(`Reason: ${result.reason}`);
|
|
447
|
+
if (result.requiredEnv) lines.push(`Required env: ${result.requiredEnv}`);
|
|
448
|
+
if (result.durableVerification) {
|
|
449
|
+
lines.push(`Live mutation sent: ${result.durableVerification.liveMutationSent ? 'yes' : 'no'}`);
|
|
450
|
+
lines.push(`Durable verified: ${result.durableVerification.durableVerified ? 'yes' : 'no'}`);
|
|
451
|
+
lines.push(`Post-inject visibility: ${result.durableVerification.postInjectVisibilityStatus}`);
|
|
452
|
+
lines.push(`Restore safety: ${result.durableVerification.restoreSafetyStatus}`);
|
|
453
|
+
if (result.durableVerification.rolloutPath) {
|
|
454
|
+
lines.push(`Durable rollout: ${result.durableVerification.rolloutPath}`);
|
|
455
|
+
lines.push(`Rollout checked: ${result.durableVerification.rolloutChecked ? 'yes' : 'no'}`);
|
|
456
|
+
lines.push(`New rollback marker observed: ${result.durableVerification.observedNewRollbackEvent ? 'yes' : 'no'}`);
|
|
457
|
+
lines.push(`Injected memory observed in rollout: ${result.durableVerification.observedInjectedMemory ? 'yes' : 'no'}`);
|
|
458
|
+
}
|
|
459
|
+
for (const reason of result.durableVerification.reasons ?? []) {
|
|
460
|
+
lines.push(`Durable verification reason: ${reason}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (result.preflight) {
|
|
465
|
+
lines.push('');
|
|
466
|
+
lines.push(`Codex thread: ${result.preflight.threadId}`);
|
|
467
|
+
lines.push(`Read turns: ${result.preflight.readTurns ?? 'unknown'}`);
|
|
468
|
+
lines.push(`Resumed turns: ${result.preflight.resumedTurns ?? 'unknown'}`);
|
|
469
|
+
if (result.preflight.turnCountCheck) {
|
|
470
|
+
lines.push(`Turn count check: ${result.preflight.turnCountCheck.status}`);
|
|
471
|
+
lines.push(`Expected turns: ${result.preflight.turnCountCheck.expectedTurns ?? 'unchecked'}`);
|
|
472
|
+
}
|
|
473
|
+
lines.push(`Rollback sent: ${result.preflight.rollbackSent ? 'yes' : 'no'}`);
|
|
474
|
+
lines.push(`Inject sent: ${result.preflight.injectSent ? 'yes' : 'no'}`);
|
|
475
|
+
lines.push(...renderTrimMemoryContractLines(result.plan?.memoryPreview?.stats, { planned: true }));
|
|
476
|
+
lines.push(...renderRestoreSafetyLines(result.plan?.trim?.restoreSafety));
|
|
477
|
+
lines.push(...renderPlannedRollbackRestoreSafetyLines(result.plan?.trim?.plannedRollbackRestoreSafety));
|
|
478
|
+
lines.push(`Rollback candidate turns: ${result.plan.trim.rollbackTurns}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (result.execution) {
|
|
482
|
+
lines.push('');
|
|
483
|
+
lines.push(`Codex thread: ${result.execution.threadId}`);
|
|
484
|
+
lines.push(`Read turns: ${result.execution.readTurns ?? 'unknown'}`);
|
|
485
|
+
lines.push(`Resumed turns: ${result.execution.resumedTurns ?? 'unknown'}`);
|
|
486
|
+
if (result.execution.turnCountCheck) {
|
|
487
|
+
lines.push(`Turn count check: ${result.execution.turnCountCheck.status}`);
|
|
488
|
+
lines.push(`Expected turns: ${result.execution.turnCountCheck.expectedTurns ?? 'unchecked'}`);
|
|
489
|
+
}
|
|
490
|
+
lines.push(`Rollback sent: ${result.execution.rollbackSent ? 'yes' : 'no'}`);
|
|
491
|
+
lines.push(`Inject sent: ${result.execution.injectSent ? 'yes' : 'no'}`);
|
|
492
|
+
lines.push(`Injected items: ${result.execution.injectedItems}`);
|
|
493
|
+
lines.push(...renderTrimMemoryContractLines(result.plan?.memoryPreview?.stats, { planned: false }));
|
|
494
|
+
lines.push(...renderRestoreSafetyLines(result.plan?.trim?.restoreSafety));
|
|
495
|
+
lines.push(...renderPlannedRollbackRestoreSafetyLines(result.plan?.trim?.plannedRollbackRestoreSafety));
|
|
496
|
+
lines.push(`Rollback candidate turns: ${result.plan.trim.rollbackTurns}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (result.hostPrimitiveAudit) {
|
|
500
|
+
const hasNonResurrectionPrimitive =
|
|
501
|
+
result.hostPrimitiveAudit.facts?.hasCurrentThreadNonResurrectionPrimitive ??
|
|
502
|
+
result.hostPrimitiveAudit.facts?.hasCurrentThreadRemediationPrimitive;
|
|
503
|
+
lines.push('');
|
|
504
|
+
lines.push(`Host primitive audit: ${result.hostPrimitiveAudit.status}`);
|
|
505
|
+
lines.push(`Host primitive audit reason: ${result.hostPrimitiveAudit.reason}`);
|
|
506
|
+
lines.push(
|
|
507
|
+
`Current-thread non-resurrection primitive: ${
|
|
508
|
+
hasNonResurrectionPrimitive ? 'yes' : 'no'
|
|
509
|
+
}`,
|
|
510
|
+
);
|
|
511
|
+
if (result.hostPrimitiveAudit.repairContract) {
|
|
512
|
+
lines.push(`Same-thread repair contract: ${result.hostPrimitiveAudit.repairContract.status}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return lines.join('\n');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function renderRestoreSafetyLines(restoreSafety) {
|
|
520
|
+
if (!restoreSafety) return [];
|
|
521
|
+
const lines = [];
|
|
522
|
+
lines.push(`Restore safety: ${restoreSafety.status}`);
|
|
523
|
+
lines.push(`Compacted rows: ${restoreSafety.compactedRows}`);
|
|
524
|
+
lines.push(
|
|
525
|
+
`Rollback text retained in compacted history: ${restoreSafety.rollbackTextRetainedInCompacted}`,
|
|
526
|
+
);
|
|
527
|
+
lines.push(`Resurrected user messages after rollback: ${restoreSafety.resurrectedUserMessages}`);
|
|
528
|
+
for (const risk of restoreSafety.risks ?? []) {
|
|
529
|
+
lines.push(`Restore safety risk: ${risk.type} (${risk.count})`);
|
|
530
|
+
}
|
|
531
|
+
return lines;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function renderPlannedRollbackRestoreSafetyLines(plannedSafety) {
|
|
535
|
+
if (!plannedSafety) return [];
|
|
536
|
+
const lines = [];
|
|
537
|
+
lines.push(`Planned rollback restore safety: ${plannedSafety.status}`);
|
|
538
|
+
lines.push(
|
|
539
|
+
`Planned rollback text retained in compacted history: ${plannedSafety.rollbackTextRetainedInCompacted}`,
|
|
540
|
+
);
|
|
541
|
+
for (const risk of plannedSafety.risks ?? []) {
|
|
542
|
+
lines.push(`Planned rollback restore safety risk: ${risk.type} (${risk.count})`);
|
|
543
|
+
}
|
|
544
|
+
return lines;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function renderTrimMemoryContractLines(stats, { planned }) {
|
|
548
|
+
if (!stats) return [];
|
|
549
|
+
|
|
550
|
+
const lines = [];
|
|
551
|
+
const sourceLabel = planned ? 'Planned memory source' : 'Injected memory source';
|
|
552
|
+
lines.push(`${sourceLabel}: ${stats.source ?? 'unknown'}`);
|
|
553
|
+
|
|
554
|
+
if (stats.source !== 'throughline-db') return lines;
|
|
555
|
+
|
|
556
|
+
const recentBodies =
|
|
557
|
+
typeof stats.recentBodies === 'number'
|
|
558
|
+
? `${stats.recentBodies} rows (latest ${stats.recentTurnLimit ?? DEFAULT_TRIM_KEEP_RECENT} turns)`
|
|
559
|
+
: 'unknown';
|
|
560
|
+
lines.push('Memory contract: older L1 + latest 20 L2 full bodies + L3 references only');
|
|
561
|
+
lines.push(`Recent L2 bodies: ${recentBodies}`);
|
|
562
|
+
lines.push(`L3 bodies injected: no (references only: ${stats.l3References ?? 0})`);
|
|
563
|
+
return lines;
|
|
564
|
+
}
|