@fiale-plus/pi-rogue 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +75 -31
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +2 -2
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +25 -4
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +4 -3
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +38 -4
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +52 -6
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +10 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +17 -2
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +2 -2
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +11 -2
- package/node_modules/@fiale-plus/pi-rogue-router/README.md +32 -0
- package/node_modules/@fiale-plus/pi-rogue-router/package.json +30 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.test.ts +84 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.ts +355 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/cli.ts +277 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/completions.ts +34 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/config-extension.test.ts +133 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/config.ts +168 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/dataset.ts +154 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/decision-ledger.test.ts +148 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/decision.ts +138 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/extension.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/git-features.ts +119 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/hash.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/index.ts +15 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/learning.test.ts +241 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/learning.ts +382 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/ledger.ts +94 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/observe.ts +119 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/outcomes.ts +128 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/progress.ts +93 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/session-reader.ts +217 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/subagents.ts +178 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/types.ts +150 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/v1-telemetry.test.ts +293 -0
- package/package.json +5 -3
- package/src/extension.test.ts +1 -0
- package/src/extension.ts +2 -0
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ It stitches together (and bundles for a true single-package install):
|
|
|
8
8
|
- `@fiale-plus/pi-rogue-advisor` (logic; direct releases paused)
|
|
9
9
|
- `@fiale-plus/pi-rogue-context-broker` (context-broker runtime; registered by default with an env kill switch)
|
|
10
10
|
- `@fiale-plus/pi-rogue-orchestration` (logic; direct releases paused)
|
|
11
|
+
- `@fiale-plus/pi-rogue-router` (observe-only trajectory-router lab; direct releases paused)
|
|
11
12
|
|
|
12
13
|
Direct installs of the advisor/orchestration packages are paused (marked private). All users and future releases go through the bundle. See `docs/release.md` and root `AGENTS.md` / `README.md` for the release policy.
|
|
13
14
|
|
|
@@ -37,7 +38,7 @@ npm install
|
|
|
37
38
|
|
|
38
39
|
## Command surface
|
|
39
40
|
|
|
40
|
-
- Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
|
|
41
|
+
- Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab`, `/router` plus status/config/command paths (all provided via the bundle).
|
|
41
42
|
- Context broker: enabled by default; `PI_CONTEXT_BROKER_ENABLED=false` disables `/context status`, `/context brief`, `/context lookup <handle|text>`, `/context pin <handle>`, `/context export <handle>`, and `/context prune` with autocomplete.
|
|
42
43
|
|
|
43
44
|
## Status
|
|
@@ -5,7 +5,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { completeSimple, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
|
-
import { featureFile, readText, truncate, writeText, atomicWriteText } from "./internal.js";
|
|
8
|
+
import { featureDir, featureFile, readText, truncate, writeText, atomicWriteText } from "./internal.js";
|
|
9
9
|
import { advisorArgumentCompletions, piRogueArgumentCompletions } from "./completions.js";
|
|
10
10
|
import {
|
|
11
11
|
appendRouteLog,
|
|
@@ -46,10 +46,10 @@ const DEFAULT_CONFIG: AdvisorConfig = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
const CONFIG_PATH = featureFile("advisor", "config.json");
|
|
49
|
-
const
|
|
49
|
+
const LEGACY_STATE_PATH = featureFile("advisor", "state.json");
|
|
50
50
|
const CACHE_PATH = featureFile("advisor", "cache.json");
|
|
51
|
-
const CURRENT_PATH = featureFile("advisor", "current.md");
|
|
52
51
|
const HISTORY_PATH = featureFile("advisor", "history.jsonl");
|
|
52
|
+
const SESSION_STATE_PROP = "__piRogueAdvisorStatePath";
|
|
53
53
|
const ORCHESTRATION_DIR = join(homedir(), ".pi", "agent", "fiale-plus", "orchestration");
|
|
54
54
|
|
|
55
55
|
const MAX_CACHE = 64;
|
|
@@ -169,8 +169,47 @@ function saveConfig(c: AdvisorConfig) {
|
|
|
169
169
|
writeJson(CONFIG_PATH, c);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
function
|
|
173
|
-
const
|
|
172
|
+
function advisorSessionDir(ctxOrKey?: any): string {
|
|
173
|
+
const key = typeof ctxOrKey === "string" ? ctxOrKey : sessionKey(ctxOrKey);
|
|
174
|
+
return join(featureDir("advisor"), "sessions", safeSessionKey(key));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function advisorSessionStatePath(ctxOrKey?: any): string {
|
|
178
|
+
return join(advisorSessionDir(ctxOrKey), "state.json");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function advisorCurrentPath(ctxOrKey?: any): string {
|
|
182
|
+
return join(advisorSessionDir(ctxOrKey), "current.md");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function safeSessionKey(key: string): string {
|
|
186
|
+
const safe = String(key || "session").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
187
|
+
return safe || "session";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function statePathFor(state: SessionState): string {
|
|
191
|
+
return String((state as any)[SESSION_STATE_PROP] || LEGACY_STATE_PATH);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function attachStatePath<T extends SessionState>(state: T, path: string): T {
|
|
195
|
+
Object.defineProperty(state, SESSION_STATE_PROP, {
|
|
196
|
+
value: path,
|
|
197
|
+
enumerable: false,
|
|
198
|
+
configurable: true,
|
|
199
|
+
writable: true,
|
|
200
|
+
});
|
|
201
|
+
return state;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function loadState(ctxOrKey?: any): SessionState {
|
|
205
|
+
// Do not fall back to LEGACY_STATE_PATH here: that file was unscoped and is
|
|
206
|
+
// the source of issue #103 context bleed. New/resumed sessions must only load
|
|
207
|
+
// their own namespaced mutable advisor state.
|
|
208
|
+
return loadStateFromPath(advisorSessionStatePath(ctxOrKey));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function loadStateFromPath(path: string): SessionState {
|
|
212
|
+
const raw = readJson<Partial<SessionState>>(path, {});
|
|
174
213
|
// Handle state versioning: migrate old versions to current
|
|
175
214
|
const version = raw._v ?? 0;
|
|
176
215
|
if (version < STATE_VERSION) {
|
|
@@ -181,7 +220,7 @@ function loadState(): SessionState {
|
|
|
181
220
|
}
|
|
182
221
|
const control = raw.reviewControl;
|
|
183
222
|
const pauseUntil = Number(raw.advisorPauseUntilTurn);
|
|
184
|
-
return {
|
|
223
|
+
return attachStatePath({
|
|
185
224
|
_v: STATE_VERSION,
|
|
186
225
|
turns: raw.turns ?? 0,
|
|
187
226
|
lastTask: raw.lastTask ?? "",
|
|
@@ -217,11 +256,11 @@ function loadState(): SessionState {
|
|
|
217
256
|
lastAppliedAt: control?.lastAppliedAt,
|
|
218
257
|
},
|
|
219
258
|
advisorPauseUntilTurn: Number.isFinite(pauseUntil) ? pauseUntil : undefined,
|
|
220
|
-
};
|
|
259
|
+
}, path);
|
|
221
260
|
}
|
|
222
261
|
|
|
223
262
|
function saveState(s: SessionState) {
|
|
224
|
-
atomicWriteText(
|
|
263
|
+
atomicWriteText(statePathFor(s), JSON.stringify(s, null, 2) + "\n");
|
|
225
264
|
}
|
|
226
265
|
|
|
227
266
|
function loadCache(): Record<string, string> {
|
|
@@ -489,7 +528,7 @@ function markReviewApplied(state: SessionState, signature: string, trigger: stri
|
|
|
489
528
|
}
|
|
490
529
|
|
|
491
530
|
function persistReviewState(state: SessionState, includeReviewRoute: boolean): void {
|
|
492
|
-
const persisted =
|
|
531
|
+
const persisted = loadStateFromPath(statePathFor(state));
|
|
493
532
|
persisted.reviewControl = state.reviewControl;
|
|
494
533
|
persisted.followUp = state.followUp;
|
|
495
534
|
persisted.followUpTask = state.followUpTask;
|
|
@@ -771,8 +810,12 @@ function mergeRouteReview(configReview: AdvisorConfig["review"], route?: ReviewP
|
|
|
771
810
|
|
|
772
811
|
function sessionKey(ctx: any): string {
|
|
773
812
|
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
|
774
|
-
if (
|
|
775
|
-
|
|
813
|
+
if (typeof sessionFile === "string" && sessionFile.length > 0) {
|
|
814
|
+
return safeSessionKey(basename(String(sessionFile)).replace(/\.[^.]+$/, ""));
|
|
815
|
+
}
|
|
816
|
+
const sessionId = ctx?.session?.id || process.env.PI_ROGUE_SESSION_ID;
|
|
817
|
+
if (typeof sessionId === "string" && sessionId.length > 0) return safeSessionKey(sessionId);
|
|
818
|
+
return "session";
|
|
776
819
|
}
|
|
777
820
|
|
|
778
821
|
type OrchestrationSnapshot = {
|
|
@@ -846,12 +889,13 @@ function checkinDescription(config: AdvisorConfig): string {
|
|
|
846
889
|
return `checkins ${config.checkinIntervalMinutes}m`;
|
|
847
890
|
}
|
|
848
891
|
|
|
849
|
-
function setPiRogueStatus(ctx: any, config = loadConfig(), state
|
|
892
|
+
function setPiRogueStatus(ctx: any, config = loadConfig(), state?: SessionState): void {
|
|
893
|
+
const currentState = state ?? loadState(ctx);
|
|
850
894
|
const normalized = normalizeAdvisorConfig(config);
|
|
851
895
|
const checkin = checkinDescription(normalized);
|
|
852
|
-
const pause = advisorPauseRemaining(
|
|
896
|
+
const pause = advisorPauseRemaining(currentState, currentState.turns);
|
|
853
897
|
const pauseText = pause > 0 ? ` · pause ${pause} turn${pause === 1 ? "" : "s"}` : "";
|
|
854
|
-
const last =
|
|
898
|
+
const last = currentState.checkin.lastAt ? ` · last ${new Date(currentState.checkin.lastAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}` : "";
|
|
855
899
|
ctx.ui.setStatus("pi-rogue", `☠︎ advisor ${normalized.mode}/${normalized.review} · ${checkin}${pauseText}${last}`);
|
|
856
900
|
}
|
|
857
901
|
|
|
@@ -896,7 +940,7 @@ async function maybeAdvisorCheckin(pi: ExtensionAPI, ctx: any, source: string):
|
|
|
896
940
|
if (checkinLocks.has(key)) return false;
|
|
897
941
|
|
|
898
942
|
const config = loadConfig();
|
|
899
|
-
const state = loadState();
|
|
943
|
+
const state = loadState(ctx);
|
|
900
944
|
const reason = shouldRunCheckin(config, state, Date.now(), Date.now());
|
|
901
945
|
if (!reason) {
|
|
902
946
|
if (state.checkin.queued) {
|
|
@@ -935,7 +979,7 @@ async function maybeAdvisorCheckin(pi: ExtensionAPI, ctx: any, source: string):
|
|
|
935
979
|
);
|
|
936
980
|
if (!completed) return false;
|
|
937
981
|
|
|
938
|
-
const next = loadState();
|
|
982
|
+
const next = loadState(ctx);
|
|
939
983
|
next.checkin = {
|
|
940
984
|
lastAt: new Date().toISOString(),
|
|
941
985
|
lastTurn: next.turns,
|
|
@@ -1056,7 +1100,7 @@ export async function completeWithHigherAdvisorModel(
|
|
|
1056
1100
|
|
|
1057
1101
|
async function askAdvisor(pi: ExtensionAPI, ctx: any, question: string, scope: string, includeWork: boolean) {
|
|
1058
1102
|
const config = loadConfig();
|
|
1059
|
-
const state = loadState();
|
|
1103
|
+
const state = loadState(ctx);
|
|
1060
1104
|
if (!question.trim()) return { text: "Ask a question.", error: "empty" };
|
|
1061
1105
|
|
|
1062
1106
|
const brokerBrief = includeWork ? contextBrokerBrief(pi) : "";
|
|
@@ -1085,7 +1129,7 @@ async function askAdvisor(pi: ExtensionAPI, ctx: any, question: string, scope: s
|
|
|
1085
1129
|
async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: string, meta: ReviewMaterialMeta) {
|
|
1086
1130
|
const config = loadConfig();
|
|
1087
1131
|
if (config.review === "off") return;
|
|
1088
|
-
const state = loadState();
|
|
1132
|
+
const state = loadState(ctx);
|
|
1089
1133
|
|
|
1090
1134
|
const signature = reviewMaterialSignature(state, delta, meta);
|
|
1091
1135
|
if (state.reviewControl.running) {
|
|
@@ -1252,7 +1296,7 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
|
|
|
1252
1296
|
finalReason = (parsed.reason || parsed.summary || "review result").slice(0, 120);
|
|
1253
1297
|
|
|
1254
1298
|
const display = formatAdvisorDisplay("advisor:llm", decision, finalReason);
|
|
1255
|
-
writeText(
|
|
1299
|
+
writeText(advisorCurrentPath(ctx), `${display}\n`);
|
|
1256
1300
|
|
|
1257
1301
|
const reviewTask = parsed.activeTask || state.lastTask || "";
|
|
1258
1302
|
const hasTaskActions = parsed.taskActions.length > 0;
|
|
@@ -1300,7 +1344,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1300
1344
|
pi.on("session_start", (_event, ctx) => {
|
|
1301
1345
|
const key = sessionKey(ctx);
|
|
1302
1346
|
checkinLocks.delete(key);
|
|
1303
|
-
const state = loadState();
|
|
1347
|
+
const state = loadState(ctx);
|
|
1304
1348
|
recoverReviewControl(state);
|
|
1305
1349
|
saveState(state);
|
|
1306
1350
|
setPiRogueStatus(ctx, loadConfig(), state);
|
|
@@ -1334,7 +1378,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1334
1378
|
// ── Preflight (heuristics only — no LLM call, <1ms) ──────────────────
|
|
1335
1379
|
pi.on("before_agent_start", async (event: any, ctx: any) => {
|
|
1336
1380
|
const cfg = loadConfig();
|
|
1337
|
-
const state = loadState();
|
|
1381
|
+
const state = loadState(ctx);
|
|
1338
1382
|
const hasFollowUp = Boolean(state.followUp);
|
|
1339
1383
|
if ((isAdvisorAutoRunSuppressed(state, state.turns) && !hasFollowUp) || cfg.mode === "off" || cfg.mode === "manual") {
|
|
1340
1384
|
return { systemPrompt: event.systemPrompt };
|
|
@@ -1391,7 +1435,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1391
1435
|
const note = routeNote(route);
|
|
1392
1436
|
const control = state.reviewControl;
|
|
1393
1437
|
const controlTag = control.status === "needed" || control.status === "running" ? `Review-control: ${control.status}${control.lastDecision ? ` (${control.lastDecision})` : ""}` : "";
|
|
1394
|
-
writeText(
|
|
1438
|
+
writeText(advisorCurrentPath(ctx), `${note}\n`);
|
|
1395
1439
|
return {
|
|
1396
1440
|
systemPrompt: [
|
|
1397
1441
|
event.systemPrompt,
|
|
@@ -1409,7 +1453,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1409
1453
|
pi.on("turn_end", async (event: any, ctx: any) => {
|
|
1410
1454
|
const cfg = loadConfig();
|
|
1411
1455
|
if (cfg.mode === "off") return;
|
|
1412
|
-
const state = loadState();
|
|
1456
|
+
const state = loadState(ctx);
|
|
1413
1457
|
const suppressedThisTurn = isAdvisorAutoRunSuppressedForTurnContext(state, state.turns);
|
|
1414
1458
|
const tools = (event.toolResults || []).map((t: any) => String(t?.toolName || t?.name || "tool"));
|
|
1415
1459
|
const fileChanged = tools.some((t: string) => /^(edit|write)$/i.test(t));
|
|
@@ -1431,7 +1475,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1431
1475
|
});
|
|
1432
1476
|
}
|
|
1433
1477
|
|
|
1434
|
-
const post = loadState();
|
|
1478
|
+
const post = loadState(ctx);
|
|
1435
1479
|
if (!isAdvisorAutoRunSuppressed(post, post.turns)) {
|
|
1436
1480
|
void maybeAdvisorCheckin(pi, ctx, "turn_end");
|
|
1437
1481
|
}
|
|
@@ -1441,7 +1485,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1441
1485
|
pi.on("agent_end", async (event: any, ctx: any) => {
|
|
1442
1486
|
const cfg = loadConfig();
|
|
1443
1487
|
if (cfg.mode === "off") return;
|
|
1444
|
-
const state = loadState();
|
|
1488
|
+
const state = loadState(ctx);
|
|
1445
1489
|
const suppressed = isAdvisorAutoRunSuppressedForTurnContext(state, state.turns);
|
|
1446
1490
|
if (cfg.review === "off" || suppressed) {
|
|
1447
1491
|
if (!suppressed) {
|
|
@@ -1466,7 +1510,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1466
1510
|
materialSignals: signals,
|
|
1467
1511
|
});
|
|
1468
1512
|
|
|
1469
|
-
const post = loadState();
|
|
1513
|
+
const post = loadState(ctx);
|
|
1470
1514
|
if (!isAdvisorAutoRunSuppressed(post, post.turns)) {
|
|
1471
1515
|
void maybeAdvisorCheckin(pi, ctx, "agent_end");
|
|
1472
1516
|
}
|
|
@@ -1478,12 +1522,12 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1478
1522
|
getArgumentCompletions: (prefix: string) => piRogueArgumentCompletions(prefix),
|
|
1479
1523
|
handler: async (args, ctx) => {
|
|
1480
1524
|
const cfg = loadConfig();
|
|
1481
|
-
const state = loadState();
|
|
1525
|
+
const state = loadState(ctx);
|
|
1482
1526
|
const arg = String(args ?? "").trim().toLowerCase();
|
|
1483
1527
|
setPiRogueStatus(ctx, cfg, state);
|
|
1484
1528
|
|
|
1485
1529
|
if (!arg || arg === "status" || arg === "help") {
|
|
1486
|
-
ctx.ui.notify(piRogueCockpitText(cfg, state, readText(
|
|
1530
|
+
ctx.ui.notify(piRogueCockpitText(cfg, state, readText(advisorCurrentPath(ctx)).trim(), orchestrationSnapshotText(ctx)), "info");
|
|
1487
1531
|
return;
|
|
1488
1532
|
}
|
|
1489
1533
|
|
|
@@ -1519,7 +1563,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1519
1563
|
return;
|
|
1520
1564
|
}
|
|
1521
1565
|
|
|
1522
|
-
ctx.ui.notify(piRogueCockpitText(cfg, state, readText(
|
|
1566
|
+
ctx.ui.notify(piRogueCockpitText(cfg, state, readText(advisorCurrentPath(ctx)).trim(), orchestrationSnapshotText(ctx)), "info");
|
|
1523
1567
|
},
|
|
1524
1568
|
});
|
|
1525
1569
|
|
|
@@ -1531,10 +1575,10 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1531
1575
|
const a = String(args ?? "").trim().toLowerCase();
|
|
1532
1576
|
const [cmd, ...rest] = a.split(/\s+/);
|
|
1533
1577
|
const cfg = loadConfig();
|
|
1534
|
-
const state = loadState();
|
|
1578
|
+
const state = loadState(ctx);
|
|
1535
1579
|
|
|
1536
1580
|
if (!a || cmd === "status") {
|
|
1537
|
-
const note = readText(
|
|
1581
|
+
const note = readText(advisorCurrentPath(ctx)).trim();
|
|
1538
1582
|
const resolved = await resolveModel(ctx, cfg);
|
|
1539
1583
|
const route = state.router.review ?? state.router.preflight;
|
|
1540
1584
|
const pause = advisorPauseRemaining(state, state.turns);
|
|
@@ -4,7 +4,7 @@ import { dirname } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { completeSimple } from "@earendil-works/pi-ai";
|
|
7
|
-
import { registerAdvisor } from "./extension.js";
|
|
7
|
+
import { advisorSessionStatePath, registerAdvisor } from "./extension.js";
|
|
8
8
|
|
|
9
9
|
const testHome = vi.hoisted(() => `/tmp/pi-rogue-advisor-loop-convergence-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
10
10
|
|
|
@@ -57,7 +57,7 @@ function makeHandlers() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
|
|
60
|
-
const ADVISOR_STATE_PATH =
|
|
60
|
+
const ADVISOR_STATE_PATH = advisorSessionStatePath("session");
|
|
61
61
|
const ADVISOR_CONFIG_PATH = join(ADVISOR_STATE_DIR, "config.json");
|
|
62
62
|
const ADVISOR_CACHE_PATH = join(ADVISOR_STATE_DIR, "cache.json");
|
|
63
63
|
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { registerAdvisor } from "./extension.js";
|
|
5
|
+
import { advisorSessionStatePath, registerAdvisor } from "./extension.js";
|
|
6
6
|
|
|
7
7
|
const testHome = vi.hoisted(() => `/tmp/pi-rogue-advisor-state-versioning-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
8
8
|
|
|
@@ -37,17 +37,17 @@ function makeHandlers() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
|
|
40
|
-
const ADVISOR_STATE_PATH = join(ADVISOR_STATE_DIR, "state.json");
|
|
41
40
|
const ADVISOR_CONFIG_PATH = join(ADVISOR_STATE_DIR, "config.json");
|
|
41
|
+
const ADVISOR_STATE_PATH = advisorSessionStatePath("session");
|
|
42
42
|
|
|
43
43
|
function readAdvisorState(): any {
|
|
44
44
|
return JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function mkCtx() {
|
|
47
|
+
function mkCtx(session = "session") {
|
|
48
48
|
return {
|
|
49
49
|
sessionManager: {
|
|
50
|
-
getSessionFile: () => join(homedir(), ".pi", "agent", "pi-rogue", "advisor",
|
|
50
|
+
getSessionFile: () => join(homedir(), ".pi", "agent", "pi-rogue", "advisor", `${session}.jsonl`),
|
|
51
51
|
},
|
|
52
52
|
isIdle: () => true,
|
|
53
53
|
modelRegistry: {
|
|
@@ -224,4 +224,25 @@ describe("state versioning and recovery", () => {
|
|
|
224
224
|
expect(recovered.reviewControl.pending).toBe(true);
|
|
225
225
|
expect(recovered.reviewControl.lastDecision).toBe("review");
|
|
226
226
|
});
|
|
227
|
+
|
|
228
|
+
it("keeps mutable advisor state isolated by session", async () => {
|
|
229
|
+
const setup = makeHandlers();
|
|
230
|
+
const { handlers: h, pi } = setup;
|
|
231
|
+
registerAdvisor(pi);
|
|
232
|
+
|
|
233
|
+
const ctxA = mkCtx("model-training");
|
|
234
|
+
const ctxB = mkCtx("runpod");
|
|
235
|
+
|
|
236
|
+
void h.session_start?.[0]?.({}, ctxA);
|
|
237
|
+
await h.before_agent_start?.[0]?.({ prompt: "train advisor on regex logs", systemPrompt: "base" }, ctxA);
|
|
238
|
+
|
|
239
|
+
const stateA = JSON.parse(readFileSync(advisorSessionStatePath("model-training"), "utf8"));
|
|
240
|
+
expect(stateA.lastTask).toBe("train advisor on regex logs");
|
|
241
|
+
|
|
242
|
+
void h.session_start?.[0]?.({}, ctxB);
|
|
243
|
+
const stateB = JSON.parse(readFileSync(advisorSessionStatePath("runpod"), "utf8"));
|
|
244
|
+
expect(stateB.lastTask).toBe("");
|
|
245
|
+
expect(stateB.followUp).toBe("");
|
|
246
|
+
expect(stateB.reviewSignals).toEqual([]);
|
|
247
|
+
});
|
|
227
248
|
});
|
|
@@ -22,7 +22,7 @@ PI_CONTEXT_BROKER_ENABLED=false pi
|
|
|
22
22
|
|
|
23
23
|
When active, the bundle registers:
|
|
24
24
|
|
|
25
|
-
- `/context status` — enabled state, record/byte counts, pinned counts, and
|
|
25
|
+
- `/context status` — enabled state, record/byte counts, pinned counts, routing telemetry, and prompt rewrite savings bytes.
|
|
26
26
|
- `/context brief` — bounded prompt-safe broker brief with handles and summaries.
|
|
27
27
|
- `/context lookup <handle|text>` — exact handle rehydration or current-session text search.
|
|
28
28
|
- `/context pin <handle>` — protect an artifact from normal TTL/cap pruning.
|
|
@@ -33,9 +33,9 @@ The command includes autocomplete for subcommands and known artifact handles. Ex
|
|
|
33
33
|
|
|
34
34
|
Optional durability is available with `PI_CONTEXT_BROKER_DURABLE=true` or `PI_CONTEXT_BROKER_STORE_DIR=/path/to/store`. Durable mode now defaults to SQLite (`artifacts.sqlite`) with an FTS index for text lookup, so exact handles, tier, and pin state survive restarts without replay reconstruction. Set `PI_CONTEXT_BROKER_BACKEND=jsonl` to use the legacy JSONL/blob backend.
|
|
35
35
|
|
|
36
|
-
- `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES` controls when large `toolResult` / `bashExecution` payloads are rewritten in-context. The default is `
|
|
36
|
+
- `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES` controls when large `toolResult` / `bashExecution` payloads are rewritten in-context. The default is `8192` bytes, so small tool evidence remains inline while larger outputs are replaced by handles.
|
|
37
37
|
|
|
38
|
-
For
|
|
38
|
+
For more aggressive prompt reduction, set `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES=0`. For quieter sessions, set it to a higher value to only rewrite larger outputs.
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
## Session behavior and limits
|
|
@@ -45,6 +45,7 @@ For quieter sessions, set `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES` to a highe
|
|
|
45
45
|
- Without durable mode, restarting Pi loses broker state until the current branch is backfilled again.
|
|
46
46
|
- Prompt integration injects a bounded, tier-aware broker brief and lookup guidance; the LLM also gets a `context_lookup` tool for exact handle dereferencing. Payloads that hit hostile-binary heuristics are represented in prompt as handles plus short guidance to export the full content.
|
|
47
47
|
- The `context` hook rewrites prompt-visible `toolResult` and `bashExecution` payloads in the LLM-bound message copy to broker handles and summaries, reducing prompt load while preserving exact `/context lookup` rehydration.
|
|
48
|
+
- Current-turn `context_lookup` results are left visible so the model can consume requested exact evidence once. Historical `context_lookup` results that already have a later assistant response are omitted from later prompt assembly to avoid recursive prompt growth.
|
|
48
49
|
- Pi `excludeFromContext` bash entries are not backfilled or rewritten into broker prompts.
|
|
49
50
|
- Basic secret redaction runs before broker storage and display for common token/password/API-key patterns.
|
|
50
51
|
- Optional global caps can be configured via env vars:
|
|
@@ -382,7 +382,7 @@ lookupBytes: 500,
|
|
|
382
382
|
await commands.get("context").handler(`export ${handle}`, ctx);
|
|
383
383
|
const result = await handlers.get("context")?.[0]({
|
|
384
384
|
type: "context",
|
|
385
|
-
messages: [{ role: "toolResult", toolCallId: "tool-result-telemetry", toolName: "bash", content: [{ type: "text", text: "telemetry_payload_" + "y".repeat(
|
|
385
|
+
messages: [{ role: "toolResult", toolCallId: "tool-result-telemetry", toolName: "bash", content: [{ type: "text", text: "telemetry_payload_" + "y".repeat(1000) }], isError: false, timestamp: 1 }],
|
|
386
386
|
}, ctx);
|
|
387
387
|
|
|
388
388
|
await commands.get("context").handler("status", ctx);
|
|
@@ -392,15 +392,46 @@ lookupBytes: 500,
|
|
|
392
392
|
expect(result).toBeDefined();
|
|
393
393
|
const telemetry = notifications.at(-1)?.message ?? "";
|
|
394
394
|
expect(telemetry).toContain("Context broker routing telemetry:");
|
|
395
|
+
expect(telemetry).toContain("rewriteSavings rawBytes=");
|
|
396
|
+
expect(telemetry).toContain("replacementBytes=");
|
|
397
|
+
expect(telemetry).toContain("savedBytes=");
|
|
398
|
+
expect(telemetry).toMatch(/savedBytes=[1-9]\d*/);
|
|
399
|
+
expect(telemetry).toContain("contextLookupHistoryOmitted=");
|
|
395
400
|
expect(telemetry).toContain("lookups tool(calls=");
|
|
396
401
|
expect(telemetry).toContain("lookups slash(calls=");
|
|
397
402
|
expect(telemetry).toContain("exports=");
|
|
398
403
|
expect(telemetry).toContain("pins=");
|
|
399
404
|
});
|
|
400
405
|
|
|
401
|
-
it("
|
|
406
|
+
it("keeps current context_lookup results visible before the model consumes them", async () => {
|
|
402
407
|
const { pi, handlers, commands, tools } = createPiMock();
|
|
403
|
-
registerContextBrokerBeta(pi, { lookupBytes:
|
|
408
|
+
registerContextBrokerBeta(pi, { lookupBytes: 1000, rewriteThresholdBytes: 1 });
|
|
409
|
+
const { ctx } = createCtx();
|
|
410
|
+
|
|
411
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
412
|
+
await runHandlers(handlers, "tool_result", {
|
|
413
|
+
type: "tool_result",
|
|
414
|
+
toolCallId: "call-current-lookup-source",
|
|
415
|
+
toolName: "bash",
|
|
416
|
+
input: { command: "printf current-lookup" },
|
|
417
|
+
content: [{ type: "text", text: "CURRENT_LOOKUP_EVIDENCE_" + "x".repeat(120) }],
|
|
418
|
+
isError: false,
|
|
419
|
+
}, ctx);
|
|
420
|
+
const handle = commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
421
|
+
const lookupResult = await tools.get("context_lookup").execute("lookup-current", { handle }, undefined, undefined, ctx);
|
|
422
|
+
|
|
423
|
+
const contextResult = await handlers.get("context")?.[0]({
|
|
424
|
+
type: "context",
|
|
425
|
+
messages: [{ role: "toolResult", toolCallId: "lookup-current", toolName: "context_lookup", content: lookupResult.content, isError: false }],
|
|
426
|
+
}, ctx);
|
|
427
|
+
|
|
428
|
+
expect(contextResult).toBeUndefined();
|
|
429
|
+
expect(lookupResult.content[0].text).toContain("CURRENT_LOOKUP_EVIDENCE_");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("does not broker historical context_lookup results recursively", async () => {
|
|
433
|
+
const { pi, handlers, commands, tools } = createPiMock();
|
|
434
|
+
registerContextBrokerBeta(pi, { lookupBytes: 500 });
|
|
404
435
|
const { ctx, notifications } = createCtx();
|
|
405
436
|
|
|
406
437
|
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
@@ -425,7 +456,10 @@ lookupBytes: 500,
|
|
|
425
456
|
}, ctx);
|
|
426
457
|
const contextResult = await handlers.get("context")?.[0]({
|
|
427
458
|
type: "context",
|
|
428
|
-
messages: [
|
|
459
|
+
messages: [
|
|
460
|
+
{ role: "toolResult", toolCallId: "lookup-call", toolName: "context_lookup", content: lookupResult.content, isError: false },
|
|
461
|
+
{ role: "assistant", content: [{ type: "text", text: "I consumed the lookup." }] },
|
|
462
|
+
],
|
|
429
463
|
}, ctx);
|
|
430
464
|
await commands.get("context").handler("brief", ctx);
|
|
431
465
|
|
|
@@ -176,6 +176,16 @@ function compact(value: string, max = 120): string {
|
|
|
176
176
|
return truncateUtf8(value.replace(/\s+/g, " ").trim(), max);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function utf8Bytes(text: string): number {
|
|
180
|
+
return Buffer.byteLength(text, "utf8");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function promptPayloadBytes(message: any): number {
|
|
184
|
+
if (message?.role === "bashExecution") return utf8Bytes(String(message.output ?? ""));
|
|
185
|
+
if (message?.role === "toolResult") return utf8Bytes(contentText(message.content));
|
|
186
|
+
return utf8Bytes(toText(message));
|
|
187
|
+
}
|
|
188
|
+
|
|
179
189
|
function stableHash(value: string): string {
|
|
180
190
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
181
191
|
}
|
|
@@ -308,6 +318,9 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
308
318
|
contextHookBash: 0,
|
|
309
319
|
contextHookBashRewrites: 0,
|
|
310
320
|
contextHookBashHostile: 0,
|
|
321
|
+
contextHookRewriteRawBytes: 0,
|
|
322
|
+
contextHookRewriteReplacementBytes: 0,
|
|
323
|
+
contextHookContextLookupHistoryOmissions: 0,
|
|
311
324
|
toolResultEvents: 0,
|
|
312
325
|
toolResultArtifacts: 0,
|
|
313
326
|
backfillScans: 0,
|
|
@@ -329,11 +342,21 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
329
342
|
pruneCalls: 0,
|
|
330
343
|
};
|
|
331
344
|
|
|
345
|
+
function recordContextRewrite(rawBytes: number, replacementBytes: number): void {
|
|
346
|
+
routingTelemetry.contextHookRewriteRawBytes += Math.max(0, rawBytes);
|
|
347
|
+
routingTelemetry.contextHookRewriteReplacementBytes += Math.max(0, replacementBytes);
|
|
348
|
+
}
|
|
349
|
+
|
|
332
350
|
function formatRoutingTelemetry(): string {
|
|
351
|
+
const savedBytes = Math.max(0, routingTelemetry.contextHookRewriteRawBytes - routingTelemetry.contextHookRewriteReplacementBytes);
|
|
352
|
+
const savedPct = routingTelemetry.contextHookRewriteRawBytes > 0
|
|
353
|
+
? ((savedBytes / routingTelemetry.contextHookRewriteRawBytes) * 100).toFixed(1)
|
|
354
|
+
: "0.0";
|
|
333
355
|
const line = [
|
|
334
356
|
`contextHook calls=${routingTelemetry.contextHookCalls}`,
|
|
335
357
|
`toolResults seen=${routingTelemetry.contextHookToolResults} rewritten=${routingTelemetry.contextHookToolResultRewrites} hostile=${routingTelemetry.contextHookToolResultHostile}`,
|
|
336
358
|
`bash seen=${routingTelemetry.contextHookBash} rewritten=${routingTelemetry.contextHookBashRewrites} hostile=${routingTelemetry.contextHookBashHostile}`,
|
|
359
|
+
`rewriteSavings rawBytes=${routingTelemetry.contextHookRewriteRawBytes} replacementBytes=${routingTelemetry.contextHookRewriteReplacementBytes} savedBytes=${savedBytes} savedPct=${savedPct}% contextLookupHistoryOmitted=${routingTelemetry.contextHookContextLookupHistoryOmissions}`,
|
|
337
360
|
`lookups tool(calls=${routingTelemetry.toolLookupCalls}, hits=${routingTelemetry.toolLookupHits}, misses=${routingTelemetry.toolLookupMisses})`,
|
|
338
361
|
`lookups slash(calls=${routingTelemetry.commandLookupCalls}, hits=${routingTelemetry.commandLookupHits}, misses=${routingTelemetry.commandLookupMisses})`,
|
|
339
362
|
`exports=${routingTelemetry.exportCalls}`,
|
|
@@ -572,19 +595,35 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
572
595
|
activeSessionId = sessionIdFor(ctx);
|
|
573
596
|
routingTelemetry.contextHookCalls += 1;
|
|
574
597
|
const toolInputs = collectToolInputs(event.messages);
|
|
575
|
-
|
|
598
|
+
type RewriteDraft = {
|
|
599
|
+
original: any;
|
|
600
|
+
replacement?: any;
|
|
601
|
+
rawBytes?: number;
|
|
602
|
+
artifact?: ContextArtifact;
|
|
603
|
+
rewrite?: (artifact: ContextArtifact) => any;
|
|
604
|
+
safeFallback?: any;
|
|
605
|
+
};
|
|
606
|
+
const drafts = event.messages.map((message: any, index: number): RewriteDraft => {
|
|
576
607
|
if (message?.role === "toolResult") {
|
|
577
608
|
routingTelemetry.contextHookToolResults += 1;
|
|
578
609
|
const raw = contentText(message.content);
|
|
610
|
+
const rawBytes = utf8Bytes(raw);
|
|
579
611
|
const toolInput = typeof message.toolCallId === "string" ? toolInputs.get(message.toolCallId) : undefined;
|
|
580
612
|
const toolName = String(message.toolName ?? toolInput?.toolName ?? "tool");
|
|
581
613
|
const hostile = hasHostileText(raw) || hasHostileValue(message.content);
|
|
582
614
|
if (hostile) routingTelemetry.contextHookToolResultHostile += 1;
|
|
583
|
-
const shouldRewrite = Buffer.byteLength(raw, "utf8") > rewriteThresholdBytes || hostile;
|
|
584
|
-
if (!shouldRewrite) return { original: message };
|
|
585
615
|
if (!shouldBrokerToolName(toolName)) {
|
|
586
|
-
|
|
616
|
+
const hasLaterAssistant = event.messages.slice(index + 1).some((candidate: any) => candidate?.role === "assistant");
|
|
617
|
+
if (!hasLaterAssistant) return { original: message };
|
|
618
|
+
routingTelemetry.contextHookContextLookupHistoryOmissions += 1;
|
|
619
|
+
return {
|
|
620
|
+
original: message,
|
|
621
|
+
rawBytes,
|
|
622
|
+
replacement: { ...message, content: [{ type: "text", text: contextLookupHistoryPlaceholder() }] },
|
|
623
|
+
};
|
|
587
624
|
}
|
|
625
|
+
const shouldRewrite = rawBytes > rewriteThresholdBytes || hostile;
|
|
626
|
+
if (!shouldRewrite) return { original: message };
|
|
588
627
|
const artifact = publishToolArtifact({
|
|
589
628
|
toolName,
|
|
590
629
|
input: message.input ?? toolInput?.input,
|
|
@@ -599,6 +638,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
599
638
|
routingTelemetry.contextHookToolResultRewrites += 1;
|
|
600
639
|
return {
|
|
601
640
|
original: message,
|
|
641
|
+
rawBytes,
|
|
602
642
|
artifact,
|
|
603
643
|
rewrite: (live) => ({ ...message, content: [{ type: "text", text: brokerPlaceholder(live) }] }),
|
|
604
644
|
safeFallback: { ...message, content: [{ type: "text", text: prunedPayloadPlaceholder(hostile) }] },
|
|
@@ -608,9 +648,10 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
608
648
|
if (message?.role === "bashExecution" && message.excludeFromContext !== true) {
|
|
609
649
|
routingTelemetry.contextHookBash += 1;
|
|
610
650
|
const raw = String(message.output ?? "");
|
|
651
|
+
const rawBytes = utf8Bytes(raw);
|
|
611
652
|
const hostile = hasHostileText(raw) || hasHostileValue(message.output);
|
|
612
653
|
if (hostile) routingTelemetry.contextHookBashHostile += 1;
|
|
613
|
-
const shouldRewrite =
|
|
654
|
+
const shouldRewrite = rawBytes > rewriteThresholdBytes || hostile;
|
|
614
655
|
if (!shouldRewrite) return { original: message };
|
|
615
656
|
const sourceId = typeof message.timestamp === "number"
|
|
616
657
|
? `bash:${message.timestamp}:${stableHash([message.command ?? "", raw, message.exitCode ?? "", message.cancelled ?? ""].join("\n"))}`
|
|
@@ -634,6 +675,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
634
675
|
routingTelemetry.contextHookBashRewrites += 1;
|
|
635
676
|
return {
|
|
636
677
|
original: message,
|
|
678
|
+
rawBytes,
|
|
637
679
|
artifact,
|
|
638
680
|
rewrite: (live) => ({ ...message, output: brokerPlaceholder(live), truncated: true }),
|
|
639
681
|
safeFallback: { ...message, output: prunedPayloadPlaceholder(hostile), truncated: true },
|
|
@@ -647,6 +689,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
647
689
|
const messages = drafts.map((draft) => {
|
|
648
690
|
if (draft.replacement) {
|
|
649
691
|
changed = true;
|
|
692
|
+
recordContextRewrite(draft.rawBytes ?? promptPayloadBytes(draft.original), promptPayloadBytes(draft.replacement));
|
|
650
693
|
return draft.replacement;
|
|
651
694
|
}
|
|
652
695
|
if (!draft.artifact || !draft.rewrite) return draft.original;
|
|
@@ -655,12 +698,15 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
655
698
|
for (const parentId of draft.artifact.parentIds) sourceHandles.delete(parentId);
|
|
656
699
|
if (draft.safeFallback) {
|
|
657
700
|
changed = true;
|
|
701
|
+
recordContextRewrite(draft.rawBytes ?? promptPayloadBytes(draft.original), promptPayloadBytes(draft.safeFallback));
|
|
658
702
|
return draft.safeFallback;
|
|
659
703
|
}
|
|
660
704
|
return draft.original;
|
|
661
705
|
}
|
|
662
706
|
changed = true;
|
|
663
|
-
|
|
707
|
+
const replacement = draft.rewrite(live);
|
|
708
|
+
recordContextRewrite(draft.rawBytes ?? promptPayloadBytes(draft.original), promptPayloadBytes(replacement));
|
|
709
|
+
return replacement;
|
|
664
710
|
});
|
|
665
711
|
|
|
666
712
|
return changed ? { messages } : undefined;
|
|
@@ -62,6 +62,16 @@ describe("advisor check-in lifecycle bridge", () => {
|
|
|
62
62
|
expect(JSON.parse(readFileSync(file, "utf8")).checkins).toBe("off");
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("preserves legacy one-argument reset signature", () => {
|
|
66
|
+
const { config } = tempState();
|
|
67
|
+
writeFileSync(config, JSON.stringify({ checkins: "off" }), "utf8");
|
|
68
|
+
|
|
69
|
+
const next = resetAdvisorSessionContext(config);
|
|
70
|
+
|
|
71
|
+
expect(next.config.checkins).toBe("off");
|
|
72
|
+
expect(JSON.parse(readFileSync(config, "utf8")).checkins).toBe("off");
|
|
73
|
+
});
|
|
74
|
+
|
|
65
75
|
it("resets advisor brief context and check-in timing for a new goal", () => {
|
|
66
76
|
const { config, state } = tempState();
|
|
67
77
|
const startedAt = Date.now();
|