throughline 0.10.1 → 0.10.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,20 @@ shipped to npm but were not individually tagged on GitHub.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.10.2] — 2026-08-24
14
+
15
+ ### Changed
16
+
17
+ - 挙動不変のOS/harness層集約リファクタ(harness用語統一campaignの分離規約):
18
+ - LOCALAPPDATA / XDG_CONFIG_HOME / XDG_STATE_HOME のベースディレクトリ組み立てを
19
+ 新設`src/os/app-dirs.mjs`へ一本化(runtime-error-store / completed-turn-receipts
20
+ に3重実装されていた)。
21
+ - hostリテラル比較(`'claude'`/`'codex'`)6ファイルを`src/hosts/identity.mjs`の
22
+ 定数importへ統一し、「識別の唯一の正本」宣言と実装を一致させた。
23
+ - codex-auto-refreshの独自パス正規化を`src/os/paths.mjs`の
24
+ `foldPathCaseForPlatform`へ、codex-sidecarのwin32判定を同`isWin32Platform`へ委譲。
25
+ - `hosts/identity.mjs`の分類語をvendorからharnessへ更新(用語のみ)。
26
+
13
27
  ## [0.10.1] — 2026-08-23
14
28
 
15
29
  ### Changed
package/README.ja.md CHANGED
@@ -390,7 +390,7 @@ throughline handoff-context --session codex:<thread-id> --json
390
390
  成功時の`throughline.handoff_context.v1`は`schema`、`status`、`sessionId`、`context`だけを返す。
391
391
  `context`はSessionStartと同じ予算付き継承文脈で、DB作成・migration・baton消費・session merge・
392
392
  latest session推測・`sessions.merged_into`変更・L1/L2/L3 rowの所属変更は行わない。AItermは任意の
393
- vendor portable forkでこの境界を使う。Observer feedはcompleted-turn projectionであり代替ではない。
393
+ harness portable forkでこの境界を使う。Observer feedはcompleted-turn projectionであり代替ではない。
394
394
 
395
395
  スラッシュコマンド (Claude Code 内でユーザーが叩く):
396
396
 
package/README.md CHANGED
@@ -834,7 +834,7 @@ local-only contracts.
834
834
  | `throughline runtime-errors reopen <fingerprint> --json` | Explicitly reopen a resolved aggregate without fabricating a new occurrence |
835
835
  | `throughline runtime-errors compact --json` | Remove only acknowledged, resolved aggregates after retention; open or unacknowledged records remain |
836
836
  | `throughline handoff-preview --session <id>` | Print a Codex-facing `throughline_handoff` JSON projection |
837
- | `throughline handoff-context --session <id> --json` | Print the exact SessionStart inheritance context as versioned JSON without moving memory rows or changing `sessions.merged_into`; intended for local launchers that need portable cross-vendor context |
837
+ | `throughline handoff-context --session <id> --json` | Print the exact SessionStart inheritance context as versioned JSON without moving memory rows or changing `sessions.merged_into`; intended for local launchers that need portable cross-harness context |
838
838
  | `throughline grok-continue --session <id>` | Spawn a person-facing Grok seat whose first user text is the handoff-context body. cwd is the source session `project_path`. Does not spawn without ready context. No `--rules`. macOS Terminal only |
839
839
  | `throughline codex-capture --codex-thread-id <id>` | Capture active Codex rollout turns into a `codex:<thread_id>` DB session |
840
840
  | `throughline codex-summarize --session codex:<id>` | Summarize captured Codex L2 into L1 with the Codex CLI backend |
@@ -872,7 +872,7 @@ The successful `throughline.handoff_context.v1` object contains only `schema`,
872
872
  inheritance text used by SessionStart. The command does not create or migrate a
873
873
  database, consume a baton, merge sessions, infer a latest session, change
874
874
  `sessions.merged_into`, or reassign L1/L2/L3 rows. AIterm uses this boundary for
875
- its optional cross-vendor portable fork; the Observer feed is a separate
875
+ its optional cross-harness portable fork; the Observer feed is a separate
876
876
  completed-turn projection and is not a substitute.
877
877
 
878
878
  Slash commands (invoked by the user in Claude Code):
@@ -4,14 +4,14 @@
4
4
 
5
5
  通常 handoff が使う `buildBudgetedResumeContext` の出力を、DB 所有権を変えずにローカルプログラムが
6
6
  取得できる read-only CLI として公開する。最初の consumer は AIterm の portable fork だが、I/F は
7
- 特定 consumer や vendor に依存させない。
7
+ 特定 consumer や harness に依存させない。
8
8
 
9
9
  実行 ToDo、依存、状態、完了証拠の正本は Lattice plan `readonly-handoff-context` とする。
10
10
 
11
11
  ## 完了
12
12
 
13
13
  2026-08-04に`throughline@0.9.0`としてnpm、tag、GitHub Release、global installまで公開した。
14
- focused契約testと全回帰は729 pass/1 skip/0 fail。AIterm v0.23.0の代表cross-vendor smokeでは
14
+ focused契約testと全回帰は729 pass/1 skip/0 fail。AIterm v0.23.0の代表cross-harness smokeでは
15
15
  Codex source memoryをClaudeへ注入し、前後でsource session、`sessions.merged_into`、L1/L2/L3 row所属が
16
16
  完全一致することを確認した。公開後の現行ドキュメント全域監査は、Latticeの終端ToDoを再openして
17
17
  README、作業者入口、配布Codex skill、docs索引、計画、CHANGELOGへ同期した。変更Markdownの
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "throughline",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "description": "Persistent memory hooks for Claude Code, Codex, and Grok (/clear-safe context compression)",
6
6
  "keywords": [
@@ -3,7 +3,7 @@ import { homedir } from 'node:os';
3
3
  import { isAbsolute, join, resolve, sep } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
5
  import { buildBodyRowsFromActiveTurns } from './codex-capture.mjs';
6
- import { isCodexSessionId } from './hosts/identity.mjs';
6
+ import { CLAUDE_HOST, CODEX_HOST, isCodexSessionId } from './hosts/identity.mjs';
7
7
  import { parseCodexRolloutFile } from './codex-rollout-memory.mjs';
8
8
  import { getLogicalTurnGroups } from './transcript-reader.mjs';
9
9
  import { hashAuditorBody, normalizeAuditorBody } from './body-digest.mjs';
@@ -24,7 +24,7 @@ export function defaultAuditorContextDbPath() {
24
24
  export function deriveAuditorFreshnessExpectation({ host, transcriptPath, sessionId } = {}) {
25
25
  assertNonEmptyString(transcriptPath, 'transcriptPath');
26
26
  assertNonEmptyString(sessionId, 'sessionId');
27
- if (host === 'claude') {
27
+ if (host === CLAUDE_HOST) {
28
28
  const latest = getLogicalTurnGroups(transcriptPath).at(-1);
29
29
  if (!latest) return null;
30
30
  return {
@@ -34,7 +34,7 @@ export function deriveAuditorFreshnessExpectation({ host, transcriptPath, sessio
34
34
  expectedAssistantSha256: hashAuditorBody(latest.representative.content),
35
35
  };
36
36
  }
37
- if (host === 'codex') {
37
+ if (host === CODEX_HOST) {
38
38
  const parsed = parseCodexRolloutFile(transcriptPath, { includeInFlightTurn: false });
39
39
  const rows = buildBodyRowsFromActiveTurns(parsed.activeTurns, { sessionId, now: 0 });
40
40
  const latestTurnNumber = rows.reduce((max, row) => Math.max(max, row.turnNumber), 0);
@@ -1,3 +1,4 @@
1
+ import { CLAUDE_HOST, CODEX_HOST } from '../hosts/identity.mjs';
1
2
  import {
2
3
  AUDITOR_CONTEXT_SCHEMA,
3
4
  deriveAuditorFreshnessExpectation,
@@ -53,7 +54,7 @@ export function parseArgs(argv = []) {
53
54
  else if (arg === '--max-body-chars') out.maxBodyChars = parsePositiveInteger(value);
54
55
  else if (arg === '--max-total-chars') out.maxTotalChars = parsePositiveInteger(value);
55
56
  else if (arg === '--db') out.dbPath = value;
56
- else if (arg === '--host' && (value === 'claude' || value === 'codex')) out.host = value;
57
+ else if (arg === '--host' && (value === CLAUDE_HOST || value === CODEX_HOST)) out.host = value;
57
58
  else if (arg === '--transcript') out.transcriptPath = value;
58
59
  else throw new TypeError('unknown option');
59
60
  }
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { existsSync, accessSync, readFileSync, constants, readdirSync, statSync } from 'node:fs';
17
+ import { CODEX_HOST } from '../hosts/identity.mjs';
17
18
  import { join, dirname } from 'node:path';
18
19
  import { homedir } from 'node:os';
19
20
  import { execSync } from 'node:child_process';
@@ -302,9 +303,9 @@ function runTrimDiagnosis(
302
303
  ) {
303
304
  const info = describeTrimHost(host);
304
305
  const codexIdentity =
305
- info.host === 'codex' ? resolveCodexThreadIdentity({ codexThreadId: null }, env) : null;
306
+ info.host === CODEX_HOST ? resolveCodexThreadIdentity({ codexThreadId: null }, env) : null;
306
307
  const hostPrimitiveDiagnosis =
307
- info.host === 'codex' ? readCodexHostPrimitiveDiagnosis({ env, auditRunner }) : null;
308
+ info.host === CODEX_HOST ? readCodexHostPrimitiveDiagnosis({ env, auditRunner }) : null;
308
309
  console.log(`${BOLD}[Trim]${RESET}\n`);
309
310
  console.log(` host: ${info.host}`);
310
311
  console.log(` default keep-recent: ${DEFAULT_TRIM_KEEP_RECENT}`);
@@ -330,19 +331,19 @@ function runTrimDiagnosis(
330
331
  }
331
332
  console.log('');
332
333
  console.log(' dry-run command:');
333
- if (info.host === 'codex' && !codexIdentity?.codexThreadId) {
334
+ if (info.host === CODEX_HOST && !codexIdentity?.codexThreadId) {
334
335
  console.log(' throughline trim --dry-run --host codex --codex-thread-id <id>');
335
336
  console.log(' throughline trim --preflight --host codex --codex-thread-id <id>');
336
- } else if (info.host === 'codex') {
337
+ } else if (info.host === CODEX_HOST) {
337
338
  console.log(' throughline trim --dry-run --host codex');
338
339
  console.log(' throughline trim --preflight --host codex');
339
340
  } else {
340
341
  console.log(` throughline trim --dry-run --host ${info.host}`);
341
342
  }
342
- if (info.host === 'codex') {
343
+ if (info.host === CODEX_HOST) {
343
344
  console.log(' throughline trim --execute --host codex');
344
345
  }
345
- if (info.host === 'codex') {
346
+ if (info.host === CODEX_HOST) {
346
347
  const sessionId = codexIdentity?.codexThreadId
347
348
  ? `codex:${codexIdentity.codexThreadId}`
348
349
  : 'codex:<thread-id>';
package/src/cli/trim.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { runCodexTrimExecution, runCodexTrimPreflight } from '../codex-app-server.mjs';
2
+ import { CODEX_HOST } from '../hosts/identity.mjs';
2
3
  import {
3
4
  buildCodexRolloutTrimSource,
4
5
  parseCodexRolloutFile,
@@ -122,7 +123,7 @@ export async function run(args) {
122
123
  const inflightMemo = parsed.memoStdin ? await readStdin() : null;
123
124
  const db = getDb();
124
125
  const trimSource =
125
- parsed.host === 'codex' && parsed.codexThreadId
126
+ parsed.host === CODEX_HOST && parsed.codexThreadId
126
127
  ? buildCodexRolloutTrimSource({
127
128
  threadId: parsed.codexThreadId,
128
129
  projectPath: process.cwd(),
@@ -376,7 +377,7 @@ async function runPreflight(parsed, plan) {
376
377
  }
377
378
 
378
379
  function validateCodexAction(parsed, plan, action) {
379
- if (parsed.host !== 'codex') {
380
+ if (parsed.host !== CODEX_HOST) {
380
381
  return {
381
382
  status: `${action}-refused`,
382
383
  reason: `${action}_requires_codex_host`,
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { homedir, platform } from 'node:os';
2
+ import { foldPathCaseForPlatform } from './os/paths.mjs';
3
+ import { homedir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
4
5
 
5
6
  import { runCodexTrimExecution } from './codex-app-server.mjs';
@@ -509,8 +510,7 @@ function normalizeProjectPath(value) {
509
510
  if (typeof value !== 'string' || value.length === 0) return '';
510
511
  let result = resolve(value).replace(/\\/g, '/');
511
512
  if (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1);
512
- if (platform() === 'win32') result = result.toLowerCase();
513
- return result;
513
+ return foldPathCaseForPlatform(result);
514
514
  }
515
515
 
516
516
  function numberOrNull(value) {
@@ -1,4 +1,5 @@
1
1
  import { spawnPortableSync } from './os/portable-spawn-sync.mjs';
2
+ import { isWin32Platform } from './os/paths.mjs';
2
3
 
3
4
  export const CODEX_SIDECAR_WORKFLOWS = Object.freeze([
4
5
  'review',
@@ -23,7 +24,7 @@ function resolveCommand({ command, env }) {
23
24
  }
24
25
 
25
26
  export function shouldShellWrapSidecarCommand(platform = process.platform) {
26
- return platform === 'win32';
27
+ return isWin32Platform(platform);
27
28
  }
28
29
 
29
30
  export function runCodexSidecarCommand(command, args, options = {}) {
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
+ import { CLAUDE_HOST } from './hosts/identity.mjs';
2
3
  import {
3
4
  chmodSync,
4
5
  lstatSync,
@@ -8,12 +9,12 @@ import {
8
9
  rmSync,
9
10
  writeFileSync,
10
11
  } from 'node:fs';
11
- import { homedir } from 'node:os';
12
12
  import { dirname, join } from 'node:path';
13
13
  import { DatabaseSync } from 'node:sqlite';
14
14
  import { hashAuditorBody } from './body-digest.mjs';
15
15
  import { normalizeProjectPathForCompare } from './project-path.mjs';
16
16
  import { applyAndVerifyWindowsAcl, isWindows, verifyWindowsAcl } from './os/windows-acl.mjs';
17
+ import { windowsLocalAppData, xdgStateHome } from './os/app-dirs.mjs';
17
18
 
18
19
  export const COMPLETED_TURN_RECEIPT_STORE_SCHEMA = 'throughline.completed_turn_receipts.v1';
19
20
  export const COMPLETED_TURN_RECEIPT_SCHEMA_VERSION = '1.0';
@@ -24,11 +25,10 @@ const PRIVATE_DIRECTORY_CAPABILITY = Symbol('throughline.completed-turn-receipt-
24
25
 
25
26
  export function defaultCompletedTurnReceiptStorePath(projectSha256, env = process.env) {
26
27
  if (!isSha256(projectSha256)) throw new TypeError('projectSha256 must be a SHA-256 digest');
27
- const home = env.HOME || env.USERPROFILE || homedir();
28
28
  if (isWindows(env)) {
29
- return join(env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'throughline', 'completed-turn-receipts', `${projectSha256}.json`);
29
+ return join(windowsLocalAppData(env), 'throughline', 'completed-turn-receipts', `${projectSha256}.json`);
30
30
  }
31
- return join(env.XDG_STATE_HOME || join(home, '.local', 'state'), 'throughline', 'completed-turn-receipts', `${projectSha256}.json`);
31
+ return join(xdgStateHome(env), 'throughline', 'completed-turn-receipts', `${projectSha256}.json`);
32
32
  }
33
33
 
34
34
  /**
@@ -273,7 +273,7 @@ function validateReceipt(receipt, nextSequence) {
273
273
  ];
274
274
  if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) ||
275
275
  !exactKeys(receipt, keys) || receipt.schema_version !== COMPLETED_TURN_RECEIPT_SCHEMA_VERSION ||
276
- receipt.host !== 'claude' || !isSha256(receipt.project_sha256) ||
276
+ receipt.host !== CLAUDE_HOST || !isSha256(receipt.project_sha256) ||
277
277
  !isIdentity(receipt.target_session_id) || !isIdentity(receipt.origin_session_id) ||
278
278
  !isSha256(receipt.user_sha256) || !isSha256(receipt.assistant_sha256) ||
279
279
  !Number.isSafeInteger(receipt.completed_at) || receipt.completed_at < 0 ||
@@ -1,5 +1,5 @@
1
1
  /**
2
- * hosts/identity.mjs — ベンダー (hook host) 識別の唯一の正本
2
+ * hosts/identity.mjs — harness (hook host) 識別の唯一の正本
3
3
  *
4
4
  * Throughline は Claude / Codex / Grok の 3 hook host を同じ SQLite に保存する。
5
5
  * host の見分け方は session_id prefix だけであり、その prefix 定義と判定関数を
@@ -0,0 +1,25 @@
1
+ /**
2
+ * os/app-dirs.mjs — OS別のユーザー設定/状態ベースディレクトリ解決
3
+ *
4
+ * LOCALAPPDATA(Windows)と XDG_CONFIG_HOME / XDG_STATE_HOME(POSIX)の
5
+ * フォールバック組み立てが runtime-error-store と completed-turn-receipts に
6
+ * 別々に書かれていたため、ここへ集約する。最終的なアプリ別 join は呼び出し側が持つ。
7
+ */
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+
11
+ function homeOf(env) {
12
+ return env.HOME || env.USERPROFILE || homedir();
13
+ }
14
+
15
+ export function windowsLocalAppData(env = process.env) {
16
+ return env.LOCALAPPDATA || join(homeOf(env), 'AppData', 'Local');
17
+ }
18
+
19
+ export function xdgConfigHome(env = process.env) {
20
+ return env.XDG_CONFIG_HOME || join(homeOf(env), '.config');
21
+ }
22
+
23
+ export function xdgStateHome(env = process.env) {
24
+ return env.XDG_STATE_HOME || join(homeOf(env), '.local', 'state');
25
+ }
package/src/os/paths.mjs CHANGED
@@ -10,3 +10,7 @@ import { platform } from 'node:os';
10
10
  export function foldPathCaseForPlatform(path, { hostPlatform = platform() } = {}) {
11
11
  return hostPlatform === 'win32' ? path.toLowerCase() : path;
12
12
  }
13
+
14
+ export function isWin32Platform(hostPlatform = platform()) {
15
+ return hostPlatform === 'win32';
16
+ }
@@ -10,12 +10,13 @@ import {
10
10
  writeFileSync,
11
11
  } from 'node:fs';
12
12
  import childProcess from 'node:child_process';
13
- import { arch as hostArch, homedir, platform as hostPlatform } from 'node:os';
13
+ import { arch as hostArch, platform as hostPlatform } from 'node:os';
14
14
  import { dirname, join } from 'node:path';
15
15
  import { createRequire } from 'node:module';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { DatabaseSync } from 'node:sqlite';
18
18
  import { applyAndVerifyWindowsAcl, isWindows, verifyWindowsAcl } from './os/windows-acl.mjs';
19
+ import { windowsLocalAppData, xdgConfigHome, xdgStateHome } from './os/app-dirs.mjs';
19
20
 
20
21
  const require = createRequire(import.meta.url);
21
22
  const PACKAGE_VERSION = require('../package.json').version;
@@ -53,19 +54,17 @@ const DEFINITIONS = Object.freeze({
53
54
  });
54
55
 
55
56
  export function defaultFactoryReporterConfigPath(env = process.env) {
56
- const home = env.HOME || env.USERPROFILE || homedir();
57
57
  if (isWindows(env)) {
58
- return join(env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'dotagents', 'factory-reporter', 'config.json');
58
+ return join(windowsLocalAppData(env), 'dotagents', 'factory-reporter', 'config.json');
59
59
  }
60
- return join(env.XDG_CONFIG_HOME || join(home, '.config'), 'dotagents', 'factory-reporter.json');
60
+ return join(xdgConfigHome(env), 'dotagents', 'factory-reporter.json');
61
61
  }
62
62
 
63
63
  export function defaultRuntimeErrorStorePath(env = process.env) {
64
- const home = env.HOME || env.USERPROFILE || homedir();
65
64
  if (isWindows(env)) {
66
- return join(env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'throughline', 'runtime-errors.json');
65
+ return join(windowsLocalAppData(env), 'throughline', 'runtime-errors.json');
67
66
  }
68
- return join(env.XDG_STATE_HOME || join(home, '.local', 'state'), 'throughline', 'runtime-errors.json');
67
+ return join(xdgStateHome(env), 'throughline', 'runtime-errors.json');
69
68
  }
70
69
 
71
70
  export function isRuntimeErrorCollectionEnabled({ env = process.env, configPath } = {}) {
@@ -1,4 +1,5 @@
1
1
  import { inspectCodexPlannedRollbackRestoreSafety } from './codex-rollout-memory.mjs';
2
+ import { CLAUDE_HOST, CODEX_HOST } from './hosts/identity.mjs';
2
3
  import { buildHandoffRecord, N_RECENT_L2 } from './handoff-record.mjs';
3
4
  import { sameProjectPath } from './project-path.mjs';
4
5
  import { estimateTokens } from './token-estimator.mjs';
@@ -48,7 +49,7 @@ export function findLatestSessionIdForProject(db, projectPath) {
48
49
 
49
50
  function resolveDefaultSessionId({ sessionId, host, codexThreadId, db, projectPath }) {
50
51
  if (sessionId) return sessionId;
51
- if (host === 'codex') {
52
+ if (host === CODEX_HOST) {
52
53
  return codexThreadId ? `codex:${codexThreadId}` : null;
53
54
  }
54
55
  return findLatestSessionIdForProject(db, projectPath);
@@ -79,7 +80,7 @@ function countDistinctCapturedTurns(db, sessionId) {
79
80
  }
80
81
 
81
82
  export function describeTrimHost(host) {
82
- if (host === 'claude') {
83
+ if (host === CLAUDE_HOST) {
83
84
  return {
84
85
  host,
85
86
  automaticRollback: false,
@@ -94,7 +95,7 @@ export function describeTrimHost(host) {
94
95
  };
95
96
  }
96
97
 
97
- if (host === 'codex') {
98
+ if (host === CODEX_HOST) {
98
99
  return {
99
100
  host,
100
101
  automaticRollback: true,
@@ -123,7 +124,7 @@ export function describeTrimHost(host) {
123
124
  }
124
125
 
125
126
  function buildSafeContinuation({ host, hostIdentity }) {
126
- if (host !== 'codex') return null;
127
+ if (host !== CODEX_HOST) return null;
127
128
 
128
129
  const threadId = hostIdentity?.codexThreadId ?? '<thread-id>';
129
130
  const sessionId = threadId === '<thread-id>' ? 'codex:<thread-id>' : `codex:${threadId}`;
@@ -561,7 +562,7 @@ function buildPlanSession({ resolvedSessionId, session, trimSource, projectPath
561
562
  }
562
563
 
563
564
  function buildHostIdentity({ host, codexThreadId, codexThreadIdSource = null }) {
564
- if (host !== 'codex') {
565
+ if (host !== CODEX_HOST) {
565
566
  return {
566
567
  host,
567
568
  codexThreadId: null,