throughline 0.4.9 → 0.4.11
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 +35 -3
- package/README.ja.md +27 -0
- package/README.md +58 -31
- package/bin/throughline.mjs +4 -2
- package/codex/skills/throughline/SKILL.md +30 -29
- package/codex/skills/throughline/agents/openai.yaml +2 -2
- package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +1 -1
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +19 -19
- package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +6 -6
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +28 -17
- package/docs/throughline-rollback-context-trim-insight.md +15 -9
- package/package.json +2 -2
- package/src/cli/codex-handoff-model-smoke.mjs +7 -7
- package/src/cli/codex-handoff-smoke.mjs +7 -7
- package/src/cli/codex-handoff-start.mjs +184 -10
- package/src/cli/codex-handoff-start.test.mjs +80 -1
- package/src/cli/codex-hook.mjs +37 -110
- package/src/cli/codex-hook.test.mjs +147 -32
- package/src/cli/codex-resume.mjs +7 -7
- package/src/cli/codex-summarize.mjs +7 -7
- package/src/cli/handoff-preview.mjs +6 -6
- package/src/cli/help.test.mjs +3 -1
- package/src/cli/install.mjs +42 -6
- package/src/cli/install.test.mjs +39 -5
- package/src/cli/trim.mjs +0 -9
- package/src/codex-app-server.mjs +216 -19
- package/src/codex-app-server.test.mjs +53 -0
- package/src/codex-auto-refresh.mjs +361 -29
- package/src/codex-auto-refresh.test.mjs +357 -0
- package/src/codex-thread-index.mjs +8 -2
- package/src/project-path.mjs +20 -0
- package/src/test-env.mjs +4 -0
- package/src/trim-cli.test.mjs +19 -11
- package/src/trim-model.mjs +6 -6
|
@@ -3,7 +3,10 @@ import { buildHandoffRecord } from '../handoff-record.mjs';
|
|
|
3
3
|
import { renderCodexNewThreadHandoff } from '../codex-handoff.mjs';
|
|
4
4
|
import { buildCodexHandoffSmoke } from '../codex-handoff-smoke.mjs';
|
|
5
5
|
import { buildCodexHandoffModelSmokePrompt } from '../codex-handoff-model-smoke.mjs';
|
|
6
|
+
import { runCodexNewThreadHandoff } from '../codex-app-server.mjs';
|
|
6
7
|
import { estimateTokens } from '../token-estimator.mjs';
|
|
8
|
+
import { sameProjectPath } from '../project-path.mjs';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
7
10
|
|
|
8
11
|
async function readStdin() {
|
|
9
12
|
let raw = '';
|
|
@@ -43,6 +46,11 @@ function parseArgs(args) {
|
|
|
43
46
|
maxDetailRefs: undefined,
|
|
44
47
|
maxRecentBodies: undefined,
|
|
45
48
|
maxBodyChars: undefined,
|
|
49
|
+
execute: false,
|
|
50
|
+
openHost: 'auto',
|
|
51
|
+
codexAppServerBin: null,
|
|
52
|
+
timeoutMs: 120_000,
|
|
53
|
+
requestTimeoutMs: 60_000,
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -67,6 +75,24 @@ function parseArgs(args) {
|
|
|
67
75
|
out.maxRecentBodies = parseNonNegativeInteger(args, ++i, '--max-recent-bodies');
|
|
68
76
|
} else if (arg === '--max-body-chars') {
|
|
69
77
|
out.maxBodyChars = parseNonNegativeInteger(args, ++i, '--max-body-chars');
|
|
78
|
+
} else if (arg === '--execute') {
|
|
79
|
+
out.execute = true;
|
|
80
|
+
} else if (arg === '--open-host') {
|
|
81
|
+
const value = args[++i];
|
|
82
|
+
if (!['auto', 'vscode', 'cli', 'none'].includes(value)) {
|
|
83
|
+
throw new Error('--open-host must be auto, vscode, cli, or none');
|
|
84
|
+
}
|
|
85
|
+
out.openHost = value;
|
|
86
|
+
} else if (arg === '--codex-app-server-bin') {
|
|
87
|
+
const value = args[++i];
|
|
88
|
+
if (!value || value.startsWith('-')) {
|
|
89
|
+
throw new Error('--codex-app-server-bin requires a command path');
|
|
90
|
+
}
|
|
91
|
+
out.codexAppServerBin = value;
|
|
92
|
+
} else if (arg === '--timeout-ms') {
|
|
93
|
+
out.timeoutMs = parsePositiveInteger(args, ++i, '--timeout-ms');
|
|
94
|
+
} else if (arg === '--request-timeout-ms') {
|
|
95
|
+
out.requestTimeoutMs = parsePositiveInteger(args, ++i, '--request-timeout-ms');
|
|
70
96
|
} else if (!arg.startsWith('-') && !out.sessionId) {
|
|
71
97
|
out.sessionId = arg;
|
|
72
98
|
} else {
|
|
@@ -78,16 +104,15 @@ function parseArgs(args) {
|
|
|
78
104
|
}
|
|
79
105
|
|
|
80
106
|
function findLatestCodexSessionId(db, projectPath) {
|
|
81
|
-
const
|
|
107
|
+
const rows = db
|
|
82
108
|
.prepare(
|
|
83
|
-
`SELECT session_id
|
|
109
|
+
`SELECT session_id, project_path
|
|
84
110
|
FROM sessions
|
|
85
|
-
WHERE
|
|
86
|
-
|
|
87
|
-
ORDER BY updated_at DESC
|
|
88
|
-
LIMIT 1`,
|
|
111
|
+
WHERE session_id LIKE 'codex:%'
|
|
112
|
+
ORDER BY updated_at DESC`,
|
|
89
113
|
)
|
|
90
|
-
.
|
|
114
|
+
.all();
|
|
115
|
+
const row = rows.find((candidate) => sameProjectPath(candidate.project_path, projectPath));
|
|
91
116
|
return row?.session_id ?? null;
|
|
92
117
|
}
|
|
93
118
|
|
|
@@ -170,7 +195,9 @@ function buildGuidance({ sessionId, parsed, handoffSmoke, handoffPrompt }) {
|
|
|
170
195
|
reason: ready ? 'fresh_thread_handoff_start_ready' : 'handoff_smoke_not_ready',
|
|
171
196
|
sessionId,
|
|
172
197
|
mutatesCurrentThread: false,
|
|
173
|
-
startThreadManually:
|
|
198
|
+
startThreadManually: !parsed.execute,
|
|
199
|
+
execute: parsed.execute,
|
|
200
|
+
openHost: parsed.openHost,
|
|
174
201
|
handoffSmoke,
|
|
175
202
|
modelPromptChars: modelPrompt.length,
|
|
176
203
|
estimatedModelPromptTokens: estimateTokens(modelPrompt),
|
|
@@ -187,7 +214,9 @@ function buildGuidance({ sessionId, parsed, handoffSmoke, handoffPrompt }) {
|
|
|
187
214
|
...(parsed.memoStdin
|
|
188
215
|
? ['When replaying individual commands, pipe the same memo because they include --memo-stdin.']
|
|
189
216
|
: []),
|
|
190
|
-
|
|
217
|
+
parsed.execute
|
|
218
|
+
? 'This command will start a new Codex thread through app-server, inject handoff memory, then open the selected host.'
|
|
219
|
+
: 'Start a new Codex thread and provide the handoff prompt as the opening context.',
|
|
191
220
|
]
|
|
192
221
|
: [
|
|
193
222
|
'Fix the failing handoff smoke checks before starting a new Codex thread.',
|
|
@@ -204,12 +233,23 @@ function renderTextResult(result) {
|
|
|
204
233
|
lines.push(` reason: ${result.reason}`);
|
|
205
234
|
lines.push(` session: ${result.sessionId}`);
|
|
206
235
|
lines.push(` mutates thread: ${result.mutatesCurrentThread ? 'yes' : 'no'}`);
|
|
236
|
+
lines.push(` execute: ${result.execute ? 'yes' : 'no'}`);
|
|
237
|
+
lines.push(` open host: ${result.openHost}`);
|
|
207
238
|
lines.push(` handoff smoke: ${result.handoffSmoke.status}`);
|
|
208
239
|
lines.push(` prompt chars: ${result.handoffSmoke.promptChars}/${result.handoffSmoke.maxPromptChars}`);
|
|
209
240
|
lines.push(` model prompt: ${result.modelPromptChars}`);
|
|
210
241
|
if (result.memoReplayNote) {
|
|
211
242
|
lines.push(` memo replay: ${result.memoReplayNote}`);
|
|
212
243
|
}
|
|
244
|
+
if (result.newThread) {
|
|
245
|
+
lines.push(` new thread: ${result.newThread.threadId}`);
|
|
246
|
+
lines.push(` turn status: ${result.newThread.turnStatus}`);
|
|
247
|
+
}
|
|
248
|
+
if (result.open) {
|
|
249
|
+
lines.push(` open status: ${result.open.status}`);
|
|
250
|
+
lines.push(` vscode url: ${result.open.vscodeUrl}`);
|
|
251
|
+
lines.push(` resume command: ${result.open.resumeCommand}`);
|
|
252
|
+
}
|
|
213
253
|
lines.push('');
|
|
214
254
|
lines.push(' commands:');
|
|
215
255
|
lines.push(` structural smoke: ${result.commands.structuralSmoke}`);
|
|
@@ -228,6 +268,114 @@ function renderTextResult(result) {
|
|
|
228
268
|
return lines.join('\n');
|
|
229
269
|
}
|
|
230
270
|
|
|
271
|
+
function resolveOpenHost(host) {
|
|
272
|
+
if (host !== 'auto') return host;
|
|
273
|
+
if (
|
|
274
|
+
process.env.VSCODE_IPC_HOOK_CLI ||
|
|
275
|
+
process.env.VSCODE_IPC_HOOK ||
|
|
276
|
+
process.env.TERM_PROGRAM === 'vscode' ||
|
|
277
|
+
process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === 'codex_vscode'
|
|
278
|
+
) {
|
|
279
|
+
return 'vscode';
|
|
280
|
+
}
|
|
281
|
+
return 'cli';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function openStartedCodexThread({ threadId, host, cwd }) {
|
|
285
|
+
const resolvedHost = resolveOpenHost(host);
|
|
286
|
+
const vscodeUrl = `vscode://openai.chatgpt/local/${encodeURIComponent(threadId)}`;
|
|
287
|
+
const resumeCommand = `codex resume ${threadId} --no-alt-screen`;
|
|
288
|
+
if (resolvedHost === 'none') {
|
|
289
|
+
return {
|
|
290
|
+
status: 'skipped',
|
|
291
|
+
reason: 'open_host_none',
|
|
292
|
+
host: resolvedHost,
|
|
293
|
+
vscodeUrl,
|
|
294
|
+
resumeCommand,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (resolvedHost === 'vscode') {
|
|
299
|
+
const result =
|
|
300
|
+
process.platform === 'darwin'
|
|
301
|
+
? spawnSync('open', [vscodeUrl], { encoding: 'utf8' })
|
|
302
|
+
: process.platform === 'win32'
|
|
303
|
+
? spawnSync('cmd.exe', ['/c', 'start', '', vscodeUrl], { encoding: 'utf8' })
|
|
304
|
+
: spawnSync('xdg-open', [vscodeUrl], { encoding: 'utf8' });
|
|
305
|
+
if (result.status !== 0) {
|
|
306
|
+
return {
|
|
307
|
+
status: 'failed',
|
|
308
|
+
reason: 'vscode_deep_link_open_failed',
|
|
309
|
+
host: resolvedHost,
|
|
310
|
+
vscodeUrl,
|
|
311
|
+
resumeCommand,
|
|
312
|
+
error: (result.stderr || result.stdout || '').trim(),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
status: 'opened',
|
|
317
|
+
reason: 'vscode_deep_link_opened',
|
|
318
|
+
host: resolvedHost,
|
|
319
|
+
vscodeUrl,
|
|
320
|
+
resumeCommand,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (resolvedHost === 'cli') {
|
|
325
|
+
if (process.platform === 'darwin') {
|
|
326
|
+
const shellCommand = `cd ${shQuote(cwd)} && TERM=xterm-256color codex resume ${shQuote(threadId)} --no-alt-screen`;
|
|
327
|
+
const result = spawnSync('osascript', [], {
|
|
328
|
+
input: `tell application "Terminal"
|
|
329
|
+
activate
|
|
330
|
+
do script ${appleString(shellCommand)}
|
|
331
|
+
end tell
|
|
332
|
+
`,
|
|
333
|
+
encoding: 'utf8',
|
|
334
|
+
});
|
|
335
|
+
if (result.status !== 0) {
|
|
336
|
+
return {
|
|
337
|
+
status: 'failed',
|
|
338
|
+
reason: 'terminal_open_failed',
|
|
339
|
+
host: resolvedHost,
|
|
340
|
+
vscodeUrl,
|
|
341
|
+
resumeCommand,
|
|
342
|
+
error: (result.stderr || result.stdout || '').trim(),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
status: 'opened',
|
|
347
|
+
reason: 'terminal_resume_opened',
|
|
348
|
+
host: resolvedHost,
|
|
349
|
+
vscodeUrl,
|
|
350
|
+
resumeCommand,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
status: 'manual',
|
|
355
|
+
reason: 'cli_auto_open_unsupported_on_platform',
|
|
356
|
+
host: resolvedHost,
|
|
357
|
+
vscodeUrl,
|
|
358
|
+
resumeCommand,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
status: 'failed',
|
|
364
|
+
reason: 'unsupported_open_host',
|
|
365
|
+
host: resolvedHost,
|
|
366
|
+
vscodeUrl,
|
|
367
|
+
resumeCommand,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function shQuote(value) {
|
|
372
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function appleString(value) {
|
|
376
|
+
return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
377
|
+
}
|
|
378
|
+
|
|
231
379
|
export async function run(args) {
|
|
232
380
|
let parsed;
|
|
233
381
|
try {
|
|
@@ -280,9 +428,35 @@ export async function run(args) {
|
|
|
280
428
|
result.prompt = handoffPrompt;
|
|
281
429
|
}
|
|
282
430
|
|
|
431
|
+
if (parsed.execute && result.status === 'ready') {
|
|
432
|
+
const startResult = await runCodexNewThreadHandoff({
|
|
433
|
+
cwd: process.cwd(),
|
|
434
|
+
prompt: handoffPrompt,
|
|
435
|
+
command: parsed.codexAppServerBin ?? 'codex',
|
|
436
|
+
commandArgs: parsed.codexAppServerBin ? [] : ['app-server', '--listen', 'stdio://'],
|
|
437
|
+
timeoutMs: parsed.timeoutMs,
|
|
438
|
+
requestTimeoutMs: parsed.requestTimeoutMs,
|
|
439
|
+
waitForTurn: false,
|
|
440
|
+
delivery: 'developer-item',
|
|
441
|
+
});
|
|
442
|
+
const openResult = openStartedCodexThread({
|
|
443
|
+
threadId: startResult.threadId,
|
|
444
|
+
host: parsed.openHost,
|
|
445
|
+
cwd: process.cwd(),
|
|
446
|
+
});
|
|
447
|
+
result.status = startResult.status === 'started' && openResult.status !== 'failed' ? 'started' : 'started-unverified';
|
|
448
|
+
result.reason =
|
|
449
|
+
startResult.status === 'started' && openResult.status !== 'failed'
|
|
450
|
+
? 'new_thread_handoff_started'
|
|
451
|
+
: 'new_thread_handoff_started_but_open_unverified';
|
|
452
|
+
result.startThreadManually = openResult.status === 'manual';
|
|
453
|
+
result.newThread = startResult;
|
|
454
|
+
result.open = openResult;
|
|
455
|
+
}
|
|
456
|
+
|
|
283
457
|
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
284
458
|
else process.stdout.write(renderTextResult(result) + '\n');
|
|
285
|
-
process.exit(result.status === 'ready' ? 0 : 1);
|
|
459
|
+
process.exit(result.status === 'ready' || result.status === 'started' ? 0 : 1);
|
|
286
460
|
}
|
|
287
461
|
|
|
288
462
|
export const _internal = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { chmodSync, mkdtempSync, readFileSync, rmSync, 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';
|
|
@@ -16,6 +16,41 @@ function makeTempProject() {
|
|
|
16
16
|
return mkdtempSync(join(tmpdir(), 'tl-codex-handoff-start-project-'));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function makeFakeCodexAppServer(dir) {
|
|
20
|
+
const script = join(dir, 'fake-codex-app-server.mjs');
|
|
21
|
+
const log = join(dir, 'fake-codex-app-server.log');
|
|
22
|
+
writeFileSync(
|
|
23
|
+
script,
|
|
24
|
+
`#!/usr/bin/env node
|
|
25
|
+
import { appendFileSync } from 'node:fs';
|
|
26
|
+
import { createInterface } from 'node:readline';
|
|
27
|
+
const log = ${JSON.stringify(log)};
|
|
28
|
+
const rl = createInterface({ input: process.stdin });
|
|
29
|
+
function send(message) { process.stdout.write(JSON.stringify(message) + '\\n'); }
|
|
30
|
+
rl.on('line', (line) => {
|
|
31
|
+
const msg = JSON.parse(line);
|
|
32
|
+
appendFileSync(log, JSON.stringify(msg) + '\\n');
|
|
33
|
+
if (msg.method === 'initialized') return;
|
|
34
|
+
if (msg.method === 'initialize') {
|
|
35
|
+
send({ id: msg.id, result: { userAgent: 'fake-codex' } });
|
|
36
|
+
} else if (msg.method === 'thread/start') {
|
|
37
|
+
send({ id: msg.id, result: { thread: { id: '019e2000-0000-7000-8000-000000000001', turns: [] } } });
|
|
38
|
+
} else if (msg.method === 'thread/inject_items') {
|
|
39
|
+
send({ id: msg.id, result: { thread: { id: msg.params.threadId, turns: [] } } });
|
|
40
|
+
} else if (msg.method === 'turn/start') {
|
|
41
|
+
send({ id: msg.id, result: { turn: { id: 'turn-handoff' } } });
|
|
42
|
+
send({ method: 'item/agentMessage/delta', params: { threadId: msg.params.threadId, turnId: 'turn-handoff', itemId: 'item-1', delta: 'OK' } });
|
|
43
|
+
send({ method: 'turn/completed', params: { threadId: msg.params.threadId, turnId: 'turn-handoff' } });
|
|
44
|
+
} else {
|
|
45
|
+
send({ id: msg.id, error: { code: -32601, message: 'unknown method' } });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
`,
|
|
49
|
+
);
|
|
50
|
+
chmodSync(script, 0o755);
|
|
51
|
+
return { script, log };
|
|
52
|
+
}
|
|
53
|
+
|
|
19
54
|
async function seedDb(home, project) {
|
|
20
55
|
const originalHome = process.env.HOME;
|
|
21
56
|
const originalUserProfile = process.env.USERPROFILE;
|
|
@@ -192,3 +227,47 @@ test('codex-handoff-start propagates memo-stdin to replay commands in JSON guida
|
|
|
192
227
|
rmSync(home, { recursive: true, force: true });
|
|
193
228
|
}
|
|
194
229
|
});
|
|
230
|
+
|
|
231
|
+
test('codex-handoff-start execute creates a new app-server thread and can skip opening a host', async () => {
|
|
232
|
+
const home = makeTempHome();
|
|
233
|
+
const project = makeTempProject();
|
|
234
|
+
try {
|
|
235
|
+
await seedDb(home, project);
|
|
236
|
+
const { script, log } = makeFakeCodexAppServer(project);
|
|
237
|
+
const result = runStart(home, project, [
|
|
238
|
+
'--session',
|
|
239
|
+
'codex:thread-handoff-start',
|
|
240
|
+
'--execute',
|
|
241
|
+
'--open-host',
|
|
242
|
+
'none',
|
|
243
|
+
'--codex-app-server-bin',
|
|
244
|
+
script,
|
|
245
|
+
'--json',
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
assert.equal(result.status, 0, result.stderr);
|
|
249
|
+
const payload = JSON.parse(result.stdout);
|
|
250
|
+
assert.equal(payload.status, 'started');
|
|
251
|
+
assert.equal(payload.reason, 'new_thread_handoff_started');
|
|
252
|
+
assert.equal(payload.execute, true);
|
|
253
|
+
assert.equal(payload.startThreadManually, false);
|
|
254
|
+
assert.equal(payload.newThread.threadId, '019e2000-0000-7000-8000-000000000001');
|
|
255
|
+
assert.equal(payload.newThread.delivery, 'developer-item');
|
|
256
|
+
assert.equal(payload.newThread.injectSent, true);
|
|
257
|
+
assert.equal(payload.newThread.turnStatus, 'not-started');
|
|
258
|
+
assert.equal(payload.open.status, 'skipped');
|
|
259
|
+
assert.equal(payload.open.vscodeUrl, 'vscode://openai.chatgpt/local/019e2000-0000-7000-8000-000000000001');
|
|
260
|
+
assert.match(payload.open.resumeCommand, /codex resume 019e2000-0000-7000-8000-000000000001/);
|
|
261
|
+
|
|
262
|
+
const fakeLog = readFileSync(log, 'utf8');
|
|
263
|
+
assert.match(fakeLog, /"method":"thread\/start"/);
|
|
264
|
+
assert.match(fakeLog, /"sessionStartSource":"clear"/);
|
|
265
|
+
assert.match(fakeLog, /"method":"thread\/inject_items"/);
|
|
266
|
+
assert.doesNotMatch(fakeLog, /"method":"turn\/start"/);
|
|
267
|
+
assert.match(fakeLog, /## Throughline: New Codex Thread Handoff/);
|
|
268
|
+
assert.match(fakeLog, /latest handoff start body/);
|
|
269
|
+
} finally {
|
|
270
|
+
rmSync(project, { recursive: true, force: true });
|
|
271
|
+
rmSync(home, { recursive: true, force: true });
|
|
272
|
+
}
|
|
273
|
+
});
|
package/src/cli/codex-hook.mjs
CHANGED
|
@@ -71,36 +71,11 @@ function suppressExperimentalWarnings() {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function renderAutoRefreshContext({ usage, decision, threadId, eventName = 'UserPromptSubmit' }) {
|
|
84
|
-
const tokens = Number(usage?.tokens);
|
|
85
|
-
const contextWindowSize = Number(usage?.contextWindowSize);
|
|
86
|
-
const ratio = Number(decision?.ratio);
|
|
87
|
-
const percent = Number.isFinite(ratio) ? `${(ratio * 100).toFixed(1)}%` : 'unknown';
|
|
88
|
-
const timing =
|
|
89
|
-
eventName === 'PostToolUse'
|
|
90
|
-
? 'Before continuing the current tool loop, run the installed $throughline workflow now.'
|
|
91
|
-
: 'Before answering the user prompt, run the installed $throughline workflow now.';
|
|
92
|
-
return [
|
|
93
|
-
'[throughline] Codex context refresh threshold reached for this current session.',
|
|
94
|
-
`Thread: ${threadId}`,
|
|
95
|
-
`Usage: ${Number.isFinite(tokens) ? tokens : 'unknown'} / ${Number.isFinite(contextWindowSize) ? contextWindowSize : 'unknown'} (${percent}); threshold ${(decision.threshold * 100).toFixed(0)}%.`,
|
|
96
|
-
'',
|
|
97
|
-
timing,
|
|
98
|
-
'Execute exactly:',
|
|
99
|
-
'throughline trim --execute --host codex --all --json',
|
|
100
|
-
'',
|
|
101
|
-
'Report only the execution status, whether rollback/inject were sent, durable evidence if present, and the selected memory session; then continue from the refreshed context.',
|
|
102
|
-
'This trigger was computed from the current Codex rollout token_count, not from token-monitor output.',
|
|
103
|
-
].join('\n');
|
|
74
|
+
function codexAutoRefreshDisabledResult() {
|
|
75
|
+
return {
|
|
76
|
+
status: 'skipped',
|
|
77
|
+
reason: 'codex_auto_refresh_disabled',
|
|
78
|
+
};
|
|
104
79
|
}
|
|
105
80
|
|
|
106
81
|
async function captureCodexHookSession({
|
|
@@ -236,20 +211,20 @@ export async function runCodexStopHook({
|
|
|
236
211
|
ensureMonitorTask = null,
|
|
237
212
|
buildMonitorUsage = null,
|
|
238
213
|
runAutoRefresh = null,
|
|
214
|
+
autoRefreshStateStore = null,
|
|
239
215
|
} = {}) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
]);
|
|
216
|
+
void runAutoRefresh;
|
|
217
|
+
void autoRefreshStateStore;
|
|
218
|
+
const capturedState = await captureCodexHookSession({
|
|
219
|
+
args,
|
|
220
|
+
payload,
|
|
221
|
+
env,
|
|
222
|
+
db,
|
|
223
|
+
writeMonitorState,
|
|
224
|
+
ensureMonitorTask,
|
|
225
|
+
buildMonitorUsage,
|
|
226
|
+
summarize: true,
|
|
227
|
+
});
|
|
253
228
|
|
|
254
229
|
if (capturedState.status !== 'ok') {
|
|
255
230
|
return {
|
|
@@ -261,28 +236,6 @@ export async function runCodexStopHook({
|
|
|
261
236
|
};
|
|
262
237
|
}
|
|
263
238
|
|
|
264
|
-
let autoRefresh = null;
|
|
265
|
-
try {
|
|
266
|
-
autoRefresh = await (runAutoRefresh ?? runCodexAutoRefresh)({
|
|
267
|
-
db: capturedState.db,
|
|
268
|
-
threadId: capturedState.identity.codexThreadId,
|
|
269
|
-
codexThreadIdSource: capturedState.identity.codexThreadIdSource,
|
|
270
|
-
codexHome: capturedState.codexHome,
|
|
271
|
-
projectPath: capturedState.captured.projectPath ?? capturedState.projectPath,
|
|
272
|
-
sessionId: capturedState.captured.sessionId,
|
|
273
|
-
usage: capturedState.usage,
|
|
274
|
-
command: env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex',
|
|
275
|
-
});
|
|
276
|
-
} catch (err) {
|
|
277
|
-
const msg = err instanceof Error ? err.message : 'unknown';
|
|
278
|
-
autoRefresh = {
|
|
279
|
-
status: 'error',
|
|
280
|
-
reason: 'auto_refresh_failed',
|
|
281
|
-
message: msg,
|
|
282
|
-
};
|
|
283
|
-
process.stderr.write(`[codex-hook:auto-refresh] ${msg}\n`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
239
|
return {
|
|
287
240
|
status: 'ok',
|
|
288
241
|
reason: 'codex_rollout_captured',
|
|
@@ -290,7 +243,7 @@ export async function runCodexStopHook({
|
|
|
290
243
|
captured: capturedState.captured,
|
|
291
244
|
summarized: capturedState.summarized,
|
|
292
245
|
monitorState: capturedState.monitorState,
|
|
293
|
-
autoRefresh,
|
|
246
|
+
autoRefresh: codexAutoRefreshDisabledResult(),
|
|
294
247
|
};
|
|
295
248
|
}
|
|
296
249
|
|
|
@@ -302,6 +255,7 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
302
255
|
writeMonitorState = null,
|
|
303
256
|
ensureMonitorTask = null,
|
|
304
257
|
buildMonitorUsage = null,
|
|
258
|
+
autoRefreshStateStore = null,
|
|
305
259
|
} = {}) {
|
|
306
260
|
return runCodexContextRefreshInstructionHook({
|
|
307
261
|
eventName: 'UserPromptSubmit',
|
|
@@ -312,6 +266,7 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
312
266
|
writeMonitorState,
|
|
313
267
|
ensureMonitorTask,
|
|
314
268
|
buildMonitorUsage,
|
|
269
|
+
autoRefreshStateStore,
|
|
315
270
|
});
|
|
316
271
|
}
|
|
317
272
|
|
|
@@ -323,6 +278,7 @@ export async function runCodexPostToolUseHook({
|
|
|
323
278
|
writeMonitorState = null,
|
|
324
279
|
ensureMonitorTask = null,
|
|
325
280
|
buildMonitorUsage = null,
|
|
281
|
+
autoRefreshStateStore = null,
|
|
326
282
|
} = {}) {
|
|
327
283
|
return runCodexContextRefreshInstructionHook({
|
|
328
284
|
eventName: 'PostToolUse',
|
|
@@ -333,6 +289,7 @@ export async function runCodexPostToolUseHook({
|
|
|
333
289
|
writeMonitorState,
|
|
334
290
|
ensureMonitorTask,
|
|
335
291
|
buildMonitorUsage,
|
|
292
|
+
autoRefreshStateStore,
|
|
336
293
|
});
|
|
337
294
|
}
|
|
338
295
|
|
|
@@ -345,20 +302,20 @@ async function runCodexContextRefreshInstructionHook({
|
|
|
345
302
|
writeMonitorState = null,
|
|
346
303
|
ensureMonitorTask = null,
|
|
347
304
|
buildMonitorUsage = null,
|
|
305
|
+
autoRefreshStateStore = null,
|
|
348
306
|
} = {}) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
]);
|
|
307
|
+
void eventName;
|
|
308
|
+
void autoRefreshStateStore;
|
|
309
|
+
const capturedState = await captureCodexHookSession({
|
|
310
|
+
args,
|
|
311
|
+
payload,
|
|
312
|
+
env,
|
|
313
|
+
db,
|
|
314
|
+
writeMonitorState,
|
|
315
|
+
ensureMonitorTask,
|
|
316
|
+
buildMonitorUsage,
|
|
317
|
+
summarize: false,
|
|
318
|
+
});
|
|
362
319
|
|
|
363
320
|
if (capturedState.status !== 'ok') {
|
|
364
321
|
return {
|
|
@@ -371,41 +328,13 @@ async function runCodexContextRefreshInstructionHook({
|
|
|
371
328
|
};
|
|
372
329
|
}
|
|
373
330
|
|
|
374
|
-
const decision = evaluateCodexAutoRefreshUsage(capturedState.usage);
|
|
375
|
-
if (!decision.shouldRefresh) {
|
|
376
|
-
return {
|
|
377
|
-
status: 'ok',
|
|
378
|
-
reason: 'codex_rollout_captured',
|
|
379
|
-
codexThreadIdSource: capturedState.identity.codexThreadIdSource,
|
|
380
|
-
captured: capturedState.captured,
|
|
381
|
-
monitorState: capturedState.monitorState,
|
|
382
|
-
autoRefreshPrompt: {
|
|
383
|
-
status: 'skipped',
|
|
384
|
-
reason: decision.reason,
|
|
385
|
-
decision,
|
|
386
|
-
},
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const context = renderAutoRefreshContext({
|
|
391
|
-
usage: capturedState.usage,
|
|
392
|
-
decision,
|
|
393
|
-
threadId: capturedState.identity.codexThreadId,
|
|
394
|
-
eventName,
|
|
395
|
-
});
|
|
396
331
|
return {
|
|
397
332
|
status: 'ok',
|
|
398
333
|
reason: 'codex_rollout_captured',
|
|
399
334
|
codexThreadIdSource: capturedState.identity.codexThreadIdSource,
|
|
400
335
|
captured: capturedState.captured,
|
|
401
336
|
monitorState: capturedState.monitorState,
|
|
402
|
-
autoRefreshPrompt:
|
|
403
|
-
status: 'ready',
|
|
404
|
-
reason: 'threshold_reached',
|
|
405
|
-
decision,
|
|
406
|
-
context,
|
|
407
|
-
output: codexContextOutput(context, eventName),
|
|
408
|
-
},
|
|
337
|
+
autoRefreshPrompt: codexAutoRefreshDisabledResult(),
|
|
409
338
|
};
|
|
410
339
|
}
|
|
411
340
|
|
|
@@ -475,10 +404,8 @@ export async function run(argv = []) {
|
|
|
475
404
|
|
|
476
405
|
export const _internal = {
|
|
477
406
|
codexHomeFromTranscriptPath,
|
|
478
|
-
codexContextOutput,
|
|
479
407
|
parseArgs,
|
|
480
408
|
parsePayload,
|
|
481
|
-
renderAutoRefreshContext,
|
|
482
409
|
resolveCodexHookThreadIdentity,
|
|
483
410
|
};
|
|
484
411
|
|