@hicaru/pi-rlm 0.3.19 → 0.3.21
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 +8 -5
- package/package.json +1 -1
- package/src/bridge/handlers/completion.ts +3 -0
- package/src/bridge/handlers/emitting.ts +0 -4
- package/src/bridge/handlers/rlm-query.ts +3 -3
- package/src/bridge/handlers/task-registry.ts +46 -19
- package/src/bridge/handlers/types.ts +6 -3
- package/src/bridge/model.ts +4 -0
- package/src/commands/rlm.ts +14 -7
- package/src/config/defaults.ts +41 -13
- package/src/config/settings.ts +7 -3
- package/src/config/skillstate.ts +236 -44
- package/src/context/merge.ts +10 -3
- package/src/context/namespace.ts +6 -2
- package/src/context/refresh.ts +32 -11
- package/src/core/answer.ts +15 -0
- package/src/core/budget.ts +39 -17
- package/src/core/compaction.ts +85 -9
- package/src/core/engine.ts +117 -32
- package/src/core/iteration.ts +4 -0
- package/src/core/limits.ts +10 -14
- package/src/core/root-context.ts +83 -19
- package/src/core/root-digest.ts +48 -11
- package/src/core/root-state.ts +39 -12
- package/src/core/run-state.ts +86 -14
- package/src/core/session-archive.ts +174 -0
- package/src/core/types.ts +13 -2
- package/src/index.ts +142 -12
- package/src/mode/rlm-mode.ts +2 -2
- package/src/prompts/glossary.ts +36 -5
- package/src/prompts/native.ts +8 -2
- package/src/prompts/user.ts +6 -4
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/retrieval.py +202 -36
- package/src/sandbox/py/scaffold.py +20 -5
- package/src/sandbox/py/worker.py +1 -1
- package/src/sandbox/sandbox-manager.ts +19 -0
- package/src/sandbox/sandbox.ts +13 -1
- package/src/text/parsing.ts +133 -2
- package/src/text/tokens.ts +39 -4
- package/src/tool/repl-details.ts +2 -2
- package/src/tool/repl-render.ts +38 -2
- package/src/tool/repl-tool.ts +37 -23
- package/src/tool/rlm-aggregator.ts +1 -1
- package/src/tool/rlm-details.ts +1 -2
- package/src/tool/rlm-events.ts +3 -6
- package/src/tool/rlm-tool.ts +1 -1
- package/src/tool/subcall-render.ts +7 -4
- package/src/tool/subcall-store.ts +5 -18
- package/src/ui/config-panel.ts +4 -19
- package/src/ui/intro.ts +1 -1
- package/src/ui/panel/run-registry.ts +2 -2
- package/src/ui/python-highlight.ts +49 -0
- package/src/ui/stage-cards.ts +192 -0
- package/src/ui/tree/tree-model.ts +69 -19
- package/src/ui/tree/tree-rows.ts +2 -1
- package/src/util/abort.ts +34 -0
- package/src/util/bm25.ts +170 -21
- package/src/util/errors.ts +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session archive (recall W1) — the model-reachable copy of elided root history.
|
|
3
|
+
*
|
|
4
|
+
* The root context transform (core/root-context.ts) stubs every turn older than the keep
|
|
5
|
+
* window; before this module those bytes were unrecoverable — the session log is host-side
|
|
6
|
+
* only and the repl sandbox never saw native read/bash payloads. The archive closes the
|
|
7
|
+
* loop: every message the elision destroys is recorded here, rendered into markdown
|
|
8
|
+
* segments, and materialized into the sandbox under `ctx/session-log/` through the SAME
|
|
9
|
+
* upsert seam native edit/write uses (context/refresh.ts) — so the existing free
|
|
10
|
+
* search() / grep_context() BM25 recall them (RLM paper §2: context stays an environment;
|
|
11
|
+
* LLM-memory survey §5: query, not storage, is the recall bottleneck).
|
|
12
|
+
*
|
|
13
|
+
* The elision re-runs on EVERY provider call over a fresh clone, so records dedup by
|
|
14
|
+
* content hash (the transcript is append-only; the same stale turn re-elides each call).
|
|
15
|
+
* Segment materialization is idempotent per segment path and fail-soft end to end.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { truncateOutput } from "../text/parsing.ts";
|
|
19
|
+
|
|
20
|
+
/** Sandbox namespace the segments materialize under — one wording source for the stubs
|
|
21
|
+
* that teach recall (prompts/glossary.ts) and for the materialized paths. */
|
|
22
|
+
export const ARCHIVE_NAMESPACE = "ctx/session-log/";
|
|
23
|
+
|
|
24
|
+
/** Per-record cap: a 2MB tool payload is archived mid-truncated (head+tail) — recall needs
|
|
25
|
+
* the shape and the needles, not every byte (paper A.4 compact-serialization doctrine). */
|
|
26
|
+
const ARCHIVE_RECORD_MAX_CHARS = 100_000;
|
|
27
|
+
/** Host-memory ring cap (chars). Already-materialized segments live in the worker; this
|
|
28
|
+
* caps only the host copy. Configurable as `rootArchiveMaxChars` (0 = archive off). */
|
|
29
|
+
export const ARCHIVE_DEFAULT_MAX_CHARS = 2_000_000;
|
|
30
|
+
/** Dedup-set cap: FIFO eviction of the oldest hashes; a re-elided evicted message is
|
|
31
|
+
* re-recorded harmlessly (a duplicate segment entry, not a correctness issue). */
|
|
32
|
+
const HASH_SET_MAX = 8_192;
|
|
33
|
+
|
|
34
|
+
const ARCHIVE_TRUNCATE_MARK = "chars archived out — the middle never enters the archive";
|
|
35
|
+
|
|
36
|
+
export interface ArchivedEntry {
|
|
37
|
+
readonly role: "assistant" | "toolResult";
|
|
38
|
+
readonly toolName: string | undefined;
|
|
39
|
+
readonly text: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** FNV-1a 32-bit over role+name+text — the dedup key (same double-hash idiom as skillstate). */
|
|
43
|
+
function entryHash(entry: ArchivedEntry): string {
|
|
44
|
+
const basis = `${entry.role}\u0000${entry.toolName ?? ""}\u0000${entry.text}`;
|
|
45
|
+
let h1 = 0x811c9dc5;
|
|
46
|
+
let h2 = 0x811c9dc5;
|
|
47
|
+
for (let i = 0; i < basis.length; i++) {
|
|
48
|
+
const c = basis.charCodeAt(i);
|
|
49
|
+
h1 = (h1 ^ c) * 0x01000193;
|
|
50
|
+
h2 = (h2 ^ (c + i)) * 0x01000193;
|
|
51
|
+
h1 >>>= 0;
|
|
52
|
+
h2 >>>= 0;
|
|
53
|
+
}
|
|
54
|
+
return `${h1.toString(36)}${h2.toString(36)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface RingEntry {
|
|
58
|
+
readonly seq: number;
|
|
59
|
+
readonly hash: string;
|
|
60
|
+
readonly entry: ArchivedEntry;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ArchiveSegment {
|
|
64
|
+
/** Unique context path for this segment (`ctx/session-log/turn-<a>-<b>.md`). */
|
|
65
|
+
readonly path: string;
|
|
66
|
+
readonly text: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class SessionArchive {
|
|
70
|
+
private ring: RingEntry[] = [];
|
|
71
|
+
private readonly seen = new Map<string, true>();
|
|
72
|
+
private totalChars = 0;
|
|
73
|
+
private nextSeq = 1;
|
|
74
|
+
private materializedThrough = 0;
|
|
75
|
+
private lostWhilePending = 0;
|
|
76
|
+
private pendingChars = 0;
|
|
77
|
+
private pendingCount = 0;
|
|
78
|
+
|
|
79
|
+
constructor(private readonly maxChars: number) {}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Record one elided message. Returns the assigned seq, or undefined for
|
|
83
|
+
* empty/duplicate records. Per-record truncation applies BEFORE the ring cap.
|
|
84
|
+
*/
|
|
85
|
+
record(entry: ArchivedEntry): number | undefined {
|
|
86
|
+
const text = entry.text.trim();
|
|
87
|
+
if (text === "") return undefined;
|
|
88
|
+
const capped: ArchivedEntry = {
|
|
89
|
+
...entry,
|
|
90
|
+
text: text.length > ARCHIVE_RECORD_MAX_CHARS
|
|
91
|
+
? truncateOutput(text, ARCHIVE_RECORD_MAX_CHARS, ARCHIVE_TRUNCATE_MARK)
|
|
92
|
+
: text,
|
|
93
|
+
};
|
|
94
|
+
const hash = entryHash(capped);
|
|
95
|
+
if (this.seen.has(hash)) return undefined;
|
|
96
|
+
this.seen.set(hash, true);
|
|
97
|
+
if (this.seen.size > HASH_SET_MAX) {
|
|
98
|
+
// FIFO: Map preserves insertion order; drop the oldest hash only.
|
|
99
|
+
const oldest = this.seen.keys().next();
|
|
100
|
+
if (oldest.done !== true) this.seen.delete(oldest.value);
|
|
101
|
+
}
|
|
102
|
+
const seq = this.nextSeq++;
|
|
103
|
+
this.ring.push({ seq, hash, entry: capped });
|
|
104
|
+
this.totalChars += capped.text.length;
|
|
105
|
+
if (seq > this.materializedThrough) {
|
|
106
|
+
this.pendingChars += capped.text.length;
|
|
107
|
+
this.pendingCount += 1;
|
|
108
|
+
}
|
|
109
|
+
this.evictOverCap();
|
|
110
|
+
return seq;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** True when unmaterialized records exist (flush when the sandbox is next alive). */
|
|
114
|
+
get hasPending(): boolean {
|
|
115
|
+
return this.pendingCount > 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get pendingCharsValue(): number {
|
|
119
|
+
return this.pendingChars;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Telemetry: entries + chars currently held (host copy). */
|
|
123
|
+
get stats(): { readonly entries: number; readonly chars: number; readonly recorded: number } {
|
|
124
|
+
return { entries: this.ring.length, chars: this.totalChars, recorded: this.nextSeq - 1 };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Render every unmaterialized record into ONE segment and mark it materialized.
|
|
129
|
+
* Returns undefined when nothing is pending. The path embeds the covered seq range so
|
|
130
|
+
* segments never collide and the `ctx/session-log/*` glob covers them all.
|
|
131
|
+
*/
|
|
132
|
+
renderPending(): ArchiveSegment | undefined {
|
|
133
|
+
const pending = this.ring.filter((r) => r.seq > this.materializedThrough);
|
|
134
|
+
if (pending.length === 0) return undefined;
|
|
135
|
+
const parts: string[] = [];
|
|
136
|
+
if (this.lostWhilePending > 0) {
|
|
137
|
+
parts.push(
|
|
138
|
+
`(archive cap dropped ${this.lostWhilePending} older elided turn(s) before materialization)`,
|
|
139
|
+
);
|
|
140
|
+
this.lostWhilePending = 0;
|
|
141
|
+
}
|
|
142
|
+
for (const r of pending) {
|
|
143
|
+
const label = r.entry.role === "toolResult" && r.entry.toolName !== undefined
|
|
144
|
+
? `${r.entry.role} (${r.entry.toolName})`
|
|
145
|
+
: r.entry.role;
|
|
146
|
+
parts.push(`### turn ${r.seq} — ${label}\n${r.entry.text}`);
|
|
147
|
+
}
|
|
148
|
+
const first = pending[0]?.seq ?? 0;
|
|
149
|
+
const last = pending[pending.length - 1]?.seq ?? first;
|
|
150
|
+
this.materializedThrough = last;
|
|
151
|
+
this.pendingChars = 0;
|
|
152
|
+
this.pendingCount = 0;
|
|
153
|
+
return {
|
|
154
|
+
path: `${ARCHIVE_NAMESPACE}turn-${first}-${last}.md`,
|
|
155
|
+
text: parts.join("\n\n"),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Ring eviction: drop OLDEST entries until under cap. Evicting an unmaterialized
|
|
160
|
+
* record loses it from recall — counted so the next segment says so honestly. */
|
|
161
|
+
private evictOverCap(): void {
|
|
162
|
+
while (this.totalChars > this.maxChars && this.ring.length > 1) {
|
|
163
|
+
const oldest = this.ring[0];
|
|
164
|
+
if (oldest === undefined) return;
|
|
165
|
+
this.ring = this.ring.slice(1);
|
|
166
|
+
this.totalChars -= oldest.entry.text.length;
|
|
167
|
+
if (oldest.seq > this.materializedThrough) {
|
|
168
|
+
this.pendingChars -= oldest.entry.text.length;
|
|
169
|
+
this.pendingCount -= 1;
|
|
170
|
+
this.lostWhilePending += 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -13,7 +13,8 @@ export interface RlmConfig {
|
|
|
13
13
|
readonly enabled: boolean;
|
|
14
14
|
/** Max recursion depth. depth >= maxDepth ⇒ rlm_query falls back to a plain llm_query. */
|
|
15
15
|
readonly maxDepth: number;
|
|
16
|
-
/** Max turns before the engine must finalize.
|
|
16
|
+
/** Max turns before the engine must finalize. Deliberately large — runs end on FINAL
|
|
17
|
+
* answer, errors, or wall-clock long before this bites. */
|
|
17
18
|
readonly maxIterations: number;
|
|
18
19
|
/** Per-`repl`-block wall-clock timeout inside the worker (seconds).
|
|
19
20
|
* v5 doctrine: content limits are the token budget's job — this is a HANG backstop only. */
|
|
@@ -120,6 +121,9 @@ export interface RlmConfig {
|
|
|
120
121
|
readonly skillStateLeafTokens: number;
|
|
121
122
|
/** BM25 score a note must clear before a leaf prompt gets grounded (below ⇒ byte-identical). */
|
|
122
123
|
readonly skillStateMinScore: number;
|
|
124
|
+
/** BM25 score a note must clear before it enters the Ξ root block (below ⇒ not injected).
|
|
125
|
+
* Was effectively 0 (`Number.MIN_VALUE`) — stale cross-session notes rode every prompt. */
|
|
126
|
+
readonly skillStateXiMinScore: number;
|
|
123
127
|
/** Per-project note cap; LRU by ts with the top-hits quartile pinned (Workstream B). */
|
|
124
128
|
readonly skillStateNotesPerProject: number;
|
|
125
129
|
|
|
@@ -140,6 +144,9 @@ export interface RlmConfig {
|
|
|
140
144
|
readonly rootContextKeepTurns: number;
|
|
141
145
|
/** Tool-result payloads older than the keep window are preview-capped at this many chars. */
|
|
142
146
|
readonly rootContextElideChars: number;
|
|
147
|
+
/** Recall W1: host-memory ring cap (chars) for the session archive that makes elision
|
|
148
|
+
* dereferenceable (`ctx/session-log/*` in the sandbox). 0 = archive off (plain stubs). */
|
|
149
|
+
readonly rootArchiveMaxChars: number;
|
|
143
150
|
/** Splice the RootStateTracker Σ snapshot before the last user message each call. */
|
|
144
151
|
readonly rootContextSnapshot: boolean;
|
|
145
152
|
/** WS-4.2 (default OFF, paper §5.7 fence tax): the root may commit ΔΣ_t via a ```state
|
|
@@ -181,10 +188,14 @@ export interface RlmInput {
|
|
|
181
188
|
export interface RlmResult {
|
|
182
189
|
readonly answer: string;
|
|
183
190
|
readonly iterations: number;
|
|
184
|
-
readonly costUsd: number;
|
|
185
191
|
readonly inputTokens: number;
|
|
186
192
|
readonly outputTokens: number;
|
|
187
193
|
readonly durationMs: number;
|
|
194
|
+
/** Last non-empty repl stdout of the run (capped, P2 §3.4). The engine itself never reads
|
|
195
|
+
* it: a run that ends without `answer[...]` (no final frame) would otherwise score as an
|
|
196
|
+
* empty submission even though the winning value was printed. The bench/grader recovers
|
|
197
|
+
* that value from here instead of re-running the whole task. */
|
|
198
|
+
readonly lastStdout: string;
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
/** A function that runs an RLM to completion — used to wire recursion (rlm_query). */
|
package/src/index.ts
CHANGED
|
@@ -29,11 +29,18 @@ import type { AddContextHandlerBundle } from "./bridge/add-context.ts";
|
|
|
29
29
|
import { buildNativeSystemPrompt } from "./prompts/native.ts";
|
|
30
30
|
import { SkillStore, notesFromRunState, xiQuery } from "./config/skillstate.ts";
|
|
31
31
|
import { buildRootDigestCompaction } from "./core/root-digest.ts";
|
|
32
|
-
import { RootStateTracker } from "./core/root-state.ts";
|
|
32
|
+
import { ROOT_IDLE_DEGRADE_TURNS, RootStateTracker } from "./core/root-state.ts";
|
|
33
33
|
import { elideStalePayloads, spliceSigmaSnapshot } from "./core/root-context.ts";
|
|
34
|
+
import { SessionArchive } from "./core/session-archive.ts";
|
|
34
35
|
import { agentMessageText, firstLine, textContentOf } from "./text/agent-text.ts";
|
|
35
36
|
import { findStatePatches } from "./text/parsing.ts";
|
|
36
37
|
import { capToolResultText } from "./mode/native-guards.ts";
|
|
38
|
+
import {
|
|
39
|
+
STAGE_CUSTOM_TYPE,
|
|
40
|
+
renderStageCard,
|
|
41
|
+
stageCardMarkdown,
|
|
42
|
+
type StageCardDetails,
|
|
43
|
+
} from "./ui/stage-cards.ts";
|
|
37
44
|
import {
|
|
38
45
|
isSubagentChildBypass,
|
|
39
46
|
commitSubagentForceActivation,
|
|
@@ -108,17 +115,36 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
108
115
|
hideWhenEmpty: true,
|
|
109
116
|
});
|
|
110
117
|
let treePanelInstalled = false;
|
|
118
|
+
/**
|
|
119
|
+
* Native-mode abort: repl cells' child engines, detached spawn() tasks and add_context
|
|
120
|
+
* loads read this signal lazily (createReplTool getSignal), so /rlm-stop aborts AND
|
|
121
|
+
* rotates the controller mid-session — work started after a stop sees a fresh signal.
|
|
122
|
+
*/
|
|
123
|
+
let nativeAbort = new AbortController();
|
|
124
|
+
const stopNativeWork = (): boolean => {
|
|
125
|
+
const hadWork = runRegistry.hasActive() || background.pending > 0;
|
|
126
|
+
nativeAbort.abort();
|
|
127
|
+
nativeAbort = new AbortController();
|
|
128
|
+
return hadWork;
|
|
129
|
+
};
|
|
111
130
|
/** SKILL.state (Workstream B): session store — hydrated at session_start, flushed at shutdown. */
|
|
112
131
|
let skillStore: SkillStore | undefined;
|
|
113
132
|
/** Root Σ (WS-3/4): the native session's digest-level Σ_t — runtime-derived (tool outcomes,
|
|
114
133
|
* engine mirrors, prompts); lazily born on the first prompt, harvested + dropped at shutdown. */
|
|
115
134
|
let rootTracker: RootStateTracker | undefined;
|
|
135
|
+
/** Recall W1: elided turns archive here and materialize into the sandbox
|
|
136
|
+
* (ctx/session-log/*) so search()/grep_context() recall them. Per-session, closure-only. */
|
|
137
|
+
const sessionArchive = new SessionArchive(config.rootArchiveMaxChars);
|
|
138
|
+
/** Archive gate: enabled (chars > 0) AND the context transform actually running. */
|
|
139
|
+
const archiveActive = (): boolean =>
|
|
140
|
+
controller.config.rootArchiveMaxChars > 0 && rootContextActive();
|
|
116
141
|
// Root Σ WS-5.1 telemetry — journal counters (trace lines + status widget when tracing).
|
|
117
142
|
let xiCompositions = 0;
|
|
118
143
|
let rootDigests = 0;
|
|
119
144
|
let elidedMessages = 0;
|
|
120
145
|
let sigmaSplices = 0;
|
|
121
146
|
let idleDegrades = 0;
|
|
147
|
+
let archivedTurns = 0;
|
|
122
148
|
/** R6: Σ counter snapshot for the status line — a fresh readonly object per render. */
|
|
123
149
|
const sigmaTelemetry = (): RootSigmaTelemetry => ({
|
|
124
150
|
xiCompositions,
|
|
@@ -127,6 +153,21 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
127
153
|
sigmaSplices,
|
|
128
154
|
idleDegrades,
|
|
129
155
|
});
|
|
156
|
+
/** [rlm.stage]: post one stage-transition card into the transcript (persisted, ctrl+o-expandable). */
|
|
157
|
+
const postStageCard = (details: StageCardDetails): void => {
|
|
158
|
+
try {
|
|
159
|
+
pi.sendMessage({ customType: STAGE_CUSTOM_TYPE, content: stageCardMarkdown(details), display: true, details });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// Stage cards are decoration — never fail the hook that produced the transition.
|
|
162
|
+
if (traceEnabled) trace("stage-card.fail", { error: errorMessage(err) });
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Compaction-hook re-entrancy guard: session_before_compact builds the digest card but does
|
|
167
|
+
* NOT post it from inside the hook (a message appended mid-compaction would fold into the
|
|
168
|
+
* very span being digested) — the pending card flushes at the next turn_start instead.
|
|
169
|
+
*/
|
|
170
|
+
let pendingDigestCard: StageCardDetails | undefined;
|
|
130
171
|
// A detached child works in its OWN sandbox, so this one sees no frames and its request
|
|
131
172
|
// watchdog would fire mid-await and SIGKILL a healthy worker, taking the REPL namespace
|
|
132
173
|
// with it. Keep it alive while detached work is genuinely in flight.
|
|
@@ -173,7 +214,9 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
173
214
|
const cfg = controller.config;
|
|
174
215
|
// R0: enableSkillState is enforced (validateEnforcedOn) — no config check remains.
|
|
175
216
|
if (skillStore === undefined) return undefined;
|
|
176
|
-
|
|
217
|
+
// Recall W3: the Ξ block honors the configured score floor (was MIN_VALUE — any
|
|
218
|
+
// positively-scored stale note rode every prompt).
|
|
219
|
+
const block = skillStore.blockFor(query, cfg.skillStateMaxTokens, cfg.skillStateXiMinScore);
|
|
177
220
|
return block === "" ? undefined : block;
|
|
178
221
|
};
|
|
179
222
|
|
|
@@ -189,6 +232,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
189
232
|
pi.registerMessageRenderer("rlm-intro", (message, _options, theme) =>
|
|
190
233
|
new Markdown(textContentOf(message.content), 1, 0, markdownTheme(theme)),
|
|
191
234
|
);
|
|
235
|
+
// [rlm.stage] cards — orchestrator stage transitions, collapsed until the user's ctrl+o.
|
|
236
|
+
pi.registerMessageRenderer(STAGE_CUSTOM_TYPE, renderStageCard);
|
|
192
237
|
|
|
193
238
|
// ── CLI flag: `pi --rlm` / `pi --rlm=false` overrides the persisted mode for this run ──
|
|
194
239
|
pi.registerFlag("rlm", {
|
|
@@ -197,7 +242,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
197
242
|
});
|
|
198
243
|
|
|
199
244
|
// ── Commands ──
|
|
200
|
-
registerRlmCommand(pi, controller);
|
|
245
|
+
registerRlmCommand(pi, controller, stopNativeWork);
|
|
201
246
|
registerRlmConfigCommand(pi, controller);
|
|
202
247
|
registerRlmLlmCommand(pi, controller);
|
|
203
248
|
registerRlmRlmCommand(pi, controller);
|
|
@@ -322,6 +367,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
322
367
|
background,
|
|
323
368
|
runRegistry,
|
|
324
369
|
skillStore,
|
|
370
|
+
getSignal: () => nativeAbort.signal,
|
|
325
371
|
onRunState: (state) => { rootTracker?.absorbEngineState(state); },
|
|
326
372
|
getSkillBlock: composeSkillBlock,
|
|
327
373
|
registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
|
|
@@ -362,6 +408,14 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
362
408
|
setRlmModeStatus(ctx, controller, ctx.getContextUsage(), sigmaTelemetry());
|
|
363
409
|
});
|
|
364
410
|
|
|
411
|
+
// Deferred [rlm.stage] digest card — built inside session_before_compact, posted here.
|
|
412
|
+
pi.on("turn_start", async () => {
|
|
413
|
+
const card = pendingDigestCard;
|
|
414
|
+
if (card === undefined) return;
|
|
415
|
+
pendingDigestCard = undefined;
|
|
416
|
+
postStageCard(card);
|
|
417
|
+
});
|
|
418
|
+
|
|
365
419
|
/** True when the native-mode trade holds: enabled AND repl is in the active tool set. */
|
|
366
420
|
const nativeTradeHolds = (): boolean =>
|
|
367
421
|
shouldEnforceNativeReaderBlock({
|
|
@@ -423,8 +477,13 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
423
477
|
const tracker = rootTracker;
|
|
424
478
|
if (tracker === undefined || !controller.config.enableRootStateFences) return;
|
|
425
479
|
if (event.message.role !== "assistant") return;
|
|
480
|
+
// Recall W2: a turn that ran TOOLS did work — it is never fence-idle (file-editing
|
|
481
|
+
// sessions were degrading before their first fence landed). Productive turns neither
|
|
482
|
+
// grow nor reset the idle streak; prose-only turns keep the R4 ladder.
|
|
483
|
+
const productiveTurn = Array.isArray(event.message.content) &&
|
|
484
|
+
(event.message.content as Array<{ type?: string }>).some((b) => b?.type === "toolCall");
|
|
426
485
|
const wasActive = tracker.isActive;
|
|
427
|
-
const outcome = tracker.applyFences(findStatePatches(agentMessageText(event.message)));
|
|
486
|
+
const outcome = tracker.applyFences(findStatePatches(agentMessageText(event.message)), { productiveTurn });
|
|
428
487
|
// R3 soak observability: per-turn fence outcomes — the soak-B bars (≥50% of turns commit
|
|
429
488
|
// ≥1 accepted delta, rejection storms <10%) are computed from these journal lines.
|
|
430
489
|
if (traceEnabled) {
|
|
@@ -439,6 +498,12 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
439
498
|
}
|
|
440
499
|
if (wasActive && !tracker.isActive) {
|
|
441
500
|
idleDegrades += 1;
|
|
501
|
+
postStageCard({
|
|
502
|
+
kind: "degrade",
|
|
503
|
+
reason: tracker.degradeReason ?? "unknown",
|
|
504
|
+
idleTurns: tracker.idleTurns,
|
|
505
|
+
idleMax: ROOT_IDLE_DEGRADE_TURNS,
|
|
506
|
+
});
|
|
442
507
|
if (traceEnabled) {
|
|
443
508
|
const reason = tracker.degradeReason ?? "unknown";
|
|
444
509
|
trace(reason.startsWith("idle") ? "root-state.idle-degrade" : "root-state.degrade", {
|
|
@@ -446,6 +511,9 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
446
511
|
reason,
|
|
447
512
|
});
|
|
448
513
|
}
|
|
514
|
+
} else if (!wasActive && tracker.isActive) {
|
|
515
|
+
// R7-fix recovery observability, user-visible: a degraded tracker accepted a clean batch.
|
|
516
|
+
postStageCard({ kind: "recover", fencesAccepted: outcome.accepted, fencesTotal: outcome.fences });
|
|
449
517
|
}
|
|
450
518
|
});
|
|
451
519
|
|
|
@@ -462,6 +530,14 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
462
530
|
});
|
|
463
531
|
if (result !== undefined) {
|
|
464
532
|
rootDigests += 1;
|
|
533
|
+
pendingDigestCard = {
|
|
534
|
+
kind: "digest",
|
|
535
|
+
index: rootDigests,
|
|
536
|
+
turnsFolded: event.preparation.messagesToSummarize.length,
|
|
537
|
+
tokensBefore: result.compaction.tokensBefore,
|
|
538
|
+
tokensBeforeRecomputed: result.tokensBeforeRecomputed,
|
|
539
|
+
summary: result.compaction.summary,
|
|
540
|
+
};
|
|
465
541
|
if (traceEnabled) {
|
|
466
542
|
// V1 soak probe: host-consumed tokensBefore vs our recomputation over the same span.
|
|
467
543
|
trace("root-digest.built", {
|
|
@@ -486,7 +562,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
486
562
|
// ── Context injection: listing of whatever is currently loaded ──
|
|
487
563
|
// Re-inject only when the payload identity changes (seed / add_context), not every turn —
|
|
488
564
|
// the listing can be up to 200 file lines and the plugin exists to shrink the root window.
|
|
489
|
-
pi.on("context", async (event) => {
|
|
565
|
+
pi.on("context", async (event, ctx) => {
|
|
490
566
|
const filtered = event.messages.filter(
|
|
491
567
|
(message) =>
|
|
492
568
|
!(message.role === "custom" && message.customType === "rlm-intro")
|
|
@@ -499,10 +575,23 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
499
575
|
// then splice exactly one fresh Σ snapshot. Fail-soft: a throw here must never break a turn.
|
|
500
576
|
if (rootContextActive()) {
|
|
501
577
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
578
|
+
// Recall W1: every destroyed message's full text rides the sink into the session
|
|
579
|
+
// archive BEFORE the stub replaces it — elision stays dereferenceable.
|
|
580
|
+
const sink = archiveActive()
|
|
581
|
+
? (entry: { role: "assistant" | "toolResult"; toolName: string | undefined; text: string }) => {
|
|
582
|
+
const seq = sessionArchive.record(entry);
|
|
583
|
+
if (seq !== undefined) archivedTurns += 1;
|
|
584
|
+
}
|
|
585
|
+
: undefined;
|
|
586
|
+
const elided = elideStalePayloads(
|
|
587
|
+
filtered,
|
|
588
|
+
{
|
|
589
|
+
keepTurns: controller.config.rootContextKeepTurns,
|
|
590
|
+
elideChars: controller.config.rootContextElideChars,
|
|
591
|
+
archiveActive: sink !== undefined,
|
|
592
|
+
},
|
|
593
|
+
sink,
|
|
594
|
+
);
|
|
506
595
|
elidedMessages += elided;
|
|
507
596
|
const tracker = rootTracker;
|
|
508
597
|
// R4 REV (amnesia fix): degrade suspends fence WRITES (applyFences gate) — never Σ
|
|
@@ -523,8 +612,31 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
523
612
|
});
|
|
524
613
|
sigmaSplices += 1;
|
|
525
614
|
}
|
|
615
|
+
// Recall W1 flush: materialize new archive segments into the sandbox — ONLY when a
|
|
616
|
+
// worker already exists (never spawn Python from a context event) and fail-soft.
|
|
617
|
+
const pendingSegment = archiveActive() && sandboxManager.isAlive
|
|
618
|
+
? sessionArchive.renderPending()
|
|
619
|
+
: undefined;
|
|
620
|
+
if (pendingSegment !== undefined) {
|
|
621
|
+
const cwd = resolve(ctx?.cwd ?? process.cwd());
|
|
622
|
+
const written = await sandboxManager.upsertArchiveSegment(pendingSegment.path, pendingSegment.text, cwd);
|
|
623
|
+
if (traceEnabled) {
|
|
624
|
+
trace("root-archive.flush", {
|
|
625
|
+
path: pendingSegment.path,
|
|
626
|
+
chars: pendingSegment.text.length,
|
|
627
|
+
written,
|
|
628
|
+
stats: sessionArchive.stats,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
526
632
|
if (traceEnabled && (elided > 0 || sigmaSplices > 0)) {
|
|
527
|
-
trace("root-context.transform", {
|
|
633
|
+
trace("root-context.transform", {
|
|
634
|
+
elided,
|
|
635
|
+
total: elidedMessages,
|
|
636
|
+
splices: sigmaSplices,
|
|
637
|
+
archived: archivedTurns,
|
|
638
|
+
archiveChars: sessionArchive.stats.chars,
|
|
639
|
+
});
|
|
528
640
|
}
|
|
529
641
|
} catch (err) {
|
|
530
642
|
if (traceEnabled) trace("root-context.fail", { error: errorMessage(err) });
|
|
@@ -571,6 +683,13 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
571
683
|
event.isError,
|
|
572
684
|
event.isError ? firstLine(textContentOf(event.content)) : "",
|
|
573
685
|
);
|
|
686
|
+
// Recall W2 deterministic harvest (commit-at-first-sight, paper §7): successful reads
|
|
687
|
+
// are Σ facts the moment they happen — previously the only runtime feeds were tool
|
|
688
|
+
// ERRORS and edits, so the early turns (the ones elided first) never reached Σ.
|
|
689
|
+
if (!event.isError && event.toolName === "read") {
|
|
690
|
+
const path = extractEditPaths(event.input)[0];
|
|
691
|
+
if (path !== undefined) tracker.noteFact(`read ${path}`);
|
|
692
|
+
}
|
|
574
693
|
}
|
|
575
694
|
|
|
576
695
|
// ── Keep RLM context fresh after native file mutations ──
|
|
@@ -617,14 +736,25 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
617
736
|
const tracker = rootTracker;
|
|
618
737
|
if (skillStore !== undefined && tracker !== undefined && tracker.dirty) {
|
|
619
738
|
try {
|
|
620
|
-
|
|
621
|
-
|
|
739
|
+
const merged = notesFromRunState(tracker.snapshot());
|
|
740
|
+
skillStore.merge(merged);
|
|
741
|
+
if (merged.length > 0) {
|
|
742
|
+
// One-line stage card: what this session taught the store (tag histogram via stats()).
|
|
743
|
+
postStageCard({
|
|
744
|
+
kind: "distill",
|
|
745
|
+
merged,
|
|
746
|
+
total: skillStore.noteCount,
|
|
747
|
+
byTag: skillStore.stats().byTag,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
if (traceEnabled) trace("root-harvest.merged", { notes: merged.length });
|
|
622
751
|
} catch (err) {
|
|
623
752
|
if (traceEnabled) trace("root-harvest.fail", { error: errorMessage(err) });
|
|
624
753
|
}
|
|
625
754
|
}
|
|
626
755
|
await skillStore?.flush(); // SKILL.state (Workstream B): persist distilled notes
|
|
627
756
|
controller.abort();
|
|
757
|
+
nativeAbort.abort(); // detach engines/spawns still holding the session signal
|
|
628
758
|
clearInterval(watchdogHeartbeat);
|
|
629
759
|
background.dispose();
|
|
630
760
|
await sandboxManager.dispose();
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -120,10 +120,10 @@ export class RlmController {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/** Ξ (Workstream C): BM25 slice of the session SkillState for a root prompt; undefined when
|
|
123
|
-
* the store is absent/disabled or nothing is relevant. */
|
|
123
|
+
* the store is absent/disabled or nothing is relevant. Recall W3: honors the Ξ score floor. */
|
|
124
124
|
private skillBlockFor(query: string): string | undefined {
|
|
125
125
|
if (this.skillStore === undefined || !this.config.enableSkillState) return undefined;
|
|
126
|
-
const block = this.skillStore.blockFor(query, this.config.skillStateMaxTokens);
|
|
126
|
+
const block = this.skillStore.blockFor(query, this.config.skillStateMaxTokens, this.config.skillStateXiMinScore);
|
|
127
127
|
return block === "" ? undefined : block;
|
|
128
128
|
}
|
|
129
129
|
|
package/src/prompts/glossary.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* about functions that do not exist, or not told about ones that do.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { ARCHIVE_NAMESPACE } from "../core/session-archive.ts";
|
|
11
|
+
|
|
10
12
|
export type ContextKind = "files" | "text";
|
|
11
13
|
|
|
12
14
|
/** "str" (raw string context, e.g. rlm_query children) → text; everything else → files. */
|
|
@@ -28,9 +30,11 @@ export function promptCapTokensK(maxPromptChars: number): number {
|
|
|
28
30
|
* the outcome (§5, Fig. 4a). These cost no tokens and no sub-calls.
|
|
29
31
|
*/
|
|
30
32
|
const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
31
|
-
"- `search(query: str, k=10, path_glob=None)`: BM25 ranking over `context
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"- `search(query: str, k=10, path_glob=None)`: BM25 ranking over `context` (stemmed, with",
|
|
34
|
+
" query expansion — try plain words, not just exact identifiers). Returns",
|
|
35
|
+
" [{path, line, end, score, snippet, text}] — POINTERS, not bodies (`text` aliases",
|
|
36
|
+
" `snippet`; line..end is the match span; `index_truncated: true` means the context tail",
|
|
37
|
+
" is NOT indexed — narrow with path_glob). **Start here.** Free: no sub-LLM call.",
|
|
34
38
|
"- `grep_context(pattern, k=50, path_glob=None, before=0, after=0) -> dict`: regex over",
|
|
35
39
|
" `context`. Returns {hits: [{path, line, text, snippet}], counts, total, truncated} —",
|
|
36
40
|
" `counts` is complete even when `hits` is capped, so a wide pattern reports its shape",
|
|
@@ -56,6 +60,15 @@ const SKILL_SEARCH_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
56
60
|
" smells like something already learned — do not re-discover it.",
|
|
57
61
|
]);
|
|
58
62
|
|
|
63
|
+
/** W3 recall discoverability: the condensed NATIVE twins. glossary doctrine — divergence
|
|
64
|
+
* between the headless and native surfaces is a bug; `skill_search` had no native twin, so
|
|
65
|
+
* native models could not discover cross-session recall at all (and the archive line
|
|
66
|
+
* teaches the ctx/session-log recall that honest elision stubs point at). */
|
|
67
|
+
export const SKILL_SEARCH_LINE_NATIVE =
|
|
68
|
+
"- `skill_search(query, k=8) -> [{id, text, tags, score}]` — BM25 over distilled project facts from PRIOR sessions. Free; use before re-discovering a learned config/gotcha/symbol.";
|
|
69
|
+
export const ARCHIVE_RECALL_LINE_NATIVE =
|
|
70
|
+
`- Elided chat turns are archived in the sandbox: \`search("<keywords>", path_glob="${ARCHIVE_NAMESPACE}*")\` / \`grep_context()\` recall text that scrolled out of your context — free, no sub-LLM call.`;
|
|
71
|
+
|
|
59
72
|
/** Single source of wording for the injected SkillState block (headless + native, Workstream C).
|
|
60
73
|
* Takes the dynamic body as an argument — the glossary itself stays static-only. */
|
|
61
74
|
/** One wording source for the skill_search recall hint (Ξ block + root Σ snapshot). */
|
|
@@ -63,9 +76,22 @@ export const SKILL_RECALL_LINE =
|
|
|
63
76
|
"Recall more anytime inside repl: `skill_search(query, k=8)` → [{id, text, tags, score}].";
|
|
64
77
|
|
|
65
78
|
/** R5 (G4, /tmp/ROOT_FULL_SKILLSTATE_PLAN.md): the one-line replacement for assistant prose
|
|
66
|
-
* older than the keep window — durable facts live in
|
|
79
|
+
* older than the keep window — durable facts live in Σ; the repl sandbox (answers/vars) is
|
|
80
|
+
* the model-reachable recovery channel for repl-owned payloads. Stubs must promise only a
|
|
81
|
+
* channel that actually holds the bytes (recall W1): repl-owned payloads → the repl line,
|
|
82
|
+
* native payloads → the archive line (searchable `ctx/session-log/`), else the plain line. */
|
|
67
83
|
export const ROOT_TURN_ELIDED_LINE =
|
|
68
|
-
"… turn elided — durable facts live in Σ;
|
|
84
|
+
"… turn elided — durable facts live in Σ; your repl sandbox persists: print(answers) / SHOW_VARS() to re-derive";
|
|
85
|
+
export const ROOT_TURN_ELIDED_ARCHIVE_LINE =
|
|
86
|
+
`… turn elided — durable facts live in Σ; the full text is archived in the sandbox: ` +
|
|
87
|
+
`search('<keywords>', path_glob='${ARCHIVE_NAMESPACE}*') or grep_context() recalls it`;
|
|
88
|
+
export const ROOT_TURN_ELIDED_PLAIN_LINE =
|
|
89
|
+
"… turn elided — durable facts live in Σ";
|
|
90
|
+
/** Preview mark twin: payloads kept as head+tail previews point at the same archive. */
|
|
91
|
+
export const ROOT_ELIDE_PREVIEW_MARK =
|
|
92
|
+
`chars elided — full text archived under ${ARCHIVE_NAMESPACE} (search/grep_context it)`;
|
|
93
|
+
export const ROOT_ELIDE_PREVIEW_MARK_REPL =
|
|
94
|
+
"chars elided — repl sandbox persists: print(answers[k]) or re-run repl to re-derive";
|
|
69
95
|
|
|
70
96
|
export function skillStateLines(noteCount: number, body: string): string {
|
|
71
97
|
return [
|
|
@@ -341,6 +367,9 @@ export function replGlossary(
|
|
|
341
367
|
lines.push(
|
|
342
368
|
"- `llm_query(prompt: str) -> Task`: spawn one sub-LLM (await_task for str). The prompt must",
|
|
343
369
|
" **contain the text** to analyze — this call has no filesystem and no `context`.",
|
|
370
|
+
" Leaf convention: a sub-LLM answers exactly `NOT_FOUND` when its slice lacks the answer —",
|
|
371
|
+
" treat that as \"not in this slice\": slice differently, search elsewhere, or narrow the ask.",
|
|
372
|
+
" Never re-send an identical prompt hoping for a different verdict.",
|
|
344
373
|
"- `llm_batch(prompts: list[str]) -> Task`: many parallel one-shots (same rule: embed text).",
|
|
345
374
|
" await_task → ordered list[str]. NEVER pass bare file paths as if the worker can open them.",
|
|
346
375
|
...CHUNKED_GLOSSARY_LINES,
|
|
@@ -393,6 +422,8 @@ export function replGlossary(
|
|
|
393
422
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
394
423
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
|
395
424
|
' **You MUST flip `answer["ready"] = True` — runs that never finalize are discarded.**',
|
|
425
|
+
' Never write FINAL(...) / FINAL_VAR(...) prose and never emit a ```state fence in the',
|
|
426
|
+
' reply that finalizes — `answer` is the only finalize channel; Σ bookkeeping waits.',
|
|
396
427
|
);
|
|
397
428
|
return lines.join("\n");
|
|
398
429
|
}
|
package/src/prompts/native.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
ARCHIVE_RECALL_LINE_NATIVE,
|
|
9
10
|
CHUNKED_GLOSSARY_LINE_NATIVE,
|
|
10
11
|
ENV_TIPS_CONDENSED,
|
|
11
12
|
LARGE_FILE_RULE_NATIVE,
|
|
12
13
|
DEFAULT_PROMPT_CAP,
|
|
13
14
|
promptCapTokensK,
|
|
15
|
+
SKILL_SEARCH_LINE_NATIVE,
|
|
14
16
|
} from "./glossary.ts";
|
|
15
17
|
import { STATE_FENCE_INSTRUCTION } from "../core/run-state.ts";
|
|
16
18
|
|
|
@@ -25,6 +27,7 @@ function nativeReplGlossary(): string {
|
|
|
25
27
|
"- `search(query, k=10, path_glob=None) -> [{path, line, score, snippet, text}]` — BM25 pointers, not bodies",
|
|
26
28
|
"- `grep_context(pattern, k=50, …) -> {hits, counts, total, truncated}` — regex / lexical needles",
|
|
27
29
|
"- `outline(path) -> str` — definition skeleton (~200 chars)",
|
|
30
|
+
ARCHIVE_RECALL_LINE_NATIVE,
|
|
28
31
|
"",
|
|
29
32
|
"### Always-spawn fan-out (return Task + run ↯bg — NEVER the answer)",
|
|
30
33
|
"| Call | await_task → | When | NOT for |",
|
|
@@ -47,6 +50,7 @@ function nativeReplGlossary(): string {
|
|
|
47
50
|
"- `answers` / `plan` — persistent dicts for **collected** results. Task handles are REPL vars (`t`), not `answers` keys.",
|
|
48
51
|
"- `add_context(source) -> dict` — append external dir/file/doc/git under `ctx/<id>/…` (metadata only).",
|
|
49
52
|
"- `list_claims()` — the live `[ledger]` table of agent work.",
|
|
53
|
+
SKILL_SEARCH_LINE_NATIVE,
|
|
50
54
|
"- `SHOW_VARS()` — list REPL vars (Tasks as `<Task …>`). `list_tasks()` finds Task handles. `answer[\"ready\"]=True` only for headless finalize (native: write a normal message).",
|
|
51
55
|
"",
|
|
52
56
|
ENV_TIPS_CONDENSED,
|
|
@@ -189,8 +193,10 @@ export function buildNativeSystemPrompt(opts?: { readonly stateFences?: boolean
|
|
|
189
193
|
].join("\n");
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
/** Soft cap on the static native prompt. Raised for v5-style contract/routing/examples
|
|
193
|
-
|
|
196
|
+
/** Soft cap on the static native prompt. Raised for v5-style contract/routing/examples;
|
|
197
|
+
* recall W3 raised it again (+200) for the archive-recall + skill_search twin lines —
|
|
198
|
+
* the model cannot recall elided turns or prior-session facts it is never told about. */
|
|
199
|
+
export const NATIVE_PROMPT_BUDGET = 9_700;
|
|
194
200
|
|
|
195
201
|
/** Exported for tests — prompt length without context metadata (which is injected separately). */
|
|
196
202
|
export const NATIVE_PROMPT_STATIC = buildNativeSystemPrompt();
|