@hicaru/pi-rlm 0.3.15 → 0.3.17

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 (90) hide show
  1. package/README.md +96 -70
  2. package/README.ru.md +86 -59
  3. package/README.zh-CN.md +95 -65
  4. package/package.json +5 -5
  5. package/src/bridge/add-context.ts +1 -1
  6. package/src/bridge/handlers/await.ts +13 -22
  7. package/src/bridge/handlers/completion.ts +27 -5
  8. package/src/bridge/handlers/emitting.ts +2 -2
  9. package/src/bridge/handlers/llm-query.ts +46 -68
  10. package/src/bridge/handlers/rlm-query.ts +14 -84
  11. package/src/bridge/handlers/task-registry.ts +22 -17
  12. package/src/bridge/handlers/types.ts +8 -6
  13. package/src/bridge/model.ts +21 -4
  14. package/src/commands/rlm-llm.ts +1 -10
  15. package/src/commands/rlm-rlm.ts +1 -8
  16. package/src/config/defaults.ts +31 -12
  17. package/src/config/settings.ts +47 -33
  18. package/src/config/skillstate.ts +465 -0
  19. package/src/context/md-cache.ts +1 -1
  20. package/src/context/merge.ts +1 -1
  21. package/src/context/namespace.ts +2 -2
  22. package/src/context/refresh.ts +1 -1
  23. package/src/context/source-dir.ts +21 -11
  24. package/src/context/source-doc.ts +1 -1
  25. package/src/context/source-git.ts +3 -15
  26. package/src/context/source-text.ts +1 -1
  27. package/src/context/walk.ts +6 -14
  28. package/src/core/budget.ts +107 -21
  29. package/src/core/compaction.ts +44 -1
  30. package/src/core/engine.ts +192 -94
  31. package/src/core/iteration.ts +1 -1
  32. package/src/core/ledger.ts +10 -13
  33. package/src/core/limits.ts +1 -1
  34. package/src/core/model-registry.ts +1 -1
  35. package/src/core/resource-limits.ts +1 -1
  36. package/src/core/root-context.ts +126 -0
  37. package/src/core/root-digest.ts +213 -0
  38. package/src/core/root-state.ts +240 -0
  39. package/src/core/run-state.ts +577 -0
  40. package/src/core/types.ts +56 -12
  41. package/src/index.ts +167 -36
  42. package/src/mode/llm-model.ts +13 -1
  43. package/src/mode/native-guards.ts +0 -6
  44. package/src/mode/rlm-mode.ts +34 -11
  45. package/src/mode/subagent.ts +5 -5
  46. package/src/prompts/glossary.ts +41 -25
  47. package/src/prompts/native.ts +1 -3
  48. package/src/prompts/system.ts +12 -4
  49. package/src/prompts/user.ts +17 -0
  50. package/src/sandbox/context-file.ts +1 -1
  51. package/src/sandbox/interrupts.ts +25 -31
  52. package/src/sandbox/protocol.ts +14 -20
  53. package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
  54. package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
  55. package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
  56. package/src/sandbox/py/__pycache__/worker.cpython-314.pyc +0 -0
  57. package/src/sandbox/py/guards.py +1 -1
  58. package/src/sandbox/py/retrieval.py +4 -1
  59. package/src/sandbox/py/scaffold.py +24 -31
  60. package/src/sandbox/py/worker.py +3 -1
  61. package/src/sandbox/sandbox-manager.ts +2 -2
  62. package/src/sandbox/sandbox.ts +35 -5
  63. package/src/text/agent-text.ts +58 -0
  64. package/src/text/parsing.ts +35 -3
  65. package/src/text/preview.ts +3 -0
  66. package/src/text/repl-output.ts +1 -1
  67. package/src/tool/background-tasks.ts +1 -1
  68. package/src/tool/repl-render.ts +1 -1
  69. package/src/tool/repl-result.ts +1 -1
  70. package/src/tool/repl-tool.ts +50 -26
  71. package/src/tool/rlm-tool.ts +4 -5
  72. package/src/tool/subcall-render.ts +1 -1
  73. package/src/tool/subcall-store.ts +2 -2
  74. package/src/tool/tool-utils.ts +5 -5
  75. package/src/ui/config-panel.ts +39 -0
  76. package/src/ui/intro.ts +1 -1
  77. package/src/ui/modal/timeline-store.ts +1 -1
  78. package/src/ui/model-picker/drilldown.ts +1 -1
  79. package/src/ui/model-picker/levels.ts +1 -1
  80. package/src/ui/panel/run-registry.ts +1 -1
  81. package/src/ui/tree/tree-rows.ts +1 -1
  82. package/src/ui/tree/tree-widget.ts +1 -1
  83. package/src/util/bm25.ts +97 -0
  84. package/src/util/concurrency.ts +1 -1
  85. package/src/util/errors.ts +1 -1
  86. package/src/util/retry.ts +22 -7
  87. package/src/util/state-merge.ts +34 -0
  88. package/src/util/throttle.ts +1 -1
  89. package/src/util/type-guards.ts +6 -0
  90. package/src/core/memory.ts +0 -589
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hicaru/pi-rlm",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "author": "hicaru",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,9 +10,9 @@
10
10
  "typescript": "^5.0.0"
11
11
  },
12
12
  "peerDependencies": {
13
- "@earendil-works/pi-ai": "^0.84.1",
14
- "@earendil-works/pi-coding-agent": "^0.84.1",
15
- "@earendil-works/pi-tui": "^0.84.1",
13
+ "@earendil-works/pi-ai": "^0.85.1",
14
+ "@earendil-works/pi-coding-agent": "^0.85.1",
15
+ "@earendil-works/pi-tui": "^0.85.1",
16
16
  "typebox": "*"
17
17
  },
18
18
  "bugs": {
@@ -52,7 +52,7 @@
52
52
  "node": ">=20"
53
53
  },
54
54
  "dependencies": {
55
- "@earendil-works/pi-agent-core": "^0.84.1",
55
+ "@earendil-works/pi-agent-core": "^0.85.1",
56
56
  "@firecrawl/anydoc": "^0.2.4"
57
57
  }
58
58
  }
@@ -24,7 +24,7 @@ import {
24
24
  import { resolveSource } from "../context/resolve.ts";
25
25
  import { previewText } from "../text/preview.ts";
26
26
 
27
- export interface AddContextBridgeOpts {
27
+ interface AddContextBridgeOpts {
28
28
  /** Fixed cwd (headless). Prefer getCwd when the sandbox outlives a single invocation. */
29
29
  readonly cwd?: string;
30
30
  /** Late-bound cwd (native mode — sandbox handlers outlive a single repl()). */
@@ -9,7 +9,7 @@
9
9
  import { errorMessage, formatError } from "../../util/errors.ts";
10
10
  import type { AwaitResult, SubcallHandlerDeps, TaskEntry } from "./types.ts";
11
11
  import type { SubcallOpts } from "../../sandbox/interrupts.ts";
12
- import type { AwaitDeps } from "./task-registry.ts";
12
+ import { entryToAwaitResult, type AwaitDeps } from "./task-registry.ts";
13
13
 
14
14
  export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
15
15
  return async (
@@ -37,7 +37,7 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
37
37
  if (entry.status === "pending") {
38
38
  try {
39
39
  const resolved = await ad.wait(taskId, timeoutMs);
40
- return toAwaitResult(resolved);
40
+ return entryToAwaitResult(resolved);
41
41
  } catch (err: unknown) {
42
42
  return {
43
43
  ok: false,
@@ -49,7 +49,7 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
49
49
  }
50
50
  }
51
51
 
52
- return toAwaitResult(entry);
52
+ return entryToAwaitResult(entry);
53
53
  }
54
54
 
55
55
  // Multiple tasks
@@ -91,7 +91,7 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
91
91
  }),
92
92
  );
93
93
 
94
- const awaited = resolved.map(toAwaitResult);
94
+ const awaited = resolved.map(entryToAwaitResult);
95
95
  const allDone = awaited.every((a) => a.status === "done");
96
96
  const hasResults = awaited.some((a) => a.results !== undefined);
97
97
  const firstError = awaited.find((a) => a.error)?.error;
@@ -99,12 +99,18 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
99
99
  const kind = first?.kind ?? "unknown";
100
100
 
101
101
  if (hasResults) {
102
- const allResults: string[] = [];
102
+ // Pre-allocated (rule: no .push() growth when the size is computable) — count, then fill.
103
+ const total = awaited.reduce(
104
+ (n, a) => n + (a.results !== undefined ? a.results.length : a.result !== undefined ? 1 : 0),
105
+ 0,
106
+ );
107
+ const allResults = new Array<string>(total);
108
+ let n = 0;
103
109
  for (const a of awaited) {
104
110
  if (a.results !== undefined) {
105
- for (const r of a.results) allResults.push(r);
111
+ for (const r of a.results) allResults[n++] = r;
106
112
  } else if (a.result !== undefined) {
107
- allResults.push(a.result);
113
+ allResults[n++] = a.result;
108
114
  }
109
115
  }
110
116
  return {
@@ -131,18 +137,3 @@ export function createAwaitHandler(_deps: SubcallHandlerDeps, ad: AwaitDeps) {
131
137
  };
132
138
  };
133
139
  }
134
-
135
- function toAwaitResult(entry: TaskEntry): AwaitResult {
136
- const status = entry.status === "pending" ? "error" : entry.status;
137
- return {
138
- ok: entry.status === "done",
139
- task_id: entry.taskId,
140
- kind: entry.kind,
141
- status,
142
- result: entry.result,
143
- results: entry.results,
144
- error:
145
- entry.error ??
146
- (entry.status === "pending" ? "Task still pending" : undefined),
147
- };
148
- }
@@ -14,15 +14,33 @@ import { checkResourceLimits } from "../../core/resource-limits.ts";
14
14
  import { errorMessage, formatError } from "../../util/errors.ts";
15
15
  import { retryPolicy } from "../../util/retry.ts";
16
16
  import type { Semaphore } from "../../util/concurrency.ts";
17
- import type { Invocation, SubcallConfig } from "./types.ts";
17
+ import type { Invocation, SubcallConfig, SubcallHandlerDeps } from "./types.ts";
18
18
 
19
- export interface Complete1Deps {
19
+ interface Complete1Deps {
20
20
  readonly leafGate: Semaphore;
21
21
  readonly registry: ModelRegistry;
22
22
  readonly getLlmModel: () => Model<Api>;
23
23
  readonly getConfig: () => SubcallConfig;
24
24
  readonly signal?: AbortSignal;
25
25
  readonly onUsage?: (usage: Usage, role: "sub") => void;
26
+ /** SKILL.state (Workstream D): the one grounding seam, projected verbatim from deps. */
27
+ readonly groundLeaf?: (prompt: string) => string;
28
+ }
29
+
30
+ /**
31
+ * AGENTS.md DRY: the ONE projection of SubcallHandlerDeps for complete1 — shared verbatim by
32
+ * the llm-query and rlm-query handlers. Never inline a second copy.
33
+ */
34
+ export function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
35
+ return {
36
+ leafGate: deps.gates.leaf,
37
+ registry: deps.registry,
38
+ getLlmModel: deps.getLlmModel,
39
+ getConfig: deps.getConfig,
40
+ signal: deps.signal,
41
+ onUsage: deps.onUsage,
42
+ groundLeaf: deps.groundLeaf,
43
+ };
26
44
  }
27
45
 
28
46
  /**
@@ -41,14 +59,18 @@ export async function complete1(
41
59
  timeoutMs: inv.limits.remainingTimeoutMs(),
42
60
  });
43
61
  if (limitError !== undefined) return limitError;
44
- if (prompt.length > config.maxPromptChars) {
62
+ // Workstream D (DRY #1): the ONE grounding application — llm_query, batch items, rlm→llm
63
+ // demotion and depth-cap degrade all pass through here, so all of them get Ξ facts or none
64
+ // does. Below-threshold prompts come back byte-identical from the groundLeaf impl.
65
+ const grounded = deps.groundLeaf !== undefined ? deps.groundLeaf(prompt) : prompt;
66
+ if (grounded.length > config.maxPromptChars) {
45
67
  return formatError(
46
- `sub-LLM prompt exceeded the size limit (${prompt.length.toLocaleString()} chars > ` +
68
+ `sub-LLM prompt exceeded the size limit (${grounded.length.toLocaleString()} chars > ` +
47
69
  `${config.maxPromptChars.toLocaleString()}). Shorten or chunk the prompt before calling llm_query.`,
48
70
  );
49
71
  }
50
72
  try {
51
- const messages: ChatMsg[] = [{ role: "user", content: prompt }];
73
+ const messages: ChatMsg[] = [{ role: "user", content: grounded }];
52
74
  const res = await deps.leafGate.run(() =>
53
75
  modelComplete(messages, {
54
76
  model: deps.getLlmModel(),
@@ -11,14 +11,14 @@ import { previewText } from "../../text/preview.ts";
11
11
  import type { SubcallPhase } from "../../tool/rlm-details.ts";
12
12
  import type { Invocation } from "./types.ts";
13
13
 
14
- export interface EmitOpts {
14
+ interface EmitOpts {
15
15
  readonly kind: "llm" | "batch";
16
16
  readonly label: string;
17
17
  readonly args: string;
18
18
  readonly model?: string;
19
19
  }
20
20
 
21
- export interface EmitSummary {
21
+ interface EmitSummary {
22
22
  readonly preview: string;
23
23
  readonly error?: string;
24
24
  readonly failed?: number;
@@ -4,29 +4,20 @@
4
4
 
5
5
  import type { Usage } from "@earendil-works/pi-ai";
6
6
  import { modelRef } from "../../config/settings.ts";
7
- import { complete1, type Complete1Deps } from "./completion.ts";
8
- import { emitting, summarizeLeaf, throttleHooks } from "./emitting.ts";
7
+ import { complete1, completeDeps } from "./completion.ts";
8
+ import { emitting, summarizeLeaf, throttleHooks, type EmitNote } from "./emitting.ts";
9
9
  import { formatError, errorMessage } from "../../util/errors.ts";
10
10
  import { previewText } from "../../text/preview.ts";
11
- import type { SpawnResult, SubcallHandlerDeps } from "./types.ts";
11
+ import type { Invocation, SpawnResult, SubcallHandlerDeps } from "./types.ts";
12
12
  import type { SubcallOpts } from "../../sandbox/interrupts.ts";
13
13
  import { SPAWN_HINT, spawnAndRun, type SpawnDeps } from "./task-registry.ts";
14
14
  import { ECHO_STUB, taskKey, type TaskLedger } from "../../core/ledger.ts";
15
15
 
16
- const UNWIRED = formatError("RLM bridge not wired for this invocation");
16
+ /** Shared with rlm-query.ts the unwired rejection sentinel (AGENTS DRY). */
17
+ export const UNWIRED = formatError("RLM bridge not wired for this invocation");
17
18
 
18
- function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
19
- return {
20
- leafGate: deps.gates.leaf,
21
- registry: deps.registry,
22
- getLlmModel: deps.getLlmModel,
23
- getConfig: deps.getConfig,
24
- signal: deps.signal,
25
- onUsage: deps.onUsage,
26
- };
27
- }
28
-
29
- function displayModel(deps: SubcallHandlerDeps): string | undefined {
19
+ /** Shared with rlm-query.ts — one display-model resolution (AGENTS DRY #3). */
20
+ export function displayModel(deps: SubcallHandlerDeps): string | undefined {
30
21
  try {
31
22
  const m = deps.getLlmModel();
32
23
  return modelRef(m) ?? m.id;
@@ -36,10 +27,36 @@ function displayModel(deps: SubcallHandlerDeps): string | undefined {
36
27
  }
37
28
 
38
29
  /** The ledger active for leaf calls — undefined when disabled by config or not threaded in. */
39
- function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
30
+ export function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
40
31
  return deps.getConfig().enableLedger ? deps.ledger : undefined;
41
32
  }
42
33
 
34
+ /** DRY: one unwired-spawn shape — kind/n vary, everything else is the same rejection.
35
+ * Shared with rlm-query.ts. */
36
+ export function unwiredSpawn(kind: SpawnResult["kind"], n: number): SpawnResult {
37
+ return { ok: false, task_id: null, kind, n, status: "pending", hint: SPAWN_HINT, error: UNWIRED };
38
+ }
39
+
40
+ /** DRY: the shared leaf recipe — one UI node, one complete1 execution, one summarizer. */
41
+ function emitLeaf(
42
+ inv: Invocation,
43
+ deps: SubcallHandlerDeps,
44
+ prompt: string,
45
+ exec: (track: (u: Usage) => void, note: EmitNote) => Promise<string>,
46
+ ): Promise<string> {
47
+ return emitting(
48
+ inv,
49
+ {
50
+ kind: "llm",
51
+ label: "llm_query",
52
+ args: `prompt: ${previewText(prompt)}`,
53
+ model: displayModel(deps),
54
+ },
55
+ (track, note) => exec(track, note),
56
+ summarizeLeaf,
57
+ );
58
+ }
59
+
43
60
  /** v5 TaskLedger routing for ONE leaf prompt (audit H3 — shared by llm_query and every
44
61
  * llm_batch item, which v5 routed through `_spawn_single` too): echo → stub string,
45
62
  * coalesce → the twin's result (bounded wait), run → caller executes then finish/fail.
@@ -84,31 +101,12 @@ export function createLlmQueryHandler(
84
101
  opts: SubcallOpts,
85
102
  ): Promise<SpawnResult> => {
86
103
  const inv = deps.resolve(opts, depth);
87
- if (inv === null) {
88
- return {
89
- ok: false,
90
- task_id: null,
91
- kind: "llm",
92
- n: 1,
93
- status: "pending",
94
- hint: SPAWN_HINT,
95
- error: UNWIRED,
96
- };
97
- }
104
+ if (inv === null) return unwiredSpawn("llm", 1);
98
105
 
99
106
  const cdeps = completeDeps(deps);
100
107
  const runLeaf = (): Promise<string> =>
101
- emitting(
102
- inv,
103
- {
104
- kind: "llm",
105
- label: "llm_query",
106
- args: `prompt: ${previewText(prompt)}`,
107
- model: displayModel(deps),
108
- },
109
- (track: (u: Usage) => void, note) => complete1(inv, prompt, track, cdeps, throttleHooks(note)),
110
- summarizeLeaf,
111
- );
108
+ emitLeaf(inv, deps, prompt, (track, note) =>
109
+ complete1(inv, prompt, track, cdeps, throttleHooks(note)));
112
110
  // v5 TaskLedger for leaves: identical prompts coalesce onto one completion (key has no
113
111
  // context — a leaf's entire world is the prompt text itself).
114
112
  return spawnAndRun(
@@ -132,17 +130,7 @@ export function createLlmBatchHandler(
132
130
  opts: SubcallOpts,
133
131
  ): Promise<SpawnResult> => {
134
132
  const inv = deps.resolve(opts, depth);
135
- if (inv === null) {
136
- return {
137
- ok: false,
138
- task_id: null,
139
- kind: "llm_batch",
140
- n: prompts.length,
141
- status: "pending",
142
- hint: SPAWN_HINT,
143
- error: UNWIRED,
144
- };
145
- }
133
+ if (inv === null) return unwiredSpawn("llm_batch", prompts.length);
146
134
 
147
135
  const cdeps = completeDeps(deps);
148
136
  const ledger = activeLedger(deps);
@@ -155,27 +143,17 @@ export function createLlmBatchHandler(
155
143
  () =>
156
144
  Promise.all(
157
145
  prompts.map((p) =>
158
- emitting(
159
- inv,
160
- {
161
- kind: "llm",
162
- label: "llm_query",
163
- args: `prompt: ${previewText(p)}`,
164
- model: displayModel(deps),
165
- },
146
+ emitLeaf(inv, deps, p, (track, note) =>
166
147
  // NO outer gate — complete1 takes the single leaf slot per prompt.
167
148
  // v5 (audit H3): every item routes through the ledger — duplicate prompts inside
168
149
  // one batch (or twins of other in-flight leaves) coalesce instead of paying N times.
169
- (track: (u: Usage) => void, note) =>
170
- runClaimedLeaf(
171
- ledger,
172
- ledger === undefined ? undefined : leafClaimKey(deps, p),
173
- p,
174
- inv.depth,
175
- () => complete1(inv, p, track, cdeps, throttleHooks(note)),
176
- ),
177
- summarizeLeaf,
178
- ),
150
+ runClaimedLeaf(
151
+ ledger,
152
+ ledger === undefined ? undefined : leafClaimKey(deps, p),
153
+ p,
154
+ inv.depth,
155
+ () => complete1(inv, p, track, cdeps, throttleHooks(note)),
156
+ )),
179
157
  ),
180
158
  ),
181
159
  deps.trackDetached,
@@ -4,7 +4,7 @@
4
4
  * AGENTS.md DRY #2: childRun exists once, here.
5
5
  */
6
6
 
7
- import { modelRef } from "../../config/settings.ts";
7
+ import { modelLabelOf } from "../../config/settings.ts";
8
8
  import { errorMessage, formatError } from "../../util/errors.ts";
9
9
  import { filterContextByPaths } from "../../context/merge.ts";
10
10
  import { previewText } from "../../text/preview.ts";
@@ -13,12 +13,11 @@ import { checkResourceLimits } from "../../core/resource-limits.ts";
13
13
  import { contextSig, ECHO_STUB, taskKey } from "../../core/ledger.ts";
14
14
  import type { Invocation, SpawnResult, SubcallHandlerDeps } from "./types.ts";
15
15
  import type { SubcallOpts } from "../../sandbox/interrupts.ts";
16
- import { SPAWN_HINT, spawnAndRun, type SpawnDeps } from "./task-registry.ts";
17
- import { complete1, type Complete1Deps } from "./completion.ts";
16
+ import { spawnAndRun, type SpawnDeps } from "./task-registry.ts";
17
+ import { complete1, completeDeps } from "./completion.ts";
18
18
  import { emitting, summarizeLeaf, throttleHooks } from "./emitting.ts";
19
- import { leafClaimKey, runClaimedLeaf } from "./llm-query.ts";
19
+ import { activeLedger, leafClaimKey, runClaimedLeaf, unwiredSpawn } from "./llm-query.ts";
20
20
 
21
- const UNWIRED = formatError("RLM bridge not wired for this invocation");
22
21
  const NO_UNMATCHED: readonly string[] = Object.freeze([]);
23
22
 
24
23
  function emptyResult(answer: string): RlmResult {
@@ -56,25 +55,9 @@ function childContextFor(
56
55
  });
57
56
  }
58
57
 
59
- function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
60
- return {
61
- leafGate: deps.gates.leaf,
62
- registry: deps.registry,
63
- getLlmModel: deps.getLlmModel,
64
- getConfig: deps.getConfig,
65
- signal: deps.signal,
66
- onUsage: deps.onUsage,
67
- };
68
- }
69
-
70
- /** Ledger active for this call — undefined when disabled by config or not threaded in. */
71
- function activeLedger(deps: SubcallHandlerDeps) {
72
- return deps.getConfig().enableLedger ? deps.ledger : undefined;
73
- }
74
-
75
58
  function claimKeyFor(deps: SubcallHandlerDeps, kind: "llm" | "rlm", prompt: string, paths: readonly string[], ctx: string): string {
76
59
  const rootModel = deps.getModel?.();
77
- const modelId = rootModel === undefined ? "" : (modelRef(rootModel) ?? rootModel.id);
60
+ const modelId = rootModel === undefined ? "" : modelLabelOf(rootModel);
78
61
  return taskKey(kind, prompt, paths, modelId, ctx);
79
62
  }
80
63
 
@@ -111,31 +94,8 @@ async function childRun(
111
94
  ? prompt
112
95
  : `${prompt}\n\n[rlm] paths=${child.unmatched.join(", ")} matched no files; you received the full context.`;
113
96
 
114
- // ── v5 memory replay: an identical, still-fresh child answer replays for zero API calls ──
115
- const memory = deps.memory;
116
97
  const sig = contextSig(child.context);
117
98
  const key = claimKeyFor(deps, "rlm", prompt, paths ?? [], sig);
118
- if (memory !== undefined && deps.getConfig().enableMemory !== false) {
119
- const hit = memory.replay(key);
120
- if (hit !== undefined) {
121
- const replayId = inv.emitter.emitSubcallCreated({
122
- kind: "rlm",
123
- parentId: inv.parentId,
124
- label: "rlm_query (replay)",
125
- detail: prompt.slice(0, 60),
126
- depth: childDepth,
127
- });
128
- inv.emitter.emitSubcallUpdated({ id: replayId, status: "done", resultPreview: hit.result.slice(0, 200) });
129
- return {
130
- answer: hit.result,
131
- iterations: 0,
132
- costUsd: 0,
133
- inputTokens: 0,
134
- outputTokens: 0,
135
- durationMs: 0,
136
- };
137
- }
138
- }
139
99
 
140
100
  // ── v5 TaskLedger: echo → stub; duplicate → coalesce onto the existing runner ──────
141
101
  const ledger = activeLedger(deps);
@@ -148,8 +108,7 @@ async function childRun(
148
108
  // ONE subcall node per childRun (audit C2 / DRY #5): the decision branch reuses it, the
149
109
  // run branch reports the engine's turns/cost on it. Never a second emit below.
150
110
  const rootModel = deps.getModel?.();
151
- const modelLabel =
152
- rootModel === undefined ? undefined : (modelRef(rootModel) ?? rootModel.id);
111
+ const modelLabel = rootModel === undefined ? undefined : modelLabelOf(rootModel);
153
112
  const subId = inv.emitter.emitSubcallCreated({
154
113
  kind: "rlm",
155
114
  parentId: inv.parentId,
@@ -168,13 +127,14 @@ async function childRun(
168
127
  const twin = await ledger
169
128
  .waitFor(decision.key)
170
129
  .catch((err: unknown) => errorMessage(err));
171
- inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(String(twin).slice(0, 80)) });
172
- return emptyResult(String(twin));
130
+ inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(twin.slice(0, 80)) });
131
+ return emptyResult(twin);
173
132
  }
174
133
  if (ledger !== undefined && claimKey !== undefined) {
175
134
  ledger.markRunning(claimKey);
176
135
  }
177
136
 
137
+ const skillBlock = deps.getSkillBlock?.(prompt);
178
138
  const input: RlmInput = {
179
139
  rootPrompt,
180
140
  context: child.context,
@@ -182,6 +142,9 @@ async function childRun(
182
142
  parentNodeId: subId,
183
143
  remainingTimeoutMs: remTimeout,
184
144
  ledger, // DRY #6: the one seam — children share the parent's blackboard
145
+ // SKILL.state Ξ (Workstream C, DRY #6): the parent's block rides along — the only
146
+ // child-RlmInput construction site, so inheritance cannot grow a second path.
147
+ ...(skillBlock === undefined ? {} : { skillBlock }),
185
148
  };
186
149
 
187
150
  try {
@@ -189,19 +152,6 @@ async function childRun(
189
152
  inv.limits.addRaw(res.costUsd, res.inputTokens, res.outputTokens);
190
153
  deps.onChildUsage?.(res.costUsd, res.inputTokens, res.outputTokens);
191
154
  if (ledger !== undefined && claimKey !== undefined) ledger.finish(claimKey, res.answer);
192
- // v5: child answers persist unconditionally — this is what later identical runs replay.
193
- if (memory !== undefined && deps.getConfig().enableMemory !== false) {
194
- memory.recordEpisode({
195
- key,
196
- kind: "rlm",
197
- model: modelLabel ?? "",
198
- prompt,
199
- paths: paths ?? [],
200
- result: res.answer,
201
- tokensIn: res.inputTokens,
202
- tokensOut: res.outputTokens,
203
- });
204
- }
205
155
  inv.emitter.emitSubcallUpdated({
206
156
  id: subId,
207
157
  status: "done",
@@ -225,17 +175,7 @@ export function createRlmQueryHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
225
175
  opts: SubcallOpts,
226
176
  ): Promise<SpawnResult> => {
227
177
  const inv = deps.resolve(opts, depth);
228
- if (inv === null) {
229
- return {
230
- ok: false,
231
- task_id: null,
232
- kind: "rlm",
233
- n: 1,
234
- status: "pending",
235
- hint: SPAWN_HINT,
236
- error: UNWIRED,
237
- };
238
- }
178
+ if (inv === null) return unwiredSpawn("rlm", 1);
239
179
 
240
180
  const pathArg = opts.paths;
241
181
 
@@ -297,17 +237,7 @@ export function createRlmBatchHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
297
237
  opts: SubcallOpts,
298
238
  ): Promise<SpawnResult> => {
299
239
  const inv = deps.resolve(opts, depth);
300
- if (inv === null) {
301
- return {
302
- ok: false,
303
- task_id: null,
304
- kind: "rlm_batch",
305
- n: tasks.length,
306
- status: "pending",
307
- hint: SPAWN_HINT,
308
- error: UNWIRED,
309
- };
310
- }
240
+ if (inv === null) return unwiredSpawn("rlm_batch", tasks.length);
311
241
 
312
242
  const pathArg = opts.paths;
313
243
  // No wrapper "rlm_batch ×N" node: every task already gets its own rlm_query node from
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { AwaitResult, SpawnResult, TaskEntry } from "./types.ts";
10
+ import { formatError, isErrorText } from "../../util/errors.ts";
10
11
 
11
12
  export const SPAWN_HINT =
12
13
  "Call await_task(task_id=...) to get the result — this is NOT the answer.";
@@ -24,6 +25,22 @@ export interface AwaitDeps {
24
25
  unawaitedIds(): readonly string[];
25
26
  }
26
27
 
28
+ /** DRY: the ONE TaskEntry → AwaitResult mapping — shared by the await handler and the registry. */
29
+ export function entryToAwaitResult(entry: TaskEntry): AwaitResult {
30
+ const status = entry.status === "pending" ? "error" : entry.status;
31
+ return {
32
+ ok: entry.status === "done",
33
+ task_id: entry.taskId,
34
+ kind: entry.kind,
35
+ status,
36
+ result: entry.result,
37
+ results: entry.results,
38
+ error:
39
+ entry.error ??
40
+ (entry.status === "pending" ? "Task still pending" : undefined),
41
+ };
42
+ }
43
+
27
44
  export interface TaskRegistry {
28
45
  readonly spawnDeps: SpawnDeps;
29
46
  readonly awaitDeps: AwaitDeps;
@@ -32,8 +49,8 @@ export interface TaskRegistry {
32
49
  }
33
50
 
34
51
  interface Waiter {
35
- resolve: (entry: TaskEntry) => void;
36
- reject: (err: Error) => void;
52
+ readonly resolve: (entry: TaskEntry) => void;
53
+ readonly reject: (err: Error) => void;
37
54
  timer?: ReturnType<typeof setTimeout>;
38
55
  }
39
56
 
@@ -146,19 +163,7 @@ export function createTaskRegistry(): TaskRegistry {
146
163
  error: `Task ${taskId} not found`,
147
164
  };
148
165
  }
149
- const status =
150
- entry.status === "pending" ? "error" : entry.status;
151
- return {
152
- ok: entry.status === "done",
153
- task_id: entry.taskId,
154
- kind: entry.kind,
155
- status,
156
- result: entry.result,
157
- results: entry.results,
158
- error:
159
- entry.error ??
160
- (entry.status === "pending" ? "Task still pending" : undefined),
161
- };
166
+ return entryToAwaitResult(entry);
162
167
  },
163
168
  };
164
169
  }
@@ -185,14 +190,14 @@ export function spawnAndRun(
185
190
  sd.resolve(taskId, result);
186
191
  } catch (err: unknown) {
187
192
  const message = err instanceof Error ? err.message : String(err);
188
- sd.reject(taskId, message.startsWith("Error:") ? message : `Error: ${message}`);
193
+ sd.reject(taskId, isErrorText(message) ? message : formatError(message));
189
194
  }
190
195
  };
191
196
 
192
197
  if (trackDetached !== undefined && detached) {
193
198
  void trackDetached(run).catch((err: unknown) => {
194
199
  const message = err instanceof Error ? err.message : String(err);
195
- sd.reject(taskId, message.startsWith("Error:") ? message : `Error: ${message}`);
200
+ sd.reject(taskId, isErrorText(message) ? message : formatError(message));
196
201
  });
197
202
  } else {
198
203
  void run();
@@ -13,7 +13,6 @@ import type { SubcallGates } from "../../util/concurrency.ts";
13
13
  import type { SubcallOpts } from "../../sandbox/interrupts.ts";
14
14
  import type { RlmEmitter } from "../../tool/rlm-events.ts";
15
15
  import type { TaskLedger } from "../../core/ledger.ts";
16
- import type { MemoryStore } from "../../core/memory.ts";
17
16
 
18
17
  // ---------------------------------------------------------------------------
19
18
  // Spawn / Await / Finish — the three shapes the model sees
@@ -73,7 +72,7 @@ export function limitsFromRemaining(
73
72
 
74
73
  export interface Invocation {
75
74
  readonly emitter: RlmEmitter;
76
- readonly parentId: string | undefined;
75
+ readonly parentId?: string;
77
76
  readonly depth: number;
78
77
  readonly limits: InvocationLimits;
79
78
  }
@@ -91,8 +90,6 @@ export interface SubcallConfig {
91
90
  readonly enableLedger?: boolean;
92
91
  /** v5: real rlm spawns before demotion to llm (0 = never demote). */
93
92
  readonly rlmBudget?: number;
94
- /** v5 durable memory gates (optional; omitted → memory off). */
95
- readonly enableMemory?: boolean;
96
93
  /** v5.1 retry knobs — structural slice of RlmConfig so retryPolicy() can read them. */
97
94
  readonly retryMaxAttempts?: number;
98
95
  readonly rateLimitMaxAttempts?: number;
@@ -121,8 +118,13 @@ export interface SubcallHandlerDeps {
121
118
  readonly trackDetached?: <T>(run: () => Promise<T>) => Promise<T>;
122
119
  /** v5 TaskLedger blackboard shared across the whole run tree (claim/coalesce/echo/demote). */
123
120
  readonly ledger?: TaskLedger;
124
- /** v5 durable memory (session-wide store) for child replay + episode persistence. */
125
- readonly memory?: MemoryStore;
121
+ /** SKILL.state (Workstream D): ground a leaf prompt with verified facts. Applied ONCE in
122
+ * complete1 (DRY #1) so every leaf path inherits it; returns the prompt unchanged when
123
+ * nothing clears the score threshold — below it, byte-identical. */
124
+ readonly groundLeaf?: (prompt: string) => string;
125
+ /** SKILL.state (Workstream C): the parent run's Ξ block source, for childRun to copy
126
+ * into the child RlmInput (DRY #6 — one construction site). */
127
+ readonly getSkillBlock?: (task: string) => string | undefined;
126
128
  }
127
129
 
128
130
  // ---------------------------------------------------------------------------