throughline 0.10.3 → 0.10.6
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 +82 -29
- package/README.ja.md +79 -47
- package/README.md +103 -78
- package/bin/throughline.mjs +32 -13
- package/docs/00_overview.md +56 -43
- package/docs/01_l1_l2_l3_redesign.md +1 -1
- package/docs/02_clear_auto_handoff_plan.md +39 -333
- package/docs/04_public_release_plan.md +77 -190
- package/docs/05_codex_first_roadmap.md +4 -4
- package/docs/06_codex_trim_rollback_fix_plan.md +1 -1
- package/docs/08_codex_dual_support.md +1 -1
- package/docs/09_rollback_context_trim_insight.md +1 -1
- package/docs/12_desktop_clear_handoff_plan.md +6 -213
- package/docs/15_windows_ci_release_latency_plan.md +6 -87
- package/docs/16_readonly_handoff_context_plan.md +7 -38
- package/docs/adr/0005-observer-read-pagination.md +1 -1
- package/docs/adr/0014-two-phase-handoff-ghost-baton.md +1 -1
- package/docs/adr/0019-product-owned-database-migration-acceptance.md +1 -1
- package/docs/adr/0021-grok-host-capture.md +1 -1
- package/docs/archive/02_clear_auto_handoff_plan.md +350 -0
- package/docs/{03_inheritance_on_clear_only.md → archive/03_inheritance_on_clear_only.md} +22 -22
- package/docs/{07_codex_trim_implementation_plan.md → archive/07_codex_trim_implementation_plan.md} +8 -8
- package/docs/{10_transcript_injection_plan.md → archive/10_transcript_injection_plan.md} +12 -12
- package/docs/archive/12_desktop_clear_handoff_plan.md +218 -0
- package/docs/{14_observer_completed_turn_feed_plan.md → archive/14_observer_completed_turn_feed_plan.md} +7 -7
- package/docs/archive/15_windows_ci_release_latency_plan.md +89 -0
- package/docs/archive/16_readonly_handoff_context_plan.md +40 -0
- package/docs/archive/README.md +28 -15
- package/docs/archive/plan_grok-successor-launch.md +99 -0
- package/docs/archive/room-log_throughline_20260830-155052.md +285 -0
- package/docs/plan_grok-successor-launch.md +6 -97
- package/package.json +11 -4
- package/rag/INDEX.md +2 -2
- package/src/baton.mjs +11 -9
- package/src/cli/handoff-context.test.mjs +36 -0
- package/src/cli/help.test.mjs +5 -0
- package/src/cli/runtime-errors.mjs +9 -3
- package/src/cli/runtime-errors.test.mjs +13 -13
- package/src/cli/self-update.mjs +402 -0
- package/src/cli/self-update.test.mjs +525 -0
- package/src/db.mjs +1 -1
- package/src/docs-contract.test.mjs +153 -0
- package/src/product-ci-contract.test.mjs +14 -0
- package/src/prompt-submit.mjs +8 -10
- package/src/resume-context.mjs +4 -4
- package/src/runtime-error-hook.test.mjs +4 -6
- package/src/runtime-error-store.mjs +40 -18
- package/src/runtime-error-store.test.mjs +53 -26
- package/src/session-merger.mjs +16 -7
- package/src/session-merger.test.mjs +27 -0
- package/src/spike-transcript-writer.mjs +1 -1
- /package/docs/{11_codex_monitor_implementation_plan.md → archive/11_codex_monitor_implementation_plan.md} +0 -0
- /package/docs/{13_native_factory_diagnostics_plan.md → archive/13_native_factory_diagnostics_plan.md} +0 -0
- /package/docs/{BUGHUB_RUNTIME_ERROR_STORE_PLAN.md → archive/BUGHUB_RUNTIME_ERROR_STORE_PLAN.md} +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
|
|
5
|
+
test('windows-native product CI uses PowerShell 7 only', () => {
|
|
6
|
+
const source = readFileSync(new URL('../.github/workflows/product-full-ci.yml', import.meta.url), 'utf8');
|
|
7
|
+
const windowsBlocks = source
|
|
8
|
+
.split(/\n(?= {6}- name:)/)
|
|
9
|
+
.filter((block) => block.includes("matrix.environment == 'windows-native'"));
|
|
10
|
+
assert.equal(windowsBlocks.length, 3);
|
|
11
|
+
for (const block of windowsBlocks) assert.match(block, /\n shell: pwsh\n/);
|
|
12
|
+
assert.match(windowsBlocks[0], /\$env:GITHUB_ENV/);
|
|
13
|
+
assert.doesNotMatch(source, /Git\\bin\\bash|Git\/bin\/bash|shell:\s*(?:powershell|cmd)\b/i);
|
|
14
|
+
});
|
package/src/prompt-submit.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* UserPromptSubmit hook — 二相ハンドオフ第二相 + /tl
|
|
3
|
+
* UserPromptSubmit hook — 二相ハンドオフ第二相 + /tl バトン書き込み + Phase 0-5 spike
|
|
4
4
|
*
|
|
5
5
|
* stdin: { session_id, cwd, prompt, transcript_path, hook_event_name, ... }
|
|
6
6
|
*
|
|
@@ -13,23 +13,21 @@
|
|
|
13
13
|
* 注入がこの hook に移ったため、SessionStart 側の注入は廃止済み
|
|
14
14
|
* (旧「二重注入回避」制約はこの構成では発生しない)。
|
|
15
15
|
* - prompt が /tl (単独 or /tl ... 形式) で始まっていればバトンを書き込んで終了
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* 確定的に「/clear が打たれたセッション」を新セッションに引き継げる。複数
|
|
20
|
-
* VSCode ウィンドウ等で「最新更新セッション = clear されたセッション」が
|
|
21
|
-
* 成立しない multi-window シナリオで誤った前任を選ばないための確定的指名)
|
|
16
|
+
* - `/clear` 文字列を受け取った場合の互換バトン分岐は残す。ただし、実測した
|
|
17
|
+
* Claude Code の組み込み `/clear` はこの hook に届かない。VS Code は
|
|
18
|
+
* SessionStart source='clear' の auto path、Desktop は事前 `/tl` を使う。
|
|
22
19
|
* - それ以外は何もせず exit 0(プロンプトはそのまま Claude に渡る)
|
|
23
|
-
* -
|
|
20
|
+
* - pending intent がある最初のプロンプトでは、本 hook が引き継ぎ注入を行う。
|
|
24
21
|
*
|
|
25
22
|
* - **Phase 0-5 spike (SPIKE ONLY)**: marker file `~/.throughline/spike-prompt.flag`
|
|
26
23
|
* が存在し、当該セッションでまだ spike を打っていなければ、JSONL に user/assistant
|
|
27
24
|
* 行を chain-reachable (= 直前の attachment uuid を parent に取る) で append する。
|
|
28
25
|
* SessionStart 経路の spike (chain (a) = orphan) ではモデルに届かなかったため、
|
|
29
26
|
* UserPromptSubmit 経路で chain (b) を成立させて再検証する。
|
|
30
|
-
* docs/10_transcript_injection_plan.md Phase 0-5 参照。
|
|
27
|
+
* docs/archive/10_transcript_injection_plan.md Phase 0-5 参照。
|
|
31
28
|
*
|
|
32
|
-
* 設計背景: docs/03_inheritance_on_clear_only.md バトン方式
|
|
29
|
+
* 設計背景: docs/archive/03_inheritance_on_clear_only.md バトン方式
|
|
30
|
+
* 現行契約: docs/02_clear_auto_handoff_plan.md
|
|
33
31
|
*/
|
|
34
32
|
|
|
35
33
|
import { getDb } from './db.mjs';
|
package/src/resume-context.mjs
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
* resume-context.mjs — 引継ぎ注入テキストを組み立てる共有モジュール
|
|
3
3
|
*
|
|
4
4
|
* 呼び出し元:
|
|
5
|
-
* -
|
|
5
|
+
* - handoff-executor.mjs(最初のUserPromptSubmitでauto path / baton path共通注入)
|
|
6
6
|
*
|
|
7
7
|
* 設計 (docs/02_clear_auto_handoff_plan.md):
|
|
8
|
-
* -
|
|
8
|
+
* - 予算付き注入: ヘッダ + 現在地アンカー + pull案内を固定部とし、残りへ
|
|
9
|
+
* 直近L2ターン全文を新しい順にターン原子で詰める。L1は直接注入しない。
|
|
9
10
|
* - 「現在地」アンカーは直前の user / assistant turn をヘッダ直下に再掲して
|
|
10
11
|
* 最初の注意を最新ターンに固定する。L2 末尾アンカーは補強として残す。
|
|
11
12
|
* (L2 が長くなると末尾アンカーだけでは前半の古いターンに注意が固着し、
|
|
12
13
|
* 話の流れを取り違える事例があった)
|
|
13
|
-
* - L3
|
|
14
|
-
* `[→ throughline detail HH:MM:SS (kind …)]` ヒントを付ける
|
|
14
|
+
* - L3は本文を注入せず、`throughline detail HH:MM:SS`でpullする
|
|
15
15
|
* - L2 全文があれば最後の assistant turn 自体に「次に何をしようとしていたか」が
|
|
16
16
|
* 含まれるため、memo / thinking / 末尾の再開指示は注入しない
|
|
17
17
|
* - 各行頭に [HH:MM:SS] 時刻プレフィックス(L2 は body の created_at、
|
|
@@ -5,7 +5,7 @@ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import {
|
|
8
|
+
import { defaultRuntimeErrorConfigPath, defaultRuntimeErrorStorePath } from './runtime-error-store.mjs';
|
|
9
9
|
import { applyWindowsPrivateAcl } from './os/windows-acl-test-helper.mjs';
|
|
10
10
|
|
|
11
11
|
const BIN = fileURLToPath(new URL('../bin/throughline.mjs', import.meta.url));
|
|
@@ -20,13 +20,11 @@ function createEnabledEnvironment(prefix) {
|
|
|
20
20
|
XDG_CONFIG_HOME: join(root, 'config'),
|
|
21
21
|
XDG_STATE_HOME: join(root, 'state'),
|
|
22
22
|
};
|
|
23
|
-
const configPath =
|
|
23
|
+
const configPath = defaultRuntimeErrorConfigPath(env);
|
|
24
24
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
25
25
|
writeFileSync(configPath, JSON.stringify({
|
|
26
|
-
|
|
27
|
-
host: { id: 'test-host', profile: process.platform === 'win32' ? 'windows-native' : 'mac' },
|
|
26
|
+
schema: 'throughline.runtime_error_config.v1',
|
|
28
27
|
collection: { enabled: true },
|
|
29
|
-
reporting: { enabled: false },
|
|
30
28
|
}));
|
|
31
29
|
applyWindowsPrivateAcl(configPath);
|
|
32
30
|
return { root, env };
|
|
@@ -90,7 +88,7 @@ test('store failure preserves product failure and emits only fixed storage diagn
|
|
|
90
88
|
|
|
91
89
|
test('FIFO config cannot block the original hook failure', { skip: process.platform === 'win32' }, () => {
|
|
92
90
|
const { env } = createEnabledEnvironment('throughline-runtime-hook-fifo-');
|
|
93
|
-
const config =
|
|
91
|
+
const config = defaultRuntimeErrorConfigPath(env);
|
|
94
92
|
execFileSync('rm', ['-f', config]);
|
|
95
93
|
execFileSync('mkfifo', [config]);
|
|
96
94
|
const started = Date.now();
|
|
@@ -22,6 +22,7 @@ const require = createRequire(import.meta.url);
|
|
|
22
22
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
23
23
|
|
|
24
24
|
export const RUNTIME_ERROR_STORE_SCHEMA = 'throughline.runtime_errors.v1';
|
|
25
|
+
export const RUNTIME_ERROR_CONFIG_SCHEMA = 'throughline.runtime_error_config.v1';
|
|
25
26
|
export const RUNTIME_ERROR_STATE_SCHEMA_VERSION = '1.0';
|
|
26
27
|
export const RUNTIME_ERROR_DIAGNOSTIC = '[throughline:runtime-errors] store_unavailable\n';
|
|
27
28
|
const DEFAULT_SNAPSHOT_LIMIT = 256;
|
|
@@ -53,11 +54,11 @@ const DEFINITIONS = Object.freeze({
|
|
|
53
54
|
}),
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
export function
|
|
57
|
+
export function defaultRuntimeErrorConfigPath(env = process.env) {
|
|
57
58
|
if (isWindows(env)) {
|
|
58
|
-
return join(windowsLocalAppData(env), '
|
|
59
|
+
return join(windowsLocalAppData(env), 'throughline', 'runtime-errors.config.json');
|
|
59
60
|
}
|
|
60
|
-
return join(xdgConfigHome(env), '
|
|
61
|
+
return join(xdgConfigHome(env), 'throughline', 'runtime-errors.config.json');
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function defaultRuntimeErrorStorePath(env = process.env) {
|
|
@@ -69,16 +70,46 @@ export function defaultRuntimeErrorStorePath(env = process.env) {
|
|
|
69
70
|
|
|
70
71
|
export function isRuntimeErrorCollectionEnabled({ env = process.env, configPath } = {}) {
|
|
71
72
|
try {
|
|
72
|
-
const path = configPath ||
|
|
73
|
+
const path = configPath || defaultRuntimeErrorConfigPath(env);
|
|
73
74
|
const info = lstatSync(path);
|
|
74
75
|
if (!info.isFile() || info.isSymbolicLink()) return false;
|
|
75
76
|
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
76
|
-
return
|
|
77
|
+
return isCanonicalRuntimeErrorConfig(parsed) && parsed.collection.enabled === true;
|
|
77
78
|
} catch {
|
|
78
79
|
return false;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
export function setRuntimeErrorCollectionEnabled(enabled, options = {}) {
|
|
84
|
+
assertExactOptions(options, ['env', 'configPath']);
|
|
85
|
+
if (typeof enabled !== 'boolean') throw new TypeError('enabled は boolean が必要です');
|
|
86
|
+
const env = options.env ?? process.env;
|
|
87
|
+
const path = options.configPath || defaultRuntimeErrorConfigPath(env);
|
|
88
|
+
const directory = dirname(path);
|
|
89
|
+
ensurePrivateStoreDirectory(directory, env);
|
|
90
|
+
const temporary = join(directory, `.runtime-errors-config.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
|
|
91
|
+
const config = {
|
|
92
|
+
schema: RUNTIME_ERROR_CONFIG_SCHEMA,
|
|
93
|
+
collection: { enabled },
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(temporary, `${JSON.stringify(config)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
97
|
+
if (isWindows(env)) {
|
|
98
|
+
applyAndVerifyWindowsAcl(temporary, false);
|
|
99
|
+
assertPrivateStoreFileShape(lstatSync(temporary));
|
|
100
|
+
} else {
|
|
101
|
+
chmodSync(temporary, 0o600);
|
|
102
|
+
assertPrivateStoreFile(lstatSync(temporary), env, temporary);
|
|
103
|
+
}
|
|
104
|
+
renameSync(temporary, path);
|
|
105
|
+
if (isWindows(env)) assertPrivateStoreFileShape(lstatSync(path));
|
|
106
|
+
else assertPrivateStoreFile(lstatSync(path), env, path);
|
|
107
|
+
return config;
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(temporary, { force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
export function observeRuntimeError(input, options = {}) {
|
|
83
114
|
assertExactInput(input, ['code', 'now']);
|
|
84
115
|
assertExactOptions(options, ['env', 'configPath', 'storePath', 'version', 'platform', 'arch']);
|
|
@@ -410,20 +441,11 @@ function assertPrivateStoreFileShape(info) {
|
|
|
410
441
|
}
|
|
411
442
|
}
|
|
412
443
|
|
|
413
|
-
function
|
|
414
|
-
if (!isPlainObject(value) || !exactKeys(value, ['
|
|
415
|
-
|
|
416
|
-
typeof value.host.id !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(value.host.id) ||
|
|
417
|
-
!['server', 'mac', 'wsl', 'windows-native'].includes(value.host.profile)) return false;
|
|
444
|
+
function isCanonicalRuntimeErrorConfig(value) {
|
|
445
|
+
if (!isPlainObject(value) || !exactKeys(value, ['schema', 'collection']) ||
|
|
446
|
+
value.schema !== RUNTIME_ERROR_CONFIG_SCHEMA) return false;
|
|
418
447
|
if (!isPlainObject(value.collection) || !exactKeys(value.collection, ['enabled']) || typeof value.collection.enabled !== 'boolean') return false;
|
|
419
|
-
|
|
420
|
-
if (value.reporting.endpoint !== undefined) {
|
|
421
|
-
if (typeof value.reporting.endpoint !== 'string' || value.reporting.endpoint.length > 2048) return false;
|
|
422
|
-
try { if (!['http:', 'https:'].includes(new URL(value.reporting.endpoint).protocol)) return false; } catch { return false; }
|
|
423
|
-
}
|
|
424
|
-
if (value.reporting.credential_file !== undefined &&
|
|
425
|
-
(typeof value.reporting.credential_file !== 'string' || value.reporting.credential_file.length < 1 || value.reporting.credential_file.length > 4096)) return false;
|
|
426
|
-
return !value.reporting.enabled || (value.reporting.endpoint !== undefined && value.reporting.credential_file !== undefined);
|
|
448
|
+
return true;
|
|
427
449
|
}
|
|
428
450
|
|
|
429
451
|
function isPlainObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
@@ -8,13 +8,14 @@ import { dirname, join } from 'node:path';
|
|
|
8
8
|
import {
|
|
9
9
|
acknowledgeRuntimeErrors,
|
|
10
10
|
compactRuntimeErrors,
|
|
11
|
-
|
|
11
|
+
defaultRuntimeErrorConfigPath,
|
|
12
12
|
defaultRuntimeErrorStorePath,
|
|
13
13
|
getRuntimeErrorDiagnostics,
|
|
14
14
|
observeRuntimeError,
|
|
15
15
|
readRuntimeErrorSnapshot,
|
|
16
16
|
reopenRuntimeError,
|
|
17
17
|
resolveRuntimeError,
|
|
18
|
+
setRuntimeErrorCollectionEnabled,
|
|
18
19
|
} from './runtime-error-store.mjs';
|
|
19
20
|
import { applyWindowsPrivateAcl, verifyWindowsPrivateAcl } from './os/windows-acl-test-helper.mjs';
|
|
20
21
|
|
|
@@ -29,18 +30,16 @@ function sandbox() {
|
|
|
29
30
|
XDG_CONFIG_HOME: join(root, 'config'),
|
|
30
31
|
XDG_STATE_HOME: join(root, 'state'),
|
|
31
32
|
};
|
|
32
|
-
const configPath =
|
|
33
|
+
const configPath = defaultRuntimeErrorConfigPath(env);
|
|
33
34
|
const storePath = defaultRuntimeErrorStorePath(env);
|
|
34
35
|
return { root, env, configPath, storePath };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function enableCollection(box
|
|
38
|
+
function enableCollection(box) {
|
|
38
39
|
mkdirSync(dirname(box.configPath), { recursive: true });
|
|
39
40
|
writeFileSync(box.configPath, JSON.stringify({
|
|
40
|
-
|
|
41
|
-
host: { id: 'test-host', profile: process.platform === 'win32' ? 'windows-native' : 'mac' },
|
|
41
|
+
schema: 'throughline.runtime_error_config.v1',
|
|
42
42
|
collection: { enabled: true },
|
|
43
|
-
reporting,
|
|
44
43
|
}));
|
|
45
44
|
applyWindowsPrivateAcl(box.configPath);
|
|
46
45
|
}
|
|
@@ -49,8 +48,8 @@ test('runtime error store: missing/false/malformed config is fail-closed and cre
|
|
|
49
48
|
for (const config of [
|
|
50
49
|
null,
|
|
51
50
|
{ collection: { enabled: true } },
|
|
52
|
-
{ collection: { enabled: false } },
|
|
53
|
-
{ collection: { enabled: 'true' } },
|
|
51
|
+
{ schema: 'throughline.runtime_error_config.v1', collection: { enabled: false } },
|
|
52
|
+
{ schema: 'throughline.runtime_error_config.v1', collection: { enabled: 'true' } },
|
|
54
53
|
'{malformed',
|
|
55
54
|
]) {
|
|
56
55
|
const box = sandbox();
|
|
@@ -67,15 +66,15 @@ test('runtime error store: missing/false/malformed config is fail-closed and cre
|
|
|
67
66
|
}
|
|
68
67
|
});
|
|
69
68
|
|
|
70
|
-
test('runtime error store: Windows native uses
|
|
69
|
+
test('runtime error store: Windows native uses product-owned LocalAppData paths', () => {
|
|
71
70
|
const env = {
|
|
72
71
|
OS: 'Windows_NT',
|
|
73
72
|
USERPROFILE: 'C:\\Users\\kite_',
|
|
74
73
|
LOCALAPPDATA: 'C:\\Users\\kite_\\AppData\\Local',
|
|
75
74
|
};
|
|
76
75
|
assert.equal(
|
|
77
|
-
|
|
78
|
-
join(env.LOCALAPPDATA, '
|
|
76
|
+
defaultRuntimeErrorConfigPath(env),
|
|
77
|
+
join(env.LOCALAPPDATA, 'throughline', 'runtime-errors.config.json'),
|
|
79
78
|
);
|
|
80
79
|
assert.equal(
|
|
81
80
|
defaultRuntimeErrorStorePath(env),
|
|
@@ -83,6 +82,34 @@ test('runtime error store: Windows native uses the canonical LocalAppData paths'
|
|
|
83
82
|
);
|
|
84
83
|
});
|
|
85
84
|
|
|
85
|
+
test('runtime error store: product-owned config enables and disables collection', () => {
|
|
86
|
+
const box = sandbox();
|
|
87
|
+
assert.deepEqual(setRuntimeErrorCollectionEnabled(true, { env: box.env }), {
|
|
88
|
+
schema: 'throughline.runtime_error_config.v1',
|
|
89
|
+
collection: { enabled: true },
|
|
90
|
+
});
|
|
91
|
+
assert.equal(observeRuntimeError({ code: 'HOOK_PROCESS_TURN_FAILED' }, { env: box.env }).status, 'recorded');
|
|
92
|
+
assert.deepEqual(setRuntimeErrorCollectionEnabled(false, { env: box.env }), {
|
|
93
|
+
schema: 'throughline.runtime_error_config.v1',
|
|
94
|
+
collection: { enabled: false },
|
|
95
|
+
});
|
|
96
|
+
assert.equal(getRuntimeErrorDiagnostics({ env: box.env }).collection, 'disabled');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('runtime error store: legacy dotagents config cannot control collection', () => {
|
|
100
|
+
const box = sandbox();
|
|
101
|
+
const legacyPath = join(box.env.XDG_CONFIG_HOME, 'dotagents', 'factory-reporter.json');
|
|
102
|
+
mkdirSync(dirname(legacyPath), { recursive: true });
|
|
103
|
+
writeFileSync(legacyPath, JSON.stringify({
|
|
104
|
+
schema_version: '1.0',
|
|
105
|
+
host: { id: 'test-host', profile: 'mac' },
|
|
106
|
+
collection: { enabled: true },
|
|
107
|
+
reporting: { enabled: false },
|
|
108
|
+
}));
|
|
109
|
+
assert.equal(observeRuntimeError({ code: 'HOOK_PROCESS_TURN_FAILED' }, { env: box.env }).status, 'disabled');
|
|
110
|
+
assert.throws(() => statSync(box.storePath), { code: 'ENOENT' });
|
|
111
|
+
});
|
|
112
|
+
|
|
86
113
|
test('runtime error store: one Windows mutation spends ACL processes only on distinct state transitions', (t) => {
|
|
87
114
|
const box = sandbox();
|
|
88
115
|
box.env.OS = 'Windows_NT';
|
|
@@ -127,20 +154,26 @@ test('runtime error store: Windows temporary ACL failure leaves the previous ato
|
|
|
127
154
|
assert.deepEqual(readdirSync(dirname(box.storePath)).filter((name) => name.endsWith('.tmp')), []);
|
|
128
155
|
});
|
|
129
156
|
|
|
130
|
-
test('runtime error store: reporting
|
|
157
|
+
test('runtime error store: factory reporting fields cannot enable product collection', () => {
|
|
131
158
|
const box = sandbox();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
mkdirSync(dirname(box.configPath), { recursive: true });
|
|
160
|
+
writeFileSync(box.configPath, JSON.stringify({
|
|
161
|
+
schema_version: '1.0',
|
|
162
|
+
host: { id: 'test-host', profile: 'mac' },
|
|
163
|
+
collection: { enabled: true },
|
|
164
|
+
reporting: {
|
|
165
|
+
enabled: true,
|
|
166
|
+
endpoint: 'https://should-never-be-read.invalid/private',
|
|
167
|
+
credential_file: '/private/token',
|
|
168
|
+
},
|
|
169
|
+
}));
|
|
170
|
+
applyWindowsPrivateAcl(box.configPath);
|
|
137
171
|
const result = observeRuntimeError(
|
|
138
172
|
{ code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-07-13T00:00:00.000Z' },
|
|
139
173
|
{ env: box.env, version: '0.6.1', platform: TEST_PLATFORM, arch: 'arm64' },
|
|
140
174
|
);
|
|
141
|
-
assert.equal(result.status, '
|
|
142
|
-
|
|
143
|
-
assert.doesNotMatch(bytes, /should-never-be-read|private\/token|endpoint|credential/i);
|
|
175
|
+
assert.equal(result.status, 'disabled');
|
|
176
|
+
assert.throws(() => statSync(box.storePath), { code: 'ENOENT' });
|
|
144
177
|
});
|
|
145
178
|
|
|
146
179
|
test('runtime error store: observation API rejects raw or arbitrary fields', () => {
|
|
@@ -294,12 +327,6 @@ test('runtime error store: atomic private store has owner-only modes and bounded
|
|
|
294
327
|
assert.doesNotMatch(JSON.stringify(diagnostics), /Users|\\|\.throughline|runtime-errors\.json/);
|
|
295
328
|
});
|
|
296
329
|
|
|
297
|
-
test('runtime error store: non-canonical reporting values keep collection fail-closed', () => {
|
|
298
|
-
const box = sandbox();
|
|
299
|
-
enableCollection(box, { enabled: true, endpoint: 'ftp://invalid', credential_file: '' });
|
|
300
|
-
assert.deepEqual(observeRuntimeError({ code: 'HOOK_CODEX_FAILED' }, { env: box.env }), { status: 'disabled' });
|
|
301
|
-
});
|
|
302
|
-
|
|
303
330
|
test('runtime error store: unknown top-level fields and future ack are rejected before compaction', () => {
|
|
304
331
|
const box = sandbox();
|
|
305
332
|
enableCollection(box);
|
package/src/session-merger.mjs
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* session-merger.mjs — 記憶張り替え + merged_into チェーン解決
|
|
3
3
|
*
|
|
4
4
|
* 用途:
|
|
5
|
-
* -
|
|
5
|
+
* - 初回 UserPromptSubmit: baton または source='clear' で凍結した旧セッションを
|
|
6
|
+
* mergeSpecificPredecessor で新セッションに張り替え
|
|
6
7
|
* - Stop hook: resolveMergeTarget で「入力 session_id → 実書き込み先」を解決
|
|
7
8
|
*
|
|
8
|
-
* 設計背景: docs/archive/SESSION_LINKING_DESIGN.md, docs/03_inheritance_on_clear_only.md (バトン方式採用)
|
|
9
|
+
* 設計背景: docs/archive/SESSION_LINKING_DESIGN.md, docs/archive/03_inheritance_on_clear_only.md (バトン方式採用)
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* 旧実装の無条件な時間差ヒューリスティックは撤去済み。
|
|
12
|
+
* 引き継ぎは `/tl` baton、または VS Code `/clear` の `source='clear'` で
|
|
13
|
+
* SessionStart 時に凍結した transcript-backed predecessor からだけ発火する。
|
|
12
14
|
*/
|
|
13
15
|
|
|
16
|
+
import { sameProjectPath } from './project-path.mjs';
|
|
17
|
+
|
|
14
18
|
const MAX_CHAIN_DEPTH = 10;
|
|
15
19
|
|
|
16
20
|
/**
|
|
@@ -61,7 +65,7 @@ export function resolveMergeTarget(db, sessionId) {
|
|
|
61
65
|
* merged: boolean,
|
|
62
66
|
* predecessorId?: string,
|
|
63
67
|
* rowCounts?: { sk: number, dt: number, bd: number },
|
|
64
|
-
* skipReason?: 'self_handoff' | 'predecessor_not_found' | 'already_merged' | 'predecessor_not_older',
|
|
68
|
+
* skipReason?: 'self_handoff' | 'predecessor_not_found' | 'already_merged' | 'project_mismatch' | 'predecessor_not_older',
|
|
65
69
|
* }}
|
|
66
70
|
*/
|
|
67
71
|
export function mergeSpecificPredecessor(db, { newSessionId, predecessorId, now = Date.now() }) {
|
|
@@ -72,7 +76,7 @@ export function mergeSpecificPredecessor(db, { newSessionId, predecessorId, now
|
|
|
72
76
|
db.exec('BEGIN IMMEDIATE');
|
|
73
77
|
try {
|
|
74
78
|
const pred = db
|
|
75
|
-
.prepare('SELECT session_id, created_at, merged_into FROM sessions WHERE session_id = ?')
|
|
79
|
+
.prepare('SELECT session_id, project_path, created_at, merged_into FROM sessions WHERE session_id = ?')
|
|
76
80
|
.get(predecessorId);
|
|
77
81
|
|
|
78
82
|
if (!pred) {
|
|
@@ -85,9 +89,14 @@ export function mergeSpecificPredecessor(db, { newSessionId, predecessorId, now
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
const self = db
|
|
88
|
-
.prepare('SELECT created_at FROM sessions WHERE session_id = ?')
|
|
92
|
+
.prepare('SELECT project_path, created_at FROM sessions WHERE session_id = ?')
|
|
89
93
|
.get(newSessionId);
|
|
90
94
|
|
|
95
|
+
if (self && !sameProjectPath(pred.project_path, self.project_path)) {
|
|
96
|
+
db.exec('COMMIT');
|
|
97
|
+
return { merged: false, skipReason: 'project_mismatch' };
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
// 時系列単調制約: 前任は新セッションより created_at が古いこと。
|
|
92
101
|
// バトンが自分より新しい session を指していたら(異常データ)merge しない。
|
|
93
102
|
if (self && pred.created_at >= self.created_at) {
|
|
@@ -151,6 +151,33 @@ test('mergeSpecificPredecessor: predecessor already merged into third session',
|
|
|
151
151
|
assert.equal(result.skipReason, 'already_merged');
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
test('mergeSpecificPredecessor: refuses to move memory across projects', () => {
|
|
155
|
+
const db = makeDb();
|
|
156
|
+
insertSession(db, 'bot-a-old', 100, null, '/bots/bot-a');
|
|
157
|
+
insertSession(db, 'bot-b-new', 200, null, '/bots/bot-b');
|
|
158
|
+
db.prepare(
|
|
159
|
+
`INSERT INTO bodies
|
|
160
|
+
(session_id, origin_session_id, turn_number, role, text, created_at)
|
|
161
|
+
VALUES ('bot-a-old', 'bot-a-old', 1, 'user', 'A_PRIVATE_MEMORY', 100)`,
|
|
162
|
+
).run();
|
|
163
|
+
|
|
164
|
+
const result = mergeSpecificPredecessor(db, {
|
|
165
|
+
newSessionId: 'bot-b-new',
|
|
166
|
+
predecessorId: 'bot-a-old',
|
|
167
|
+
now: 200,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
assert.deepEqual(result, { merged: false, skipReason: 'project_mismatch' });
|
|
171
|
+
assert.equal(
|
|
172
|
+
db.prepare('SELECT session_id FROM bodies WHERE text = ?').get('A_PRIVATE_MEMORY').session_id,
|
|
173
|
+
'bot-a-old',
|
|
174
|
+
);
|
|
175
|
+
assert.equal(
|
|
176
|
+
db.prepare('SELECT merged_into FROM sessions WHERE session_id = ?').get('bot-a-old').merged_into,
|
|
177
|
+
null,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
154
181
|
test('mergeSpecificPredecessor: refuses predecessor with created_at >= self', () => {
|
|
155
182
|
const db = makeDb();
|
|
156
183
|
insertSession(db, 'new', 100);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SPIKE ONLY — Phase 0-2 / 0-4 検証用。本実装ではない。
|
|
3
3
|
*
|
|
4
|
-
* docs/10_transcript_injection_plan.md Phase 0-2 で
|
|
4
|
+
* docs/archive/10_transcript_injection_plan.md Phase 0-2 で
|
|
5
5
|
* 「`/clear` 直後の SessionStart hook 内で transcript_path に L2 を user/assistant
|
|
6
6
|
* role 付きで append すると、Claude が次の short prompt の文脈として読むか」を実機検証する。
|
|
7
7
|
*
|
|
File without changes
|
|
File without changes
|
/package/docs/{BUGHUB_RUNTIME_ERROR_STORE_PLAN.md → archive/BUGHUB_RUNTIME_ERROR_STORE_PLAN.md}
RENAMED
|
File without changes
|