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.
@@ -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>;
@@ -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
- if (observed && Number.isFinite(observed.used)) {
46
- contextSample = this.contextSample(observed, now);
47
- this.lastGoodContext = contextSample;
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 (this.lastGoodContext) {
50
- contextSample = { ...this.lastGoodContext, freshness: "stale" };
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
- contextSample = { ...this.measuringSample(now), freshness: "unavailable" };
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 = undefined;
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 { adapter: _adapter, displaySessionCost: _display, compactionCount: _compactions, handoffGeneration: _handoffs, ...base } = this.input;
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
- `compactions=${contextSample.compactionCount}`,
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";
@@ -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;
@@ -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
- this.db.prepare("INSERT OR REPLACE INTO project_settings_revisions(revision,defaults_json,created_at) VALUES(?,?,?)").run(revision, json(defaults), now.toISOString());
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 rows = this.db.prepare("SELECT * FROM live_settings_acknowledgments WHERE revision=? ORDER BY acknowledged_at").all(revision);
161
- 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 } : {}), ...(row.session_ref_json ? { sessionRef: parseJson(row.session_ref_json) } : {}), revision: row.revision, acknowledgedAt: row.acknowledged_at }));
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',?,?,?)`).run(input.idempotencyKey, input.runId, input.role, session.id, session.key, session.refJson, input.crossingKey, input.beforeSample ? json(input.beforeSample) : null, at, at);
445
- return this.compactionAttempt(input.idempotencyKey);
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
- this.insertEvent(prior.runId, input.ok ? "compaction_succeeded" : "compaction_failed", `context:${prior.role}`, { idempotencyKey, providerSessionId: prior.providerSessionId, sessionKey: prior.sessionKey, crossingKey: prior.crossingKey }, now.toISOString());
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='succeeded'`).get(runId, role, session.key)
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='succeeded'`).get(runId, role, session.id);
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.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.5",
162
- "rafi-spec": "0.8.5",
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",