throughline 0.10.17 → 0.10.18
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/.codex-sidecar.yml +0 -1
- package/CHANGELOG.md +14 -1
- package/docs/00_overview.md +1 -2
- package/docs/04_public_release_plan.md +2 -2
- package/docs/05_codex_first_roadmap.md +3 -3
- package/package.json +1 -1
- package/src/docs-contract.test.mjs +1 -1
- package/src/hosts/claude.mjs +19 -0
- package/src/runtime-error-store.mjs +1 -0
- package/src/runtime-error-store.test.mjs +12 -0
- package/src/transcript-reader.mjs +30 -8
- package/src/transcript-reader.test.mjs +76 -5
package/.codex-sidecar.yml
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.10.18] — 2026-09-23
|
|
14
|
+
|
|
15
|
+
### 修正
|
|
16
|
+
|
|
17
|
+
- runtime snapshotに実発生時の`product_version`を公開し、取得時の導入版と区別する。snapshotによる件数・時刻・発生版の変更は行わない。
|
|
18
|
+
- L2のuser本文から端末制御を落とす。色・cursor移動・private mode・OSC(Windows ConPTYのtitle等)を除き、CRLFはLFに、行内のCR上書きは最後の表示だけにする。L3のtool出力にも同じ処理を使う。
|
|
19
|
+
- Claude Codeの背景task通知は、状態・要約・Monitorのevent・subagentのresultだけを記憶に残す。識別子・出力path・定型の注記と、入力待ち時に付く端末の生出力は落とす。
|
|
20
|
+
|
|
21
|
+
### 変更
|
|
22
|
+
|
|
23
|
+
- npm公開をTrusted Publishingへ移した。既定ブランチへ着地したrelease commitに`v<version>` tagをpushすると、`.github/workflows/publish.yml`が公開する。
|
|
24
|
+
|
|
13
25
|
## [0.10.17] — 2026-09-12
|
|
14
26
|
|
|
15
27
|
### 修正
|
|
@@ -1466,7 +1478,8 @@ two attempts, instrument first instead of patching again.
|
|
|
1466
1478
|
|
|
1467
1479
|
---
|
|
1468
1480
|
|
|
1469
|
-
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.10.
|
|
1481
|
+
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.10.18...HEAD
|
|
1482
|
+
[0.10.18]: https://github.com/kitepon/Throughline/compare/v0.10.17...v0.10.18
|
|
1470
1483
|
[0.10.17]: https://github.com/kitepon/Throughline/compare/v0.10.16...v0.10.17
|
|
1471
1484
|
[0.10.16]: https://github.com/kitepon/Throughline/compare/v0.10.15...v0.10.16
|
|
1472
1485
|
[0.10.15]: https://github.com/kitepon/Throughline/compare/v0.10.14...v0.10.15
|
package/docs/00_overview.md
CHANGED
|
@@ -8,8 +8,7 @@ Throughline の文書は、現行契約・履歴・証拠を分ける。通常
|
|
|
8
8
|
| 文書 | 役割 |
|
|
9
9
|
|---|---|
|
|
10
10
|
| [../README.md](../README.md) / [../README.ja.md](../README.ja.md) | 利用者向けの導入、設定、コマンド、状態、診断、復旧、更新 |
|
|
11
|
-
| [
|
|
12
|
-
| [AGENTS.md](https://github.com/kitepon/Throughline/blob/main/AGENTS.md) | Codex など Claude Code 以外の作業者向け入口 |
|
|
11
|
+
| [AGENTS.md](https://github.com/kitepon/Throughline/blob/main/AGENTS.md) | 全 host の作業者向けの製品正本 |
|
|
13
12
|
| [01_l1_l2_l3_redesign.md](01_l1_l2_l3_redesign.md) | L1/L2/L3 記憶レイヤーの設計 |
|
|
14
13
|
| [02_clear_auto_handoff_plan.md](02_clear_auto_handoff_plan.md) | `/clear` / `/tl` handoff の現行仕様 |
|
|
15
14
|
| [04_public_release_plan.md](04_public_release_plan.md) | 公開配布、明示的失敗、release gate |
|
|
@@ -61,13 +61,13 @@ Windows PowerShell 5.1へ切り替えない。更新前CLIを再利用せず、
|
|
|
61
61
|
|
|
62
62
|
release候補は次の全条件を満たしたときだけ公開する。
|
|
63
63
|
|
|
64
|
-
1. `package.json` と `CHANGELOG.md` の候補版が一致し、READMEと
|
|
64
|
+
1. `package.json` と `CHANGELOG.md` の候補版が一致し、READMEとAGENTS.mdが版の正本を参照する。
|
|
65
65
|
schema・host契約も実装と一致し、`npm run verify:docs` が全Markdownと梱包内リンクを検査する。
|
|
66
66
|
2. 変更に直結するfocused testがgreenで、その後に `npm test` を最終確認として1回通す。
|
|
67
67
|
3. `npm run verify:release-commit` が、clean working treeかつHEADが`origin`既定ブランチの
|
|
68
68
|
祖先であることを確認する。既定ブランチへ着地していないcommitからpublishしない。
|
|
69
69
|
4. `npm pack --dry-run`でtarball内容、秘密混入、必要なREADME/docsの収録を確認する。
|
|
70
|
-
5. npm publish
|
|
70
|
+
5. 公開は既定ブランチへ着地したrelease commitへ`v<version>` tagをpushし、`.github/workflows/publish.yml`がnpm Trusted Publishingで行う。手元から`npm publish`しない。公開後にregistryのversionとshasum、tag、GitHub Releaseを確認する。
|
|
71
71
|
6. registry由来の隔離installで `throughline --version`、`throughline install`、
|
|
72
72
|
`throughline migrate --json`、`throughline doctor`を確認する。
|
|
73
73
|
7. 変更したhost面で実captureまたはfocused install/diagnostics smokeを行う。
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
| [08_codex_dual_support.md](08_codex_dual_support.md) | Claude / Codex 両対応の architecture brief。adapter 境界の基本方針として維持する |
|
|
16
16
|
| [09_rollback_context_trim_insight.md](09_rollback_context_trim_insight.md) | rollback / rewind を context delete primitive と見る設計メモ |
|
|
17
17
|
| [04_public_release_plan.md](04_public_release_plan.md) | 公開配布の状態表。実装済み behavior だけを公開説明に出す |
|
|
18
|
-
| [
|
|
18
|
+
| [AGENTS.md](https://github.com/kitepon/Throughline/blob/main/AGENTS.md) | 作業者向け入口。Claude 正本を守りつつ、この文書を次フェーズ計画として参照する |
|
|
19
19
|
|
|
20
20
|
この文書は、以後の実装順について [archive/07_codex_trim_implementation_plan.md](archive/07_codex_trim_implementation_plan.md) を上書きする。
|
|
21
21
|
ただし、Claude primary を壊さない、Claude hooks / `/tl` / baton / DB / resume context を Codex 用に置き換えない、という既存の絶対条件は維持する。
|
|
@@ -75,7 +75,7 @@ Codex 側で再実装しないこと:
|
|
|
75
75
|
|
|
76
76
|
1. この `新セッション引き継ぎ` を読む。
|
|
77
77
|
2. Codex 側をやり直さず、Claude `/rewind conversation only` の手動 UX 確認へ進む。
|
|
78
|
-
3. Claude 側に触る前に [
|
|
78
|
+
3. Claude 側に触る前に [AGENTS.md](https://github.com/kitepon/Throughline/blob/main/AGENTS.md) を読む。
|
|
79
79
|
4. Codex rollback incident の追加診断が必要な場合だけ、[06_codex_trim_rollback_fix_plan.md](06_codex_trim_rollback_fix_plan.md) の Phase 4 を参照する。
|
|
80
80
|
5. `.claude/settings.json` はユーザー環境差分を含み得るため、明示依頼なしに整理・置換しない。
|
|
81
81
|
|
|
@@ -103,7 +103,7 @@ Codex 側で再実装しないこと:
|
|
|
103
103
|
## 絶対に守ること
|
|
104
104
|
|
|
105
105
|
- Claude の設定を Codex 用に置き換えない。
|
|
106
|
-
- `
|
|
106
|
+
- `AGENTS.md` を全 host 共通の作業者向け正本とし、Claude 契約を守る規則もそこに置く。
|
|
107
107
|
- Claude hooks / slash command / transcript parser / baton / resume context の既存 semantics を rename しない。
|
|
108
108
|
- Codex 対応は adapter / bridge / Codex primary entrypoint として足す。
|
|
109
109
|
- Codex CLI / app-server / rollout の仕様は実測で固定する。未確認の host behavior を成功扱いにしない。
|
package/package.json
CHANGED
|
@@ -64,7 +64,7 @@ test('現行文書は版の正本を参照し、変更履歴は候補版と一
|
|
|
64
64
|
const version = packageJson.version;
|
|
65
65
|
const currentSources = [
|
|
66
66
|
readFileSync(new URL('../README.md', import.meta.url), 'utf8'),
|
|
67
|
-
readFileSync(new URL('../
|
|
67
|
+
readFileSync(new URL('../AGENTS.md', import.meta.url), 'utf8'),
|
|
68
68
|
readFileSync(new URL('../docs/04_public_release_plan.md', import.meta.url), 'utf8'),
|
|
69
69
|
];
|
|
70
70
|
for (const source of currentSources) {
|
package/src/hosts/claude.mjs
CHANGED
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { CLAUDE_HOST, hostOfSessionId } from './identity.mjs';
|
|
8
8
|
|
|
9
|
+
// Claude Code の背景 task 通知は user 発言として transcript に入る。識別子・出力 path・
|
|
10
|
+
// 定型の注記と、入力待ち時に付く端末の生出力 (`Last output:` 以降) は記憶に要らない。
|
|
11
|
+
// 状態・要約・Monitor の event・subagent の result だけを残す。
|
|
12
|
+
const TASK_NOTIFICATION_NOISE_TAGS = ['task-id', 'tool-use-id', 'output-file', 'usage', 'note', 'task-type', 'diagnostics'];
|
|
13
|
+
|
|
14
|
+
export function compactClaudeTaskNotification(text) {
|
|
15
|
+
if (typeof text !== 'string' || !text.trimStart().startsWith('<task-notification>')) return text;
|
|
16
|
+
const close = text.indexOf('</task-notification>');
|
|
17
|
+
if (close < 0) return text;
|
|
18
|
+
let block = text.slice(0, close + '</task-notification>'.length);
|
|
19
|
+
for (const tag of TASK_NOTIFICATION_NOISE_TAGS) {
|
|
20
|
+
block = block.replace(new RegExp(`\\n?<${tag}>[\\s\\S]*?</${tag}>`, 'g'), '');
|
|
21
|
+
}
|
|
22
|
+
const tail = text.slice(close + '</task-notification>'.length);
|
|
23
|
+
const lastOutput = tail.search(/^Last output:/m);
|
|
24
|
+
const kept = (lastOutput < 0 ? tail : tail.slice(0, lastOutput)).trim();
|
|
25
|
+
return kept ? `${block}\n${kept}` : block;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
export const claudeHostAdapter = Object.freeze({
|
|
10
29
|
host: CLAUDE_HOST,
|
|
11
30
|
matchesSessionId: (sessionId) => hostOfSessionId(sessionId) === CLAUDE_HOST,
|
|
@@ -530,6 +530,7 @@ function toPublicRuntimeError(record) {
|
|
|
530
530
|
occurrence_count: record.count,
|
|
531
531
|
first_seen: record.first_seen,
|
|
532
532
|
last_seen: record.last_seen,
|
|
533
|
+
product_version: record.product_version,
|
|
533
534
|
state_schema_version: record.state_schema_version,
|
|
534
535
|
};
|
|
535
536
|
}
|
|
@@ -44,6 +44,17 @@ function enableCollection(box) {
|
|
|
44
44
|
applyWindowsPrivateAcl(box.configPath);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
test('発生版は直近の発生で更新し、snapshotの実行版から推測しない', () => {
|
|
48
|
+
const box = sandbox(); enableCollection(box);
|
|
49
|
+
observeRuntimeError({ code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-09-01T00:00:00.000Z' }, { env: box.env, version: '1.0.0' });
|
|
50
|
+
assert.equal(readRuntimeErrorSnapshot({ env: box.env, version: '2.0.0' }).runtime_errors[0].product_version, '1.0.0');
|
|
51
|
+
observeRuntimeError({ code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-09-02T00:00:00.000Z' }, { env: box.env, version: '2.0.0' });
|
|
52
|
+
const entry = readRuntimeErrorSnapshot({ env: box.env, version: '3.0.0' }).runtime_errors[0];
|
|
53
|
+
assert.equal(entry.product_version, '2.0.0');
|
|
54
|
+
assert.equal(entry.occurrence_count, 2);
|
|
55
|
+
assert.equal(entry.last_seen, '2026-09-02T00:00:00.000Z');
|
|
56
|
+
});
|
|
57
|
+
|
|
47
58
|
test('runtime error store: missing/false/malformed config is fail-closed and creates no state', () => {
|
|
48
59
|
for (const config of [
|
|
49
60
|
null,
|
|
@@ -232,6 +243,7 @@ test('runtime error store: fixed template SHA-256 fingerprint aggregates and reo
|
|
|
232
243
|
occurrence_count: 2,
|
|
233
244
|
first_seen: '2026-07-13T00:00:00.000Z',
|
|
234
245
|
last_seen: '2026-07-13T00:01:00.000Z',
|
|
246
|
+
product_version: '0.6.1',
|
|
235
247
|
state_schema_version: '1.0',
|
|
236
248
|
});
|
|
237
249
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { readFileSync, existsSync } from 'fs';
|
|
12
12
|
import { DETAIL_KIND } from './constants.mjs';
|
|
13
|
+
import { compactClaudeTaskNotification } from './hosts/claude.mjs';
|
|
13
14
|
|
|
14
15
|
function entryKind(entry) {
|
|
15
16
|
if (typeof entry?.type === 'string' && entry.type.length > 0) return entry.type;
|
|
@@ -71,7 +72,12 @@ export function readTranscript(transcriptPath) {
|
|
|
71
72
|
const rawContent = msg?.content ?? grokContent;
|
|
72
73
|
if (!role || rawContent == null) continue;
|
|
73
74
|
|
|
74
|
-
const
|
|
75
|
+
const extracted = extractText(rawContent);
|
|
76
|
+
// user 発言には端末の生出力 (貼り付け・Claude の task 通知) が入る。記憶には
|
|
77
|
+
// 制御文字を落とした本文だけを残す。
|
|
78
|
+
const text = role === 'user'
|
|
79
|
+
? normalizeTerminalText(compactClaudeTaskNotification(extracted))
|
|
80
|
+
: extracted;
|
|
75
81
|
if (!text) continue;
|
|
76
82
|
|
|
77
83
|
const ts = typeof entry.timestamp === 'string' ? Date.parse(entry.timestamp) : NaN;
|
|
@@ -185,14 +191,30 @@ export function readLatestLogicalTurnCompletion(transcriptPath) {
|
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
194
|
+
* 端末出力の制御を除いて、画面に残る文字列にする。
|
|
195
|
+
* - ESC 系列: OSC (window title 等。Windows ConPTY が出す)、DCS/SOS/PM/APC、
|
|
196
|
+
* CSI (色・cursor 移動・private mode)、その他の 2 byte 以上の ESC 系列。
|
|
197
|
+
* - 改行: CRLF は LF にし、行内の CR は上書き表示として最後の区間だけを残す。
|
|
190
198
|
* @param {string} s
|
|
191
199
|
*/
|
|
192
|
-
export function
|
|
200
|
+
export function normalizeTerminalText(s) {
|
|
193
201
|
if (typeof s !== 'string') return s;
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
/* eslint-disable no-control-regex */
|
|
203
|
+
const stripped = s
|
|
204
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
205
|
+
.replace(/\x1b[PX^_][^\x1b]*\x1b\\/g, '')
|
|
206
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
207
|
+
.replace(/\x1b[ -/]*[0-~]/g, '');
|
|
208
|
+
/* eslint-enable no-control-regex */
|
|
209
|
+
return stripped
|
|
210
|
+
.replace(/\r\n/g, '\n')
|
|
211
|
+
.split('\n')
|
|
212
|
+
.map((line) => {
|
|
213
|
+
if (!line.includes('\r')) return line;
|
|
214
|
+
const segments = line.split('\r').filter((segment) => segment.length > 0);
|
|
215
|
+
return segments.length ? segments[segments.length - 1] : '';
|
|
216
|
+
})
|
|
217
|
+
.join('\n');
|
|
196
218
|
}
|
|
197
219
|
|
|
198
220
|
/**
|
|
@@ -378,7 +400,7 @@ export function extractDetailBlocks(turnEntries) {
|
|
|
378
400
|
tool_name: toolName,
|
|
379
401
|
source_id: toolUseId ? `${toolUseId}:result` : null,
|
|
380
402
|
input_text: null,
|
|
381
|
-
output_text:
|
|
403
|
+
output_text: normalizeTerminalText(rawOutput),
|
|
382
404
|
});
|
|
383
405
|
} else if (b.type === 'image') {
|
|
384
406
|
out.push({
|
|
@@ -419,7 +441,7 @@ export function extractDetailBlocks(turnEntries) {
|
|
|
419
441
|
tool_name: toolName,
|
|
420
442
|
source_id: e.uuid ?? null,
|
|
421
443
|
input_text: a.command ?? a.path ?? null,
|
|
422
|
-
output_text:
|
|
444
|
+
output_text: normalizeTerminalText(String(output)),
|
|
423
445
|
});
|
|
424
446
|
}
|
|
425
447
|
// type === 'system' (stop_hook_summary) や queue-operation / file-history-snapshot は skip
|
|
@@ -1,17 +1,88 @@
|
|
|
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';
|
|
5
|
+
import { join } from 'node:path';
|
|
3
6
|
import {
|
|
4
|
-
|
|
7
|
+
normalizeTerminalText,
|
|
5
8
|
normalizeToolResultContent,
|
|
9
|
+
readTranscript,
|
|
6
10
|
sliceCurrentTurnEntries,
|
|
7
11
|
extractDetailBlocks,
|
|
8
12
|
} from './transcript-reader.mjs';
|
|
9
13
|
import { DETAIL_KIND } from './constants.mjs';
|
|
10
14
|
|
|
11
|
-
test('
|
|
12
|
-
assert.equal(
|
|
13
|
-
assert.equal(
|
|
14
|
-
assert.equal(
|
|
15
|
+
test('normalizeTerminalText: ANSI 色コードを除去する', () => {
|
|
16
|
+
assert.equal(normalizeTerminalText('\x1b[32mgreen\x1b[0m text'), 'green text');
|
|
17
|
+
assert.equal(normalizeTerminalText('plain'), 'plain');
|
|
18
|
+
assert.equal(normalizeTerminalText(''), '');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('normalizeTerminalText: cursor 移動・private mode・OSC・2 byte ESC を除去する', () => {
|
|
22
|
+
// npm の spinner (macOS 実測) と Windows ConPTY の title / cursor 表示切替
|
|
23
|
+
assert.equal(normalizeTerminalText('\x1b[1G\x1b[0K|\x1b[1G\x1b[0Jdone\x1b[38G'), '|done');
|
|
24
|
+
assert.equal(normalizeTerminalText('\x1b]0;C:\\Windows\\pwsh.exe\x07\x1b[?25lok\x1b[?25h'), 'ok');
|
|
25
|
+
assert.equal(normalizeTerminalText('\x1b]8;;https://e.x\x1b\\link\x1b]8;;\x1b\\'), 'link');
|
|
26
|
+
assert.equal(normalizeTerminalText('\x1b(Bx\x1b7y\x1b8'), 'xy');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('normalizeTerminalText: CRLF を LF にし、行内 CR は最後の上書きだけを残す', () => {
|
|
30
|
+
assert.equal(normalizeTerminalText('a\r\nb\r\n'), 'a\nb\n');
|
|
31
|
+
assert.equal(normalizeTerminalText('10%\r50%\r100%\nnext'), '100%\nnext');
|
|
32
|
+
assert.equal(normalizeTerminalText('line\r'), 'line');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function writeTranscript(entries) {
|
|
36
|
+
const dir = mkdtempSync(join(tmpdir(), 'tl-transcript-'));
|
|
37
|
+
const path = join(dir, 't.jsonl');
|
|
38
|
+
writeFileSync(path, entries.map((e) => JSON.stringify(e)).join('\n') + '\n');
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('readTranscript: Claude task 通知は識別子・path・端末生出力を落として残す', () => {
|
|
43
|
+
const notice = [
|
|
44
|
+
'<task-notification>',
|
|
45
|
+
'<task-id>b1</task-id>',
|
|
46
|
+
'<tool-use-id>toolu_1</tool-use-id>',
|
|
47
|
+
'<output-file>/tmp/tasks/b1.output</output-file>',
|
|
48
|
+
'<status>completed</status>',
|
|
49
|
+
'<summary>Background command "x" completed (exit code 0)</summary>',
|
|
50
|
+
'<result>subagent report</result>',
|
|
51
|
+
'<usage><subagent_tokens>1</subagent_tokens></usage>',
|
|
52
|
+
'<note>A task-notification fires each time this agent stops.</note>',
|
|
53
|
+
'</task-notification>',
|
|
54
|
+
'Last output:',
|
|
55
|
+
'\x1b[34mEstablishing\x1b[39m\r\nPress ENTER',
|
|
56
|
+
'',
|
|
57
|
+
'The command is likely blocked on an interactive prompt.',
|
|
58
|
+
].join('\n');
|
|
59
|
+
const path = writeTranscript([
|
|
60
|
+
{ type: 'user', message: { role: 'user', content: notice } },
|
|
61
|
+
{ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: '\x1b[1mkeep\x1b[0m' }] } },
|
|
62
|
+
]);
|
|
63
|
+
const [user, assistant] = readTranscript(path);
|
|
64
|
+
assert.equal(
|
|
65
|
+
user.content,
|
|
66
|
+
[
|
|
67
|
+
'<task-notification>',
|
|
68
|
+
'<status>completed</status>',
|
|
69
|
+
'<summary>Background command "x" completed (exit code 0)</summary>',
|
|
70
|
+
'<result>subagent report</result>',
|
|
71
|
+
'</task-notification>',
|
|
72
|
+
].join('\n'),
|
|
73
|
+
);
|
|
74
|
+
// assistant 本文は Stop payload との照合に使うため変えない
|
|
75
|
+
assert.equal(assistant.content, '\x1b[1mkeep\x1b[0m');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('readTranscript: task 通知の後ろの system-reminder と通常の user 発言は残す', () => {
|
|
79
|
+
const path = writeTranscript([
|
|
80
|
+
{ type: 'user', message: { role: 'user', content: '<task-notification>\n<task-id>b2</task-id>\n<summary>s</summary>\n</task-notification>\n<system-reminder>r</system-reminder>' } },
|
|
81
|
+
{ type: 'user', message: { role: 'user', content: [{ type: 'text', text: '貼った出力 \x1b[31mERR\x1b[0m' }] } },
|
|
82
|
+
]);
|
|
83
|
+
const [notice, typed] = readTranscript(path);
|
|
84
|
+
assert.equal(notice.content, '<task-notification>\n<summary>s</summary>\n</task-notification>\n<system-reminder>r</system-reminder>');
|
|
85
|
+
assert.equal(typed.content, '貼った出力 ERR');
|
|
15
86
|
});
|
|
16
87
|
|
|
17
88
|
test('normalizeToolResultContent: string / array / image mix', () => {
|