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.
- package/CHANGELOG.md +69 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/docs/adr/0033-claude-auto-handoff-new-session.md +8 -0
- package/docs/adr/0057-claude-aiterm-seat-conversation-is-not-handed-off.md +88 -0
- package/docs/adr/0059-codex-handoff-carries-thread-goal.md +96 -0
- package/package.json +1 -1
- package/src/claude-auto-handoff.mjs +40 -2
- package/src/claude-auto-handoff.test.mjs +118 -0
- package/src/claude-pre-compact.mjs +2 -1
- package/src/cli/auto-handoff.mjs +9 -5
- package/src/cli/grok-continue.test.mjs +9 -4
- package/src/cli/runtime-errors.test.mjs +2 -3
- package/src/codex-auto-handoff.mjs +137 -5
- package/src/codex-auto-handoff.test.mjs +255 -3
- package/src/haiku-summarizer.test.mjs +8 -1
- package/src/hosts/codex-handoff-state.mjs +20 -0
- package/src/room-context.test.mjs +2 -1
- package/src/runtime-error-hook.test.mjs +3 -3
- package/src/runtime-error-report.test.mjs +3 -3
- package/src/runtime-error-store.test.mjs +3 -3
- package/src/spike-transcript-writer.test.mjs +3 -3
- package/src/test-temp-dir.mjs +24 -0
- package/src/transcript-reader.test.mjs +3 -3
|
@@ -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 {
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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;
|