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
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { inspectCodexPlannedRollbackRestoreSafety } from './codex-rollout-memory.mjs';
|
|
2
|
+
import { buildHandoffRecord, N_RECENT_L2 } from './handoff-record.mjs';
|
|
3
|
+
import { estimateTokens } from './token-estimator.mjs';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_TRIM_KEEP_RECENT = N_RECENT_L2;
|
|
6
|
+
export const DEFAULT_TRIM_PREVIEW_MAX_CHARS = 1_500;
|
|
7
|
+
export const TRIM_HOSTS = Object.freeze(['claude', 'codex', 'unknown']);
|
|
8
|
+
|
|
9
|
+
function assertKeepRecent(value) {
|
|
10
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
11
|
+
throw new Error('keepRecent must be a non-negative integer');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadSession(db, sessionId) {
|
|
16
|
+
try {
|
|
17
|
+
return (
|
|
18
|
+
db
|
|
19
|
+
.prepare(
|
|
20
|
+
`SELECT session_id, project_path, status, created_at, updated_at, merged_into
|
|
21
|
+
FROM sessions
|
|
22
|
+
WHERE session_id = ?`,
|
|
23
|
+
)
|
|
24
|
+
.get(sessionId) ?? null
|
|
25
|
+
);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function findLatestSessionIdForProject(db, projectPath) {
|
|
32
|
+
if (!projectPath) return null;
|
|
33
|
+
try {
|
|
34
|
+
const row = db
|
|
35
|
+
.prepare(
|
|
36
|
+
`SELECT session_id
|
|
37
|
+
FROM sessions
|
|
38
|
+
WHERE lower(project_path) = lower(?)
|
|
39
|
+
ORDER BY updated_at DESC
|
|
40
|
+
LIMIT 1`,
|
|
41
|
+
)
|
|
42
|
+
.get(projectPath);
|
|
43
|
+
return row?.session_id ?? null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function countDistinctCapturedTurns(db, sessionId) {
|
|
50
|
+
try {
|
|
51
|
+
const row = db
|
|
52
|
+
.prepare(
|
|
53
|
+
`SELECT COUNT(*) AS count
|
|
54
|
+
FROM (
|
|
55
|
+
SELECT origin_session_id, turn_number
|
|
56
|
+
FROM bodies
|
|
57
|
+
WHERE session_id = ?
|
|
58
|
+
GROUP BY origin_session_id, turn_number
|
|
59
|
+
UNION
|
|
60
|
+
SELECT origin_session_id, turn_number
|
|
61
|
+
FROM skeletons
|
|
62
|
+
WHERE session_id = ?
|
|
63
|
+
GROUP BY origin_session_id, turn_number
|
|
64
|
+
)`,
|
|
65
|
+
)
|
|
66
|
+
.get(sessionId, sessionId);
|
|
67
|
+
return row?.count ?? 0;
|
|
68
|
+
} catch {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function describeTrimHost(host) {
|
|
74
|
+
if (host === 'claude') {
|
|
75
|
+
return {
|
|
76
|
+
host,
|
|
77
|
+
automaticRollback: false,
|
|
78
|
+
automaticInject: false,
|
|
79
|
+
status: 'manual-only',
|
|
80
|
+
reason: 'claude_rewind_conversation_only_not_automated',
|
|
81
|
+
manualProcedure: [
|
|
82
|
+
'Run this dry-run first and review the rollback / injection plan.',
|
|
83
|
+
'If acceptable, use Claude Code /rewind conversation only manually.',
|
|
84
|
+
'Paste or otherwise provide the curated memory preview back to Claude after rewind.',
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (host === 'codex') {
|
|
90
|
+
return {
|
|
91
|
+
host,
|
|
92
|
+
automaticRollback: true,
|
|
93
|
+
automaticInject: true,
|
|
94
|
+
status: 'ready',
|
|
95
|
+
reason: 'codex_rollback_inject_available',
|
|
96
|
+
manualProcedure: [
|
|
97
|
+
'Run this dry-run first and review the rollback / injection plan.',
|
|
98
|
+
'Use --preflight for a read/resume guard; it does not send rollback or inject.',
|
|
99
|
+
'Use --execute for rollback + Throughline DB memory injection into the current Codex thread.',
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
host: 'unknown',
|
|
106
|
+
automaticRollback: false,
|
|
107
|
+
automaticInject: false,
|
|
108
|
+
status: 'unresolved',
|
|
109
|
+
reason: 'host_unknown',
|
|
110
|
+
manualProcedure: [
|
|
111
|
+
'Pass --host claude or --host codex to get host-specific trim guidance.',
|
|
112
|
+
'Do not run automatic rollback from an unknown host.',
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildSafeContinuation({ host, hostIdentity }) {
|
|
118
|
+
if (host !== 'codex') return null;
|
|
119
|
+
|
|
120
|
+
const threadId = hostIdentity?.codexThreadId ?? '<thread-id>';
|
|
121
|
+
const sessionId = threadId === '<thread-id>' ? 'codex:<thread-id>' : `codex:${threadId}`;
|
|
122
|
+
return {
|
|
123
|
+
status: 'fresh-thread-handoff-available',
|
|
124
|
+
reason: 'optional_fresh_thread_continuation',
|
|
125
|
+
safetyScope: 'fresh_thread_handoff_no_current_thread_mutation',
|
|
126
|
+
mutatesCurrentThread: false,
|
|
127
|
+
memoryCommand: `throughline codex-resume --session ${sessionId} --format handoff`,
|
|
128
|
+
smokeCommand: `throughline codex-handoff-smoke --session ${sessionId}`,
|
|
129
|
+
modelSmokeDryRunCommand: `throughline codex-handoff-model-smoke --session ${sessionId} --dry-run --json`,
|
|
130
|
+
guidedCommand: `throughline codex-handoff-start --session ${sessionId}`,
|
|
131
|
+
procedure: [
|
|
132
|
+
'Use the guided command for the full read-only fresh-thread start plan, or run the individual smoke / render commands below.',
|
|
133
|
+
'Validate the fresh-thread handoff prompt with the smoke command.',
|
|
134
|
+
'Optionally dry-run the model smoke command to inspect the exact Codex exec boundary without starting a model turn.',
|
|
135
|
+
'Render the fresh-thread handoff with the memory command.',
|
|
136
|
+
'Start a new Codex thread with that handoff context only when fresh-thread continuation is explicitly desired.',
|
|
137
|
+
'Use trim --execute --host codex for current-thread rollback / inject when the guarded execute inputs are present.',
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function collectMemoryPreview(record) {
|
|
143
|
+
if (!record) {
|
|
144
|
+
return {
|
|
145
|
+
text: '(no captured memory available)',
|
|
146
|
+
truncated: false,
|
|
147
|
+
stats: {
|
|
148
|
+
source: 'throughline-db',
|
|
149
|
+
l1Summaries: 0,
|
|
150
|
+
recentBodies: 0,
|
|
151
|
+
latestThinking: 0,
|
|
152
|
+
l3References: 0,
|
|
153
|
+
recentTurnLimit: N_RECENT_L2,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const lines = [];
|
|
159
|
+
lines.push('## Throughline: Active Work Context');
|
|
160
|
+
lines.push('');
|
|
161
|
+
lines.push(`Intent: ${record.intent}`);
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push('### Reading Contract');
|
|
164
|
+
lines.push(
|
|
165
|
+
'This preview is current-task context for continuation, not a passive archive. ' +
|
|
166
|
+
'Entries are oldest-to-newest; later entries may supersede earlier hypotheses.',
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (record.memory.inflightMemo) {
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push('### In-flight Memo');
|
|
172
|
+
lines.push(record.memory.inflightMemo);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (record.memory.latestThinking.length > 0) {
|
|
176
|
+
lines.push('');
|
|
177
|
+
lines.push('### Latest Thinking');
|
|
178
|
+
for (const row of record.memory.latestThinking.slice(-2)) {
|
|
179
|
+
lines.push(`[${row.time}] ${row.text}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (record.memory.l1Summaries.length > 0) {
|
|
184
|
+
lines.push('');
|
|
185
|
+
lines.push('### L1 Summaries');
|
|
186
|
+
for (const row of record.memory.l1Summaries) {
|
|
187
|
+
lines.push(`[${row.time}] ${row.summary.replace(/\n+/g, ' ').trim()}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (record.memory.recentBodies.length > 0) {
|
|
192
|
+
lines.push('');
|
|
193
|
+
lines.push('### Active Work Thread (Recent L2)');
|
|
194
|
+
lines.push('Entries are oldest-to-newest; later entries may supersede earlier hypotheses.');
|
|
195
|
+
for (const row of record.memory.recentBodies) {
|
|
196
|
+
lines.push(`[${row.time}] [${row.role}] ${row.text.replace(/\n+/g, ' ').trim()}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (record.references.l3.length > 0) {
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push('### L3 Detail References (Bodies Not Injected)');
|
|
203
|
+
for (const ref of record.references.l3) {
|
|
204
|
+
lines.push(`- ${ref.kind}: ${ref.detailCommand}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
lines.push('');
|
|
209
|
+
lines.push('### Continuation Instruction');
|
|
210
|
+
lines.push(
|
|
211
|
+
'Use the latest L2 entries, in-flight memo, and latest thinking to infer the next action. ' +
|
|
212
|
+
'Do not treat every older line as still-current truth.',
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const fullText = lines.join('\n');
|
|
216
|
+
return {
|
|
217
|
+
text: fullText,
|
|
218
|
+
truncated: false,
|
|
219
|
+
stats: {
|
|
220
|
+
source: 'throughline-db',
|
|
221
|
+
l1Summaries: record.memory.l1Summaries.length,
|
|
222
|
+
recentBodies: record.memory.recentBodies.length,
|
|
223
|
+
latestThinking: record.memory.latestThinking.length,
|
|
224
|
+
l3References: record.references.l3.length,
|
|
225
|
+
recentTurnLimit: N_RECENT_L2,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function buildTrimPlan(
|
|
231
|
+
db,
|
|
232
|
+
{
|
|
233
|
+
sessionId = null,
|
|
234
|
+
projectPath = null,
|
|
235
|
+
host = 'unknown',
|
|
236
|
+
keepRecent = DEFAULT_TRIM_KEEP_RECENT,
|
|
237
|
+
trimAll = false,
|
|
238
|
+
inflightMemo = null,
|
|
239
|
+
codexThreadId = null,
|
|
240
|
+
codexThreadIdSource = null,
|
|
241
|
+
trimSource = null,
|
|
242
|
+
previewMaxChars = DEFAULT_TRIM_PREVIEW_MAX_CHARS,
|
|
243
|
+
} = {},
|
|
244
|
+
) {
|
|
245
|
+
const normalizedHost = TRIM_HOSTS.includes(host) ? host : 'unknown';
|
|
246
|
+
const normalizedTrimSource = normalizeTrimSource(trimSource);
|
|
247
|
+
const resolvedSessionId = sessionId ?? findLatestSessionIdForProject(db, projectPath);
|
|
248
|
+
if (!resolvedSessionId && !normalizedTrimSource) {
|
|
249
|
+
return {
|
|
250
|
+
status: 'unavailable',
|
|
251
|
+
reason: 'no_session',
|
|
252
|
+
session: null,
|
|
253
|
+
host: describeTrimHost(normalizedHost),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const session = resolvedSessionId ? loadSession(db, resolvedSessionId) : null;
|
|
258
|
+
if (resolvedSessionId && !session && !normalizedTrimSource) {
|
|
259
|
+
return {
|
|
260
|
+
status: 'unavailable',
|
|
261
|
+
reason: 'session_not_found',
|
|
262
|
+
session: { id: resolvedSessionId },
|
|
263
|
+
host: describeTrimHost(normalizedHost),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const effectiveKeepRecent = trimAll ? 0 : keepRecent;
|
|
268
|
+
assertKeepRecent(effectiveKeepRecent);
|
|
269
|
+
|
|
270
|
+
const capturedTurns =
|
|
271
|
+
normalizedTrimSource?.capturedTurns ?? countDistinctCapturedTurns(db, resolvedSessionId);
|
|
272
|
+
const rollbackTurns = Math.max(0, capturedTurns - effectiveKeepRecent);
|
|
273
|
+
const keepTurns = capturedTurns - rollbackTurns;
|
|
274
|
+
const record = resolvedSessionId
|
|
275
|
+
? buildHandoffRecord(db, {
|
|
276
|
+
sessionId: resolvedSessionId,
|
|
277
|
+
isInheritance: false,
|
|
278
|
+
inflightMemo,
|
|
279
|
+
recentTurnLimit: DEFAULT_TRIM_KEEP_RECENT,
|
|
280
|
+
})
|
|
281
|
+
: null;
|
|
282
|
+
const memoryPreview = record ? collectMemoryPreview(record) : normalizedTrimSource?.memoryPreview ?? collectMemoryPreview(null);
|
|
283
|
+
const contextReductionEstimate = estimateContextReduction({
|
|
284
|
+
trimSource: normalizedTrimSource,
|
|
285
|
+
rollbackTurns,
|
|
286
|
+
memoryPreviewText: memoryPreview.text,
|
|
287
|
+
});
|
|
288
|
+
const plannedRollbackRestoreSafety = buildPlannedRollbackRestoreSafety({
|
|
289
|
+
trimSource: normalizedTrimSource,
|
|
290
|
+
rollbackTurns,
|
|
291
|
+
});
|
|
292
|
+
const hostInfo = describeTrimHost(normalizedHost);
|
|
293
|
+
const hostIdentity = buildHostIdentity({
|
|
294
|
+
host: normalizedHost,
|
|
295
|
+
codexThreadId,
|
|
296
|
+
codexThreadIdSource,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
status: rollbackTurns === 0 ? 'noop' : hostInfo.status,
|
|
301
|
+
reason: rollbackTurns === 0 ? 'nothing_to_trim' : hostInfo.reason,
|
|
302
|
+
mode: 'dry-run',
|
|
303
|
+
session: buildPlanSession({
|
|
304
|
+
resolvedSessionId,
|
|
305
|
+
session,
|
|
306
|
+
trimSource: normalizedTrimSource,
|
|
307
|
+
projectPath,
|
|
308
|
+
}),
|
|
309
|
+
host: hostInfo,
|
|
310
|
+
hostIdentity,
|
|
311
|
+
safeContinuation: buildSafeContinuation({ host: normalizedHost, hostIdentity }),
|
|
312
|
+
display: {
|
|
313
|
+
previewMaxChars,
|
|
314
|
+
},
|
|
315
|
+
trim: {
|
|
316
|
+
source: normalizedTrimSource?.source ?? 'throughline-db',
|
|
317
|
+
sourceReason: normalizedTrimSource?.sourceReason ?? 'throughline_db_session',
|
|
318
|
+
rolloutPath: normalizedTrimSource?.rolloutPath ?? null,
|
|
319
|
+
capturedTurns,
|
|
320
|
+
keepRecent: effectiveKeepRecent,
|
|
321
|
+
keepTurns,
|
|
322
|
+
rollbackTurns,
|
|
323
|
+
trimAll: Boolean(trimAll),
|
|
324
|
+
automaticExecutionAllowed:
|
|
325
|
+
rollbackTurns > 0 && hostInfo.automaticRollback && hostInfo.automaticInject,
|
|
326
|
+
contextReductionEstimate,
|
|
327
|
+
restoreSafety: normalizedTrimSource?.restoreSafety ?? null,
|
|
328
|
+
plannedRollbackRestoreSafety,
|
|
329
|
+
rolloutStats: normalizedTrimSource?.stats ?? null,
|
|
330
|
+
},
|
|
331
|
+
memoryPreview,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function renderTrimDryRunReport(plan) {
|
|
336
|
+
const lines = [];
|
|
337
|
+
lines.push('## Throughline Trim Dry-run');
|
|
338
|
+
lines.push('');
|
|
339
|
+
lines.push(`Status: ${plan.status}`);
|
|
340
|
+
if (plan.reason) lines.push(`Reason: ${plan.reason}`);
|
|
341
|
+
|
|
342
|
+
if (!plan.session) {
|
|
343
|
+
lines.push('');
|
|
344
|
+
lines.push('No session was found for this project. Pass --session <id> explicitly.');
|
|
345
|
+
return lines.join('\n');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
lines.push('');
|
|
349
|
+
lines.push(`Session: ${plan.session.id}`);
|
|
350
|
+
lines.push(`Project: ${plan.session.projectPath}`);
|
|
351
|
+
lines.push(`Host: ${plan.host.host}`);
|
|
352
|
+
if (plan.hostIdentity?.codexThreadId) {
|
|
353
|
+
lines.push(`Codex thread: ${plan.hostIdentity.codexThreadId}`);
|
|
354
|
+
}
|
|
355
|
+
if (plan.trim.source) {
|
|
356
|
+
lines.push(`Trim source: ${plan.trim.source}`);
|
|
357
|
+
}
|
|
358
|
+
lines.push(`Captured turns: ${plan.trim.capturedTurns}`);
|
|
359
|
+
lines.push(`Keep recent turns: ${plan.trim.keepRecent}`);
|
|
360
|
+
lines.push(`Rollback candidate turns: ${plan.trim.rollbackTurns}`);
|
|
361
|
+
if (plan.trim.contextReductionEstimate) {
|
|
362
|
+
const estimate = plan.trim.contextReductionEstimate;
|
|
363
|
+
lines.push(`Estimated rollback tokens: ${estimate.rollbackEstimatedTokens}`);
|
|
364
|
+
lines.push(`Estimated injected memory tokens: ${estimate.injectedMemoryEstimatedTokens}`);
|
|
365
|
+
lines.push(
|
|
366
|
+
`Estimated net token reduction: ${estimate.netEstimatedTokens} (${estimate.reductionPct}%, ${estimate.method})`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (plan.trim.restoreSafety) {
|
|
370
|
+
lines.push(...renderRestoreSafetyLines(plan.trim.restoreSafety));
|
|
371
|
+
}
|
|
372
|
+
if (plan.trim.plannedRollbackRestoreSafety) {
|
|
373
|
+
lines.push(...renderPlannedRollbackRestoreSafetyLines(plan.trim.plannedRollbackRestoreSafety));
|
|
374
|
+
}
|
|
375
|
+
lines.push(`Automatic execution allowed: ${plan.trim.automaticExecutionAllowed ? 'yes' : 'no'}`);
|
|
376
|
+
|
|
377
|
+
lines.push('');
|
|
378
|
+
lines.push('### Host Boundary');
|
|
379
|
+
lines.push(`- automatic rollback: ${plan.host.automaticRollback ? 'yes' : 'no'}`);
|
|
380
|
+
lines.push(`- automatic inject: ${plan.host.automaticInject ? 'yes' : 'no'}`);
|
|
381
|
+
lines.push(`- boundary status: ${plan.host.status}`);
|
|
382
|
+
lines.push(`- boundary reason: ${plan.host.reason}`);
|
|
383
|
+
|
|
384
|
+
if (plan.safeContinuation) {
|
|
385
|
+
lines.push('');
|
|
386
|
+
lines.push('### Safe Continuation Path');
|
|
387
|
+
lines.push(`- status: ${plan.safeContinuation.status}`);
|
|
388
|
+
lines.push(`- reason: ${plan.safeContinuation.reason}`);
|
|
389
|
+
if (plan.safeContinuation.safetyScope) {
|
|
390
|
+
lines.push(`- safety scope: ${plan.safeContinuation.safetyScope}`);
|
|
391
|
+
}
|
|
392
|
+
lines.push(`- mutates current thread: ${plan.safeContinuation.mutatesCurrentThread ? 'yes' : 'no'}`);
|
|
393
|
+
if (plan.safeContinuation.guidedCommand) {
|
|
394
|
+
lines.push(`- guided command: ${plan.safeContinuation.guidedCommand}`);
|
|
395
|
+
}
|
|
396
|
+
if (plan.safeContinuation.smokeCommand) {
|
|
397
|
+
lines.push(`- smoke command: ${plan.safeContinuation.smokeCommand}`);
|
|
398
|
+
}
|
|
399
|
+
if (plan.safeContinuation.modelSmokeDryRunCommand) {
|
|
400
|
+
lines.push(`- model smoke dry-run: ${plan.safeContinuation.modelSmokeDryRunCommand}`);
|
|
401
|
+
}
|
|
402
|
+
lines.push(`- memory command: ${plan.safeContinuation.memoryCommand}`);
|
|
403
|
+
for (const step of plan.safeContinuation.procedure) {
|
|
404
|
+
lines.push(`- ${step}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
lines.push('');
|
|
409
|
+
lines.push('### Manual Procedure');
|
|
410
|
+
for (const step of plan.host.manualProcedure) {
|
|
411
|
+
lines.push(`- ${step}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
lines.push('');
|
|
415
|
+
lines.push('### Curated Memory Preview');
|
|
416
|
+
const renderedPreview = renderMemoryPreviewForReport({
|
|
417
|
+
text: plan.memoryPreview.text,
|
|
418
|
+
maxChars: plan.display?.previewMaxChars,
|
|
419
|
+
});
|
|
420
|
+
lines.push(renderedPreview.text);
|
|
421
|
+
if (renderedPreview.truncated) {
|
|
422
|
+
lines.push('');
|
|
423
|
+
lines.push(
|
|
424
|
+
`[preview truncated to ${renderedPreview.maxChars} chars; full memory remains available in JSON memoryPreview.text]`,
|
|
425
|
+
);
|
|
426
|
+
if (plan.safeContinuation?.guidedCommand) {
|
|
427
|
+
lines.push(`[fresh-thread Codex guided start: ${plan.safeContinuation.guidedCommand}]`);
|
|
428
|
+
}
|
|
429
|
+
if (plan.safeContinuation?.memoryCommand) {
|
|
430
|
+
lines.push(`[fresh-thread Codex handoff: ${plan.safeContinuation.memoryCommand}]`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return lines.join('\n');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderMemoryPreviewForReport({ text, maxChars }) {
|
|
438
|
+
const normalizedMaxChars = Number.isInteger(maxChars) && maxChars > 0 ? maxChars : null;
|
|
439
|
+
if (!normalizedMaxChars || text.length <= normalizedMaxChars) {
|
|
440
|
+
return { text, truncated: false, maxChars: normalizedMaxChars };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
text: `${text.slice(0, normalizedMaxChars).trimEnd()}\n...`,
|
|
445
|
+
truncated: true,
|
|
446
|
+
maxChars: normalizedMaxChars,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function renderRestoreSafetyLines(restoreSafety) {
|
|
451
|
+
const lines = [];
|
|
452
|
+
lines.push(`Restore safety: ${restoreSafety.status}`);
|
|
453
|
+
lines.push(`Compacted rows: ${restoreSafety.compactedRows}`);
|
|
454
|
+
lines.push(`Compacted replacement user messages: ${restoreSafety.compactedReplacementUserMessages}`);
|
|
455
|
+
lines.push(
|
|
456
|
+
`Rollback text retained in compacted history: ${restoreSafety.rollbackTextRetainedInCompacted}`,
|
|
457
|
+
);
|
|
458
|
+
lines.push(`Resurrected user messages after rollback: ${restoreSafety.resurrectedUserMessages}`);
|
|
459
|
+
for (const risk of restoreSafety.risks ?? []) {
|
|
460
|
+
lines.push(`Restore safety risk: ${risk.type} (${risk.count})`);
|
|
461
|
+
}
|
|
462
|
+
return lines;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function renderPlannedRollbackRestoreSafetyLines(plannedSafety) {
|
|
466
|
+
const lines = [];
|
|
467
|
+
lines.push(`Planned rollback restore safety: ${plannedSafety.status}`);
|
|
468
|
+
lines.push(
|
|
469
|
+
`Planned rollback text retained in compacted history: ${plannedSafety.rollbackTextRetainedInCompacted}`,
|
|
470
|
+
);
|
|
471
|
+
for (const risk of plannedSafety.risks ?? []) {
|
|
472
|
+
lines.push(`Planned rollback restore safety risk: ${risk.type} (${risk.count})`);
|
|
473
|
+
}
|
|
474
|
+
return lines;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizeTrimSource(trimSource) {
|
|
478
|
+
if (!trimSource) return null;
|
|
479
|
+
if (!Number.isInteger(trimSource.capturedTurns) || trimSource.capturedTurns < 0) {
|
|
480
|
+
throw new Error('trimSource.capturedTurns must be a non-negative integer');
|
|
481
|
+
}
|
|
482
|
+
if (!trimSource.memoryPreview || typeof trimSource.memoryPreview.text !== 'string') {
|
|
483
|
+
throw new Error('trimSource.memoryPreview.text is required');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
...trimSource,
|
|
488
|
+
source: trimSource.source ?? 'external',
|
|
489
|
+
sourceReason: trimSource.sourceReason ?? 'external_trim_source',
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function estimateContextReduction({ trimSource, rollbackTurns, memoryPreviewText }) {
|
|
494
|
+
const turnEstimates = trimSource?.contextEstimate?.turns;
|
|
495
|
+
if (!Array.isArray(turnEstimates)) return null;
|
|
496
|
+
|
|
497
|
+
const rollbackRows = rollbackTurns > 0 ? turnEstimates.slice(-rollbackTurns) : [];
|
|
498
|
+
const rollbackEstimatedTokens = rollbackRows.reduce(
|
|
499
|
+
(sum, row) => sum + (Number.isFinite(row.estimatedTokens) ? row.estimatedTokens : 0),
|
|
500
|
+
0,
|
|
501
|
+
);
|
|
502
|
+
const injectedMemoryEstimatedTokens = rollbackTurns > 0 ? estimateTokens(memoryPreviewText) : 0;
|
|
503
|
+
const netEstimatedTokens = Math.max(0, rollbackEstimatedTokens - injectedMemoryEstimatedTokens);
|
|
504
|
+
const reductionPct =
|
|
505
|
+
rollbackEstimatedTokens > 0 ? Math.max(0, Math.round((netEstimatedTokens / rollbackEstimatedTokens) * 100)) : 0;
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
method: trimSource.contextEstimate.method ?? 'chars_div_4',
|
|
509
|
+
scope: 'rollback_candidate_vs_injected_memory',
|
|
510
|
+
rollbackTurns,
|
|
511
|
+
rollbackEstimatedTokens,
|
|
512
|
+
injectedMemoryEstimatedTokens,
|
|
513
|
+
netEstimatedTokens,
|
|
514
|
+
reductionPct,
|
|
515
|
+
note: 'Heuristic estimate from rollout text length; not a host tokenizer measurement.',
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function buildPlannedRollbackRestoreSafety({ trimSource, rollbackTurns }) {
|
|
520
|
+
if (trimSource?.source !== 'codex-rollout') return null;
|
|
521
|
+
if (!trimSource.rolloutPath) return null;
|
|
522
|
+
if (!Number.isInteger(rollbackTurns) || rollbackTurns < 1) return null;
|
|
523
|
+
return inspectCodexPlannedRollbackRestoreSafety({
|
|
524
|
+
rolloutPath: trimSource.rolloutPath,
|
|
525
|
+
rollbackTurns,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function buildPlanSession({ resolvedSessionId, session, trimSource, projectPath }) {
|
|
530
|
+
if (session) {
|
|
531
|
+
return {
|
|
532
|
+
id: resolvedSessionId,
|
|
533
|
+
projectPath: session.project_path,
|
|
534
|
+
status: session.status,
|
|
535
|
+
mergedInto: session.merged_into ?? null,
|
|
536
|
+
source: 'throughline-db',
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
id: trimSource?.threadId ?? resolvedSessionId ?? null,
|
|
542
|
+
projectPath: trimSource?.projectPath ?? projectPath ?? null,
|
|
543
|
+
status: 'external',
|
|
544
|
+
mergedInto: null,
|
|
545
|
+
source: trimSource?.source ?? 'external',
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function buildHostIdentity({ host, codexThreadId, codexThreadIdSource = null }) {
|
|
550
|
+
if (host !== 'codex') {
|
|
551
|
+
return {
|
|
552
|
+
host,
|
|
553
|
+
codexThreadId: null,
|
|
554
|
+
explicit: false,
|
|
555
|
+
reason: 'not_codex_host',
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (typeof codexThreadId === 'string' && codexThreadId.length > 0) {
|
|
560
|
+
if (typeof codexThreadIdSource === 'string' && codexThreadIdSource.startsWith('env:')) {
|
|
561
|
+
return {
|
|
562
|
+
host,
|
|
563
|
+
codexThreadId,
|
|
564
|
+
explicit: false,
|
|
565
|
+
reason: 'env_codex_thread_id',
|
|
566
|
+
source: codexThreadIdSource,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
host,
|
|
572
|
+
codexThreadId,
|
|
573
|
+
explicit: true,
|
|
574
|
+
reason: 'explicit_codex_thread_id',
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
host,
|
|
580
|
+
codexThreadId: null,
|
|
581
|
+
explicit: false,
|
|
582
|
+
reason: 'codex_thread_id_not_provided',
|
|
583
|
+
};
|
|
584
|
+
}
|