@hicaru/pi-rlm 0.2.0 → 0.2.2

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.
Files changed (68) hide show
  1. package/README.md +12 -35
  2. package/README.ru.md +18 -23
  3. package/README.zh-CN.md +17 -28
  4. package/package.json +1 -1
  5. package/src/bridge/library.ts +61 -26
  6. package/src/bridge/subcall-handlers.ts +382 -0
  7. package/src/commands/rlm-config.ts +47 -18
  8. package/src/commands/rlm.ts +3 -152
  9. package/src/config/defaults.ts +7 -15
  10. package/src/config/settings.ts +8 -32
  11. package/src/context/library-context.ts +90 -17
  12. package/src/core/engine.ts +115 -360
  13. package/src/core/history.ts +1 -1
  14. package/src/core/limits.ts +5 -12
  15. package/src/core/resource-limits.ts +0 -2
  16. package/src/core/types.ts +3 -36
  17. package/src/index.ts +49 -10
  18. package/src/mode/llm-model.ts +54 -0
  19. package/src/mode/rlm-mode.ts +26 -57
  20. package/src/prompts/glossary.ts +287 -0
  21. package/src/prompts/native.ts +127 -0
  22. package/src/prompts/system.ts +14 -386
  23. package/src/sandbox/context-file.ts +154 -0
  24. package/src/sandbox/interrupts.ts +145 -0
  25. package/src/sandbox/protocol.ts +14 -69
  26. package/src/sandbox/py/guards.py +150 -0
  27. package/src/sandbox/py/retrieval.py +265 -0
  28. package/src/sandbox/py/tasks.py +116 -0
  29. package/src/sandbox/py/worker.py +836 -0
  30. package/src/sandbox/sandbox-manager.ts +33 -6
  31. package/src/sandbox/sandbox.ts +153 -182
  32. package/src/text/tokens.ts +29 -3
  33. package/src/tool/background-tasks.ts +95 -0
  34. package/src/tool/repl-details.ts +4 -2
  35. package/src/tool/repl-render.ts +58 -0
  36. package/src/tool/repl-result.ts +70 -0
  37. package/src/tool/repl-tool.ts +178 -216
  38. package/src/tool/rlm-aggregator.ts +2 -10
  39. package/src/tool/rlm-details.ts +0 -2
  40. package/src/tool/rlm-events.ts +10 -16
  41. package/src/tool/rlm-tool.ts +1 -12
  42. package/src/tool/subcall-render.ts +15 -3
  43. package/src/tool/subcall-store.ts +57 -1
  44. package/src/ui/config-panel.ts +4 -16
  45. package/src/ui/intro.ts +1 -2
  46. package/src/ui/model-picker.ts +34 -10
  47. package/src/ui/status.ts +3 -7
  48. package/src/util/concurrency.ts +91 -13
  49. package/src/util/trace.ts +42 -0
  50. package/src/bridge/fallback-todo.ts +0 -137
  51. package/src/bridge/interactive.ts +0 -65
  52. package/src/bridge/llm-query.ts +0 -156
  53. package/src/bridge/pi-interactive.ts +0 -41
  54. package/src/bridge/rlm-query.ts +0 -108
  55. package/src/core/artifacts.ts +0 -89
  56. package/src/core/critique.ts +0 -92
  57. package/src/core/gates.ts +0 -301
  58. package/src/core/pipeline-handlers.ts +0 -319
  59. package/src/core/pipeline.ts +0 -268
  60. package/src/prompts/phases.ts +0 -104
  61. package/src/sandbox/worker.py +0 -1078
  62. package/src/state/index.ts +0 -24
  63. package/src/state/internal.ts +0 -46
  64. package/src/state/paths.ts +0 -44
  65. package/src/state/reads.ts +0 -133
  66. package/src/state/resume.ts +0 -173
  67. package/src/state/rows.ts +0 -123
  68. package/src/state/writes.ts +0 -58
@@ -1,24 +0,0 @@
1
- /**
2
- * Barrel for the RLM run-state module.
3
- *
4
- * Re-exports every public symbol so `core/engine.ts` and `mode/rlm-mode.ts`
5
- * import from one door. Type-only re-exports use `export type`.
6
- */
7
-
8
- export { generateRunId, runsDir, runDir, trailPath, contextPath, snapshotPath } from "./paths.ts";
9
- export type {
10
- UsageRow,
11
- RunHeader,
12
- TurnRow,
13
- CompactionRow,
14
- TerminalRow,
15
- TodoRow,
16
- PhaseRow,
17
- Row,
18
- } from "./rows.ts";
19
- export { STATE_SCHEMA_VERSION, isHeader, isTurn, isCompaction, isPhase, isTodo, isTerminal, isRow } from "./rows.ts";
20
- export { appendRow, appendTodoRow, pruneRuns, writeContextSidecar } from "./writes.ts";
21
- export { readRows, readHeader, readContextSidecar, readLibrarySidecars, listRunIds, resolveRunId } from "./reads.ts";
22
- export type { LibrarySlot } from "./reads.ts";
23
- export { reconstructRlmState } from "./resume.ts";
24
- export type { PhaseRecon, ReconstructResult } from "./resume.ts";
@@ -1,46 +0,0 @@
1
- /** Internal helpers shared across the RLM run-state module. */
2
-
3
- import { access, readdir } from "node:fs/promises";
4
- import { errorMessage } from "../util/errors.ts";
5
-
6
- export { errorMessage } from "../util/errors.ts";
7
-
8
- export interface FailSoftOptions {
9
- readonly label?: string;
10
- readonly warn?: boolean;
11
- }
12
-
13
- const DEFAULT_FAIL_SOFT_OPTIONS = Object.freeze({});
14
-
15
- export const warn = (e: unknown): void => console.warn(`[rlm-state] ${errorMessage(e)}`);
16
-
17
- export async function failSoft<T>(
18
- fn: () => Promise<T>,
19
- fallback: T,
20
- options: FailSoftOptions = DEFAULT_FAIL_SOFT_OPTIONS,
21
- ): Promise<T> {
22
- try {
23
- return await fn();
24
- } catch (e) {
25
- if (options.warn !== false) warn(options.label ? `${options.label}: ${errorMessage(e)}` : e);
26
- return fallback;
27
- }
28
- }
29
-
30
- export async function listDirectoriesSorted(root: string): Promise<string[]> {
31
- const entries = await readdir(root, { withFileTypes: true });
32
- return entries
33
- .filter((entry) => entry.isDirectory())
34
- .map((entry) => entry.name)
35
- .sort()
36
- .reverse();
37
- }
38
-
39
- export async function pathExists(path: string): Promise<boolean> {
40
- try {
41
- await access(path);
42
- return true;
43
- } catch {
44
- return false;
45
- }
46
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Pure path/id helpers for the RLM run-state module.
3
- *
4
- * Run-IDs are filename-sortable ISO-like slugs with a random hex suffix
5
- * for sub-second collision safety. All helpers are pure — no I/O.
6
- */
7
-
8
- import { randomBytes } from "node:crypto";
9
- import { isAbsolute, join } from "node:path";
10
-
11
- const RUN_ID_SUFFIX_BYTES = 2;
12
- const ISO_DATETIME_LENGTH = 19;
13
-
14
- /** `YYYY-MM-DD_HH-MM-SS-<4hex>` — filename-sortable, sub-second collision-safe.
15
- *
16
- * Prune ordering in writes.ts:pruneRuns depends on the ISO-slug format producing
17
- * chronologically sortable strings. If the format changes, update pruning logic
18
- * to maintain oldest-first deletion. */
19
- export function generateRunId(
20
- now: Date = new Date(),
21
- suffix: string = randomBytes(RUN_ID_SUFFIX_BYTES).toString("hex"),
22
- ): string {
23
- const pad = (n: number): string => String(n).padStart(2, "0");
24
- const iso = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
25
- return `${iso.slice(0, ISO_DATETIME_LENGTH).replaceAll(":", "-").replace("T", "_")}-${suffix}`;
26
- }
27
-
28
- export const runsDir = (cwd: string, dir: string): string =>
29
- isAbsolute(dir) ? dir : join(cwd, dir);
30
-
31
- export const runDir = (cwd: string, dir: string, runId: string): string => join(runsDir(cwd, dir), runId);
32
-
33
- export const trailPath = (cwd: string, dir: string, runId: string): string => join(runDir(cwd, dir, runId), "trail.jsonl");
34
-
35
- export const contextPath = (cwd: string, dir: string, runId: string, json: boolean, index = 0): string =>
36
- join(runDir(cwd, dir, runId), index === 0
37
- ? (json ? "context.json" : "context.txt")
38
- : `context.${index}.${json ? "json" : "txt"}`);
39
-
40
- /** R-C1: per-turn snapshot files — `sandbox-<turn>.pkl` so resume can fall back to a prior turn if the latest rename failed. */
41
- export function snapshotPath(cwd: string, dir: string, runId: string, turn?: number): string {
42
- const name = turn !== undefined ? `sandbox-${turn}.pkl` : "sandbox.pkl";
43
- return join(runDir(cwd, dir, runId), name);
44
- }
@@ -1,133 +0,0 @@
1
- /**
2
- * Fail-soft JSONL readers for the RLM run-state module.
3
- *
4
- * `readRows` parses each line in its own try/catch so a truncated trailing
5
- * line cannot erase prior rows. `listRunIds` sorts directories newest-first
6
- * by the slug (ISO-like timestamps are self-sorting).
7
- */
8
-
9
- import { open, readdir, readFile } from "node:fs/promises";
10
- import { join } from "node:path";
11
- import { runsDir, runDir, trailPath, contextPath } from "./paths.ts";
12
- import { isHeader, isRow, type RunHeader, type Row } from "./rows.ts";
13
- import { errorMessage, failSoft, listDirectoriesSorted, pathExists, warn } from "./internal.ts";
14
-
15
- /**
16
- * Raw JSONL lines of a run's trail, in order. Missing or unreadable file → [].
17
- * Shared by the fail-soft reader here and the hole-detecting reader in resume.ts, which
18
- * differ only in how they treat a bad line.
19
- */
20
- export async function readTrailLines(cwd: string, dir: string, runId: string): Promise<string[]> {
21
- const path = trailPath(cwd, dir, runId);
22
- if (!await pathExists(path)) return [];
23
- const content = await failSoft(
24
- () => readFile(path, "utf-8"),
25
- undefined as string | undefined,
26
- );
27
- const trimmed = content?.trim();
28
- return trimmed ? trimmed.split("\n") : [];
29
- }
30
-
31
- /** Every well-formed row, in trail order. Malformed line → one warn, skipped. */
32
- export async function readRows(cwd: string, dir: string, runId: string): Promise<Row[]> {
33
- const lines = await readTrailLines(cwd, dir, runId);
34
- const rows: Row[] = [];
35
- for (const line of lines) {
36
- try {
37
- const row = JSON.parse(line) as unknown;
38
- if (isRow(row)) rows.push(row);
39
- else warn("skipping invalid JSONL row shape");
40
- } catch (e) {
41
- warn(`skipping malformed JSONL row — ${errorMessage(e)}`);
42
- }
43
- }
44
- return rows;
45
- }
46
-
47
- /** Read the first line of a trail file without reading the entire file (P1). */
48
- async function readFirstLine(path: string): Promise<string | undefined> {
49
- return await failSoft(async () => {
50
- const file = await open(path, "r");
51
- try {
52
- const stats = await file.stat();
53
- const size = Math.min(stats.size, 65536);
54
- if (size <= 0) return undefined;
55
- const buffer = Buffer.alloc(size);
56
- const { bytesRead } = await file.read(buffer, 0, size, 0);
57
- const content = buffer.toString("utf-8", 0, bytesRead);
58
- const nl = content.indexOf("\n");
59
- return nl >= 0 ? content.slice(0, nl) : content.trim() || undefined;
60
- } finally {
61
- await file.close();
62
- }
63
- }, undefined as string | undefined, { warn: false });
64
- }
65
-
66
- /** First well-formed header row, or undefined. Bounded read — never reads the full trail (P1). */
67
- export async function readHeader(cwd: string, dir: string, runId: string): Promise<RunHeader | undefined> {
68
- const line = await readFirstLine(trailPath(cwd, dir, runId));
69
- if (!line) return undefined;
70
- try {
71
- const row = JSON.parse(line) as unknown;
72
- return isHeader(row) ? row : undefined;
73
- } catch {
74
- return undefined;
75
- }
76
- }
77
-
78
- /** Reload context from a persistent sidecar file. */
79
- export async function readContextSidecar(cwd: string, dir: string, runId: string, json: boolean): Promise<unknown> {
80
- const path = contextPath(cwd, dir, runId, json);
81
- if (!await pathExists(path)) return undefined;
82
- const content = await failSoft(
83
- () => readFile(path, "utf-8"),
84
- undefined as string | undefined,
85
- );
86
- if (content === undefined) return undefined;
87
- try {
88
- return json ? JSON.parse(content) as unknown : content;
89
- } catch (e) {
90
- warn(e);
91
- return undefined;
92
- }
93
- }
94
-
95
- const LIBRARY_SIDECAR = /^context\.(\d+)\.(json|txt)$/;
96
-
97
- export interface LibrarySlot {
98
- readonly index: number;
99
- readonly payload: unknown;
100
- }
101
-
102
- /** Fail-soft lister for load_library resume sidecars (`context.<index>.json|txt`). */
103
- export async function readLibrarySidecars(cwd: string, dir: string, runId: string): Promise<LibrarySlot[]> {
104
- const entries = await failSoft(() => readdir(runDir(cwd, dir, runId)), [] as string[]);
105
- const slots: LibrarySlot[] = [];
106
- for (const name of entries) {
107
- const m = LIBRARY_SIDECAR.exec(name);
108
- if (!m) continue;
109
- const index = Number(m[1]);
110
- const json = m[2] === "json";
111
- const content = await failSoft(
112
- () => readFile(join(runDir(cwd, dir, runId), name), "utf-8"),
113
- undefined as string | undefined,
114
- );
115
- if (content === undefined) continue;
116
- try {
117
- slots.push({ index, payload: json ? JSON.parse(content) as unknown : content });
118
- } catch (e) { warn(e); }
119
- }
120
- return slots.sort((a, b) => a.index - b.index);
121
- }
122
-
123
- /** Enumerate run-ids by directory listing; newest first (slug sorts chronologically). */
124
- export async function listRunIds(cwd: string, dir: string): Promise<string[]> {
125
- return await failSoft(() => listDirectoriesSorted(runsDir(cwd, dir)), [], { warn: false });
126
- }
127
-
128
- /** `@latest` / explicit id resolution. */
129
- export async function resolveRunId(cwd: string, dir: string, ref: string): Promise<string | undefined> {
130
- const ids = await listRunIds(cwd, dir);
131
- if (ref === "@latest") return ids[0];
132
- return ids.includes(ref) ? ref : undefined;
133
- }
@@ -1,173 +0,0 @@
1
- /**
2
- * Resume fold — rebuilds engine state from a JSONL trail in one pass.
3
- *
4
- * The fold reuses the live engine's own prompt builders (`buildTurnPrompt`) so
5
- * the rebuilt history is faithful — DRY: the fold and the live loop share the
6
- * same message-construction helpers. Mid-file malformed rows fail; trailing
7
- * garbage from a crash is tolerated.
8
- */
9
-
10
- import { type ChatMsg } from "../bridge/model.ts";
11
- import { appendUserMessage } from "../core/history.ts";
12
- import { buildTurnPrompt } from "../prompts/user.ts";
13
- import { readHeader, readTrailLines } from "./reads.ts";
14
- import {
15
- isCompaction,
16
- isHeader,
17
- isPhase,
18
- isRow,
19
- isTerminal,
20
- isTodo,
21
- isTurn,
22
- STATE_SCHEMA_VERSION,
23
- type Row,
24
- type RunHeader,
25
- } from "./rows.ts";
26
- import { snapshotPath } from "./paths.ts";
27
- import { pathExists } from "./internal.ts";
28
-
29
- /** Artifact path + supersede flag reconstructed from phase rows. */
30
- export interface PhaseReconArtifact {
31
- readonly path: string;
32
- readonly superseded: boolean;
33
- }
34
-
35
- export interface PhaseRecon {
36
- readonly current: string;
37
- readonly advancedAt: number;
38
- readonly summary?: string;
39
- /** Repo-relative artifacts keyed by the phase that produced them. */
40
- readonly artifacts?: Readonly<Partial<Record<string, PhaseReconArtifact>>>;
41
- readonly backwardJumps?: number;
42
- }
43
-
44
- export type ReconstructResult =
45
- | {
46
- readonly ok: true;
47
- readonly header: RunHeader;
48
- readonly history: ChatMsg[];
49
- readonly pendingReplOutputs?: string;
50
- readonly usageSeed: { readonly costUsd: number; readonly inputTokens: number; readonly outputTokens: number; readonly durationMs: number };
51
- readonly best: string;
52
- readonly completedTurns: number;
53
- readonly compactions: number;
54
- /** R-C1: the latest turn whose per-turn snapshot file exists on disk (undefined ⇒ no restore). */
55
- readonly snapshotTurn: number | undefined;
56
- readonly todoRows: readonly { readonly action: string; readonly params: Record<string, unknown>; readonly result: string }[];
57
- readonly terminated: boolean;
58
- /** Reconstructed pipeline phase state. undefined ⇒ pre-v3 trail (treats as research). */
59
- readonly phase?: PhaseRecon;
60
- }
61
- | { readonly ok: false; readonly reason: "no-header" | "version-mismatch" | "no-turns" | "mid-file-hole"; readonly detail: string };
62
-
63
- /** QB: single read + parse — detects mid-file holes without reading the trail twice. */
64
- async function readRowsStrict(cwd: string, dir: string, runId: string): Promise<{ readonly rows: Row[]; readonly hole: boolean }> {
65
- const lines = await readTrailLines(cwd, dir, runId);
66
- const rows: Row[] = [];
67
- let sawBad = false;
68
- for (const line of lines) {
69
- try {
70
- const row = JSON.parse(line) as unknown;
71
- if (!isRow(row)) {
72
- sawBad = true;
73
- continue;
74
- }
75
- rows.push(row);
76
- if (sawBad) return { rows, hole: true }; // good line after a bad one = mid-file hole
77
- } catch {
78
- sawBad = true; // trailing bad line tolerated; a subsequent good line means a hole
79
- }
80
- }
81
- return { rows, hole: false };
82
- }
83
-
84
- export async function reconstructRlmState(
85
- cwd: string,
86
- dir: string,
87
- runId: string,
88
- systemPrompt: string,
89
- ): Promise<ReconstructResult> {
90
- const header = await readHeader(cwd, dir, runId);
91
- if (!header) return { ok: false, reason: "no-header", detail: runId };
92
- // QB: ??1 backward-compat — when bumping STATE_SCHEMA_VERSION, also bump this default
93
- // so trails written without an explicit `v` field are rejected rather than silently passed.
94
- if ((header.v ?? 1) !== STATE_SCHEMA_VERSION)
95
- return { ok: false, reason: "version-mismatch", detail: `run ${runId} written under schema v${header.v}` };
96
-
97
- const { rows, hole } = await readRowsStrict(cwd, dir, runId);
98
- if (hole) return { ok: false, reason: "mid-file-hole", detail: runId };
99
-
100
- let history: ChatMsg[] = [{ role: "system", content: systemPrompt }];
101
- const usageSeed = { costUsd: 0, inputTokens: 0, outputTokens: 0, durationMs: 0 };
102
- let best = "";
103
- let completedTurns = 0;
104
- let compactions = 0;
105
- let snapshotTurn: number | undefined; // R-C1: latest turn with an existing snapshot file
106
- let pendingReplOutputs: string | undefined;
107
- const todoRows: { action: string; params: Record<string, unknown>; result: string }[] = [];
108
- let terminated = false;
109
- let phase: PhaseRecon | undefined;
110
- // Append-only journal: paths stay; supersededPath flips the blueprint slot.
111
- const artifactsAcc = new Map<string, PhaseReconArtifact>();
112
-
113
- for (const row of rows) {
114
- if (isHeader(row)) continue;
115
- if (isCompaction(row)) {
116
- history = [...row.history];
117
- compactions++;
118
- usageSeed.costUsd += row.usage.costUsd;
119
- usageSeed.inputTokens += row.usage.inputTokens;
120
- usageSeed.outputTokens += row.usage.outputTokens;
121
- pendingReplOutputs = undefined;
122
- continue;
123
- }
124
- if (isTurn(row)) {
125
- const i = row.turn - 1;
126
- if (pendingReplOutputs) appendUserMessage(history, pendingReplOutputs);
127
- appendUserMessage(history, buildTurnPrompt(i, header.meta.maxIterations));
128
- history.push({ role: "assistant", content: row.response });
129
- usageSeed.costUsd += row.usage.costUsd;
130
- usageSeed.inputTokens += row.usage.inputTokens;
131
- usageSeed.outputTokens += row.usage.outputTokens;
132
- if (row.answerContent) best = row.answerContent;
133
- else if (!best && row.response.trim()) best = row.response; // C3: mirror engine fallback
134
- completedTurns = row.turn;
135
- // R-C1: verify the per-turn snapshot file exists — a crashed finalize leaves the row claiming snapshotOk:true with no pkl.
136
- if (row.snapshotOk && await pathExists(snapshotPath(cwd, dir, runId, row.turn)))
137
- snapshotTurn = row.turn;
138
- usageSeed.durationMs = row.cumulativeDurationMs; // C2: seed wall-clock
139
- pendingReplOutputs = row.replOutputs;
140
- continue;
141
- }
142
- if (isPhase(row)) {
143
- if (row.artifactPath !== undefined && row.artifactPhase !== undefined) {
144
- artifactsAcc.set(row.artifactPhase, { path: row.artifactPath, superseded: false });
145
- }
146
- // Append-only: a superseded artifact keeps its slot, flipped to superseded.
147
- if (row.supersededPath !== undefined) {
148
- const prior = artifactsAcc.get("blueprint");
149
- if (prior !== undefined) {
150
- artifactsAcc.set("blueprint", { path: prior.path, superseded: true });
151
- }
152
- }
153
- const artifactsObj: Record<string, PhaseReconArtifact> = {};
154
- for (const [k, v] of artifactsAcc) artifactsObj[k] = v;
155
- phase = {
156
- current: row.phase,
157
- advancedAt: row.turn - 1,
158
- summary: row.summary,
159
- artifacts: artifactsAcc.size > 0 ? artifactsObj : undefined,
160
- backwardJumps: row.backwardJumps,
161
- };
162
- continue;
163
- }
164
- if (isTodo(row)) {
165
- todoRows.push({ action: row.action, params: row.params, result: row.result });
166
- continue;
167
- }
168
- if (isTerminal(row)) terminated = true;
169
- }
170
-
171
- if (completedTurns === 0 && !terminated) return { ok: false, reason: "no-turns", detail: runId };
172
- return { ok: true, header, history, pendingReplOutputs, usageSeed, best, completedTurns, compactions, snapshotTurn, todoRows, terminated, phase };
173
- }
package/src/state/rows.ts DELETED
@@ -1,123 +0,0 @@
1
- /**
2
- * Run-state row shapes for the RLM JSONL audit trail.
3
- *
4
- * REPLAY CONTRACT: every field below is part of the resume fold's reconstruction.
5
- * If any field is added, removed, or its semantics change, bump STATE_SCHEMA_VERSION
6
- * so older trails are rejected rather than mis-replayed.
7
- *
8
- * Guards accept `unknown` and narrow via `hasKind` — no `any`, no `!`.
9
- */
10
-
11
- import type { ChatMsg } from "../bridge/model.ts";
12
-
13
- /** Bump when a row shape changes such that the resume fold cannot replay older files. */
14
- export const STATE_SCHEMA_VERSION = 5;
15
-
16
- export interface UsageRow {
17
- readonly costUsd: number;
18
- readonly inputTokens: number;
19
- readonly outputTokens: number;
20
- }
21
-
22
- /** Line 1 of every trail. Carries everything the fold needs to rebuild the system prompt + reload context. */
23
- export interface RunHeader {
24
- readonly kind: "header";
25
- readonly v: number;
26
- readonly runId: string;
27
- readonly ts: string;
28
- readonly rootPrompt: string;
29
- readonly context: { readonly type: string; readonly chars: number; readonly json: boolean };
30
- readonly models: { readonly model: string; readonly worker: string };
31
- /** Snapshot of the replay-affecting config (maxIterations, orchestrator, pipeline…). */
32
- readonly meta: {
33
- readonly maxIterations: number;
34
- readonly maxDepth: number;
35
- readonly orchestrator: boolean;
36
- /** pipeline-enabled gate behaviour (always true from v4 on). */
37
- readonly pipeline?: boolean;
38
- };
39
- }
40
-
41
- /** One completed turn. `response`+`replOutputs` rebuild history; the rest restore scalars. */
42
- export interface TurnRow {
43
- readonly kind: "turn";
44
- readonly turn: number; // 1-based (== engine `i + 1`)
45
- readonly ts: string;
46
- readonly response: string; // assistant message
47
- readonly replOutputs?: string; // formatReplOutputs(results) → next user message
48
- readonly answerContent?: string; // restores `best`
49
- readonly error: boolean; // turnHadError → limits.observe on resume
50
- readonly usage: UsageRow;
51
- readonly cumulativeDurationMs: number; // limits.usage().durationMs at turn-write time
52
- readonly snapshotOk: boolean; // whether sandbox.pkl reflects THIS turn
53
- }
54
-
55
- /** Emitted when compaction rewrites history; the fold replaces history wholesale. */
56
- export interface CompactionRow {
57
- readonly kind: "compaction";
58
- readonly turn: number;
59
- readonly ts: string;
60
- readonly history: readonly ChatMsg[]; // post-compaction array (small by design)
61
- readonly usage: UsageRow; // compaction model cost added to limits
62
- }
63
-
64
- export interface TodoRow {
65
- readonly kind: "todo";
66
- readonly turn: number;
67
- readonly ts: string;
68
- readonly action: string;
69
- readonly params: Record<string, unknown>;
70
- readonly result: string;
71
- }
72
-
73
- export interface TerminalRow {
74
- readonly kind: "terminal";
75
- readonly ts: string;
76
- readonly status: "completed" | "finalized" | "aborted" | "stopped";
77
- readonly answer: string;
78
- readonly iterations: number;
79
- readonly usage: UsageRow;
80
- }
81
-
82
- /** Emitted when the root RLM advances to a new pipeline phase. */
83
- export interface PhaseRow {
84
- readonly kind: "phase";
85
- readonly turn: number; // 1-based turn when advanced
86
- readonly ts: string;
87
- readonly phase: string;
88
- readonly summary?: string;
89
- /** Optional fields (no schema-version break — isPhase guard unchanged). */
90
- readonly artifactPath?: string;
91
- /** Phase that produced `artifactPath` (not inferred from order — loop-back safe). */
92
- readonly artifactPhase?: string;
93
- readonly blockersCount?: number;
94
- readonly backwardJumps?: number;
95
- /** Path of the artifact this transition superseded (validate loop-back). */
96
- readonly supersededPath?: string;
97
- }
98
-
99
- export type Row = RunHeader | TurnRow | CompactionRow | TodoRow | TerminalRow | PhaseRow;
100
-
101
- const hasKind = (r: unknown, k: Row["kind"]): boolean =>
102
- typeof r === "object" && r !== null && (r as { kind?: unknown }).kind === k;
103
-
104
- export const isHeader = (r: unknown): r is RunHeader =>
105
- hasKind(r, "header") && typeof (r as RunHeader).runId === "string" && typeof (r as RunHeader).rootPrompt === "string"
106
- && typeof (r as RunHeader).meta?.maxIterations === "number";
107
-
108
- export const isTurn = (r: unknown): r is TurnRow =>
109
- hasKind(r, "turn") && typeof (r as TurnRow).turn === "number" && typeof (r as TurnRow).response === "string";
110
-
111
- export const isCompaction = (r: unknown): r is CompactionRow =>
112
- hasKind(r, "compaction") && Array.isArray((r as CompactionRow).history);
113
-
114
- export const isTodo = (r: unknown): r is TodoRow =>
115
- hasKind(r, "todo") && typeof (r as TodoRow).action === "string" && typeof (r as TodoRow).result === "string";
116
-
117
- export const isTerminal = (r: unknown): r is TerminalRow => hasKind(r, "terminal");
118
-
119
- export const isPhase = (r: unknown): r is PhaseRow =>
120
- hasKind(r, "phase") && typeof (r as PhaseRow).phase === "string" && typeof (r as PhaseRow).turn === "number";
121
-
122
- export const isRow = (r: unknown): r is Row =>
123
- isHeader(r) || isTurn(r) || isCompaction(r) || isTodo(r) || isTerminal(r) || isPhase(r);
@@ -1,58 +0,0 @@
1
- /**
2
- * Fail-soft JSONL writes for the RLM run-state module.
3
- *
4
- * Every writer returns `boolean` and warns on failure — never throws into
5
- * the engine loop. A failed `appendRow` disables persistence for the rest
6
- * of the run without aborting the answer.
7
- */
8
-
9
- import { appendFile, mkdir, open, rm, writeFile } from "node:fs/promises";
10
- import { contextPath, runDir, runsDir, trailPath } from "./paths.ts";
11
- import type { Row, TodoRow } from "./rows.ts";
12
- import { errorMessage, failSoft, listDirectoriesSorted, warn } from "./internal.ts";
13
-
14
- /** mkdir + append one JSON line. Returns true on success; warns + false on throw. Never throws. */
15
- export async function appendRow(cwd: string, dir: string, runId: string, row: Row): Promise<boolean> {
16
- return await failSoft(async () => {
17
- await mkdir(runDir(cwd, dir, runId), { recursive: true });
18
- const path = trailPath(cwd, dir, runId);
19
- await appendFile(path, `${JSON.stringify(row)}\n`, "utf-8");
20
- // QC: fsync to flush kernel buffers — crash between write and sync would lose the last row
21
- const file = await open(path, "r+");
22
- try {
23
- await file.sync();
24
- } finally {
25
- await file.close();
26
- }
27
- return true;
28
- }, false);
29
- }
30
-
31
- export async function appendTodoRow(cwd: string, dir: string, runId: string, row: Omit<TodoRow, "kind">): Promise<boolean> {
32
- return await appendRow(cwd, dir, runId, { kind: "todo", ...row });
33
- }
34
-
35
- /** Persist a context payload for resume. Slot 0 = repo context; index ≥ 1 = load_library slots. */
36
- export async function writeContextSidecar(
37
- cwd: string, dir: string, runId: string, context: unknown, json: boolean, index = 0,
38
- ): Promise<boolean> {
39
- return await failSoft(async () => {
40
- await mkdir(runDir(cwd, dir, runId), { recursive: true });
41
- await writeFile(contextPath(cwd, dir, runId, json, index), json ? JSON.stringify(context) : String(context), "utf-8");
42
- return true;
43
- }, false);
44
- }
45
-
46
- /** Prune oldest run directories beyond maxRuns. Best-effort; never throws. */
47
- export async function pruneRuns(cwd: string, dir: string, maxRuns: number): Promise<void> {
48
- try {
49
- const ids = await listDirectoriesSorted(runsDir(cwd, dir)); // newest first (slug sorts chronologically)
50
- const pruned = ids.slice(maxRuns);
51
- if (pruned.length > 0) console.log(`[rlm-state] pruning ${pruned.length} runs (maxRuns=${maxRuns})`);
52
- for (const id of pruned) {
53
- await rm(runDir(cwd, dir, id), { recursive: true, force: true });
54
- }
55
- } catch (e) {
56
- warn(`pruneRuns failed: ${errorMessage(e)}`);
57
- }
58
- }