throughline 0.10.0 → 0.10.2
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 +38 -1
- package/README.ja.md +1 -1
- package/README.md +2 -2
- package/docs/16_readonly_handoff_context_plan.md +2 -2
- package/package.json +1 -1
- package/src/auditor-context.mjs +4 -3
- package/src/cli/auditor-context.mjs +2 -1
- package/src/cli/codex-handoff-start.mjs +5 -23
- package/src/cli/codex-summarize.mjs +2 -1
- package/src/cli/codex-visibility-smoke.mjs +5 -4
- package/src/cli/doctor.mjs +7 -6
- package/src/cli/grok-continue.mjs +8 -21
- package/src/cli/runtime-errors.test.mjs +1 -1
- package/src/cli/trim.mjs +3 -2
- package/src/codex-auto-refresh.mjs +3 -3
- package/src/codex-capture.mjs +14 -18
- package/src/codex-handoff-model-smoke.mjs +1 -1
- package/src/codex-sidecar.mjs +3 -2
- package/src/completed-turn-receipts.mjs +6 -33
- package/src/completed-turn-receipts.test.mjs +1 -1
- package/src/haiku-summarizer.mjs +1 -1
- package/src/handoff-record.mjs +3 -1
- package/src/hosts/claude.mjs +27 -0
- package/src/hosts/codex.mjs +28 -0
- package/src/hosts/grok.mjs +96 -0
- package/src/{hook-envelope.test.mjs → hosts/grok.test.mjs} +1 -1
- package/src/hosts/identity.mjs +71 -0
- package/src/hosts/identity.test.mjs +65 -0
- package/src/hosts/index.mjs +37 -0
- package/src/os/app-dirs.mjs +25 -0
- package/src/os/macos-terminal.mjs +44 -0
- package/src/os/open-url.mjs +16 -0
- package/src/os/paths.mjs +16 -0
- package/src/os/shell.mjs +16 -0
- package/src/os/windows-acl.mjs +57 -0
- package/src/project-path.mjs +4 -4
- package/src/prompt-submit.mjs +23 -55
- package/src/prompt-submit.test.mjs +27 -10
- package/src/runtime-error-hook.test.mjs +1 -1
- package/src/runtime-error-store.mjs +9 -47
- package/src/runtime-error-store.test.mjs +1 -1
- package/src/session-start.mjs +12 -4
- package/src/state-file.mjs +6 -5
- package/src/token-monitor.mjs +7 -6
- package/src/trim-model.mjs +6 -5
- package/src/turn-processor.mjs +2 -2
- package/src/hook-envelope.mjs +0 -51
- /package/src/{portable-spawn-sync.mjs → os/portable-spawn-sync.mjs} +0 -0
- /package/src/{portable-spawn-sync.test.mjs → os/portable-spawn-sync.test.mjs} +0 -0
- /package/src/{windows-acl-test-helper.mjs → os/windows-acl-test-helper.mjs} +0 -0
|
@@ -4,9 +4,8 @@ import {
|
|
|
4
4
|
commandTextFromPrompt,
|
|
5
5
|
isBatonCommand,
|
|
6
6
|
isClearCommand,
|
|
7
|
-
launchGrokContinueAfterTl,
|
|
8
|
-
shouldLaunchGrokContinue,
|
|
9
7
|
} from './prompt-submit.mjs';
|
|
8
|
+
import { hostAdapterForSessionId } from './hosts/index.mjs';
|
|
10
9
|
|
|
11
10
|
test('isBatonCommand: bare /tl', () => {
|
|
12
11
|
assert.equal(isBatonCommand('/tl'), true);
|
|
@@ -79,16 +78,34 @@ test('commandTextFromPrompt unwraps Grok user_query and leaves bare Claude text'
|
|
|
79
78
|
);
|
|
80
79
|
});
|
|
81
80
|
|
|
82
|
-
test('
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
assert.
|
|
81
|
+
test('afterBatonWrite launches grok-continue only for Grok /tl', () => {
|
|
82
|
+
const noLaunch = () => {
|
|
83
|
+
throw new Error('must not launch');
|
|
84
|
+
};
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
hostAdapterForSessionId('grok:abc').afterBatonWrite({
|
|
87
|
+
trigger: 'clear', sessionId: 'grok:abc', cwd: '/work', continueRun: noLaunch,
|
|
88
|
+
}),
|
|
89
|
+
{ launched: false },
|
|
90
|
+
);
|
|
91
|
+
assert.deepEqual(
|
|
92
|
+
hostAdapterForSessionId('old-session').afterBatonWrite({
|
|
93
|
+
trigger: 'tl', sessionId: 'old-session', cwd: '/work', continueRun: noLaunch,
|
|
94
|
+
}),
|
|
95
|
+
{ launched: false },
|
|
96
|
+
);
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
hostAdapterForSessionId('codex:thread').afterBatonWrite({
|
|
99
|
+
trigger: 'tl', sessionId: 'codex:thread', cwd: '/work', continueRun: noLaunch,
|
|
100
|
+
}),
|
|
101
|
+
{ launched: false },
|
|
102
|
+
);
|
|
87
103
|
});
|
|
88
104
|
|
|
89
|
-
test('
|
|
105
|
+
test('Grok afterBatonWrite calls grok-continue with the source session', () => {
|
|
90
106
|
const calls = [];
|
|
91
|
-
const
|
|
107
|
+
const result = hostAdapterForSessionId('grok:abc').afterBatonWrite({
|
|
108
|
+
trigger: 'tl',
|
|
92
109
|
sessionId: 'grok:abc',
|
|
93
110
|
cwd: '/work/Throughline',
|
|
94
111
|
continueRun: (argv, opts) => {
|
|
@@ -96,7 +113,7 @@ test('launchGrokContinueAfterTl calls grok-continue with the source session', ()
|
|
|
96
113
|
return 0;
|
|
97
114
|
},
|
|
98
115
|
});
|
|
99
|
-
assert.
|
|
116
|
+
assert.deepEqual(result, { launched: true, exitCode: 0 });
|
|
100
117
|
assert.deepEqual(calls, [{
|
|
101
118
|
argv: ['--session', 'grok:abc'],
|
|
102
119
|
opts: { cwd: '/work/Throughline' },
|
|
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os';
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { defaultFactoryReporterConfigPath, defaultRuntimeErrorStorePath } from './runtime-error-store.mjs';
|
|
9
|
-
import { applyWindowsPrivateAcl } from './windows-acl-test-helper.mjs';
|
|
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));
|
|
12
12
|
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from 'node:fs';
|
|
12
12
|
import childProcess from 'node:child_process';
|
|
13
|
-
import { arch as hostArch,
|
|
13
|
+
import { arch as hostArch, platform as hostPlatform } from 'node:os';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { DatabaseSync } from 'node:sqlite';
|
|
18
|
+
import { applyAndVerifyWindowsAcl, isWindows, verifyWindowsAcl } from './os/windows-acl.mjs';
|
|
19
|
+
import { windowsLocalAppData, xdgConfigHome, xdgStateHome } from './os/app-dirs.mjs';
|
|
18
20
|
|
|
19
21
|
const require = createRequire(import.meta.url);
|
|
20
22
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
@@ -25,8 +27,6 @@ export const RUNTIME_ERROR_DIAGNOSTIC = '[throughline:runtime-errors] store_unav
|
|
|
25
27
|
const DEFAULT_SNAPSHOT_LIMIT = 256;
|
|
26
28
|
const BEST_EFFORT_TIMEOUT_MS = 750;
|
|
27
29
|
const WINDOWS_BEST_EFFORT_TIMEOUT_MS = 5_000;
|
|
28
|
-
// CI実測でPowerShellコールドスタートが3.0〜3.2秒に達しflakeしたため15秒 (run 29586852389 / 29628634501)
|
|
29
|
-
const WINDOWS_ACL_TIMEOUT_MS = 15_000;
|
|
30
30
|
const RESOLUTION_REASONS = new Set(['manual', 'recovered']);
|
|
31
31
|
const PRIVATE_DIRECTORY_CAPABILITY = Symbol('throughline.private-directory');
|
|
32
32
|
|
|
@@ -54,19 +54,17 @@ const DEFINITIONS = Object.freeze({
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
export function defaultFactoryReporterConfigPath(env = process.env) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return join(env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'dotagents', 'factory-reporter', 'config.json');
|
|
57
|
+
if (isWindows(env)) {
|
|
58
|
+
return join(windowsLocalAppData(env), 'dotagents', 'factory-reporter', 'config.json');
|
|
60
59
|
}
|
|
61
|
-
return join(env
|
|
60
|
+
return join(xdgConfigHome(env), 'dotagents', 'factory-reporter.json');
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
export function defaultRuntimeErrorStorePath(env = process.env) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return join(env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'throughline', 'runtime-errors.json');
|
|
64
|
+
if (isWindows(env)) {
|
|
65
|
+
return join(windowsLocalAppData(env), 'throughline', 'runtime-errors.json');
|
|
68
66
|
}
|
|
69
|
-
return join(env
|
|
67
|
+
return join(xdgStateHome(env), 'throughline', 'runtime-errors.json');
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
export function isRuntimeErrorCollectionEnabled({ env = process.env, configPath } = {}) {
|
|
@@ -412,10 +410,6 @@ function assertPrivateStoreFileShape(info) {
|
|
|
412
410
|
}
|
|
413
411
|
}
|
|
414
412
|
|
|
415
|
-
function isWindows(env = process.env) {
|
|
416
|
-
return env.OS === 'Windows_NT' || hostPlatform() === 'win32';
|
|
417
|
-
}
|
|
418
|
-
|
|
419
413
|
function isCanonicalFactoryReporterConfig(value) {
|
|
420
414
|
if (!isPlainObject(value) || !exactKeys(value, ['schema_version', 'host', 'collection', 'reporting']) || value.schema_version !== '1.0') return false;
|
|
421
415
|
if (!isPlainObject(value.host) || !exactKeys(value.host, ['id', 'profile']) ||
|
|
@@ -555,38 +549,6 @@ function assertPosixOwnerMode(info, expectedMode) {
|
|
|
555
549
|
if (typeof process.getuid === 'function' && info.uid !== process.getuid()) throw new Error('runtime error store owner unsafe');
|
|
556
550
|
}
|
|
557
551
|
|
|
558
|
-
function applyAndVerifyWindowsAcl(path, directory) {
|
|
559
|
-
runWindowsAclScript(path, directory, WINDOWS_ACL_APPLY_SCRIPT);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function verifyWindowsAcl(path, directory) {
|
|
563
|
-
runWindowsAclScript(path, directory, WINDOWS_ACL_VERIFY_SCRIPT);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function runWindowsAclScript(path, directory, script) {
|
|
567
|
-
const result = childProcess.spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
568
|
-
env: { ...process.env, FACTORY_ACL_PATH: path, FACTORY_ACL_DIRECTORY: directory ? '1' : '0' },
|
|
569
|
-
stdio: 'ignore', timeout: WINDOWS_ACL_TIMEOUT_MS, windowsHide: true,
|
|
570
|
-
});
|
|
571
|
-
if (result.status !== 0) throw new Error('Windows owner-only ACL verification failed');
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const WINDOWS_ACL_VERIFY_SCRIPT = String.raw`
|
|
575
|
-
$p=$env:FACTORY_ACL_PATH; $isDir=$env:FACTORY_ACL_DIRECTORY -eq '1'; $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
576
|
-
$acl=if($isDir){[System.IO.Directory]::GetAccessControl($p)}else{[System.IO.File]::GetAccessControl($p)}
|
|
577
|
-
$owner=$acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
|
578
|
-
if($owner -ne $sid){exit 41}; $rules=@($acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])); if($rules.Count -ne 1){exit 42}
|
|
579
|
-
$r=$rules[0]; if($r.IdentityReference.Value -ne $sid -or $r.AccessControlType -ne 'Allow' -or $r.IsInherited -or ($r.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl){exit 43}
|
|
580
|
-
`;
|
|
581
|
-
|
|
582
|
-
const WINDOWS_ACL_APPLY_SCRIPT = String.raw`
|
|
583
|
-
$p=$env:FACTORY_ACL_PATH; $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
584
|
-
$isDir=$env:FACTORY_ACL_DIRECTORY -eq '1'; $acl=if($isDir){New-Object System.Security.AccessControl.DirectorySecurity}else{New-Object System.Security.AccessControl.FileSecurity}; $acl.SetAccessRuleProtection($true,$false)
|
|
585
|
-
$flags=if($isDir){[System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'}else{[System.Security.AccessControl.InheritanceFlags]::None}
|
|
586
|
-
$rule=New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl',$flags,[System.Security.AccessControl.PropagationFlags]::None,[System.Security.AccessControl.AccessControlType]::Allow)
|
|
587
|
-
$acl.SetOwner($sid); $acl.AddAccessRule($rule); if($isDir){[System.IO.Directory]::SetAccessControl($p,$acl)}else{[System.IO.File]::SetAccessControl($p,$acl)}
|
|
588
|
-
` + WINDOWS_ACL_VERIFY_SCRIPT;
|
|
589
|
-
|
|
590
552
|
function assertExactInput(input, allowed) {
|
|
591
553
|
if (!input || typeof input !== 'object' || Array.isArray(input) ||
|
|
592
554
|
Object.keys(input).some((key) => !allowed.includes(key))) {
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
reopenRuntimeError,
|
|
17
17
|
resolveRuntimeError,
|
|
18
18
|
} from './runtime-error-store.mjs';
|
|
19
|
-
import { applyWindowsPrivateAcl, verifyWindowsPrivateAcl } from './windows-acl-test-helper.mjs';
|
|
19
|
+
import { applyWindowsPrivateAcl, verifyWindowsPrivateAcl } from './os/windows-acl-test-helper.mjs';
|
|
20
20
|
|
|
21
21
|
const TEST_PLATFORM = process.platform === 'win32' ? 'win32' : 'darwin';
|
|
22
22
|
|
package/src/session-start.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import { logDecision } from './decision-log.mjs';
|
|
|
32
32
|
import { existsSync } from 'node:fs';
|
|
33
33
|
import { pathToFileURL } from 'node:url';
|
|
34
34
|
import { recordRuntimeErrorBestEffort } from './runtime-error-store.mjs';
|
|
35
|
-
import { normalizeHookPayload } from './
|
|
35
|
+
import { NON_CLAUDE_SESSION_PREFIXES, normalizeHookPayload } from './hosts/index.mjs';
|
|
36
36
|
|
|
37
37
|
const ENV_DISABLE_AUTO_HANDOFF = 'THROUGHLINE_DISABLE_AUTO_HANDOFF';
|
|
38
38
|
|
|
@@ -55,18 +55,26 @@ function isAutoHandoffDisabled(env) {
|
|
|
55
55
|
* @returns {{ session_id: string } | null}
|
|
56
56
|
*/
|
|
57
57
|
function findLatestClaudePredecessor(db, projectPath, currentSessionId) {
|
|
58
|
+
// Claude 以外の host session (prefix 付き) を前任候補から除外する。
|
|
59
|
+
// prefix の正本は hosts/identity.mjs。
|
|
60
|
+
const nonClaudeExclusion = NON_CLAUDE_SESSION_PREFIXES
|
|
61
|
+
.map(() => 'AND session_id NOT LIKE ?')
|
|
62
|
+
.join('\n ');
|
|
58
63
|
const candidates = db
|
|
59
64
|
.prepare(
|
|
60
65
|
`SELECT session_id FROM sessions
|
|
61
66
|
WHERE lower(project_path) = lower(?)
|
|
62
67
|
AND merged_into IS NULL
|
|
63
68
|
AND session_id != ?
|
|
64
|
-
|
|
65
|
-
AND session_id NOT LIKE 'grok:%'
|
|
69
|
+
${nonClaudeExclusion}
|
|
66
70
|
ORDER BY updated_at DESC
|
|
67
71
|
LIMIT 5`,
|
|
68
72
|
)
|
|
69
|
-
.all(
|
|
73
|
+
.all(
|
|
74
|
+
projectPath,
|
|
75
|
+
currentSessionId,
|
|
76
|
+
...NON_CLAUDE_SESSION_PREFIXES.map((prefix) => `${prefix}%`),
|
|
77
|
+
);
|
|
70
78
|
|
|
71
79
|
if (candidates.length === 0) return null;
|
|
72
80
|
|
package/src/state-file.mjs
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs';
|
|
15
|
-
import { homedir
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
16
|
import { join, resolve } from 'node:path';
|
|
17
|
+
import { CLAUDE_HOST, KNOWN_STATE_HOSTS } from './hosts/identity.mjs';
|
|
18
|
+
import { foldPathCaseForPlatform } from './os/paths.mjs';
|
|
17
19
|
|
|
18
20
|
const STATE_DIR = join(homedir(), '.throughline', 'state');
|
|
19
21
|
|
|
@@ -31,8 +33,7 @@ export function normalizeProjectPath(p) {
|
|
|
31
33
|
if (!p) return '';
|
|
32
34
|
let result = resolve(p).replace(/\\/g, '/');
|
|
33
35
|
if (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1);
|
|
34
|
-
|
|
35
|
-
return result;
|
|
36
|
+
return foldPathCaseForPlatform(result);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -155,8 +156,8 @@ function stateFilename(sessionId) {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
function normalizeHost(host) {
|
|
158
|
-
if (host === undefined || host === null || host === '') return
|
|
159
|
-
if (host
|
|
159
|
+
if (host === undefined || host === null || host === '') return CLAUDE_HOST;
|
|
160
|
+
if (KNOWN_STATE_HOSTS.includes(host)) return host;
|
|
160
161
|
return 'unknown';
|
|
161
162
|
}
|
|
162
163
|
|
package/src/token-monitor.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { buildCodexMonitorUsage } from './codex-usage.mjs';
|
|
|
28
28
|
import { listCodexThreadCandidates } from './codex-thread-index.mjs';
|
|
29
29
|
import { readLatestUsage } from './transcript-usage.mjs';
|
|
30
30
|
import { startSizeQuery } from './terminal-size.mjs';
|
|
31
|
+
import { CODEX_HOST, CODEX_SESSION_PREFIX, codexSessionIdToThreadId } from './hosts/identity.mjs';
|
|
31
32
|
|
|
32
33
|
const REFRESH_MS = 1000;
|
|
33
34
|
// データ変化が無くても N ms ごとに再描画して「(24m ago)」表示を進める。
|
|
@@ -292,7 +293,7 @@ export function resolveColumns() {
|
|
|
292
293
|
function formatLine({ state, usage, isActive, now = Date.now() }) {
|
|
293
294
|
const project = basename(state.projectPath || '?');
|
|
294
295
|
const shortId = formatShortSessionId(state);
|
|
295
|
-
const host = state.host ===
|
|
296
|
+
const host = state.host === CODEX_HOST ? 'Codex' : state.host === 'unknown' ? 'Unknown' : 'Claude';
|
|
296
297
|
const tokens = usage?.tokens ?? 0;
|
|
297
298
|
const max = usage?.contextWindowSize ?? 200_000;
|
|
298
299
|
const ratio = max > 0 ? tokens / max : 0;
|
|
@@ -337,8 +338,8 @@ function formatLine({ state, usage, isActive, now = Date.now() }) {
|
|
|
337
338
|
|
|
338
339
|
function formatShortSessionId(state) {
|
|
339
340
|
const sessionId = String(state?.sessionId ?? '');
|
|
340
|
-
if (state?.host ===
|
|
341
|
-
return sessionId.slice(
|
|
341
|
+
if (state?.host === CODEX_HOST && sessionId.startsWith(CODEX_SESSION_PREFIX)) {
|
|
342
|
+
return (codexSessionIdToThreadId(sessionId) ?? '').slice(0, 8);
|
|
342
343
|
}
|
|
343
344
|
return sessionId.slice(0, 8);
|
|
344
345
|
}
|
|
@@ -372,7 +373,7 @@ function withLiveActivity(state, now = Date.now()) {
|
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
function resolveMonitorUsage(state) {
|
|
375
|
-
if (state.host ===
|
|
376
|
+
if (state.host === CODEX_HOST && state.rolloutPath) {
|
|
376
377
|
return buildCodexMonitorUsage(state.rolloutPath) ?? state.usage ?? null;
|
|
377
378
|
}
|
|
378
379
|
if (state.transcriptPath) {
|
|
@@ -410,8 +411,8 @@ function discoverCodexSessionStates(args = {}, cwd = process.cwd()) {
|
|
|
410
411
|
|
|
411
412
|
lastCodexDiscoveryError = null;
|
|
412
413
|
return candidates.map((candidate) => ({
|
|
413
|
-
sessionId:
|
|
414
|
-
host:
|
|
414
|
+
sessionId: `${CODEX_SESSION_PREFIX}${candidate.id}`,
|
|
415
|
+
host: CODEX_HOST,
|
|
415
416
|
projectPath: normalizeProjectPath(candidate.cwd ?? cwd),
|
|
416
417
|
transcriptPath: null,
|
|
417
418
|
rolloutPath: candidate.rolloutPath,
|
package/src/trim-model.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { inspectCodexPlannedRollbackRestoreSafety } from './codex-rollout-memory.mjs';
|
|
2
|
+
import { CLAUDE_HOST, CODEX_HOST } from './hosts/identity.mjs';
|
|
2
3
|
import { buildHandoffRecord, N_RECENT_L2 } from './handoff-record.mjs';
|
|
3
4
|
import { sameProjectPath } from './project-path.mjs';
|
|
4
5
|
import { estimateTokens } from './token-estimator.mjs';
|
|
@@ -48,7 +49,7 @@ export function findLatestSessionIdForProject(db, projectPath) {
|
|
|
48
49
|
|
|
49
50
|
function resolveDefaultSessionId({ sessionId, host, codexThreadId, db, projectPath }) {
|
|
50
51
|
if (sessionId) return sessionId;
|
|
51
|
-
if (host ===
|
|
52
|
+
if (host === CODEX_HOST) {
|
|
52
53
|
return codexThreadId ? `codex:${codexThreadId}` : null;
|
|
53
54
|
}
|
|
54
55
|
return findLatestSessionIdForProject(db, projectPath);
|
|
@@ -79,7 +80,7 @@ function countDistinctCapturedTurns(db, sessionId) {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export function describeTrimHost(host) {
|
|
82
|
-
if (host ===
|
|
83
|
+
if (host === CLAUDE_HOST) {
|
|
83
84
|
return {
|
|
84
85
|
host,
|
|
85
86
|
automaticRollback: false,
|
|
@@ -94,7 +95,7 @@ export function describeTrimHost(host) {
|
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
if (host ===
|
|
98
|
+
if (host === CODEX_HOST) {
|
|
98
99
|
return {
|
|
99
100
|
host,
|
|
100
101
|
automaticRollback: true,
|
|
@@ -123,7 +124,7 @@ export function describeTrimHost(host) {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
function buildSafeContinuation({ host, hostIdentity }) {
|
|
126
|
-
if (host !==
|
|
127
|
+
if (host !== CODEX_HOST) return null;
|
|
127
128
|
|
|
128
129
|
const threadId = hostIdentity?.codexThreadId ?? '<thread-id>';
|
|
129
130
|
const sessionId = threadId === '<thread-id>' ? 'codex:<thread-id>' : `codex:${threadId}`;
|
|
@@ -561,7 +562,7 @@ function buildPlanSession({ resolvedSessionId, session, trimSource, projectPath
|
|
|
561
562
|
}
|
|
562
563
|
|
|
563
564
|
function buildHostIdentity({ host, codexThreadId, codexThreadIdSource = null }) {
|
|
564
|
-
if (host !==
|
|
565
|
+
if (host !== CODEX_HOST) {
|
|
565
566
|
return {
|
|
566
567
|
host,
|
|
567
568
|
codexThreadId: null,
|
package/src/turn-processor.mjs
CHANGED
|
@@ -46,7 +46,7 @@ import { readLatestUsage } from './transcript-usage.mjs';
|
|
|
46
46
|
import { pathToFileURL } from 'node:url';
|
|
47
47
|
import { recordRuntimeErrorBestEffort } from './runtime-error-store.mjs';
|
|
48
48
|
import { writeCompletedTurnReceipt } from './completed-turn-receipts.mjs';
|
|
49
|
-
import { normalizeHookPayload } from './
|
|
49
|
+
import { hostAdapterForSessionId, normalizeHookPayload } from './hosts/index.mjs';
|
|
50
50
|
|
|
51
51
|
/** 直近 N ターンは bodies を生で残し、それより古いものだけ L1 要約する。 */
|
|
52
52
|
export const L2_WINDOW = 20;
|
|
@@ -205,7 +205,7 @@ export async function run() {
|
|
|
205
205
|
process.stderr.write(`[vscode-task] ${msg}\n`);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
if (
|
|
208
|
+
if (hostAdapterForSessionId(session_id).waitsForStopTranscriptFlush) {
|
|
209
209
|
await waitForClaudeStopTranscriptFlush({
|
|
210
210
|
transcriptPath: transcript_path,
|
|
211
211
|
lastAssistantMessage: last_assistant_message,
|
package/src/hook-envelope.mjs
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// Grok sends Claude-compatible hook commands with a camelCase wire
|
|
2
|
-
// (sessionId, hookEventName) and no session_id. Throughline treats that
|
|
3
|
-
// envelope as host=grok: normalize to the Claude snake_case contract and
|
|
4
|
-
// prefix session ids so they never mix with Claude predecessor search.
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
|
|
8
|
-
export const GROK_SESSION_PREFIX = 'grok:';
|
|
9
|
-
|
|
10
|
-
export function isGrokEnvelope(payload) {
|
|
11
|
-
return payload !== null
|
|
12
|
-
&& typeof payload === 'object'
|
|
13
|
-
&& typeof payload.sessionId === 'string'
|
|
14
|
-
&& payload.sessionId.length > 0
|
|
15
|
-
&& typeof payload.hookEventName === 'string'
|
|
16
|
-
&& payload.hookEventName.length > 0
|
|
17
|
-
&& !Object.hasOwn(payload, 'session_id');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function grokBareSessionId(sessionId) {
|
|
21
|
-
if (typeof sessionId !== 'string' || sessionId.length === 0) return null;
|
|
22
|
-
return sessionId.startsWith(GROK_SESSION_PREFIX)
|
|
23
|
-
? sessionId.slice(GROK_SESSION_PREFIX.length)
|
|
24
|
-
: sessionId;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function deriveGrokChatHistoryPath(projectPath, sessionId, { home = homedir() } = {}) {
|
|
28
|
-
const bare = grokBareSessionId(sessionId);
|
|
29
|
-
if (!projectPath || !bare) return null;
|
|
30
|
-
return join(home, '.grok', 'sessions', encodeURIComponent(projectPath), bare, 'chat_history.jsonl');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function normalizeHookPayload(payload, { home = homedir() } = {}) {
|
|
34
|
-
if (!isGrokEnvelope(payload)) return payload;
|
|
35
|
-
const cwd = typeof payload.cwd === 'string' && payload.cwd.length > 0
|
|
36
|
-
? payload.cwd
|
|
37
|
-
: (typeof payload.workspaceRoot === 'string' ? payload.workspaceRoot : undefined);
|
|
38
|
-
// Live Grok Stop sets transcriptPath to updates.jsonl (sessionUpdate frames,
|
|
39
|
-
// no user/assistant rows). L2 lives in chat_history.jsonl only.
|
|
40
|
-
const transcriptPath = deriveGrokChatHistoryPath(cwd, payload.sessionId, { home });
|
|
41
|
-
return {
|
|
42
|
-
...payload,
|
|
43
|
-
session_id: `${GROK_SESSION_PREFIX}${payload.sessionId}`,
|
|
44
|
-
cwd,
|
|
45
|
-
source: payload.source,
|
|
46
|
-
prompt: payload.prompt,
|
|
47
|
-
hook_event_name: payload.hookEventName,
|
|
48
|
-
transcript_path: transcriptPath,
|
|
49
|
-
last_assistant_message: payload.lastAssistantMessage,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|