@hicaru/pi-rlm 0.3.17 → 0.3.18
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/package.json +1 -1
- package/src/config/defaults.ts +13 -9
- package/src/config/settings.ts +26 -14
- package/src/core/root-context.ts +70 -12
- package/src/core/root-state.ts +75 -5
- package/src/core/run-state.ts +16 -6
- package/src/index.ts +68 -18
- package/src/prompts/glossary.ts +5 -0
- package/src/prompts/native.ts +26 -2
- package/src/ui/config-panel.ts +12 -0
- package/src/ui/status.ts +39 -4
package/package.json
CHANGED
package/src/config/defaults.ts
CHANGED
|
@@ -56,25 +56,29 @@ export const DEFAULT_CONFIG: Readonly<RlmConfig> = Object.freeze({
|
|
|
56
56
|
// bare-number finalize gets one coached redo instead of being accepted. Opt-in via rlm.json.
|
|
57
57
|
enableVerificationNudge: false,
|
|
58
58
|
// SKILL.state integration: Σ_t execution state + cross-session distilled knowledge.
|
|
59
|
-
|
|
59
|
+
// Paradigm flags are ENFORCED (R0, /tmp/ROOT_FULL_SKILLSTATE_PLAN.md) — validateEnforcedOn
|
|
60
|
+
// forces true whatever rlm.json carries; only calibrations are tunable.
|
|
61
|
+
enableRunState: true, // ENFORCED — see /tmp/ROOT_FULL_SKILLSTATE_PLAN.md R0
|
|
60
62
|
runStateRetryMax: 2,
|
|
61
|
-
enableSkillState: true,
|
|
63
|
+
enableSkillState: true, // ENFORCED — see /tmp/ROOT_FULL_SKILLSTATE_PLAN.md R0
|
|
62
64
|
// Default ON (bench rec #3): deterministic harvest — one cheap distill leaf per finalize
|
|
63
65
|
// replaces the stochastic fence-emission harvest (0 vs 4 notes across identical ON arms).
|
|
64
|
-
enableSkillStateDistill: true,
|
|
66
|
+
enableSkillStateDistill: true, // ENFORCED — see /tmp/ROOT_FULL_SKILLSTATE_PLAN.md R0
|
|
65
67
|
skillStateMaxTokens: 1_200,
|
|
66
68
|
skillStateLeafTokens: 200,
|
|
67
69
|
skillStateMinScore: 4.0,
|
|
68
70
|
skillStateNotesPerProject: 128,
|
|
69
|
-
// Root Σ integration (WS-2..WS-4):
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
71
|
+
// Root Σ integration (WS-2..WS-4): every LLM call assembles A_t = (P, Σ_t, O_t) — discard
|
|
72
|
+
// semantics on stale payloads + exactly one Σ snapshot splice, and model-proposed ΔΣ_t
|
|
73
|
+
// fences taught in the native prompt (v2 R1/R2). Digest compaction swaps Pi's summarizer
|
|
74
|
+
// for a deterministic digest. Flags are ENFORCED (R0); RLM_BENCH_NO_ROOTCONTEXT=1 remains
|
|
75
|
+
// the dev-only A/B measurement hatch — it alters measurement, never ships as a disable path.
|
|
76
|
+
enableRootDigestCompaction: true, // ENFORCED — see /tmp/ROOT_FULL_SKILLSTATE_PLAN.md R0
|
|
73
77
|
rootDigestKeepRecentChars: 12_000,
|
|
74
78
|
rootDigestMaxChars: 8_000,
|
|
75
|
-
enableRootContextTransform:
|
|
79
|
+
enableRootContextTransform: true, // ENFORCED — see /tmp/ROOT_FULL_SKILLSTATE_PLAN.md R0 (was soak-OFF pre-v2)
|
|
76
80
|
rootContextKeepTurns: 2,
|
|
77
81
|
rootContextElideChars: 1_500,
|
|
78
82
|
rootContextSnapshot: true,
|
|
79
|
-
enableRootStateFences:
|
|
83
|
+
enableRootStateFences: true, // ENFORCED — see /tmp/ROOT_FULL_SKILLSTATE_PLAN.md R0 (was soak-OFF pre-v2)
|
|
80
84
|
});
|
package/src/config/settings.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { getAgentDir, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
7
7
|
import type { RlmConfig } from "../core/types.ts";
|
|
8
|
+
import { trace, traceEnabled } from "../util/trace.ts";
|
|
8
9
|
import { DEFAULT_CONFIG } from "./defaults.ts";
|
|
9
10
|
|
|
10
11
|
interface PersistedSettings {
|
|
@@ -31,6 +32,21 @@ function validateBoolean(v: unknown): boolean | undefined {
|
|
|
31
32
|
return typeof v === "boolean" ? v : undefined;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* R0 enforcement (/tmp/ROOT_FULL_SKILLSTATE_PLAN.md): the SKILL.state paradigm flags are
|
|
37
|
+
* operating law — they ALWAYS resolve to `true`, whatever rlm.json says. An explicit `false`
|
|
38
|
+
* is not an error (fail-soft by contract): it is traced (`skillstate.override-ignored`) and
|
|
39
|
+
* ignored, so a hostile or typo'd config loses visibly instead of crashing the load or
|
|
40
|
+
* silently unbounding the root context. Calibrations (window sizes, budgets) stay tunable —
|
|
41
|
+
* the PARADIGM is enforced, the calibrations are not.
|
|
42
|
+
*/
|
|
43
|
+
function validateEnforcedOn(value: unknown, flag: string): true {
|
|
44
|
+
if (value === false && traceEnabled) {
|
|
45
|
+
trace("skillstate.override-ignored", { flag, value });
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
function validateString(v: unknown): string | undefined {
|
|
35
51
|
return typeof v === "string" && v.trim() ? v : undefined;
|
|
36
52
|
}
|
|
@@ -138,15 +154,13 @@ export function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
138
154
|
// Verification-discipline nudge (default OFF).
|
|
139
155
|
const enableVerificationNudge = validateBoolean(r.enableVerificationNudge);
|
|
140
156
|
if (enableVerificationNudge !== undefined) out.enableVerificationNudge = enableVerificationNudge;
|
|
141
|
-
// SKILL.state integration (Workstreams A–F)
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
// SKILL.state integration (Workstreams A–F) — paradigm flags ENFORCED (R0); runStateRetryMax
|
|
158
|
+
// is a calibration and stays tunable.
|
|
159
|
+
out.enableRunState = validateEnforcedOn(r.enableRunState, "enableRunState");
|
|
144
160
|
const runStateRetryMax = validateNumber(r.runStateRetryMax, 0);
|
|
145
161
|
if (runStateRetryMax !== undefined) out.runStateRetryMax = runStateRetryMax;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const enableSkillStateDistill = validateBoolean(r.enableSkillStateDistill);
|
|
149
|
-
if (enableSkillStateDistill !== undefined) out.enableSkillStateDistill = enableSkillStateDistill;
|
|
162
|
+
out.enableSkillState = validateEnforcedOn(r.enableSkillState, "enableSkillState");
|
|
163
|
+
out.enableSkillStateDistill = validateEnforcedOn(r.enableSkillStateDistill, "enableSkillStateDistill");
|
|
150
164
|
const skillStateMaxTokens = validateNumber(r.skillStateMaxTokens, 50);
|
|
151
165
|
if (skillStateMaxTokens !== undefined) out.skillStateMaxTokens = skillStateMaxTokens;
|
|
152
166
|
const skillStateLeafTokens = validateNumber(r.skillStateLeafTokens, 0);
|
|
@@ -155,23 +169,21 @@ export function validateConfig(raw: unknown): Partial<RlmConfig> {
|
|
|
155
169
|
if (skillStateMinScore !== undefined) out.skillStateMinScore = skillStateMinScore;
|
|
156
170
|
const skillStateNotesPerProject = validateNumber(r.skillStateNotesPerProject, 1);
|
|
157
171
|
if (skillStateNotesPerProject !== undefined) out.skillStateNotesPerProject = skillStateNotesPerProject;
|
|
158
|
-
// Root Σ integration (WS-2..WS-4)
|
|
159
|
-
|
|
160
|
-
|
|
172
|
+
// Root Σ integration (WS-2..WS-4) — paradigm flags ENFORCED (R0); the window/byte knobs
|
|
173
|
+
// below are calibrations and stay tunable.
|
|
174
|
+
out.enableRootDigestCompaction = validateEnforcedOn(r.enableRootDigestCompaction, "enableRootDigestCompaction");
|
|
161
175
|
const rootDigestKeepRecentChars = validateNumber(r.rootDigestKeepRecentChars, 200);
|
|
162
176
|
if (rootDigestKeepRecentChars !== undefined) out.rootDigestKeepRecentChars = rootDigestKeepRecentChars;
|
|
163
177
|
const rootDigestMaxChars = validateNumber(r.rootDigestMaxChars, 200);
|
|
164
178
|
if (rootDigestMaxChars !== undefined) out.rootDigestMaxChars = rootDigestMaxChars;
|
|
165
|
-
|
|
166
|
-
if (enableRootContextTransform !== undefined) out.enableRootContextTransform = enableRootContextTransform;
|
|
179
|
+
out.enableRootContextTransform = validateEnforcedOn(r.enableRootContextTransform, "enableRootContextTransform");
|
|
167
180
|
const rootContextKeepTurns = validateNumber(r.rootContextKeepTurns, 0);
|
|
168
181
|
if (rootContextKeepTurns !== undefined) out.rootContextKeepTurns = rootContextKeepTurns;
|
|
169
182
|
const rootContextElideChars = validateNumber(r.rootContextElideChars, 100);
|
|
170
183
|
if (rootContextElideChars !== undefined) out.rootContextElideChars = rootContextElideChars;
|
|
171
184
|
const rootContextSnapshot = validateBoolean(r.rootContextSnapshot);
|
|
172
185
|
if (rootContextSnapshot !== undefined) out.rootContextSnapshot = rootContextSnapshot;
|
|
173
|
-
|
|
174
|
-
if (enableRootStateFences !== undefined) out.enableRootStateFences = enableRootStateFences;
|
|
186
|
+
out.enableRootStateFences = validateEnforcedOn(r.enableRootStateFences, "enableRootStateFences");
|
|
175
187
|
if (typeof r.subSampling === "object" && r.subSampling !== null) {
|
|
176
188
|
const ss = r.subSampling as Record<string, unknown>;
|
|
177
189
|
const sampling: { maxTokens?: number; temperature?: number; reasoning?: ThinkingLevel } = {};
|
package/src/core/root-context.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import type { ContextEvent } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import type { RunState } from "./run-state.ts";
|
|
21
21
|
import { runStateRootBlock } from "./run-state.ts";
|
|
22
|
+
import { ROOT_TURN_ELIDED_LINE } from "../prompts/glossary.ts";
|
|
22
23
|
import { truncateOutput } from "../text/parsing.ts";
|
|
23
24
|
import { textContentOf } from "../text/agent-text.ts";
|
|
24
25
|
|
|
@@ -33,15 +34,29 @@ export interface ElideOptions {
|
|
|
33
34
|
const ELIDE_MARK = "chars elided — full result in session log";
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* WS-3a: elide stale
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* WS-3a: elide stale turns. The newest `keepTurns` assistant turns and the final user message
|
|
38
|
+
* stay verbatim. Older turns collapse in two tiers (§5.3 discard semantics + R5/G4 honest
|
|
39
|
+
* strictness, and the only way the R6 acceptance bound — per-call ≤ Σ + window — can hold):
|
|
40
|
+
* - recent stale ring (the last `max(keepTurns, 1)` stale turns): toolResult payloads over
|
|
41
|
+
* `elideChars` become head+tail previews (same truncation shape as repl stdout); assistant
|
|
42
|
+
* prose always collapses to the one-line stub.
|
|
43
|
+
* - older still: EVERYTHING (payloads included) becomes the one-line session-log stub —
|
|
44
|
+
* a stale preview per turn would itself accumulate linearly and re-create O(T).
|
|
45
|
+
* `role:"custom"` messages of the Σ/intro kinds are immune (WS-3b owns them). Mutates the
|
|
46
|
+
* array in place; returns the number of messages elided (telemetry), for zero-cost counters.
|
|
41
47
|
*/
|
|
42
48
|
export function elideStalePayloads(messages: RootMessage[], opts: ElideOptions): number {
|
|
43
49
|
const keepTurns = Math.max(0, Math.floor(opts.keepTurns));
|
|
44
|
-
if (
|
|
50
|
+
if (messages.length === 0) return 0;
|
|
51
|
+
// Preview ring: stale turns recent enough to deserve the §5.3 head+tail preview. Scales with
|
|
52
|
+
// the window knob — one calibration, two tiers (preview ring, then stub) — and never zero,
|
|
53
|
+
// so keepTurns=0 still previews the single closest stale payload instead of stubbing blind.
|
|
54
|
+
const previewRing = Math.max(keepTurns, 1);
|
|
55
|
+
if (keepTurns === 0) {
|
|
56
|
+
// R5 strict-0: nothing inside the window — Σ + immune customs + the final user message
|
|
57
|
+
// are all that survive verbatim.
|
|
58
|
+
return elideRange(messages, 0, messages.length, opts, previewRing);
|
|
59
|
+
}
|
|
45
60
|
|
|
46
61
|
// Index of the assistant message that opens the keepTurns-th-from-last turn — everything
|
|
47
62
|
// from there on is the protected tail (same walk as core/compaction.ts elideOldToolPayloads).
|
|
@@ -57,15 +72,55 @@ export function elideStalePayloads(messages: RootMessage[], opts: ElideOptions):
|
|
|
57
72
|
}
|
|
58
73
|
}
|
|
59
74
|
if (tailStart <= 0) return 0; // fewer turns than the window — nothing to elide
|
|
75
|
+
return elideRange(messages, 0, tailStart, opts, previewRing);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Custom messages the elision never touches — the Σ snapshot/observation and the intro. */
|
|
79
|
+
const IMMUNE_CUSTOM_TYPES: ReadonlySet<string> = new Set(["rlm-sigma", "rlm-sigma-observation", "rlm-intro"]);
|
|
80
|
+
|
|
81
|
+
function isImmuneCustom(m: RootMessage): boolean {
|
|
82
|
+
if (m.role !== "custom") return false;
|
|
83
|
+
const customType: unknown = (m as { customType?: unknown }).customType;
|
|
84
|
+
return typeof customType === "string" && IMMUNE_CUSTOM_TYPES.has(customType);
|
|
85
|
+
}
|
|
60
86
|
|
|
87
|
+
/** R5: elide [from, to) — stale assistant prose always becomes the one-line Σ stub; stale
|
|
88
|
+
* toolResults get the §5.3 preview while `assistantsAfter < previewRing` and the stub beyond
|
|
89
|
+
* (two-tier elision — previews must not accumulate linearly). Immune customs and the final
|
|
90
|
+
* user message are never touched. Mutates in place; returns the elided count. */
|
|
91
|
+
function elideRange(
|
|
92
|
+
messages: RootMessage[],
|
|
93
|
+
from: number,
|
|
94
|
+
to: number,
|
|
95
|
+
opts: ElideOptions,
|
|
96
|
+
previewRing: number,
|
|
97
|
+
): number {
|
|
61
98
|
const lastUser = lastIndexOfRole(messages, "user");
|
|
99
|
+
// Pre-pass: stale assistant indices in [from, to) — a payload's recency is measured by the
|
|
100
|
+
// stale turns AFTER it (pre-allocated walk, no per-message allocation).
|
|
101
|
+
let staleAssistants = 0;
|
|
102
|
+
for (let i = from; i < to; i++) {
|
|
103
|
+
if (messages[i]?.role === "assistant") staleAssistants += 1;
|
|
104
|
+
}
|
|
62
105
|
let elided = 0;
|
|
63
|
-
for (let i =
|
|
106
|
+
for (let i = from; i < to; i++) {
|
|
64
107
|
const m = messages[i];
|
|
65
|
-
if (m === undefined || m
|
|
108
|
+
if (m === undefined || isImmuneCustom(m)) continue;
|
|
66
109
|
if (i === lastUser) continue; // paranoia: the final user message is never touched
|
|
67
|
-
|
|
68
|
-
|
|
110
|
+
if (m.role === "assistant") {
|
|
111
|
+
staleAssistants -= 1; // turns AFTER this one = count minus itself
|
|
112
|
+
messages[i] = { ...m, content: [{ type: "text", text: ROOT_TURN_ELIDED_LINE }] } as RootMessage;
|
|
113
|
+
elided += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (m.role !== "toolResult") continue;
|
|
117
|
+
if (staleAssistants >= previewRing) {
|
|
118
|
+
// Deep-stale payload: even the preview would accumulate — collapse to the stub.
|
|
119
|
+
messages[i] = { ...m, content: [{ type: "text", text: ROOT_TURN_ELIDED_LINE }] } as RootMessage;
|
|
120
|
+
elided += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (totalTextLength(m) <= opts.elideChars) continue;
|
|
69
124
|
messages[i] = {
|
|
70
125
|
...m,
|
|
71
126
|
content: [{ type: "text", text: previewToolText(m, opts.elideChars) }],
|
|
@@ -75,13 +130,16 @@ export function elideStalePayloads(messages: RootMessage[], opts: ElideOptions):
|
|
|
75
130
|
return elided;
|
|
76
131
|
}
|
|
77
132
|
|
|
78
|
-
/** WS-3b: exactly one live Σ snapshot, immediately before the LAST user message.
|
|
133
|
+
/** WS-3b: exactly one live Σ snapshot, immediately before the LAST user message.
|
|
134
|
+
* R2 (G2): `withContract` makes the splice carry the fence contract (A.4 authoring mode) —
|
|
135
|
+
* pass-through to runStateRootBlock; without it, the v1 observation-only block. */
|
|
79
136
|
export function spliceSigmaSnapshot(
|
|
80
137
|
messages: RootMessage[],
|
|
81
138
|
state: RunState,
|
|
82
139
|
rectifyHint: string | undefined,
|
|
140
|
+
opts?: { readonly withContract?: boolean },
|
|
83
141
|
): void {
|
|
84
|
-
const block = runStateRootBlock(state);
|
|
142
|
+
const block = runStateRootBlock(state, opts);
|
|
85
143
|
const text = rectifyHint === undefined ? block : `${block}\n${rectifyHint}`;
|
|
86
144
|
// Remove any previous instance (only one lives at a time — idempotent across calls).
|
|
87
145
|
for (let i = messages.length - 1; i >= 0; i--) {
|
package/src/core/root-state.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* controller/sandboxManager. It is runtime-derived — tool outcomes, engine-run mirrors,
|
|
7
7
|
* the user's latest prompt — zero model cooperation required (paper §5.3: observation
|
|
8
8
|
* override; §5.7: small models must not be the state's author by default). The optional
|
|
9
|
-
* fence protocol (enableRootStateFences
|
|
9
|
+
* fence protocol (enableRootStateFences — ENFORCED ON since Root Σ v2 R0) is the only
|
|
10
|
+
* model-proposed input and
|
|
10
11
|
* rides the SAME V(ΔΣ_t,Σ_t) validator + retry/degrade ladder as engine runs.
|
|
11
12
|
*
|
|
12
13
|
* Caps/dedup/serialization are the engine's own machinery: RUN_STATE_LIMITS, dedupStrings,
|
|
@@ -35,6 +36,23 @@ const RECTIFY_FAILURE_THRESHOLD = 2;
|
|
|
35
36
|
/** Task restatement cap — mirrors run-state.ts TASK_MAX_CHARS (kept in sync by comment). */
|
|
36
37
|
const ROOT_TASK_MAX_CHARS = 200;
|
|
37
38
|
|
|
39
|
+
/** R4: idle-degrade threshold for the NATIVE root tracker — deliberately ROOT-SPECIFIC
|
|
40
|
+
* (soak finding, 2025-09-08 live sessions: qwen3.8-27b ×3, qwen3-30b/32b, gemini-flash).
|
|
41
|
+
* The engine's `RUN_STATE_IDLE_DEGRADE_TURNS = 4` is bench-tuned for runs conditioned on Σ
|
|
42
|
+
* from turn 1; NATIVE sessions have a cold-start ramp — first fences land on turn 3 (short
|
|
43
|
+
* tasks) or turn 5–6 (study tasks), so 4 amputated exactly before the first commit (2/2
|
|
44
|
+
* study sessions degraded at 4, then fenced at 5). 6 clears the observed ramp while still
|
|
45
|
+
* bounding the fence tax. The engine const and its tuning are untouched.
|
|
46
|
+
*/
|
|
47
|
+
export const ROOT_IDLE_DEGRADE_TURNS = 6;
|
|
48
|
+
|
|
49
|
+
/** R3 soak observability: per-turn fence outcome, returned by applyFences. */
|
|
50
|
+
export interface FenceOutcome {
|
|
51
|
+
readonly fences: number;
|
|
52
|
+
readonly accepted: number;
|
|
53
|
+
readonly problems: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
/** Root Σ mode — same discriminated union shape as the engine's (active | degraded). */
|
|
39
57
|
type RootStateMode =
|
|
40
58
|
| { readonly kind: "active"; readonly retries: number }
|
|
@@ -45,6 +63,8 @@ export class RootStateTracker {
|
|
|
45
63
|
private mode: RootStateMode = { kind: "active", retries: 0 };
|
|
46
64
|
private opCounter = 0;
|
|
47
65
|
private dirtyFlag = false;
|
|
66
|
+
/** R4: consecutive fence-eligible turns with zero accepted deltas (idle streak). */
|
|
67
|
+
private idleFenceTurns = 0;
|
|
48
68
|
private readonly failures = new Map<string, number>();
|
|
49
69
|
private pendingObservation: string | undefined;
|
|
50
70
|
private readonly retryMax: number;
|
|
@@ -82,6 +102,21 @@ export class RootStateTracker {
|
|
|
82
102
|
return this.dirtyFlag;
|
|
83
103
|
}
|
|
84
104
|
|
|
105
|
+
/** R4: true while the tracker accepts fences and the context transform may splice Σ. */
|
|
106
|
+
get isActive(): boolean {
|
|
107
|
+
return this.mode.kind === "active";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** R4 telemetry: consecutive fence-eligible turns with zero accepted deltas. */
|
|
111
|
+
get idleTurns(): number {
|
|
112
|
+
return this.idleFenceTurns;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** R4 telemetry: the degrade reason while degraded; undefined while active. */
|
|
116
|
+
get degradeReason(): string | undefined {
|
|
117
|
+
return this.mode.kind === "degraded" ? this.mode.reason : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
snapshot(): RunState {
|
|
86
121
|
const capped = enforceCaps(this.draft);
|
|
87
122
|
// enforceCaps never fails in practice; the fallback keeps the tracker fail-soft anyway.
|
|
@@ -160,11 +195,28 @@ export class RootStateTracker {
|
|
|
160
195
|
* deltas land sequentially, ALL problems accumulate into ONE observation (error-as-
|
|
161
196
|
* observation), and only past `runStateRetryMax` total rejections the tracker degrades and
|
|
162
197
|
* fences stop being applied. Wording delegates to run-state.ts (N1) — one source.
|
|
198
|
+
*
|
|
199
|
+
* R4 (G6, /tmp/ROOT_FULL_SKILLSTATE_PLAN.md): idle-degrade parity with the engine — once
|
|
200
|
+
* the native prompt teaches the fence contract, EVERY finalized assistant turn is
|
|
201
|
+
* fence-eligible; a turn with zero accepted deltas grows `idleFenceTurns` and
|
|
202
|
+
* `ROOT_IDLE_DEGRADE_TURNS` consecutive idle turns degrade the tracker (an idle Σ is
|
|
203
|
+
* pure input tax — bench rec #2, paper §5.7; root threshold is 6, not the engine's 4 —
|
|
204
|
+
* see the const's soak citation). Any accepted delta resets the streak. In
|
|
205
|
+
* degraded mode fences stop applying and the context transform stops splicing (`isActive`),
|
|
206
|
+
* while runtime `observeToolResult` remains the Σ floor (degrade, never crash).
|
|
163
207
|
*/
|
|
164
|
-
applyFences(fences: readonly StateFenceResult[]):
|
|
165
|
-
if (this.mode.kind !== "active"
|
|
208
|
+
applyFences(fences: readonly StateFenceResult[]): FenceOutcome {
|
|
209
|
+
if (this.mode.kind !== "active") return { fences: fences.length, accepted: 0, problems: 0 };
|
|
210
|
+
if (fences.length === 0) {
|
|
211
|
+
// R4 (G6): a fence-free turn on a conditioned loop is IDLE — the contract rode the
|
|
212
|
+
// prompt for nothing. Grow the streak; degrade at the engine's threshold.
|
|
213
|
+
this.idleFenceTurns += 1;
|
|
214
|
+
this.degradeIfIdle();
|
|
215
|
+
return { fences: 0, accepted: 0, problems: 0 };
|
|
216
|
+
}
|
|
166
217
|
let state = this.snapshot();
|
|
167
218
|
const problems: string[] = [];
|
|
219
|
+
let accepted = 0;
|
|
168
220
|
for (const fence of fences) {
|
|
169
221
|
if (!fence.ok) {
|
|
170
222
|
problems.push(malformedFenceProblem(fence.error));
|
|
@@ -173,16 +225,21 @@ export class RootStateTracker {
|
|
|
173
225
|
const next: Result<RunState, PatchError> = applyPatch(state, fence.value, ++this.opCounter);
|
|
174
226
|
if (next.ok) {
|
|
175
227
|
state = next.value;
|
|
228
|
+
accepted += 1;
|
|
176
229
|
} else {
|
|
177
230
|
problems.push(patchErrorText(next.error));
|
|
178
231
|
}
|
|
179
232
|
}
|
|
233
|
+
// Accepted deltas reset the idle streak — even in a partially-failing batch (engine
|
|
234
|
+
// parity: real work is never punished for a sibling's malformed fence).
|
|
235
|
+
this.idleFenceTurns = accepted > 0 ? 0 : this.idleFenceTurns + 1;
|
|
236
|
+
this.degradeIfIdle();
|
|
180
237
|
if (problems.length === 0) {
|
|
181
238
|
this.mode = { kind: "active", retries: 0 };
|
|
182
239
|
this.pendingObservation = undefined;
|
|
183
240
|
this.draft = this.toMutable(state);
|
|
184
241
|
this.touch();
|
|
185
|
-
return;
|
|
242
|
+
return { fences: fences.length, accepted, problems: 0 };
|
|
186
243
|
}
|
|
187
244
|
const retries = this.mode.retries + problems.length;
|
|
188
245
|
this.pendingObservation = statePatchObservation(problems);
|
|
@@ -192,9 +249,22 @@ export class RootStateTracker {
|
|
|
192
249
|
this.touch();
|
|
193
250
|
if (retries > this.retryMax) {
|
|
194
251
|
this.mode = { kind: "degraded", reason: `state-patch retry cap exceeded (${retries} rejected)` };
|
|
195
|
-
} else {
|
|
252
|
+
} else if (this.mode.kind === "active") {
|
|
253
|
+
// An idle degrade fired earlier in this call wins over re-activating — degrade is
|
|
254
|
+
// sticky; the runtime observation floor keeps Σ alive until the session ends.
|
|
196
255
|
this.mode = { kind: "active", retries };
|
|
197
256
|
}
|
|
257
|
+
return { fences: fences.length, accepted, problems: problems.length };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** R4: fire the idle degrade at the root threshold (active trackers only). */
|
|
261
|
+
private degradeIfIdle(): void {
|
|
262
|
+
if (this.mode.kind === "active" && this.idleFenceTurns >= ROOT_IDLE_DEGRADE_TURNS) {
|
|
263
|
+
this.mode = {
|
|
264
|
+
kind: "degraded",
|
|
265
|
+
reason: `idle degrade — ${this.idleFenceTurns} consecutive turns with zero accepted deltas`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
198
268
|
}
|
|
199
269
|
|
|
200
270
|
/** Consume (and clear) the pending fence-rejection observation, if any. */
|
package/src/core/run-state.ts
CHANGED
|
@@ -560,18 +560,28 @@ export const STATE_FENCE_INSTRUCTION: string =
|
|
|
560
560
|
`rejected), and a patch over ${RUN_STATE_LIMITS.patchBytes} bytes is rejected whole.\n` +
|
|
561
561
|
"Commit anything possibly relevant NOW; the raw observation will not be shown again.";
|
|
562
562
|
|
|
563
|
+
/** ONE Σ-block composer (R2): the engine turn block and the root splice both delegate here —
|
|
564
|
+
* never duplicate the contract + Σ assembly. `withContract` is paper A.4 authoring mode;
|
|
565
|
+
* without it, observation-only mode. */
|
|
566
|
+
function sigmaBlock(state: RunState, withContract: boolean): string {
|
|
567
|
+
const sigma = `[Σ] ${compactJSON(state)}`;
|
|
568
|
+
return withContract ? `${STATE_FENCE_INSTRUCTION}\n\n${sigma}` : sigma;
|
|
569
|
+
}
|
|
570
|
+
|
|
563
571
|
/** The per-turn A_t block: the fence contract + the current Σ (paper A_t = (P, Σ_t, O_t)). */
|
|
564
572
|
export function runStateTurnBlock(state: RunState): string {
|
|
565
|
-
return
|
|
573
|
+
return sigmaBlock(state, true);
|
|
566
574
|
}
|
|
567
575
|
|
|
568
576
|
/**
|
|
569
|
-
* Root Σ (WS-3b): the ROOT's A_t block
|
|
570
|
-
*
|
|
571
|
-
*
|
|
577
|
+
* Root Σ (WS-3b): the ROOT's A_t block. R2 (G2): with `withContract` the splice carries the
|
|
578
|
+
* SAME fence contract (delegating to the one composer above — no re-wording), making the
|
|
579
|
+
* splice paper-faithful A.4 authoring mode; without it, byte-identical to the v1
|
|
580
|
+
* observation-only snapshot. Recall line comes from the glossary (one wording source).
|
|
572
581
|
*/
|
|
573
|
-
export function runStateRootBlock(state: RunState): string {
|
|
574
|
-
return
|
|
582
|
+
export function runStateRootBlock(state: RunState, opts?: { readonly withContract?: boolean }): string {
|
|
583
|
+
return sigmaBlock(state, opts?.withContract === true) +
|
|
584
|
+
"\n" +
|
|
575
585
|
"Fresh tool results outrank Σ when they disagree.\n" +
|
|
576
586
|
`[Project facts recall: skill_search()] — ${SKILL_RECALL_LINE}`;
|
|
577
587
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { loadSettings, mergeConfig, resolveModelId } from "./config/settings.ts"
|
|
|
13
13
|
import { RlmController } from "./mode/rlm-mode.ts";
|
|
14
14
|
import { cheapestModel } from "./mode/llm-model.ts";
|
|
15
15
|
import { postRlmGuide } from "./ui/intro.ts";
|
|
16
|
-
import { setRlmModeStatus } from "./ui/status.ts";
|
|
16
|
+
import { setRlmModeStatus, type RootSigmaTelemetry } from "./ui/status.ts";
|
|
17
17
|
import { RunRegistry } from "./ui/panel/run-registry.ts";
|
|
18
18
|
import { installTreePanel } from "./ui/panel/tree-panel.ts";
|
|
19
19
|
import { markdownTheme } from "./ui/theme-adapter.ts";
|
|
@@ -113,11 +113,20 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
113
113
|
/** Root Σ (WS-3/4): the native session's digest-level Σ_t — runtime-derived (tool outcomes,
|
|
114
114
|
* engine mirrors, prompts); lazily born on the first prompt, harvested + dropped at shutdown. */
|
|
115
115
|
let rootTracker: RootStateTracker | undefined;
|
|
116
|
-
// Root Σ WS-5.1 telemetry — journal counters (trace lines
|
|
116
|
+
// Root Σ WS-5.1 telemetry — journal counters (trace lines + status widget when tracing).
|
|
117
117
|
let xiCompositions = 0;
|
|
118
118
|
let rootDigests = 0;
|
|
119
119
|
let elidedMessages = 0;
|
|
120
120
|
let sigmaSplices = 0;
|
|
121
|
+
let idleDegrades = 0;
|
|
122
|
+
/** R6: Σ counter snapshot for the status line — a fresh readonly object per render. */
|
|
123
|
+
const sigmaTelemetry = (): RootSigmaTelemetry => ({
|
|
124
|
+
xiCompositions,
|
|
125
|
+
rootDigests,
|
|
126
|
+
elidedMessages,
|
|
127
|
+
sigmaSplices,
|
|
128
|
+
idleDegrades,
|
|
129
|
+
});
|
|
121
130
|
// A detached child works in its OWN sandbox, so this one sees no frames and its request
|
|
122
131
|
// watchdog would fire mid-await and SIGKILL a healthy worker, taking the REPL namespace
|
|
123
132
|
// with it. Keep it alive while detached work is genuinely in flight.
|
|
@@ -162,7 +171,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
162
171
|
/** Ξ (Workstream C): BM25 slice of the SkillState store for a query; undefined when off. */
|
|
163
172
|
const composeSkillBlock = (query: string): string | undefined => {
|
|
164
173
|
const cfg = controller.config;
|
|
165
|
-
|
|
174
|
+
// R0: enableSkillState is enforced (validateEnforcedOn) — no config check remains.
|
|
175
|
+
if (skillStore === undefined) return undefined;
|
|
166
176
|
const block = skillStore.blockFor(query, cfg.skillStateMaxTokens);
|
|
167
177
|
return block === "" ? undefined : block;
|
|
168
178
|
};
|
|
@@ -204,10 +214,10 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
204
214
|
controller.savedLlmRef = persisted.llm ?? undefined;
|
|
205
215
|
controller.savedRlmRef = persisted.rlm ?? undefined;
|
|
206
216
|
|
|
207
|
-
// SKILL.state (Workstream B): hydrate the cross-session note store
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
// SKILL.state (Workstream B / R0): hydrate the cross-session note store UNCONDITIONALLY —
|
|
218
|
+
// the SkillStore is operating law; no rlm.json, command, or UI path can prevent its birth
|
|
219
|
+
// (hostile configs are traced + ignored at the validateEnforcedOn seam; fail-soft).
|
|
220
|
+
skillStore = await SkillStore.hydrate(controller.config.skillStateNotesPerProject);
|
|
211
221
|
controller.skillStore = skillStore;
|
|
212
222
|
|
|
213
223
|
// An explicit --rlm flag wins over the persisted setting for this session.
|
|
@@ -336,7 +346,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
336
346
|
}
|
|
337
347
|
}
|
|
338
348
|
|
|
339
|
-
setRlmModeStatus(ctx, controller, ctx.getContextUsage());
|
|
349
|
+
setRlmModeStatus(ctx, controller, ctx.getContextUsage(), sigmaTelemetry());
|
|
340
350
|
if (!treePanelInstalled) {
|
|
341
351
|
treePanelInstalled = true;
|
|
342
352
|
installTreePanel(ctx, runRegistry);
|
|
@@ -349,7 +359,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
349
359
|
|
|
350
360
|
// ── Keep the footer's context reading live (RLM exists to shrink this number) ──
|
|
351
361
|
pi.on("turn_end", async (_event, ctx) => {
|
|
352
|
-
setRlmModeStatus(ctx, controller, ctx.getContextUsage());
|
|
362
|
+
setRlmModeStatus(ctx, controller, ctx.getContextUsage(), sigmaTelemetry());
|
|
353
363
|
});
|
|
354
364
|
|
|
355
365
|
/** True when the native-mode trade holds: enabled AND repl is in the active tool set. */
|
|
@@ -387,21 +397,54 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
387
397
|
const message = observation === undefined
|
|
388
398
|
? undefined
|
|
389
399
|
: { customType: "rlm-sigma-observation", content: observation, display: false, details: undefined };
|
|
400
|
+
// R1 + R3 soak instrumentation: what the plugin RETURNS as the system prompt — the host
|
|
401
|
+
// applies result.systemPrompt verbatim (agent-session.js), so this line + the journal
|
|
402
|
+
// decide "wiring bug vs model non-compliance" for the fence contract.
|
|
403
|
+
const nativePrompt = buildNativeSystemPrompt({ stateFences: controller.config.enableRootStateFences });
|
|
404
|
+
if (traceEnabled) {
|
|
405
|
+
trace("root-prompt.composed", {
|
|
406
|
+
chars: nativePrompt.length,
|
|
407
|
+
fences: controller.config.enableRootStateFences,
|
|
408
|
+
contract: nativePrompt.includes("[state] Alongside"),
|
|
409
|
+
xi: xi !== undefined,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
390
412
|
return {
|
|
391
413
|
...(message === undefined ? {} : { message }),
|
|
392
|
-
systemPrompt: event.systemPrompt + "\n\n" + xiPart +
|
|
414
|
+
systemPrompt: event.systemPrompt + "\n\n" + xiPart + nativePrompt,
|
|
393
415
|
};
|
|
394
416
|
});
|
|
395
417
|
|
|
396
|
-
// Root Σ WS-4.2
|
|
397
|
-
// replies and run them through the ONE patch validator (run-state.ts applyPatch).
|
|
418
|
+
// Root Σ WS-4.2 + v2 R4: capture model-proposed ΔΣ_t fences from finalized assistant
|
|
419
|
+
// replies and run them through the ONE patch validator (run-state.ts applyPatch). EVERY
|
|
420
|
+
// assistant turn feeds the ladder — a fence-free turn grows the idle streak, and
|
|
421
|
+
// RUN_STATE_IDLE_DEGRADE_TURNS consecutive idle turns degrade the tracker (G6 parity).
|
|
398
422
|
pi.on("message_end", async (event) => {
|
|
399
423
|
const tracker = rootTracker;
|
|
400
424
|
if (tracker === undefined || !controller.config.enableRootStateFences) return;
|
|
401
425
|
if (event.message.role !== "assistant") return;
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
426
|
+
const wasActive = tracker.isActive;
|
|
427
|
+
const outcome = tracker.applyFences(findStatePatches(agentMessageText(event.message)));
|
|
428
|
+
// R3 soak observability: per-turn fence outcomes — the soak-B bars (≥50% of turns commit
|
|
429
|
+
// ≥1 accepted delta, rejection storms <10%) are computed from these journal lines.
|
|
430
|
+
if (traceEnabled) {
|
|
431
|
+
trace("root-state.turn", {
|
|
432
|
+
...outcome,
|
|
433
|
+
idle: tracker.idleTurns,
|
|
434
|
+
active: tracker.isActive,
|
|
435
|
+
degraded: wasActive && !tracker.isActive,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (wasActive && !tracker.isActive) {
|
|
439
|
+
idleDegrades += 1;
|
|
440
|
+
if (traceEnabled) {
|
|
441
|
+
const reason = tracker.degradeReason ?? "unknown";
|
|
442
|
+
trace(reason.startsWith("idle") ? "root-state.idle-degrade" : "root-state.degrade", {
|
|
443
|
+
idleTurns: tracker.idleTurns,
|
|
444
|
+
reason,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
405
448
|
});
|
|
406
449
|
|
|
407
450
|
// ── Root Σ WS-2: deterministic root compaction (no summary LLM call) ──
|
|
@@ -460,8 +503,14 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
460
503
|
});
|
|
461
504
|
elidedMessages += elided;
|
|
462
505
|
const tracker = rootTracker;
|
|
463
|
-
|
|
464
|
-
|
|
506
|
+
// R4: a DEGRADED tracker stops splicing Σ (isActive gate) — pure input tax otherwise.
|
|
507
|
+
if (
|
|
508
|
+
controller.config.rootContextSnapshot && tracker !== undefined &&
|
|
509
|
+
tracker.isActive && !tracker.isEmpty
|
|
510
|
+
) {
|
|
511
|
+
spliceSigmaSnapshot(filtered, tracker.snapshot(), tracker.rectifyHint(), {
|
|
512
|
+
withContract: controller.config.enableRootStateFences,
|
|
513
|
+
});
|
|
465
514
|
sigmaSplices += 1;
|
|
466
515
|
}
|
|
467
516
|
if (traceEnabled && (elided > 0 || sigmaSplices > 0)) {
|
|
@@ -554,8 +603,9 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
554
603
|
pi.on("session_shutdown", async () => {
|
|
555
604
|
// Root Σ WS-4 harvest symmetry: the root tracker teaches the store exactly like engine
|
|
556
605
|
// runs — the ONE notesFromRunState path — then the existing flush persists everything.
|
|
606
|
+
// R0: enableSkillState is enforced; no config check remains on this path.
|
|
557
607
|
const tracker = rootTracker;
|
|
558
|
-
if (skillStore !== undefined && tracker !== undefined && tracker.dirty
|
|
608
|
+
if (skillStore !== undefined && tracker !== undefined && tracker.dirty) {
|
|
559
609
|
try {
|
|
560
610
|
skillStore.merge(notesFromRunState(tracker.snapshot()));
|
|
561
611
|
if (traceEnabled) trace("root-harvest.merged", { notes: tracker.snapshot().verifiedFacts.length });
|
package/src/prompts/glossary.ts
CHANGED
|
@@ -62,6 +62,11 @@ const SKILL_SEARCH_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
62
62
|
export const SKILL_RECALL_LINE =
|
|
63
63
|
"Recall more anytime inside repl: `skill_search(query, k=8)` → [{id, text, tags, score}].";
|
|
64
64
|
|
|
65
|
+
/** 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 Σ, the full text in the session log. */
|
|
67
|
+
export const ROOT_TURN_ELIDED_LINE =
|
|
68
|
+
"… turn elided — durable facts live in Σ; full text in session log";
|
|
69
|
+
|
|
65
70
|
export function skillStateLines(noteCount: number, body: string): string {
|
|
66
71
|
return [
|
|
67
72
|
`[Project facts — SkillState, ${noteCount} note${noteCount === 1 ? "" : "s"}, distilled from prior sessions]`,
|
package/src/prompts/native.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
DEFAULT_PROMPT_CAP,
|
|
13
13
|
promptCapTokensK,
|
|
14
14
|
} from "./glossary.ts";
|
|
15
|
+
import { STATE_FENCE_INSTRUCTION } from "../core/run-state.ts";
|
|
15
16
|
|
|
16
17
|
/** Adapts the REPL glossary for native mode — agent calls `repl({code})` instead of writing ```repl``` blocks. */
|
|
17
18
|
function nativeReplGlossary(): string {
|
|
@@ -67,8 +68,15 @@ function nativeReplGlossary(): string {
|
|
|
67
68
|
].join("\n");
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
/** Build the native-mode system prompt for the main Pi agent.
|
|
71
|
-
|
|
71
|
+
/** Build the native-mode system prompt for the main Pi agent.
|
|
72
|
+
*
|
|
73
|
+
* R1 (G1, /tmp/ROOT_FULL_SKILLSTATE_PLAN.md): `stateFences` appends the ONE Σ fence contract
|
|
74
|
+
* (STATE_FENCE_INSTRUCTION verbatim — one wording source, engine and native share it) so the
|
|
75
|
+
* native model can author ΔΣ_t through ```state fences. The fence text is STATIC, so it may
|
|
76
|
+
* ride call-time composition; the FLAG decision happens at the call site — NATIVE_PROMPT_STATIC
|
|
77
|
+
* (the frozen module-load snapshot) is composed with no options and stays contract-free.
|
|
78
|
+
*/
|
|
79
|
+
export function buildNativeSystemPrompt(opts?: { readonly stateFences?: boolean }): string {
|
|
72
80
|
return [
|
|
73
81
|
"╔══════════════════════════════════════════════════════════════════╗",
|
|
74
82
|
"║ NATIVE RLM MODE — YOU ARE AN ORCHESTRATOR, NOT A READER ║",
|
|
@@ -162,6 +170,22 @@ export function buildNativeSystemPrompt(): string {
|
|
|
162
170
|
"</rules>",
|
|
163
171
|
"",
|
|
164
172
|
nativeReplGlossary(),
|
|
173
|
+
...(opts?.stateFences === true
|
|
174
|
+
? [
|
|
175
|
+
"",
|
|
176
|
+
STATE_FENCE_INSTRUCTION,
|
|
177
|
+
// Soak-B finding (R3): models ignore the contract when it only speaks headless
|
|
178
|
+
// \u201c```repl block(s)\u201d \u2014 in native mode those are repl({code}) TOOL calls. The engine
|
|
179
|
+
// wording above stays byte-identical (one source); this line maps it 1:1 onto the
|
|
180
|
+
// native tool-call loop so the fence obligation is unambiguous.
|
|
181
|
+
"NATIVE MODE: you emit repl({code}) as TOOL calls, not ```repl text blocks \u2014 the " +
|
|
182
|
+
"contract above maps 1:1 onto this loop. In ANY reply where you learned something " +
|
|
183
|
+
"durable (a path, a fact, a failed approach, the next step), ALSO emit a ```state " +
|
|
184
|
+
'fenced block in that same reply: {"state_patch": {"verifiedFacts[+]": ' +
|
|
185
|
+
'"src/x.ts \u2014 what you just verified"}}. Deltas only; one small patch per turn; ' +
|
|
186
|
+
"never restate unchanged keys.",
|
|
187
|
+
]
|
|
188
|
+
: []),
|
|
165
189
|
].join("\n");
|
|
166
190
|
}
|
|
167
191
|
|
package/src/ui/config-panel.ts
CHANGED
|
@@ -68,6 +68,18 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
68
68
|
"Allow add_context() to pull an external dir, file, document, or git repo into context."),
|
|
69
69
|
item("autoSeedCwd", "Auto-seed cwd", config.autoSeedCwd ? "on" : "off", CHOICES.autoSeedCwd,
|
|
70
70
|
"Seed the working directory into context on the first repl() call (otherwise starts empty)."),
|
|
71
|
+
// R0 (/tmp/ROOT_FULL_SKILLSTATE_PLAN.md): the SKILL.state / Root Σ paradigm flags are
|
|
72
|
+
// ENFORCED — rendered as a read-only badge so the truth is visible instead of hidden.
|
|
73
|
+
// No toggle exists: applySetting has no case for them and the validator forces true.
|
|
74
|
+
item("__sigma_enforced__", "SKILL.state / Root Σ", "enforced", ["enforced"],
|
|
75
|
+
"ENFORCED (no opt-out): run state, skill state + distill, root context transform, state fences, digest compaction. " +
|
|
76
|
+
"Override attempts in rlm.json are traced (skillstate.override-ignored) and ignored; RLM_BENCH_NO_ROOTCONTEXT=1 is the dev-only measurement hatch."),
|
|
77
|
+
// R5: the window calibrations are rlm.json-only knobs — shown read-only with live values.
|
|
78
|
+
item("__sigma_window__", "Root Σ window (calibration)",
|
|
79
|
+
`keepTurns=${config.rootContextKeepTurns} · elide=${config.rootContextElideChars} · snapshot=${config.rootContextSnapshot ? "on" : "off"}`,
|
|
80
|
+
["rlm.json"],
|
|
81
|
+
"Query-time window calibrations, rlm.json only: rootContextKeepTurns (1 = strict: Σ + current turn; 2 = default), rootContextElideChars, rootContextSnapshot. " +
|
|
82
|
+
"Session resume/fork: the tracker is reborn lazily and Σ re-grows from live observations — the first call after a resume has an empty Σ by design."),
|
|
71
83
|
item("__save__", "Save & close", "↵", ["↵"], "Save these settings and close (Esc also saves)."),
|
|
72
84
|
];
|
|
73
85
|
|
package/src/ui/status.ts
CHANGED
|
@@ -3,23 +3,47 @@
|
|
|
3
3
|
* The footer's extension-status row is sanitized to a single line, so the
|
|
4
4
|
* two-model layout lives in a dedicated multi-line widget instead: one line
|
|
5
5
|
* for the mode, one per model lane (llm = leaf sub-calls, rlm = child engines),
|
|
6
|
-
* each with the live context token spend
|
|
6
|
+
* each with the live context token spend — and, when trace mode is on, a Root Σ
|
|
7
|
+
* telemetry line (R6): Ξ compositions, digest compactions, elisions, Σ splices,
|
|
8
|
+
* idle degrades.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import type { ContextUsage, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
12
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
11
13
|
import type { RlmController } from "../mode/rlm-mode.ts";
|
|
14
|
+
import { traceEnabled } from "../util/trace.ts";
|
|
12
15
|
import { formatTokens } from "./theme.ts";
|
|
13
16
|
|
|
14
17
|
const KEY = "rlm";
|
|
15
18
|
|
|
19
|
+
/** R6: Root Σ closure-counter snapshot (fresh readonly object per render). */
|
|
20
|
+
export interface RootSigmaTelemetry {
|
|
21
|
+
readonly xiCompositions: number;
|
|
22
|
+
readonly rootDigests: number;
|
|
23
|
+
readonly elidedMessages: number;
|
|
24
|
+
readonly sigmaSplices: number;
|
|
25
|
+
readonly idleDegrades: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export function modelLabel(model: Model<Api> | undefined, fallback: string): string {
|
|
17
29
|
return model ? `${model.provider}/${model.id}` : fallback;
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
/** R6: the Σ telemetry line — built only when tracing and at least one counter is live. */
|
|
33
|
+
function sigmaLine(telemetry: RootSigmaTelemetry): string | undefined {
|
|
34
|
+
const parts: string[] = [];
|
|
35
|
+
if (telemetry.xiCompositions > 0) parts.push(`Ξ${telemetry.xiCompositions}`);
|
|
36
|
+
if (telemetry.elidedMessages > 0) parts.push(`elided ${telemetry.elidedMessages}`);
|
|
37
|
+
if (telemetry.sigmaSplices > 0) parts.push(`Σ${telemetry.sigmaSplices}`);
|
|
38
|
+
if (telemetry.rootDigests > 0) parts.push(`digest ${telemetry.rootDigests}`);
|
|
39
|
+
if (telemetry.idleDegrades > 0) parts.push(`degraded ${telemetry.idleDegrades}`);
|
|
40
|
+
return parts.length === 0 ? undefined : ` Σ ${parts.join(" · ")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
export function formatRlmStatusLines(
|
|
21
44
|
controller: RlmController,
|
|
22
45
|
contextUsage?: ContextUsage,
|
|
46
|
+
telemetry?: RootSigmaTelemetry,
|
|
23
47
|
): readonly string[] {
|
|
24
48
|
if (!controller.enabled) return ["○ RLM OFF"];
|
|
25
49
|
const tokens = contextUsage?.tokens;
|
|
@@ -28,14 +52,25 @@ export function formatRlmStatusLines(
|
|
|
28
52
|
const llmSuffix = controller.config.subSampling.reasoning ? `:${controller.config.subSampling.reasoning}` : "";
|
|
29
53
|
const rlm = modelLabel(controller.rlmModel, controller.savedRlmRef ?? "session");
|
|
30
54
|
const rlmSuffix = controller.config.rootSampling?.reasoning ? `:${controller.config.rootSampling.reasoning}` : "";
|
|
31
|
-
|
|
55
|
+
const lines = [
|
|
32
56
|
"● RLM ON",
|
|
33
57
|
` llm=${llm}${llmSuffix}${tokSuffix}`,
|
|
34
58
|
` rlm=${rlm}${rlmSuffix}${tokSuffix}`,
|
|
35
59
|
];
|
|
60
|
+
// R6: counters surface only under trace mode — the default UI stays clean.
|
|
61
|
+
if (traceEnabled && telemetry !== undefined) {
|
|
62
|
+
const sigma = sigmaLine(telemetry);
|
|
63
|
+
if (sigma !== undefined) lines.push(sigma);
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
36
66
|
}
|
|
37
67
|
|
|
38
68
|
/** Set the above-editor status widget. Idempotent — call on every state change. */
|
|
39
|
-
export function setRlmModeStatus(
|
|
40
|
-
ctx
|
|
69
|
+
export function setRlmModeStatus(
|
|
70
|
+
ctx: ExtensionContext,
|
|
71
|
+
controller: RlmController,
|
|
72
|
+
contextUsage?: ContextUsage,
|
|
73
|
+
telemetry?: RootSigmaTelemetry,
|
|
74
|
+
): void {
|
|
75
|
+
ctx.ui.setWidget(KEY, [...formatRlmStatusLines(controller, contextUsage, telemetry)], { placement: "aboveEditor" });
|
|
41
76
|
}
|