@hicaru/pi-rlm 0.3.5 → 0.3.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 +1 -1
- package/package.json +2 -2
- package/src/bridge/handlers/llm-query.ts +73 -17
- package/src/bridge/handlers/rlm-query.ts +130 -8
- package/src/bridge/handlers/types.ts +12 -0
- package/src/config/defaults.ts +17 -0
- package/src/config/settings.ts +45 -2
- package/src/core/answer.ts +7 -10
- package/src/core/budget.ts +182 -0
- package/src/core/compaction.ts +46 -0
- package/src/core/engine.ts +171 -5
- package/src/core/ledger.ts +451 -0
- package/src/core/memory.ts +577 -0
- package/src/core/model-registry.ts +88 -0
- package/src/core/types.ts +44 -3
- package/src/index.ts +60 -9
- package/src/mode/rlm-mode.ts +47 -9
- package/src/prompts/glossary.ts +139 -57
- package/src/prompts/native.ts +12 -7
- package/src/prompts/system.ts +22 -7
- package/src/prompts/user.ts +6 -3
- package/src/sandbox/interrupts.ts +24 -0
- package/src/sandbox/protocol.ts +69 -5
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +11 -6
- package/src/sandbox/py/scaffold.py +615 -0
- package/src/sandbox/py/worker.py +53 -506
- package/src/sandbox/sandbox.ts +21 -3
- package/src/text/repl-output.ts +15 -0
- package/src/tool/repl-result.ts +54 -10
- package/src/tool/repl-tool.ts +43 -9
- package/src/ui/status.ts +4 -1
- package/src/util/concurrency.ts +47 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskLedger — the session blackboard (port of rlm_test v5 `ledger.py`).
|
|
3
|
+
*
|
|
4
|
+
* One instance per root run (engine) or per session (native repl tool), threaded down to every
|
|
5
|
+
* child through `SubcallHandlerDeps.ledger` / `RlmInput.ledger` — the same seam as
|
|
6
|
+
* getChildContext. It gives every agent global state visibility (the `[ledger]` block) and
|
|
7
|
+
* stops duplicate work three ways:
|
|
8
|
+
* - exact-hash coalesce: identical task → one runner, many waiters
|
|
9
|
+
* - ancestor-echo reject: a child restating an ancestor task gets a stub
|
|
10
|
+
* - near-dup coalesce: Jaccard ≥ 0.8, or ≥ 0.7 with the same path set
|
|
11
|
+
* plus the `rlmBudget` demotion counter (extra rlm_query → llm_query).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
const NOISE = /\b(no edits?|do not edit|analysis[- ]only|do not change)\.?/gi;
|
|
17
|
+
const TOK = /[a-z0-9_]{2,}/g;
|
|
18
|
+
const NEAR_JACCARD = 0.8;
|
|
19
|
+
const NEAR_JACCARD_SAME_PATHS = 0.7;
|
|
20
|
+
const ECHO_JACCARD = 0.8;
|
|
21
|
+
const INFLIGHT_LINES = 8;
|
|
22
|
+
const DONE_LINES = 6;
|
|
23
|
+
const PROMPT_PREVIEW = 80;
|
|
24
|
+
/** v5 wait() parity (audit H1): a coalescing twin never parks forever. Generous default —
|
|
25
|
+
* a twin can legitimately wait out a full child engine run. */
|
|
26
|
+
export const WAIT_TIMEOUT_MS = 600_000;
|
|
27
|
+
|
|
28
|
+
export type ClaimKind = "llm" | "rlm";
|
|
29
|
+
export type ClaimStatus = "pending" | "running" | "done" | "error";
|
|
30
|
+
|
|
31
|
+
/** All-readonly (project rule): transitions replace the map entry with a new frozen Claim. */
|
|
32
|
+
export interface Claim {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly kind: ClaimKind;
|
|
35
|
+
readonly prompt: string;
|
|
36
|
+
readonly paths: readonly string[];
|
|
37
|
+
readonly depth: number;
|
|
38
|
+
readonly status: ClaimStatus;
|
|
39
|
+
readonly result: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ClaimRequest {
|
|
43
|
+
readonly kind: ClaimKind;
|
|
44
|
+
readonly prompt: string;
|
|
45
|
+
readonly paths: readonly string[];
|
|
46
|
+
readonly depth: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Result of `tryClaim` — a discriminated union, never an exception. */
|
|
50
|
+
export type ClaimDecision =
|
|
51
|
+
| { readonly type: "run"; readonly key: string }
|
|
52
|
+
| { readonly type: "coalesce"; readonly key: string; readonly done: boolean }
|
|
53
|
+
| { readonly type: "echo" };
|
|
54
|
+
|
|
55
|
+
export interface LedgerHits {
|
|
56
|
+
readonly exact: number;
|
|
57
|
+
readonly echo: number;
|
|
58
|
+
readonly near: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface Waiter {
|
|
62
|
+
readonly resolve: (result: string) => void;
|
|
63
|
+
readonly reject: (err: Error) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** v5 `normalize_prompt`: lowercase, strip standing instructions noise, fold whitespace.
|
|
67
|
+
* v5 strips trailing periods too (`.strip(" .\t")`) — `trimEnd(" .")` mirrors that. */
|
|
68
|
+
export function normalizePrompt(prompt: string): string {
|
|
69
|
+
NOISE.lastIndex = 0;
|
|
70
|
+
return prompt
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(NOISE, " ")
|
|
73
|
+
.replace(/[^a-z0-9_/.-]+/g, " ")
|
|
74
|
+
.replace(/\s+/g, " ")
|
|
75
|
+
.replace(/^[ .\t]+|[ .\t]+$/g, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function tokenSet(text: string): ReadonlySet<string> {
|
|
79
|
+
return new Set(text.toLowerCase().match(TOK) ?? []);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function jaccard(a: ReadonlySet<string>, b: ReadonlySet<string>): number {
|
|
83
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
84
|
+
let inter = 0;
|
|
85
|
+
for (const t of a) if (b.has(t)) inter++;
|
|
86
|
+
return inter / (a.size + b.size - inter);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function pathSig(paths: readonly string[]): string {
|
|
90
|
+
const cleaned = new Set<string>();
|
|
91
|
+
for (const p of paths) {
|
|
92
|
+
if (p) cleaned.add(p.replace(/\\/g, "/").replace(/\/+$/, ""));
|
|
93
|
+
}
|
|
94
|
+
return [...cleaned].sort().join(",");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sha256Hex(text: string): string {
|
|
98
|
+
return createHash("sha256").update(text).digest("hex");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Type guard (project rule: no `as` narrowing) — used by contextSig over unknown payloads. */
|
|
102
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
103
|
+
return typeof value === "object" && value !== null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** v5 `context_sig`: fingerprint a packed context so same-question/different-haystack never collide. */
|
|
107
|
+
export function contextSig(context: unknown): string {
|
|
108
|
+
if (context === undefined || context === null) return "";
|
|
109
|
+
if (typeof context === "string") return context === "" ? "" : sha256Hex(context).slice(0, 16);
|
|
110
|
+
if (Array.isArray(context)) {
|
|
111
|
+
const h = createHash("sha256");
|
|
112
|
+
for (const item of context) {
|
|
113
|
+
if (isRecord(item)) {
|
|
114
|
+
const path = typeof item.path === "string" ? item.path : "";
|
|
115
|
+
const body = typeof item.content === "string" ? item.content : typeof item.text === "string" ? item.text : "";
|
|
116
|
+
h.update(path);
|
|
117
|
+
h.update("\0");
|
|
118
|
+
h.update(body);
|
|
119
|
+
h.update("\n");
|
|
120
|
+
} else {
|
|
121
|
+
h.update(String(item));
|
|
122
|
+
h.update("\n");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return h.digest("hex").slice(0, 16);
|
|
126
|
+
}
|
|
127
|
+
return sha256Hex(String(context)).slice(0, 16);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function taskKey(
|
|
131
|
+
kind: string,
|
|
132
|
+
prompt: string,
|
|
133
|
+
paths: readonly string[],
|
|
134
|
+
model: string,
|
|
135
|
+
ctx: string,
|
|
136
|
+
): string {
|
|
137
|
+
const raw = `${kind}|${model}|${pathSig(paths)}|${normalizePrompt(prompt)}|${ctx}`;
|
|
138
|
+
return sha256Hex(raw).slice(0, 24);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const ECHO_STUB: string = Object.freeze(
|
|
142
|
+
"[ledger echo] this task restates an ancestor goal — the parent run already covers it. " +
|
|
143
|
+
"Do not spawn a duplicate; answer from what you already know or await the existing task.",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const RLM_CALL_OPEN = /\brlm_(?:query|batch)\s*\(/g;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Native `repl()` cells are Python, not a task (audit R1). Pull quoted
|
|
150
|
+
* `rlm_query` / `rlm_batch` arguments so `beginRun` has a task-shaped ancestor
|
|
151
|
+
* instead of `print` / `await_task` tokens. Falls back to the raw cell when no
|
|
152
|
+
* such call is present. `paths=` keyword args are not tasks.
|
|
153
|
+
*/
|
|
154
|
+
export function nativeRunAncestors(code: string): readonly string[] {
|
|
155
|
+
const found = extractRlmTaskPrompts(code);
|
|
156
|
+
return Object.freeze(found.length > 0 ? found : [code]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractRlmTaskPrompts(code: string): readonly string[] {
|
|
160
|
+
const out: string[] = [];
|
|
161
|
+
RLM_CALL_OPEN.lastIndex = 0;
|
|
162
|
+
for (const m of code.matchAll(RLM_CALL_OPEN)) {
|
|
163
|
+
const start = (m.index ?? 0) + m[0].length;
|
|
164
|
+
const body = sliceCallBody(code, start);
|
|
165
|
+
const pathSplit = body.split(/\bpaths\s*=/);
|
|
166
|
+
const taskPart = pathSplit[0] ?? body;
|
|
167
|
+
const strings = quotedStrings(taskPart);
|
|
168
|
+
for (let i = 0; i < strings.length; i++) {
|
|
169
|
+
const s = strings[i];
|
|
170
|
+
if (s !== undefined && s.trim() !== "") out.push(s);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sliceCallBody(src: string, start: number): string {
|
|
177
|
+
let depth = 1;
|
|
178
|
+
let i = start;
|
|
179
|
+
while (i < src.length && depth > 0) {
|
|
180
|
+
const c = src[i];
|
|
181
|
+
if (c === "'" || c === '"') {
|
|
182
|
+
i = skipPyString(src, i);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (c === "#") {
|
|
186
|
+
const nl = src.indexOf("\n", i);
|
|
187
|
+
i = nl === -1 ? src.length : nl + 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (c === "(") depth++;
|
|
191
|
+
else if (c === ")") depth--;
|
|
192
|
+
i++;
|
|
193
|
+
}
|
|
194
|
+
return src.slice(start, depth === 0 ? i - 1 : i);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function quotedStrings(src: string): readonly string[] {
|
|
198
|
+
const out: string[] = [];
|
|
199
|
+
let i = 0;
|
|
200
|
+
while (i < src.length) {
|
|
201
|
+
const c = src[i];
|
|
202
|
+
if (c === "'" || c === '"') {
|
|
203
|
+
const parsed = readPyString(src, i);
|
|
204
|
+
if (parsed.keep) out.push(parsed.value);
|
|
205
|
+
i = parsed.end;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function skipPyString(src: string, quoteAt: number): number {
|
|
214
|
+
return readPyString(src, quoteAt).end;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readPyString(
|
|
218
|
+
src: string,
|
|
219
|
+
quoteAt: number,
|
|
220
|
+
): { readonly value: string; readonly end: number; readonly keep: boolean } {
|
|
221
|
+
const quote = src[quoteAt] ?? '"';
|
|
222
|
+
const prefix = quoteAt > 0 ? src[quoteAt - 1] : "";
|
|
223
|
+
const keep = prefix !== "f" && prefix !== "F";
|
|
224
|
+
const triple = src.startsWith(quote + quote + quote, quoteAt);
|
|
225
|
+
const delimLen = triple ? 3 : 1;
|
|
226
|
+
const from = quoteAt + delimLen;
|
|
227
|
+
if (triple) {
|
|
228
|
+
const close = src.indexOf(quote + quote + quote, from);
|
|
229
|
+
if (close === -1) return { value: src.slice(from), end: src.length, keep };
|
|
230
|
+
return { value: src.slice(from, close), end: close + 3, keep };
|
|
231
|
+
}
|
|
232
|
+
const parts: string[] = [];
|
|
233
|
+
let j = from;
|
|
234
|
+
while (j < src.length) {
|
|
235
|
+
const ch = src[j];
|
|
236
|
+
if (ch === "\\") {
|
|
237
|
+
parts.push(src[j + 1] ?? "");
|
|
238
|
+
j += 2;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (ch === quote) return { value: parts.join(""), end: j + 1, keep };
|
|
242
|
+
parts.push(ch ?? "");
|
|
243
|
+
j++;
|
|
244
|
+
}
|
|
245
|
+
return { value: parts.join(""), end: src.length, keep };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export class TaskLedger {
|
|
249
|
+
private readonly claims = new Map<string, Claim>();
|
|
250
|
+
private readonly waiters = new Map<string, Waiter[]>();
|
|
251
|
+
private readonly stack: string[] = [];
|
|
252
|
+
private readonly hitCounts = { exact: 0, echo: 0, near: 0 };
|
|
253
|
+
private rlmRuns = 0;
|
|
254
|
+
|
|
255
|
+
/** Engine marks the active run's root prompt — the ancestor chain for echo detection. */
|
|
256
|
+
beginRun(rootPrompt: string): void {
|
|
257
|
+
this.stack.push(normalizePrompt(rootPrompt));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
endRun(): void {
|
|
261
|
+
this.stack.pop();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Native `repl()` path (audit R1): push task-shaped ancestors extracted from the cell. */
|
|
265
|
+
beginNativeCell(code: string): number {
|
|
266
|
+
const ancestors = nativeRunAncestors(code);
|
|
267
|
+
for (let i = 0; i < ancestors.length; i++) {
|
|
268
|
+
const a = ancestors[i];
|
|
269
|
+
if (a !== undefined) this.beginRun(a);
|
|
270
|
+
}
|
|
271
|
+
return ancestors.length;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
endNativeCell(n: number): void {
|
|
275
|
+
for (let i = 0; i < n; i++) this.endRun();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A child prompt echoing any ancestor (exact or ≥ 0.8 Jaccard) is rejected as a stub. */
|
|
279
|
+
detectEcho(prompt: string): boolean {
|
|
280
|
+
const np = normalizePrompt(prompt);
|
|
281
|
+
if (np === "") return false;
|
|
282
|
+
const toks = tokenSet(np);
|
|
283
|
+
for (const anc of this.stack) {
|
|
284
|
+
if (anc === np) return true;
|
|
285
|
+
if (anc !== "" && jaccard(toks, tokenSet(anc)) >= ECHO_JACCARD) return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Near-duplicate over inflight + done claims (Jaccard, or lower bar with identical paths). */
|
|
291
|
+
findNear(prompt: string, paths: readonly string[]): Claim | undefined {
|
|
292
|
+
const toks = tokenSet(normalizePrompt(prompt));
|
|
293
|
+
const ps = pathSig(paths);
|
|
294
|
+
for (const c of this.claims.values()) {
|
|
295
|
+
if (c.status === "error") continue;
|
|
296
|
+
const score = jaccard(toks, tokenSet(normalizePrompt(c.prompt)));
|
|
297
|
+
if (score >= NEAR_JACCARD) return c;
|
|
298
|
+
if (score >= NEAR_JACCARD_SAME_PATHS && pathSig(c.paths) === ps) return c;
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Exact-key lookup among live (non-error) claims. */
|
|
304
|
+
lookup(key: string): Claim | undefined {
|
|
305
|
+
const c = this.claims.get(key);
|
|
306
|
+
return c !== undefined && c.status !== "error" ? c : undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Atomically decide: run it, coalesce onto an existing runner, or reject as echo. */
|
|
310
|
+
tryClaim(req: ClaimRequest, key: string): ClaimDecision {
|
|
311
|
+
if (this.detectEcho(req.prompt)) {
|
|
312
|
+
this.hitCounts.echo++;
|
|
313
|
+
return { type: "echo" };
|
|
314
|
+
}
|
|
315
|
+
const exact = this.lookup(key);
|
|
316
|
+
if (exact !== undefined) {
|
|
317
|
+
this.hitCounts.exact++;
|
|
318
|
+
return { type: "coalesce", key, done: exact.status === "done" };
|
|
319
|
+
}
|
|
320
|
+
const near = this.findNear(req.prompt, req.paths);
|
|
321
|
+
if (near !== undefined) {
|
|
322
|
+
this.hitCounts.near++;
|
|
323
|
+
return { type: "coalesce", key: near.key, done: near.status === "done" };
|
|
324
|
+
}
|
|
325
|
+
this.claims.set(key, Object.freeze({
|
|
326
|
+
key,
|
|
327
|
+
kind: req.kind,
|
|
328
|
+
prompt: req.prompt,
|
|
329
|
+
paths: Object.freeze([...req.paths]),
|
|
330
|
+
depth: req.depth,
|
|
331
|
+
status: "pending",
|
|
332
|
+
result: null,
|
|
333
|
+
}));
|
|
334
|
+
if (req.kind === "rlm") this.rlmRuns++;
|
|
335
|
+
return { type: "run", key };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** v5 `begin_run` parity: the runner actually started — inflight lines say "running". */
|
|
339
|
+
markRunning(key: string): void {
|
|
340
|
+
const claim = this.claims.get(key);
|
|
341
|
+
if (claim === undefined || claim.status !== "pending") return;
|
|
342
|
+
this.claims.set(key, Object.freeze({ ...claim, status: "running" }));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Park a waiter on a claim (coalescing twin). Bounded by `timeoutMs` (audit H1): a dead
|
|
346
|
+
* runner must park nobody forever. Rejects immediately on an errored claim. */
|
|
347
|
+
waitFor(key: string, timeoutMs: number = WAIT_TIMEOUT_MS): Promise<string> {
|
|
348
|
+
const claim = this.claims.get(key);
|
|
349
|
+
if (claim !== undefined && claim.status === "done" && claim.result !== null) {
|
|
350
|
+
return Promise.resolve(claim.result);
|
|
351
|
+
}
|
|
352
|
+
if (claim !== undefined && claim.status === "error") {
|
|
353
|
+
return Promise.reject(new Error(`ledger: claim ${key} failed while waiting`));
|
|
354
|
+
}
|
|
355
|
+
return new Promise<string>((resolve, reject) => {
|
|
356
|
+
const timer = setTimeout(() => {
|
|
357
|
+
const waiters = this.waiters.get(key);
|
|
358
|
+
if (waiters === undefined) return;
|
|
359
|
+
this.waiters.set(key, waiters.filter((w) => w.resolve !== resolve && w.reject !== reject));
|
|
360
|
+
reject(new Error(`[ledger: timeout waiting for ${key}]`));
|
|
361
|
+
}, timeoutMs);
|
|
362
|
+
const wrapped: Waiter = {
|
|
363
|
+
resolve: (result) => {
|
|
364
|
+
clearTimeout(timer);
|
|
365
|
+
resolve(result);
|
|
366
|
+
},
|
|
367
|
+
reject: (err) => {
|
|
368
|
+
clearTimeout(timer);
|
|
369
|
+
reject(err);
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
const list = this.waiters.get(key) ?? [];
|
|
373
|
+
list.push(wrapped);
|
|
374
|
+
this.waiters.set(key, list);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Runner finished — store the result and wake every coalescing waiter. */
|
|
379
|
+
finish(key: string, result: string): void {
|
|
380
|
+
const claim = this.claims.get(key);
|
|
381
|
+
if (claim === undefined) return;
|
|
382
|
+
this.claims.set(key, Object.freeze({ ...claim, status: "done", result }));
|
|
383
|
+
this.wake(key, result, undefined);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Runner failed — waiters get the error; the key becomes claimable again. */
|
|
387
|
+
fail(key: string, error: string): void {
|
|
388
|
+
const claim = this.claims.get(key);
|
|
389
|
+
if (claim === undefined) return;
|
|
390
|
+
this.claims.set(key, Object.freeze({ ...claim, status: "error" }));
|
|
391
|
+
this.wake(key, undefined, new Error(error));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private wake(key: string, result: string | undefined, err: Error | undefined): void {
|
|
395
|
+
const list = this.waiters.get(key);
|
|
396
|
+
if (list === undefined) return;
|
|
397
|
+
this.waiters.delete(key);
|
|
398
|
+
for (const w of list) {
|
|
399
|
+
if (err !== undefined) w.reject(err);
|
|
400
|
+
else if (result !== undefined) w.resolve(result);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Real rlm runs started (claimed) — drives the `rlmBudget` demotion. */
|
|
405
|
+
rlmCount(): number {
|
|
406
|
+
return this.rlmRuns;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
hits(): LedgerHits {
|
|
410
|
+
return Object.freeze({ ...this.hitCounts });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Compact table for the sandbox's `list_claims()` REPL call. */
|
|
414
|
+
listClaims(): string {
|
|
415
|
+
if (this.claims.size === 0) return "ledger: no claims";
|
|
416
|
+
const lines: string[] = new Array<string>(this.claims.size + 1);
|
|
417
|
+
lines[0] = "ledger claims:";
|
|
418
|
+
let n = 1;
|
|
419
|
+
for (const c of this.claims.values()) {
|
|
420
|
+
lines[n++] = ` ${c.key.slice(0, 8)} ${c.kind} ${c.status} depth=${c.depth} paths=${pathSig(c.paths) || "-"} '${c.prompt.slice(0, PROMPT_PREVIEW)}'`;
|
|
421
|
+
}
|
|
422
|
+
return lines.slice(0, n).join("\n");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** v5 verbatim `[ledger]` block — empty when nothing is claimed and the stack is shallow. */
|
|
426
|
+
injectBlock(): string {
|
|
427
|
+
const inflight: string[] = [];
|
|
428
|
+
const done: string[] = [];
|
|
429
|
+
for (const c of this.claims.values()) {
|
|
430
|
+
const line = ` ${c.key.slice(0, 8)} ${c.kind} paths=${pathSig(c.paths) || "-"} '${c.prompt.slice(0, PROMPT_PREVIEW)}'`;
|
|
431
|
+
if (c.status === "pending" || c.status === "running") inflight.push(line);
|
|
432
|
+
else if (c.status === "done") done.push(line);
|
|
433
|
+
}
|
|
434
|
+
const stackN = this.stack.length;
|
|
435
|
+
if (inflight.length === 0 && done.length === 0 && stackN <= 1) return "";
|
|
436
|
+
const lines: string[] = [
|
|
437
|
+
"[ledger]",
|
|
438
|
+
` depth_stack=${stackN} inflight=${inflight.length} done=${done.length}`,
|
|
439
|
+
" rlm_query only for a disjoint goal. ancestor echo is rejected.",
|
|
440
|
+
];
|
|
441
|
+
if (inflight.length > 0) {
|
|
442
|
+
lines.push(" inflight:");
|
|
443
|
+
lines.push(...inflight.slice(0, INFLIGHT_LINES));
|
|
444
|
+
}
|
|
445
|
+
if (done.length > 0) {
|
|
446
|
+
lines.push(" done:");
|
|
447
|
+
lines.push(...done.slice(0, DONE_LINES));
|
|
448
|
+
}
|
|
449
|
+
return lines.join("\n");
|
|
450
|
+
}
|
|
451
|
+
}
|