ai-foreman 1.7.7 → 1.7.8
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/README.md +11 -1
- package/dist/activity.js +2 -2
- package/dist/adapters/claude.d.ts +29 -5
- package/dist/adapters/claude.js +340 -23
- package/dist/adapters/codex.d.ts +18 -1
- package/dist/adapters/codex.js +207 -11
- package/dist/adapters/recovering.d.ts +6 -1
- package/dist/adapters/recovering.js +9 -1
- package/dist/adapters/types.d.ts +66 -1
- package/dist/branch/currentGuard.d.ts +5 -1
- package/dist/branch/currentGuard.js +6 -0
- package/dist/cli/start.js +159 -21
- package/dist/continuity.d.ts +8 -2
- package/dist/continuity.js +72 -22
- package/dist/handoffs.js +223 -3
- package/dist/qaReview.js +4 -0
- package/dist/sessionLifecycle.d.ts +108 -14
- package/dist/sessionLifecycle.js +1034 -239
- package/dist/statusReporter.d.ts +26 -0
- package/dist/statusReporter.js +76 -16
- package/dist/workflowDb.d.ts +47 -2
- package/dist/workflowDb.js +149 -14
- package/package.json +4 -4
package/dist/cli/start.js
CHANGED
|
@@ -625,6 +625,13 @@ export function buildStartCommand() {
|
|
|
625
625
|
fast: builderFast,
|
|
626
626
|
systemPromptAppend: roleBundle.system || undefined,
|
|
627
627
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
628
|
+
contextManagementPolicy: {
|
|
629
|
+
role: "builder",
|
|
630
|
+
configuredThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
|
|
631
|
+
compactMaximum: roleDefaults.builder?.compact_maximum ?? 10,
|
|
632
|
+
settingsRevision,
|
|
633
|
+
model: model ?? "default",
|
|
634
|
+
},
|
|
628
635
|
};
|
|
629
636
|
const adapter = agent === "codex"
|
|
630
637
|
? new CodexAdapter(adapterOpts)
|
|
@@ -641,6 +648,7 @@ export function buildStartCommand() {
|
|
|
641
648
|
let activeContinuityRunId;
|
|
642
649
|
let liveStatusReporter;
|
|
643
650
|
let activeBuilderForStatus;
|
|
651
|
+
let activeBuilderContextSnapshot;
|
|
644
652
|
const resolvedContinuitySettings = (role) => role === "builder" ? {
|
|
645
653
|
role, source: roleDefaults.builder ? "project" : "provider", make: agent, model: model ?? "default",
|
|
646
654
|
reasoning: builderEffort ?? "default", fast: Boolean(builderFast), session_strategy: roleDefaults.builder?.session_strategy ?? "compact",
|
|
@@ -651,27 +659,70 @@ export function buildStartCommand() {
|
|
|
651
659
|
role, source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
|
|
652
660
|
reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact",
|
|
653
661
|
settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
|
|
654
|
-
auto_compact_threshold_percent:
|
|
662
|
+
auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
|
|
663
|
+
compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
664
|
+
};
|
|
665
|
+
const liveRoleFields = [
|
|
666
|
+
"make", "model", "reasoning", "fast", "session_strategy", "display_session_cost",
|
|
667
|
+
"auto_compact_threshold_percent", "compact_maximum",
|
|
668
|
+
];
|
|
669
|
+
const observedRoleDefaults = {
|
|
670
|
+
builder: { ...(roleDefaults.builder ?? {}) },
|
|
671
|
+
qa: { ...(roleDefaults.qa ?? {}) },
|
|
672
|
+
};
|
|
673
|
+
const observedProjectRevision = { builder: settingsRevision, qa: settingsRevision };
|
|
674
|
+
const effectiveRoleRevision = { builder: settingsRevision, qa: settingsRevision };
|
|
675
|
+
const liveRoleOverrides = {
|
|
676
|
+
builder: new Map(),
|
|
677
|
+
qa: new Map(),
|
|
655
678
|
};
|
|
656
679
|
const liveRoleSettings = (role, base) => {
|
|
657
680
|
const live = readAgentDefaults(cwd);
|
|
658
681
|
const revision = live.revision ?? 0;
|
|
659
|
-
if (revision <= base.settings_revision)
|
|
660
|
-
return base;
|
|
661
682
|
const defaults = role === "builder" ? live.builder : live.qa;
|
|
662
|
-
|
|
683
|
+
if (revision > observedProjectRevision[role]) {
|
|
684
|
+
const prior = observedRoleDefaults[role];
|
|
685
|
+
let roleChanged = false;
|
|
686
|
+
for (const field of liveRoleFields) {
|
|
687
|
+
const value = defaults?.[field];
|
|
688
|
+
if (!Object.is(value, prior[field])) {
|
|
689
|
+
liveRoleOverrides[role].set(field, value);
|
|
690
|
+
roleChanged = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const revisionDb = new WorkflowDb(cwd);
|
|
694
|
+
let targetedFields;
|
|
695
|
+
try {
|
|
696
|
+
targetedFields = revisionDb.projectSettingsRevisionTargets(revision)[role] ?? [];
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
revisionDb.close();
|
|
700
|
+
}
|
|
701
|
+
for (const field of targetedFields) {
|
|
702
|
+
if (liveRoleFields.includes(field)) {
|
|
703
|
+
const typedField = field;
|
|
704
|
+
liveRoleOverrides[role].set(typedField, defaults?.[typedField]);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
observedRoleDefaults[role] = { ...(defaults ?? {}) };
|
|
708
|
+
observedProjectRevision[role] = revision;
|
|
709
|
+
if (roleChanged || targetedFields.length)
|
|
710
|
+
effectiveRoleRevision[role] = revision;
|
|
711
|
+
}
|
|
712
|
+
if (effectiveRoleRevision[role] <= base.settings_revision)
|
|
713
|
+
return base;
|
|
714
|
+
const merged = {
|
|
663
715
|
...base,
|
|
664
716
|
source: defaults ? "project" : base.source,
|
|
665
|
-
|
|
666
|
-
model: defaults?.model ?? base.model,
|
|
667
|
-
reasoning: defaults?.reasoning ?? base.reasoning,
|
|
668
|
-
fast: defaults?.fast ?? base.fast,
|
|
669
|
-
session_strategy: defaults?.session_strategy ?? base.session_strategy,
|
|
670
|
-
display_session_cost: defaults?.display_session_cost ?? base.display_session_cost,
|
|
671
|
-
auto_compact_threshold_percent: defaults?.auto_compact_threshold_percent ?? base.auto_compact_threshold_percent,
|
|
672
|
-
compact_maximum: defaults?.compact_maximum ?? base.compact_maximum,
|
|
673
|
-
settings_revision: revision,
|
|
717
|
+
settings_revision: effectiveRoleRevision[role],
|
|
674
718
|
};
|
|
719
|
+
const mutable = merged;
|
|
720
|
+
for (const [field, value] of liveRoleOverrides[role]) {
|
|
721
|
+
if (value === undefined)
|
|
722
|
+
continue;
|
|
723
|
+
mutable[field] = value;
|
|
724
|
+
}
|
|
725
|
+
return merged;
|
|
675
726
|
};
|
|
676
727
|
const createBuilderForSettings = async (builderCwd, settings) => {
|
|
677
728
|
const ready = await ensureRuntimeReadyForCommand(builderCwd, settings.make, {
|
|
@@ -696,6 +747,10 @@ export function buildStartCommand() {
|
|
|
696
747
|
fast: settings.fast,
|
|
697
748
|
systemPromptAppend: roleBundle.system || undefined,
|
|
698
749
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
750
|
+
contextManagementPolicy: {
|
|
751
|
+
role: "builder", configuredThresholdPercent: settings.auto_compact_threshold_percent,
|
|
752
|
+
compactMaximum: settings.compact_maximum, settingsRevision: settings.settings_revision, model: settings.model,
|
|
753
|
+
},
|
|
699
754
|
};
|
|
700
755
|
const created = settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
|
|
701
756
|
return !branchMode ? new CurrentWorkflowGuardAdapter(created, builderCwd) : created;
|
|
@@ -722,6 +777,10 @@ export function buildStartCommand() {
|
|
|
722
777
|
fast: settings.fast,
|
|
723
778
|
systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
|
|
724
779
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
780
|
+
contextManagementPolicy: {
|
|
781
|
+
role: "qa", configuredThresholdPercent: settings.auto_compact_threshold_percent,
|
|
782
|
+
compactMaximum: settings.compact_maximum, settingsRevision: settings.settings_revision, model: settings.model,
|
|
783
|
+
},
|
|
725
784
|
};
|
|
726
785
|
return settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
|
|
727
786
|
};
|
|
@@ -928,6 +987,13 @@ export function buildStartCommand() {
|
|
|
928
987
|
fast: qaFast,
|
|
929
988
|
systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
|
|
930
989
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
990
|
+
contextManagementPolicy: {
|
|
991
|
+
role: "qa",
|
|
992
|
+
configuredThresholdPercent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
|
|
993
|
+
compactMaximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
994
|
+
settingsRevision,
|
|
995
|
+
model: qaModel ?? "default",
|
|
996
|
+
},
|
|
931
997
|
};
|
|
932
998
|
return qaAgent === "codex"
|
|
933
999
|
? new CodexAdapter(adapterOpts)
|
|
@@ -1048,6 +1114,7 @@ export function buildStartCommand() {
|
|
|
1048
1114
|
finally {
|
|
1049
1115
|
db.close();
|
|
1050
1116
|
} },
|
|
1117
|
+
contextSnapshot: activeBuilderContextSnapshot,
|
|
1051
1118
|
});
|
|
1052
1119
|
});
|
|
1053
1120
|
};
|
|
@@ -1151,6 +1218,7 @@ export function buildStartCommand() {
|
|
|
1151
1218
|
projectDir: cwd,
|
|
1152
1219
|
runId: activeContinuityRunId,
|
|
1153
1220
|
role: "qa",
|
|
1221
|
+
requireNativeContextManagement: true,
|
|
1154
1222
|
initialSettings: qaSettings,
|
|
1155
1223
|
historicalCountUncertain: Boolean(recoveryRecord && (recoveryRecord.legacy || !recoveryRecord.runDecisions)),
|
|
1156
1224
|
readSettings: () => liveRoleSettings("qa", qaSettings),
|
|
@@ -1163,6 +1231,7 @@ export function buildStartCommand() {
|
|
|
1163
1231
|
report: (event) => {
|
|
1164
1232
|
currentActivity()?.update(event.kind, event.detail, { provider: qaSettings.make, model: qaSettings.model });
|
|
1165
1233
|
log.write("context-lifecycle", { role: "qa", ...event, sample: event.sample });
|
|
1234
|
+
void liveStatusReporter?.tick();
|
|
1166
1235
|
},
|
|
1167
1236
|
handoff: async ({ reason, adapter: predecessor, sample, settings, compactionCount }) => {
|
|
1168
1237
|
const nativeUsage = await predecessor.sessionUsage?.().catch(() => undefined);
|
|
@@ -1183,6 +1252,7 @@ export function buildStartCommand() {
|
|
|
1183
1252
|
return decorateQa(transfer.successor, qaCwd, undefined, settings);
|
|
1184
1253
|
},
|
|
1185
1254
|
});
|
|
1255
|
+
liveStatusReporter?.updateState({ contextSnapshot: () => controller.contextSnapshot() });
|
|
1186
1256
|
return (await controller.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter;
|
|
1187
1257
|
};
|
|
1188
1258
|
const qaNonconvergence = createQaNonconvergenceHandler(cwd, Boolean(opts.yes), roleDefaults.planner);
|
|
@@ -1286,12 +1356,68 @@ export function buildStartCommand() {
|
|
|
1286
1356
|
});
|
|
1287
1357
|
let auditBuilder;
|
|
1288
1358
|
let auditViewer;
|
|
1359
|
+
let auditCompleted = false;
|
|
1289
1360
|
const auditRunId = `audit-${stamp}`;
|
|
1290
1361
|
activeContinuityRunId = auditRunId;
|
|
1291
1362
|
try {
|
|
1363
|
+
const auditSettings = resolvedContinuitySettings("builder");
|
|
1364
|
+
const auditInstruction = buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket));
|
|
1365
|
+
const auditDb = new WorkflowDb(cwd);
|
|
1366
|
+
try {
|
|
1367
|
+
auditDb.ensureRun(auditRunId);
|
|
1368
|
+
auditDb.appendContinuityEvent({
|
|
1369
|
+
runId: auditRunId, role: "host", kind: "branch_audit_initialized",
|
|
1370
|
+
payload: { frozenActionDigest: createHash("sha256").update(auditInstruction).digest("hex") },
|
|
1371
|
+
authoritativeStateRevision: auditSettings.settings_revision,
|
|
1372
|
+
});
|
|
1373
|
+
auditDb.publishContinuityCheckpoint({
|
|
1374
|
+
runId: auditRunId, role: "builder", authoritativeStateRevision: auditSettings.settings_revision,
|
|
1375
|
+
delta: {
|
|
1376
|
+
version: 1, decisions: [], constraints: [], discoveries: [], completedActions: [], evidence: [], failures: [], blockers: [],
|
|
1377
|
+
openWork: ["complete the selected-ticket dependency audit"], nextAction: "complete the selected-ticket dependency audit",
|
|
1378
|
+
},
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
finally {
|
|
1382
|
+
auditDb.close();
|
|
1383
|
+
}
|
|
1292
1384
|
auditBuilder = await createBuilder(cwd);
|
|
1385
|
+
const auditController = RoleSessionController.managed({
|
|
1386
|
+
projectDir: cwd, runId: auditRunId, role: "builder", initialSettings: auditSettings,
|
|
1387
|
+
requireNativeContextManagement: true,
|
|
1388
|
+
readSettings: () => liveRoleSettings("builder", auditSettings),
|
|
1389
|
+
settingsBoundary: (input) => applyBuilderSettingsBoundary(auditRunId, cwd, input),
|
|
1390
|
+
report: (event) => log.write("context-lifecycle", { phase: "branch-audit", ...event, sample: event.sample }),
|
|
1391
|
+
handoff: async ({ reason, adapter, sample, settings, compactionCount, frozenAction }) => {
|
|
1392
|
+
const nativeUsage = await adapter.sessionUsage?.().catch(() => undefined);
|
|
1393
|
+
const transfer = await new HandoffService(cwd).transfer({
|
|
1394
|
+
runId: auditRunId, role: "builder", reason, predecessorSessionId: adapter.sessionId(), predecessorSessionRef: adapter.sessionRef?.(),
|
|
1395
|
+
roleState: { phase: "branch-audit", frozenActionDigest: createHash("sha256").update(frozenAction).digest("hex"), contextSample: sample },
|
|
1396
|
+
...(nativeUsage ? { sessionUsage: {
|
|
1397
|
+
version: 1, runId: auditRunId, role: "builder", provider: adapter.agent,
|
|
1398
|
+
providerSessionId: adapter.sessionId(), observedAt: nativeUsage.observedAt, source: nativeUsage.source,
|
|
1399
|
+
cumulativeInputTokens: nativeUsage.inputTokens, cumulativeOutputTokens: nativeUsage.outputTokens,
|
|
1400
|
+
cumulativeTotalTokens: nativeUsage.totalTokens, authoritativeCostUsd: nativeUsage.authoritativeCostUsd,
|
|
1401
|
+
} } : {}),
|
|
1402
|
+
compactionCount, compactMaximum: settings.compact_maximum,
|
|
1403
|
+
resources: [{ label: "frozen-branch-audit", content: frozenAction, authoritative: true }],
|
|
1404
|
+
}, () => createBuilderForSettings(cwd, settings));
|
|
1405
|
+
log.write("handoff-transfer", {
|
|
1406
|
+
phase: "branch-audit", reason, generation: transfer.manifest.generation,
|
|
1407
|
+
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
1408
|
+
acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest,
|
|
1409
|
+
});
|
|
1410
|
+
if (adapter instanceof ContinuityAdapter) {
|
|
1411
|
+
await adapter.adoptValidatedSuccessor(transfer.successor);
|
|
1412
|
+
return adapter;
|
|
1413
|
+
}
|
|
1414
|
+
await adapter.close().catch(() => { });
|
|
1415
|
+
return continuousBuilder(transfer.successor, cwd, auditRunId, settings);
|
|
1416
|
+
},
|
|
1417
|
+
});
|
|
1418
|
+
auditBuilder = (await auditController.atSafeBoundary(auditBuilder, auditInstruction)).adapter;
|
|
1293
1419
|
auditViewer = printEvents(auditBuilder.events());
|
|
1294
|
-
const audit = await auditBuilder.sendTurn(
|
|
1420
|
+
const audit = await auditBuilder.sendTurn(auditInstruction);
|
|
1295
1421
|
if (audit.isError)
|
|
1296
1422
|
throw new Error(audit.text);
|
|
1297
1423
|
const auditDependencies = parseAuditDependencies(audit.text);
|
|
@@ -1305,6 +1431,7 @@ export function buildStartCommand() {
|
|
|
1305
1431
|
ticketIds: selectedNewTicket ? [selectedNewTicket] : selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
|
|
1306
1432
|
});
|
|
1307
1433
|
auditDependencyCount = auditDependencies.length;
|
|
1434
|
+
auditCompleted = true;
|
|
1308
1435
|
}
|
|
1309
1436
|
finally {
|
|
1310
1437
|
await auditBuilder?.close().catch(() => { });
|
|
@@ -1312,7 +1439,9 @@ export function buildStartCommand() {
|
|
|
1312
1439
|
const auditDb = new WorkflowDb(cwd);
|
|
1313
1440
|
try {
|
|
1314
1441
|
if (auditDb.getRun(auditRunId))
|
|
1315
|
-
auditDb.transition(auditRunId,
|
|
1442
|
+
auditDb.transition(auditRunId, auditCompleted
|
|
1443
|
+
? { status: "completed", checkpoint: "branch-audit-complete", remainingWork: {}, event: "audit_completed" }
|
|
1444
|
+
: { status: "paused", checkpoint: "branch-audit-interrupted", remainingWork: { action: "complete the selected-ticket dependency audit" }, event: "audit_interrupted" });
|
|
1316
1445
|
}
|
|
1317
1446
|
finally {
|
|
1318
1447
|
auditDb.close();
|
|
@@ -1416,7 +1545,8 @@ export function buildStartCommand() {
|
|
|
1416
1545
|
role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
|
|
1417
1546
|
reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
1418
1547
|
display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
|
|
1419
|
-
auto_compact_threshold_percent:
|
|
1548
|
+
auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
|
|
1549
|
+
compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
1420
1550
|
};
|
|
1421
1551
|
const firstResumeRef = plan.nodes[0] ? resumeSessionByTicket.get(plan.nodes[0].ticket.id)?.sessionRef : undefined;
|
|
1422
1552
|
let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null }) : createBuildRun({
|
|
@@ -1432,6 +1562,7 @@ export function buildStartCommand() {
|
|
|
1432
1562
|
return existing;
|
|
1433
1563
|
const controller = RoleSessionController.managed({
|
|
1434
1564
|
projectDir: cwd, runId: masterRun.runId, role: "builder", initialSettings: capturedBranchBuilder,
|
|
1565
|
+
requireNativeContextManagement: true,
|
|
1435
1566
|
historicalCountUncertain: Boolean(recoveryRecord && (recoveryRecord.legacy || !recoveryRecord.runDecisions)),
|
|
1436
1567
|
readSettings: () => liveRoleSettings("builder", capturedBranchBuilder),
|
|
1437
1568
|
settingsBoundary: (input) => applyBuilderSettingsBoundary(masterRun.runId, worktreePath, input),
|
|
@@ -1440,7 +1571,7 @@ export function buildStartCommand() {
|
|
|
1440
1571
|
adapter: active, settingsRevision: settings.settings_revision, displaySessionCost: settings.display_session_cost,
|
|
1441
1572
|
sessionTransition: "adopted live settings",
|
|
1442
1573
|
}),
|
|
1443
|
-
report: (event) => { currentActivity()?.update(event.kind, event.detail, { provider: capturedBranchBuilder.make, model: capturedBranchBuilder.model }); log.write("context-lifecycle", { worktreePath, ...event, sample: event.sample }); },
|
|
1574
|
+
report: (event) => { currentActivity()?.update(event.kind, event.detail, { provider: capturedBranchBuilder.make, model: capturedBranchBuilder.model }); log.write("context-lifecycle", { worktreePath, ...event, sample: event.sample }); void liveStatusReporter?.tick(); },
|
|
1444
1575
|
handoff: async ({ reason, adapter, sample, settings, compactionCount, frozenAction }) => {
|
|
1445
1576
|
const nativeUsage = await adapter.sessionUsage?.().catch(() => undefined);
|
|
1446
1577
|
const transfer = await new HandoffService(cwd).transfer({
|
|
@@ -1492,7 +1623,7 @@ export function buildStartCommand() {
|
|
|
1492
1623
|
archiveDoc: ticketsConfig.paths.archiveDoc,
|
|
1493
1624
|
},
|
|
1494
1625
|
resumeSessions: resumeSessionByTicket,
|
|
1495
|
-
createBuilder: (builderCwd, sessionId, sessionRef) => createBuilder(builderCwd, sessionId, sessionRef),
|
|
1626
|
+
createBuilder: async (builderCwd, sessionId, sessionRef) => branchContextController(builderCwd).manage(await createBuilder(builderCwd, sessionId, sessionRef)),
|
|
1496
1627
|
recordBuilderSession: (session, ticketId) => { masterRun = persistBuildSession(cwd, { ...masterRun, currentTicket: ticketId }, "builder", session); },
|
|
1497
1628
|
recordQaSession: (session, ticketId) => { masterRun = persistBuildSession(cwd, { ...masterRun, currentTicket: ticketId }, "qa", session); },
|
|
1498
1629
|
onSessionUnavailable: (error) => { branchSessionFailure = error; },
|
|
@@ -1502,6 +1633,9 @@ export function buildStartCommand() {
|
|
|
1502
1633
|
observeBuilder: async (builder) => {
|
|
1503
1634
|
liveStatusReporter?.stop();
|
|
1504
1635
|
activeBuilderForStatus = () => builder;
|
|
1636
|
+
activeBuilderContextSnapshot = () => [...branchContextControllers.values()]
|
|
1637
|
+
.map((controller) => controller.contextSnapshot())
|
|
1638
|
+
.find((snapshot) => snapshot?.contextSample.providerSessionId === builder.sessionId());
|
|
1505
1639
|
const reporter = liveStatusReporter = new AgentStatusReporter({
|
|
1506
1640
|
runId: masterRun.runId, role: "builder", provider: builder.agent, model: capturedBranchBuilder.model,
|
|
1507
1641
|
reasoning: capturedBranchBuilder.reasoning, fast: capturedBranchBuilder.fast, step: Math.min(plan.nodes.length, branchContextControllers.size + 1), total: plan.nodes.length,
|
|
@@ -1519,6 +1653,7 @@ export function buildStartCommand() {
|
|
|
1519
1653
|
finally {
|
|
1520
1654
|
db.close();
|
|
1521
1655
|
} },
|
|
1656
|
+
contextSnapshot: activeBuilderContextSnapshot,
|
|
1522
1657
|
}, (line, snapshot) => {
|
|
1523
1658
|
const activity = currentActivity();
|
|
1524
1659
|
if (activity && process.stdout.isTTY)
|
|
@@ -1529,7 +1664,6 @@ export function buildStartCommand() {
|
|
|
1529
1664
|
const db = new WorkflowDb(cwd);
|
|
1530
1665
|
try {
|
|
1531
1666
|
db.recordTelemetry(masterRun.runId, snapshot);
|
|
1532
|
-
db.recordContextSample(snapshot.contextSample);
|
|
1533
1667
|
if (snapshot.sessionUsage)
|
|
1534
1668
|
db.recordSessionUsage(snapshot.sessionUsage);
|
|
1535
1669
|
}
|
|
@@ -1653,7 +1787,7 @@ export function buildStartCommand() {
|
|
|
1653
1787
|
auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
|
|
1654
1788
|
compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
|
|
1655
1789
|
};
|
|
1656
|
-
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: 50, compact_maximum: 10, settings_revision: settingsRevision };
|
|
1790
|
+
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50, compact_maximum: roleDefaults.qa?.compact_maximum ?? 10, settings_revision: settingsRevision };
|
|
1657
1791
|
let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBuilder, qa: capturedQa, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null }) : createBuildRun({
|
|
1658
1792
|
tickets: [], repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
|
|
1659
1793
|
runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 50, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
@@ -1691,6 +1825,7 @@ export function buildStartCommand() {
|
|
|
1691
1825
|
projectDir: cwd,
|
|
1692
1826
|
runId: buildRun.runId,
|
|
1693
1827
|
role: "builder",
|
|
1828
|
+
requireNativeContextManagement: true,
|
|
1694
1829
|
initialSettings: capturedBuilder,
|
|
1695
1830
|
historicalCountUncertain: Boolean(recoveryRecord && (recoveryRecord.legacy || !recoveryRecord.runDecisions)),
|
|
1696
1831
|
readSettings: () => liveRoleSettings("builder", capturedBuilder),
|
|
@@ -1703,6 +1838,7 @@ export function buildStartCommand() {
|
|
|
1703
1838
|
report: (event) => {
|
|
1704
1839
|
currentActivity()?.update(event.kind, event.detail, { provider: capturedBuilder.make, model: capturedBuilder.model });
|
|
1705
1840
|
log.write("context-lifecycle", { ...event, sample: event.sample });
|
|
1841
|
+
void liveStatusReporter?.tick();
|
|
1706
1842
|
},
|
|
1707
1843
|
handoff: async ({ reason, adapter, sample, settings, compactionCount, frozenAction }) => {
|
|
1708
1844
|
const nativeUsage = await adapter.sessionUsage?.().catch(() => undefined);
|
|
@@ -1732,6 +1868,8 @@ export function buildStartCommand() {
|
|
|
1732
1868
|
return continuousBuilder(transfer.successor, cwd, buildRun.runId, settings);
|
|
1733
1869
|
},
|
|
1734
1870
|
});
|
|
1871
|
+
builder = contextController.manage(builder);
|
|
1872
|
+
activeBuilderContextSnapshot = () => contextController.contextSnapshot();
|
|
1735
1873
|
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(adapter, frozenAction)).adapter, async (adapter, frozenAction, strategy) => (await contextController.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter, qaSessionBoundary);
|
|
1736
1874
|
activeBuilderForStatus = () => foreman.builderAdapter();
|
|
1737
1875
|
const statusReporter = liveStatusReporter = new AgentStatusReporter({
|
|
@@ -1753,6 +1891,7 @@ export function buildStartCommand() {
|
|
|
1753
1891
|
finally {
|
|
1754
1892
|
db.close();
|
|
1755
1893
|
} },
|
|
1894
|
+
contextSnapshot: () => contextController.contextSnapshot(),
|
|
1756
1895
|
}, (line, snapshot) => {
|
|
1757
1896
|
const activity = currentActivity();
|
|
1758
1897
|
if (activity && process.stdout.isTTY)
|
|
@@ -1763,7 +1902,6 @@ export function buildStartCommand() {
|
|
|
1763
1902
|
const db = new WorkflowDb(cwd);
|
|
1764
1903
|
try {
|
|
1765
1904
|
db.recordTelemetry(buildRun.runId, snapshot);
|
|
1766
|
-
db.recordContextSample(snapshot.contextSample);
|
|
1767
1905
|
if (snapshot.sessionUsage)
|
|
1768
1906
|
db.recordSessionUsage(snapshot.sessionUsage);
|
|
1769
1907
|
}
|
package/dist/continuity.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContinuityDelta, ResolvedAgentSettings } from "rafi-spec";
|
|
2
|
-
import type { BuilderAdapter, CompactResult, ContextUsage, ProviderSessionUsage, ProviderSettingSwitch, RuntimeFailure, TurnResult } from "./adapters/types.js";
|
|
2
|
+
import type { BuilderAdapter, CompactResult, ContextManagementPolicy, ContextUsage, InterruptResult, ManagedTurnDispatcher, PreparedContextManagement, ProviderSessionUsage, ProviderSettingSwitch, RuntimeFailure, TurnResult } from "./adapters/types.js";
|
|
3
3
|
export declare const CONTINUITY_MARKER = "RAFI_CONTINUITY_DELTA:";
|
|
4
4
|
export declare class ContinuityValidationError extends Error {
|
|
5
5
|
readonly problems: string[];
|
|
@@ -38,7 +38,7 @@ export interface ContinuityAdapterOptions {
|
|
|
38
38
|
role: "builder" | "qa";
|
|
39
39
|
settings: ResolvedAgentSettings;
|
|
40
40
|
authoritativeStateRevision?: () => number;
|
|
41
|
-
/**
|
|
41
|
+
/** @deprecated Use recoverWithHandoff so successor acceptance is lifecycle-managed and durable. */
|
|
42
42
|
createSuccessor?: (handoff: string) => Promise<BuilderAdapter>;
|
|
43
43
|
/**
|
|
44
44
|
* Production recovery path for a double-invalid delta. The callback must
|
|
@@ -62,6 +62,7 @@ export declare class ContinuityAdapter implements BuilderAdapter {
|
|
|
62
62
|
private readonly queue;
|
|
63
63
|
private sourcePump?;
|
|
64
64
|
private recoveryLeasePending;
|
|
65
|
+
private turnDispatcher?;
|
|
65
66
|
constructor(options: ContinuityAdapterOptions);
|
|
66
67
|
get agent(): "claude" | "codex";
|
|
67
68
|
sendTurn(instruction: string): Promise<TurnResult>;
|
|
@@ -70,6 +71,10 @@ export declare class ContinuityAdapter implements BuilderAdapter {
|
|
|
70
71
|
adoptSessionRef(ref: import("rafi-spec").ProviderSessionRefV1): void;
|
|
71
72
|
validateSession(): Promise<import("rafi-spec").SessionAvailabilityV1>;
|
|
72
73
|
compact(): Promise<CompactResult>;
|
|
74
|
+
prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
75
|
+
updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
76
|
+
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
|
|
77
|
+
installManagedTurnDispatcher(dispatcher: ManagedTurnDispatcher): void;
|
|
73
78
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
74
79
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
75
80
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
@@ -77,6 +82,7 @@ export declare class ContinuityAdapter implements BuilderAdapter {
|
|
|
77
82
|
close(): Promise<void>;
|
|
78
83
|
/** Keep the observable wrapper and event stream stable while moving to an accepted successor. */
|
|
79
84
|
adoptValidatedSuccessor(successor: BuilderAdapter): Promise<void>;
|
|
85
|
+
private providerTurn;
|
|
80
86
|
private publish;
|
|
81
87
|
private moveRecoveryLeaseAfterCheckpoint;
|
|
82
88
|
private recordSessionUnavailable;
|
package/dist/continuity.js
CHANGED
|
@@ -109,6 +109,7 @@ export class ContinuityAdapter {
|
|
|
109
109
|
queue = new BuilderEventQueue();
|
|
110
110
|
sourcePump;
|
|
111
111
|
recoveryLeasePending;
|
|
112
|
+
turnDispatcher;
|
|
112
113
|
constructor(options) {
|
|
113
114
|
this.options = options;
|
|
114
115
|
this.adapter = options.adapter;
|
|
@@ -138,7 +139,7 @@ export class ContinuityAdapter {
|
|
|
138
139
|
finally {
|
|
139
140
|
db.close();
|
|
140
141
|
}
|
|
141
|
-
const original = await this.
|
|
142
|
+
const original = await this.providerTurn(`${instruction}\n\n${continuityInstruction()}`);
|
|
142
143
|
const activeSession = this.adapter.sessionRef?.() ?? this.adapter.sessionId();
|
|
143
144
|
if (activeSession) {
|
|
144
145
|
const leaseDb = new WorkflowDb(this.options.projectDir);
|
|
@@ -153,6 +154,30 @@ export class ContinuityAdapter {
|
|
|
153
154
|
this.recordSessionUnavailable(original.failure, instruction);
|
|
154
155
|
throw new SessionUnavailableContinuityError(this.options.runId, this.options.role, original.failure);
|
|
155
156
|
}
|
|
157
|
+
if (original.interrupted?.reason === "compaction-boundary") {
|
|
158
|
+
// The coordinator owns the validated handoff and replay of this frozen
|
|
159
|
+
// action. A protocol-repair turn here would run in the interrupted
|
|
160
|
+
// predecessor (or race successor adoption) without a completed delta.
|
|
161
|
+
const interruptedDb = new WorkflowDb(this.options.projectDir);
|
|
162
|
+
try {
|
|
163
|
+
interruptedDb.appendContinuityEvent({
|
|
164
|
+
runId: this.options.runId,
|
|
165
|
+
role: "host",
|
|
166
|
+
kind: "turn_interrupted_at_compaction_boundary",
|
|
167
|
+
payload: {
|
|
168
|
+
role: this.options.role,
|
|
169
|
+
providerEventId: original.interrupted.providerEventId,
|
|
170
|
+
instructionDigest: sha(instruction),
|
|
171
|
+
},
|
|
172
|
+
authoritativeStateRevision: this.revision(),
|
|
173
|
+
sessionRef: this.adapter.sessionRef?.(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
interruptedDb.close();
|
|
178
|
+
}
|
|
179
|
+
return original;
|
|
180
|
+
}
|
|
156
181
|
const parsed = parseContinuityDelta(original.text);
|
|
157
182
|
if (parsed.delta) {
|
|
158
183
|
this.publish(parsed.delta, "turn_completed", original);
|
|
@@ -167,12 +192,37 @@ export class ContinuityAdapter {
|
|
|
167
192
|
}
|
|
168
193
|
return { ...original, text: parsed.cleanText };
|
|
169
194
|
}
|
|
170
|
-
const repair = await this.
|
|
195
|
+
const repair = await this.providerTurn([
|
|
171
196
|
"Continuity protocol repair only. Do not run tools, repeat work, or change files.",
|
|
172
197
|
`Your prior turn's continuity record was invalid: ${parsed.error?.problems.join("; ")}.`,
|
|
173
198
|
continuityInstruction(),
|
|
174
199
|
"Return only the continuity record.",
|
|
175
200
|
].join("\n"));
|
|
201
|
+
if (repair.interrupted?.reason === "compaction-boundary") {
|
|
202
|
+
// Preserve the predecessor and let the outer managed dispatch perform
|
|
203
|
+
// the validated handoff/replay. Treating this as another invalid delta
|
|
204
|
+
// would race continuity recovery against the coordinator and could lose
|
|
205
|
+
// the original frozen action.
|
|
206
|
+
const interruptedDb = new WorkflowDb(this.options.projectDir);
|
|
207
|
+
try {
|
|
208
|
+
interruptedDb.appendContinuityEvent({
|
|
209
|
+
runId: this.options.runId,
|
|
210
|
+
role: "host",
|
|
211
|
+
kind: "continuity_repair_interrupted_at_compaction_boundary",
|
|
212
|
+
payload: {
|
|
213
|
+
role: this.options.role,
|
|
214
|
+
providerEventId: repair.interrupted.providerEventId,
|
|
215
|
+
instructionDigest: sha(instruction),
|
|
216
|
+
},
|
|
217
|
+
authoritativeStateRevision: this.revision(),
|
|
218
|
+
sessionRef: this.adapter.sessionRef?.(),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
interruptedDb.close();
|
|
223
|
+
}
|
|
224
|
+
return repair;
|
|
225
|
+
}
|
|
176
226
|
const repaired = parseContinuityDelta(repair.text);
|
|
177
227
|
if (repaired.delta) {
|
|
178
228
|
this.publish(repaired.delta, "turn_completed_after_repair", original);
|
|
@@ -207,31 +257,25 @@ export class ContinuityAdapter {
|
|
|
207
257
|
await this.adoptValidatedSuccessor(successor);
|
|
208
258
|
return { ...original, text: parsed.cleanText };
|
|
209
259
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const successorDelta = parseContinuityDelta(accepted.text);
|
|
215
|
-
if (!/^HANDOFF_ACCEPTED\b/m.test(accepted.text) || !successorDelta.delta || !successor.sessionId()) {
|
|
216
|
-
await successor.close().catch(() => { });
|
|
217
|
-
throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "fresh successor did not validate and accept the cumulative checkpoint");
|
|
218
|
-
}
|
|
219
|
-
await this.adoptValidatedSuccessor(successor);
|
|
220
|
-
this.publish(successorDelta.delta, "fresh_successor_accepted", accepted);
|
|
221
|
-
const leaseDb = new WorkflowDb(this.options.projectDir);
|
|
222
|
-
try {
|
|
223
|
-
leaseDb.moveRoleLeaseAfterValidatedRecovery(this.options.runId, this.options.role, successor.sessionRef?.() ?? successor.sessionId(), "double-invalid continuity recovery");
|
|
224
|
-
}
|
|
225
|
-
finally {
|
|
226
|
-
leaseDb.close();
|
|
227
|
-
}
|
|
228
|
-
return { ...original, text: parsed.cleanText };
|
|
260
|
+
// A raw create-and-send fallback cannot install native context control,
|
|
261
|
+
// account compactions, or validate the scoped successor atomically. Refuse
|
|
262
|
+
// it instead of allowing continuity recovery to escape the managed gate.
|
|
263
|
+
throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "continuity repair failed in the original session and no lifecycle-managed recoverWithHandoff callback is configured");
|
|
229
264
|
}
|
|
230
265
|
sessionId() { return this.adapter.sessionId(); }
|
|
231
266
|
sessionRef() { return this.adapter.sessionRef?.(); }
|
|
232
267
|
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
233
268
|
validateSession() { return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
|
|
234
269
|
compact() { return this.adapter.compact ? this.adapter.compact() : Promise.resolve({ ok: false, error: "provider adapter does not expose native compaction" }); }
|
|
270
|
+
prepareContextManagement(policy) { if (!this.adapter.prepareContextManagement)
|
|
271
|
+
return Promise.reject(new Error("provider adapter does not expose native context management")); return this.adapter.prepareContextManagement(policy); }
|
|
272
|
+
updateContextManagement(policy) { if (!this.adapter.updateContextManagement)
|
|
273
|
+
return Promise.reject(new Error("provider adapter does not expose native context reconfiguration")); return this.adapter.updateContextManagement(policy); }
|
|
274
|
+
interruptTurnAtCompactionBoundary(providerEventId) { return this.adapter.interruptTurnAtCompactionBoundary?.(providerEventId) ?? Promise.resolve({ ok: false, error: "provider adapter does not expose compaction-boundary interruption", providerEventId }); }
|
|
275
|
+
installManagedTurnDispatcher(dispatcher) {
|
|
276
|
+
this.turnDispatcher = dispatcher;
|
|
277
|
+
this.adapter.installManagedTurnDispatcher?.(dispatcher);
|
|
278
|
+
}
|
|
235
279
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
236
280
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
237
281
|
switchSettings(settings) { return this.adapter.switchSettings ? this.adapter.switchSettings(settings) : Promise.resolve({ ok: false, error: "provider adapter does not support settings changes" }); }
|
|
@@ -243,9 +287,15 @@ export class ContinuityAdapter {
|
|
|
243
287
|
return;
|
|
244
288
|
const prior = this.adapter;
|
|
245
289
|
this.adapter = successor;
|
|
290
|
+
if (this.turnDispatcher)
|
|
291
|
+
successor.installManagedTurnDispatcher?.(this.turnDispatcher);
|
|
246
292
|
this.pumpEvents();
|
|
247
293
|
await prior.close().catch(() => { });
|
|
248
294
|
}
|
|
295
|
+
providerTurn(text) {
|
|
296
|
+
const invoke = () => this.adapter.sendTurn(text);
|
|
297
|
+
return this.turnDispatcher ? this.turnDispatcher(text, invoke) : invoke();
|
|
298
|
+
}
|
|
249
299
|
publish(delta, kind, result) {
|
|
250
300
|
const db = new WorkflowDb(this.options.projectDir);
|
|
251
301
|
try {
|
|
@@ -322,7 +372,7 @@ export class ContinuityAdapter {
|
|
|
322
372
|
try {
|
|
323
373
|
for await (const event of source.events()) {
|
|
324
374
|
this.queue.push(event);
|
|
325
|
-
if (event.kind === "tool" || event.kind === "session-transition" || event.kind === "context-usage") {
|
|
375
|
+
if (event.kind === "tool" || event.kind === "session-transition" || event.kind === "context-usage" || event.kind === "context-compaction") {
|
|
326
376
|
const db = new WorkflowDb(this.options.projectDir);
|
|
327
377
|
try {
|
|
328
378
|
const payload = event.kind === "tool"
|