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.
Files changed (111) hide show
  1. package/.claude/commands/tl-trim.md +42 -0
  2. package/.codex-sidecar.yml +62 -0
  3. package/CHANGELOG.md +583 -0
  4. package/README.ja.md +42 -5
  5. package/README.md +400 -23
  6. package/bin/throughline.mjs +168 -4
  7. package/codex/skills/throughline/SKILL.md +157 -0
  8. package/codex/skills/throughline/agents/openai.yaml +7 -0
  9. package/docs/INHERITANCE_ON_CLEAR_ONLY.md +146 -0
  10. package/docs/L1_L2_L3_REDESIGN.md +415 -0
  11. package/docs/PUBLIC_RELEASE_PLAN.md +184 -0
  12. package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
  13. package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
  14. package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
  15. package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
  16. package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
  17. package/docs/archive/CONCEPT.md +476 -0
  18. package/docs/archive/EXPERIMENT.md +371 -0
  19. package/docs/archive/README.md +22 -0
  20. package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
  21. package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
  22. package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
  23. package/docs/throughline-handoff-context.example.json +57 -0
  24. package/docs/throughline-rollback-context-trim-insight.md +455 -0
  25. package/package.json +6 -2
  26. package/src/cli/codex-capture.mjs +95 -0
  27. package/src/cli/codex-handoff-model-smoke.mjs +292 -0
  28. package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
  29. package/src/cli/codex-handoff-smoke.mjs +163 -0
  30. package/src/cli/codex-handoff-smoke.test.mjs +149 -0
  31. package/src/cli/codex-handoff-start.mjs +291 -0
  32. package/src/cli/codex-handoff-start.test.mjs +194 -0
  33. package/src/cli/codex-hook.mjs +276 -0
  34. package/src/cli/codex-hook.test.mjs +293 -0
  35. package/src/cli/codex-host-primitive-audit.mjs +110 -0
  36. package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
  37. package/src/cli/codex-restore-smoke.mjs +357 -0
  38. package/src/cli/codex-restore-source-audit.mjs +304 -0
  39. package/src/cli/codex-resume.mjs +138 -0
  40. package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
  41. package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
  42. package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
  43. package/src/cli/codex-sidecar-dry-run.mjs +85 -0
  44. package/src/cli/codex-summarize.mjs +224 -0
  45. package/src/cli/codex-threads.mjs +89 -0
  46. package/src/cli/codex-visibility-smoke.mjs +196 -0
  47. package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
  48. package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
  49. package/src/cli/doctor.mjs +503 -1
  50. package/src/cli/doctor.test.mjs +542 -3
  51. package/src/cli/handoff-preview.mjs +78 -0
  52. package/src/cli/help.test.mjs +64 -0
  53. package/src/cli/install.mjs +227 -4
  54. package/src/cli/install.test.mjs +207 -4
  55. package/src/cli/trim.mjs +564 -0
  56. package/src/codex-app-server.mjs +1816 -0
  57. package/src/codex-app-server.test.mjs +512 -0
  58. package/src/codex-auto-refresh.mjs +194 -0
  59. package/src/codex-auto-refresh.test.mjs +182 -0
  60. package/src/codex-capture.mjs +235 -0
  61. package/src/codex-capture.test.mjs +393 -0
  62. package/src/codex-handoff-model-smoke.mjs +114 -0
  63. package/src/codex-handoff-model-smoke.test.mjs +89 -0
  64. package/src/codex-handoff-smoke.mjs +124 -0
  65. package/src/codex-handoff-smoke.test.mjs +103 -0
  66. package/src/codex-handoff.mjs +331 -0
  67. package/src/codex-handoff.test.mjs +220 -0
  68. package/src/codex-host-primitive-audit.mjs +374 -0
  69. package/src/codex-host-primitive-audit.test.mjs +208 -0
  70. package/src/codex-restore-smoke.test.mjs +639 -0
  71. package/src/codex-restore-source-audit.mjs +1348 -0
  72. package/src/codex-restore-source-audit.test.mjs +623 -0
  73. package/src/codex-resume.test.mjs +242 -0
  74. package/src/codex-rollout-memory.mjs +711 -0
  75. package/src/codex-rollout-memory.test.mjs +610 -0
  76. package/src/codex-sidecar-cli.test.mjs +75 -0
  77. package/src/codex-sidecar.mjs +246 -0
  78. package/src/codex-sidecar.test.mjs +172 -0
  79. package/src/codex-summarize.test.mjs +143 -0
  80. package/src/codex-thread-identity.mjs +23 -0
  81. package/src/codex-thread-index.mjs +173 -0
  82. package/src/codex-thread-index.test.mjs +164 -0
  83. package/src/codex-usage.mjs +110 -0
  84. package/src/codex-usage.test.mjs +140 -0
  85. package/src/codex-visibility-smoke.test.mjs +222 -0
  86. package/src/codex-vscode-restore-smoke.mjs +206 -0
  87. package/src/codex-vscode-restore-smoke.test.mjs +325 -0
  88. package/src/codex-vscode-rollback-smoke.mjs +90 -0
  89. package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
  90. package/src/db-schema.test.mjs +97 -0
  91. package/src/haiku-summarizer.mjs +267 -26
  92. package/src/haiku-summarizer.test.mjs +282 -0
  93. package/src/handoff-preview.test.mjs +108 -0
  94. package/src/handoff-record.mjs +294 -0
  95. package/src/handoff-record.test.mjs +226 -0
  96. package/src/hook-entrypoints.test.mjs +326 -0
  97. package/src/package-files.test.mjs +19 -0
  98. package/src/prompt-submit.mjs +9 -6
  99. package/src/resume-context.mjs +44 -140
  100. package/src/resume-context.test.mjs +172 -0
  101. package/src/session-start.mjs +8 -5
  102. package/src/state-file.mjs +50 -6
  103. package/src/state-file.test.mjs +50 -0
  104. package/src/token-monitor.mjs +14 -10
  105. package/src/token-monitor.test.mjs +27 -0
  106. package/src/trim-cli.test.mjs +1584 -0
  107. package/src/trim-model.mjs +584 -0
  108. package/src/trim-model.test.mjs +568 -0
  109. package/src/turn-processor.mjs +17 -10
  110. package/src/vscode-task.mjs +94 -6
  111. package/src/vscode-task.test.mjs +186 -6
@@ -19,6 +19,16 @@ import { homedir } from 'node:os';
19
19
  import { execSync } from 'node:child_process';
20
20
  import { getStateDir } from '../state-file.mjs';
21
21
  import { readLatestUsage } from '../transcript-usage.mjs';
22
+ import { buildCodexRolloutTrimSource } from '../codex-rollout-memory.mjs';
23
+ import { runCodexHostPrimitiveAudit } from '../codex-host-primitive-audit.mjs';
24
+ import { buildCodexHandoffSmoke } from '../codex-handoff-smoke.mjs';
25
+ import { buildHandoffRecord } from '../handoff-record.mjs';
26
+ import { DEFAULT_TRIM_KEEP_RECENT, buildTrimPlan, describeTrimHost } from '../trim-model.mjs';
27
+ import { resolveCodexThreadIdentity } from '../codex-thread-identity.mjs';
28
+ import { defaultCodexHome, listCodexThreadCandidates } from '../codex-thread-index.mjs';
29
+ import { getDb } from '../db.mjs';
30
+ import { detectJsoncFeatures, findMonitorTaskIndex, isMonitorTaskBroken } from '../vscode-task.mjs';
31
+ import { buildCodexStopHookCommand, isThroughlineCodexStopCommand } from './install.mjs';
22
32
 
23
33
  const GREEN = '\x1b[32m✓\x1b[0m';
24
34
  const RED = '\x1b[31m✗\x1b[0m';
@@ -43,7 +53,7 @@ async function check(label, fn) {
43
53
  }
44
54
 
45
55
  function parseArgs(argv) {
46
- const args = { session: null };
56
+ const args = { session: null, trim: false, host: 'unknown', codex: false };
47
57
  for (let i = 0; i < argv.length; i++) {
48
58
  if (argv[i] === '--session') {
49
59
  const value = argv[i + 1];
@@ -52,6 +62,17 @@ function parseArgs(argv) {
52
62
  }
53
63
  args.session = value;
54
64
  i++;
65
+ } else if (argv[i] === '--trim') {
66
+ args.trim = true;
67
+ } else if (argv[i] === '--codex') {
68
+ args.codex = true;
69
+ } else if (argv[i] === '--host') {
70
+ const value = argv[i + 1];
71
+ if (!['claude', 'codex', 'unknown'].includes(value)) {
72
+ throw new Error('--host must be claude, codex, or unknown');
73
+ }
74
+ args.host = value;
75
+ i++;
55
76
  }
56
77
  }
57
78
  return args;
@@ -266,6 +287,469 @@ function runSessionDiagnosis(prefix) {
266
287
  }
267
288
  }
268
289
 
290
+ function runTrimDiagnosis(
291
+ host,
292
+ env = process.env,
293
+ { auditRunner = runCodexHostPrimitiveAudit } = {},
294
+ ) {
295
+ const info = describeTrimHost(host);
296
+ const codexIdentity =
297
+ info.host === 'codex' ? resolveCodexThreadIdentity({ codexThreadId: null }, env) : null;
298
+ const hostPrimitiveDiagnosis =
299
+ info.host === 'codex' ? readCodexHostPrimitiveDiagnosis({ env, auditRunner }) : null;
300
+ console.log(`${BOLD}[Trim]${RESET}\n`);
301
+ console.log(` host: ${info.host}`);
302
+ console.log(` default keep-recent: ${DEFAULT_TRIM_KEEP_RECENT}`);
303
+ console.log(` automatic rollback: ${info.automaticRollback ? 'yes' : 'no'}`);
304
+ console.log(` automatic inject: ${info.automaticInject ? 'yes' : 'no'}`);
305
+ console.log(` boundary status: ${info.status}`);
306
+ console.log(` boundary reason: ${info.reason}`);
307
+ if (codexIdentity) {
308
+ const identityText = codexIdentity.codexThreadId
309
+ ? `${codexIdentity.codexThreadId} (${codexIdentity.codexThreadIdSource})`
310
+ : 'not detected';
311
+ console.log(` current Codex thread: ${identityText}`);
312
+ }
313
+ if (hostPrimitiveDiagnosis) {
314
+ console.log(` host primitive audit: ${hostPrimitiveDiagnosis.status}`);
315
+ console.log(` host primitive reason: ${hostPrimitiveDiagnosis.reason}`);
316
+ console.log(
317
+ ` current-thread non-resurrection: ${
318
+ hostPrimitiveDiagnosis.hasCurrentThreadNonResurrectionPrimitive ? 'yes' : 'no'
319
+ }`,
320
+ );
321
+ console.log(` repair contract: ${hostPrimitiveDiagnosis.repairContractStatus}`);
322
+ }
323
+ console.log('');
324
+ console.log(' dry-run command:');
325
+ if (info.host === 'codex' && !codexIdentity?.codexThreadId) {
326
+ console.log(' throughline trim --dry-run --host codex --codex-thread-id <id>');
327
+ console.log(' throughline trim --preflight --host codex --codex-thread-id <id>');
328
+ } else if (info.host === 'codex') {
329
+ console.log(' throughline trim --dry-run --host codex');
330
+ console.log(' throughline trim --preflight --host codex');
331
+ } else {
332
+ console.log(` throughline trim --dry-run --host ${info.host}`);
333
+ }
334
+ if (info.host === 'codex') {
335
+ console.log(' throughline trim --execute --host codex');
336
+ }
337
+ if (info.host === 'codex') {
338
+ const sessionId = codexIdentity?.codexThreadId
339
+ ? `codex:${codexIdentity.codexThreadId}`
340
+ : 'codex:<thread-id>';
341
+ console.log('');
342
+ console.log(' fresh-thread continuation path:');
343
+ console.log(' status: fresh-thread-handoff-available');
344
+ console.log(' reason: optional_fresh_thread_continuation');
345
+ console.log(' safety scope: fresh_thread_handoff_no_current_thread_mutation');
346
+ console.log(` guided: throughline codex-handoff-start --session ${sessionId}`);
347
+ console.log(` smoke: throughline codex-handoff-smoke --session ${sessionId}`);
348
+ console.log(` model smoke dry-run: throughline codex-handoff-model-smoke --session ${sessionId} --dry-run --json`);
349
+ console.log(` memory: throughline codex-resume --session ${sessionId} --format handoff`);
350
+ console.log(' then: start a new Codex thread with that handoff context only if desired');
351
+ }
352
+ console.log('');
353
+ console.log(' manual procedure:');
354
+ for (const step of info.manualProcedure) {
355
+ console.log(` - ${step}`);
356
+ }
357
+ }
358
+
359
+ function findLatestCapturedCodexSession(db, projectPath) {
360
+ try {
361
+ return (
362
+ db
363
+ .prepare(
364
+ `SELECT session_id, updated_at
365
+ FROM sessions
366
+ WHERE lower(project_path) = lower(?)
367
+ AND session_id LIKE 'codex:%'
368
+ ORDER BY updated_at DESC
369
+ LIMIT 1`,
370
+ )
371
+ .get(projectPath) ?? null
372
+ );
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+
378
+ function countCapturedCodexSessions(db, projectPath) {
379
+ try {
380
+ return db
381
+ .prepare(
382
+ `SELECT COUNT(*) AS count
383
+ FROM sessions
384
+ WHERE lower(project_path) = lower(?)
385
+ AND session_id LIKE 'codex:%'`,
386
+ )
387
+ .get(projectPath).count;
388
+ } catch {
389
+ return 0;
390
+ }
391
+ }
392
+
393
+ function readCodexHookDiagnosis(codexHome) {
394
+ const hooksPath = join(codexHome, 'hooks.json');
395
+ const configPath = join(codexHome, 'config.toml');
396
+ const expectedCommand = buildCodexStopHookCommand();
397
+ const out = {
398
+ hooksPath,
399
+ configPath,
400
+ expectedCommand,
401
+ hooksReadable: false,
402
+ featureEnabled: false,
403
+ managedStopHooks: [],
404
+ legacyManagedStopHooks: [],
405
+ };
406
+
407
+ if (existsSync(configPath)) {
408
+ try {
409
+ out.featureEnabled = /^\s*codex_hooks\s*=\s*true\s*$/m.test(readFileSync(configPath, 'utf8'));
410
+ } catch {
411
+ out.featureEnabled = false;
412
+ }
413
+ }
414
+
415
+ if (!existsSync(hooksPath)) return out;
416
+ let parsed;
417
+ try {
418
+ parsed = JSON.parse(readFileSync(hooksPath, 'utf8'));
419
+ } catch {
420
+ return out;
421
+ }
422
+
423
+ out.hooksReadable = true;
424
+ const stopHooks = (parsed.hooks?.Stop ?? []).flatMap(group => group.hooks ?? []);
425
+ out.managedStopHooks = stopHooks.filter(h => isThroughlineCodexStopCommand(h.command));
426
+ out.legacyManagedStopHooks = out.managedStopHooks.filter(h => h.command !== expectedCommand);
427
+ return out;
428
+ }
429
+
430
+ function runCodexDiagnosis({
431
+ env = process.env,
432
+ cwd = process.cwd(),
433
+ db = getDb(),
434
+ auditRunner = runCodexHostPrimitiveAudit,
435
+ } = {}) {
436
+ const codexHome = env.CODEX_HOME || defaultCodexHome();
437
+ const identity = resolveCodexThreadIdentity({ codexThreadId: null }, env);
438
+ const hookDiagnosis = readCodexHookDiagnosis(codexHome);
439
+ const hostPrimitiveDiagnosis = readCodexHostPrimitiveDiagnosis({ env, auditRunner });
440
+ const monitorTaskDiagnosis = readVsCodeMonitorTaskDiagnosis(cwd);
441
+ const candidates = listCodexThreadCandidates({
442
+ codexHome,
443
+ projectPath: cwd,
444
+ limit: 3,
445
+ });
446
+ const latestCaptured = findLatestCapturedCodexSession(db, cwd);
447
+ const capturedCount = countCapturedCodexSessions(db, cwd);
448
+ const refreshDiagnosis = buildCodexContextRefreshDiagnosis({
449
+ db,
450
+ cwd,
451
+ codexHome,
452
+ identity,
453
+ });
454
+
455
+ console.log(`${BOLD}[Codex primary]${RESET}\n`);
456
+ console.log(` project: ${cwd}`);
457
+ console.log(` CODEX_HOME: ${codexHome}`);
458
+ console.log(` Codex hooks feature: ${hookDiagnosis.featureEnabled ? 'enabled' : 'not enabled'}`);
459
+ console.log(` Codex Stop hook: ${
460
+ hookDiagnosis.managedStopHooks.length === 0
461
+ ? 'not registered'
462
+ : hookDiagnosis.legacyManagedStopHooks.length > 0
463
+ ? 'legacy command needs reinstall'
464
+ : 'registered'
465
+ }`);
466
+ if (hookDiagnosis.managedStopHooks.length > 0) {
467
+ const h = hookDiagnosis.managedStopHooks[0];
468
+ console.log(` command: ${h.command}`);
469
+ console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
470
+ console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
471
+ }
472
+ console.log(` VSCode monitor task: ${monitorTaskDiagnosis.status}`);
473
+ if (monitorTaskDiagnosis.path) {
474
+ console.log(` path: ${monitorTaskDiagnosis.path}`);
475
+ }
476
+ if (monitorTaskDiagnosis.runOn) {
477
+ console.log(` runOn: ${monitorTaskDiagnosis.runOn}`);
478
+ }
479
+ if (monitorTaskDiagnosis.note) {
480
+ console.log(` note: ${monitorTaskDiagnosis.note}`);
481
+ }
482
+ console.log(
483
+ ` current Codex thread: ${
484
+ identity.codexThreadId
485
+ ? `${identity.codexThreadId} (${identity.codexThreadIdSource})`
486
+ : 'not detected'
487
+ }`,
488
+ );
489
+ console.log(` rollout candidates: ${candidates.length}`);
490
+ if (candidates.length > 0) {
491
+ const latest = candidates[0];
492
+ console.log(` latest rollout: ${latest.id}`);
493
+ console.log(` updatedAt: ${latest.updatedAt}`);
494
+ console.log(` path: ${latest.rolloutPath}`);
495
+ }
496
+ console.log(` captured DB sessions: ${capturedCount}`);
497
+ if (latestCaptured) {
498
+ console.log(` latest DB session: ${latestCaptured.session_id}`);
499
+ console.log(` updatedAt: ${formatTs(latestCaptured.updated_at)}`);
500
+ }
501
+ if (refreshDiagnosis) {
502
+ console.log(` context refresh: ${refreshDiagnosis.status}`);
503
+ if (refreshDiagnosis.blockedReason) {
504
+ console.log(` blocked reason: ${refreshDiagnosis.blockedReason}`);
505
+ }
506
+ console.log(` rollback source: ${refreshDiagnosis.rollbackSource}`);
507
+ console.log(` inject memory source: ${refreshDiagnosis.injectMemorySource}`);
508
+ console.log(` memory contract: ${refreshDiagnosis.memoryContract}`);
509
+ console.log(` L1 summaries: ${refreshDiagnosis.l1Summaries}`);
510
+ console.log(` recent L2 bodies: ${refreshDiagnosis.recentBodies}`);
511
+ console.log(` L3 references only: ${refreshDiagnosis.l3References} (bodies not injected)`);
512
+ if (refreshDiagnosis.handoffSmoke) {
513
+ console.log(` new-thread handoff: ${refreshDiagnosis.handoffSmoke.status}`);
514
+ if (refreshDiagnosis.safeContinuationStatus) {
515
+ console.log(` safe continuation: ${refreshDiagnosis.safeContinuationStatus}`);
516
+ }
517
+ console.log(` prompt chars: ${refreshDiagnosis.handoffSmoke.promptChars}`);
518
+ console.log(` estimated tokens: ${refreshDiagnosis.handoffSmoke.estimatedTokens}`);
519
+ }
520
+ if (refreshDiagnosis.estimate) {
521
+ console.log(` estimated reduction: ${refreshDiagnosis.estimate}`);
522
+ }
523
+ }
524
+ console.log(` host primitive audit: ${hostPrimitiveDiagnosis.status}`);
525
+ console.log(` reason: ${hostPrimitiveDiagnosis.reason}`);
526
+ console.log(
527
+ ` current-thread non-resurrection: ${
528
+ hostPrimitiveDiagnosis.hasCurrentThreadNonResurrectionPrimitive ? 'yes' : 'no'
529
+ }`,
530
+ );
531
+ console.log(` repair contract: ${hostPrimitiveDiagnosis.repairContractStatus}`);
532
+ console.log('');
533
+ console.log(' next commands:');
534
+ if (identity.codexThreadId) {
535
+ console.log(` throughline codex-capture --codex-thread-id ${identity.codexThreadId}`);
536
+ console.log(` throughline codex-handoff-start --session codex:${identity.codexThreadId}`);
537
+ console.log(` throughline codex-handoff-smoke --session codex:${identity.codexThreadId}`);
538
+ console.log(` throughline codex-handoff-model-smoke --session codex:${identity.codexThreadId} --dry-run --json`);
539
+ console.log(` throughline codex-resume --session codex:${identity.codexThreadId} --format handoff`);
540
+ console.log(` throughline codex-resume --session codex:${identity.codexThreadId}`);
541
+ console.log(` THROUGHLINE_EXPERIMENTAL_CODEX_HANDOFF_MODEL_SMOKE=1 throughline codex-handoff-model-smoke --session codex:${identity.codexThreadId}`);
542
+ } else {
543
+ console.log(' throughline codex-threads --limit 5');
544
+ console.log(' throughline codex-capture --codex-thread-id <id>');
545
+ console.log(' throughline codex-handoff-start --session codex:<id>');
546
+ console.log(' throughline codex-handoff-smoke --session codex:<id>');
547
+ console.log(' throughline codex-handoff-model-smoke --session codex:<id> --dry-run --json');
548
+ console.log(' throughline codex-resume --session codex:<id> --format handoff');
549
+ console.log(' throughline codex-resume --session codex:<id>');
550
+ console.log(' THROUGHLINE_EXPERIMENTAL_CODEX_HANDOFF_MODEL_SMOKE=1 throughline codex-handoff-model-smoke --session codex:<id>');
551
+ }
552
+ console.log(' throughline doctor --trim --host codex');
553
+ console.log(' throughline codex-host-primitive-audit');
554
+ }
555
+
556
+ function readCodexHostPrimitiveDiagnosis({
557
+ env = process.env,
558
+ auditRunner = runCodexHostPrimitiveAudit,
559
+ } = {}) {
560
+ const command = env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex';
561
+ try {
562
+ const audit = auditRunner({ command });
563
+ return {
564
+ status: audit.status ?? 'unknown',
565
+ reason: audit.reason ?? 'unknown',
566
+ hasCurrentThreadRemediationPrimitive: Boolean(
567
+ audit.facts?.hasCurrentThreadRemediationPrimitive,
568
+ ),
569
+ hasCurrentThreadNonResurrectionPrimitive: Boolean(
570
+ audit.facts?.hasCurrentThreadNonResurrectionPrimitive ??
571
+ audit.facts?.hasCurrentThreadRemediationPrimitive,
572
+ ),
573
+ repairContractStatus: audit.repairContract?.status ?? 'unknown',
574
+ methodCount: audit.methodCount ?? null,
575
+ };
576
+ } catch (err) {
577
+ return {
578
+ status: 'unavailable',
579
+ reason: err instanceof Error ? err.message : String(err),
580
+ hasCurrentThreadRemediationPrimitive: false,
581
+ hasCurrentThreadNonResurrectionPrimitive: false,
582
+ repairContractStatus: 'unavailable',
583
+ methodCount: null,
584
+ };
585
+ }
586
+ }
587
+
588
+ function readVsCodeMonitorTaskDiagnosis(cwd) {
589
+ const tasksPath = join(cwd, '.vscode', 'tasks.json');
590
+ if (!existsSync(tasksPath)) {
591
+ return {
592
+ status: 'not registered',
593
+ path: tasksPath,
594
+ note: 'created by the next VSCode hook event; if the folder is already open, reload VSCode once after creation',
595
+ };
596
+ }
597
+
598
+ let text;
599
+ try {
600
+ text = readFileSync(tasksPath, 'utf8');
601
+ } catch (err) {
602
+ return {
603
+ status: 'unreadable',
604
+ path: tasksPath,
605
+ note: err instanceof Error ? err.message : 'read failed',
606
+ };
607
+ }
608
+
609
+ if (detectJsoncFeatures(text)) {
610
+ return {
611
+ status: 'jsonc not inspected',
612
+ path: tasksPath,
613
+ note: 'Throughline will not auto-edit JSONC tasks; add or verify the monitor task manually',
614
+ };
615
+ }
616
+
617
+ let parsed;
618
+ try {
619
+ parsed = JSON.parse(text);
620
+ } catch {
621
+ return {
622
+ status: 'parse error',
623
+ path: tasksPath,
624
+ note: 'tasks.json is not valid JSON; Throughline will not auto-edit it',
625
+ };
626
+ }
627
+
628
+ const index = findMonitorTaskIndex(parsed);
629
+ if (index < 0) {
630
+ return {
631
+ status: 'not registered',
632
+ path: tasksPath,
633
+ note: 'created by the next VSCode hook event; if the folder is already open, reload VSCode once after creation',
634
+ };
635
+ }
636
+
637
+ const task = parsed.tasks[index];
638
+ if (isMonitorTaskBroken(task)) {
639
+ return {
640
+ status: 'registered but broken',
641
+ path: tasksPath,
642
+ runOn: task?.runOptions?.runOn ?? '(missing)',
643
+ note: 'existing task points at a missing absolute path; the next VSCode hook event should repair it',
644
+ };
645
+ }
646
+
647
+ return {
648
+ status: 'registered',
649
+ path: tasksPath,
650
+ runOn: task?.runOptions?.runOn ?? '(missing)',
651
+ note: 'if it was created after this folder was already open, run Developer: Reload Window once or start the Throughline Monitor task manually',
652
+ };
653
+ }
654
+
655
+ function buildCodexContextRefreshDiagnosis({ db, cwd, codexHome, identity }) {
656
+ if (!identity.codexThreadId) return null;
657
+
658
+ let trimSource = null;
659
+ try {
660
+ trimSource = buildCodexRolloutTrimSource({
661
+ threadId: identity.codexThreadId,
662
+ codexHome,
663
+ projectPath: cwd,
664
+ sourceReason:
665
+ identity.codexThreadIdSource && identity.codexThreadIdSource.startsWith('env:')
666
+ ? 'env_codex_thread_rollout'
667
+ : 'explicit_codex_thread_rollout',
668
+ });
669
+ } catch {
670
+ trimSource = null;
671
+ }
672
+
673
+ let plan;
674
+ try {
675
+ plan = buildTrimPlan(db, {
676
+ projectPath: cwd,
677
+ host: 'codex',
678
+ trimAll: true,
679
+ codexThreadId: identity.codexThreadId,
680
+ codexThreadIdSource: identity.codexThreadIdSource,
681
+ trimSource,
682
+ });
683
+ } catch {
684
+ return {
685
+ status: 'unavailable',
686
+ rollbackSource: trimSource?.source ?? 'unknown',
687
+ injectMemorySource: 'unknown',
688
+ memoryContract: 'unavailable',
689
+ l1Summaries: 'unknown',
690
+ recentBodies: 'unknown',
691
+ l3References: 'unknown',
692
+ estimate: null,
693
+ };
694
+ }
695
+
696
+ const stats = plan.memoryPreview?.stats ?? {};
697
+ const hasDbMemory =
698
+ stats.source === 'throughline-db' &&
699
+ ((stats.l1Summaries ?? 0) > 0 || (stats.recentBodies ?? 0) > 0 || (stats.l3References ?? 0) > 0);
700
+ let handoffSmoke = null;
701
+ if (hasDbMemory) {
702
+ try {
703
+ const record = buildHandoffRecord(db, {
704
+ sessionId: `codex:${identity.codexThreadId}`,
705
+ isInheritance: false,
706
+ });
707
+ if (record) {
708
+ const smoke = buildCodexHandoffSmoke(record);
709
+ handoffSmoke = {
710
+ status: smoke.status,
711
+ reason: smoke.reason,
712
+ promptChars: smoke.promptChars,
713
+ estimatedTokens: smoke.estimatedTokens,
714
+ };
715
+ }
716
+ } catch {
717
+ handoffSmoke = {
718
+ status: 'unavailable',
719
+ reason: 'handoff_smoke_failed',
720
+ promptChars: 'unknown',
721
+ estimatedTokens: 'unknown',
722
+ };
723
+ }
724
+ }
725
+ const estimate = plan.trim?.contextReductionEstimate;
726
+ return {
727
+ status: hasDbMemory ? 'ready' : 'not ready',
728
+ blockedReason: null,
729
+ rollbackSource: plan.trim?.source ?? 'unknown',
730
+ injectMemorySource: stats.source ?? 'unknown',
731
+ memoryContract: hasDbMemory
732
+ ? 'older L1 + latest 20 L2 full bodies + L3 references only'
733
+ : 'Throughline DB memory required; rollout preview is not injected',
734
+ l1Summaries: stats.l1Summaries ?? 0,
735
+ recentBodies:
736
+ typeof stats.recentBodies === 'number'
737
+ ? `${stats.recentBodies} rows (latest ${stats.recentTurnLimit ?? DEFAULT_TRIM_KEEP_RECENT} turns)`
738
+ : 'unknown',
739
+ l3References: stats.l3References ?? 0,
740
+ handoffSmoke,
741
+ safeContinuationStatus:
742
+ handoffSmoke?.status === 'ready'
743
+ ? 'fresh-thread-handoff-available'
744
+ : handoffSmoke
745
+ ? 'handoff-not-ready'
746
+ : null,
747
+ estimate: estimate
748
+ ? `${estimate.netEstimatedTokens} tokens (${estimate.reductionPct}%, ${estimate.method})`
749
+ : null,
750
+ };
751
+ }
752
+
269
753
  export async function run(argv = []) {
270
754
  let args;
271
755
  try {
@@ -280,6 +764,16 @@ export async function run(argv = []) {
280
764
  return;
281
765
  }
282
766
 
767
+ if (args.trim) {
768
+ runTrimDiagnosis(args.host);
769
+ return;
770
+ }
771
+
772
+ if (args.codex) {
773
+ runCodexDiagnosis();
774
+ return;
775
+ }
776
+
283
777
  console.log('throughline doctor\n');
284
778
 
285
779
  // Node.js バージョン
@@ -343,6 +837,8 @@ export async function run(argv = []) {
343
837
 
344
838
  console.log('');
345
839
  console.log(`${DIM}ヒント: 特定セッションが止まって見えるときは ${RESET}throughline doctor --session <id-prefix>${DIM} で診断できます。${RESET}`);
840
+ console.log(`${DIM}ヒント: trim の host 境界を見るには ${RESET}throughline doctor --trim --host claude${DIM} を使います。${RESET}`);
841
+ console.log(`${DIM}ヒント: Codex primary の入口を見るには ${RESET}throughline doctor --codex${DIM} を使います。${RESET}`);
346
842
  }
347
843
 
348
844
  // テスト用エクスポート
@@ -351,6 +847,12 @@ export const _internal = {
351
847
  formatAgo,
352
848
  formatBytes,
353
849
  runSessionDiagnosis,
850
+ runTrimDiagnosis,
851
+ runCodexDiagnosis,
852
+ buildCodexContextRefreshDiagnosis,
853
+ readCodexHostPrimitiveDiagnosis,
854
+ readCodexHookDiagnosis,
855
+ readVsCodeMonitorTaskDiagnosis,
354
856
  isPidAlive,
355
857
  findLatestJsonlInSameDir,
356
858
  };