@oh-my-pi/pi-coding-agent 15.7.3 → 15.7.5
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 +39 -0
- package/dist/types/config/settings-schema.d.ts +3 -22
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/modes/controllers/command-controller.d.ts +2 -3
- package/dist/types/session/agent-session.d.ts +2 -6
- package/dist/types/session/shake-types.d.ts +3 -3
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +0 -14
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -9
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +7 -7
- package/package.json +9 -9
- package/scripts/build-binary.ts +0 -1
- package/src/cli.ts +59 -0
- package/src/config/settings-schema.ts +3 -24
- package/src/config/settings.ts +10 -0
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/mcp/manager.ts +87 -4
- package/src/modes/controllers/command-controller.ts +7 -39
- package/src/modes/controllers/event-controller.ts +33 -26
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/session/agent-session.ts +11 -93
- package/src/session/shake-types.ts +4 -5
- package/src/slash-commands/builtin-registry.ts +2 -4
- package/src/task/executor.ts +14 -4
- package/src/task/index.ts +3 -2
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +0 -28
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +11 -16
- package/src/tiny/worker.ts +6 -61
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +12 -1
- package/src/tui/output-block.ts +37 -75
- package/src/utils/git.ts +9 -3
|
@@ -5,7 +5,7 @@ import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
|
5
5
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
6
6
|
import { parseInternalUrl } from "./parse";
|
|
7
7
|
import { validateRelativePath } from "./skill-protocol";
|
|
8
|
-
import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
|
|
8
|
+
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
|
|
9
9
|
|
|
10
10
|
export interface LocalProtocolOptions {
|
|
11
11
|
getArtifactsDir?: () => string | null;
|
|
@@ -164,13 +164,25 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
164
164
|
* Returns the active local-protocol options.
|
|
165
165
|
*
|
|
166
166
|
* Resolution order:
|
|
167
|
-
* 1.
|
|
168
|
-
* that
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
167
|
+
* 1. **Caller-supplied** `context.localProtocolOptions` (the actual session
|
|
168
|
+
* that initiated the `read`/`find`/`search`/`router.resolve` call). This
|
|
169
|
+
* is what keeps `local://` reads pinned to the calling session in
|
|
170
|
+
* multi-session hosts (cmux/ACP, embedded SDK consumers) where every
|
|
171
|
+
* session registers as `kind: "main"` and "first one wins" would route
|
|
172
|
+
* to the wrong artifacts directory.
|
|
173
|
+
* 2. Explicit process-global override installed via {@link setOverride}
|
|
174
|
+
* (used by SDK consumers with a custom artifacts/session-id mapping and
|
|
175
|
+
* by code paths that do not have a calling session, e.g. TUI hyperlink
|
|
176
|
+
* resolution).
|
|
177
|
+
* 3. The first `main`-kind session in `AgentRegistry.global()`. Its
|
|
178
|
+
* `SessionManager` supplies both `getArtifactsDir` and `getSessionId`.
|
|
179
|
+
* Last-resort fallback — every caller that has a session reference
|
|
180
|
+
* SHOULD thread it through `context` so this branch is never taken in
|
|
181
|
+
* multi-session setups.
|
|
172
182
|
*/
|
|
173
|
-
static resolveOptions(): LocalProtocolOptions | undefined {
|
|
183
|
+
static resolveOptions(context?: ResolveContext): LocalProtocolOptions | undefined {
|
|
184
|
+
const fromContext = context?.localProtocolOptions;
|
|
185
|
+
if (fromContext) return fromContext;
|
|
174
186
|
const override = LocalProtocolHandler.#override;
|
|
175
187
|
if (override) return override;
|
|
176
188
|
const main = AgentRegistry.global()
|
|
@@ -184,8 +196,8 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
184
196
|
};
|
|
185
197
|
}
|
|
186
198
|
|
|
187
|
-
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
188
|
-
const opts = LocalProtocolHandler.resolveOptions();
|
|
199
|
+
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
200
|
+
const opts = LocalProtocolHandler.resolveOptions(context);
|
|
189
201
|
if (!opts) {
|
|
190
202
|
throw new Error("No session - local:// unavailable");
|
|
191
203
|
}
|
|
@@ -247,8 +259,8 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
247
259
|
};
|
|
248
260
|
}
|
|
249
261
|
|
|
250
|
-
async complete(): Promise<UrlCompletion[]> {
|
|
251
|
-
const opts = LocalProtocolHandler.resolveOptions();
|
|
262
|
+
async complete(_query?: string, context?: ResolveContext): Promise<UrlCompletion[]> {
|
|
263
|
+
const opts = LocalProtocolHandler.resolveOptions(context);
|
|
252
264
|
if (!opts) return [];
|
|
253
265
|
const localRoot = path.resolve(resolveLocalRoot(opts));
|
|
254
266
|
try {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* providing access to agent outputs and server resources without exposing filesystem paths.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { LocalProtocolOptions } from "./local-protocol";
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Raw resource payload returned by protocol handlers. The `immutable` flag is
|
|
10
12
|
* applied by the router from {@link ProtocolHandler.immutable}, so handlers do
|
|
@@ -77,6 +79,17 @@ export interface ResolveContext {
|
|
|
77
79
|
settings?: unknown;
|
|
78
80
|
/** Caller's abort signal. */
|
|
79
81
|
signal?: AbortSignal;
|
|
82
|
+
/**
|
|
83
|
+
* Calling session's `local://` root mapping. When present, the local-protocol
|
|
84
|
+
* handler resolves the URL against THIS session's artifacts dir instead of
|
|
85
|
+
* picking the first `main`-kind session from the global `AgentRegistry`.
|
|
86
|
+
*
|
|
87
|
+
* Required for correctness in multi-session hosts (cmux/ACP, embedded SDK
|
|
88
|
+
* consumers) where multiple sessions are registered as `main` and the
|
|
89
|
+
* "first one wins" lookup picks the wrong artifacts directory — see
|
|
90
|
+
* [#1608](https://github.com/can1357/oh-my-pi/issues/1608).
|
|
91
|
+
*/
|
|
92
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
/**
|
|
@@ -89,6 +102,8 @@ export interface WriteContext {
|
|
|
89
102
|
cwd?: string;
|
|
90
103
|
/** Caller's abort signal. */
|
|
91
104
|
signal?: AbortSignal;
|
|
105
|
+
/** Calling session's `local://` root mapping — see {@link ResolveContext.localProtocolOptions}. */
|
|
106
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
/**
|
package/src/lsp/client.ts
CHANGED
|
@@ -415,6 +415,10 @@ export const WARMUP_TIMEOUT_MS = 5000;
|
|
|
415
415
|
/** Max time to wait for the server to report project loading completion via $/progress */
|
|
416
416
|
const PROJECT_LOAD_TIMEOUT_MS = 15_000;
|
|
417
417
|
|
|
418
|
+
/** Max time to wait for graceful LSP shutdown and process exit. */
|
|
419
|
+
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
420
|
+
const EXIT_TIMEOUT_MS = 1_000;
|
|
421
|
+
|
|
418
422
|
/**
|
|
419
423
|
* Get or create an LSP client for the given server configuration and working directory.
|
|
420
424
|
* @param config - Server configuration
|
|
@@ -768,8 +772,18 @@ export async function refreshFile(client: LspClient, filePath: string, signal?:
|
|
|
768
772
|
}
|
|
769
773
|
}
|
|
770
774
|
|
|
775
|
+
async function waitForExit(client: LspClient, timeoutMs: number): Promise<boolean> {
|
|
776
|
+
return await Promise.race([
|
|
777
|
+
client.proc.exited.then(
|
|
778
|
+
() => true,
|
|
779
|
+
() => true,
|
|
780
|
+
),
|
|
781
|
+
Bun.sleep(timeoutMs).then(() => false),
|
|
782
|
+
]);
|
|
783
|
+
}
|
|
784
|
+
|
|
771
785
|
/**
|
|
772
|
-
* Shutdown a specific client
|
|
786
|
+
* Shutdown a specific client instance using the LSP shutdown/exit handshake.
|
|
773
787
|
*/
|
|
774
788
|
async function shutdownClientInstance(client: LspClient): Promise<void> {
|
|
775
789
|
const err = new Error("LSP client shutdown");
|
|
@@ -778,13 +792,22 @@ async function shutdownClientInstance(client: LspClient): Promise<void> {
|
|
|
778
792
|
}
|
|
779
793
|
client.pendingRequests.clear();
|
|
780
794
|
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
795
|
+
const shutdownCompleted = await sendRequest(client, "shutdown", null, undefined, SHUTDOWN_TIMEOUT_MS).then(
|
|
796
|
+
() => true,
|
|
797
|
+
() => false,
|
|
798
|
+
);
|
|
799
|
+
if (shutdownCompleted) {
|
|
800
|
+
await sendNotification(client, "exit", undefined).catch(() => {});
|
|
801
|
+
if (await waitForExit(client, EXIT_TIMEOUT_MS)) return;
|
|
802
|
+
}
|
|
803
|
+
|
|
784
804
|
client.proc.kill();
|
|
785
|
-
await
|
|
805
|
+
await waitForExit(client, EXIT_TIMEOUT_MS);
|
|
786
806
|
}
|
|
787
807
|
|
|
808
|
+
/**
|
|
809
|
+
* Shutdown a specific client by key.
|
|
810
|
+
*/
|
|
788
811
|
export async function shutdownClient(key: string): Promise<void> {
|
|
789
812
|
const client = clients.get(key);
|
|
790
813
|
if (!client) return;
|
package/src/mcp/manager.ts
CHANGED
|
@@ -59,6 +59,27 @@ type TrackedPromise<T> = {
|
|
|
59
59
|
|
|
60
60
|
const STARTUP_TIMEOUT_MS = 250;
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Per-server reconnect-storm circuit breaker.
|
|
64
|
+
*
|
|
65
|
+
* `transport.onClose` (wired in {@link MCPManager.connectServers} and
|
|
66
|
+
* {@link MCPManager.#connectAndWireServer}) fires `reconnectServer` on every
|
|
67
|
+
* clean process exit, so a stdio MCP server that completes the
|
|
68
|
+
* `initialize` + `tools/list` handshake and then exits will pull the agent
|
|
69
|
+
* into a fork loop with no rate limit. That pathology shipped in issue #1592
|
|
70
|
+
* (a `php`-shebang MCP fork-bombing macOS, parented directly to the agent's
|
|
71
|
+
* `bun` PID via shebang exec).
|
|
72
|
+
*
|
|
73
|
+
* We keep the sliding window short — older crashes age out so a single
|
|
74
|
+
* transient failure stays cheap — but cap the burst tightly enough that the
|
|
75
|
+
* agent never spawns more than `RECONNECT_BURST_LIMIT * #doReconnect retries`
|
|
76
|
+
* (≤ 25) processes per stuck server per window. Manual `/mcp reconnect`
|
|
77
|
+
* resets the window so users can recover after fixing the underlying
|
|
78
|
+
* misconfiguration.
|
|
79
|
+
*/
|
|
80
|
+
const RECONNECT_BURST_WINDOW_MS = 30_000;
|
|
81
|
+
const RECONNECT_BURST_LIMIT = 5;
|
|
82
|
+
|
|
62
83
|
function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
|
|
63
84
|
const tracked: TrackedPromise<T> = { promise, status: "pending" };
|
|
64
85
|
promise.then(
|
|
@@ -166,6 +187,11 @@ export class MCPManager {
|
|
|
166
187
|
#pendingReconnections = new Map<string, Promise<MCPServerConnection | null>>();
|
|
167
188
|
/** Preserved configs for reconnection after connection loss. */
|
|
168
189
|
#serverConfigs = new Map<string, MCPServerConfig>();
|
|
190
|
+
/**
|
|
191
|
+
* Timestamps of recent `reconnectServer` invocations per server, used by the
|
|
192
|
+
* crash-storm circuit breaker (see {@link RECONNECT_BURST_LIMIT}).
|
|
193
|
+
*/
|
|
194
|
+
#reconnectHistory = new Map<string, number[]>();
|
|
169
195
|
/** Monotonic epoch incremented on disconnectAll to invalidate stale reconnections. */
|
|
170
196
|
#epoch = 0;
|
|
171
197
|
|
|
@@ -666,6 +692,7 @@ export class MCPManager {
|
|
|
666
692
|
this.#sources.delete(name);
|
|
667
693
|
this.#serverConfigs.delete(name);
|
|
668
694
|
this.#pendingResourceRefresh.delete(name);
|
|
695
|
+
this.#reconnectHistory.delete(name);
|
|
669
696
|
|
|
670
697
|
const connection = this.#connections.get(name);
|
|
671
698
|
|
|
@@ -714,24 +741,80 @@ export class MCPManager {
|
|
|
714
741
|
this.#connections.clear();
|
|
715
742
|
this.#tools = [];
|
|
716
743
|
this.#subscribedResources.clear();
|
|
744
|
+
this.#reconnectHistory.clear();
|
|
717
745
|
}
|
|
718
746
|
|
|
719
747
|
/**
|
|
720
748
|
* Reconnect to a server after a connection failure.
|
|
749
|
+
*
|
|
721
750
|
* Tears down the stale connection, re-resolves auth, establishes a new
|
|
722
|
-
* connection, reloads tools, and notifies consumers.
|
|
723
|
-
*
|
|
724
|
-
*
|
|
751
|
+
* connection, reloads tools, and notifies consumers. Concurrent calls for
|
|
752
|
+
* the same server share one reconnection attempt. Returns the new
|
|
753
|
+
* connection, or `null` if reconnection failed or the per-server crash
|
|
754
|
+
* burst limit (see {@link RECONNECT_BURST_LIMIT}) is exceeded.
|
|
755
|
+
*
|
|
756
|
+
* @param options.manual - When `true`, resets the crash-burst window so a
|
|
757
|
+
* user-driven retry (e.g. `/mcp reconnect`) is never blocked by an
|
|
758
|
+
* earlier storm. Defaults to `false`; the transport `onClose` callback
|
|
759
|
+
* and the per-tool-call retry path in `tool-bridge` MUST NOT set it.
|
|
725
760
|
*/
|
|
726
|
-
async reconnectServer(name: string): Promise<MCPServerConnection | null> {
|
|
761
|
+
async reconnectServer(name: string, options?: { manual?: boolean }): Promise<MCPServerConnection | null> {
|
|
762
|
+
if (options?.manual) {
|
|
763
|
+
this.#reconnectHistory.delete(name);
|
|
764
|
+
}
|
|
765
|
+
|
|
727
766
|
const pending = this.#pendingReconnections.get(name);
|
|
728
767
|
if (pending) return pending;
|
|
729
768
|
|
|
769
|
+
if (this.#tripReconnectBreaker(name)) {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
730
773
|
const attempt = this.#doReconnect(name);
|
|
731
774
|
this.#pendingReconnections.set(name, attempt);
|
|
732
775
|
return attempt.finally(() => this.#pendingReconnections.delete(name));
|
|
733
776
|
}
|
|
734
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Record a reconnect attempt against the per-server crash window and report
|
|
780
|
+
* whether the circuit breaker is now open. Sliding window: entries older
|
|
781
|
+
* than {@link RECONNECT_BURST_WINDOW_MS} are pruned before the new
|
|
782
|
+
* timestamp is appended, so a single transient failure ages out cheaply
|
|
783
|
+
* but repeated rapid crashes accumulate until the limit is hit.
|
|
784
|
+
*/
|
|
785
|
+
#tripReconnectBreaker(name: string): boolean {
|
|
786
|
+
const now = Date.now();
|
|
787
|
+
const previous = this.#reconnectHistory.get(name) ?? [];
|
|
788
|
+
const recent = previous.filter(ts => now - ts < RECONNECT_BURST_WINDOW_MS);
|
|
789
|
+
recent.push(now);
|
|
790
|
+
this.#reconnectHistory.set(name, recent);
|
|
791
|
+
|
|
792
|
+
if (recent.length > RECONNECT_BURST_LIMIT) {
|
|
793
|
+
logger.error("MCP server crashed too many times; suspending automatic reconnects", {
|
|
794
|
+
path: `mcp:${name}`,
|
|
795
|
+
crashes: recent.length,
|
|
796
|
+
windowMs: RECONNECT_BURST_WINDOW_MS,
|
|
797
|
+
});
|
|
798
|
+
// Tear down the stale connection so `getConnectionStatus()` no
|
|
799
|
+
// longer reports it as "connected" and `waitForConnection()` does
|
|
800
|
+
// not hand a closed transport to callers. Tools stay registered
|
|
801
|
+
// in `#tools` — the user can recover with `/mcp reconnect <name>`
|
|
802
|
+
// once they've fixed the underlying misconfiguration. Mirrors the
|
|
803
|
+
// teardown in `#doReconnect`: detach `onClose` first so the
|
|
804
|
+
// transport's own `close()` cannot re-arm this path.
|
|
805
|
+
const stale = this.#connections.get(name);
|
|
806
|
+
if (stale) {
|
|
807
|
+
stale.transport.onClose = undefined;
|
|
808
|
+
void stale.transport.close().catch(() => {});
|
|
809
|
+
this.#connections.delete(name);
|
|
810
|
+
}
|
|
811
|
+
this.#pendingConnections.delete(name);
|
|
812
|
+
this.#pendingToolLoads.delete(name);
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
|
|
735
818
|
async #doReconnect(name: string): Promise<MCPServerConnection | null> {
|
|
736
819
|
const oldConnection = this.#connections.get(name);
|
|
737
820
|
const config = oldConnection?.config ?? this.#serverConfigs.get(name);
|
|
@@ -1124,48 +1124,16 @@ export class CommandController {
|
|
|
1124
1124
|
}
|
|
1125
1125
|
|
|
1126
1126
|
/**
|
|
1127
|
-
* TUI handler for `/shake`. `elide
|
|
1128
|
-
* `
|
|
1129
|
-
* (Esc aborts via `abortCompaction`). Rebuilds the chat and reports counts.
|
|
1127
|
+
* TUI handler for `/shake`. `elide` drops heavy structural content and
|
|
1128
|
+
* `images` strips image blocks. Rebuilds the chat and reports counts.
|
|
1130
1129
|
*/
|
|
1131
1130
|
async handleShakeCommand(mode: ShakeMode): Promise<void> {
|
|
1132
1131
|
let result: ShakeResult;
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
this.ctx.statusContainer.clear();
|
|
1139
|
-
const originalOnEscape = this.ctx.editor.onEscape;
|
|
1140
|
-
this.ctx.editor.onEscape = () => {
|
|
1141
|
-
this.ctx.session.abortCompaction();
|
|
1142
|
-
};
|
|
1143
|
-
const loader = new Loader(
|
|
1144
|
-
this.ctx.ui,
|
|
1145
|
-
spinner => theme.fg("accent", spinner),
|
|
1146
|
-
text => theme.fg("muted", text),
|
|
1147
|
-
"Shaking context (summary)… (esc to cancel)",
|
|
1148
|
-
getSymbolTheme().spinnerFrames,
|
|
1149
|
-
);
|
|
1150
|
-
this.ctx.statusContainer.addChild(loader);
|
|
1151
|
-
this.ctx.ui.requestRender();
|
|
1152
|
-
try {
|
|
1153
|
-
result = await this.ctx.session.shake("summary");
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1156
|
-
return;
|
|
1157
|
-
} finally {
|
|
1158
|
-
loader.stop();
|
|
1159
|
-
this.ctx.statusContainer.clear();
|
|
1160
|
-
this.ctx.editor.onEscape = originalOnEscape;
|
|
1161
|
-
}
|
|
1162
|
-
} else {
|
|
1163
|
-
try {
|
|
1164
|
-
result = await this.ctx.session.shake(mode);
|
|
1165
|
-
} catch (error) {
|
|
1166
|
-
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1132
|
+
try {
|
|
1133
|
+
result = await this.ctx.session.shake(mode);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
this.ctx.showError(`Shake failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1136
|
+
return;
|
|
1169
1137
|
}
|
|
1170
1138
|
|
|
1171
1139
|
const dropped = result.toolResultsDropped + result.blocksDropped + (result.imagesDropped ?? 0);
|
|
@@ -25,12 +25,14 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
|
25
25
|
|
|
26
26
|
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
27
27
|
|
|
28
|
-
// Events that change
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
const TOOL_RENDER_MODE_EVENTS: Record<string, true> = {
|
|
28
|
+
// Events that change foreground streaming state, or that reset a turn. The TUI
|
|
29
|
+
// eager native-scrollback rebuild mode is recomputed only on these so unrelated
|
|
30
|
+
// IRC/notices/status refreshes do not toggle scrollback replay policy.
|
|
31
|
+
const STREAM_RENDER_MODE_EVENTS: Record<string, true> = {
|
|
33
32
|
agent_start: true,
|
|
33
|
+
agent_end: true,
|
|
34
|
+
message_start: true,
|
|
35
|
+
message_end: true,
|
|
34
36
|
tool_execution_start: true,
|
|
35
37
|
tool_execution_update: true,
|
|
36
38
|
tool_execution_end: true,
|
|
@@ -46,6 +48,7 @@ export class EventController {
|
|
|
46
48
|
#renderedCustomMessages = new Set<string>();
|
|
47
49
|
#lastIntent: string | undefined = undefined;
|
|
48
50
|
#backgroundToolCallIds = new Set<string>();
|
|
51
|
+
#assistantMessageStreaming = false;
|
|
49
52
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
50
53
|
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
51
54
|
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
|
@@ -169,24 +172,27 @@ export class EventController {
|
|
|
169
172
|
|
|
170
173
|
const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
|
|
171
174
|
await run(event);
|
|
172
|
-
// While a foreground tool is
|
|
173
|
-
// re-
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
175
|
+
// While assistant text or a foreground tool is streaming, rows above the
|
|
176
|
+
// viewport can re-layout after they have already entered native scrollback
|
|
177
|
+
// (Markdown fences, wrapping, previews). Let the TUI rebuild history on
|
|
178
|
+
// those offscreen edits instead of deferring, which otherwise leaves stale
|
|
179
|
+
// tail rows duplicated above the live viewport.
|
|
180
|
+
// Background-running tools are excluded so late async updates outside the
|
|
181
|
+
// active foreground stream keep the no-yank deferral; agent_start resets
|
|
182
|
+
// the mode at every turn boundary.
|
|
183
|
+
if (STREAM_RENDER_MODE_EVENTS[event.type]) {
|
|
180
184
|
this.#refreshToolRenderMode();
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
#refreshToolRenderMode(): void {
|
|
185
|
-
let foregroundToolActive =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
let foregroundToolActive = this.#assistantMessageStreaming;
|
|
190
|
+
if (!foregroundToolActive) {
|
|
191
|
+
for (const toolCallId of this.ctx.pendingTools.keys()) {
|
|
192
|
+
if (!this.#backgroundToolCallIds.has(toolCallId)) {
|
|
193
|
+
foregroundToolActive = true;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
this.ctx.ui.setEagerNativeScrollbackRebuild(foregroundToolActive);
|
|
@@ -196,6 +202,7 @@ export class EventController {
|
|
|
196
202
|
this.#lastIntent = undefined;
|
|
197
203
|
this.#readToolCallArgs.clear();
|
|
198
204
|
this.#readToolCallAssistantComponents.clear();
|
|
205
|
+
this.#assistantMessageStreaming = false;
|
|
199
206
|
this.#lastAssistantComponent = undefined;
|
|
200
207
|
if (this.ctx.retryEscapeHandler) {
|
|
201
208
|
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
|
@@ -268,6 +275,7 @@ export class EventController {
|
|
|
268
275
|
this.ctx.ui.requestRender();
|
|
269
276
|
} else if (event.message.role === "assistant") {
|
|
270
277
|
this.#lastThinkingCount = 0;
|
|
278
|
+
this.#assistantMessageStreaming = true;
|
|
271
279
|
this.#resetReadGroup();
|
|
272
280
|
this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock, () =>
|
|
273
281
|
this.ctx.ui.requestRender(),
|
|
@@ -414,6 +422,9 @@ export class EventController {
|
|
|
414
422
|
|
|
415
423
|
async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
|
|
416
424
|
if (event.message.role === "user") return;
|
|
425
|
+
if (event.message.role === "assistant") {
|
|
426
|
+
this.#assistantMessageStreaming = false;
|
|
427
|
+
}
|
|
417
428
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
418
429
|
this.ctx.streamingMessage = event.message;
|
|
419
430
|
let errorMessage: string | undefined;
|
|
@@ -596,8 +607,8 @@ export class EventController {
|
|
|
596
607
|
}
|
|
597
608
|
}
|
|
598
609
|
}
|
|
599
|
-
|
|
600
610
|
async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
|
611
|
+
this.#assistantMessageStreaming = false;
|
|
601
612
|
if (this.ctx.loadingAnimation) {
|
|
602
613
|
this.ctx.loadingAnimation.stop();
|
|
603
614
|
this.ctx.loadingAnimation = undefined;
|
|
@@ -647,9 +658,7 @@ export class EventController {
|
|
|
647
658
|
? "Auto-handoff"
|
|
648
659
|
: event.action === "shake"
|
|
649
660
|
? "Auto-shake"
|
|
650
|
-
:
|
|
651
|
-
? "Auto-shake (summary)"
|
|
652
|
-
: "Auto context-full maintenance";
|
|
661
|
+
: "Auto context-full maintenance";
|
|
653
662
|
this.ctx.autoCompactionLoader = new Loader(
|
|
654
663
|
this.ctx.ui,
|
|
655
664
|
spinner => theme.fg("accent", spinner),
|
|
@@ -673,7 +682,7 @@ export class EventController {
|
|
|
673
682
|
this.ctx.statusContainer.clear();
|
|
674
683
|
}
|
|
675
684
|
const isHandoffAction = event.action === "handoff";
|
|
676
|
-
const isShakeAction = event.action === "shake"
|
|
685
|
+
const isShakeAction = event.action === "shake";
|
|
677
686
|
if (event.aborted) {
|
|
678
687
|
this.ctx.showStatus(
|
|
679
688
|
isHandoffAction
|
|
@@ -690,9 +699,7 @@ export class EventController {
|
|
|
690
699
|
this.ctx.rebuildChatFromMessages();
|
|
691
700
|
this.ctx.statusLine.invalidate();
|
|
692
701
|
this.ctx.updateEditorTopBorder();
|
|
693
|
-
this.ctx.showStatus(
|
|
694
|
-
event.action === "shake-summary" ? "Auto-shake (summary) completed" : "Auto-shake completed",
|
|
695
|
-
);
|
|
702
|
+
this.ctx.showStatus("Auto-shake completed");
|
|
696
703
|
}
|
|
697
704
|
} else if (event.result) {
|
|
698
705
|
this.ctx.rebuildChatFromMessages();
|
|
@@ -1425,7 +1425,7 @@ export class MCPCommandController {
|
|
|
1425
1425
|
this.#showMessage(["", theme.fg("muted", `Reconnecting to "${name}"...`), ""].join("\n"));
|
|
1426
1426
|
|
|
1427
1427
|
try {
|
|
1428
|
-
const connection = await this.ctx.mcpManager.reconnectServer(name);
|
|
1428
|
+
const connection = await this.ctx.mcpManager.reconnectServer(name, { manual: true });
|
|
1429
1429
|
if (connection) {
|
|
1430
1430
|
// refreshMCPTools re-registers tools and preserves the user's prior
|
|
1431
1431
|
// MCP tool selection. No need to call activateDiscoveredMCPTools —
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
PROJECT
|
|
2
|
+
===================================
|
|
3
|
+
|
|
2
4
|
<workstation>
|
|
3
5
|
{{#list environment prefix="- " join="\n"}}{{label}}: {{value}}{{/list}}
|
|
4
6
|
</workstation>
|
|
@@ -47,4 +49,3 @@ Today is {{date}}, and the current working directory is '{{cwd}}'.
|
|
|
47
49
|
{{#if appendPrompt}}
|
|
48
50
|
{{appendPrompt}}
|
|
49
51
|
{{/if}}
|
|
50
|
-
[/PROJECT]
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
ROLE
|
|
2
|
+
===================================
|
|
3
|
+
|
|
2
4
|
{{agent}}
|
|
3
|
-
[/ROLE]
|
|
4
5
|
|
|
5
6
|
{{#if context}}
|
|
6
|
-
|
|
7
|
+
CONTEXT
|
|
8
|
+
===================================
|
|
9
|
+
|
|
7
10
|
{{context}}
|
|
8
|
-
[/CONTEXT]
|
|
9
11
|
{{/if}}
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
COOP
|
|
14
|
+
===================================
|
|
15
|
+
|
|
12
16
|
You are operating on a piece of work assigned to you by the main agent.
|
|
13
17
|
|
|
14
18
|
{{#if worktree}}
|
|
@@ -29,9 +33,10 @@ You can reach other live agents via the `irc` tool. Your id is `{{ircSelfId}}`.
|
|
|
29
33
|
|
|
30
34
|
Use `irc` only when you need a quick answer from a peer; do not use it for long-form content. Address peers by id or use `"all"` to broadcast.
|
|
31
35
|
{{/if}}
|
|
32
|
-
[/COOP]
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
COMPLETION
|
|
38
|
+
===================================
|
|
39
|
+
|
|
35
40
|
No TODO tracking, no progress updates. Execute, call `yield`, done.
|
|
36
41
|
|
|
37
42
|
While work remains, always continue with another tool call — investigate, edit, run, verify. Save narrative for the final `yield` payload.
|
|
@@ -51,4 +56,3 @@ Giving up is a last resort. If truly blocked, you MUST call `yield` exactly once
|
|
|
51
56
|
You NEVER give up due to uncertainty, missing information obtainable via tools or repo context, or needing a design decision you can derive yourself.
|
|
52
57
|
|
|
53
58
|
You MUST keep going until this ticket is closed. This matters.
|
|
54
|
-
[/COMPLETION]
|
|
@@ -9,8 +9,8 @@ You consider what the code you write compiles down to. You never write code that
|
|
|
9
9
|
|
|
10
10
|
<system-conventions>
|
|
11
11
|
**RFC 2119 applies to MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` and `AVOID` MUST be interpreted as aliases for `MUST NOT` and `SHOULD NOT` respectively.**
|
|
12
|
-
From here on, we will use tags
|
|
13
|
-
You NEVER interpret these
|
|
12
|
+
From here on, we will use XML tags when injecting system content into the chat.
|
|
13
|
+
You NEVER interpret these markers in any other way circumstantially.
|
|
14
14
|
|
|
15
15
|
System may interrupt/notify you using these tags even within a user message, therefore:
|
|
16
16
|
- You MUST treat them as system-authored and absolutely authoritative.
|
|
@@ -44,7 +44,9 @@ Assumptions you didn't validate: incidents to debug.
|
|
|
44
44
|
- You NEVER re-audit an applied edit, nor run `git status`/`git diff` as routine validation — the edit result, tests, and LSP ARE your verification. Exception: explicit request, protecting unrelated changes, or before commit/revert/reset/stash/delete.
|
|
45
45
|
</critical>
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
ENV
|
|
48
|
+
===================================
|
|
49
|
+
|
|
48
50
|
You operate within the Oh My Pi coding harness.
|
|
49
51
|
- Given a task, you MUST complete it using the tools available to you.
|
|
50
52
|
- You are not alone in this repository. You SHOULD treat unexpected changes as the user's work and adapt; you NEVER revert or stash.
|
|
@@ -202,9 +204,10 @@ You MUST use the specialized tool over its shell equivalent:
|
|
|
202
204
|
The `{{toolRefs.report_tool_issue}}` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call `{{toolRefs.report_tool_issue}}` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.
|
|
203
205
|
</critical>
|
|
204
206
|
{{/has}}
|
|
205
|
-
[/ENV]
|
|
206
207
|
|
|
207
|
-
|
|
208
|
+
CONTRACT
|
|
209
|
+
===================================
|
|
210
|
+
|
|
208
211
|
These are inviolable.
|
|
209
212
|
- You NEVER yield unless the deliverable is complete. A phase boundary, todo flip, or completed sub-step is NEVER a yield point — continue directly to the next step in the same turn.
|
|
210
213
|
- You NEVER suppress tests to make code pass.
|
|
@@ -265,4 +268,3 @@ Before declaring blocked:
|
|
|
265
268
|
- Do not test defaults: changing the default configuration, or a string, should not break the test. Assert logical behavior, not the current state.
|
|
266
269
|
- Aim at: conditional branches and edge values, invariants across fields, error handling on bad input vs silent broken results.
|
|
267
270
|
</workflow>
|
|
268
|
-
[/CONTRACT]
|