@prestyj/cli 5.3.0 → 5.4.0
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/dist/app-sidecar.js +696 -59
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +138 -0
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +71 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +280 -47
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autopilot-cycle.d.ts +69 -17
- package/dist/core/autopilot-cycle.d.ts.map +1 -1
- package/dist/core/autopilot-cycle.js +82 -13
- package/dist/core/autopilot-cycle.js.map +1 -1
- package/dist/core/autopilot-cycle.test.js +206 -10
- package/dist/core/autopilot-cycle.test.js.map +1 -1
- package/dist/core/autopilot-gate.d.ts +48 -22
- package/dist/core/autopilot-gate.d.ts.map +1 -1
- package/dist/core/autopilot-gate.js +110 -1
- package/dist/core/autopilot-gate.js.map +1 -1
- package/dist/core/autopilot-gate.test.js +139 -3
- package/dist/core/autopilot-gate.test.js.map +1 -1
- package/dist/core/autopilot-verdict.d.ts +6 -0
- package/dist/core/autopilot-verdict.d.ts.map +1 -1
- package/dist/core/autopilot-verdict.js +75 -1
- package/dist/core/autopilot-verdict.js.map +1 -1
- package/dist/core/autopilot-verdict.test.js +63 -0
- package/dist/core/autopilot-verdict.test.js.map +1 -1
- package/dist/core/bash-spawn-benchmark.d.ts +2 -0
- package/dist/core/bash-spawn-benchmark.d.ts.map +1 -0
- package/dist/core/bash-spawn-benchmark.js +139 -0
- package/dist/core/bash-spawn-benchmark.js.map +1 -0
- package/dist/core/compaction/compactor.test.js +1 -1
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/fast-apply-benchmark.d.ts +1 -1
- package/dist/core/fast-apply-benchmark.js +2 -2
- package/dist/core/fast-apply-benchmark.js.map +1 -1
- package/dist/core/hashline-edit-benchmark.d.ts +33 -1
- package/dist/core/hashline-edit-benchmark.d.ts.map +1 -1
- package/dist/core/hashline-edit-benchmark.js +193 -21
- package/dist/core/hashline-edit-benchmark.js.map +1 -1
- package/dist/core/hashline-edit-benchmark.test.js +51 -2
- package/dist/core/hashline-edit-benchmark.test.js.map +1 -1
- package/dist/core/mcp/deferred-catalog.d.ts +28 -0
- package/dist/core/mcp/deferred-catalog.d.ts.map +1 -0
- package/dist/core/mcp/deferred-catalog.js +72 -0
- package/dist/core/mcp/deferred-catalog.js.map +1 -0
- package/dist/core/mcp/resolve-stdio.d.ts +19 -3
- package/dist/core/mcp/resolve-stdio.d.ts.map +1 -1
- package/dist/core/mcp/resolve-stdio.js +136 -5
- package/dist/core/mcp/resolve-stdio.js.map +1 -1
- package/dist/core/mcp/resolve-stdio.test.js +82 -1
- package/dist/core/mcp/resolve-stdio.test.js.map +1 -1
- package/dist/core/nolan-context.d.ts +26 -2
- package/dist/core/nolan-context.d.ts.map +1 -1
- package/dist/core/nolan-context.js +46 -4
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/nolan-context.test.js +53 -12
- package/dist/core/nolan-context.test.js.map +1 -1
- package/dist/core/nolan-prompt.d.ts +2 -15
- package/dist/core/nolan-prompt.d.ts.map +1 -1
- package/dist/core/nolan-prompt.js +96 -15
- package/dist/core/nolan-prompt.js.map +1 -1
- package/dist/core/nolan-prompt.test.js +102 -7
- package/dist/core/nolan-prompt.test.js.map +1 -1
- package/dist/core/persistent-shell.d.ts +27 -0
- package/dist/core/persistent-shell.d.ts.map +1 -0
- package/dist/core/persistent-shell.js +150 -0
- package/dist/core/persistent-shell.js.map +1 -0
- package/dist/core/persistent-shell.test.d.ts +2 -0
- package/dist/core/persistent-shell.test.d.ts.map +1 -0
- package/dist/core/persistent-shell.test.js +84 -0
- package/dist/core/persistent-shell.test.js.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.js +34 -6
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
- package/dist/core/progress/engine.d.ts +23 -0
- package/dist/core/progress/engine.d.ts.map +1 -0
- package/dist/core/progress/engine.js +131 -0
- package/dist/core/progress/engine.js.map +1 -0
- package/dist/core/progress/engine.test.d.ts +2 -0
- package/dist/core/progress/engine.test.d.ts.map +1 -0
- package/dist/core/progress/engine.test.js +136 -0
- package/dist/core/progress/engine.test.js.map +1 -0
- package/dist/core/progress/git-xp.d.ts +16 -0
- package/dist/core/progress/git-xp.d.ts.map +1 -0
- package/dist/core/progress/git-xp.js +106 -0
- package/dist/core/progress/git-xp.js.map +1 -0
- package/dist/core/progress/git-xp.test.d.ts +2 -0
- package/dist/core/progress/git-xp.test.d.ts.map +1 -0
- package/dist/core/progress/git-xp.test.js +88 -0
- package/dist/core/progress/git-xp.test.js.map +1 -0
- package/dist/core/progress/ranks.d.ts +21 -0
- package/dist/core/progress/ranks.d.ts.map +1 -0
- package/dist/core/progress/ranks.js +141 -0
- package/dist/core/progress/ranks.js.map +1 -0
- package/dist/core/progress/ranks.test.d.ts +2 -0
- package/dist/core/progress/ranks.test.d.ts.map +1 -0
- package/dist/core/progress/ranks.test.js +59 -0
- package/dist/core/progress/ranks.test.js.map +1 -0
- package/dist/core/progress/rebuild.d.ts +9 -0
- package/dist/core/progress/rebuild.d.ts.map +1 -0
- package/dist/core/progress/rebuild.js +119 -0
- package/dist/core/progress/rebuild.js.map +1 -0
- package/dist/core/progress/rebuild.test.d.ts +2 -0
- package/dist/core/progress/rebuild.test.d.ts.map +1 -0
- package/dist/core/progress/rebuild.test.js +88 -0
- package/dist/core/progress/rebuild.test.js.map +1 -0
- package/dist/core/progress/store.d.ts +35 -0
- package/dist/core/progress/store.d.ts.map +1 -0
- package/dist/core/progress/store.js +200 -0
- package/dist/core/progress/store.js.map +1 -0
- package/dist/core/progress/store.test.d.ts +2 -0
- package/dist/core/progress/store.test.d.ts.map +1 -0
- package/dist/core/progress/store.test.js +108 -0
- package/dist/core/progress/store.test.js.map +1 -0
- package/dist/core/progress/types.d.ts +108 -0
- package/dist/core/progress/types.d.ts.map +1 -0
- package/dist/core/progress/types.js +3 -0
- package/dist/core/progress/types.js.map +1 -0
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +40 -9
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.d.ts +2 -0
- package/dist/core/project-discovery.test.d.ts.map +1 -0
- package/dist/core/project-discovery.test.js +104 -0
- package/dist/core/project-discovery.test.js.map +1 -0
- package/dist/core/session-history.d.ts +51 -0
- package/dist/core/session-history.d.ts.map +1 -0
- package/dist/core/session-history.js +156 -0
- package/dist/core/session-history.js.map +1 -0
- package/dist/core/session-history.test.d.ts +2 -0
- package/dist/core/session-history.test.d.ts.map +1 -0
- package/dist/core/session-history.test.js +101 -0
- package/dist/core/session-history.test.js.map +1 -0
- package/dist/core/session-manager.d.ts +34 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +67 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +140 -1
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +12 -8
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +15 -20
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +27 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +9 -2
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +91 -10
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +97 -0
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/plan-mode.test.js +25 -0
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +3 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/subagent.d.ts +2 -0
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +213 -194
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.d.ts +2 -0
- package/dist/tools/subagent.test.d.ts.map +1 -0
- package/dist/tools/subagent.test.js +73 -0
- package/dist/tools/subagent.test.js.map +1 -0
- package/dist/tools/tool-search.d.ts +15 -0
- package/dist/tools/tool-search.d.ts.map +1 -0
- package/dist/tools/tool-search.js +35 -0
- package/dist/tools/tool-search.js.map +1 -0
- package/dist/ui/components/Footer.js +3 -3
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +22 -9
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/package.json +4 -4
|
@@ -8,7 +8,7 @@ import { SettingsManager } from "./settings-manager.js";
|
|
|
8
8
|
import { AuthStorage } from "./auth-storage.js";
|
|
9
9
|
import { getClaudeCliUserAgent } from "./claude-code-version.js";
|
|
10
10
|
import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
|
|
11
|
-
import { SessionManager, NOLAN_TURN_CUSTOM_KIND, } from "./session-manager.js";
|
|
11
|
+
import { SessionManager, NOLAN_TURN_CUSTOM_KIND, AUTOPILOT_MARKER_CUSTOM_KIND, APP_MARKER_CUSTOM_KIND, } from "./session-manager.js";
|
|
12
12
|
import { ExtensionLoader } from "./extensions/loader.js";
|
|
13
13
|
import { shouldCompact, compact, getCompactionReserveTokens } from "./compaction/compactor.js";
|
|
14
14
|
import { getAuthStorageKeys, getContextWindow, getModel, MODELS } from "./model-registry.js";
|
|
@@ -17,6 +17,8 @@ import { ensureAppDirs } from "../config.js";
|
|
|
17
17
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
18
18
|
import { createTools, createWebSearchTool, } from "../tools/index.js";
|
|
19
19
|
import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
|
|
20
|
+
import { DeferredToolCatalog } from "./mcp/deferred-catalog.js";
|
|
21
|
+
import { createToolSearchTool } from "../tools/tool-search.js";
|
|
20
22
|
import { log } from "./logger.js";
|
|
21
23
|
import { setEstimatorModel } from "./compaction/token-estimator.js";
|
|
22
24
|
import { discoverAgents } from "./agents.js";
|
|
@@ -45,6 +47,17 @@ export class AgentSession {
|
|
|
45
47
|
// transcript. Each carries the non-system message count at record time so the
|
|
46
48
|
// webview can interleave them chronologically.
|
|
47
49
|
nolanTurns = [];
|
|
50
|
+
// Autopilot Nolan (auto-reviewer) markers recorded against this build session:
|
|
51
|
+
// the review verdict shown in the transcript (prompted / done / human /
|
|
52
|
+
// capped). Same not-on-the-DAG treatment as nolanTurns — advisory only,
|
|
53
|
+
// persisted + reloaded so a resumed session shows the identical Nolan bubble
|
|
54
|
+
// the live run showed instead of dropping it or replaying a raw verdict.
|
|
55
|
+
autopilotMarkers = [];
|
|
56
|
+
// Generic app transcript markers (plan-mode banner, task header, error rows,
|
|
57
|
+
// user-bubble display hints). Same not-on-the-DAG treatment as nolanTurns —
|
|
58
|
+
// display only, persisted + reloaded so a resumed session shows the same
|
|
59
|
+
// transcript rows the live run showed.
|
|
60
|
+
appMarkers = [];
|
|
48
61
|
tools = [];
|
|
49
62
|
/** Rebuilds the read tool for a new model (video byte cap is baked in at
|
|
50
63
|
* creation). Called from switchModel so video-capable models get the
|
|
@@ -87,6 +100,8 @@ export class AgentSession {
|
|
|
87
100
|
processManager;
|
|
88
101
|
lspManager;
|
|
89
102
|
mcpManager;
|
|
103
|
+
/** Deferred MCP tools awaiting discovery via tool_search (bench A win). */
|
|
104
|
+
mcpCatalog;
|
|
90
105
|
provider;
|
|
91
106
|
model;
|
|
92
107
|
cwd;
|
|
@@ -352,7 +367,7 @@ export class AgentSession {
|
|
|
352
367
|
const mcpTools = this.opts.allowedTools
|
|
353
368
|
? connected.filter((t) => this.isToolAllowed(t.name))
|
|
354
369
|
: connected;
|
|
355
|
-
this.
|
|
370
|
+
this.addMcpTools(mcpTools);
|
|
356
371
|
// Background connect resolves AFTER initialize() has already built the
|
|
357
372
|
// system prompt (the default path awaits this before buildSystemPrompt,
|
|
358
373
|
// so its prompt already lists the tools). Refresh messages[0] so the
|
|
@@ -369,6 +384,31 @@ export class AgentSession {
|
|
|
369
384
|
log("WARN", "mcp", `MCP initialization failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
370
385
|
}
|
|
371
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Route freshly connected MCP tools: deferred into the tool_search catalog
|
|
389
|
+
* (default — keeps ~8k tokens of schema out of every cache-miss turn, see
|
|
390
|
+
* bench/RESULTS.md bench A) or pushed eagerly when the user opted out.
|
|
391
|
+
* Allow-listed sessions (Nolan) always get the eager path — their fixed tool
|
|
392
|
+
* expectations predate the catalog, and tool_search isn't allow-listed.
|
|
393
|
+
* Promotion pushes onto the live `this.tools` array the running agent loop
|
|
394
|
+
* re-reads every turn, so promoted tools are callable on the next step.
|
|
395
|
+
*/
|
|
396
|
+
addMcpTools(mcpTools) {
|
|
397
|
+
if (mcpTools.length === 0)
|
|
398
|
+
return;
|
|
399
|
+
const defer = !this.opts.allowedTools && this.settingsManager.get("deferredMcpTools");
|
|
400
|
+
if (!defer) {
|
|
401
|
+
this.tools.push(...mcpTools);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.mcpCatalog ??= new DeferredToolCatalog();
|
|
405
|
+
this.mcpCatalog.add(mcpTools);
|
|
406
|
+
if (!this.tools.some((t) => t.name === "tool_search")) {
|
|
407
|
+
this.tools.push(createToolSearchTool(this.mcpCatalog, (promoted) => {
|
|
408
|
+
this.tools.push(...promoted);
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
372
412
|
/**
|
|
373
413
|
* Process user input. Handles slash commands or runs agent loop.
|
|
374
414
|
*/
|
|
@@ -579,22 +619,23 @@ export class AgentSession {
|
|
|
579
619
|
// agent sees them mid-loop instead of after it stops.
|
|
580
620
|
if (this.userQueue.length > 0) {
|
|
581
621
|
const queued = this.userQueue.splice(0);
|
|
582
|
-
// Frame
|
|
583
|
-
// model treats a mid-run message as a fresh request that supersedes
|
|
584
|
-
// original task and silently drops it.
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
622
|
+
// Frame each queued item as concurrent steering — without this wrapper
|
|
623
|
+
// the model treats a mid-run message as a fresh request that supersedes
|
|
624
|
+
// the original task and silently drops it. ONE message per queued item
|
|
625
|
+
// (not merged): each persists as its own user message, so a resumed
|
|
626
|
+
// session shows the same number of bubbles the live run did.
|
|
627
|
+
return queued.map((m) => {
|
|
628
|
+
if (m.attachments.length === 0) {
|
|
629
|
+
return { role: "user", content: wrapSteeringText(m.text) };
|
|
630
|
+
}
|
|
631
|
+
// Queued attachments ride the same native-block path as a non-queued
|
|
632
|
+
// attachment prompt, prefixed with the steering framing.
|
|
633
|
+
const parts = [
|
|
634
|
+
{ type: "text", text: STEERING_PREFIX },
|
|
635
|
+
...this.buildAttachmentParts(m.text, m.attachments),
|
|
636
|
+
];
|
|
637
|
+
return { role: "user", content: parts };
|
|
638
|
+
});
|
|
598
639
|
}
|
|
599
640
|
if (!this.settingsManager.get("idealReviewEnabled"))
|
|
600
641
|
return null;
|
|
@@ -723,6 +764,25 @@ export class AgentSession {
|
|
|
723
764
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
724
765
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
725
766
|
getFollowUpMessages: () => this.getHookFollowUpMessages(),
|
|
767
|
+
// Overflow recovery: the loop calls this with { force: true } when the
|
|
768
|
+
// provider rejects a turn as too large (request_too_large / context
|
|
769
|
+
// overflow). Force-compact the in-flight history and hand it back so the
|
|
770
|
+
// loop retries with a smaller request, instead of surfacing the error.
|
|
771
|
+
// Without this the desktop app (which drives the loop through
|
|
772
|
+
// AgentSession, not the TUI's useContextCompaction hook) had NO auto
|
|
773
|
+
// recovery on 413 — the error went straight to the user. The non-force
|
|
774
|
+
// pre-call invocations pass through untouched: pre-turn compaction is
|
|
775
|
+
// already handled above, so we only act on the overflow force path.
|
|
776
|
+
// `loopMessages === this.messages` (prepareDynamicContext returns it by
|
|
777
|
+
// reference) and the post-loop `this.messages = loopMessages` re-sync
|
|
778
|
+
// keeps persistence correct after compact() swaps the array.
|
|
779
|
+
transformContext: async (messages, transformOpts) => {
|
|
780
|
+
if (!transformOpts?.force)
|
|
781
|
+
return messages;
|
|
782
|
+
await this.compact();
|
|
783
|
+
this.compactionOccurred = true;
|
|
784
|
+
return this.messages;
|
|
785
|
+
},
|
|
726
786
|
});
|
|
727
787
|
for await (const event of generator) {
|
|
728
788
|
this.trackHookEvent(event);
|
|
@@ -840,7 +900,11 @@ export class AgentSession {
|
|
|
840
900
|
// Use getAllMcpServers so user-configured servers survive the reconnect.
|
|
841
901
|
const servers = await getAllMcpServers(this.provider, apiKey, this.cwd);
|
|
842
902
|
const mcpTools = await this.mcpManager.connectAll(servers);
|
|
843
|
-
|
|
903
|
+
// Drop stale MCP tools from both the live set and deferred catalog before
|
|
904
|
+
// re-adding. Some tools may already have been promoted out of the catalog.
|
|
905
|
+
this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
|
|
906
|
+
this.mcpCatalog?.removeWhere((name) => name.startsWith("mcp__"));
|
|
907
|
+
this.addMcpTools(mcpTools);
|
|
844
908
|
}
|
|
845
909
|
catch (err) {
|
|
846
910
|
log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -869,20 +933,39 @@ export class AgentSession {
|
|
|
869
933
|
signal: this.opts.signal,
|
|
870
934
|
});
|
|
871
935
|
this.messages = result.messages;
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
936
|
+
// Transient sessions (Nolan chat/autopilot, subagent spawns) must NEVER touch
|
|
937
|
+
// the session store: without this guard, the first auto-compaction called
|
|
938
|
+
// sessionManager.create() and assigned a real sessionPath, silently turning
|
|
939
|
+
// the "in-memory only" session into a persisted one — every later turn (and
|
|
940
|
+
// every further compaction) then leaked a Nolan transcript file into the
|
|
941
|
+
// project's session list. Compact in memory only and keep sessionPath empty.
|
|
942
|
+
if (this.opts.transient) {
|
|
943
|
+
this.lastPersistedIndex = this.messages.length;
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
// Persist compacted messages to a new session file so `ezcoder continue`
|
|
947
|
+
// picks up the compacted state instead of the full original history.
|
|
948
|
+
const session = await this.sessionManager.create(this.cwd, this.provider, this.model);
|
|
949
|
+
this.sessionId = session.id;
|
|
950
|
+
this.sessionPath = session.path;
|
|
951
|
+
// Write compacted messages (skip system — it's rebuilt on load)
|
|
952
|
+
for (const msg of this.messages) {
|
|
953
|
+
if (msg.role === "system")
|
|
954
|
+
continue;
|
|
955
|
+
await this.persistMessage(msg);
|
|
956
|
+
}
|
|
957
|
+
this.lastPersistedIndex = this.messages.length;
|
|
958
|
+
// Carry Nolan's advisory turns into the new file so they survive compaction.
|
|
959
|
+
await this.rePersistNolanTurns();
|
|
960
|
+
await this.rePersistAutopilotMarkers();
|
|
961
|
+
await this.rePersistAppMarkers();
|
|
962
|
+
// Persist the compaction counts so a resumed session's quiet notice can
|
|
963
|
+
// show the same "N → M messages" summary the live run did.
|
|
964
|
+
await this.persistAppMarker("compaction", {
|
|
965
|
+
originalCount: result.result.originalCount,
|
|
966
|
+
newCount: result.result.newCount,
|
|
967
|
+
});
|
|
882
968
|
}
|
|
883
|
-
this.lastPersistedIndex = this.messages.length;
|
|
884
|
-
// Carry Nolan's advisory turns into the new file so they survive compaction.
|
|
885
|
-
await this.rePersistNolanTurns();
|
|
886
969
|
this.eventBus.emit("compaction_end", {
|
|
887
970
|
originalCount: result.result.originalCount,
|
|
888
971
|
newCount: result.result.newCount,
|
|
@@ -892,10 +975,31 @@ export class AgentSession {
|
|
|
892
975
|
// A fresh session drops any in-flight plan state so its prompt is clean.
|
|
893
976
|
this.planModeRef.current = false;
|
|
894
977
|
this.approvedPlanPath = undefined;
|
|
978
|
+
// Display-only history belongs to the OLD session. Without this, stale Nolan
|
|
979
|
+
// turns / autopilot verdicts / app markers linger in memory, show up in the
|
|
980
|
+
// new session's /history, and get re-persisted into the new file by the
|
|
981
|
+
// next compaction — the cross-session duplicate-marker propagation bug.
|
|
982
|
+
this.nolanTurns = [];
|
|
983
|
+
this.autopilotMarkers = [];
|
|
984
|
+
this.appMarkers = [];
|
|
895
985
|
const basePrompt = this.customSystemPrompt ??
|
|
896
986
|
(await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider));
|
|
897
987
|
this.messages = [{ role: "system", content: basePrompt }];
|
|
898
|
-
|
|
988
|
+
// Fresh conversation — new entries must not chain onto the old DAG's leaf.
|
|
989
|
+
this.currentLeafId = null;
|
|
990
|
+
// Transient sessions (Nolan chat/autopilot, subagent spawns) never touch the
|
|
991
|
+
// session store. Without this guard, autopilot's per-cycle resetReviewer
|
|
992
|
+
// (nolanAutoSession.newSession()) created a real session file EVERY review
|
|
993
|
+
// cycle — the stream of 3-line "## Who you are … Nolan Grout" sessions that
|
|
994
|
+
// polluted the project's session list.
|
|
995
|
+
if (this.opts.transient) {
|
|
996
|
+
this.sessionId = "";
|
|
997
|
+
this.sessionPath = "";
|
|
998
|
+
this.lastPersistedIndex = this.messages.length;
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
await this.createNewSession();
|
|
1002
|
+
}
|
|
899
1003
|
this.eventBus.emit("session_start", { sessionId: this.sessionId });
|
|
900
1004
|
}
|
|
901
1005
|
async loadSession(sessionPath) {
|
|
@@ -1040,6 +1144,12 @@ export class AgentSession {
|
|
|
1040
1144
|
getNolanTurns() {
|
|
1041
1145
|
return this.nolanTurns;
|
|
1042
1146
|
}
|
|
1147
|
+
/** Autopilot verdict markers recorded against this session, in record order.
|
|
1148
|
+
* Used by the host to interleave the auto-review loop's markers back into
|
|
1149
|
+
* the transcript on resume, mirroring `getNolanTurns`. */
|
|
1150
|
+
getAutopilotMarkers() {
|
|
1151
|
+
return this.autopilotMarkers;
|
|
1152
|
+
}
|
|
1043
1153
|
/**
|
|
1044
1154
|
* Record one Nolan Grout (mentor agent) turn against this build session: the
|
|
1045
1155
|
* user's question + Nolan's reply. Kept in memory for the live transcript and
|
|
@@ -1085,6 +1195,103 @@ export class AgentSession {
|
|
|
1085
1195
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1086
1196
|
}
|
|
1087
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Record one autopilot verdict marker (prompted / done / human / capped)
|
|
1200
|
+
* against this build session. Kept in memory for the live transcript and
|
|
1201
|
+
* persisted as a `custom` entry (parentId null, same as Nolan turns) so a
|
|
1202
|
+
* resumed session renders the exact same Nolan bubble the live run showed
|
|
1203
|
+
* instead of dropping the marker or falling back to a raw verdict string.
|
|
1204
|
+
* No-op persistence for transient sessions (kept in memory only).
|
|
1205
|
+
*/
|
|
1206
|
+
async persistAutopilotMarker(phase, extra) {
|
|
1207
|
+
const afterMessageCount = this.messages.filter((m) => m.role !== "system").length;
|
|
1208
|
+
const payload = {
|
|
1209
|
+
version: 1,
|
|
1210
|
+
phase,
|
|
1211
|
+
afterMessageCount,
|
|
1212
|
+
...(extra?.reason !== undefined ? { reason: extra.reason } : {}),
|
|
1213
|
+
...(extra?.body !== undefined ? { body: extra.body } : {}),
|
|
1214
|
+
};
|
|
1215
|
+
this.autopilotMarkers.push(payload);
|
|
1216
|
+
if (!this.sessionPath)
|
|
1217
|
+
return;
|
|
1218
|
+
const entry = {
|
|
1219
|
+
type: "custom",
|
|
1220
|
+
kind: AUTOPILOT_MARKER_CUSTOM_KIND,
|
|
1221
|
+
id: crypto.randomUUID(),
|
|
1222
|
+
parentId: null,
|
|
1223
|
+
timestamp: new Date().toISOString(),
|
|
1224
|
+
data: payload,
|
|
1225
|
+
};
|
|
1226
|
+
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1227
|
+
}
|
|
1228
|
+
/** Re-append the in-memory autopilot markers to the current session file.
|
|
1229
|
+
* Mirrors `rePersistNolanTurns` — called after a continuation/compaction file
|
|
1230
|
+
* is created so the auto-review history survives the rewrite. */
|
|
1231
|
+
async rePersistAutopilotMarkers() {
|
|
1232
|
+
if (!this.sessionPath)
|
|
1233
|
+
return;
|
|
1234
|
+
for (const payload of this.autopilotMarkers) {
|
|
1235
|
+
const entry = {
|
|
1236
|
+
type: "custom",
|
|
1237
|
+
kind: AUTOPILOT_MARKER_CUSTOM_KIND,
|
|
1238
|
+
id: crypto.randomUUID(),
|
|
1239
|
+
parentId: null,
|
|
1240
|
+
timestamp: new Date().toISOString(),
|
|
1241
|
+
data: payload,
|
|
1242
|
+
};
|
|
1243
|
+
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
/** App transcript markers recorded against this session, in record order.
|
|
1247
|
+
* Used by the host to interleave display-only rows (plan banner, task
|
|
1248
|
+
* header, errors, user-bubble hints) back into the transcript on resume. */
|
|
1249
|
+
getAppMarkers() {
|
|
1250
|
+
return this.appMarkers;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Record one app transcript marker (display-only row) against this session.
|
|
1254
|
+
* Same treatment as autopilot markers: kept in memory for the live
|
|
1255
|
+
* transcript, persisted as a `custom` entry (parentId null, never on the
|
|
1256
|
+
* message DAG) so a resumed session shows the identical row. `anchorOffset`
|
|
1257
|
+
* shifts the recorded `afterMessageCount` — pass +1 for a marker that should
|
|
1258
|
+
* attach to the user message about to be pushed by the imminent prompt.
|
|
1259
|
+
* No-op persistence for transient sessions.
|
|
1260
|
+
*/
|
|
1261
|
+
async persistAppMarker(kind, data, anchorOffset = 0) {
|
|
1262
|
+
const afterMessageCount = this.messages.filter((m) => m.role !== "system").length + anchorOffset;
|
|
1263
|
+
const payload = { version: 1, kind, afterMessageCount, data };
|
|
1264
|
+
this.appMarkers.push(payload);
|
|
1265
|
+
if (!this.sessionPath)
|
|
1266
|
+
return;
|
|
1267
|
+
const entry = {
|
|
1268
|
+
type: "custom",
|
|
1269
|
+
kind: APP_MARKER_CUSTOM_KIND,
|
|
1270
|
+
id: crypto.randomUUID(),
|
|
1271
|
+
parentId: null,
|
|
1272
|
+
timestamp: new Date().toISOString(),
|
|
1273
|
+
data: payload,
|
|
1274
|
+
};
|
|
1275
|
+
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1276
|
+
}
|
|
1277
|
+
/** Re-append the in-memory app markers to the current session file. Mirrors
|
|
1278
|
+
* `rePersistNolanTurns` — called after a continuation/compaction file is
|
|
1279
|
+
* created so display-only rows survive the rewrite. */
|
|
1280
|
+
async rePersistAppMarkers() {
|
|
1281
|
+
if (!this.sessionPath)
|
|
1282
|
+
return;
|
|
1283
|
+
for (const payload of this.appMarkers) {
|
|
1284
|
+
const entry = {
|
|
1285
|
+
type: "custom",
|
|
1286
|
+
kind: APP_MARKER_CUSTOM_KIND,
|
|
1287
|
+
id: crypto.randomUUID(),
|
|
1288
|
+
parentId: null,
|
|
1289
|
+
timestamp: new Date().toISOString(),
|
|
1290
|
+
data: payload,
|
|
1291
|
+
};
|
|
1292
|
+
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1088
1295
|
/**
|
|
1089
1296
|
* Generate a short LLM session title from the conversation so far (first user
|
|
1090
1297
|
* message + first assistant reply). Best-effort; returns null on failure or
|
|
@@ -1165,9 +1372,11 @@ export class AgentSession {
|
|
|
1165
1372
|
setSignal(signal) {
|
|
1166
1373
|
this.opts = { ...this.opts, signal };
|
|
1167
1374
|
}
|
|
1168
|
-
/** True when speedProfile is "optimized" (1-h cache TTL + pre-warm)
|
|
1375
|
+
/** True when speedProfile is "optimized" (1-h cache TTL + pre-warm), or the
|
|
1376
|
+
* session was constructed with `forceLongCacheRetention` (Nolan sessions). */
|
|
1169
1377
|
isSpeedOptimized() {
|
|
1170
|
-
return this.
|
|
1378
|
+
return (this.opts.forceLongCacheRetention === true ||
|
|
1379
|
+
this.settingsManager?.get("speedProfile") === "optimized");
|
|
1171
1380
|
}
|
|
1172
1381
|
/**
|
|
1173
1382
|
* Ordered auth-storage keys the current (provider, model) pair tries, first
|
|
@@ -1247,6 +1456,10 @@ export class AgentSession {
|
|
|
1247
1456
|
// Restore Nolan's advisory turns (custom entries, not on the message branch) so
|
|
1248
1457
|
// they reappear in the transcript and survive into the continuation file.
|
|
1249
1458
|
this.nolanTurns = this.sessionManager.getNolanTurns(loaded.entries);
|
|
1459
|
+
// Restore autopilot verdict markers the same way (not on the message DAG).
|
|
1460
|
+
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries);
|
|
1461
|
+
// Restore app transcript markers (plan banner / task header / errors / hints).
|
|
1462
|
+
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries);
|
|
1250
1463
|
// Track the current leaf for subsequent entries
|
|
1251
1464
|
this.currentLeafId = loaded.header.leafId;
|
|
1252
1465
|
// Rebuild messages: keep system, add loaded
|
|
@@ -1282,20 +1495,40 @@ export class AgentSession {
|
|
|
1282
1495
|
before: String(compacted.result.originalCount),
|
|
1283
1496
|
after: String(compacted.result.newCount),
|
|
1284
1497
|
});
|
|
1498
|
+
// Compaction rewrote history, so the on-disk file no longer reflects
|
|
1499
|
+
// what's in memory — fork a fresh session file for the compacted state
|
|
1500
|
+
// (mirrors compact()'s own persistence) so `ezcoder continue` picks up
|
|
1501
|
+
// the summary instead of the full original transcript.
|
|
1502
|
+
const session = await this.sessionManager.create(this.cwd, this.provider, this.model);
|
|
1503
|
+
this.sessionId = session.id;
|
|
1504
|
+
this.sessionPath = session.path;
|
|
1505
|
+
this.currentLeafId = null;
|
|
1506
|
+
// Re-persist (compacted) messages — skip system, it's rebuilt on load
|
|
1507
|
+
for (const msg of this.messages) {
|
|
1508
|
+
if (msg.role === "system")
|
|
1509
|
+
continue;
|
|
1510
|
+
await this.persistMessage(msg);
|
|
1511
|
+
}
|
|
1512
|
+
this.lastPersistedIndex = this.messages.length;
|
|
1513
|
+
// Carry Nolan's restored turns into the continuation file.
|
|
1514
|
+
await this.rePersistNolanTurns();
|
|
1515
|
+
await this.rePersistAutopilotMarkers();
|
|
1516
|
+
await this.rePersistAppMarkers();
|
|
1517
|
+
// Record this load-time auto-compaction's counts for the resumed notice.
|
|
1518
|
+
await this.persistAppMarker("compaction", {
|
|
1519
|
+
originalCount: compacted.result.originalCount,
|
|
1520
|
+
newCount: compacted.result.newCount,
|
|
1521
|
+
});
|
|
1522
|
+
return;
|
|
1285
1523
|
}
|
|
1286
|
-
//
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
//
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
continue;
|
|
1294
|
-
await this.persistMessage(msg);
|
|
1295
|
-
}
|
|
1524
|
+
// Plain resume (no compaction needed): keep using the original session
|
|
1525
|
+
// file/id and append future turns to it in place. Forking a new file here
|
|
1526
|
+
// unconditionally used to create a byte-identical duplicate every time a
|
|
1527
|
+
// session was merely reopened (e.g. app/window restart) with zero new
|
|
1528
|
+
// messages in between — the duplicate entries seen in the session list.
|
|
1529
|
+
this.sessionId = loaded.header.id;
|
|
1530
|
+
this.sessionPath = sessionPath;
|
|
1296
1531
|
this.lastPersistedIndex = this.messages.length;
|
|
1297
|
-
// Carry Nolan's restored turns into the continuation file.
|
|
1298
|
-
await this.rePersistNolanTurns();
|
|
1299
1532
|
}
|
|
1300
1533
|
async prepareDynamicContext(_latestUserPrompt) {
|
|
1301
1534
|
return this.messages;
|