throughline 0.16.14 → 0.16.15

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,28 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.16.15] — 2026-10-10
14
+
15
+ ### 追加
16
+
17
+ - `throughline auto-handoff successor --thread <Codexの会話のID> --json`。あるCodexのタスクの「今の続き」を返す、読むだけの入口。ほかの製品が、旧タスクあての連絡を今の続きへ渡す時に使う。`auto-handoff status --json`は新しい20件だけを返すので、古い引き継ぎの続きを引けなかった。
18
+ - 返す物: `handed_off`(残っている後継があるか)、`current_thread_id`(引き継ぎを先までたどった、残っている一番先の後継。無ければ渡したID自身)、`chain`(そこまでの引き継ぎ)、`pending`(今の続きのタスクで、後継へ指示を送る前の引き継ぎ。`in_flight`がtrueならworkerが動いている最中、falseなら止まっていて`error_code`と`resume_state`が入る)。
19
+ - 消された後継とアーカイブ済みの後継は、無い物として扱う(旧タスクへ入力が来た時に開く後継と同じ決め方)。
20
+ - 記録が無い端末では、引き継ぎが1つも無い時と同じ答えを返す。記録は読み取り専用で開く。
21
+
22
+ ### 対応の範囲
23
+
24
+ | | macOS | Linux | Windows |
25
+ |---|---|---|---|
26
+ | Codex(Desktop) | 追加した。実機の記録で確認(引き継ぎ済みのタスク2つから今の続きが返り、引き継ぎの無いタスクは自分自身が返った) | 追加した。実機の記録で確認(後継がアーカイブ済みの系列は、旧タスク自身が返った) | 追加した。実機の記録で確認(16本続いた系列の最初と途中から、同じ今の続きが返った。止まったタスクは`pending`に理由が出た) |
27
+ | Claude Code | 対象外。Claude Codeの自動継続の記録は別の形で、`auto-handoff status --host claude --json`が全件を返す | 同じ | 同じ |
28
+ | Grok | 対象外。自動継続の機能が無い | 同じ | 同じ |
29
+ | Cursor | 対象外。自動継続の機能が無い | 同じ | 同じ |
30
+
31
+ ### 変わらない事
32
+
33
+ - 引き継ぎの動き。`auto-handoff status --json`の出力。
34
+
13
35
  ## [0.16.14] — 2026-10-10
14
36
 
15
37
  ### 修正
@@ -2195,7 +2217,8 @@ two attempts, instrument first instead of patching again.
2195
2217
 
2196
2218
  ---
2197
2219
 
2198
- [Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.14...HEAD
2220
+ [Unreleased]: https://github.com/kitepon/Throughline/compare/v0.16.15...HEAD
2221
+ [0.16.15]: https://github.com/kitepon/Throughline/compare/v0.16.14...v0.16.15
2199
2222
  [0.16.14]: https://github.com/kitepon/Throughline/compare/v0.16.13...v0.16.14
2200
2223
  [0.16.13]: https://github.com/kitepon/Throughline/compare/v0.16.12...v0.16.13
2201
2224
  [0.16.12]: https://github.com/kitepon/Throughline/compare/v0.16.11...v0.16.12
package/README.ja.md CHANGED
@@ -40,6 +40,14 @@ throughline auto-handoff status --operation <handoff-id> --json
40
40
  throughline auto-handoff resume --operation <handoff-id> --json
41
41
  ```
42
42
 
43
+ あるタスクの今の続きは、次の命令で引けます(読むだけ。ほかの道具が、旧タスクあての連絡を今の続きへ渡す時に使います)。
44
+
45
+ ```bash
46
+ throughline auto-handoff successor --thread <codex-thread-id> --json
47
+ ```
48
+
49
+ `current_thread_id`が今の続きです(後継が無ければ渡したID自身)。`pending.in_flight`がtrueの間は、引き継ぎの途中です。
50
+
43
51
  原因を解消した後の`resume`は同じ引き継ぎと既知の後継を再利用します。配送後の結果不明は実際の入力・開始を観測した場合だけ回復し、作成結果が不明な後継は再作成しません。従来の手動`$throughline`とcurrent-thread実験の設定は独立しています。
44
52
 
45
53
  [実測と検証範囲](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-03-codex-auto-continuation.md)、[Windowsの実測](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-08-windows-codex-auto-handoff.md)、[Linuxの実測](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-08-linux-codex-auto-handoff.md)を参照してください。
package/README.md CHANGED
@@ -39,6 +39,14 @@ throughline auto-handoff status --operation <handoff-id> --json
39
39
  throughline auto-handoff resume --operation <handoff-id> --json
40
40
  ```
41
41
 
42
+ あるタスクの今の続きは、次の命令で引けます(読むだけ。ほかの道具が、旧タスクあての連絡を今の続きへ渡す時に使います)。
43
+
44
+ ```bash
45
+ throughline auto-handoff successor --thread <codex-thread-id> --json
46
+ ```
47
+
48
+ `current_thread_id`が今の続きです(後継が無ければ渡したID自身)。`pending.in_flight`がtrueの間は、引き継ぎの途中です。
49
+
42
50
  原因を解消した後の`resume`は同じ引き継ぎと既知の後継を再利用します。配送後の結果不明は実際の入力・開始を観測した場合だけ回復し、作成結果が不明な後継は再作成しません。従来の手動`$throughline`とcurrent-thread実験の設定は独立しています。
43
51
 
44
52
  [実測と検証範囲](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-03-codex-auto-continuation.md)、[Windowsの実測](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-08-windows-codex-auto-handoff.md)、[Linuxの実測](https://github.com/kitepon/Throughline/blob/main/evidence/2026-10-08-linux-codex-auto-handoff.md)を参照してください。
@@ -412,6 +412,8 @@ Usage:
412
412
  引き継ぎの状態と固定理由を確認
413
413
  throughline auto-handoff resume --operation <id> --json
414
414
  同じ引き継ぎを再開(結果不明は再送しない)
415
+ throughline auto-handoff successor --thread <codex-thread-id> --json
416
+ あるCodexのタスクの今の続きを返す(読むだけ)
415
417
  throughline auto-handoff mark-sources [--dry-run] [--json]
416
418
  今までの引き継ぎの名前を付け直す(旧タスクへ印、
417
419
  名前の無い後継へ名前)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.16.14",
3
+ "version": "0.16.15",
4
4
  "type": "module",
5
5
  "description": "Persistent memory hooks for Claude Code, Codex, Grok, and Cursor (/clear-safe context compression)",
6
6
  "keywords": [
@@ -5,7 +5,7 @@ import { getDb, openReadOnlyDb, DB_PATH, CURRENT_VERSION } from '../db.mjs';
5
5
  import { listAutoHandoffs, getAutoHandoff } from '../codex-auto-handoff-store.mjs';
6
6
  import { renderFrozenDetail } from '../codex-auto-handoff-memory.mjs';
7
7
  import { readAutoHandoffConfig, writeAutoHandoffConfig } from '../codex-auto-handoff-config.mjs';
8
- import { autoHandoffDeliveryProfile, runAutoHandoffWorker, markAutoHandoffSources } from '../codex-auto-handoff.mjs';
8
+ import { autoHandoffDeliveryProfile, runAutoHandoffWorker, markAutoHandoffSources, resolveAutoHandoffSuccessor } from '../codex-auto-handoff.mjs';
9
9
  import { installCodexAutoHandoffHook, installClaudeAutoHandoffHooks, removeClaudeAutoHandoffHooks,
10
10
  claudeAutoHandoffHooksRegistered } from './install.mjs';
11
11
  import { sameProjectPath } from '../project-path.mjs';
@@ -17,17 +17,17 @@ import { listClaudeAutoHandoffs, publicClaudeAutoHandoff, runClaudeAutoHandoffWo
17
17
  const CLAUDE_ACTIONS = ['enable', 'disable', 'status', 'worker', 'desktop-open'];
18
18
 
19
19
  export function parseAutoHandoffArgs(args) {
20
- const out = { action: args[0] ?? 'status', host: 'codex', operation: null, project: null, origin: null, turn: null, database: null, json: false, dryRun: false };
21
- if (!['enable', 'disable', 'status', 'resume', 'detail', 'worker', 'desktop-open', 'mark-sources'].includes(out.action)) throw Error('auto_handoff_action_invalid');
20
+ const out = { action: args[0] ?? 'status', host: 'codex', operation: null, project: null, origin: null, turn: null, database: null, thread: null, json: false, dryRun: false };
21
+ if (!['enable', 'disable', 'status', 'resume', 'detail', 'worker', 'desktop-open', 'mark-sources', 'successor'].includes(out.action)) throw Error('auto_handoff_action_invalid');
22
22
  const allowed = ['host', ...{ enable: ['project'], disable: [], status: ['project', 'operation'],
23
- resume: ['operation'], worker: ['operation'], 'desktop-open': ['operation'], 'mark-sources': [], detail: ['operation', 'origin', 'turn', 'database'] }[out.action]];
23
+ resume: ['operation'], worker: ['operation'], 'desktop-open': ['operation'], 'mark-sources': [], successor: ['thread'], detail: ['operation', 'origin', 'turn', 'database'] }[out.action]];
24
24
  const seen = new Set();
25
25
  for (let i = 1; i < args.length; i++) {
26
26
  if (seen.has(args[i])) throw Error('auto_handoff_argument_invalid');
27
27
  seen.add(args[i]);
28
28
  if (args[i] === '--json') { out.json = true; continue; }
29
29
  if (args[i] === '--dry-run' && out.action === 'mark-sources') { out.dryRun = true; continue; }
30
- const key = { '--host': 'host', '--operation': 'operation', '--project': 'project', '--origin': 'origin', '--turn': 'turn', '--database': 'database' }[args[i]];
30
+ const key = { '--host': 'host', '--operation': 'operation', '--project': 'project', '--origin': 'origin', '--turn': 'turn', '--database': 'database', '--thread': 'thread' }[args[i]];
31
31
  if (!key || !allowed.includes(key) || !args[i + 1] || args[i + 1].startsWith('--')) throw Error('auto_handoff_argument_invalid');
32
32
  out[key] = args[++i];
33
33
  }
@@ -36,6 +36,7 @@ export function parseAutoHandoffArgs(args) {
36
36
  if (out.project && !isAbsolute(out.project)) out.project = resolve(out.project);
37
37
  if (out.project && out.operation) throw Error('auto_handoff_argument_invalid');
38
38
  if (['worker', 'resume', 'detail', 'desktop-open'].includes(out.action) && !out.operation) throw Error('auto_handoff_operation_required');
39
+ if (out.action === 'successor' && !out.thread) throw Error('auto_handoff_thread_required');
39
40
  if (out.action === 'detail') {
40
41
  out.turn = Number(out.turn);
41
42
  if (!out.origin || !Number.isSafeInteger(out.turn) || out.turn < 1) throw Error('auto_handoff_detail_identity_required');
@@ -128,6 +129,14 @@ export async function run(args = []) {
128
129
  finally { db.close(); }
129
130
  process.stdout.write(parsed.json ? JSON.stringify({ status: 'ok', text }) + '\n' : text + '\n');
130
131
  return 0;
132
+ } else if (parsed.action === 'successor') {
133
+ // あるタスクの今の続きを返す。読むだけ。DB がまだ無い端末では、引き継ぎが1つも無いのと同じ答えにする。
134
+ const db = existsSync(DB_PATH) ? openReadOnlyDb() : null;
135
+ try {
136
+ if (db && db.prepare('PRAGMA user_version').get().user_version !== CURRENT_VERSION) throw Error('auto_handoff_schema_mismatch');
137
+ result = { schema: 'throughline.codex_auto_handoff_successor.v1', ...(db ? resolveAutoHandoffSuccessor(db, parsed.thread)
138
+ : { thread_id: parsed.thread, handed_off: false, current_thread_id: parsed.thread, chain: [], pending: null }) };
139
+ } finally { db?.close(); }
131
140
  } else if (parsed.action === 'mark-sources') {
132
141
  // 0.16.9 までの版が引き継いだ分の名前を付け直す(名前の無い後継へ名前、旧タスクへ引き継ぎ済みの印)。何度流しても同じ結果になる。
133
142
  const db = getDb();
@@ -301,6 +301,39 @@ export async function nameAutoHandoffTarget(request, targetId, name) {
301
301
  }
302
302
  }
303
303
 
304
+ /**
305
+ * あるタスクの「今の続き」を引く(ほかの製品が、旧タスクあての連絡を今の続きへ渡すための入口)。読むだけ。
306
+ * 引き継ぎを先までたどり、残っている一番先の後継を返す。消された後継は無い物として扱う。
307
+ * @returns {{
308
+ * thread_id: string, handed_off: boolean, current_thread_id: string,
309
+ * chain: Array<{handoff_id: string, source_thread_id: string, target_thread_id: string, state: string, created_at: number, updated_at: number}>,
310
+ * pending: null | {handoff_id: string, source_thread_id: string, state: string, error_code: string|null, resume_state: string|null,
311
+ * in_flight: boolean, created_at: number, updated_at: number}
312
+ * }} current_thread_id は、後継が無ければ thread_id 自身。pending は、今の続きのタスクで、後継へ指示を送る前の引き継ぎ
313
+ * (一番新しい1つ)。in_flight が true なら worker が動いている最中、false なら止まっている(error_code と resume_state を見る)。
314
+ */
315
+ export function resolveAutoHandoffSuccessor(db, threadId, { findThread = findCodexThreadCandidate,
316
+ processes = readRuntimeProcesses, now = Date.now() } = {}) {
317
+ if (!CODEX_NATIVE_ID_PATTERN.test(threadId ?? '')) throw error('handoff_thread_id_invalid');
318
+ // 残っている一番先の後継(旧タスクへ入力が来た時に開く物と同じ)。そこから threadId まで、作った引き継ぎをさかのぼる。
319
+ const leaf = findLiveAutoHandoffSuccessor(db, threadId, { findThread });
320
+ const path = [];
321
+ for (let operation = leaf, depth = 0; operation && depth < 200; depth++) {
322
+ path.unshift(operation);
323
+ if (operation.source_thread_id === threadId) break;
324
+ operation = findAutoHandoffForTarget(db, operation.source_thread_id);
325
+ }
326
+ const chain = path.map(operation => ({ handoff_id: operation.handoff_id, source_thread_id: operation.source_thread_id,
327
+ target_thread_id: operation.target_thread_id, state: operation.state, created_at: operation.created_at, updated_at: operation.updated_at }));
328
+ const current = leaf ? leaf.target_thread_id : threadId;
329
+ const undelivered = listUndeliveredAutoHandoffsForSource(db, current)[0] ?? null;
330
+ const pending = undelivered ? { handoff_id: undelivered.handoff_id, source_thread_id: undelivered.source_thread_id,
331
+ state: undelivered.state, error_code: undelivered.error_code ?? null, resume_state: undelivered.resume_state ?? null,
332
+ in_flight: !HANDOFF_TERMINAL_STATES.has(undelivered.state) && workerActive(undelivered, processes, now),
333
+ created_at: undelivered.created_at, updated_at: undelivered.updated_at } : null;
334
+ return { thread_id: threadId, handed_off: chain.length > 0, current_thread_id: current, chain, pending };
335
+ }
336
+
304
337
  /**
305
338
  * 引き継ぎが済んだ旧タスクの名前の頭へ、引き継ぎ済みの印を付ける。同じ名前の後継が一覧に並んでも、
306
339
  * 印の無い1本が今の続きだと分かるようにする。今の名前を読んでから付けるので、利用者が付け直した名前も残る。
@@ -15,7 +15,7 @@ import { readCodexHandoffState, settingsMatch, threadStartSettings, resolvePrepa
15
15
  import { runAutoHandoffWorker, requestCodexAutoHandoff, sourceBoundary,
16
16
  autoHandoffTargetName, nameAutoHandoffTarget, findLiveAutoHandoffSuccessor,
17
17
  markAutoHandoffSource, markAutoHandoffSources, autoHandoffSourceFallbackTitle, carryAutoHandoffQueue,
18
- untrackedInheritedMemory } from './codex-auto-handoff.mjs';
18
+ untrackedInheritedMemory, resolveAutoHandoffSuccessor } from './codex-auto-handoff.mjs';
19
19
  import { parseAutoHandoffArgs } from './cli/auto-handoff.mjs';
20
20
 
21
21
  async function withDb(fn) {
@@ -287,6 +287,58 @@ test('PreCompactの重複ではworkerを増やさず、manualと別projectでは
287
287
  assert.equal((await requestCodexAutoHandoff({ ...args, config: { ...config, projects: ['/other'] } })).status, 'skipped');
288
288
  }));
289
289
 
290
+ test('タスクの今の続きを引く。先までたどり、消えた後継は無い物として扱い、途中の引き継ぎと止まった引き継ぎを見分ける', async () => withDb(async db => {
291
+ const id = n => `${String(n).repeat(8)}-${String(n).repeat(4)}-${String(n).repeat(4)}-${String(n).repeat(4)}-${String(n).repeat(12)}`;
292
+ const [A, B, C, D, X, Y, Z] = [1, 2, 3, 4, 5, 6, 7].map(id);
293
+ const done = (source, target) => updateAutoHandoff(db, request(db, source).operation.handoff_id,
294
+ { state: 'continued', target_thread_id: target, queued_submission_id: `receipt-${target}` }).operation;
295
+ const gone = new Set();
296
+ const options = { findThread: ({ threadId }) => gone.has(threadId) ? null : { rolloutPath: `/r/${threadId}`, mtimeMs: 1 }, processes: () => [], now: 1_000_000 };
297
+ // 引き継ぎの無いタスクは、自分が今の続き
298
+ assert.deepEqual(resolveAutoHandoffSuccessor(db, X, options), { thread_id: X, handed_off: false, current_thread_id: X, chain: [], pending: null });
299
+
300
+ const ab = done(A, B); const bc = done(B, C);
301
+ const fromA = resolveAutoHandoffSuccessor(db, A, options);
302
+ assert.equal(fromA.handed_off, true); assert.equal(fromA.current_thread_id, C); assert.equal(fromA.pending, null);
303
+ assert.deepEqual(fromA.chain.map(item => [item.handoff_id, item.source_thread_id, item.target_thread_id, item.state]),
304
+ [[ab.handoff_id, A, B, 'continued'], [bc.handoff_id, B, C, 'continued']]);
305
+ assert.equal(resolveAutoHandoffSuccessor(db, B, options).current_thread_id, C);
306
+ assert.deepEqual(resolveAutoHandoffSuccessor(db, B, options).chain.map(item => item.handoff_id), [bc.handoff_id]);
307
+ assert.equal(resolveAutoHandoffSuccessor(db, C, options).handed_off, false);
308
+
309
+ // 今の続き C が、さらに引き継いでいる最中(worker が動いている)
310
+ const cd = updateAutoHandoff(db, request(db, C).operation.handoff_id, { state: 'memory_ready' }).operation;
311
+ db.prepare('UPDATE codex_handoffs SET worker_identity_json = ?, updated_at = ? WHERE handoff_id = ?').run(JSON.stringify({ pid: 4242, started_identity: 'w' }), 999_000, cd.handoff_id);
312
+ const running = resolveAutoHandoffSuccessor(db, A, { ...options, processes: () => [{ pid: 4242, started_identity: 'w' }] });
313
+ assert.equal(running.current_thread_id, C); assert.equal(running.pending.handoff_id, cd.handoff_id);
314
+ assert.equal(running.pending.in_flight, true); assert.equal(running.pending.state, 'memory_ready');
315
+ // worker が居なくなっていれば、途中で止まった物として返す
316
+ assert.equal(resolveAutoHandoffSuccessor(db, A, options).pending.in_flight, false);
317
+
318
+ // C の引き継ぎが失敗して止まった
319
+ updateAutoHandoff(db, cd.handoff_id, { state: 'failed', error_code: 'handoff_summarizer_backend_failed', resume_state: 'source_stopped' });
320
+ const stopped = resolveAutoHandoffSuccessor(db, A, options);
321
+ assert.equal(stopped.current_thread_id, C);
322
+ assert.deepEqual([stopped.pending.state, stopped.pending.error_code, stopped.pending.resume_state, stopped.pending.in_flight],
323
+ ['failed', 'handoff_summarizer_backend_failed', 'source_stopped', false]);
324
+
325
+ // やり直して D へ続いた。C が消されても、D が残っていれば D が今の続き
326
+ updateAutoHandoff(db, cd.handoff_id, { state: 'continued', error_code: null, resume_state: null, target_thread_id: D, queued_submission_id: 'receipt-D' });
327
+ gone.add(C);
328
+ const toD = resolveAutoHandoffSuccessor(db, A, options);
329
+ assert.equal(toD.current_thread_id, D); assert.equal(toD.chain.length, 3); assert.equal(toD.pending, null);
330
+ // 後継が全部消されたら、旧タスクが今の続きに戻る
331
+ gone.add(D); gone.add(B);
332
+ assert.deepEqual(resolveAutoHandoffSuccessor(db, A, options), { thread_id: A, handed_off: false, current_thread_id: A, chain: [], pending: null });
333
+
334
+ // 後継が出来る前に失敗したタスク(後継なし)
335
+ updateAutoHandoff(db, request(db, Y).operation.handoff_id, { state: 'failed', error_code: 'handoff_target_not_loaded', resume_state: 'memory_ready' });
336
+ const failed = resolveAutoHandoffSuccessor(db, Y, options);
337
+ assert.equal(failed.handed_off, false); assert.equal(failed.current_thread_id, Y); assert.equal(failed.pending.error_code, 'handoff_target_not_loaded');
338
+ assert.throws(() => resolveAutoHandoffSuccessor(db, 'not-a-thread', options), /handoff_thread_id_invalid/);
339
+ void Z;
340
+ }));
341
+
290
342
  test('Codexの内部の文脈(auto-compact-N)からの圧縮は、例外にせず止めて記録する。引き継ぎは作らない', async () => withDb(async (db, home) => {
291
343
  const thread = '11111111-1111-1111-1111-111111111111';
292
344
  const file = join(home, 'rollout.jsonl'); writeFileSync(file, '');