throughline 0.4.1 → 0.4.2
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/CHANGELOG.md +10 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/trim-cli.test.mjs +21 -7
- package/src/trim-model.mjs +16 -2
- package/src/trim-model.test.mjs +69 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.2] — 2026-05-09
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Codex trim no longer falls back to the latest project session when `--session`
|
|
18
|
+
is omitted. For `--host codex`, the default memory session is now the current
|
|
19
|
+
Codex thread (`codex:<thread_id>` from `--codex-thread-id`,
|
|
20
|
+
`CODEX_THREAD_ID`, or `THROUGHLINE_CODEX_THREAD_ID`), so Claude-side work
|
|
21
|
+
cannot accidentally become the injected memory for a Codex rollback.
|
|
22
|
+
|
|
13
23
|
## [0.4.1] — 2026-05-09
|
|
14
24
|
|
|
15
25
|
### Changed
|
package/README.md
CHANGED
|
@@ -356,6 +356,12 @@ CODEX_THREAD_ID=<id> throughline trim --preflight --host codex
|
|
|
356
356
|
throughline trim --execute --host codex --all --codex-thread-id <id>
|
|
357
357
|
```
|
|
358
358
|
|
|
359
|
+
For Codex trim, the default memory session is the current Codex thread:
|
|
360
|
+
`codex:<CODEX_THREAD_ID>` / `codex:<THROUGHLINE_CODEX_THREAD_ID>`. Throughline
|
|
361
|
+
does not fall back to the latest project session for Codex injection, because
|
|
362
|
+
that can mix Claude-side memory into a Codex rollback. Pass `--session` only
|
|
363
|
+
when deliberately injecting a different captured session.
|
|
364
|
+
|
|
359
365
|
That current-work framing matters: the original `/tl` design learned that L1/L2
|
|
360
366
|
memory alone can read like past logs rather than "the work in progress". The
|
|
361
367
|
memo is one strong signal, but the broader mechanism is explicit structure:
|
package/package.json
CHANGED
package/src/trim-cli.test.mjs
CHANGED
|
@@ -191,12 +191,24 @@ async function seedDb(home, project) {
|
|
|
191
191
|
`INSERT INTO sessions (session_id, project_path, status, created_at, updated_at)
|
|
192
192
|
VALUES ('sess-trim-cli', ?, 'active', 1, 2)`,
|
|
193
193
|
).run(project);
|
|
194
|
-
for (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
for (const sessionId of [
|
|
195
|
+
'sess-trim-cli',
|
|
196
|
+
'codex:019dfabf-thread',
|
|
197
|
+
'codex:019dfaba-f87e-7f41-a144-d5ca7c6dd7f9',
|
|
198
|
+
]) {
|
|
199
|
+
if (sessionId !== 'sess-trim-cli') {
|
|
200
|
+
db.prepare(
|
|
201
|
+
`INSERT INTO sessions (session_id, project_path, status, created_at, updated_at)
|
|
202
|
+
VALUES (?, ?, 'active', 1, 1)`,
|
|
203
|
+
).run(sessionId, project);
|
|
204
|
+
}
|
|
205
|
+
for (let turn = 1; turn <= 22; turn++) {
|
|
206
|
+
db.prepare(
|
|
207
|
+
`INSERT INTO bodies
|
|
208
|
+
(session_id, origin_session_id, turn_number, role, text, token_count, created_at)
|
|
209
|
+
VALUES (?, ?, ?, 'assistant', ?, 1, ?)`,
|
|
210
|
+
).run(sessionId, sessionId, turn, `assistant body ${turn}`, turn * 1000);
|
|
211
|
+
}
|
|
200
212
|
}
|
|
201
213
|
db.close();
|
|
202
214
|
} finally {
|
|
@@ -284,6 +296,7 @@ test('trim CLI carries explicit Codex thread id in dry-run JSON', async () => {
|
|
|
284
296
|
explicit: true,
|
|
285
297
|
reason: 'explicit_codex_thread_id',
|
|
286
298
|
});
|
|
299
|
+
assert.equal(plan.session.id, 'codex:019dfabf-thread');
|
|
287
300
|
assert.equal(plan.trim.automaticExecutionAllowed, true);
|
|
288
301
|
} finally {
|
|
289
302
|
rmSync(project, { recursive: true, force: true });
|
|
@@ -361,7 +374,8 @@ test('trim CLI uses explicit Codex rollout source when DB has no captured turns'
|
|
|
361
374
|
|
|
362
375
|
assert.equal(result.status, 0, result.stderr);
|
|
363
376
|
const plan = JSON.parse(result.stdout);
|
|
364
|
-
assert.equal(plan.session.id, '
|
|
377
|
+
assert.equal(plan.session.id, 'codex:019dfaba-f87e-7f41-a144-d5ca7c6dd7f9');
|
|
378
|
+
assert.equal(plan.session.source, 'codex-rollout');
|
|
365
379
|
assert.equal(plan.trim.source, 'codex-rollout');
|
|
366
380
|
assert.equal(plan.trim.sourceReason, 'explicit_codex_thread_rollout');
|
|
367
381
|
assert.equal(plan.trim.capturedTurns, 22);
|
package/src/trim-model.mjs
CHANGED
|
@@ -46,6 +46,14 @@ export function findLatestSessionIdForProject(db, projectPath) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function resolveDefaultSessionId({ sessionId, host, codexThreadId, db, projectPath }) {
|
|
50
|
+
if (sessionId) return sessionId;
|
|
51
|
+
if (host === 'codex') {
|
|
52
|
+
return codexThreadId ? `codex:${codexThreadId}` : null;
|
|
53
|
+
}
|
|
54
|
+
return findLatestSessionIdForProject(db, projectPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
function countDistinctCapturedTurns(db, sessionId) {
|
|
50
58
|
try {
|
|
51
59
|
const row = db
|
|
@@ -244,7 +252,13 @@ export function buildTrimPlan(
|
|
|
244
252
|
) {
|
|
245
253
|
const normalizedHost = TRIM_HOSTS.includes(host) ? host : 'unknown';
|
|
246
254
|
const normalizedTrimSource = normalizeTrimSource(trimSource);
|
|
247
|
-
const resolvedSessionId =
|
|
255
|
+
const resolvedSessionId = resolveDefaultSessionId({
|
|
256
|
+
sessionId,
|
|
257
|
+
host: normalizedHost,
|
|
258
|
+
codexThreadId,
|
|
259
|
+
db,
|
|
260
|
+
projectPath,
|
|
261
|
+
});
|
|
248
262
|
if (!resolvedSessionId && !normalizedTrimSource) {
|
|
249
263
|
return {
|
|
250
264
|
status: 'unavailable',
|
|
@@ -538,7 +552,7 @@ function buildPlanSession({ resolvedSessionId, session, trimSource, projectPath
|
|
|
538
552
|
}
|
|
539
553
|
|
|
540
554
|
return {
|
|
541
|
-
id: trimSource?.threadId ??
|
|
555
|
+
id: resolvedSessionId ?? trimSource?.threadId ?? null,
|
|
542
556
|
projectPath: trimSource?.projectPath ?? projectPath ?? null,
|
|
543
557
|
status: 'external',
|
|
544
558
|
mergedInto: null,
|
package/src/trim-model.test.mjs
CHANGED
|
@@ -89,6 +89,26 @@ function seedSkeleton(db, { turn = 1, summary = 'old L1 summary' } = {}) {
|
|
|
89
89
|
).run(turn, summary, turn * 1000 + 500);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function seedSessionTurns(db, { sessionId, projectPath = '/repo', updatedAt = 2, textPrefix, count = 2 }) {
|
|
93
|
+
db.prepare(
|
|
94
|
+
`INSERT INTO sessions (session_id, project_path, status, created_at, updated_at)
|
|
95
|
+
VALUES (?, ?, 'active', 1, ?)`,
|
|
96
|
+
).run(sessionId, projectPath, updatedAt);
|
|
97
|
+
|
|
98
|
+
for (let turn = 1; turn <= count; turn++) {
|
|
99
|
+
db.prepare(
|
|
100
|
+
`INSERT INTO bodies
|
|
101
|
+
(session_id, origin_session_id, turn_number, role, text, token_count, created_at)
|
|
102
|
+
VALUES (?, ?, ?, 'user', ?, 1, ?)`,
|
|
103
|
+
).run(sessionId, sessionId, turn, `${textPrefix} user ${turn}`, turn * 1000);
|
|
104
|
+
db.prepare(
|
|
105
|
+
`INSERT INTO bodies
|
|
106
|
+
(session_id, origin_session_id, turn_number, role, text, token_count, created_at)
|
|
107
|
+
VALUES (?, ?, ?, 'assistant', ?, 1, ?)`,
|
|
108
|
+
).run(sessionId, sessionId, turn, `${textPrefix} assistant ${turn}`, turn * 1000 + 100);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
test('buildTrimPlan: default dry-run keeps recent 20 and marks Claude as manual-only', () => {
|
|
93
113
|
const db = makeDb();
|
|
94
114
|
seedTurns(db);
|
|
@@ -220,6 +240,54 @@ test('buildTrimPlan: env Codex thread id is marked non-explicit', () => {
|
|
|
220
240
|
});
|
|
221
241
|
});
|
|
222
242
|
|
|
243
|
+
test('buildTrimPlan: Codex defaults memory session to the current Codex thread, not latest project session', () => {
|
|
244
|
+
const db = makeDb();
|
|
245
|
+
const threadId = '019dfabf-thread';
|
|
246
|
+
seedSessionTurns(db, {
|
|
247
|
+
sessionId: 'claude-latest',
|
|
248
|
+
updatedAt: 999,
|
|
249
|
+
textPrefix: 'claude latest memory',
|
|
250
|
+
});
|
|
251
|
+
seedSessionTurns(db, {
|
|
252
|
+
sessionId: `codex:${threadId}`,
|
|
253
|
+
updatedAt: 100,
|
|
254
|
+
textPrefix: 'codex current memory',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const plan = buildTrimPlan(db, {
|
|
258
|
+
projectPath: '/repo',
|
|
259
|
+
host: 'codex',
|
|
260
|
+
codexThreadId: threadId,
|
|
261
|
+
trimAll: true,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
assert.equal(plan.status, 'ready');
|
|
265
|
+
assert.equal(plan.session.id, `codex:${threadId}`);
|
|
266
|
+
assert.equal(plan.session.source, 'throughline-db');
|
|
267
|
+
assert.equal(plan.trim.capturedTurns, 2);
|
|
268
|
+
assert.match(plan.memoryPreview.text, /codex current memory assistant 2/);
|
|
269
|
+
assert.doesNotMatch(plan.memoryPreview.text, /claude latest memory/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('buildTrimPlan: Codex without a thread id does not fall back to latest project session', () => {
|
|
273
|
+
const db = makeDb();
|
|
274
|
+
seedSessionTurns(db, {
|
|
275
|
+
sessionId: 'claude-latest',
|
|
276
|
+
updatedAt: 999,
|
|
277
|
+
textPrefix: 'claude latest memory',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const plan = buildTrimPlan(db, {
|
|
281
|
+
projectPath: '/repo',
|
|
282
|
+
host: 'codex',
|
|
283
|
+
trimAll: true,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
assert.equal(plan.status, 'unavailable');
|
|
287
|
+
assert.equal(plan.reason, 'no_session');
|
|
288
|
+
assert.equal(plan.session, null);
|
|
289
|
+
});
|
|
290
|
+
|
|
223
291
|
test('buildTrimPlan: current-work memo is placed in curated memory preview', () => {
|
|
224
292
|
const db = makeDb();
|
|
225
293
|
seedTurns(db, { count: 3 });
|
|
@@ -345,7 +413,7 @@ test('buildTrimPlan: external Codex rollout source can stand in when DB session
|
|
|
345
413
|
});
|
|
346
414
|
|
|
347
415
|
assert.equal(plan.status, 'ready');
|
|
348
|
-
assert.equal(plan.session.id, '019dfabf-thread');
|
|
416
|
+
assert.equal(plan.session.id, 'codex:019dfabf-thread');
|
|
349
417
|
assert.equal(plan.session.status, 'external');
|
|
350
418
|
assert.equal(plan.session.source, 'codex-rollout');
|
|
351
419
|
assert.equal(plan.trim.rollbackTurns, 2);
|