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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
6
6
  "keywords": [
@@ -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 (let turn = 1; turn <= 22; turn++) {
195
- db.prepare(
196
- `INSERT INTO bodies
197
- (session_id, origin_session_id, turn_number, role, text, token_count, created_at)
198
- VALUES ('sess-trim-cli', 'sess-trim-cli', ?, 'assistant', ?, 1, ?)`,
199
- ).run(turn, `assistant body ${turn}`, turn * 1000);
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, 'sess-empty-codex');
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);
@@ -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 = sessionId ?? findLatestSessionIdForProject(db, projectPath);
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 ?? resolvedSessionId ?? null,
555
+ id: resolvedSessionId ?? trimSource?.threadId ?? null,
542
556
  projectPath: trimSource?.projectPath ?? projectPath ?? null,
543
557
  status: 'external',
544
558
  mergedInto: null,
@@ -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);