throughline 0.3.24 → 0.4.0
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/.claude/commands/tl.md +6 -21
- package/.codex-sidecar.yml +62 -0
- package/CHANGELOG.md +632 -0
- package/README.ja.md +71 -46
- package/README.md +420 -76
- package/bin/throughline.mjs +169 -7
- package/codex/skills/throughline/SKILL.md +157 -0
- package/codex/skills/throughline/agents/openai.yaml +7 -0
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +159 -0
- package/docs/L1_L2_L3_REDESIGN.md +415 -0
- package/docs/PUBLIC_RELEASE_PLAN.md +185 -0
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +286 -0
- package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
- package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
- package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
- package/docs/archive/CONCEPT.md +476 -0
- package/docs/archive/EXPERIMENT.md +371 -0
- package/docs/archive/README.md +22 -0
- package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
- package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
- package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
- package/docs/throughline-handoff-context.example.json +57 -0
- package/docs/throughline-rollback-context-trim-insight.md +455 -0
- package/package.json +6 -2
- package/src/baton.mjs +17 -45
- package/src/baton.test.mjs +4 -41
- package/src/cli/codex-capture.mjs +95 -0
- package/src/cli/codex-handoff-model-smoke.mjs +292 -0
- package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
- package/src/cli/codex-handoff-smoke.mjs +163 -0
- package/src/cli/codex-handoff-smoke.test.mjs +149 -0
- package/src/cli/codex-handoff-start.mjs +291 -0
- package/src/cli/codex-handoff-start.test.mjs +194 -0
- package/src/cli/codex-hook.mjs +276 -0
- package/src/cli/codex-hook.test.mjs +293 -0
- package/src/cli/codex-host-primitive-audit.mjs +110 -0
- package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
- package/src/cli/codex-restore-smoke.mjs +357 -0
- package/src/cli/codex-restore-source-audit.mjs +304 -0
- package/src/cli/codex-resume.mjs +138 -0
- package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
- package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
- package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
- package/src/cli/codex-sidecar-dry-run.mjs +85 -0
- package/src/cli/codex-summarize.mjs +224 -0
- package/src/cli/codex-threads.mjs +89 -0
- package/src/cli/codex-visibility-smoke.mjs +196 -0
- package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
- package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
- package/src/cli/doctor.mjs +503 -1
- package/src/cli/doctor.test.mjs +542 -3
- package/src/cli/handoff-preview.mjs +78 -0
- package/src/cli/help.test.mjs +64 -0
- package/src/cli/install.mjs +226 -3
- package/src/cli/install.test.mjs +205 -4
- package/src/cli/trim.mjs +564 -0
- package/src/codex-app-server.mjs +1816 -0
- package/src/codex-app-server.test.mjs +512 -0
- package/src/codex-auto-refresh.mjs +194 -0
- package/src/codex-auto-refresh.test.mjs +182 -0
- package/src/codex-capture.mjs +235 -0
- package/src/codex-capture.test.mjs +393 -0
- package/src/codex-handoff-model-smoke.mjs +114 -0
- package/src/codex-handoff-model-smoke.test.mjs +89 -0
- package/src/codex-handoff-smoke.mjs +124 -0
- package/src/codex-handoff-smoke.test.mjs +103 -0
- package/src/codex-handoff.mjs +331 -0
- package/src/codex-handoff.test.mjs +220 -0
- package/src/codex-host-primitive-audit.mjs +374 -0
- package/src/codex-host-primitive-audit.test.mjs +208 -0
- package/src/codex-restore-smoke.test.mjs +639 -0
- package/src/codex-restore-source-audit.mjs +1348 -0
- package/src/codex-restore-source-audit.test.mjs +623 -0
- package/src/codex-resume.test.mjs +242 -0
- package/src/codex-rollout-memory.mjs +711 -0
- package/src/codex-rollout-memory.test.mjs +610 -0
- package/src/codex-sidecar-cli.test.mjs +75 -0
- package/src/codex-sidecar.mjs +246 -0
- package/src/codex-sidecar.test.mjs +172 -0
- package/src/codex-summarize.test.mjs +143 -0
- package/src/codex-thread-identity.mjs +23 -0
- package/src/codex-thread-index.mjs +173 -0
- package/src/codex-thread-index.test.mjs +164 -0
- package/src/codex-usage.mjs +110 -0
- package/src/codex-usage.test.mjs +140 -0
- package/src/codex-visibility-smoke.test.mjs +222 -0
- package/src/codex-vscode-restore-smoke.mjs +206 -0
- package/src/codex-vscode-restore-smoke.test.mjs +325 -0
- package/src/codex-vscode-rollback-smoke.mjs +90 -0
- package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
- package/src/db-schema.test.mjs +96 -0
- package/src/db.mjs +14 -1
- package/src/haiku-summarizer.mjs +267 -26
- package/src/haiku-summarizer.test.mjs +282 -0
- package/src/handoff-preview.test.mjs +108 -0
- package/src/handoff-record.mjs +294 -0
- package/src/handoff-record.test.mjs +226 -0
- package/src/hook-entrypoints.test.mjs +286 -0
- package/src/package-files.test.mjs +19 -0
- package/src/prompt-submit.mjs +9 -6
- package/src/resume-context.mjs +58 -171
- package/src/resume-context.test.mjs +177 -0
- package/src/session-start.mjs +85 -26
- package/src/state-file.mjs +50 -6
- package/src/state-file.test.mjs +50 -0
- package/src/token-monitor.mjs +14 -10
- package/src/token-monitor.test.mjs +27 -0
- package/src/trim-cli.test.mjs +1584 -0
- package/src/trim-model.mjs +584 -0
- package/src/trim-model.test.mjs +568 -0
- package/src/turn-processor.mjs +17 -10
- package/src/vscode-task.mjs +33 -10
- package/src/vscode-task.test.mjs +19 -9
- package/src/cli/save-inflight.mjs +0 -81
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
async function withIsolatedDb(testFn) {
|
|
8
|
+
const home = mkdtempSync(join(tmpdir(), 'tl-db-schema-'));
|
|
9
|
+
const originalHome = process.env.HOME;
|
|
10
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
11
|
+
process.env.HOME = home;
|
|
12
|
+
process.env.USERPROFILE = home;
|
|
13
|
+
try {
|
|
14
|
+
const mod = await import(`./db.mjs?isolated=${Date.now()}-${Math.random()}`);
|
|
15
|
+
const db = mod.getDb();
|
|
16
|
+
try {
|
|
17
|
+
await testFn(db);
|
|
18
|
+
} finally {
|
|
19
|
+
db.close();
|
|
20
|
+
}
|
|
21
|
+
} finally {
|
|
22
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
23
|
+
else process.env.HOME = originalHome;
|
|
24
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
25
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
26
|
+
rmSync(home, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function columnNames(db, table) {
|
|
31
|
+
return db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function indexNames(db) {
|
|
35
|
+
return db
|
|
36
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' ORDER BY name")
|
|
37
|
+
.all()
|
|
38
|
+
.map((row) => row.name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('schema v8 preserves Claude-facing tables, fields, and unique indexes', async () => {
|
|
42
|
+
await withIsolatedDb((db) => {
|
|
43
|
+
const version = db.prepare('PRAGMA user_version').get();
|
|
44
|
+
assert.equal(version.user_version, 8);
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(columnNames(db, 'sessions'), [
|
|
47
|
+
'session_id',
|
|
48
|
+
'project_path',
|
|
49
|
+
'status',
|
|
50
|
+
'created_at',
|
|
51
|
+
'updated_at',
|
|
52
|
+
'merged_into',
|
|
53
|
+
]);
|
|
54
|
+
assert.deepEqual(columnNames(db, 'skeletons'), [
|
|
55
|
+
'id',
|
|
56
|
+
'session_id',
|
|
57
|
+
'turn_number',
|
|
58
|
+
'role',
|
|
59
|
+
'summary',
|
|
60
|
+
'created_at',
|
|
61
|
+
'origin_session_id',
|
|
62
|
+
]);
|
|
63
|
+
assert.deepEqual(columnNames(db, 'bodies'), [
|
|
64
|
+
'id',
|
|
65
|
+
'session_id',
|
|
66
|
+
'origin_session_id',
|
|
67
|
+
'turn_number',
|
|
68
|
+
'role',
|
|
69
|
+
'text',
|
|
70
|
+
'token_count',
|
|
71
|
+
'created_at',
|
|
72
|
+
]);
|
|
73
|
+
assert.deepEqual(columnNames(db, 'details'), [
|
|
74
|
+
'id',
|
|
75
|
+
'session_id',
|
|
76
|
+
'turn_number',
|
|
77
|
+
'tool_name',
|
|
78
|
+
'input_text',
|
|
79
|
+
'output_text',
|
|
80
|
+
'token_count',
|
|
81
|
+
'created_at',
|
|
82
|
+
'origin_session_id',
|
|
83
|
+
'kind',
|
|
84
|
+
'source_id',
|
|
85
|
+
]);
|
|
86
|
+
assert.deepEqual(columnNames(db, 'handoff_batons'), [
|
|
87
|
+
'project_path',
|
|
88
|
+
'session_id',
|
|
89
|
+
'created_at',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const indexes = indexNames(db);
|
|
93
|
+
assert.ok(indexes.includes('uq_skeletons_turn_v3'));
|
|
94
|
+
assert.ok(indexes.includes('uq_details_source'));
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/db.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { join } from 'path';
|
|
|
9
9
|
|
|
10
10
|
const DB_DIR = join(homedir(), '.throughline');
|
|
11
11
|
const DB_PATH = join(DB_DIR, 'throughline.db');
|
|
12
|
-
const CURRENT_VERSION =
|
|
12
|
+
const CURRENT_VERSION = 8;
|
|
13
13
|
|
|
14
14
|
let _db = null;
|
|
15
15
|
|
|
@@ -202,6 +202,19 @@ function initSchema(db) {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// v7 → v8: handoff_batons から memo_text 列を drop。
|
|
206
|
+
// 新仕様 (docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md) で memo 廃止:
|
|
207
|
+
// - /clear 自動引継ぎ (SessionStart source='clear') + /tl baton (memo なし) の 2 経路に
|
|
208
|
+
// - 注入は L1 + L2 + L3 refs のみ
|
|
209
|
+
// - save-inflight CLI / updateBatonMemo 関数も併せて削除
|
|
210
|
+
// SQLite 3.35.0+ で DROP COLUMN サポート (Node.js v22.5+ 同梱版で利用可)。
|
|
211
|
+
if (version < 8) {
|
|
212
|
+
const batonCols = db.prepare('PRAGMA table_info(handoff_batons)').all();
|
|
213
|
+
if (batonCols.some((c) => c.name === 'memo_text')) {
|
|
214
|
+
db.exec('ALTER TABLE handoff_batons DROP COLUMN memo_text');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
205
218
|
if (version < CURRENT_VERSION) {
|
|
206
219
|
db.exec(`PRAGMA user_version = ${CURRENT_VERSION}`);
|
|
207
220
|
}
|
package/src/haiku-summarizer.mjs
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* haiku-summarizer.mjs —
|
|
2
|
+
* haiku-summarizer.mjs — L1 要約生成
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 基本方針:
|
|
5
|
+
* - Claude primary では、codex-sidecar diagnostics が configured なら Codex sidecar で
|
|
6
|
+
* L2→L1 要約する。
|
|
7
|
+
* - Claude primary では、codex-sidecar が disabled / unavailable なら現行の Claude
|
|
8
|
+
* Haiku 要約に戻す。
|
|
9
|
+
* - Claude primary では、どちらも失敗したら L2 全文を L1 に入れる(情報欠損ゼロ)。
|
|
10
|
+
* - Codex primary では、Codex CLI backend を使い、失敗時は Haiku / raw L2 へ
|
|
11
|
+
* fallback せず explicit error にする。
|
|
7
12
|
*
|
|
8
|
-
*
|
|
13
|
+
* Claude Haiku 経路:
|
|
14
|
+
* Claude Max 契約前提。`claude -p --model claude-haiku-4-5-20251001`
|
|
15
|
+
* を子プロセス起動する。Anthropic API キーは使わない(Claude Code CLI が
|
|
16
|
+
* Max 契約の認証を持っている前提)。
|
|
17
|
+
*
|
|
18
|
+
* 【Haiku 再帰暴走の根本対策: 隔離 cwd で spawn】
|
|
9
19
|
* 素朴に `claude -p` を spawn すると subprocess が同じ .claude/settings.json を
|
|
10
20
|
* 読んで Throughline の Stop hook を起動し、無限再帰になる。
|
|
11
21
|
*
|
|
@@ -27,14 +37,22 @@
|
|
|
27
37
|
*/
|
|
28
38
|
|
|
29
39
|
import { spawnSync } from 'child_process';
|
|
30
|
-
import { mkdirSync } from 'fs';
|
|
40
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
31
41
|
import { join } from 'path';
|
|
32
|
-
import { homedir } from 'os';
|
|
42
|
+
import { homedir, tmpdir } from 'os';
|
|
43
|
+
import {
|
|
44
|
+
diagnoseCodexSidecar,
|
|
45
|
+
CODEX_SIDECAR_STATUS,
|
|
46
|
+
runCodexSidecarCommand,
|
|
47
|
+
} from './codex-sidecar.mjs';
|
|
33
48
|
|
|
34
49
|
const MODEL = 'claude-haiku-4-5-20251001';
|
|
35
50
|
const MAX_RETRIES = 2;
|
|
36
51
|
const TIMEOUT_MS = 30_000;
|
|
52
|
+
const SIDECAR_TIMEOUT_MS = 10 * 60_000;
|
|
53
|
+
const CODEX_CLI_TIMEOUT_MS = 60_000;
|
|
37
54
|
const RECURSION_GUARD_ENV = 'THROUGHLINE_IN_HAIKU_SUBPROCESS';
|
|
55
|
+
const CODEX_SUMMARIZER_GUARD_ENV = 'THROUGHLINE_IN_CODEX_SUMMARIZER';
|
|
38
56
|
|
|
39
57
|
// 隔離 cwd: Throughline project-local 設定が見つからない空ディレクトリ
|
|
40
58
|
const HAIKU_WORKDIR = join(homedir(), '.throughline', 'haiku-workdir');
|
|
@@ -47,29 +65,141 @@ function ensureWorkdir() {
|
|
|
47
65
|
}
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
function buildPrompt(l2Text) {
|
|
69
|
+
const targetChars = Math.max(20, Math.round(l2Text.length / 5));
|
|
70
|
+
return (
|
|
71
|
+
`次の日本語テキストを約${targetChars}文字に要約してください。` +
|
|
72
|
+
`固有名詞・数値・因果関係を優先して残し、枝葉は落としてください。` +
|
|
73
|
+
`要約文だけを出力し、前置きや説明は不要です。`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildCodexPrompt(l2Text) {
|
|
78
|
+
return (
|
|
79
|
+
`${buildPrompt(l2Text)}\n\n` +
|
|
80
|
+
'Output contract:\n' +
|
|
81
|
+
'- Return only the summary text.\n' +
|
|
82
|
+
'- Do not include Markdown fences, JSON, labels, or commentary.\n\n' +
|
|
83
|
+
'Text to summarize is provided on stdin.'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function compactSubprocessStderr(stderr) {
|
|
88
|
+
if (!stderr) return '';
|
|
89
|
+
const compacted = String(stderr)
|
|
90
|
+
.split('\n')
|
|
91
|
+
.map((line) => (line.length > 600 ? `${line.slice(0, 600)} ...[line truncated]` : line))
|
|
92
|
+
.join('\n');
|
|
93
|
+
if (compacted.length <= 6_000) return compacted;
|
|
94
|
+
return `${compacted.slice(0, 1_500)}\n...[stderr truncated]...\n${compacted.slice(-3_500)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseSidecarSummary(stdout) {
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(stdout);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (parsed?.status && !['ok', 'completed'].includes(parsed.status)) return null;
|
|
105
|
+
if (typeof parsed.summary === 'string' && parsed.summary.trim()) {
|
|
106
|
+
return parsed.summary.trim();
|
|
58
107
|
}
|
|
108
|
+
if (typeof parsed.recommendation === 'string' && parsed.recommendation.trim()) {
|
|
109
|
+
return parsed.recommendation.trim();
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
59
113
|
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
return { summary:
|
|
114
|
+
function tryCodexSidecarSummary(l2Text, { projectPath, prompt, env }) {
|
|
115
|
+
if (!projectPath) {
|
|
116
|
+
return { summary: null, reason: 'missing_project_path' };
|
|
63
117
|
}
|
|
64
118
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
119
|
+
const diagnostics = diagnoseCodexSidecar({
|
|
120
|
+
projectPath,
|
|
121
|
+
preset: 'summarize-l1',
|
|
122
|
+
env,
|
|
123
|
+
timeoutMs: SIDECAR_TIMEOUT_MS,
|
|
124
|
+
});
|
|
125
|
+
if (diagnostics.status !== CODEX_SIDECAR_STATUS.CONFIGURED) {
|
|
126
|
+
return {
|
|
127
|
+
summary: null,
|
|
128
|
+
reason: `sidecar_${diagnostics.status}`,
|
|
129
|
+
diagnostics,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const contextDir = mkdtempSync(join(tmpdir(), 'throughline-l1-context-'));
|
|
134
|
+
const contextFile = join(contextDir, 'context.json');
|
|
135
|
+
try {
|
|
136
|
+
writeFileSync(
|
|
137
|
+
contextFile,
|
|
138
|
+
JSON.stringify(
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
kind: 'manual_note',
|
|
142
|
+
source: 'throughline:l2-turn',
|
|
143
|
+
trust: 'local',
|
|
144
|
+
summary: 'Throughline L2 turn text to summarize into L1 memory.',
|
|
145
|
+
data: {
|
|
146
|
+
text: l2Text,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
null,
|
|
151
|
+
2,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const command = env.THROUGHLINE_CODEX_SIDECAR_BIN ?? 'codex-sidecar';
|
|
156
|
+
const result = runCodexSidecarCommand(
|
|
157
|
+
command,
|
|
158
|
+
[
|
|
159
|
+
'explore',
|
|
160
|
+
'--project',
|
|
161
|
+
projectPath,
|
|
162
|
+
'--preset',
|
|
163
|
+
'summarize-l1',
|
|
164
|
+
'--context-file',
|
|
165
|
+
contextFile,
|
|
166
|
+
'--turn-timeout-ms',
|
|
167
|
+
String(SIDECAR_TIMEOUT_MS),
|
|
168
|
+
prompt,
|
|
169
|
+
],
|
|
170
|
+
{
|
|
171
|
+
encoding: 'utf8',
|
|
172
|
+
env,
|
|
173
|
+
timeout: SIDECAR_TIMEOUT_MS + 5_000,
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (result.status !== 0 || !result.stdout) {
|
|
178
|
+
return {
|
|
179
|
+
summary: null,
|
|
180
|
+
reason: 'sidecar_run_failed',
|
|
181
|
+
exitCode: result.status,
|
|
182
|
+
stderr: result.stderr ?? '',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const summary = parseSidecarSummary(result.stdout);
|
|
187
|
+
if (!summary) {
|
|
188
|
+
return {
|
|
189
|
+
summary: null,
|
|
190
|
+
reason: 'sidecar_summary_missing',
|
|
191
|
+
stdout: result.stdout,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { summary, reason: 'sidecar_ok' };
|
|
195
|
+
} finally {
|
|
196
|
+
rmSync(contextDir, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
70
199
|
|
|
200
|
+
function summarizeWithHaiku(l2Text, prompt, env) {
|
|
71
201
|
// child_process に渡す env: 親の env を継承しつつ再帰ガードをセット
|
|
72
|
-
const childEnv = { ...
|
|
202
|
+
const childEnv = { ...env, [RECURSION_GUARD_ENV]: '1' };
|
|
73
203
|
|
|
74
204
|
// 隔離 cwd を準備(project-local .claude/settings.json が見えない場所)
|
|
75
205
|
ensureWorkdir();
|
|
@@ -87,7 +217,7 @@ export function summarizeToL1(l2Text) {
|
|
|
87
217
|
|
|
88
218
|
if (result.status === 0 && result.stdout) {
|
|
89
219
|
const summary = result.stdout.trim();
|
|
90
|
-
if (summary) return { summary, fromFallback: false };
|
|
220
|
+
if (summary) return { summary, fromFallback: false, source: 'haiku' };
|
|
91
221
|
}
|
|
92
222
|
// status != 0 や空出力は失敗とみなしてリトライ
|
|
93
223
|
} catch {
|
|
@@ -96,5 +226,116 @@ export function summarizeToL1(l2Text) {
|
|
|
96
226
|
}
|
|
97
227
|
|
|
98
228
|
// 全リトライ失敗 → L2 全文をそのまま L1 に(情報欠損ゼロ)
|
|
99
|
-
return { summary: l2Text, fromFallback: true };
|
|
229
|
+
return { summary: l2Text, fromFallback: true, source: 'raw_l2' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function summarizeWithCodexCli(l2Text, { projectPath, env }) {
|
|
233
|
+
if (!projectPath) {
|
|
234
|
+
const err = new Error('Codex CLI summarizer requires projectPath');
|
|
235
|
+
err.source = 'codex-cli';
|
|
236
|
+
err.reason = 'missing_project_path';
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (env[CODEX_SUMMARIZER_GUARD_ENV] === '1') {
|
|
241
|
+
const err = new Error('Codex CLI summarizer recursion guard');
|
|
242
|
+
err.source = 'codex-cli';
|
|
243
|
+
err.reason = 'recursion_guard';
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const command = env.THROUGHLINE_CODEX_CLI_BIN ?? 'codex';
|
|
248
|
+
const prompt = buildCodexPrompt(l2Text);
|
|
249
|
+
const childEnv = { ...env, [CODEX_SUMMARIZER_GUARD_ENV]: '1' };
|
|
250
|
+
const result = spawnSync(
|
|
251
|
+
command,
|
|
252
|
+
[
|
|
253
|
+
'exec',
|
|
254
|
+
'--ephemeral',
|
|
255
|
+
'--ignore-user-config',
|
|
256
|
+
'--ignore-rules',
|
|
257
|
+
'--skip-git-repo-check',
|
|
258
|
+
'--sandbox',
|
|
259
|
+
'read-only',
|
|
260
|
+
'-C',
|
|
261
|
+
projectPath,
|
|
262
|
+
prompt,
|
|
263
|
+
],
|
|
264
|
+
{
|
|
265
|
+
input: l2Text,
|
|
266
|
+
encoding: 'utf8',
|
|
267
|
+
timeout: CODEX_CLI_TIMEOUT_MS,
|
|
268
|
+
shell: process.platform === 'win32',
|
|
269
|
+
env: childEnv,
|
|
270
|
+
cwd: projectPath,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (result.status !== 0) {
|
|
275
|
+
const err = new Error(`Codex CLI summarizer failed: exit ${result.status ?? 'unknown'}`);
|
|
276
|
+
err.source = 'codex-cli';
|
|
277
|
+
err.reason = 'codex_cli_failed';
|
|
278
|
+
err.exitCode = result.status;
|
|
279
|
+
err.stderr = compactSubprocessStderr(result.stderr);
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const summary = result.stdout?.trim();
|
|
284
|
+
if (!summary) {
|
|
285
|
+
const err = new Error('Codex CLI summarizer returned empty output');
|
|
286
|
+
err.source = 'codex-cli';
|
|
287
|
+
err.reason = 'empty_output';
|
|
288
|
+
err.stderr = compactSubprocessStderr(result.stderr);
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { summary, fromFallback: false, source: 'codex-cli' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* L2 本文を約 1/5 に要約する。
|
|
297
|
+
* @param {string} l2Text ターンの会話本文(user+assistant を適当な形式で結合した文字列)
|
|
298
|
+
* @param {{ projectPath?: string, env?: NodeJS.ProcessEnv, hostMode?: 'claude-primary' | 'codex-primary' | 'unknown' }} [options]
|
|
299
|
+
* @returns {{ summary: string, fromFallback: boolean, source?: string, sidecarReason?: string }}
|
|
300
|
+
*/
|
|
301
|
+
export function summarizeToL1(
|
|
302
|
+
l2Text,
|
|
303
|
+
{ projectPath = null, env = process.env, hostMode = 'unknown' } = {},
|
|
304
|
+
) {
|
|
305
|
+
if (!l2Text || !l2Text.trim()) {
|
|
306
|
+
return { summary: '(no content)', fromFallback: true, source: 'empty' };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (hostMode === 'codex-primary') {
|
|
310
|
+
return summarizeWithCodexCli(l2Text, { projectPath, env });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (hostMode !== 'claude-primary') {
|
|
314
|
+
const err = new Error('summarizeToL1 requires hostMode claude-primary or codex-primary');
|
|
315
|
+
err.source = 'unknown';
|
|
316
|
+
err.reason = 'unknown_host_mode';
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 防御(念のため): 自分自身が Haiku subprocess 内で呼ばれていたら再帰せず即フォールバック
|
|
321
|
+
if (env[RECURSION_GUARD_ENV] === '1') {
|
|
322
|
+
return { summary: l2Text, fromFallback: true, source: 'recursion_guard' };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const prompt = buildPrompt(l2Text);
|
|
326
|
+
const sidecar = tryCodexSidecarSummary(l2Text, { projectPath, prompt, env });
|
|
327
|
+
if (sidecar.summary) {
|
|
328
|
+
return {
|
|
329
|
+
summary: sidecar.summary,
|
|
330
|
+
fromFallback: false,
|
|
331
|
+
source: 'codex-sidecar',
|
|
332
|
+
sidecarReason: sidecar.reason,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const haiku = summarizeWithHaiku(l2Text, prompt, env);
|
|
337
|
+
return {
|
|
338
|
+
...haiku,
|
|
339
|
+
sidecarReason: sidecar.reason,
|
|
340
|
+
};
|
|
100
341
|
}
|