instar 1.3.232 → 1.3.233
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +71 -4
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +8 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +15 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/CorrectionCaptureBacklog.d.ts +85 -0
- package/dist/monitoring/CorrectionCaptureBacklog.d.ts.map +1 -0
- package/dist/monitoring/CorrectionCaptureBacklog.js +290 -0
- package/dist/monitoring/CorrectionCaptureBacklog.js.map +1 -0
- package/dist/monitoring/CorrectionCaptureLoop.d.ts +73 -3
- package/dist/monitoring/CorrectionCaptureLoop.d.ts.map +1 -1
- package/dist/monitoring/CorrectionCaptureLoop.js +172 -2
- package/dist/monitoring/CorrectionCaptureLoop.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +19 -19
- package/src/scaffold/templates.ts +1 -0
- package/upgrades/1.3.233.md +36 -0
- package/upgrades/side-effects/correction-capture-backlog.md +137 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CorrectionCaptureBacklog — a bounded, durable retry queue for captures the
|
|
3
|
+
* Correction & Preference Learning Sentinel could NOT distill at capture time
|
|
4
|
+
* because the LLM was rate-limited / capacity-throttled (spec §3.1 extension).
|
|
5
|
+
*
|
|
6
|
+
* THE PROBLEM IT FIXES: the capture→distill hop runs through a rate-limited
|
|
7
|
+
* LlmQueue behind the account-global LLM circuit breaker. Under sustained
|
|
8
|
+
* throttling EVERY distill throws (daily-cap / reserve-breach / aborted /
|
|
9
|
+
* breaker-open) and the old hot path dropped the capture silently — so on a
|
|
10
|
+
* throttled agent the correction ledger stayed permanently EMPTY even though
|
|
11
|
+
* Layer-0 was detecting corrections. This store PERSISTS the (already
|
|
12
|
+
* pre-scrubbed) capture instead of dropping it, so a later headroom window can
|
|
13
|
+
* distill it into the CorrectionLedger.
|
|
14
|
+
*
|
|
15
|
+
* PRIVACY POSTURE (critical — a bounded extension of the ephemeral-ring model):
|
|
16
|
+
* - It persists ONLY the ALREADY-PRE-SCRUBBED turns. The §3.3 deterministic
|
|
17
|
+
* scrub (scrubSecrets) runs in buildDistillPrompt BEFORE anything reaches
|
|
18
|
+
* this store — the caller hands us the post-scrub turn text, never raw user
|
|
19
|
+
* text. We re-scrub defensively on enqueue (belt-and-suspenders), so even a
|
|
20
|
+
* mis-wired caller cannot leak a raw secret to disk.
|
|
21
|
+
* - Bounded retention TWO ways: a max-entries cap (evict oldest on overflow)
|
|
22
|
+
* AND a TTL (pruneExpired discards stale rows). Entries are DELETED the
|
|
23
|
+
* instant they distill (markDistilled) or exhaust their retries (bumpAttempt).
|
|
24
|
+
* - No API route exposes backlog row content. The scrubbed turn text never
|
|
25
|
+
* crosses HTTP — only counts are observable.
|
|
26
|
+
*
|
|
27
|
+
* DISCIPLINE (mirrors CorrectionLedger): SQLite WAL, prune-in-transaction, a
|
|
28
|
+
* detected/age index, fail-open (every method swallows storage errors so a
|
|
29
|
+
* backlog fault can NEVER throw into the fire-and-forget capture seam).
|
|
30
|
+
*/
|
|
31
|
+
import Database from 'better-sqlite3';
|
|
32
|
+
import { registerSqliteHandle } from '../core/SqliteRegistry.js';
|
|
33
|
+
import crypto from 'node:crypto';
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { NativeModuleHealer } from '../memory/NativeModuleHealer.js';
|
|
37
|
+
import { scrubSecrets } from './scrubSecrets.js';
|
|
38
|
+
const DEFAULT_MAX_ENTRIES = 200;
|
|
39
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
40
|
+
const DEFAULT_MIN_RETRY_GAP_MS = 60_000;
|
|
41
|
+
const SCHEMA = [
|
|
42
|
+
`CREATE TABLE IF NOT EXISTS correction_capture_backlog (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
topic_id INTEGER NOT NULL,
|
|
45
|
+
scrubbed_turns_json TEXT NOT NULL,
|
|
46
|
+
dedupe_hash TEXT NOT NULL,
|
|
47
|
+
deterministic_weight INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
session_id TEXT,
|
|
49
|
+
captured_at INTEGER NOT NULL,
|
|
50
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
last_attempt_at INTEGER
|
|
52
|
+
)`,
|
|
53
|
+
// Oldest-first claim/evict ordering + TTL prune scan.
|
|
54
|
+
`CREATE INDEX IF NOT EXISTS idx_ccb_captured ON correction_capture_backlog(captured_at)`,
|
|
55
|
+
// Near-identical dedupe lookup.
|
|
56
|
+
`CREATE INDEX IF NOT EXISTS idx_ccb_dedupe ON correction_capture_backlog(dedupe_hash)`,
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Bounded durable backlog of pre-scrubbed captures awaiting an LLM headroom
|
|
60
|
+
* window. Every method is fail-open — a storage fault returns a safe default and
|
|
61
|
+
* never throws into the capture seam.
|
|
62
|
+
*/
|
|
63
|
+
export class CorrectionCaptureBacklog {
|
|
64
|
+
db;
|
|
65
|
+
maxEntries;
|
|
66
|
+
maxRetries;
|
|
67
|
+
minRetryGapMs;
|
|
68
|
+
onError;
|
|
69
|
+
nowFn;
|
|
70
|
+
constructor(opts) {
|
|
71
|
+
this.maxEntries = opts.maxEntries && opts.maxEntries > 0 ? opts.maxEntries : DEFAULT_MAX_ENTRIES;
|
|
72
|
+
this.maxRetries =
|
|
73
|
+
typeof opts.maxRetries === 'number' && opts.maxRetries >= 0 ? opts.maxRetries : DEFAULT_MAX_RETRIES;
|
|
74
|
+
this.minRetryGapMs =
|
|
75
|
+
typeof opts.minRetryGapMs === 'number' && opts.minRetryGapMs >= 0
|
|
76
|
+
? opts.minRetryGapMs
|
|
77
|
+
: DEFAULT_MIN_RETRY_GAP_MS;
|
|
78
|
+
this.nowFn = opts.now ?? (() => Date.now());
|
|
79
|
+
this.onError =
|
|
80
|
+
opts.onError ?? ((where, err) => console.error(`[CorrectionCaptureBacklog] ${where}:`, err));
|
|
81
|
+
if (opts.dbPath !== ':memory:') {
|
|
82
|
+
fs.mkdirSync(path.dirname(opts.dbPath), { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
this.db = NativeModuleHealer.openWithHealSync('CorrectionCaptureBacklog', () => new Database(opts.dbPath));
|
|
85
|
+
this.db.pragma('journal_mode = WAL');
|
|
86
|
+
this.db.pragma('synchronous = NORMAL');
|
|
87
|
+
for (const ddl of SCHEMA)
|
|
88
|
+
this.db.exec(ddl);
|
|
89
|
+
registerSqliteHandle(() => { try {
|
|
90
|
+
this.db?.close();
|
|
91
|
+
}
|
|
92
|
+
catch { /* already closed */ } });
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Re-scrub turns defensively (belt-and-suspenders over the §3.3 pre-scrub) and
|
|
96
|
+
* return ONLY {fromUser, text} — never the raw at-timestamp ring metadata.
|
|
97
|
+
*/
|
|
98
|
+
sanitizeTurns(turns) {
|
|
99
|
+
return (Array.isArray(turns) ? turns : []).map((t) => ({
|
|
100
|
+
fromUser: !!t.fromUser,
|
|
101
|
+
text: scrubSecrets(String(t?.text ?? '')).slice(0, 1000),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/** A stable hash over (topicId + scrubbed turn text) for near-identical dedupe. */
|
|
105
|
+
dedupeHashOf(topicId, turns) {
|
|
106
|
+
const canon = `${topicId}|` + turns.map((t) => `${t.fromUser ? 'u' : 'a'}:${t.text.trim()}`).join('\n');
|
|
107
|
+
return crypto.createHash('sha256').update(canon).digest('hex');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Persist a pre-scrubbed capture for a later distill. Dedupes against a
|
|
111
|
+
* near-identical existing entry (same topic + same scrubbed turns) — a repeat
|
|
112
|
+
* just bumps the existing row's captured_at forward rather than duplicating.
|
|
113
|
+
* Evicts the oldest entry when at the max-entries cap. Fail-open: returns the
|
|
114
|
+
* stored entry id, or null on any error (caller falls back to the old drop).
|
|
115
|
+
*/
|
|
116
|
+
enqueue(input) {
|
|
117
|
+
try {
|
|
118
|
+
if (input.topicId == null)
|
|
119
|
+
return null;
|
|
120
|
+
const turns = this.sanitizeTurns(input.turns);
|
|
121
|
+
if (turns.length === 0)
|
|
122
|
+
return null;
|
|
123
|
+
const dedupeHash = this.dedupeHashOf(input.topicId, turns);
|
|
124
|
+
const capturedAt = input.capturedAt ?? this.nowFn();
|
|
125
|
+
const txn = this.db.transaction(() => {
|
|
126
|
+
// Near-identical dedupe: refresh the existing row's recency instead of
|
|
127
|
+
// inserting a duplicate (so a rapid restate doesn't fill the cap).
|
|
128
|
+
const existing = this.db
|
|
129
|
+
.prepare(`SELECT id FROM correction_capture_backlog WHERE dedupe_hash = ? LIMIT 1`)
|
|
130
|
+
.get(dedupeHash);
|
|
131
|
+
if (existing) {
|
|
132
|
+
this.db
|
|
133
|
+
.prepare(`UPDATE correction_capture_backlog SET captured_at = ? WHERE id = ?`)
|
|
134
|
+
.run(capturedAt, existing.id);
|
|
135
|
+
return existing.id;
|
|
136
|
+
}
|
|
137
|
+
// Evict oldest rows while at/over the cap (leave room for this insert).
|
|
138
|
+
const count = this.db
|
|
139
|
+
.prepare(`SELECT COUNT(*) c FROM correction_capture_backlog`)
|
|
140
|
+
.get().c;
|
|
141
|
+
const overflow = count - (this.maxEntries - 1);
|
|
142
|
+
if (overflow > 0) {
|
|
143
|
+
this.db
|
|
144
|
+
.prepare(`DELETE FROM correction_capture_backlog
|
|
145
|
+
WHERE id IN (
|
|
146
|
+
SELECT id FROM correction_capture_backlog
|
|
147
|
+
ORDER BY captured_at ASC, id ASC LIMIT @overflow)`)
|
|
148
|
+
.run({ overflow });
|
|
149
|
+
}
|
|
150
|
+
const id = `CCB-${crypto.randomBytes(6).toString('hex')}`;
|
|
151
|
+
this.db
|
|
152
|
+
.prepare(`INSERT INTO correction_capture_backlog
|
|
153
|
+
(id, topic_id, scrubbed_turns_json, dedupe_hash, deterministic_weight,
|
|
154
|
+
session_id, captured_at, attempts, last_attempt_at)
|
|
155
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, NULL)`)
|
|
156
|
+
.run(id, input.topicId, JSON.stringify(turns), dedupeHash, Math.max(0, Math.round(input.deterministicWeight || 0)), input.sessionId ?? null, capturedAt);
|
|
157
|
+
return id;
|
|
158
|
+
});
|
|
159
|
+
return txn();
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
this.onError('enqueue', err);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Claim up to `n` oldest entries eligible for a drain attempt: not exhausted
|
|
168
|
+
* (attempts <= maxRetries) and not attempted within minRetryGapMs. Read-only —
|
|
169
|
+
* does NOT consume the row; the drainer markDistilled()s on success or
|
|
170
|
+
* bumpAttempt()s on failure. Fail-open: returns [] on error.
|
|
171
|
+
*/
|
|
172
|
+
claimBatch(n) {
|
|
173
|
+
try {
|
|
174
|
+
const limit = Math.max(0, Math.min(Math.floor(n) || 0, 50));
|
|
175
|
+
if (limit === 0)
|
|
176
|
+
return [];
|
|
177
|
+
const now = this.nowFn();
|
|
178
|
+
const cutoff = now - this.minRetryGapMs;
|
|
179
|
+
const rows = this.db
|
|
180
|
+
.prepare(`SELECT * FROM correction_capture_backlog
|
|
181
|
+
WHERE attempts <= @maxRetries
|
|
182
|
+
AND (last_attempt_at IS NULL OR last_attempt_at <= @cutoff)
|
|
183
|
+
ORDER BY captured_at ASC, id ASC
|
|
184
|
+
LIMIT @limit`)
|
|
185
|
+
.all({ maxRetries: this.maxRetries, cutoff, limit });
|
|
186
|
+
return rows.map((r) => this.rowToEntry(r)).filter((e) => e !== null);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
this.onError('claimBatch', err);
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Delete an entry once it has been distilled into the ledger. Fail-open. */
|
|
194
|
+
markDistilled(id) {
|
|
195
|
+
try {
|
|
196
|
+
this.db.prepare(`DELETE FROM correction_capture_backlog WHERE id = ?`).run(id);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
this.onError('markDistilled', err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Record a failed drain attempt. Increments attempts + stamps last_attempt_at;
|
|
204
|
+
* DROPS the entry once attempts EXCEEDS maxRetries (give-up — bounded, no
|
|
205
|
+
* infinite retry). Returns true if the entry was dropped. Fail-open.
|
|
206
|
+
*/
|
|
207
|
+
bumpAttempt(id) {
|
|
208
|
+
try {
|
|
209
|
+
const now = this.nowFn();
|
|
210
|
+
const res = this.db
|
|
211
|
+
.prepare(`UPDATE correction_capture_backlog
|
|
212
|
+
SET attempts = attempts + 1, last_attempt_at = @now
|
|
213
|
+
WHERE id = @id`)
|
|
214
|
+
.run({ id, now });
|
|
215
|
+
if (res.changes === 0)
|
|
216
|
+
return false;
|
|
217
|
+
const row = this.db
|
|
218
|
+
.prepare(`SELECT attempts FROM correction_capture_backlog WHERE id = ?`)
|
|
219
|
+
.get(id);
|
|
220
|
+
if (row && row.attempts > this.maxRetries) {
|
|
221
|
+
this.db.prepare(`DELETE FROM correction_capture_backlog WHERE id = ?`).run(id);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
this.onError('bumpAttempt', err);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Discard entries older than ttlMs (prune-in-transaction). Returns rows removed. Fail-open. */
|
|
232
|
+
pruneExpired(ttlMs) {
|
|
233
|
+
try {
|
|
234
|
+
if (!(ttlMs > 0))
|
|
235
|
+
return 0;
|
|
236
|
+
const cutoff = this.nowFn() - ttlMs;
|
|
237
|
+
const res = this.db
|
|
238
|
+
.prepare(`DELETE FROM correction_capture_backlog WHERE captured_at < ?`)
|
|
239
|
+
.run(cutoff);
|
|
240
|
+
return res.changes ?? 0;
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
this.onError('pruneExpired', err);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Current backlog depth (observability). Fail-open → 0. */
|
|
248
|
+
count() {
|
|
249
|
+
try {
|
|
250
|
+
const row = this.db
|
|
251
|
+
.prepare(`SELECT COUNT(*) c FROM correction_capture_backlog`)
|
|
252
|
+
.get();
|
|
253
|
+
return row?.c ?? 0;
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
this.onError('count', err);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
close() {
|
|
261
|
+
try {
|
|
262
|
+
this.db.close();
|
|
263
|
+
}
|
|
264
|
+
catch { /* ignore */ }
|
|
265
|
+
}
|
|
266
|
+
rowToEntry(r) {
|
|
267
|
+
try {
|
|
268
|
+
const parsed = JSON.parse(String(r.scrubbed_turns_json ?? '[]'));
|
|
269
|
+
const turns = (Array.isArray(parsed) ? parsed : []).map((t) => ({
|
|
270
|
+
fromUser: !!t.fromUser,
|
|
271
|
+
text: String(t?.text ?? ''),
|
|
272
|
+
at: 0, // ring metadata is not persisted; synthesized for the prompt builder.
|
|
273
|
+
}));
|
|
274
|
+
return {
|
|
275
|
+
id: r.id,
|
|
276
|
+
topicId: r.topic_id,
|
|
277
|
+
turns,
|
|
278
|
+
deterministicWeight: r.deterministic_weight ?? 0,
|
|
279
|
+
sessionId: r.session_id ?? null,
|
|
280
|
+
capturedAt: r.captured_at,
|
|
281
|
+
attempts: r.attempts ?? 0,
|
|
282
|
+
lastAttemptAt: r.last_attempt_at ?? null,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=CorrectionCaptureBacklog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CorrectionCaptureBacklog.js","sourceRoot":"","sources":["../../src/monitoring/CorrectionCaptureBacklog.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,10 +20,17 @@
|
|
|
20
20
|
* `kind` is enum-validated (the LLM cannot widen it); llm_confidence is advisory.
|
|
21
21
|
*
|
|
22
22
|
* LlmQueue discipline (spec §3.1): every enqueue() is wrapped in try/catch for
|
|
23
|
-
* ALL
|
|
24
|
-
* silently
|
|
23
|
+
* ALL FOUR capacity throw paths (daily-cap, reserve-breach, LlmAbortedError,
|
|
24
|
+
* breaker-open). Previously these DROPPED the capture silently. Now, when a
|
|
25
|
+
* bounded durable backlog is wired (default-on when the feature is enabled), a
|
|
26
|
+
* capacity throw PERSISTS the already-pre-scrubbed capture to the backlog so a
|
|
27
|
+
* later headroom window can distill it ('distill-backlogged'). A non-capacity
|
|
28
|
+
* throw (e.g. a malformed provider error) still drops ('distill-dropped'), and
|
|
29
|
+
* if the backlog enqueue itself fails the capture falls back to the old drop —
|
|
30
|
+
* the fire-and-forget seam never throws regardless.
|
|
25
31
|
*/
|
|
26
32
|
import type { CorrectionLedger, CorrectionKind } from './CorrectionLedger.js';
|
|
33
|
+
import type { CorrectionCaptureBacklog } from './CorrectionCaptureBacklog.js';
|
|
27
34
|
export interface CaptureTurn {
|
|
28
35
|
text: string;
|
|
29
36
|
fromUser: boolean;
|
|
@@ -80,6 +87,21 @@ export declare function buildDistillPrompt(turns: CaptureTurn[]): string;
|
|
|
80
87
|
* over the LLM's best-effort scrub).
|
|
81
88
|
*/
|
|
82
89
|
export declare function parseDistillEnvelope(raw: string): DistillEnvelope | null;
|
|
90
|
+
/**
|
|
91
|
+
* Is this distill rejection a rate-limit / capacity / backpressure condition
|
|
92
|
+
* (i.e. "the LLM is unavailable RIGHT NOW, try later") rather than a genuine
|
|
93
|
+
* fault (malformed output, programmer error)? Only capacity throws are worth
|
|
94
|
+
* backlogging — a later headroom window can succeed where they failed. The four
|
|
95
|
+
* shapes the per-sentinel distill path can throw:
|
|
96
|
+
* - LlmQueue daily-cap: Error('LLM daily spend cap exceeded')
|
|
97
|
+
* - LlmQueue reserve breach: Error('...breach interactive reserve')
|
|
98
|
+
* - LlmQueue abort: LlmAbortedError (name === 'LlmAbortedError')
|
|
99
|
+
* - circuit breaker open: LlmCircuitOpenError (`circuitOpen === true`)
|
|
100
|
+
* Plus a defensive net for provider rate-limit strings (429 / quota / "rate
|
|
101
|
+
* limit"). Conservative: an unrecognized error is NOT treated as capacity (it
|
|
102
|
+
* drops, preserving the old behavior for genuine faults).
|
|
103
|
+
*/
|
|
104
|
+
export declare function isCapacityThrow(err: unknown): boolean;
|
|
83
105
|
export type DistillFn = (prompt: string) => Promise<string>;
|
|
84
106
|
export interface CaptureAndDistillDeps {
|
|
85
107
|
ring: CaptureRing;
|
|
@@ -94,6 +116,10 @@ export interface CaptureAndDistillDeps {
|
|
|
94
116
|
maxPerWindow: number;
|
|
95
117
|
windowMs: number;
|
|
96
118
|
};
|
|
119
|
+
/** Durable retry backlog. When wired, a CAPACITY distill throw persists the
|
|
120
|
+
* pre-scrubbed capture here ('distill-backlogged') instead of dropping it, so
|
|
121
|
+
* a later headroom window can distill it. null/undefined → old drop behavior. */
|
|
122
|
+
backlog?: CorrectionCaptureBacklog | null;
|
|
97
123
|
/** Audit sink — one structured line per capture decision. Never throws. */
|
|
98
124
|
audit?: (event: {
|
|
99
125
|
decision: string;
|
|
@@ -102,7 +128,7 @@ export interface CaptureAndDistillDeps {
|
|
|
102
128
|
}) => void;
|
|
103
129
|
now?: () => number;
|
|
104
130
|
}
|
|
105
|
-
export type CaptureDecision = 'recorded' | 'noise' | 'no-signal' | 'no-topic' | 'shed' | 'rate-limited' | 'distill-dropped' | 'distill-malformed' | 'error';
|
|
131
|
+
export type CaptureDecision = 'recorded' | 'noise' | 'no-signal' | 'no-topic' | 'shed' | 'rate-limited' | 'distill-dropped' | 'distill-backlogged' | 'distill-malformed' | 'error';
|
|
106
132
|
/** Per-topic rate-limit state, owned by the loop wiring (persists across turns). */
|
|
107
133
|
export interface CaptureRateState {
|
|
108
134
|
attempts: Map<number, number[]>;
|
|
@@ -127,4 +153,48 @@ export declare function captureAndDistill(deps: CaptureAndDistillDeps, input: {
|
|
|
127
153
|
deterministicWeight: number;
|
|
128
154
|
isLearningSignal: boolean;
|
|
129
155
|
}, rateState?: CaptureRateState): Promise<CaptureDecision>;
|
|
156
|
+
export interface DrainBacklogDeps {
|
|
157
|
+
backlog: CorrectionCaptureBacklog;
|
|
158
|
+
ledger: CorrectionLedger;
|
|
159
|
+
distill: DistillFn;
|
|
160
|
+
/** Pure, NON-mutating "is the LLM available right now?" gate (breaker closed +
|
|
161
|
+
* not load-shedding). The drainer NEVER runs while this is false — draining
|
|
162
|
+
* into an open breaker would just re-fail every entry. Wire to
|
|
163
|
+
* llmCircuitAvailable() (which does not consume a half-open probe slot). */
|
|
164
|
+
llmAvailable: () => boolean;
|
|
165
|
+
/** TTL (ms) — entries older than this are pruned before draining. */
|
|
166
|
+
ttlMs?: number;
|
|
167
|
+
/** Audit sink — one structured line per drain decision. Never throws. */
|
|
168
|
+
audit?: (event: {
|
|
169
|
+
decision: string;
|
|
170
|
+
topicId: number | null;
|
|
171
|
+
detail?: string;
|
|
172
|
+
}) => void;
|
|
173
|
+
}
|
|
174
|
+
export interface DrainResult {
|
|
175
|
+
/** Why the drain stopped without processing (when applicable). */
|
|
176
|
+
skipped?: 'breaker-open' | 'empty';
|
|
177
|
+
claimed: number;
|
|
178
|
+
distilled: number;
|
|
179
|
+
recorded: number;
|
|
180
|
+
failed: number;
|
|
181
|
+
dropped: number;
|
|
182
|
+
pruned: number;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Opportunistically distill up to `batchSize` backlogged captures into the
|
|
186
|
+
* CorrectionLedger. OFF-HOT-PATH (call via void/async, NEVER on the message
|
|
187
|
+
* seam). Bounded (batchSize), fail-open (a per-entry fault bumps that entry, a
|
|
188
|
+
* top-level fault returns a safe result), and SKIPPED when the LLM is
|
|
189
|
+
* unavailable (breaker open / shedding) — draining into an open breaker would
|
|
190
|
+
* only re-trip it and waste the entries' retry budget.
|
|
191
|
+
*
|
|
192
|
+
* Per entry: build the SAME distill prompt (turns are already scrubbed on
|
|
193
|
+
* persist; buildDistillPrompt re-scrubs harmlessly), parse the envelope, and —
|
|
194
|
+
* on a real, non-noise learning — record it to the ledger then DELETE the
|
|
195
|
+
* backlog row (markDistilled). A capacity throw OR a malformed/failed distill
|
|
196
|
+
* bumps the entry's attempt count (dropped once it exceeds maxRetries). A 'noise'
|
|
197
|
+
* verdict is terminal: the row is deleted (markDistilled) — it carried no lesson.
|
|
198
|
+
*/
|
|
199
|
+
export declare function drainBacklog(deps: DrainBacklogDeps, batchSize: number): Promise<DrainResult>;
|
|
130
200
|
//# sourceMappingURL=CorrectionCaptureLoop.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CorrectionCaptureLoop.d.ts","sourceRoot":"","sources":["../../src/monitoring/CorrectionCaptureLoop.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"CorrectionCaptureLoop.d.ts","sourceRoot":"","sources":["../../src/monitoring/CorrectionCaptureLoop.ts"],"names":[],"mappings":"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"}
|
|
@@ -145,6 +145,43 @@ export function parseDistillEnvelope(raw) {
|
|
|
145
145
|
scrubbed_summary: scrubSecrets(summaryRaw || learningRaw).slice(0, 500),
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
+
// ── Capacity-throw classification ────────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Is this distill rejection a rate-limit / capacity / backpressure condition
|
|
151
|
+
* (i.e. "the LLM is unavailable RIGHT NOW, try later") rather than a genuine
|
|
152
|
+
* fault (malformed output, programmer error)? Only capacity throws are worth
|
|
153
|
+
* backlogging — a later headroom window can succeed where they failed. The four
|
|
154
|
+
* shapes the per-sentinel distill path can throw:
|
|
155
|
+
* - LlmQueue daily-cap: Error('LLM daily spend cap exceeded')
|
|
156
|
+
* - LlmQueue reserve breach: Error('...breach interactive reserve')
|
|
157
|
+
* - LlmQueue abort: LlmAbortedError (name === 'LlmAbortedError')
|
|
158
|
+
* - circuit breaker open: LlmCircuitOpenError (`circuitOpen === true`)
|
|
159
|
+
* Plus a defensive net for provider rate-limit strings (429 / quota / "rate
|
|
160
|
+
* limit"). Conservative: an unrecognized error is NOT treated as capacity (it
|
|
161
|
+
* drops, preserving the old behavior for genuine faults).
|
|
162
|
+
*/
|
|
163
|
+
export function isCapacityThrow(err) {
|
|
164
|
+
if (!err)
|
|
165
|
+
return false;
|
|
166
|
+
const e = err;
|
|
167
|
+
if (e.circuitOpen === true)
|
|
168
|
+
return true;
|
|
169
|
+
if (e.isRateLimit === true)
|
|
170
|
+
return true;
|
|
171
|
+
if (e.name === 'LlmAbortedError' || e.name === 'LlmCircuitOpenError')
|
|
172
|
+
return true;
|
|
173
|
+
const msg = typeof e.message === 'string' ? e.message.toLowerCase() : '';
|
|
174
|
+
if (!msg)
|
|
175
|
+
return false;
|
|
176
|
+
return (msg.includes('daily spend cap') ||
|
|
177
|
+
msg.includes('interactive reserve') ||
|
|
178
|
+
msg.includes('rate limit') ||
|
|
179
|
+
msg.includes('rate-limit') ||
|
|
180
|
+
msg.includes('circuit breaker open') ||
|
|
181
|
+
msg.includes('too many requests') ||
|
|
182
|
+
/\b429\b/.test(msg) ||
|
|
183
|
+
msg.includes('quota'));
|
|
184
|
+
}
|
|
148
185
|
export function makeCaptureRateState() {
|
|
149
186
|
return { attempts: new Map() };
|
|
150
187
|
}
|
|
@@ -207,8 +244,33 @@ export async function captureAndDistill(deps, input, rateState) {
|
|
|
207
244
|
try {
|
|
208
245
|
response = await deps.distill(prompt);
|
|
209
246
|
}
|
|
210
|
-
catch {
|
|
211
|
-
//
|
|
247
|
+
catch (distillErr) {
|
|
248
|
+
// A CAPACITY throw (daily-cap / reserve / abort / breaker-open) means "the
|
|
249
|
+
// LLM is unavailable right now" — persist the pre-scrubbed capture to the
|
|
250
|
+
// durable backlog so a later headroom window distills it (instead of the
|
|
251
|
+
// old silent drop that left a throttled agent's ledger permanently empty).
|
|
252
|
+
// enqueue() re-scrubs every turn defensively, so ONLY pre-scrubbed text is
|
|
253
|
+
// ever persisted. Wrapped in try/catch → any backlog fault falls back to
|
|
254
|
+
// the old drop. A NON-capacity fault still drops.
|
|
255
|
+
if (deps.backlog && isCapacityThrow(distillErr)) {
|
|
256
|
+
try {
|
|
257
|
+
const id = deps.backlog.enqueue({
|
|
258
|
+
topicId: input.topicId,
|
|
259
|
+
turns: window,
|
|
260
|
+
deterministicWeight: input.deterministicWeight,
|
|
261
|
+
sessionId: input.sessionId ?? null,
|
|
262
|
+
capturedAt: now(),
|
|
263
|
+
});
|
|
264
|
+
if (id) {
|
|
265
|
+
deps.audit?.({ decision: 'distill-backlogged', topicId: input.topicId, detail: id });
|
|
266
|
+
return 'distill-backlogged';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// @silent-fallback-ok — backlog fault must never break capture; drop.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// @silent-fallback-ok — non-capacity fault, or backlog absent/failed.
|
|
212
274
|
deps.audit?.({ decision: 'distill-dropped', topicId: input.topicId });
|
|
213
275
|
return 'distill-dropped';
|
|
214
276
|
}
|
|
@@ -246,4 +308,112 @@ export async function captureAndDistill(deps, input, rateState) {
|
|
|
246
308
|
return 'error';
|
|
247
309
|
}
|
|
248
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Opportunistically distill up to `batchSize` backlogged captures into the
|
|
313
|
+
* CorrectionLedger. OFF-HOT-PATH (call via void/async, NEVER on the message
|
|
314
|
+
* seam). Bounded (batchSize), fail-open (a per-entry fault bumps that entry, a
|
|
315
|
+
* top-level fault returns a safe result), and SKIPPED when the LLM is
|
|
316
|
+
* unavailable (breaker open / shedding) — draining into an open breaker would
|
|
317
|
+
* only re-trip it and waste the entries' retry budget.
|
|
318
|
+
*
|
|
319
|
+
* Per entry: build the SAME distill prompt (turns are already scrubbed on
|
|
320
|
+
* persist; buildDistillPrompt re-scrubs harmlessly), parse the envelope, and —
|
|
321
|
+
* on a real, non-noise learning — record it to the ledger then DELETE the
|
|
322
|
+
* backlog row (markDistilled). A capacity throw OR a malformed/failed distill
|
|
323
|
+
* bumps the entry's attempt count (dropped once it exceeds maxRetries). A 'noise'
|
|
324
|
+
* verdict is terminal: the row is deleted (markDistilled) — it carried no lesson.
|
|
325
|
+
*/
|
|
326
|
+
export async function drainBacklog(deps, batchSize) {
|
|
327
|
+
const result = { claimed: 0, distilled: 0, recorded: 0, failed: 0, dropped: 0, pruned: 0 };
|
|
328
|
+
try {
|
|
329
|
+
// Prune stale entries first (bounded retention).
|
|
330
|
+
if (deps.ttlMs && deps.ttlMs > 0) {
|
|
331
|
+
result.pruned = deps.backlog.pruneExpired(deps.ttlMs);
|
|
332
|
+
}
|
|
333
|
+
// NEVER drain while the breaker is open / shedding.
|
|
334
|
+
if (!deps.llmAvailable()) {
|
|
335
|
+
result.skipped = 'breaker-open';
|
|
336
|
+
deps.audit?.({ decision: 'drain-skipped-breaker-open', topicId: null });
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
const batch = deps.backlog.claimBatch(batchSize);
|
|
340
|
+
result.claimed = batch.length;
|
|
341
|
+
if (batch.length === 0) {
|
|
342
|
+
result.skipped = 'empty';
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
for (const entry of batch) {
|
|
346
|
+
// Re-check availability between entries — a mid-drain trip stops the batch
|
|
347
|
+
// cleanly (remaining entries keep their attempt budget for next time).
|
|
348
|
+
if (!deps.llmAvailable()) {
|
|
349
|
+
deps.audit?.({ decision: 'drain-stopped-breaker-open', topicId: entry.topicId });
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const prompt = buildDistillPrompt(entry.turns);
|
|
354
|
+
const response = await deps.distill(prompt);
|
|
355
|
+
result.distilled++;
|
|
356
|
+
const envelope = parseDistillEnvelope(response);
|
|
357
|
+
if (!envelope) {
|
|
358
|
+
// Malformed — count a failed attempt (a later retry may parse).
|
|
359
|
+
const dropped = deps.backlog.bumpAttempt(entry.id);
|
|
360
|
+
result.failed++;
|
|
361
|
+
if (dropped)
|
|
362
|
+
result.dropped++;
|
|
363
|
+
deps.audit?.({ decision: dropped ? 'drain-malformed-dropped' : 'drain-malformed', topicId: entry.topicId });
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (envelope.kind === 'noise') {
|
|
367
|
+
// Terminal — no lesson; delete the row.
|
|
368
|
+
deps.backlog.markDistilled(entry.id);
|
|
369
|
+
deps.audit?.({ decision: 'drain-noise', topicId: entry.topicId });
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const rec = deps.ledger.record({
|
|
373
|
+
kind: envelope.kind,
|
|
374
|
+
learning: envelope.learning,
|
|
375
|
+
scrubbedSummary: envelope.scrubbed_summary,
|
|
376
|
+
deterministicWeight: entry.deterministicWeight,
|
|
377
|
+
llmConfidence: envelope.llm_confidence,
|
|
378
|
+
topicId: entry.topicId,
|
|
379
|
+
sessionId: entry.sessionId ?? null,
|
|
380
|
+
});
|
|
381
|
+
if (rec) {
|
|
382
|
+
deps.backlog.markDistilled(entry.id);
|
|
383
|
+
result.recorded++;
|
|
384
|
+
deps.audit?.({ decision: 'drain-recorded', topicId: entry.topicId, detail: `${envelope.kind} ${rec.dedupeKey.slice(0, 24)}` });
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// Ledger write failed — keep the entry, count an attempt.
|
|
388
|
+
const dropped = deps.backlog.bumpAttempt(entry.id);
|
|
389
|
+
result.failed++;
|
|
390
|
+
if (dropped)
|
|
391
|
+
result.dropped++;
|
|
392
|
+
deps.audit?.({ decision: dropped ? 'drain-ledger-failed-dropped' : 'drain-ledger-failed', topicId: entry.topicId });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (entryErr) {
|
|
396
|
+
// A capacity throw or any other per-entry fault: bump the attempt and
|
|
397
|
+
// move on (the seam never throws). If this was a fresh breaker trip the
|
|
398
|
+
// next-entry availability check stops the batch.
|
|
399
|
+
const dropped = deps.backlog.bumpAttempt(entry.id);
|
|
400
|
+
result.failed++;
|
|
401
|
+
if (dropped)
|
|
402
|
+
result.dropped++;
|
|
403
|
+
deps.audit?.({
|
|
404
|
+
decision: dropped ? 'drain-failed-dropped' : 'drain-failed',
|
|
405
|
+
topicId: entry.topicId,
|
|
406
|
+
detail: entryErr instanceof Error ? entryErr.message.slice(0, 80) : undefined,
|
|
407
|
+
});
|
|
408
|
+
if (isCapacityThrow(entryErr))
|
|
409
|
+
break; // breaker likely tripped — stop the batch.
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
// Top-level fault — never propagates (off-hot-path, fail-open).
|
|
415
|
+
deps.audit?.({ decision: 'drain-error', topicId: null, detail: err instanceof Error ? err.message : String(err) });
|
|
416
|
+
}
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
249
419
|
//# sourceMappingURL=CorrectionCaptureLoop.js.map
|