throughline 0.4.8 → 0.4.10
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 +64 -0
- package/README.ja.md +1 -1
- package/README.md +9 -6
- package/bin/throughline.mjs +3 -3
- package/docs/PUBLIC_RELEASE_PLAN.md +4 -4
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +1 -1
- package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +1 -1
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +14 -14
- package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +7 -7
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +18 -10
- package/docs/throughline-rollback-context-trim-insight.md +7 -5
- package/package.json +1 -1
- package/src/cli/codex-handoff-smoke.mjs +1 -1
- package/src/cli/codex-hook.test.mjs +4 -4
- package/src/cli/doctor.mjs +117 -3
- package/src/cli/doctor.test.mjs +25 -1
- package/src/cli/install.mjs +2 -2
- package/src/cli/trim.mjs +0 -9
- package/src/codex-app-server.mjs +60 -19
- package/src/codex-app-server.test.mjs +45 -0
- package/src/codex-auto-refresh.mjs +1 -1
- package/src/codex-auto-refresh.test.mjs +4 -4
- package/src/codex-handoff-smoke.mjs +15 -12
- package/src/codex-handoff-smoke.test.mjs +25 -6
- package/src/codex-handoff.mjs +82 -58
- package/src/codex-handoff.test.mjs +69 -28
- package/src/codex-resume.test.mjs +11 -2
- package/src/handoff-record.mjs +51 -8
- package/src/l3-summary.mjs +72 -0
- package/src/resume-context.mjs +58 -56
- package/src/resume-context.test.mjs +332 -36
- package/src/trim-cli.test.mjs +19 -11
package/src/cli/doctor.mjs
CHANGED
|
@@ -397,6 +397,89 @@ function countCapturedCodexSessions(db, projectPath) {
|
|
|
397
397
|
}
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
function parseCodexTrustedHookState(configText) {
|
|
401
|
+
const trustedKeys = new Set();
|
|
402
|
+
let currentKey = null;
|
|
403
|
+
let currentTrusted = false;
|
|
404
|
+
|
|
405
|
+
function flush() {
|
|
406
|
+
if (currentKey && currentTrusted) trustedKeys.add(currentKey);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (const line of configText.split(/\r?\n/)) {
|
|
410
|
+
const section = line.match(/^\s*\[hooks\.state\."([^"]+)"\]\s*$/);
|
|
411
|
+
if (section) {
|
|
412
|
+
flush();
|
|
413
|
+
currentKey = section[1];
|
|
414
|
+
currentTrusted = false;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (!currentKey) continue;
|
|
418
|
+
if (/^\s*\[/.test(line)) {
|
|
419
|
+
flush();
|
|
420
|
+
currentKey = null;
|
|
421
|
+
currentTrusted = false;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (/^\s*trusted_hash\s*=\s*"sha256:[^"]+"\s*$/.test(line)) {
|
|
425
|
+
currentTrusted = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
flush();
|
|
429
|
+
return trustedKeys;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function listHooksWithTrust(parsed, eventName, eventKey, hooksPath, trustedStateKeys) {
|
|
433
|
+
const groups = parsed.hooks?.[eventName] ?? [];
|
|
434
|
+
return groups.flatMap((group, groupIndex) =>
|
|
435
|
+
(group.hooks ?? []).map((hook, hookIndex) => {
|
|
436
|
+
const trustKey = `${hooksPath}:${eventKey}:${groupIndex}:${hookIndex}`;
|
|
437
|
+
return {
|
|
438
|
+
...hook,
|
|
439
|
+
throughlineDoctorTrustKey: trustKey,
|
|
440
|
+
throughlineDoctorTrusted: trustedStateKeys.has(trustKey),
|
|
441
|
+
};
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function summarizeHookTrust(hooks, { hooksFeatureEnabled, codexHooksFeatureEnabled } = {}) {
|
|
447
|
+
if (hooks.length === 0) {
|
|
448
|
+
return {
|
|
449
|
+
status: 'no managed hooks',
|
|
450
|
+
trustedCount: 0,
|
|
451
|
+
totalCount: 0,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const trustedCount = hooks.filter(h => h.throughlineDoctorTrusted).length;
|
|
455
|
+
if (trustedCount === hooks.length) {
|
|
456
|
+
return {
|
|
457
|
+
status: 'trusted',
|
|
458
|
+
trustedCount,
|
|
459
|
+
totalCount: hooks.length,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (hooksFeatureEnabled) {
|
|
463
|
+
return {
|
|
464
|
+
status: `${trustedCount}/${hooks.length} trusted - accept hooks in Codex menu`,
|
|
465
|
+
trustedCount,
|
|
466
|
+
totalCount: hooks.length,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (codexHooksFeatureEnabled) {
|
|
470
|
+
return {
|
|
471
|
+
status: 'not recorded (legacy codex_hooks)',
|
|
472
|
+
trustedCount,
|
|
473
|
+
totalCount: hooks.length,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
status: 'not trusted',
|
|
478
|
+
trustedCount,
|
|
479
|
+
totalCount: hooks.length,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
400
483
|
function readCodexHookDiagnosis(codexHome) {
|
|
401
484
|
const hooksPath = join(codexHome, 'hooks.json');
|
|
402
485
|
const configPath = join(codexHome, 'config.toml');
|
|
@@ -413,6 +496,12 @@ function readCodexHookDiagnosis(codexHome) {
|
|
|
413
496
|
featureEnabled: false,
|
|
414
497
|
codexHooksFeatureEnabled: false,
|
|
415
498
|
hooksFeatureEnabled: false,
|
|
499
|
+
trustedStateKeys: new Set(),
|
|
500
|
+
managedHookTrust: {
|
|
501
|
+
status: 'no managed hooks',
|
|
502
|
+
trustedCount: 0,
|
|
503
|
+
totalCount: 0,
|
|
504
|
+
},
|
|
416
505
|
managedPromptHooks: [],
|
|
417
506
|
legacyManagedPromptHooks: [],
|
|
418
507
|
managedPostToolUseHooks: [],
|
|
@@ -427,6 +516,7 @@ function readCodexHookDiagnosis(codexHome) {
|
|
|
427
516
|
out.codexHooksFeatureEnabled = /^\s*codex_hooks\s*=\s*true\s*$/m.test(config);
|
|
428
517
|
out.hooksFeatureEnabled = /^\s*hooks\s*=\s*true\s*$/m.test(config);
|
|
429
518
|
out.featureEnabled = out.codexHooksFeatureEnabled || out.hooksFeatureEnabled;
|
|
519
|
+
out.trustedStateKeys = parseCodexTrustedHookState(config);
|
|
430
520
|
} catch {
|
|
431
521
|
out.featureEnabled = false;
|
|
432
522
|
}
|
|
@@ -441,9 +531,21 @@ function readCodexHookDiagnosis(codexHome) {
|
|
|
441
531
|
}
|
|
442
532
|
|
|
443
533
|
out.hooksReadable = true;
|
|
444
|
-
const promptHooks = (
|
|
445
|
-
|
|
446
|
-
|
|
534
|
+
const promptHooks = listHooksWithTrust(
|
|
535
|
+
parsed,
|
|
536
|
+
'UserPromptSubmit',
|
|
537
|
+
'user_prompt_submit',
|
|
538
|
+
hooksPath,
|
|
539
|
+
out.trustedStateKeys,
|
|
540
|
+
);
|
|
541
|
+
const postToolUseHooks = listHooksWithTrust(
|
|
542
|
+
parsed,
|
|
543
|
+
'PostToolUse',
|
|
544
|
+
'post_tool_use',
|
|
545
|
+
hooksPath,
|
|
546
|
+
out.trustedStateKeys,
|
|
547
|
+
);
|
|
548
|
+
const stopHooks = listHooksWithTrust(parsed, 'Stop', 'stop', hooksPath, out.trustedStateKeys);
|
|
447
549
|
out.managedPromptHooks = promptHooks.filter(h => isThroughlineCodexHookCommand(h.command));
|
|
448
550
|
out.legacyManagedPromptHooks = out.managedPromptHooks.filter(h => h.command !== expectedPromptCommand);
|
|
449
551
|
out.managedPostToolUseHooks = postToolUseHooks.filter(h => isThroughlineCodexPostToolUseCommand(h.command));
|
|
@@ -452,6 +554,14 @@ function readCodexHookDiagnosis(codexHome) {
|
|
|
452
554
|
);
|
|
453
555
|
out.managedStopHooks = stopHooks.filter(h => isThroughlineCodexStopCommand(h.command));
|
|
454
556
|
out.legacyManagedStopHooks = out.managedStopHooks.filter(h => h.command !== expectedStopCommand);
|
|
557
|
+
out.managedHookTrust = summarizeHookTrust(
|
|
558
|
+
[
|
|
559
|
+
...out.managedPromptHooks,
|
|
560
|
+
...out.managedPostToolUseHooks,
|
|
561
|
+
...out.managedStopHooks,
|
|
562
|
+
],
|
|
563
|
+
out,
|
|
564
|
+
);
|
|
455
565
|
return out;
|
|
456
566
|
}
|
|
457
567
|
|
|
@@ -484,6 +594,7 @@ function runCodexDiagnosis({
|
|
|
484
594
|
console.log(` project: ${cwd}`);
|
|
485
595
|
console.log(` CODEX_HOME: ${codexHome}`);
|
|
486
596
|
console.log(` Codex hooks feature: ${hookDiagnosis.featureEnabled ? 'enabled' : 'not enabled'}`);
|
|
597
|
+
console.log(` Codex hook trust: ${hookDiagnosis.managedHookTrust.status}`);
|
|
487
598
|
console.log(` Codex UserPrompt hook: ${
|
|
488
599
|
hookDiagnosis.managedPromptHooks.length === 0
|
|
489
600
|
? 'not registered'
|
|
@@ -496,6 +607,7 @@ function runCodexDiagnosis({
|
|
|
496
607
|
console.log(` command: ${h.command}`);
|
|
497
608
|
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
498
609
|
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
610
|
+
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
499
611
|
}
|
|
500
612
|
console.log(` Codex PostTool hook: ${
|
|
501
613
|
hookDiagnosis.managedPostToolUseHooks.length === 0
|
|
@@ -509,6 +621,7 @@ function runCodexDiagnosis({
|
|
|
509
621
|
console.log(` command: ${h.command}`);
|
|
510
622
|
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
511
623
|
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
624
|
+
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
512
625
|
}
|
|
513
626
|
console.log(` Codex Stop hook: ${
|
|
514
627
|
hookDiagnosis.managedStopHooks.length === 0
|
|
@@ -522,6 +635,7 @@ function runCodexDiagnosis({
|
|
|
522
635
|
console.log(` command: ${h.command}`);
|
|
523
636
|
console.log(` async: ${h.async === false ? 'false' : String(h.async)}`);
|
|
524
637
|
console.log(` timeoutSec: ${h.timeoutSec ?? '(default)'}`);
|
|
638
|
+
console.log(` trusted: ${h.throughlineDoctorTrusted ? 'yes' : 'no'}`);
|
|
525
639
|
}
|
|
526
640
|
console.log(` VSCode monitor task: ${monitorTaskDiagnosis.status}`);
|
|
527
641
|
if (monitorTaskDiagnosis.path) {
|
package/src/cli/doctor.test.mjs
CHANGED
|
@@ -381,17 +381,41 @@ test('readCodexHookDiagnosis detects Codex prompt and Stop hooks', () => {
|
|
|
381
381
|
2,
|
|
382
382
|
) + '\n',
|
|
383
383
|
);
|
|
384
|
-
|
|
384
|
+
const hooksPath = join(codexHome, 'hooks.json');
|
|
385
|
+
writeFileSync(
|
|
386
|
+
join(codexHome, 'config.toml'),
|
|
387
|
+
[
|
|
388
|
+
'[features]',
|
|
389
|
+
'codex_hooks = true',
|
|
390
|
+
'hooks = true',
|
|
391
|
+
'',
|
|
392
|
+
`[hooks.state."${hooksPath}:user_prompt_submit:0:0"]`,
|
|
393
|
+
'trusted_hash = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
|
|
394
|
+
'',
|
|
395
|
+
`[hooks.state."${hooksPath}:post_tool_use:0:0"]`,
|
|
396
|
+
'trusted_hash = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"',
|
|
397
|
+
'',
|
|
398
|
+
`[hooks.state."${hooksPath}:stop:0:0"]`,
|
|
399
|
+
'trusted_hash = "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"',
|
|
400
|
+
'',
|
|
401
|
+
].join('\n'),
|
|
402
|
+
);
|
|
385
403
|
|
|
386
404
|
const diagnosis = readCodexHookDiagnosis(codexHome);
|
|
387
405
|
assert.equal(diagnosis.featureEnabled, true);
|
|
388
406
|
assert.equal(diagnosis.codexHooksFeatureEnabled, true);
|
|
389
407
|
assert.equal(diagnosis.hooksFeatureEnabled, true);
|
|
408
|
+
assert.equal(diagnosis.managedHookTrust.status, 'trusted');
|
|
409
|
+
assert.equal(diagnosis.managedHookTrust.trustedCount, 3);
|
|
410
|
+
assert.equal(diagnosis.managedHookTrust.totalCount, 3);
|
|
390
411
|
assert.equal(diagnosis.managedPromptHooks.length, 1);
|
|
412
|
+
assert.equal(diagnosis.managedPromptHooks[0].throughlineDoctorTrusted, true);
|
|
391
413
|
assert.equal(diagnosis.legacyManagedPromptHooks.length, 1);
|
|
392
414
|
assert.equal(diagnosis.managedPostToolUseHooks.length, 1);
|
|
415
|
+
assert.equal(diagnosis.managedPostToolUseHooks[0].throughlineDoctorTrusted, true);
|
|
393
416
|
assert.equal(diagnosis.legacyManagedPostToolUseHooks.length, 1);
|
|
394
417
|
assert.equal(diagnosis.managedStopHooks.length, 1);
|
|
418
|
+
assert.equal(diagnosis.managedStopHooks[0].throughlineDoctorTrusted, true);
|
|
395
419
|
assert.equal(diagnosis.legacyManagedStopHooks.length, 1);
|
|
396
420
|
} finally {
|
|
397
421
|
rmSync(codexHome, { recursive: true, force: true });
|
package/src/cli/install.mjs
CHANGED
|
@@ -490,8 +490,8 @@ export async function run(args = []) {
|
|
|
490
490
|
console.log(' Stop → throughline process-turn (L1 要約 + L2 本文保存 + L3 詳細保存)');
|
|
491
491
|
console.log(' UserPromptSubmit → throughline prompt-submit (/tl & /clear バトン書き込み)');
|
|
492
492
|
if (codex) {
|
|
493
|
-
console.log(` Codex UserPromptSubmit → ${buildCodexUserPromptSubmitHookCommand()} (
|
|
494
|
-
console.log(` Codex PostToolUse → ${buildCodexPostToolUseHookCommand()} (tool loop 中も
|
|
493
|
+
console.log(` Codex UserPromptSubmit → ${buildCodexUserPromptSubmitHookCommand()} (75% 到達時に current session へ $throughline 指示を注入)`);
|
|
494
|
+
console.log(` Codex PostToolUse → ${buildCodexPostToolUseHookCommand()} (tool loop 中も 75% 到達時に $throughline 指示を注入)`);
|
|
495
495
|
console.log(` Codex Stop → ${buildCodexStopHookCommand()} (Codex rollout capture + L1 要約)`);
|
|
496
496
|
}
|
|
497
497
|
console.log('');
|
package/src/cli/trim.mjs
CHANGED
|
@@ -366,15 +366,6 @@ async function runPreflight(parsed, plan) {
|
|
|
366
366
|
expectedTurns: expectedCodexAppServerTurns(plan),
|
|
367
367
|
command,
|
|
368
368
|
});
|
|
369
|
-
const turnCountStatus = preflight.turnCountCheck?.status;
|
|
370
|
-
if (turnCountStatus === 'mismatch' || turnCountStatus === 'unknown') {
|
|
371
|
-
return {
|
|
372
|
-
status: 'preflight-refused',
|
|
373
|
-
reason: preflight.turnCountCheck.reason,
|
|
374
|
-
plan,
|
|
375
|
-
preflight,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
369
|
|
|
379
370
|
return {
|
|
380
371
|
status: 'preflight-ready',
|
package/src/codex-app-server.mjs
CHANGED
|
@@ -209,6 +209,48 @@ export function buildThreadRollbackRequest({ id, threadId, numTurns }) {
|
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
export function resolveRollbackTurnsForAppServer({
|
|
213
|
+
plannedRollbackTurns,
|
|
214
|
+
expectedTurns = null,
|
|
215
|
+
readTurns = null,
|
|
216
|
+
resumedTurns = null,
|
|
217
|
+
} = {}) {
|
|
218
|
+
if (!Number.isInteger(plannedRollbackTurns) || plannedRollbackTurns < 1) {
|
|
219
|
+
throw new Error('resolveRollbackTurnsForAppServer: plannedRollbackTurns must be an integer >= 1');
|
|
220
|
+
}
|
|
221
|
+
assertOptionalTurnCount(expectedTurns, 'resolveRollbackTurnsForAppServer: expectedTurns');
|
|
222
|
+
|
|
223
|
+
const result = {
|
|
224
|
+
plannedRollbackTurns,
|
|
225
|
+
requestedRollbackTurns: plannedRollbackTurns,
|
|
226
|
+
adjustment: 0,
|
|
227
|
+
basis: 'planned',
|
|
228
|
+
reason: 'using_planned_rollback_turns',
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
Number.isInteger(expectedTurns) &&
|
|
233
|
+
Number.isInteger(readTurns) &&
|
|
234
|
+
Number.isInteger(resumedTurns) &&
|
|
235
|
+
readTurns === resumedTurns &&
|
|
236
|
+
readTurns !== expectedTurns
|
|
237
|
+
) {
|
|
238
|
+
const adjustment = readTurns - expectedTurns;
|
|
239
|
+
const adjustedTurns = plannedRollbackTurns + adjustment;
|
|
240
|
+
if (adjustedTurns >= 1) {
|
|
241
|
+
return {
|
|
242
|
+
plannedRollbackTurns,
|
|
243
|
+
requestedRollbackTurns: Math.min(adjustedTurns, readTurns),
|
|
244
|
+
adjustment,
|
|
245
|
+
basis: 'app_server_turn_count',
|
|
246
|
+
reason: 'adjusted_by_app_server_turn_delta',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
212
254
|
export function buildThreadInjectItemsRequest({ id, threadId, items }) {
|
|
213
255
|
assertRequestId(id, 'buildThreadInjectItemsRequest');
|
|
214
256
|
assertNonEmptyString(threadId, 'buildThreadInjectItemsRequest: threadId');
|
|
@@ -313,6 +355,12 @@ export async function runCodexTrimPreflight({
|
|
|
313
355
|
);
|
|
314
356
|
const readTurns = countTurns(beforeRead);
|
|
315
357
|
const resumedTurns = countTurns(resumed);
|
|
358
|
+
const rollbackResolution = resolveRollbackTurnsForAppServer({
|
|
359
|
+
plannedRollbackTurns: rollbackTurns,
|
|
360
|
+
expectedTurns,
|
|
361
|
+
readTurns,
|
|
362
|
+
resumedTurns,
|
|
363
|
+
});
|
|
316
364
|
|
|
317
365
|
return {
|
|
318
366
|
status: 'preflight-ready',
|
|
@@ -321,6 +369,8 @@ export async function runCodexTrimPreflight({
|
|
|
321
369
|
injectSent: false,
|
|
322
370
|
readTurns,
|
|
323
371
|
resumedTurns,
|
|
372
|
+
rollbackRequestedTurns: rollbackResolution.requestedRollbackTurns,
|
|
373
|
+
rollbackResolution,
|
|
324
374
|
turnCountCheck: compareTurnCounts({
|
|
325
375
|
expectedTurns,
|
|
326
376
|
readTurns,
|
|
@@ -329,7 +379,7 @@ export async function runCodexTrimPreflight({
|
|
|
329
379
|
rollbackRequestPreview: buildThreadRollbackRequest({
|
|
330
380
|
id: 'rollback-preview',
|
|
331
381
|
threadId,
|
|
332
|
-
numTurns:
|
|
382
|
+
numTurns: rollbackResolution.requestedRollbackTurns,
|
|
333
383
|
}),
|
|
334
384
|
notifications: [...new Set(client.notifications)],
|
|
335
385
|
stderr: client.stderr,
|
|
@@ -964,27 +1014,17 @@ export async function runCodexTrimExecution({
|
|
|
964
1014
|
readTurns,
|
|
965
1015
|
resumedTurns,
|
|
966
1016
|
});
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
injectSent: false,
|
|
974
|
-
injectedItems: 0,
|
|
975
|
-
readTurns,
|
|
976
|
-
resumedTurns,
|
|
977
|
-
rollbackRequestedTurns: rollbackTurns,
|
|
978
|
-
turnCountCheck,
|
|
979
|
-
notifications: [...new Set(client.notifications)],
|
|
980
|
-
stderr: client.stderr,
|
|
981
|
-
};
|
|
982
|
-
}
|
|
1017
|
+
const rollbackResolution = resolveRollbackTurnsForAppServer({
|
|
1018
|
+
plannedRollbackTurns: rollbackTurns,
|
|
1019
|
+
expectedTurns,
|
|
1020
|
+
readTurns,
|
|
1021
|
+
resumedTurns,
|
|
1022
|
+
});
|
|
983
1023
|
const rollback = await client.request(
|
|
984
1024
|
buildThreadRollbackRequest({
|
|
985
1025
|
id: randomUUID(),
|
|
986
1026
|
threadId,
|
|
987
|
-
numTurns:
|
|
1027
|
+
numTurns: rollbackResolution.requestedRollbackTurns,
|
|
988
1028
|
}),
|
|
989
1029
|
);
|
|
990
1030
|
const inject = await client.request(
|
|
@@ -1016,7 +1056,8 @@ export async function runCodexTrimExecution({
|
|
|
1016
1056
|
injectedItems: 1,
|
|
1017
1057
|
readTurns,
|
|
1018
1058
|
resumedTurns,
|
|
1019
|
-
rollbackRequestedTurns:
|
|
1059
|
+
rollbackRequestedTurns: rollbackResolution.requestedRollbackTurns,
|
|
1060
|
+
rollbackResolution,
|
|
1020
1061
|
rollbackResultTurns,
|
|
1021
1062
|
injectResultTurns,
|
|
1022
1063
|
afterTurns: postInjectRead.turns,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
compareTurnCounts,
|
|
20
20
|
encodeAppServerMessage,
|
|
21
21
|
parseAppServerLine,
|
|
22
|
+
resolveRollbackTurnsForAppServer,
|
|
22
23
|
runCodexModelVisibilitySmoke,
|
|
23
24
|
runCodexRollbackModelVisiblePrepare,
|
|
24
25
|
runCodexRollbackModelVisibleVerify,
|
|
@@ -203,6 +204,50 @@ test('compareTurnCounts: rejects invalid expected turn count', () => {
|
|
|
203
204
|
);
|
|
204
205
|
});
|
|
205
206
|
|
|
207
|
+
test('resolveRollbackTurnsForAppServer adjusts planned rollback by app-server turn delta', () => {
|
|
208
|
+
assert.deepEqual(
|
|
209
|
+
resolveRollbackTurnsForAppServer({
|
|
210
|
+
plannedRollbackTurns: 6,
|
|
211
|
+
expectedTurns: 6,
|
|
212
|
+
readTurns: 7,
|
|
213
|
+
resumedTurns: 7,
|
|
214
|
+
}),
|
|
215
|
+
{
|
|
216
|
+
plannedRollbackTurns: 6,
|
|
217
|
+
requestedRollbackTurns: 7,
|
|
218
|
+
adjustment: 1,
|
|
219
|
+
basis: 'app_server_turn_count',
|
|
220
|
+
reason: 'adjusted_by_app_server_turn_delta',
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
assert.deepEqual(
|
|
225
|
+
resolveRollbackTurnsForAppServer({
|
|
226
|
+
plannedRollbackTurns: 2,
|
|
227
|
+
expectedTurns: 22,
|
|
228
|
+
readTurns: 21,
|
|
229
|
+
resumedTurns: 21,
|
|
230
|
+
}),
|
|
231
|
+
{
|
|
232
|
+
plannedRollbackTurns: 2,
|
|
233
|
+
requestedRollbackTurns: 1,
|
|
234
|
+
adjustment: -1,
|
|
235
|
+
basis: 'app_server_turn_count',
|
|
236
|
+
reason: 'adjusted_by_app_server_turn_delta',
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
assert.equal(
|
|
241
|
+
resolveRollbackTurnsForAppServer({
|
|
242
|
+
plannedRollbackTurns: 6,
|
|
243
|
+
expectedTurns: 6,
|
|
244
|
+
readTurns: 7,
|
|
245
|
+
resumedTurns: 8,
|
|
246
|
+
}).requestedRollbackTurns,
|
|
247
|
+
6,
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
206
251
|
test('summarizeAppServerStderr: compacts repeated unknown-turn item warnings', () => {
|
|
207
252
|
const stderr = [
|
|
208
253
|
'2026-05-06T00:00:00Z WARN codex_app_server_protocol::protocol::thread_history: dropping turn-scoped item for unknown turn id `turn-a` item_id="call_1"',
|
|
@@ -2,7 +2,7 @@ import { runCodexTrimExecution } from './codex-app-server.mjs';
|
|
|
2
2
|
import { buildCodexRolloutTrimSource } from './codex-rollout-memory.mjs';
|
|
3
3
|
import { buildTrimPlan } from './trim-model.mjs';
|
|
4
4
|
|
|
5
|
-
export const CODEX_AUTO_REFRESH_THRESHOLD = 0.
|
|
5
|
+
export const CODEX_AUTO_REFRESH_THRESHOLD = 0.75;
|
|
6
6
|
|
|
7
7
|
export function evaluateCodexAutoRefreshUsage(usage, { threshold = CODEX_AUTO_REFRESH_THRESHOLD } = {}) {
|
|
8
8
|
if (!usage) {
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
runCodexAutoRefresh,
|
|
8
8
|
} from './codex-auto-refresh.mjs';
|
|
9
9
|
|
|
10
|
-
test('evaluateCodexAutoRefreshUsage: default threshold is
|
|
11
|
-
assert.equal(CODEX_AUTO_REFRESH_THRESHOLD, 0.
|
|
10
|
+
test('evaluateCodexAutoRefreshUsage: default threshold is 75%', () => {
|
|
11
|
+
assert.equal(CODEX_AUTO_REFRESH_THRESHOLD, 0.75);
|
|
12
12
|
const below = evaluateCodexAutoRefreshUsage({
|
|
13
|
-
tokens:
|
|
13
|
+
tokens: 193_799,
|
|
14
14
|
contextWindowSize: 258_400,
|
|
15
15
|
estimated: false,
|
|
16
16
|
contextWindowEstimated: false,
|
|
@@ -19,7 +19,7 @@ test('evaluateCodexAutoRefreshUsage: default threshold is 80%', () => {
|
|
|
19
19
|
assert.equal(below.reason, 'below_threshold');
|
|
20
20
|
|
|
21
21
|
const atThreshold = evaluateCodexAutoRefreshUsage({
|
|
22
|
-
tokens:
|
|
22
|
+
tokens: 193_800,
|
|
23
23
|
contextWindowSize: 258_400,
|
|
24
24
|
estimated: false,
|
|
25
25
|
contextWindowEstimated: false,
|
|
@@ -13,9 +13,12 @@ function addCheck(checks, { id, status, reason }) {
|
|
|
13
13
|
checks.push({ id, status, reason });
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* 新仕様: 各 L1/L2 行末尾の `(詳細:…)` suffix を抽出する。
|
|
18
|
+
* 旧版の `throughline detail HH:MM:SS` 列挙は廃止 (groupL3ByTurn が turn 単位で集約)。
|
|
19
|
+
*/
|
|
20
|
+
function findDetailSuffixes(text) {
|
|
21
|
+
return text.match(/\(詳細:[^)]+\)/g) ?? [];
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export function buildCodexHandoffSmoke(
|
|
@@ -39,8 +42,7 @@ export function buildCodexHandoffSmoke(
|
|
|
39
42
|
maxBodyChars,
|
|
40
43
|
});
|
|
41
44
|
const checks = [];
|
|
42
|
-
const
|
|
43
|
-
const uniqueDetailCommands = new Set(detailCommands);
|
|
45
|
+
const detailSuffixes = findDetailSuffixes(prompt);
|
|
44
46
|
|
|
45
47
|
addCheck(checks, {
|
|
46
48
|
id: 'new_thread_handoff_header',
|
|
@@ -85,11 +87,11 @@ export function buildCodexHandoffSmoke(
|
|
|
85
87
|
status: prompt.includes('## Throughline: Active Work Context') ? 'fail' : 'pass',
|
|
86
88
|
reason: 'fresh-thread smoke must not accidentally use the full active-work renderer',
|
|
87
89
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
// 旧 `detail_commands_deduplicated` check は L3 の literal command 列挙を
|
|
91
|
+
// dedup するためのもの。新版は groupL3ByTurn が構造的に turn 単位に集約するため
|
|
92
|
+
// 不要。「L3 が存在すれば suffix も存在する」は localizeL3Part の挙動次第で
|
|
93
|
+
// 偽陽性 (例: tool_output だけの turn は label が null で suffix 空) になるので
|
|
94
|
+
// smoke check には入れない。
|
|
93
95
|
addCheck(checks, {
|
|
94
96
|
id: 'prompt_size_within_limit',
|
|
95
97
|
status: prompt.length <= maxPromptChars ? 'pass' : 'fail',
|
|
@@ -111,8 +113,9 @@ export function buildCodexHandoffSmoke(
|
|
|
111
113
|
l1Summaries: record.memory.l1Summaries.length,
|
|
112
114
|
recentBodies: record.memory.recentBodies.length,
|
|
113
115
|
l3References: record.references.l3.length,
|
|
114
|
-
|
|
115
|
-
uniqueRenderedDetailCommands
|
|
116
|
+
// 新仕様: per-line `(詳細:…)` suffix の出現回数 (turn 単位に集約済み)。
|
|
117
|
+
// 旧 renderedDetailCommands / uniqueRenderedDetailCommands は廃止。
|
|
118
|
+
renderedDetailSuffixes: detailSuffixes.length,
|
|
116
119
|
checks,
|
|
117
120
|
};
|
|
118
121
|
|
|
@@ -68,7 +68,11 @@ test('buildCodexHandoffSmoke: fails when prompt exceeds max size', () => {
|
|
|
68
68
|
);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
test('buildCodexHandoffSmoke:
|
|
71
|
+
test('buildCodexHandoffSmoke: aggregates same-turn L3 into a single inline (詳細:…) suffix', () => {
|
|
72
|
+
// 旧版は L3 を独立 `### Detail References` セクションで列挙し、同一 detail command
|
|
73
|
+
// を dedup していた。新版は groupL3ByTurn が turn 単位に集約して L2 ターンの
|
|
74
|
+
// 最終 role 行に inline suffix を 1 つ貼る。結果として「同 turn の tool_input +
|
|
75
|
+
// tool_output」は (詳細:exec_command) 1 件になる。
|
|
72
76
|
const detailRefs = [
|
|
73
77
|
{
|
|
74
78
|
kind: 'tool_input',
|
|
@@ -90,14 +94,29 @@ test('buildCodexHandoffSmoke: reports rendered detail command deduplication', ()
|
|
|
90
94
|
},
|
|
91
95
|
];
|
|
92
96
|
|
|
93
|
-
const
|
|
97
|
+
const record = makeRecord({ detailRefs });
|
|
98
|
+
// L3 と turn key が一致する L2 行に上書き (smoke の makeRecord 既定では
|
|
99
|
+
// recentBodies が originSessionId/turnNumber を持たない)
|
|
100
|
+
record.memory.recentBodies = [
|
|
101
|
+
{
|
|
102
|
+
time: '12:00:02',
|
|
103
|
+
role: 'assistant',
|
|
104
|
+
text: 'body of turn 1',
|
|
105
|
+
originSessionId: 'codex:thread-smoke',
|
|
106
|
+
turnNumber: 1,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const result = buildCodexHandoffSmoke(record, { includePrompt: true });
|
|
94
111
|
|
|
95
112
|
assert.equal(result.status, 'ready');
|
|
96
113
|
assert.equal(result.l3References, 2);
|
|
97
|
-
|
|
98
|
-
assert.equal(result.
|
|
114
|
+
// 2 件の L3 (tool_input + tool_output) は同一 turn なので 1 件の suffix にまとまる
|
|
115
|
+
assert.equal(result.renderedDetailSuffixes, 1);
|
|
116
|
+
assert.match(result.prompt, /\(詳細:exec_command\)/);
|
|
117
|
+
// 旧 detail_commands_deduplicated check は廃止 (構造的に重複しない)
|
|
99
118
|
assert.equal(
|
|
100
|
-
result.checks.find((check) => check.id === 'detail_commands_deduplicated')
|
|
101
|
-
|
|
119
|
+
result.checks.find((check) => check.id === 'detail_commands_deduplicated'),
|
|
120
|
+
undefined,
|
|
102
121
|
);
|
|
103
122
|
});
|