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/statusReporter.d.ts
CHANGED
|
@@ -21,6 +21,13 @@ export interface AgentStatusSnapshot {
|
|
|
21
21
|
contextSample: ContextSample;
|
|
22
22
|
sessionUsage?: SessionUsageSample;
|
|
23
23
|
sessionTransition: string;
|
|
24
|
+
lifecycleState?: string;
|
|
25
|
+
configuredCeilingPercent?: number;
|
|
26
|
+
installedNativeTokenLimit?: number;
|
|
27
|
+
installedNativePercent?: number;
|
|
28
|
+
compactionCount?: number;
|
|
29
|
+
compactMaximum?: number;
|
|
30
|
+
settingsRevision?: number;
|
|
24
31
|
at: string;
|
|
25
32
|
}
|
|
26
33
|
export interface StatusClock {
|
|
@@ -47,6 +54,20 @@ export interface AgentStatusReporterInput {
|
|
|
47
54
|
adapter: BuilderAdapter | (() => BuilderAdapter);
|
|
48
55
|
compactionCount?: number | (() => number);
|
|
49
56
|
handoffGeneration?: number | (() => number);
|
|
57
|
+
/** Shared enforcement snapshot. When supplied, status never polls occupancy independently. */
|
|
58
|
+
contextSnapshot?: () => {
|
|
59
|
+
role: "builder" | "qa";
|
|
60
|
+
provider: "claude" | "codex";
|
|
61
|
+
model: string;
|
|
62
|
+
lifecycleState: string;
|
|
63
|
+
contextSample: ContextSample;
|
|
64
|
+
configuredCeilingPercent: number;
|
|
65
|
+
installedNativeTokenLimit?: number;
|
|
66
|
+
installedNativePercent?: number;
|
|
67
|
+
compactionCount: number;
|
|
68
|
+
compactMaximum: number;
|
|
69
|
+
settingsRevision: number;
|
|
70
|
+
} | undefined;
|
|
50
71
|
}
|
|
51
72
|
/**
|
|
52
73
|
* A non-blocking ten-second reporter. It never invents occupancy or cost: an
|
|
@@ -59,6 +80,7 @@ export declare class AgentStatusReporter {
|
|
|
59
80
|
private readonly intervalMs;
|
|
60
81
|
private handle?;
|
|
61
82
|
private lastGoodContext?;
|
|
83
|
+
private readonly lastGoodByRole;
|
|
62
84
|
constructor(input: AgentStatusReporterInput, emit: (line: string, snapshot: AgentStatusSnapshot) => void, clock?: StatusClock, intervalMs?: number);
|
|
63
85
|
start(): void;
|
|
64
86
|
tick(): Promise<void>;
|
|
@@ -82,6 +104,10 @@ export declare class RoleStatusAdapter implements BuilderAdapter {
|
|
|
82
104
|
adoptSessionRef(ref: import("rafi-spec").ProviderSessionRefV1): void;
|
|
83
105
|
validateSession(): Promise<import("rafi-spec").SessionAvailabilityV1>;
|
|
84
106
|
compact(): Promise<CompactResult>;
|
|
107
|
+
prepareContextManagement(policy: import("./adapters/types.js").ContextManagementPolicy): Promise<import("./adapters/types.js").PreparedContextManagement>;
|
|
108
|
+
updateContextManagement(policy: import("./adapters/types.js").ContextManagementPolicy): Promise<import("./adapters/types.js").PreparedContextManagement>;
|
|
109
|
+
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<import("./adapters/types.js").InterruptResult>;
|
|
110
|
+
installManagedTurnDispatcher(dispatcher: import("./adapters/types.js").ManagedTurnDispatcher): void;
|
|
85
111
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
86
112
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
87
113
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
package/dist/statusReporter.js
CHANGED
|
@@ -19,6 +19,7 @@ export class AgentStatusReporter {
|
|
|
19
19
|
intervalMs;
|
|
20
20
|
handle;
|
|
21
21
|
lastGoodContext;
|
|
22
|
+
lastGoodByRole = new Map();
|
|
22
23
|
constructor(input, emit, clock = systemClock, intervalMs = 10_000) {
|
|
23
24
|
this.input = input;
|
|
24
25
|
this.emit = emit;
|
|
@@ -34,23 +35,44 @@ export class AgentStatusReporter {
|
|
|
34
35
|
async tick() {
|
|
35
36
|
const adapter = this.adapter();
|
|
36
37
|
const now = this.clock.now();
|
|
37
|
-
let observed;
|
|
38
|
-
try {
|
|
39
|
-
observed = await adapter.contextUsage?.();
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
observed = undefined;
|
|
43
|
-
}
|
|
44
38
|
let contextSample;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
const hasCoordinator = this.input.contextSnapshot !== undefined;
|
|
40
|
+
const coordinated = this.input.contextSnapshot?.();
|
|
41
|
+
if (coordinated && coordinated.role === this.input.role) {
|
|
42
|
+
contextSample = coordinated.contextSample;
|
|
43
|
+
this.input.provider = coordinated.provider;
|
|
44
|
+
this.input.model = coordinated.model;
|
|
45
|
+
this.input.settingsRevision = coordinated.settingsRevision;
|
|
46
|
+
this.input.compactionCount = coordinated.compactionCount;
|
|
47
|
+
if (contextSample.freshness === "fresh") {
|
|
48
|
+
this.lastGoodContext = contextSample;
|
|
49
|
+
this.lastGoodByRole.set(this.input.role, contextSample);
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
|
-
else if (
|
|
50
|
-
|
|
52
|
+
else if (!hasCoordinator) {
|
|
53
|
+
let observed;
|
|
54
|
+
try {
|
|
55
|
+
observed = await adapter.contextUsage?.();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
observed = undefined;
|
|
59
|
+
}
|
|
60
|
+
if (observed && Number.isFinite(observed.used))
|
|
61
|
+
contextSample = this.contextSample(observed, now);
|
|
62
|
+
else if (this.lastGoodContext)
|
|
63
|
+
contextSample = { ...this.lastGoodContext, freshness: "stale" };
|
|
64
|
+
else
|
|
65
|
+
contextSample = { ...this.measuringSample(now), freshness: "unavailable" };
|
|
51
66
|
}
|
|
52
67
|
else {
|
|
53
|
-
|
|
68
|
+
const prior = this.lastGoodByRole.get(this.input.role);
|
|
69
|
+
contextSample = prior
|
|
70
|
+
? { ...prior, freshness: "stale" }
|
|
71
|
+
: { ...this.measuringSample(now), freshness: "unavailable" };
|
|
72
|
+
}
|
|
73
|
+
if (contextSample.freshness === "fresh") {
|
|
74
|
+
this.lastGoodContext = contextSample;
|
|
75
|
+
this.lastGoodByRole.set(this.input.role, contextSample);
|
|
54
76
|
}
|
|
55
77
|
let sessionUsage;
|
|
56
78
|
if (this.input.displaySessionCost) {
|
|
@@ -90,16 +112,19 @@ export class AgentStatusReporter {
|
|
|
90
112
|
const priorRole = this.input.role;
|
|
91
113
|
const priorAdapter = this.adapter();
|
|
92
114
|
const priorSessionId = priorAdapter.sessionId();
|
|
115
|
+
if (this.lastGoodContext)
|
|
116
|
+
this.lastGoodByRole.set(priorRole, this.lastGoodContext);
|
|
93
117
|
Object.assign(this.input, patch);
|
|
94
118
|
if (priorRole !== this.input.role
|
|
95
119
|
|| priorAdapter !== this.adapter()
|
|
96
120
|
|| priorSessionId !== this.adapter().sessionId()
|
|
97
121
|
|| patch.sessionTransition !== undefined)
|
|
98
|
-
this.lastGoodContext =
|
|
122
|
+
this.lastGoodContext = this.lastGoodByRole.get(this.input.role);
|
|
99
123
|
this.emitSnapshot(this.measuringSample(), undefined);
|
|
100
124
|
}
|
|
101
125
|
emitSnapshot(contextSample, sessionUsage, now = this.clock.now()) {
|
|
102
|
-
const
|
|
126
|
+
const coordinated = this.input.contextSnapshot?.();
|
|
127
|
+
const { adapter: _adapter, displaySessionCost: _display, compactionCount: _compactions, handoffGeneration: _handoffs, contextSnapshot: _contextSnapshot, ...base } = this.input;
|
|
103
128
|
const context = contextSample.used === undefined ? undefined : {
|
|
104
129
|
used: contextSample.used,
|
|
105
130
|
maximum: contextSample.maximum,
|
|
@@ -110,13 +135,29 @@ export class AgentStatusReporter {
|
|
|
110
135
|
...(context ? { context } : {}),
|
|
111
136
|
contextSample,
|
|
112
137
|
...(sessionUsage ? { sessionUsage } : {}),
|
|
138
|
+
...(coordinated && coordinated.role === this.input.role ? {
|
|
139
|
+
lifecycleState: coordinated.lifecycleState,
|
|
140
|
+
configuredCeilingPercent: coordinated.configuredCeilingPercent,
|
|
141
|
+
installedNativeTokenLimit: coordinated.installedNativeTokenLimit,
|
|
142
|
+
installedNativePercent: coordinated.installedNativePercent,
|
|
143
|
+
compactionCount: coordinated.compactionCount,
|
|
144
|
+
compactMaximum: coordinated.compactMaximum,
|
|
145
|
+
settingsRevision: coordinated.settingsRevision,
|
|
146
|
+
} : {
|
|
147
|
+
compactionCount: contextSample.compactionCount,
|
|
148
|
+
settingsRevision: contextSample.settingsRevision,
|
|
149
|
+
}),
|
|
113
150
|
at: now.toISOString(),
|
|
114
151
|
};
|
|
115
152
|
const line = [
|
|
116
153
|
`${snapshot.role} ${snapshot.provider}/${snapshot.model}`,
|
|
117
154
|
`activity=${snapshot.phase}`,
|
|
118
155
|
formatContext(contextSample),
|
|
119
|
-
`
|
|
156
|
+
...(snapshot.configuredCeilingPercent === undefined ? [] : [`ceiling=${snapshot.configuredCeilingPercent}%`]),
|
|
157
|
+
...(snapshot.installedNativePercent === undefined ? [] : [`native=${snapshot.installedNativePercent.toFixed(1)}%${snapshot.installedNativeTokenLimit === undefined ? "" : `/${snapshot.installedNativeTokenLimit}`}`]),
|
|
158
|
+
...(snapshot.lifecycleState ? [`state=${formatLifecycle(snapshot.lifecycleState)}`] : []),
|
|
159
|
+
`compactions=${snapshot.compactionCount ?? contextSample.compactionCount}${snapshot.compactMaximum === undefined ? "" : `/${snapshot.compactMaximum}`}`,
|
|
160
|
+
`revision=${snapshot.settingsRevision ?? contextSample.settingsRevision}`,
|
|
120
161
|
`handoff=${contextSample.handoffGeneration}`,
|
|
121
162
|
`session=${snapshot.sessionTransition}`,
|
|
122
163
|
...(this.input.displaySessionCost ? [formatSessionUsage(sessionUsage)] : []),
|
|
@@ -177,6 +218,12 @@ export class RoleStatusAdapter {
|
|
|
177
218
|
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
178
219
|
validateSession() { return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
|
|
179
220
|
compact() { this.onActive(this.adapter); return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
|
|
221
|
+
prepareContextManagement(policy) { this.onActive(this.adapter); if (!this.adapter.prepareContextManagement)
|
|
222
|
+
return Promise.reject(new Error("native context management unavailable")); return this.adapter.prepareContextManagement(policy); }
|
|
223
|
+
updateContextManagement(policy) { if (!this.adapter.updateContextManagement)
|
|
224
|
+
return Promise.reject(new Error("native context reconfiguration unavailable")); return this.adapter.updateContextManagement(policy); }
|
|
225
|
+
interruptTurnAtCompactionBoundary(providerEventId) { return this.adapter.interruptTurnAtCompactionBoundary?.(providerEventId) ?? Promise.resolve({ ok: false, error: "compaction-boundary interruption unavailable", providerEventId }); }
|
|
226
|
+
installManagedTurnDispatcher(dispatcher) { this.adapter.installManagedTurnDispatcher?.(dispatcher); }
|
|
180
227
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
181
228
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
182
229
|
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|
|
@@ -215,6 +262,19 @@ function formatContext(sample) {
|
|
|
215
262
|
: `${sample.percentage.toFixed(1)}% (${sample.used ?? "unknown"}/${sample.maximum ?? "unknown"})`;
|
|
216
263
|
return `context ${occupancy}${sample.freshness === "stale" ? " stale" : ""}`;
|
|
217
264
|
}
|
|
265
|
+
function formatLifecycle(state) {
|
|
266
|
+
if (state === "initializing")
|
|
267
|
+
return "measuring";
|
|
268
|
+
if (state === "threshold_pending")
|
|
269
|
+
return "pending";
|
|
270
|
+
if (state === "native_compacting" || state === "host_compacting")
|
|
271
|
+
return "compacting";
|
|
272
|
+
if (state === "compacted_unverified")
|
|
273
|
+
return "verifying";
|
|
274
|
+
if (state === "handoff_required" || state === "resuming")
|
|
275
|
+
return "handing off";
|
|
276
|
+
return state;
|
|
277
|
+
}
|
|
218
278
|
function formatSessionUsage(sample) {
|
|
219
279
|
if (!sample || sample.source === "unavailable")
|
|
220
280
|
return "session usage unavailable";
|
package/dist/workflowDb.d.ts
CHANGED
|
@@ -55,13 +55,45 @@ export interface CompactionAttemptRecord {
|
|
|
55
55
|
sessionRef?: ProviderSessionRefV1;
|
|
56
56
|
sessionKey?: string;
|
|
57
57
|
crossingKey: string;
|
|
58
|
-
status: "started" | "succeeded" | "failed";
|
|
58
|
+
status: "started" | "succeeded" | "failed" | "unverified";
|
|
59
|
+
origin: "provider-auto" | "rafi-manual" | "recovery" | "boundary";
|
|
60
|
+
providerEventId?: string;
|
|
61
|
+
thresholdGenerationId?: string;
|
|
59
62
|
beforeSample?: ContextSample;
|
|
60
63
|
afterSample?: ContextSample;
|
|
61
64
|
error?: string;
|
|
62
65
|
createdAt: string;
|
|
63
66
|
updatedAt: string;
|
|
64
67
|
}
|
|
68
|
+
export type ThresholdLifecycleState = "initializing" | "armed" | "threshold_pending" | "native_compacting" | "host_compacting" | "compacted_unverified" | "retrying" | "handoff_required" | "resuming";
|
|
69
|
+
export interface ThresholdGenerationRecord {
|
|
70
|
+
generationId: string;
|
|
71
|
+
runId: string;
|
|
72
|
+
role: "builder" | "qa";
|
|
73
|
+
providerSessionId?: string;
|
|
74
|
+
sessionRef?: ProviderSessionRefV1;
|
|
75
|
+
sessionKey?: string;
|
|
76
|
+
generation: number;
|
|
77
|
+
state: ThresholdLifecycleState;
|
|
78
|
+
configuredCeilingPercent: number;
|
|
79
|
+
installedNativeTokenLimit?: number;
|
|
80
|
+
installedNativePercent?: number;
|
|
81
|
+
settingsRevision: number;
|
|
82
|
+
model: string;
|
|
83
|
+
latestSample?: ContextSample;
|
|
84
|
+
frozenActionDigest?: string;
|
|
85
|
+
providerEventId?: string;
|
|
86
|
+
createdAt: string;
|
|
87
|
+
updatedAt: string;
|
|
88
|
+
}
|
|
89
|
+
export interface ActiveRoleAdapterRecord {
|
|
90
|
+
runId: string;
|
|
91
|
+
role: "builder" | "qa";
|
|
92
|
+
providerSessionId?: string;
|
|
93
|
+
sessionKey?: string;
|
|
94
|
+
settingsRevision: number;
|
|
95
|
+
observedAt: string;
|
|
96
|
+
}
|
|
65
97
|
export interface RoleMutationLease {
|
|
66
98
|
runId: string;
|
|
67
99
|
role: "builder" | "qa";
|
|
@@ -108,7 +140,8 @@ export declare class WorkflowDb {
|
|
|
108
140
|
at: string;
|
|
109
141
|
}>;
|
|
110
142
|
recordSettings(runId: string, role: string, boundary: number, settings: ResolvedAgentSettings, now?: Date): void;
|
|
111
|
-
recordProjectSettingsRevision(revision: number, defaults: unknown, now?: Date): void;
|
|
143
|
+
recordProjectSettingsRevision(revision: number, defaults: unknown, targetsOrNow?: Readonly<Record<string, readonly string[]>> | Date, now?: Date): void;
|
|
144
|
+
projectSettingsRevisionTargets(revision: number): Record<string, string[]>;
|
|
112
145
|
recordTelemetry(runId: string, snapshot: unknown, now?: Date): void;
|
|
113
146
|
recordContextSample(sample: ContextSample): void;
|
|
114
147
|
contextSamples(runId: string, role?: "builder" | "qa"): ContextSample[];
|
|
@@ -186,14 +219,26 @@ export declare class WorkflowDb {
|
|
|
186
219
|
sessionKey?: string;
|
|
187
220
|
crossingKey: string;
|
|
188
221
|
beforeSample?: ContextSample;
|
|
222
|
+
origin?: CompactionAttemptRecord["origin"];
|
|
223
|
+
providerEventId?: string;
|
|
224
|
+
thresholdGenerationId?: string;
|
|
189
225
|
}, now?: Date): CompactionAttemptRecord;
|
|
190
226
|
finishCompactionAttempt(idempotencyKey: string, input: {
|
|
191
227
|
ok: boolean;
|
|
192
228
|
afterSample?: ContextSample;
|
|
193
229
|
error?: string;
|
|
230
|
+
status?: "succeeded" | "failed" | "unverified";
|
|
194
231
|
}, now?: Date): CompactionAttemptRecord;
|
|
195
232
|
compactionAttempt(idempotencyKey: string): CompactionAttemptRecord | undefined;
|
|
233
|
+
linkCompactionProviderEvent(idempotencyKey: string, providerEventId: string): CompactionAttemptRecord;
|
|
234
|
+
compactionAttempts(runId: string, role: "builder" | "qa", providerSession?: string | ProviderSessionRefV1): CompactionAttemptRecord[];
|
|
196
235
|
successfulCompactionCount(runId: string, role: "builder" | "qa", providerSession?: string | ProviderSessionRefV1): number;
|
|
236
|
+
upsertThresholdGeneration(input: Omit<ThresholdGenerationRecord, "createdAt" | "updatedAt">, now?: Date): ThresholdGenerationRecord;
|
|
237
|
+
thresholdGeneration(generationId: string): ThresholdGenerationRecord | undefined;
|
|
238
|
+
thresholdGenerations(runId: string, role?: "builder" | "qa", sessionKey?: string): ThresholdGenerationRecord[];
|
|
239
|
+
markRoleAdapterActive(input: ActiveRoleAdapterRecord): void;
|
|
240
|
+
clearRoleAdapterActive(runId: string, role: "builder" | "qa", sessionKey?: string): void;
|
|
241
|
+
activeRoleAdapters(runId?: string, observedAfter?: string): ActiveRoleAdapterRecord[];
|
|
197
242
|
recordRecoveryDecision(receipt: BuildRecoveryDecisionReceipt): void;
|
|
198
243
|
recoveryDecisions(runId: string): BuildRecoveryDecisionReceipt[];
|
|
199
244
|
beginPublication(runId: string, intent: unknown, previousDigests: unknown, now?: Date): PublicationTransaction;
|
package/dist/workflowDb.js
CHANGED
|
@@ -109,8 +109,24 @@ export class WorkflowDb {
|
|
|
109
109
|
this.insertEvent(runId, "settings_boundary", `role:${role}:${boundary}`, { revision: settings.settings_revision }, now.toISOString());
|
|
110
110
|
})();
|
|
111
111
|
}
|
|
112
|
-
recordProjectSettingsRevision(revision, defaults, now = new Date()) {
|
|
113
|
-
|
|
112
|
+
recordProjectSettingsRevision(revision, defaults, targetsOrNow = {}, now = new Date()) {
|
|
113
|
+
const targets = targetsOrNow instanceof Date ? {} : targetsOrNow;
|
|
114
|
+
const recordedAt = targetsOrNow instanceof Date ? targetsOrNow : now;
|
|
115
|
+
this.db.transaction(() => {
|
|
116
|
+
this.db.prepare(`INSERT INTO project_settings_revisions(revision,defaults_json,created_at) VALUES(?,?,?)
|
|
117
|
+
ON CONFLICT(revision) DO UPDATE SET defaults_json=excluded.defaults_json,created_at=excluded.created_at`).run(revision, json(defaults), recordedAt.toISOString());
|
|
118
|
+
const entries = Object.entries(targets);
|
|
119
|
+
if (entries.length) {
|
|
120
|
+
this.db.prepare("DELETE FROM project_settings_revision_targets WHERE revision=?").run(revision);
|
|
121
|
+
const insert = this.db.prepare("INSERT INTO project_settings_revision_targets(revision,role,fields_json) VALUES(?,?,?)");
|
|
122
|
+
for (const [role, fields] of entries)
|
|
123
|
+
insert.run(revision, role, json([...new Set(fields)].sort()));
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
}
|
|
127
|
+
projectSettingsRevisionTargets(revision) {
|
|
128
|
+
const rows = this.db.prepare("SELECT role,fields_json FROM project_settings_revision_targets WHERE revision=? ORDER BY role").all(revision);
|
|
129
|
+
return Object.fromEntries(rows.map((row) => [row.role, parseJson(row.fields_json)]));
|
|
114
130
|
}
|
|
115
131
|
recordTelemetry(runId, snapshot, now = new Date()) {
|
|
116
132
|
this.ensureRun(runId, "build", now);
|
|
@@ -153,12 +169,22 @@ export class WorkflowDb {
|
|
|
153
169
|
this.db.prepare(`INSERT INTO live_settings_acknowledgments(run_id,role,provider_session_id,session_key,session_ref_json,revision,acknowledged_at)
|
|
154
170
|
VALUES(?,?,?,?,?,?,?) ON CONFLICT(run_id,role,revision) DO UPDATE SET provider_session_id=excluded.provider_session_id,session_key=excluded.session_key,session_ref_json=excluded.session_ref_json,acknowledged_at=excluded.acknowledged_at`)
|
|
155
171
|
.run(ack.runId, ack.role, session.id, session.key, session.refJson, ack.revision, ack.acknowledgedAt);
|
|
172
|
+
if (session.key) {
|
|
173
|
+
this.db.prepare(`INSERT INTO live_settings_session_acknowledgments(run_id,role,provider_session_id,session_key,session_ref_json,revision,acknowledged_at)
|
|
174
|
+
VALUES(?,?,?,?,?,?,?) ON CONFLICT(run_id,role,session_key,revision) DO UPDATE SET provider_session_id=excluded.provider_session_id,session_ref_json=excluded.session_ref_json,acknowledged_at=excluded.acknowledged_at`)
|
|
175
|
+
.run(ack.runId, ack.role, session.id, session.key, session.refJson, ack.revision, ack.acknowledgedAt);
|
|
176
|
+
}
|
|
156
177
|
this.insertEvent(ack.runId, "live_settings_acknowledged", `settings:${ack.revision}`, { role: ack.role, providerSessionId: session.id, sessionKey: session.key }, ack.acknowledgedAt);
|
|
157
178
|
})();
|
|
158
179
|
}
|
|
159
180
|
settingsAcknowledgments(revision) {
|
|
160
|
-
const
|
|
161
|
-
|
|
181
|
+
const scoped = this.db.prepare("SELECT * FROM live_settings_session_acknowledgments WHERE revision=? ORDER BY acknowledged_at").all(revision);
|
|
182
|
+
const covered = new Set(scoped.map((row) => `${row.run_id}:${row.role}`));
|
|
183
|
+
const legacy = this.db.prepare("SELECT * FROM live_settings_acknowledgments WHERE revision=? ORDER BY acknowledged_at").all(revision)
|
|
184
|
+
.filter((row) => !covered.has(`${row.run_id}:${row.role}`));
|
|
185
|
+
return [...scoped, ...legacy]
|
|
186
|
+
.sort((left, right) => left.acknowledged_at.localeCompare(right.acknowledged_at))
|
|
187
|
+
.map((row) => ({ runId: row.run_id, role: row.role, ...(row.provider_session_id ? { providerSessionId: row.provider_session_id } : {}), ...(row.session_key ? { sessionKey: row.session_key } : {}), ...(row.session_ref_json ? { sessionRef: parseJson(row.session_ref_json) } : {}), revision: row.revision, acknowledgedAt: row.acknowledged_at }));
|
|
162
188
|
}
|
|
163
189
|
appendContinuityEvent(input, now = new Date()) {
|
|
164
190
|
this.ensureRun(input.runId, "build", now);
|
|
@@ -440,9 +466,17 @@ export class WorkflowDb {
|
|
|
440
466
|
this.ensureRun(input.runId);
|
|
441
467
|
const at = now.toISOString();
|
|
442
468
|
const session = sessionParts(input.sessionRef, input.providerSessionId, input.sessionKey);
|
|
443
|
-
this.db.prepare(`INSERT OR IGNORE INTO compaction_attempts(idempotency_key,run_id,role,provider_session_id,session_key,session_ref_json,crossing_key,status,before_sample_json,created_at,updated_at)
|
|
444
|
-
VALUES(?,?,?,?,?,?,?,'started'
|
|
445
|
-
|
|
469
|
+
this.db.prepare(`INSERT OR IGNORE INTO compaction_attempts(idempotency_key,run_id,role,provider_session_id,session_key,session_ref_json,crossing_key,status,before_sample_json,origin,provider_event_id,threshold_generation_id,created_at,updated_at)
|
|
470
|
+
VALUES(?,?,?,?,?,?,?,'started',?,?,?,?,?,?)`).run(input.idempotencyKey, input.runId, input.role, session.id, session.key, session.refJson, input.crossingKey, input.beforeSample ? json(input.beforeSample) : null, input.origin ?? "rafi-manual", input.providerEventId ?? null, input.thresholdGenerationId ?? null, at, at);
|
|
471
|
+
const direct = this.compactionAttempt(input.idempotencyKey);
|
|
472
|
+
if (direct)
|
|
473
|
+
return direct;
|
|
474
|
+
if (session.key && input.providerEventId) {
|
|
475
|
+
const duplicate = this.db.prepare("SELECT * FROM compaction_attempts WHERE run_id=? AND role=? AND session_key=? AND provider_event_id=?").get(input.runId, input.role, session.key, input.providerEventId);
|
|
476
|
+
if (duplicate)
|
|
477
|
+
return compactionAttemptFromRow(duplicate);
|
|
478
|
+
}
|
|
479
|
+
throw new Error(`failed to persist compaction attempt ${input.idempotencyKey}`);
|
|
446
480
|
}
|
|
447
481
|
finishCompactionAttempt(idempotencyKey, input, now = new Date()) {
|
|
448
482
|
const prior = this.compactionAttempt(idempotencyKey);
|
|
@@ -451,23 +485,100 @@ export class WorkflowDb {
|
|
|
451
485
|
if (prior.status !== "started")
|
|
452
486
|
return prior;
|
|
453
487
|
this.db.prepare("UPDATE compaction_attempts SET status=?,after_sample_json=?,error=?,updated_at=? WHERE idempotency_key=?")
|
|
454
|
-
.run(input.ok ? "succeeded" : "failed", input.afterSample ? json(input.afterSample) : null, input.error ? sanitizeText(input.error).slice(0, 2000) : null, now.toISOString(), idempotencyKey);
|
|
455
|
-
|
|
488
|
+
.run(input.status ?? (input.ok ? "succeeded" : "failed"), input.afterSample ? json(input.afterSample) : null, input.error ? sanitizeText(input.error).slice(0, 2000) : null, now.toISOString(), idempotencyKey);
|
|
489
|
+
const status = input.status ?? (input.ok ? "succeeded" : "failed");
|
|
490
|
+
this.insertEvent(prior.runId, `compaction_${status}`, `context:${prior.role}`, { idempotencyKey, providerSessionId: prior.providerSessionId, sessionKey: prior.sessionKey, crossingKey: prior.crossingKey, origin: prior.origin, providerEventId: prior.providerEventId }, now.toISOString());
|
|
456
491
|
return this.compactionAttempt(idempotencyKey);
|
|
457
492
|
}
|
|
458
493
|
compactionAttempt(idempotencyKey) {
|
|
459
494
|
const row = this.db.prepare("SELECT * FROM compaction_attempts WHERE idempotency_key=?").get(idempotencyKey);
|
|
460
495
|
return row ? compactionAttemptFromRow(row) : undefined;
|
|
461
496
|
}
|
|
497
|
+
linkCompactionProviderEvent(idempotencyKey, providerEventId) {
|
|
498
|
+
const prior = this.compactionAttempt(idempotencyKey);
|
|
499
|
+
if (!prior)
|
|
500
|
+
throw new Error(`compaction attempt not found: ${idempotencyKey}`);
|
|
501
|
+
if (prior.providerEventId === providerEventId)
|
|
502
|
+
return prior;
|
|
503
|
+
this.db.prepare("UPDATE compaction_attempts SET provider_event_id=?,updated_at=? WHERE idempotency_key=?")
|
|
504
|
+
.run(providerEventId, new Date().toISOString(), idempotencyKey);
|
|
505
|
+
return this.compactionAttempt(idempotencyKey);
|
|
506
|
+
}
|
|
507
|
+
compactionAttempts(runId, role, providerSession) {
|
|
508
|
+
if (!providerSession)
|
|
509
|
+
return [];
|
|
510
|
+
const session = sessionParts(typeof providerSession === "object" ? providerSession : undefined, typeof providerSession === "string" ? providerSession : undefined);
|
|
511
|
+
const rows = session.key
|
|
512
|
+
? this.db.prepare("SELECT * FROM compaction_attempts WHERE run_id=? AND role=? AND session_key=? ORDER BY created_at,idempotency_key").all(runId, role, session.key)
|
|
513
|
+
: this.db.prepare("SELECT * FROM compaction_attempts WHERE run_id=? AND role=? AND provider_session_id=? AND session_key IS NULL ORDER BY created_at,idempotency_key").all(runId, role, session.id);
|
|
514
|
+
return rows.map(compactionAttemptFromRow);
|
|
515
|
+
}
|
|
462
516
|
successfulCompactionCount(runId, role, providerSession) {
|
|
463
517
|
if (!providerSession)
|
|
464
518
|
return 0;
|
|
465
519
|
const session = sessionParts(typeof providerSession === "object" ? providerSession : undefined, typeof providerSession === "string" ? providerSession : undefined);
|
|
466
520
|
const row = session.key
|
|
467
|
-
? this.db.prepare(`SELECT COUNT(*) AS count FROM compaction_attempts WHERE run_id=? AND role=? AND session_key=? AND status
|
|
468
|
-
: this.db.prepare(`SELECT COUNT(*) AS count FROM compaction_attempts WHERE run_id=? AND role=? AND provider_session_id=? AND session_key IS NULL AND status
|
|
521
|
+
? this.db.prepare(`SELECT COUNT(*) AS count FROM compaction_attempts WHERE run_id=? AND role=? AND session_key=? AND status IN ('succeeded','unverified')`).get(runId, role, session.key)
|
|
522
|
+
: this.db.prepare(`SELECT COUNT(*) AS count FROM compaction_attempts WHERE run_id=? AND role=? AND provider_session_id=? AND session_key IS NULL AND status IN ('succeeded','unverified')`).get(runId, role, session.id);
|
|
469
523
|
return row.count;
|
|
470
524
|
}
|
|
525
|
+
upsertThresholdGeneration(input, now = new Date()) {
|
|
526
|
+
this.ensureRun(input.runId);
|
|
527
|
+
const at = now.toISOString();
|
|
528
|
+
const session = sessionParts(input.sessionRef, input.providerSessionId, input.sessionKey);
|
|
529
|
+
this.db.prepare(`INSERT INTO threshold_generations(
|
|
530
|
+
generation_id,run_id,role,provider_session_id,session_key,session_ref_json,generation,state,configured_ceiling_percent,
|
|
531
|
+
installed_native_token_limit,installed_native_percent,settings_revision,model,latest_sample_json,frozen_action_digest,provider_event_id,created_at,updated_at
|
|
532
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
533
|
+
ON CONFLICT(generation_id) DO UPDATE SET
|
|
534
|
+
provider_session_id=excluded.provider_session_id,session_key=excluded.session_key,session_ref_json=excluded.session_ref_json,
|
|
535
|
+
state=excluded.state,configured_ceiling_percent=excluded.configured_ceiling_percent,
|
|
536
|
+
installed_native_token_limit=excluded.installed_native_token_limit,installed_native_percent=excluded.installed_native_percent,
|
|
537
|
+
settings_revision=excluded.settings_revision,model=excluded.model,latest_sample_json=excluded.latest_sample_json,
|
|
538
|
+
frozen_action_digest=COALESCE(excluded.frozen_action_digest,threshold_generations.frozen_action_digest),
|
|
539
|
+
provider_event_id=COALESCE(excluded.provider_event_id,threshold_generations.provider_event_id),updated_at=excluded.updated_at`).run(input.generationId, input.runId, input.role, session.id, session.key, session.refJson, input.generation, input.state, input.configuredCeilingPercent, input.installedNativeTokenLimit ?? null, input.installedNativePercent ?? null, input.settingsRevision, input.model, input.latestSample ? json(input.latestSample) : null, input.frozenActionDigest ?? null, input.providerEventId ?? null, at, at);
|
|
540
|
+
return this.thresholdGeneration(input.generationId);
|
|
541
|
+
}
|
|
542
|
+
thresholdGeneration(generationId) {
|
|
543
|
+
const row = this.db.prepare("SELECT * FROM threshold_generations WHERE generation_id=?").get(generationId);
|
|
544
|
+
return row ? thresholdGenerationFromRow(row) : undefined;
|
|
545
|
+
}
|
|
546
|
+
thresholdGenerations(runId, role, sessionKey) {
|
|
547
|
+
const rows = role && sessionKey
|
|
548
|
+
? this.db.prepare("SELECT * FROM threshold_generations WHERE run_id=? AND role=? AND session_key=? ORDER BY generation").all(runId, role, sessionKey)
|
|
549
|
+
: role
|
|
550
|
+
? this.db.prepare("SELECT * FROM threshold_generations WHERE run_id=? AND role=? ORDER BY created_at,generation").all(runId, role)
|
|
551
|
+
: this.db.prepare("SELECT * FROM threshold_generations WHERE run_id=? ORDER BY created_at,generation").all(runId);
|
|
552
|
+
return rows.map(thresholdGenerationFromRow);
|
|
553
|
+
}
|
|
554
|
+
markRoleAdapterActive(input) {
|
|
555
|
+
this.ensureRun(input.runId);
|
|
556
|
+
const sessionKey = input.sessionKey ?? `${input.role}:unscoped:${input.providerSessionId ?? "pending"}`;
|
|
557
|
+
this.db.prepare(`INSERT INTO active_role_adapter_sessions(run_id,role,provider_session_id,session_key,settings_revision,observed_at)
|
|
558
|
+
VALUES(?,?,?,?,?,?) ON CONFLICT(run_id,role,session_key) DO UPDATE SET provider_session_id=excluded.provider_session_id,settings_revision=excluded.settings_revision,observed_at=excluded.observed_at`)
|
|
559
|
+
.run(input.runId, input.role, input.providerSessionId ?? null, sessionKey, input.settingsRevision, input.observedAt);
|
|
560
|
+
}
|
|
561
|
+
clearRoleAdapterActive(runId, role, sessionKey) {
|
|
562
|
+
if (sessionKey)
|
|
563
|
+
this.db.prepare("DELETE FROM active_role_adapter_sessions WHERE run_id=? AND role=? AND session_key=?").run(runId, role, sessionKey);
|
|
564
|
+
else
|
|
565
|
+
this.db.prepare("DELETE FROM active_role_adapter_sessions WHERE run_id=? AND role=?").run(runId, role);
|
|
566
|
+
}
|
|
567
|
+
activeRoleAdapters(runId, observedAfter) {
|
|
568
|
+
const clauses = [];
|
|
569
|
+
const values = [];
|
|
570
|
+
if (runId) {
|
|
571
|
+
clauses.push("run_id=?");
|
|
572
|
+
values.push(runId);
|
|
573
|
+
}
|
|
574
|
+
if (observedAfter) {
|
|
575
|
+
clauses.push("observed_at>=?");
|
|
576
|
+
values.push(observedAfter);
|
|
577
|
+
}
|
|
578
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
579
|
+
const rows = this.db.prepare(`SELECT * FROM active_role_adapter_sessions${where} ORDER BY run_id,role,session_key`).all(...values);
|
|
580
|
+
return rows.map((row) => ({ runId: row.run_id, role: row.role, ...(row.provider_session_id ? { providerSessionId: row.provider_session_id } : {}), ...(row.session_key ? { sessionKey: row.session_key } : {}), settingsRevision: row.settings_revision, observedAt: row.observed_at }));
|
|
581
|
+
}
|
|
471
582
|
recordRecoveryDecision(receipt) {
|
|
472
583
|
this.ensureRun(receipt.runId);
|
|
473
584
|
const digest = digestJson(receipt);
|
|
@@ -595,6 +706,7 @@ export class WorkflowDb {
|
|
|
595
706
|
CREATE TABLE IF NOT EXISTS workflow_events(sequence INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),event_type TEXT NOT NULL,checkpoint TEXT NOT NULL,payload_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
596
707
|
CREATE TABLE IF NOT EXISTS role_settings(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,boundary INTEGER NOT NULL,revision INTEGER NOT NULL,settings_json TEXT NOT NULL,created_at TEXT NOT NULL,PRIMARY KEY(run_id,role,boundary));
|
|
597
708
|
CREATE TABLE IF NOT EXISTS project_settings_revisions(revision INTEGER PRIMARY KEY,defaults_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
709
|
+
CREATE TABLE IF NOT EXISTS project_settings_revision_targets(revision INTEGER NOT NULL REFERENCES project_settings_revisions(revision),role TEXT NOT NULL,fields_json TEXT NOT NULL,PRIMARY KEY(revision,role));
|
|
598
710
|
CREATE TABLE IF NOT EXISTS workflow_telemetry(id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),snapshot_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
599
711
|
CREATE TABLE IF NOT EXISTS provider_sessions(id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,stream TEXT NOT NULL,provider TEXT NOT NULL,model TEXT NOT NULL,session_id TEXT,session_key TEXT,session_ref_json TEXT,transition TEXT NOT NULL,settings_revision INTEGER NOT NULL,created_at TEXT NOT NULL);
|
|
600
712
|
CREATE TABLE IF NOT EXISTS provider_session_bindings(session_key TEXT PRIMARY KEY,provider_session_id TEXT NOT NULL,role TEXT NOT NULL,session_ref_json TEXT NOT NULL,observed_at TEXT NOT NULL);
|
|
@@ -607,12 +719,15 @@ export class WorkflowDb {
|
|
|
607
719
|
CREATE TABLE IF NOT EXISTS context_samples(sample_id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,provider_session_id TEXT,session_key TEXT,session_ref_json TEXT,sample_json TEXT NOT NULL,observed_at TEXT NOT NULL);
|
|
608
720
|
CREATE TABLE IF NOT EXISTS session_usage_samples(sample_id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,provider_session_id TEXT,session_key TEXT,session_ref_json TEXT,sample_json TEXT NOT NULL,observed_at TEXT NOT NULL);
|
|
609
721
|
CREATE TABLE IF NOT EXISTS live_settings_acknowledgments(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,provider_session_id TEXT,session_key TEXT,session_ref_json TEXT,revision INTEGER NOT NULL,acknowledged_at TEXT NOT NULL,PRIMARY KEY(run_id,role,revision));
|
|
722
|
+
CREATE TABLE IF NOT EXISTS live_settings_session_acknowledgments(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,provider_session_id TEXT,session_key TEXT NOT NULL,session_ref_json TEXT,revision INTEGER NOT NULL,acknowledged_at TEXT NOT NULL,PRIMARY KEY(run_id,role,session_key,revision));
|
|
610
723
|
CREATE TABLE IF NOT EXISTS continuity_events(sequence INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,kind TEXT NOT NULL,payload_json TEXT NOT NULL,digest TEXT NOT NULL UNIQUE,authoritative_state_revision INTEGER NOT NULL,session_key TEXT,session_ref_json TEXT,created_at TEXT NOT NULL);
|
|
611
724
|
CREATE TABLE IF NOT EXISTS continuity_checkpoints(checkpoint_id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,event_sequence INTEGER NOT NULL,state TEXT NOT NULL,delta_json TEXT NOT NULL,digest TEXT NOT NULL UNIQUE,predecessor_digest TEXT,authoritative_state_revision INTEGER NOT NULL,session_key TEXT,session_ref_json TEXT,created_at TEXT NOT NULL);
|
|
612
725
|
CREATE TABLE IF NOT EXISTS continuity_heads(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,state TEXT NOT NULL,event_sequence INTEGER NOT NULL,digest TEXT NOT NULL,authoritative_state_revision INTEGER NOT NULL,updated_at TEXT NOT NULL,PRIMARY KEY(run_id,role));
|
|
613
726
|
CREATE TABLE IF NOT EXISTS handoffs(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),generation INTEGER NOT NULL,role TEXT NOT NULL,manifest_digest TEXT NOT NULL,markdown_digest TEXT NOT NULL,predecessor_session_id TEXT,predecessor_session_key TEXT,predecessor_session_ref_json TEXT,successor_session_id TEXT,successor_session_key TEXT,successor_session_ref_json TEXT,state TEXT NOT NULL,failure TEXT,created_at TEXT NOT NULL,accepted_at TEXT,PRIMARY KEY(run_id,generation));
|
|
614
727
|
CREATE TABLE IF NOT EXISTS role_mutation_leases(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,generation INTEGER NOT NULL,provider_session_id TEXT NOT NULL,provider_session_key TEXT,provider_session_ref_json TEXT,moved_at TEXT NOT NULL,PRIMARY KEY(run_id,role));
|
|
615
|
-
CREATE TABLE IF NOT EXISTS compaction_attempts(idempotency_key TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,provider_session_id TEXT,session_key TEXT,session_ref_json TEXT,crossing_key TEXT NOT NULL,status TEXT NOT NULL,before_sample_json TEXT,after_sample_json TEXT,error TEXT,created_at TEXT NOT NULL,updated_at TEXT NOT NULL);
|
|
728
|
+
CREATE TABLE IF NOT EXISTS compaction_attempts(idempotency_key TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,provider_session_id TEXT,session_key TEXT,session_ref_json TEXT,crossing_key TEXT NOT NULL,status TEXT NOT NULL,before_sample_json TEXT,after_sample_json TEXT,error TEXT,origin TEXT NOT NULL DEFAULT 'rafi-manual',provider_event_id TEXT,threshold_generation_id TEXT,created_at TEXT NOT NULL,updated_at TEXT NOT NULL);
|
|
729
|
+
CREATE TABLE IF NOT EXISTS threshold_generations(generation_id TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,provider_session_id TEXT,session_key TEXT,session_ref_json TEXT,generation INTEGER NOT NULL,state TEXT NOT NULL,configured_ceiling_percent REAL NOT NULL,installed_native_token_limit INTEGER,installed_native_percent REAL,settings_revision INTEGER NOT NULL,model TEXT NOT NULL,latest_sample_json TEXT,frozen_action_digest TEXT,provider_event_id TEXT,created_at TEXT NOT NULL,updated_at TEXT NOT NULL,UNIQUE(run_id,role,session_key,generation));
|
|
730
|
+
CREATE TABLE IF NOT EXISTS active_role_adapter_sessions(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,provider_session_id TEXT,session_key TEXT NOT NULL,settings_revision INTEGER NOT NULL,observed_at TEXT NOT NULL,PRIMARY KEY(run_id,role,session_key));
|
|
616
731
|
CREATE TABLE IF NOT EXISTS recovery_decisions(decision_id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),decided_at TEXT NOT NULL,mode TEXT NOT NULL,digest TEXT NOT NULL UNIQUE,session_key TEXT,session_ref_json TEXT,receipt_json TEXT NOT NULL);
|
|
617
732
|
CREATE INDEX IF NOT EXISTS workflow_events_run ON workflow_events(run_id,sequence);
|
|
618
733
|
CREATE INDEX IF NOT EXISTS operations_run ON operation_journal(run_id,status);
|
|
@@ -630,7 +745,7 @@ export class WorkflowDb {
|
|
|
630
745
|
continuity_checkpoints: { session_key: "TEXT", session_ref_json: "TEXT" },
|
|
631
746
|
handoffs: { predecessor_session_key: "TEXT", predecessor_session_ref_json: "TEXT", successor_session_key: "TEXT", successor_session_ref_json: "TEXT" },
|
|
632
747
|
role_mutation_leases: { provider_session_key: "TEXT", provider_session_ref_json: "TEXT" },
|
|
633
|
-
compaction_attempts: { session_key: "TEXT", session_ref_json: "TEXT" },
|
|
748
|
+
compaction_attempts: { session_key: "TEXT", session_ref_json: "TEXT", origin: "TEXT NOT NULL DEFAULT 'rafi-manual'", provider_event_id: "TEXT", threshold_generation_id: "TEXT" },
|
|
634
749
|
recovery_decisions: { session_key: "TEXT", session_ref_json: "TEXT" },
|
|
635
750
|
})) {
|
|
636
751
|
for (const [column, definition] of Object.entries(columns))
|
|
@@ -642,6 +757,10 @@ export class WorkflowDb {
|
|
|
642
757
|
CREATE INDEX IF NOT EXISTS context_samples_scoped ON context_samples(run_id,role,session_key,sample_id);
|
|
643
758
|
CREATE INDEX IF NOT EXISTS session_usage_scoped ON session_usage_samples(run_id,role,session_key,sample_id);
|
|
644
759
|
CREATE INDEX IF NOT EXISTS compaction_session_scoped ON compaction_attempts(run_id,role,session_key,status);
|
|
760
|
+
CREATE UNIQUE INDEX IF NOT EXISTS compaction_native_event_unique ON compaction_attempts(run_id,role,session_key,provider_event_id) WHERE provider_event_id IS NOT NULL;
|
|
761
|
+
CREATE INDEX IF NOT EXISTS threshold_generation_scoped ON threshold_generations(run_id,role,session_key,generation);
|
|
762
|
+
CREATE INDEX IF NOT EXISTS active_role_adapter_session_observed ON active_role_adapter_sessions(observed_at,run_id,role);
|
|
763
|
+
CREATE INDEX IF NOT EXISTS live_settings_session_revision ON live_settings_session_acknowledgments(revision,run_id,role,session_key);
|
|
645
764
|
`);
|
|
646
765
|
}
|
|
647
766
|
ensureColumn(table, column, definition) {
|
|
@@ -656,7 +775,23 @@ function continuityEventFromRow(row) { return { sequence: row.sequence, runId: r
|
|
|
656
775
|
function continuityCheckpointFromRow(row) { return { checkpointId: row.checkpoint_id, runId: row.run_id, role: row.role, sequence: row.event_sequence, state: row.state, delta: parseJson(row.delta_json), digest: row.digest, ...(row.predecessor_digest ? { predecessorDigest: row.predecessor_digest } : {}), authoritativeStateRevision: row.authoritative_state_revision, createdAt: row.created_at, ...(row.session_key ? { sessionKey: row.session_key } : {}), ...(row.session_ref_json ? { sessionRef: parseJson(row.session_ref_json) } : {}) }; }
|
|
657
776
|
function continuityHeadFromRow(row) { return { runId: row.run_id, role: row.role, state: row.state, sequence: row.event_sequence, digest: row.digest, authoritativeStateRevision: row.authoritative_state_revision, updatedAt: row.updated_at }; }
|
|
658
777
|
function handoffFromRow(row) { return { runId: row.run_id, generation: row.generation, manifestDigest: row.manifest_digest, markdownDigest: row.markdown_digest, ...(row.predecessor_session_id ? { predecessorSessionId: row.predecessor_session_id } : {}), ...(row.predecessor_session_ref_json ? { predecessorSessionRef: parseJson(row.predecessor_session_ref_json) } : {}), ...(row.successor_session_id ? { successorSessionId: row.successor_session_id } : {}), ...(row.successor_session_ref_json ? { successorSessionRef: parseJson(row.successor_session_ref_json) } : {}), state: row.state, createdAt: row.created_at, ...(row.accepted_at ? { acceptedAt: row.accepted_at } : {}) }; }
|
|
659
|
-
function compactionAttemptFromRow(row) { return { idempotencyKey: row.idempotency_key, runId: row.run_id, role: row.role, ...(row.provider_session_id ? { providerSessionId: row.provider_session_id } : {}), ...(row.session_key ? { sessionKey: row.session_key } : {}), ...(row.session_ref_json ? { sessionRef: parseJson(row.session_ref_json) } : {}), crossingKey: row.crossing_key, status: row.status, ...(row.before_sample_json ? { beforeSample: parseJson(row.before_sample_json) } : {}), ...(row.after_sample_json ? { afterSample: parseJson(row.after_sample_json) } : {}), ...(row.error ? { error: row.error } : {}), createdAt: row.created_at, updatedAt: row.updated_at }; }
|
|
778
|
+
function compactionAttemptFromRow(row) { return { idempotencyKey: row.idempotency_key, runId: row.run_id, role: row.role, ...(row.provider_session_id ? { providerSessionId: row.provider_session_id } : {}), ...(row.session_key ? { sessionKey: row.session_key } : {}), ...(row.session_ref_json ? { sessionRef: parseJson(row.session_ref_json) } : {}), crossingKey: row.crossing_key, status: row.status, origin: row.origin ?? "rafi-manual", ...(row.provider_event_id ? { providerEventId: row.provider_event_id } : {}), ...(row.threshold_generation_id ? { thresholdGenerationId: row.threshold_generation_id } : {}), ...(row.before_sample_json ? { beforeSample: parseJson(row.before_sample_json) } : {}), ...(row.after_sample_json ? { afterSample: parseJson(row.after_sample_json) } : {}), ...(row.error ? { error: row.error } : {}), createdAt: row.created_at, updatedAt: row.updated_at }; }
|
|
779
|
+
function thresholdGenerationFromRow(row) {
|
|
780
|
+
return {
|
|
781
|
+
generationId: row.generation_id, runId: row.run_id, role: row.role,
|
|
782
|
+
...(row.provider_session_id ? { providerSessionId: row.provider_session_id } : {}),
|
|
783
|
+
...(row.session_key ? { sessionKey: row.session_key } : {}),
|
|
784
|
+
...(row.session_ref_json ? { sessionRef: parseJson(row.session_ref_json) } : {}),
|
|
785
|
+
generation: row.generation, state: row.state, configuredCeilingPercent: row.configured_ceiling_percent,
|
|
786
|
+
...(row.installed_native_token_limit === null ? {} : { installedNativeTokenLimit: row.installed_native_token_limit }),
|
|
787
|
+
...(row.installed_native_percent === null ? {} : { installedNativePercent: row.installed_native_percent }),
|
|
788
|
+
settingsRevision: row.settings_revision, model: row.model,
|
|
789
|
+
...(row.latest_sample_json ? { latestSample: parseJson(row.latest_sample_json) } : {}),
|
|
790
|
+
...(row.frozen_action_digest ? { frozenActionDigest: row.frozen_action_digest } : {}),
|
|
791
|
+
...(row.provider_event_id ? { providerEventId: row.provider_event_id } : {}),
|
|
792
|
+
createdAt: row.created_at, updatedAt: row.updated_at,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
660
795
|
function json(value) { return JSON.stringify(value ?? null); }
|
|
661
796
|
function parseJson(value) { return JSON.parse(value); }
|
|
662
797
|
function sessionParts(ref, rawId, suppliedKey) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-foreman",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.8",
|
|
4
4
|
"description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"dev": "tsx src/index.ts",
|
|
138
138
|
"test": "tsx --test test/*.test.ts",
|
|
139
139
|
"typecheck": "tsc --noEmit",
|
|
140
|
-
"test:live-providers": "RAFI_LIVE_PROVIDER_SESSIONS=1 tsx --test test/providerSmoke.test.ts",
|
|
140
|
+
"test:live-providers": "RAFI_LIVE_PROVIDER_SESSIONS=1 tsx --test --test-concurrency=1 test/providerSmoke.test.ts test/providerContextAcceptance.test.ts",
|
|
141
141
|
"prepublishOnly": "pnpm build && pnpm test && pnpm typecheck"
|
|
142
142
|
},
|
|
143
143
|
"keywords": [
|
|
@@ -158,8 +158,8 @@
|
|
|
158
158
|
"@anthropic-ai/claude-agent-sdk": "^0.3.148"
|
|
159
159
|
},
|
|
160
160
|
"dependencies": {
|
|
161
|
-
"special-agents": "0.8.
|
|
162
|
-
"rafi-spec": "0.8.
|
|
161
|
+
"special-agents": "0.8.6",
|
|
162
|
+
"rafi-spec": "0.8.6",
|
|
163
163
|
"@clack/prompts": "^1.4.0",
|
|
164
164
|
"ajv": "^8.20.0",
|
|
165
165
|
"ajv-formats": "^3.0.1",
|