@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.3
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/CHANGELOG.md +61 -0
- package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-c5hpqt9r.md} +61 -0
- package/dist/cli.js +3856 -3850
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +16 -5
- package/dist/types/edit/hashline/block-resolver.d.ts +5 -5
- package/dist/types/edit/hashline/diff.d.ts +4 -4
- package/dist/types/eval/executor-base.d.ts +27 -0
- package/dist/types/eval/js/context-manager.d.ts +7 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +64 -5
- package/dist/types/extensibility/extensions/types.d.ts +15 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/live/visualizer.d.ts +3 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -3
- package/dist/types/modes/components/ask-dialog.d.ts +0 -1
- package/dist/types/modes/components/keybinding-hints.d.ts +1 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +13 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +22 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/codex-auto-reset.d.ts +182 -64
- package/dist/types/session/session-storage.d.ts +5 -2
- package/dist/types/session/session-tools.d.ts +2 -1
- package/dist/types/session/turn-recovery.d.ts +7 -1
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +27 -0
- package/dist/types/task/persisted-revive.d.ts +7 -0
- package/dist/types/tools/bash-interceptor.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +9 -1
- package/dist/types/tools/browser/launch.d.ts +50 -1
- package/dist/types/tools/browser/registry.d.ts +7 -0
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +8 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/shell-tokenize.d.ts +11 -0
- package/dist/types/utils/image-loading.d.ts +2 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/src/commit/agentic/index.ts +2 -1
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +6 -1
- package/src/config/settings-schema.ts +16 -3
- package/src/edit/hashline/block-resolver.ts +5 -5
- package/src/edit/hashline/diff.ts +22 -15
- package/src/edit/hashline/execute.ts +14 -13
- package/src/edit/renderer.ts +3 -3
- package/src/eval/executor-base.ts +39 -0
- package/src/eval/jl/executor.ts +13 -2
- package/src/eval/js/context-manager.ts +97 -14
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/py/executor.ts +55 -15
- package/src/eval/rb/executor.ts +8 -1
- package/src/extensibility/extensions/runner.ts +121 -4
- package/src/extensibility/extensions/types.ts +15 -0
- package/src/extensibility/extensions/wrapper.ts +19 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +5 -5
- package/src/launch/broker.ts +59 -41
- package/src/live/visualizer.ts +8 -1
- package/src/lsp/index.ts +51 -11
- package/src/main.ts +3 -0
- package/src/modes/acp/acp-agent.ts +16 -5
- package/src/modes/components/ask-dialog.ts +65 -23
- package/src/modes/components/assistant-message.ts +5 -10
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/keybinding-hints.ts +4 -13
- package/src/modes/components/status-line/segments.ts +4 -5
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/controllers/input-controller.ts +18 -40
- package/src/modes/controllers/live-command-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +28 -11
- package/src/modes/interactive-mode.ts +59 -2
- package/src/modes/theme/theme.ts +13 -6
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/sdk.ts +47 -16
- package/src/secrets/index.ts +83 -3
- package/src/secrets/obfuscator.ts +70 -15
- package/src/session/agent-session-types.ts +7 -1
- package/src/session/agent-session.ts +394 -113
- package/src/session/codex-auto-reset.ts +594 -123
- package/src/session/eval-runner.ts +2 -0
- package/src/session/messages.ts +1 -1
- package/src/session/session-maintenance.ts +50 -3
- package/src/session/session-manager.ts +4 -3
- package/src/session/session-stats.ts +14 -3
- package/src/session/session-storage.ts +47 -15
- package/src/session/session-tools.ts +30 -2
- package/src/session/turn-recovery.ts +15 -2
- package/src/slash-commands/builtin-registry.ts +21 -15
- package/src/slash-commands/helpers/reset-usage.ts +2 -0
- package/src/system-prompt.ts +13 -1
- package/src/task/executor.ts +168 -15
- package/src/task/persisted-revive.ts +26 -1
- package/src/telemetry-export.ts +8 -4
- package/src/tools/bash-interceptor.ts +85 -8
- package/src/tools/bash.ts +12 -6
- package/src/tools/browser/attach.ts +35 -10
- package/src/tools/browser/launch.ts +110 -16
- package/src/tools/browser/registry.ts +140 -31
- package/src/tools/browser/shared-daemon.ts +190 -0
- package/src/tools/browser/tab-protocol.ts +8 -0
- package/src/tools/browser/tab-supervisor.ts +31 -4
- package/src/tools/browser/tab-worker.ts +32 -2
- package/src/tools/browser.ts +1 -1
- package/src/tools/output-meta.ts +16 -1
- package/src/tools/shell-tokenize.ts +101 -0
- package/src/utils/image-loading.ts +8 -3
- package/src/utils/shell-snapshot.ts +31 -21
- package/src/web/search/providers/codex.ts +31 -6
- package/src/web/search/providers/duckduckgo.ts +183 -15
- package/src/web/search/types.ts +6 -1
|
@@ -2,6 +2,7 @@ import type { Agent } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { Settings } from "../config/settings";
|
|
4
4
|
import { disposeJuliaKernelSessionsByOwner } from "../eval/jl/executor";
|
|
5
|
+
import { disposeVmContextsByOwner } from "../eval/js/context-manager";
|
|
5
6
|
import { namespaceSessionId as namespacePythonSessionId } from "../eval/py";
|
|
6
7
|
import {
|
|
7
8
|
disposeKernelSessionsByOwner,
|
|
@@ -181,6 +182,7 @@ export class EvalRunner {
|
|
|
181
182
|
disposeKernelSessionsByOwner(this.#kernelOwnerId),
|
|
182
183
|
disposeRubyKernelSessionsByOwner(this.#kernelOwnerId),
|
|
183
184
|
disposeJuliaKernelSessionsByOwner(this.#kernelOwnerId),
|
|
185
|
+
disposeVmContextsByOwner(this.#kernelOwnerId),
|
|
184
186
|
]);
|
|
185
187
|
const errors: unknown[] = [];
|
|
186
188
|
for (const result of results) if (result.status === "rejected") errors.push(result.reason);
|
package/src/session/messages.ts
CHANGED
|
@@ -1170,7 +1170,7 @@ function convertOne(m: AgentMessage, interruptedNext: boolean): Message[] {
|
|
|
1170
1170
|
}
|
|
1171
1171
|
case "assistant": {
|
|
1172
1172
|
// A user-interrupted turn keeps its trailing thinking run on the
|
|
1173
|
-
// persisted/displayed message so reload and
|
|
1173
|
+
// persisted/displayed message so reload and display-reset rebuilds still
|
|
1174
1174
|
// show it. That run is incomplete/unsigned and gets rejected on
|
|
1175
1175
|
// resend, so strip it here — LLM path only — when the hidden
|
|
1176
1176
|
// interrupted-thinking continuity message follows.
|
|
@@ -268,6 +268,19 @@ export interface SessionMaintenanceHost {
|
|
|
268
268
|
export class SessionMaintenance {
|
|
269
269
|
#compactionAbortController: AbortController | undefined;
|
|
270
270
|
#autoCompactionAbortController: AbortController | undefined;
|
|
271
|
+
/**
|
|
272
|
+
* Live tool-loop contexts parked after mid-turn maintenance hit a no-progress
|
|
273
|
+
* dead end. Membership suppresses the repeated rescue + warning while no cut
|
|
274
|
+
* point exists; {@link maintainContextMidRun} re-arms the entry once a later
|
|
275
|
+
* tool result makes `prepareCompaction` viable again.
|
|
276
|
+
*/
|
|
277
|
+
readonly #midTurnCompactionDeadEnds = new WeakSet<AgentMessage[]>();
|
|
278
|
+
/**
|
|
279
|
+
* Carries a mid-turn dead end across the loop's final answer to the next
|
|
280
|
+
* pre-prompt check. That check must not warn again for the same oversized
|
|
281
|
+
* persisted turn, but a new agent loop still gets its own live-array guard.
|
|
282
|
+
*/
|
|
283
|
+
#midTurnDeadEndPendingPrePrompt = false;
|
|
271
284
|
#skipPostTurnMaintenanceAssistantTimestamp: number | undefined;
|
|
272
285
|
readonly #host: SessionMaintenanceHost;
|
|
273
286
|
|
|
@@ -968,7 +981,18 @@ export class SessionMaintenance {
|
|
|
968
981
|
if (contextWindow <= 0) return;
|
|
969
982
|
const compactionSettings = this.#host.settings.getGroup("compaction");
|
|
970
983
|
const contextTokens = this.#estimatePrePromptContextTokens(messages, contextWindow);
|
|
984
|
+
const pendingMidTurnDeadEnd = this.#midTurnDeadEndPendingPrePrompt;
|
|
985
|
+
this.#midTurnDeadEndPendingPrePrompt = false;
|
|
971
986
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
|
|
987
|
+
if (
|
|
988
|
+
pendingMidTurnDeadEnd &&
|
|
989
|
+
prepareCompaction(this.#host.sessionManager.getBranch(), compactionSettings, model) === undefined
|
|
990
|
+
) {
|
|
991
|
+
// The prior tool loop already attempted the rescue and warned for this
|
|
992
|
+
// persisted oversized turn. Only a later persisted cut point makes a
|
|
993
|
+
// pre-prompt retry useful; the new agent loop may warn for its own turn.
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
972
996
|
|
|
973
997
|
// Auto-promote first: switching to a larger-context model avoids compacting
|
|
974
998
|
// the history at all. The post-turn threshold path already promotes before
|
|
@@ -1039,6 +1063,25 @@ export class SessionMaintenance {
|
|
|
1039
1063
|
if (!lastAssistant || lastAssistant.stopReason === "aborted" || lastAssistant.stopReason === "error") return;
|
|
1040
1064
|
|
|
1041
1065
|
if (!(await this.#host.persistTurnMessagesForMidRunCompaction(context))) return;
|
|
1066
|
+
if (this.#midTurnCompactionDeadEnds.has(activeMessages)) {
|
|
1067
|
+
// A prior boundary already ran the dead-end rescue and could not reduce
|
|
1068
|
+
// this turn. Re-running the rescue and re-emitting its warning on every
|
|
1069
|
+
// following tool boundary is wasted work while nothing summarizable
|
|
1070
|
+
// exists. But the tool loop keeps appending turns: once a later
|
|
1071
|
+
// (smaller) tool result gives prepareCompaction a cut point before the
|
|
1072
|
+
// now-older oversized turn, compaction can finally make progress and
|
|
1073
|
+
// MUST run rather than stay suppressed until provider overflow (#7153
|
|
1074
|
+
// review). Stay parked only while no cut point is available; re-arm as
|
|
1075
|
+
// soon as one appears.
|
|
1076
|
+
if (
|
|
1077
|
+
!model ||
|
|
1078
|
+
prepareCompaction(this.#host.sessionManager.getBranch(), compactionSettings, model) === undefined
|
|
1079
|
+
) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
this.#midTurnCompactionDeadEnds.delete(activeMessages);
|
|
1083
|
+
this.#midTurnDeadEndPendingPrePrompt = false;
|
|
1084
|
+
}
|
|
1042
1085
|
|
|
1043
1086
|
const billedContextTokens = calculateContextTokens(lastAssistant.usage);
|
|
1044
1087
|
const storedContextTokens = this.#estimateStoredContextTokens();
|
|
@@ -1061,13 +1104,17 @@ export class SessionMaintenance {
|
|
|
1061
1104
|
}
|
|
1062
1105
|
|
|
1063
1106
|
const messagesBefore = activeMessages.length;
|
|
1064
|
-
await this.runAutoCompaction("threshold", false, false, false, {
|
|
1107
|
+
const result = await this.runAutoCompaction("threshold", false, false, false, {
|
|
1065
1108
|
autoContinue: false,
|
|
1066
1109
|
suppressContinuation: true,
|
|
1067
1110
|
suppressHandoff: true,
|
|
1068
1111
|
triggerContextTokens: contextTokens,
|
|
1069
1112
|
phase: "mid_turn",
|
|
1070
1113
|
});
|
|
1114
|
+
if (result.automaticContinuationBlocked) {
|
|
1115
|
+
this.#midTurnCompactionDeadEnds.add(activeMessages);
|
|
1116
|
+
this.#midTurnDeadEndPendingPrePrompt = true;
|
|
1117
|
+
}
|
|
1071
1118
|
|
|
1072
1119
|
if (signal?.aborted) return;
|
|
1073
1120
|
const compactedMessages = this.#host.agent.state.messages;
|
|
@@ -1492,7 +1539,7 @@ export class SessionMaintenance {
|
|
|
1492
1539
|
thinkingLevel: this.#host.thinkingLevel(),
|
|
1493
1540
|
tools: this.#host.agent.state.tools,
|
|
1494
1541
|
sessionId: this.#host.sessionId(),
|
|
1495
|
-
promptCacheKey: this.#host.sessionId
|
|
1542
|
+
promptCacheKey: this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId,
|
|
1496
1543
|
providerSessionState: this.#host.providerSessionState,
|
|
1497
1544
|
// Route every summarization HTTP request through the
|
|
1498
1545
|
// session's side-stream transport so the provider
|
|
@@ -2538,7 +2585,7 @@ export class SessionMaintenance {
|
|
|
2538
2585
|
thinkingLevel: this.#host.thinkingLevel(),
|
|
2539
2586
|
tools: this.#host.agent.state.tools,
|
|
2540
2587
|
sessionId: this.#host.sessionId(),
|
|
2541
|
-
promptCacheKey: this.#host.sessionId
|
|
2588
|
+
promptCacheKey: this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId,
|
|
2542
2589
|
providerSessionState: this.#host.providerSessionState,
|
|
2543
2590
|
codexCompaction,
|
|
2544
2591
|
},
|
|
@@ -870,9 +870,9 @@ export class SessionManager {
|
|
|
870
870
|
return;
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
-
// Hot path:
|
|
874
|
-
//
|
|
875
|
-
//
|
|
873
|
+
// Hot path: queue the entry directly on the writer, outside the async disk
|
|
874
|
+
// chain. File writers batch same-turn appends until their microtask
|
|
875
|
+
// boundary; flush(), flushSync(), and close() drain that batch explicitly.
|
|
876
876
|
// A mid-close writer leaves `#writer` undefined, so `#appendWriter` simply
|
|
877
877
|
// opens a fresh append handle and the entry still lands.
|
|
878
878
|
try {
|
|
@@ -1555,6 +1555,7 @@ export class SessionManager {
|
|
|
1555
1555
|
if (this.#atomicEntryBatch) throw new Error("Cannot synchronously flush during an atomic session batch.");
|
|
1556
1556
|
if (this.#diskFailure) throw this.#diskFailure;
|
|
1557
1557
|
if (this.#fileIsCurrent && !this.#rewriteRequired) {
|
|
1558
|
+
this.#writer?.flushSync?.();
|
|
1558
1559
|
const writerError = this.#writer?.getError();
|
|
1559
1560
|
if (writerError) throw writerError;
|
|
1560
1561
|
return;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Agent, AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
calculatePromptTokens,
|
|
4
|
+
estimateTokens,
|
|
5
|
+
hasContextTokenUsage,
|
|
6
|
+
type SessionMessageEntry,
|
|
7
|
+
} from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
8
|
import type { AssistantMessage, Model, ProviderResponseMetadata, Usage } from "@oh-my-pi/pi-ai";
|
|
4
9
|
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
5
10
|
import type { ModelRegistry } from "../config/model-registry";
|
|
@@ -127,7 +132,12 @@ export class SessionStatsTracker {
|
|
|
127
132
|
const entry = branchEntries[index];
|
|
128
133
|
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
129
134
|
const assistant = entry.message;
|
|
130
|
-
if (
|
|
135
|
+
if (
|
|
136
|
+
assistant.stopReason !== "aborted" &&
|
|
137
|
+
assistant.stopReason !== "error" &&
|
|
138
|
+
assistant.usage &&
|
|
139
|
+
hasContextTokenUsage(assistant.usage)
|
|
140
|
+
) {
|
|
131
141
|
anchorEntry = entry;
|
|
132
142
|
break;
|
|
133
143
|
}
|
|
@@ -184,7 +194,8 @@ export class SessionStatsTracker {
|
|
|
184
194
|
message.role !== "assistant" ||
|
|
185
195
|
message.stopReason === "aborted" ||
|
|
186
196
|
message.stopReason === "error" ||
|
|
187
|
-
!message.usage
|
|
197
|
+
!message.usage ||
|
|
198
|
+
!hasContextTokenUsage(message.usage)
|
|
188
199
|
) {
|
|
189
200
|
continue;
|
|
190
201
|
}
|
|
@@ -14,14 +14,17 @@ export interface SessionStorageStat {
|
|
|
14
14
|
|
|
15
15
|
export interface SessionStorageWriter {
|
|
16
16
|
/**
|
|
17
|
-
* Append one newline-terminated line. File
|
|
18
|
-
*
|
|
17
|
+
* Append one newline-terminated line. File storage batches same-turn appends
|
|
18
|
+
* until its microtask boundary; memory storage updates in-body; indexed
|
|
19
|
+
* backends queue in call order.
|
|
19
20
|
*
|
|
20
21
|
* `line` MUST include the trailing newline.
|
|
21
22
|
*/
|
|
22
23
|
append(line: string): Promise<void>;
|
|
23
24
|
/** Resolve once all queued appends complete. No fsync. */
|
|
24
25
|
flush(): Promise<void>;
|
|
26
|
+
/** Drain synchronously flushable queued work when the backend supports it. No fsync. */
|
|
27
|
+
flushSync?(): void;
|
|
25
28
|
/** False once close() has begun/finished. */
|
|
26
29
|
isOpen(): boolean;
|
|
27
30
|
close(): Promise<void>;
|
|
@@ -90,6 +93,8 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
90
93
|
#closed = false;
|
|
91
94
|
#error: Error | undefined;
|
|
92
95
|
#onError: ((err: Error) => void) | undefined;
|
|
96
|
+
#pending = "";
|
|
97
|
+
#flushScheduled = false;
|
|
93
98
|
|
|
94
99
|
constructor(fpath: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
|
|
95
100
|
this.#onError = options?.onError;
|
|
@@ -112,25 +117,46 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
112
117
|
return error;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const written = fs.writeSync(this.#fd, buf, offset, buf.length - offset);
|
|
123
|
-
if (written === 0) {
|
|
124
|
-
throw new Error("Short write");
|
|
125
|
-
}
|
|
126
|
-
offset += written;
|
|
120
|
+
#writeNow(line: string): void {
|
|
121
|
+
const buf = Buffer.from(line, "utf-8");
|
|
122
|
+
let offset = 0;
|
|
123
|
+
while (offset < buf.length) {
|
|
124
|
+
const written = fs.writeSync(this.#fd, buf, offset, buf.length - offset);
|
|
125
|
+
if (written === 0) {
|
|
126
|
+
throw new Error("Short write");
|
|
127
127
|
}
|
|
128
|
+
offset += written;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#flushPendingNow(): void {
|
|
133
|
+
this.#flushScheduled = false;
|
|
134
|
+
if (this.#pending.length === 0) return;
|
|
135
|
+
const pending = this.#pending;
|
|
136
|
+
this.#pending = "";
|
|
137
|
+
try {
|
|
138
|
+
this.#writeNow(pending);
|
|
128
139
|
} catch (err) {
|
|
129
|
-
|
|
140
|
+
this.#recordError(err);
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
|
|
144
|
+
async append(line: string): Promise<void> {
|
|
145
|
+
if (this.#closed) throw new Error("Writer closed");
|
|
146
|
+
if (this.#error) throw this.#error;
|
|
147
|
+
this.#pending += line;
|
|
148
|
+
if (this.#flushScheduled) return;
|
|
149
|
+
this.#flushScheduled = true;
|
|
150
|
+
queueMicrotask(() => this.#flushPendingNow());
|
|
151
|
+
}
|
|
152
|
+
|
|
133
153
|
async flush(): Promise<void> {
|
|
154
|
+
this.#flushPendingNow();
|
|
155
|
+
if (this.#error) throw this.#error;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
flushSync(): void {
|
|
159
|
+
this.#flushPendingNow();
|
|
134
160
|
if (this.#error) throw this.#error;
|
|
135
161
|
}
|
|
136
162
|
|
|
@@ -140,6 +166,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
140
166
|
|
|
141
167
|
async close(): Promise<void> {
|
|
142
168
|
if (this.#closed) return;
|
|
169
|
+
this.#flushPendingNow();
|
|
143
170
|
this.#closed = true;
|
|
144
171
|
// Unregister from finalization - we're closing properly
|
|
145
172
|
writerRegistry.unregister(this);
|
|
@@ -148,6 +175,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
148
175
|
} catch {
|
|
149
176
|
// Ignore close errors
|
|
150
177
|
}
|
|
178
|
+
if (this.#error) throw this.#error;
|
|
151
179
|
}
|
|
152
180
|
|
|
153
181
|
getError(): Error | undefined {
|
|
@@ -474,6 +502,10 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
474
502
|
if (this.#error) throw this.#error;
|
|
475
503
|
}
|
|
476
504
|
|
|
505
|
+
flushSync(): void {
|
|
506
|
+
if (this.#error) throw this.#error;
|
|
507
|
+
}
|
|
508
|
+
|
|
477
509
|
isOpen(): boolean {
|
|
478
510
|
return !this.#closed;
|
|
479
511
|
}
|
|
@@ -72,7 +72,10 @@ interface SessionToolsOptions {
|
|
|
72
72
|
builtInToolNames?: Iterable<string>;
|
|
73
73
|
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
74
74
|
ensureWriteRegistered?: () => Promise<boolean>;
|
|
75
|
-
rebuildSystemPrompt?: (
|
|
75
|
+
rebuildSystemPrompt?: (
|
|
76
|
+
toolNames: string[],
|
|
77
|
+
tools: Map<string, AgentTool>,
|
|
78
|
+
) => Promise<{ systemPrompt: string[]; xdevCatalogNames?: readonly string[] }>;
|
|
76
79
|
getLocalCalendarDate?: () => string;
|
|
77
80
|
getMcpServerInstructions?: () => Map<string, string> | undefined;
|
|
78
81
|
xdev?: XdevState;
|
|
@@ -196,6 +199,14 @@ export class SessionTools {
|
|
|
196
199
|
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
197
200
|
#baseSystemPrompt: string[];
|
|
198
201
|
#lastAppliedToolSignature: string | undefined;
|
|
202
|
+
/**
|
|
203
|
+
* `xd://` device names the current base system prompt renders in its catalog
|
|
204
|
+
* (the last rebuild's {@link BuildSystemPromptResult.xdevCatalogNames}). Consulted
|
|
205
|
+
* when a pending mount notice is delivered: a device the outgoing prompt already
|
|
206
|
+
* lists is recorded as announced without a redundant notice line. Empty when the
|
|
207
|
+
* prompt carries no catalog (no mounts, or a custom prompt that omits the section).
|
|
208
|
+
*/
|
|
209
|
+
#basePromptXdevNames: ReadonlySet<string> = new Set();
|
|
199
210
|
#mcpRefreshTail: Promise<void> = Promise.resolve();
|
|
200
211
|
#promptModelKey: string | undefined;
|
|
201
212
|
#rebuildSystemPrompt: SessionToolsOptions["rebuildSystemPrompt"];
|
|
@@ -619,6 +630,7 @@ export class SessionTools {
|
|
|
619
630
|
|
|
620
631
|
let rebuiltSystemPrompt: string[] | undefined;
|
|
621
632
|
let rebuiltSignature: string | undefined;
|
|
633
|
+
let rebuiltXdevCatalogNames: readonly string[] | undefined;
|
|
622
634
|
try {
|
|
623
635
|
if (this.#rebuildSystemPrompt) {
|
|
624
636
|
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
|
@@ -626,6 +638,7 @@ export class SessionTools {
|
|
|
626
638
|
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
627
639
|
rebuiltSystemPrompt = built.systemPrompt;
|
|
628
640
|
rebuiltSignature = signature;
|
|
641
|
+
rebuiltXdevCatalogNames = built.xdevCatalogNames;
|
|
629
642
|
}
|
|
630
643
|
}
|
|
631
644
|
} catch (error) {
|
|
@@ -649,6 +662,7 @@ export class SessionTools {
|
|
|
649
662
|
this.#host.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
650
663
|
this.#lastAppliedToolSignature = rebuiltSignature;
|
|
651
664
|
this.#promptModelKey = this.#currentPromptModelKey();
|
|
665
|
+
this.#basePromptXdevNames = new Set(rebuiltXdevCatalogNames);
|
|
652
666
|
}
|
|
653
667
|
}
|
|
654
668
|
|
|
@@ -765,11 +779,24 @@ export class SessionTools {
|
|
|
765
779
|
}
|
|
766
780
|
|
|
767
781
|
/** Consumes the hidden notice for unannounced `xd://` mount changes. */
|
|
768
|
-
takePendingXdevMountNotice(): CustomMessage<XdevMountNoticeDetails> | undefined {
|
|
782
|
+
takePendingXdevMountNotice(baseCatalogDelivered: boolean): CustomMessage<XdevMountNoticeDetails> | undefined {
|
|
769
783
|
const pending = this.#pendingXdevMountDelta;
|
|
770
784
|
if (!pending) return undefined;
|
|
771
785
|
this.#pendingXdevMountDelta = undefined;
|
|
772
786
|
this.#ensureAnnouncedMountsSeeded();
|
|
787
|
+
// A pending add for a device the outgoing base prompt already lists in its
|
|
788
|
+
// catalog needs no notice line — but only when the final provider prompt
|
|
789
|
+
// still carries that base catalog. A `before_agent_start` replacement drops
|
|
790
|
+
// it, so its additions must remain in the notice. Record prompt-carried
|
|
791
|
+
// devices announced here, after the final prompt is known and immediately
|
|
792
|
+
// before delivery. The pending delta remains untouched by rebuilds, letting
|
|
793
|
+
// {@link #notifyXdevMountDelta} cancel a mount followed by an unmount before
|
|
794
|
+
// any request is sent (issue #7139 reviews).
|
|
795
|
+
if (baseCatalogDelivered) {
|
|
796
|
+
for (const name of pending.added) {
|
|
797
|
+
if (this.#basePromptXdevNames.has(name)) this.#announcedMounts.add(name);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
773
800
|
// Only announce a net change relative to what the model already knows (from
|
|
774
801
|
// this session and persisted history): a re-mount of an already-announced
|
|
775
802
|
// device — the common resume/reconnect case — and an unmount for a device
|
|
@@ -1066,6 +1093,7 @@ export class SessionTools {
|
|
|
1066
1093
|
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
1067
1094
|
if (this.#host.isDisposed()) return;
|
|
1068
1095
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
1096
|
+
this.#basePromptXdevNames = new Set(built.xdevCatalogNames);
|
|
1069
1097
|
this.#host.clearMemoryPromotionSnapshot();
|
|
1070
1098
|
if (
|
|
1071
1099
|
previousBaseSystemPrompt.length !== this.#baseSystemPrompt.length ||
|
|
@@ -121,7 +121,13 @@ export interface TurnRecoveryHost {
|
|
|
121
121
|
sessionMessageAlreadyPersisted(message: AssistantMessage): boolean;
|
|
122
122
|
setModelWithProviderSessionReset(model: Model): Promise<void>;
|
|
123
123
|
resetCurrentResponsesProviderSession(reason: string): void;
|
|
124
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Spend a saved Codex reset for the blocked pool, if eligible.
|
|
126
|
+
* `activeBlockUnblockAtMs` is the absolute unblock time parsed from the
|
|
127
|
+
* live usage-limit error — authoritative for the active account when the
|
|
128
|
+
* usage report still shows a pre-block snapshot.
|
|
129
|
+
*/
|
|
130
|
+
maybeAutoRedeemCodexReset(activeBlockUnblockAtMs?: number): Promise<boolean>;
|
|
125
131
|
runAutoCompaction(
|
|
126
132
|
reason: "overflow" | "threshold" | "idle" | "incomplete",
|
|
127
133
|
willRetry: boolean,
|
|
@@ -1390,7 +1396,14 @@ export class TurnRecovery {
|
|
|
1390
1396
|
}
|
|
1391
1397
|
|
|
1392
1398
|
if (!retryBudgetExhausted && !staleOpenAIResponsesReplayError && recordedUsageLimitOutcome) {
|
|
1393
|
-
if (
|
|
1399
|
+
if (
|
|
1400
|
+
recordedUsageLimitOutcome.switchedCredential ||
|
|
1401
|
+
// Convert the parsed hint to an absolute timestamp NOW, before the
|
|
1402
|
+
// hook's usage IO — a duration re-anchored after slow fetches drifts.
|
|
1403
|
+
(await this.#host.maybeAutoRedeemCodexReset(
|
|
1404
|
+
parsedRetryAfterMs === undefined ? undefined : Date.now() + parsedRetryAfterMs,
|
|
1405
|
+
))
|
|
1406
|
+
) {
|
|
1394
1407
|
switchedCredential = true;
|
|
1395
1408
|
delayMs = 0;
|
|
1396
1409
|
} else {
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
import { readMCPConfigFile } from "../mcp/config-writer";
|
|
35
35
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
36
36
|
import { runPauseScreen } from "../modes/components/pause-screen";
|
|
37
|
-
import { collectMcpServerNames } from "../modes/controllers/mcp-command-controller";
|
|
37
|
+
import { collectMcpServerNames, MCPCommandController } from "../modes/controllers/mcp-command-controller";
|
|
38
38
|
import { describeLoopLimitRuntime } from "../modes/loop-limit";
|
|
39
39
|
import { theme } from "../modes/theme/theme";
|
|
40
40
|
import type { InteractiveModeContext } from "../modes/types";
|
|
@@ -2628,13 +2628,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
2628
2628
|
return commandConsumed();
|
|
2629
2629
|
},
|
|
2630
2630
|
handleTui: async (_command, runtime) => {
|
|
2631
|
-
|
|
2632
|
-
// listClaudePluginRoots re-reads from disk on next access.
|
|
2633
|
-
const projectPath = await resolveActiveProjectRegistryPath(runtime.ctx.sessionManager.getCwd());
|
|
2634
|
-
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
2635
|
-
await runtime.ctx.refreshSkillState();
|
|
2636
|
-
await runtime.ctx.refreshSlashCommandState();
|
|
2637
|
-
resetCapabilities();
|
|
2631
|
+
await reloadTuiPluginState(runtime.ctx);
|
|
2638
2632
|
runtime.ctx.showStatus("Plugins reloaded.");
|
|
2639
2633
|
runtime.ctx.editor.setText("");
|
|
2640
2634
|
},
|
|
@@ -3059,6 +3053,24 @@ export function buildTuiBuiltinSlashCommands(runtime: TuiSlashCommandRuntime): R
|
|
|
3059
3053
|
*/
|
|
3060
3054
|
export const BUILTIN_SLASH_COMMANDS_INTERNAL: ReadonlyArray<SlashCommandSpec> = BUILTIN_SLASH_COMMAND_REGISTRY;
|
|
3061
3055
|
|
|
3056
|
+
/**
|
|
3057
|
+
* Reload the interactive session's plugin runtime: invalidate fs/plugin-root
|
|
3058
|
+
* caches, rediscover skills and file slash commands, reset the capability
|
|
3059
|
+
* cache, and reconnect MCP servers (rebinding the session's MCP tools). Shared
|
|
3060
|
+
* by `/reload-plugins`'s TUI handler and the `handle`-adapter's `reloadPlugins`
|
|
3061
|
+
* hook so both honor the command's documented MCP reload scope (#7189).
|
|
3062
|
+
*/
|
|
3063
|
+
async function reloadTuiPluginState(ctx: InteractiveModeContext): Promise<void> {
|
|
3064
|
+
const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
|
|
3065
|
+
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
3066
|
+
await ctx.refreshSkillState();
|
|
3067
|
+
await ctx.refreshSlashCommandState();
|
|
3068
|
+
resetCapabilities();
|
|
3069
|
+
if (ctx.mcpManager) {
|
|
3070
|
+
await new MCPCommandController(ctx).reloadServers();
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3062
3074
|
/**
|
|
3063
3075
|
* Execute a builtin slash command in the interactive TUI.
|
|
3064
3076
|
*
|
|
@@ -3107,13 +3119,7 @@ export async function executeBuiltinSlashCommand(
|
|
|
3107
3119
|
ctx.showStatus(text);
|
|
3108
3120
|
},
|
|
3109
3121
|
refreshCommands: () => ctx.refreshSlashCommandState(),
|
|
3110
|
-
reloadPlugins:
|
|
3111
|
-
const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
|
|
3112
|
-
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
3113
|
-
await ctx.refreshSkillState();
|
|
3114
|
-
await ctx.refreshSlashCommandState();
|
|
3115
|
-
resetCapabilities();
|
|
3116
|
-
},
|
|
3122
|
+
reloadPlugins: () => reloadTuiPluginState(ctx),
|
|
3117
3123
|
};
|
|
3118
3124
|
const result = await command.handle(parsed, adapted);
|
|
3119
3125
|
ctx.editor.setText("");
|
|
@@ -54,6 +54,8 @@ export function describeRedeemOutcome(outcome: ResetCreditRedeemOutcome, label:
|
|
|
54
54
|
return `${label}: that reset was already redeemed.`;
|
|
55
55
|
case "no_credit":
|
|
56
56
|
return `${label}: no saved resets available to spend.`;
|
|
57
|
+
case "credit_list_failed":
|
|
58
|
+
return `${label}: couldn't load this account's saved resets (network/auth) — nothing was spent, try again.`;
|
|
57
59
|
case "nothing_to_reset":
|
|
58
60
|
return `${label}: nothing to reset right now — your limits aren't constrained, so no credit was spent.`;
|
|
59
61
|
case "no_account":
|
package/src/system-prompt.ts
CHANGED
|
@@ -554,6 +554,14 @@ export interface BuildSystemPromptOptions {
|
|
|
554
554
|
export interface BuildSystemPromptResult {
|
|
555
555
|
/** Ordered system prompt blocks. Providers should preserve entries as distinct messages/blocks. */
|
|
556
556
|
systemPrompt: string[];
|
|
557
|
+
/**
|
|
558
|
+
* Names of `xd://` devices whose catalog/protocol section this prompt renders.
|
|
559
|
+
* Empty/undefined when no catalog was emitted (no mounted devices, or a custom
|
|
560
|
+
* prompt template that omits the section). Lets the session fold these devices
|
|
561
|
+
* into its announced-mount baseline so a same-turn mount notice does not re-list
|
|
562
|
+
* a catalog the prompt already carries (issue #7139).
|
|
563
|
+
*/
|
|
564
|
+
xdevCatalogNames?: readonly string[];
|
|
557
565
|
}
|
|
558
566
|
|
|
559
567
|
/** Build the system prompt with tools, guidelines, and context */
|
|
@@ -895,5 +903,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
895
903
|
systemPrompt.push(activeRepoContextPrompt);
|
|
896
904
|
}
|
|
897
905
|
|
|
898
|
-
|
|
906
|
+
// The xd:// protocol section (with its device catalog) is only rendered by the
|
|
907
|
+
// default template; a resolved custom prompt uses a template that omits it.
|
|
908
|
+
const xdevCatalogNames =
|
|
909
|
+
!resolvedCustomPrompt && xdevTools.length > 0 ? xdevTools.map(mounted => mounted.name) : undefined;
|
|
910
|
+
return { systemPrompt, xdevCatalogNames };
|
|
899
911
|
}
|