throughline 0.16.26 → 0.16.28

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.
@@ -3,7 +3,14 @@ import assert from 'node:assert/strict';
3
3
  import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { delimiter, join } from 'node:path';
6
- import { summarizeToL1, resolveL1Ratio } from './haiku-summarizer.mjs';
6
+ import { testTempDir } from './test-temp-dir.mjs';
7
+
8
+ // haiku-summarizer は import の時に homedir() から隔離 cwd の道を決める。
9
+ // 試験が動かす利用者の本物の home へ ~/.throughline を作らないよう、先に home を差し替える。
10
+ const TEST_HOME = testTempDir('tl-l1-home-');
11
+ process.env.HOME = TEST_HOME;
12
+ process.env.USERPROFILE = TEST_HOME;
13
+ const { summarizeToL1, resolveL1Ratio } = await import('./haiku-summarizer.mjs');
7
14
 
8
15
  function makeBin(dir, name, body) {
9
16
  const script = join(dir, `${name}.mjs`);
@@ -95,6 +95,11 @@ export function threadStartSettings(settings) {
95
95
  ...permissions, config: { model_reasoning_effort: settings.effort } };
96
96
  }
97
97
 
98
+ // Codex が自分でターンへ入れる文脈の頭(`codex-rs/core/src/context/internal_model_context.rs` の `CONTEXT_START_MARKER`)。
99
+ const CODEX_INTERNAL_CONTEXT_PATTERN = /^\s*<codex_internal_context[\s>]/;
100
+ // Codex がターンの途中で足す環境の知らせ(日付や作業フォルダが変わった時)。入力の出どころには数えない。
101
+ const CODEX_ENVIRONMENT_CONTEXT_PATTERN = /^\s*<environment_context>/;
102
+
98
103
  export function readCodexHandoffState(path, { threadId = null, turnId = null, deliveryText = null } = {}) {
99
104
  const rows = decodeRows(path);
100
105
  let meta = null, rawSettings = null, preparedSettings = null, context = null, latestTurnId = null;
@@ -110,6 +115,15 @@ export function readCodexHandoffState(path, { threadId = null, turnId = null, de
110
115
  // activity はモデルの発言・道具の呼び出し・圧縮・完了のどれかがあったか。入力を受けただけで止まった turn は activity が false。
111
116
  const laterTurns = [];
112
117
  let sourceSeen = false, later = null;
118
+ // 見ている turn(turnId を指定した時はその turn、しない時は最後の turn)の入力の出どころ。Codex が自分で入れる文脈
119
+ // (ゴールの続行の指示など。`<codex_internal_context …>` で始まる)と、それ以外の入力(人やほかの製品が送った物)を分ける。
120
+ const inputKinds = { turn: { internal: false, other: false }, pending: { internal: false, other: false } };
121
+ const noteInput = text => {
122
+ if (CODEX_ENVIRONMENT_CONTEXT_PATTERN.test(text)) return;
123
+ const watched = !turnId || latestTurnId === turnId;
124
+ const kinds = turnOpen ? (watched ? inputKinds.turn : null) : inputKinds.pending;
125
+ if (kinds) kinds[CODEX_INTERNAL_CONTEXT_PATTERN.test(text) ? 'internal' : 'other'] = true;
126
+ };
113
127
  for (const row of rows) {
114
128
  const payload = row.payload;
115
129
  if (later && (row.type === 'compacted' || row.type === 'token_usage_record')) later.activity = true;
@@ -126,6 +140,8 @@ export function readCodexHandoffState(path, { threadId = null, turnId = null, de
126
140
  if (turnId && latestTurnId === turnId) sourceSeen = true;
127
141
  else if (turnId && sourceSeen) { later = { turnId: latestTurnId, closed: false, activity: false }; laterTurns.push(later); }
128
142
  hasTurnInput = pendingUser != null;
143
+ if (!turnId || latestTurnId === turnId) inputKinds.turn = inputKinds.pending;
144
+ inputKinds.pending = { internal: false, other: false };
129
145
  if (!turnId || latestTurnId === turnId) {
130
146
  turnStartAt = Date.parse(row.timestamp); pendingCalls.clear(); completedTools = 0;
131
147
  stoppedAt = null; stoppedReason = null; completedAt = null;
@@ -158,6 +174,7 @@ export function readCodexHandoffState(path, { threadId = null, turnId = null, de
158
174
  const text = (payload.content ?? []).filter(c => typeof c.text === 'string').map(c => c.text).join('\n');
159
175
  const input = text.trim() && !text.startsWith('# AGENTS.md instructions') && !text.startsWith('<hook_prompt');
160
176
  if (input) {
177
+ noteInput(text);
161
178
  if (turnOpen) hasTurnInput = true;
162
179
  else pendingUser = text;
163
180
  }
@@ -193,6 +210,7 @@ export function readCodexHandoffState(path, { threadId = null, turnId = null, de
193
210
  else pendingUser = payload.message;
194
211
  }
195
212
  if (row.type === 'event_msg' && payload?.type === 'user_message') {
213
+ noteInput(typeof payload.message === 'string' ? payload.message : '');
196
214
  if (turnOpen) hasTurnInput = true;
197
215
  else pendingUser = payload.message;
198
216
  }
@@ -218,5 +236,7 @@ export function readCodexHandoffState(path, { threadId = null, turnId = null, de
218
236
  return { meta, settings, rawSettings, preparedSettings, context, latestTurnId, turnStartAt, stoppedAt, stoppedReason,
219
237
  completedAt, correlatedTurnId, hasTurnInput, progress, completedTools, pendingCallCount: pendingCalls.size,
220
238
  pendingNativeSessionCount: nativeSessions.size, inheritedThroughlineMemory, inheritedThroughlineMemoryText, laterTurns,
239
+ // 見ている turn の入力が、Codex が自分で入れた文脈だけだった(人の入力ではない)。
240
+ internalInputOnly: inputKinds.turn.internal && !inputKinds.turn.other,
221
241
  compactedRows: rows.filter(r => r.type === 'compacted').length };
222
242
  }
@@ -11,7 +11,8 @@ const bin = fileURLToPath(new URL('../bin/throughline.mjs', import.meta.url));
11
11
  test('公開CLIは部屋ごとの直近3発言を現在の発言まで返す', () => {
12
12
  const home = mkdtempSync(join(tmpdir(), 'tl-room-context-'));
13
13
  const run = (input) => spawnSync(process.execPath, [bin, 'room-context', '--json'], {
14
- env: { ...process.env, HOME: home }, input: JSON.stringify(input), encoding: 'utf8',
14
+ // Windows は USERPROFILE で home を決める。HOME だけだと、動かす利用者の本物の DB を開く。
15
+ env: { ...process.env, HOME: home, USERPROFILE: home }, input: JSON.stringify(input), encoding: 'utf8',
15
16
  });
16
17
  const base = { projectPath: '/srv/bellteam', roomId: 'room-a', speaker: 'クオ', text: '発言' };
17
18
  try {
@@ -1,10 +1,10 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { execFileSync, spawnSync } from 'node:child_process';
4
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
5
  import { DatabaseSync } from 'node:sqlite';
6
- import { tmpdir } from 'node:os';
7
6
  import { dirname, join } from 'node:path';
7
+ import { testTempDir } from './test-temp-dir.mjs';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { defaultRuntimeErrorConfigPath, defaultRuntimeErrorStorePath } from './runtime-error-store.mjs';
10
10
  import { applyWindowsPrivateAcl } from './os/windows-acl-test-helper.mjs';
@@ -12,7 +12,7 @@ import { applyWindowsPrivateAcl } from './os/windows-acl-test-helper.mjs';
12
12
  const BIN = fileURLToPath(new URL('../bin/throughline.mjs', import.meta.url));
13
13
 
14
14
  function createEnabledEnvironment(prefix) {
15
- const root = mkdtempSync(join(tmpdir(), prefix));
15
+ const root = testTempDir(prefix);
16
16
  const env = {
17
17
  ...process.env,
18
18
  HOME: root,
@@ -2,10 +2,10 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { spawn, spawnSync } from 'node:child_process';
4
4
  import { createHash, createHmac } from 'node:crypto';
5
- import { chmodSync, existsSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
5
+ import { chmodSync, existsSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
6
6
  import { createServer } from 'node:http';
7
- import { tmpdir } from 'node:os';
8
7
  import { join } from 'node:path';
8
+ import { testTempDir } from './test-temp-dir.mjs';
9
9
  import { fileURLToPath } from 'node:url';
10
10
 
11
11
  import {
@@ -37,7 +37,7 @@ const KEY_ID = 'test-host.throughline';
37
37
  const NOW = Date.parse('2026-10-03T08:00:00.000Z');
38
38
 
39
39
  function createEnvironment(prefix = 'throughline-report-') {
40
- const root = mkdtempSync(join(tmpdir(), prefix));
40
+ const root = testTempDir(prefix);
41
41
  const env = {
42
42
  ...process.env,
43
43
  HOME: root,
@@ -1,9 +1,9 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import childProcess, { spawn } from 'node:child_process';
4
- import { chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
5
- import { tmpdir } from 'node:os';
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
6
5
  import { dirname, join } from 'node:path';
6
+ import { testTempDir } from './test-temp-dir.mjs';
7
7
 
8
8
  import {
9
9
  acknowledgeRuntimeErrors,
@@ -22,7 +22,7 @@ import { applyWindowsPrivateAcl, verifyWindowsPrivateAcl } from './os/windows-ac
22
22
  const TEST_PLATFORM = process.platform === 'win32' ? 'win32' : 'darwin';
23
23
 
24
24
  function sandbox() {
25
- const root = mkdtempSync(join(tmpdir(), 'throughline-runtime-errors-'));
25
+ const root = testTempDir('throughline-runtime-errors-');
26
26
  const env = {
27
27
  HOME: root,
28
28
  USERPROFILE: root,
@@ -1,9 +1,9 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
- import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
5
- import { tmpdir } from 'node:os';
4
+ import { readFileSync, writeFileSync } from 'node:fs';
6
5
  import { join } from 'node:path';
6
+ import { testTempDir } from './test-temp-dir.mjs';
7
7
  import { spikeInject, generateSpikeTracer } from './spike-transcript-writer.mjs';
8
8
 
9
9
  function makeDb() {
@@ -76,7 +76,7 @@ function seedSession(db, { newSessionId = 'NEW', originId = 'ORIG' } = {}) {
76
76
  }
77
77
 
78
78
  function makeTempJsonl() {
79
- const dir = mkdtempSync(join(tmpdir(), 'spike-writer-test-'));
79
+ const dir = testTempDir('spike-writer-test-');
80
80
  return join(dir, 'session.jsonl');
81
81
  }
82
82
 
@@ -0,0 +1,24 @@
1
+ // 試験が作る一時フォルダを、その試験 file の process が終わる時に消す。
2
+ // CI のランナーは使い捨てではないので、消さないと回ごとに OS の一時フォルダへ積もる
3
+ // (rabbit の /tmp に 3,026 個。2026-10-13 の計測)。
4
+ import { mkdtempSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ const created = [];
9
+
10
+ process.on('exit', () => {
11
+ for (const dir of created) {
12
+ try {
13
+ rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
14
+ } catch {
15
+ // 開いたままの file が残る時(Windows)は消せない。試験の成否は変えない。
16
+ }
17
+ }
18
+ });
19
+
20
+ export function testTempDir(prefix) {
21
+ const dir = mkdtempSync(join(tmpdir(), prefix));
22
+ created.push(dir);
23
+ return dir;
24
+ }
@@ -1,8 +1,8 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, writeFileSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
3
+ import { writeFileSync } from 'node:fs';
5
4
  import { join } from 'node:path';
5
+ import { testTempDir } from './test-temp-dir.mjs';
6
6
  import {
7
7
  normalizeTerminalText,
8
8
  normalizeToolResultContent,
@@ -36,7 +36,7 @@ test('normalizeTerminalText: CRLF を LF にし、行内 CR は最後の上書
36
36
  });
37
37
 
38
38
  function writeTranscript(entries) {
39
- const dir = mkdtempSync(join(tmpdir(), 'tl-transcript-'));
39
+ const dir = testTempDir('tl-transcript-');
40
40
  const path = join(dir, 't.jsonl');
41
41
  writeFileSync(path, entries.map((e) => JSON.stringify(e)).join('\n') + '\n');
42
42
  return path;