@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/cli.js +2648 -2662
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +32 -12
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/modes/components/status-line/component.d.ts +0 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/messages.d.ts +6 -7
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +196 -0
- package/src/advisor/runtime.ts +65 -2
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +30 -8
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +74 -4
- package/src/extensibility/shared-events.ts +1 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/status-line/component.ts +1 -9
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -11
- package/src/modes/interactive-mode.ts +0 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +5 -4
- package/src/sdk.ts +12 -26
- package/src/session/agent-session.ts +319 -219
- package/src/session/messages.ts +20 -12
- package/src/session/session-persistence.ts +1 -2
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +14 -7
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/tts.ts +2 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
|
@@ -1005,6 +1005,202 @@ describe("advisor", () => {
|
|
|
1005
1005
|
expect(runtime.backlog).toBe(0);
|
|
1006
1006
|
});
|
|
1007
1007
|
|
|
1008
|
+
it("notifies the host once when consecutive prompt failures make the advisor unavailable", async () => {
|
|
1009
|
+
const promptInputs: string[] = [];
|
|
1010
|
+
const failures: unknown[] = [];
|
|
1011
|
+
let shouldFail = true;
|
|
1012
|
+
const agent: AdvisorAgent = {
|
|
1013
|
+
prompt: async input => {
|
|
1014
|
+
promptInputs.push(input);
|
|
1015
|
+
if (shouldFail) {
|
|
1016
|
+
throw new Error("404 No endpoints available matching your guardrail restrictions and data policy.");
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
abort: () => {},
|
|
1020
|
+
reset: () => {},
|
|
1021
|
+
state: { messages: [] },
|
|
1022
|
+
};
|
|
1023
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1024
|
+
const host: AdvisorRuntimeHost = {
|
|
1025
|
+
snapshotMessages: () => messages,
|
|
1026
|
+
enqueueAdvice: () => {},
|
|
1027
|
+
notifyFailure: error => failures.push(error),
|
|
1028
|
+
};
|
|
1029
|
+
const runtime = new AdvisorRuntime(agent, host, 0);
|
|
1030
|
+
|
|
1031
|
+
runtime.onTurnEnd(messages);
|
|
1032
|
+
await Bun.sleep(0);
|
|
1033
|
+
await Bun.sleep(0);
|
|
1034
|
+
await Bun.sleep(0);
|
|
1035
|
+
|
|
1036
|
+
expect(promptInputs).toHaveLength(3);
|
|
1037
|
+
expect(failures).toHaveLength(1);
|
|
1038
|
+
const failure = failures[0];
|
|
1039
|
+
expect(failure).toBeInstanceOf(Error);
|
|
1040
|
+
if (!(failure instanceof Error)) throw new Error("expected advisor failure error");
|
|
1041
|
+
expect(failure.message).toContain("No endpoints available");
|
|
1042
|
+
|
|
1043
|
+
messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
|
|
1044
|
+
runtime.onTurnEnd(messages);
|
|
1045
|
+
await Bun.sleep(0);
|
|
1046
|
+
await Bun.sleep(0);
|
|
1047
|
+
await Bun.sleep(0);
|
|
1048
|
+
|
|
1049
|
+
expect(promptInputs).toHaveLength(6);
|
|
1050
|
+
expect(failures).toHaveLength(1);
|
|
1051
|
+
|
|
1052
|
+
shouldFail = false;
|
|
1053
|
+
messages.push({ role: "user", content: "ccc", timestamp: 3 } as AgentMessage);
|
|
1054
|
+
runtime.onTurnEnd(messages);
|
|
1055
|
+
await Bun.sleep(0);
|
|
1056
|
+
expect(failures).toHaveLength(1);
|
|
1057
|
+
|
|
1058
|
+
shouldFail = true;
|
|
1059
|
+
messages.push({ role: "user", content: "ddd", timestamp: 4 } as AgentMessage);
|
|
1060
|
+
runtime.onTurnEnd(messages);
|
|
1061
|
+
await Bun.sleep(0);
|
|
1062
|
+
await Bun.sleep(0);
|
|
1063
|
+
await Bun.sleep(0);
|
|
1064
|
+
|
|
1065
|
+
expect(failures).toHaveLength(2);
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("treats a clean prompt resolution with state.error as a failed turn (real Agent contract)", async () => {
|
|
1069
|
+
// `Agent.#runLoop` catches provider/stream failures internally — it resolves
|
|
1070
|
+
// `prompt()` cleanly and stores the message on `state.error` (e.g. the
|
|
1071
|
+
// OpenRouter ZDR `404 No endpoints available` case from #3635). The runtime
|
|
1072
|
+
// must surface that as a failed turn even though the awaited promise did
|
|
1073
|
+
// not reject.
|
|
1074
|
+
const promptInputs: string[] = [];
|
|
1075
|
+
const failures: unknown[] = [];
|
|
1076
|
+
const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
|
|
1077
|
+
let shouldFail = true;
|
|
1078
|
+
const agent: AdvisorAgent = {
|
|
1079
|
+
prompt: async input => {
|
|
1080
|
+
promptInputs.push(input);
|
|
1081
|
+
state.error = shouldFail
|
|
1082
|
+
? "404 No endpoints available matching your guardrail restrictions and data policy."
|
|
1083
|
+
: undefined;
|
|
1084
|
+
},
|
|
1085
|
+
abort: () => {},
|
|
1086
|
+
reset: () => {
|
|
1087
|
+
state.error = undefined;
|
|
1088
|
+
},
|
|
1089
|
+
state,
|
|
1090
|
+
};
|
|
1091
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1092
|
+
const host: AdvisorRuntimeHost = {
|
|
1093
|
+
snapshotMessages: () => messages,
|
|
1094
|
+
enqueueAdvice: () => {},
|
|
1095
|
+
notifyFailure: error => failures.push(error),
|
|
1096
|
+
};
|
|
1097
|
+
const runtime = new AdvisorRuntime(agent, host, 0);
|
|
1098
|
+
|
|
1099
|
+
runtime.onTurnEnd(messages);
|
|
1100
|
+
await Bun.sleep(0);
|
|
1101
|
+
await Bun.sleep(0);
|
|
1102
|
+
await Bun.sleep(0);
|
|
1103
|
+
|
|
1104
|
+
expect(promptInputs).toHaveLength(3);
|
|
1105
|
+
expect(failures).toHaveLength(1);
|
|
1106
|
+
const failure = failures[0];
|
|
1107
|
+
if (!(failure instanceof Error)) throw new Error("expected advisor failure error");
|
|
1108
|
+
expect(failure.message).toContain("No endpoints available");
|
|
1109
|
+
expect(runtime.backlog).toBe(0);
|
|
1110
|
+
|
|
1111
|
+
shouldFail = false;
|
|
1112
|
+
messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
|
|
1113
|
+
runtime.onTurnEnd(messages);
|
|
1114
|
+
await Bun.sleep(0);
|
|
1115
|
+
expect(failures).toHaveLength(1);
|
|
1116
|
+
|
|
1117
|
+
shouldFail = true;
|
|
1118
|
+
messages.push({ role: "user", content: "ccc", timestamp: 3 } as AgentMessage);
|
|
1119
|
+
runtime.onTurnEnd(messages);
|
|
1120
|
+
await Bun.sleep(0);
|
|
1121
|
+
await Bun.sleep(0);
|
|
1122
|
+
await Bun.sleep(0);
|
|
1123
|
+
|
|
1124
|
+
expect(failures).toHaveLength(2);
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it("rolls advisor state back after each failed prompt so retries don't replay duplicate turns", async () => {
|
|
1128
|
+
// The real `Agent` appends the user batch + a synthetic `stopReason: "error"`
|
|
1129
|
+
// assistant turn before `state.error` is read. Without rollback, the runtime's
|
|
1130
|
+
// retry/drop path would replay the failed batch on top of those orphans,
|
|
1131
|
+
// duplicating session-update user turns and leaking dropped failures into the
|
|
1132
|
+
// next successful run's context.
|
|
1133
|
+
const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
|
|
1134
|
+
const rollbackCalls: number[] = [];
|
|
1135
|
+
const lengthsBeforePrompt: number[] = [];
|
|
1136
|
+
let shouldFail = true;
|
|
1137
|
+
const agent: AdvisorAgent = {
|
|
1138
|
+
prompt: async input => {
|
|
1139
|
+
lengthsBeforePrompt.push(state.messages.length);
|
|
1140
|
+
state.messages.push({ role: "user", content: input, timestamp: Date.now() } as AgentMessage);
|
|
1141
|
+
if (shouldFail) {
|
|
1142
|
+
state.messages.push({
|
|
1143
|
+
role: "assistant",
|
|
1144
|
+
content: [{ type: "text", text: "" }],
|
|
1145
|
+
stopReason: "error",
|
|
1146
|
+
errorMessage: "404 No endpoints available",
|
|
1147
|
+
timestamp: Date.now(),
|
|
1148
|
+
} as unknown as AgentMessage);
|
|
1149
|
+
state.error = "404 No endpoints available";
|
|
1150
|
+
} else {
|
|
1151
|
+
state.messages.push({
|
|
1152
|
+
role: "assistant",
|
|
1153
|
+
content: [{ type: "text", text: "ok" }],
|
|
1154
|
+
timestamp: Date.now(),
|
|
1155
|
+
} as unknown as AgentMessage);
|
|
1156
|
+
state.error = undefined;
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
abort: () => {},
|
|
1160
|
+
reset: () => {
|
|
1161
|
+
state.messages.length = 0;
|
|
1162
|
+
state.error = undefined;
|
|
1163
|
+
},
|
|
1164
|
+
rollbackTo: count => {
|
|
1165
|
+
rollbackCalls.push(count);
|
|
1166
|
+
if (count < state.messages.length) state.messages.length = count;
|
|
1167
|
+
state.error = undefined;
|
|
1168
|
+
},
|
|
1169
|
+
state,
|
|
1170
|
+
};
|
|
1171
|
+
const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
|
|
1172
|
+
const host: AdvisorRuntimeHost = {
|
|
1173
|
+
snapshotMessages: () => messages,
|
|
1174
|
+
enqueueAdvice: () => {},
|
|
1175
|
+
};
|
|
1176
|
+
const runtime = new AdvisorRuntime(agent, host, 0);
|
|
1177
|
+
|
|
1178
|
+
runtime.onTurnEnd(messages);
|
|
1179
|
+
await Bun.sleep(0);
|
|
1180
|
+
await Bun.sleep(0);
|
|
1181
|
+
await Bun.sleep(0);
|
|
1182
|
+
|
|
1183
|
+
// Three failed prompts each rolled back to the empty baseline, so every retry
|
|
1184
|
+
// saw a clean state.messages instead of stacked failed turns.
|
|
1185
|
+
expect(lengthsBeforePrompt).toEqual([0, 0, 0]);
|
|
1186
|
+
expect(rollbackCalls).toEqual([0, 0, 0]);
|
|
1187
|
+
// The drop-after-3 path also left state.messages empty — no orphan failed
|
|
1188
|
+
// turns leak into the next successful run's context.
|
|
1189
|
+
expect(state.messages).toHaveLength(0);
|
|
1190
|
+
expect(state.error).toBeUndefined();
|
|
1191
|
+
|
|
1192
|
+
// A subsequent successful run starts from the clean baseline and is NOT
|
|
1193
|
+
// rolled back.
|
|
1194
|
+
shouldFail = false;
|
|
1195
|
+
messages.push({ role: "user", content: "bbb", timestamp: 2 } as AgentMessage);
|
|
1196
|
+
runtime.onTurnEnd(messages);
|
|
1197
|
+
await Bun.sleep(0);
|
|
1198
|
+
|
|
1199
|
+
expect(lengthsBeforePrompt[lengthsBeforePrompt.length - 1]).toBe(0);
|
|
1200
|
+
expect(rollbackCalls).toHaveLength(3);
|
|
1201
|
+
expect(state.messages).toHaveLength(2);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1008
1204
|
it("drops the in-flight batch when a reset aborts the advisor prompt", async () => {
|
|
1009
1205
|
const promptInputs: string[] = [];
|
|
1010
1206
|
const { promise: firstPromptStarted, resolve: startFirstPrompt } = Promise.withResolvers<void>();
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -5,12 +5,23 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
5
5
|
import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
|
|
6
6
|
import { formatSessionHistoryMarkdown, PRIMARY_CONTEXT_CUSTOM_TYPES } from "../session/session-history-format";
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Minimal slice of `Agent` the runtime drives — satisfied by pi-agent-core
|
|
10
|
+
* `Agent`. `state.error` mirrors `Agent.state.error`: provider/stream failures
|
|
11
|
+
* the loop catches internally never reject `prompt()`, so the runtime reads
|
|
12
|
+
* this field after every prompt to detect a failed turn.
|
|
13
|
+
*/
|
|
9
14
|
export interface AdvisorAgent {
|
|
10
15
|
prompt(input: string): Promise<void>;
|
|
11
16
|
abort(reason?: unknown): void;
|
|
12
17
|
reset(): void;
|
|
13
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Drop messages appended past `count`. Called after a failed `prompt()` so a
|
|
20
|
+
* retry doesn't replay the failed user batch + synthetic assistant-error
|
|
21
|
+
* turn `Agent.#runLoop` records on its internal state.
|
|
22
|
+
*/
|
|
23
|
+
rollbackTo?(count: number): void;
|
|
24
|
+
readonly state: { messages: AgentMessage[]; error?: string };
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
export interface AdvisorRuntimeHost {
|
|
@@ -37,6 +48,8 @@ export interface AdvisorRuntimeHost {
|
|
|
37
48
|
* one that routes `advise()` results back to the primary.
|
|
38
49
|
*/
|
|
39
50
|
beginAdvisorUpdate?(): void;
|
|
51
|
+
/** Surface a non-recovering advisor failure to the host UI without adding model-visible context. */
|
|
52
|
+
notifyFailure?(error: unknown): void;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
interface PendingDelta {
|
|
@@ -63,6 +76,7 @@ export class AdvisorRuntime {
|
|
|
63
76
|
#busy = false;
|
|
64
77
|
#backlog = 0;
|
|
65
78
|
#consecutiveFailures = 0;
|
|
79
|
+
#failureNotified = false;
|
|
66
80
|
#latestMessages?: AgentMessage[];
|
|
67
81
|
#waiters: CatchupWaiter[] = [];
|
|
68
82
|
/** Bumped by every external {@link reset}/{@link dispose}. A drain iteration
|
|
@@ -121,6 +135,7 @@ export class AdvisorRuntime {
|
|
|
121
135
|
this.#pending = [];
|
|
122
136
|
this.#backlog = 0;
|
|
123
137
|
this.#consecutiveFailures = 0;
|
|
138
|
+
this.#failureNotified = false;
|
|
124
139
|
this.#wakeAllWaiters();
|
|
125
140
|
try {
|
|
126
141
|
this.agent.abort("advisor disposed");
|
|
@@ -131,6 +146,7 @@ export class AdvisorRuntime {
|
|
|
131
146
|
this.#lastCount = 0;
|
|
132
147
|
this.#pending = [];
|
|
133
148
|
this.#consecutiveFailures = 0;
|
|
149
|
+
this.#failureNotified = false;
|
|
134
150
|
this.#seenContext.clear();
|
|
135
151
|
if (clearBacklog) {
|
|
136
152
|
this.#backlog = 0;
|
|
@@ -168,6 +184,7 @@ export class AdvisorRuntime {
|
|
|
168
184
|
this.#pending = [];
|
|
169
185
|
this.#backlog = 0;
|
|
170
186
|
this.#consecutiveFailures = 0;
|
|
187
|
+
this.#failureNotified = false;
|
|
171
188
|
this.#seenContext.clear();
|
|
172
189
|
this.#wakeAllWaiters();
|
|
173
190
|
}
|
|
@@ -233,6 +250,28 @@ export class AdvisorRuntime {
|
|
|
233
250
|
}
|
|
234
251
|
}
|
|
235
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Drop the user batch + synthetic assistant-error turn `Agent.#runLoop`
|
|
255
|
+
* appended for a failed prompt so a retry replays a clean baseline and the
|
|
256
|
+
* dropped-after-3 path never leaks orphan failures into the next successful
|
|
257
|
+
* run. Prefers the agent's own `rollbackTo` (which also re-syncs its
|
|
258
|
+
* append-only context); falls back to truncating `state.messages` for tests
|
|
259
|
+
* that hand-roll a minimal facade.
|
|
260
|
+
*/
|
|
261
|
+
#rollbackFailedTurn(snapshot: number): void {
|
|
262
|
+
const messages = this.agent.state.messages;
|
|
263
|
+
if (messages.length <= snapshot) return;
|
|
264
|
+
try {
|
|
265
|
+
if (this.agent.rollbackTo) {
|
|
266
|
+
this.agent.rollbackTo(snapshot);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
messages.length = snapshot;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
logger.debug("advisor rollback failed", { err: String(err) });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
236
275
|
async #drain(): Promise<void> {
|
|
237
276
|
if (this.#busy) return;
|
|
238
277
|
this.#busy = true;
|
|
@@ -281,24 +320,48 @@ export class AdvisorRuntime {
|
|
|
281
320
|
}
|
|
282
321
|
|
|
283
322
|
let success = false;
|
|
323
|
+
// Capture the advisor's message count BEFORE the prompt so a failure can
|
|
324
|
+
// roll back the user batch + synthetic assistant-error turn `Agent.#runLoop`
|
|
325
|
+
// appends to internal state. Without this, a retry would replay the
|
|
326
|
+
// failed batch on top of the stale turns and the dropped-after-3 path
|
|
327
|
+
// would leak orphan failures into the next successful run's context.
|
|
328
|
+
const messageSnapshot = this.agent.state.messages.length;
|
|
284
329
|
try {
|
|
285
330
|
// Reset the host's per-update advisor state (one-advise-per-update
|
|
286
331
|
// gate) before each model cycle, so the new batch starts with a
|
|
287
332
|
// fresh budget. Dedupe history persists across cycles.
|
|
288
333
|
this.host.beginAdvisorUpdate?.();
|
|
289
334
|
await this.agent.prompt(batch);
|
|
335
|
+
// `Agent.#runLoop` catches provider/stream failures internally and
|
|
336
|
+
// resolves `prompt()` cleanly with the assistant turn ending in
|
|
337
|
+
// `stopReason: "error"` and the message recorded on `state.error`.
|
|
338
|
+
// Treat that as a failed turn so OpenRouter ZDR-style endpoint
|
|
339
|
+
// rejections trip the retry/notify path instead of looking like a
|
|
340
|
+
// successful empty cycle.
|
|
341
|
+
const promptError = this.agent.state.error;
|
|
342
|
+
if (promptError) throw new Error(promptError);
|
|
290
343
|
success = true;
|
|
291
344
|
this.#consecutiveFailures = 0;
|
|
345
|
+
this.#failureNotified = false;
|
|
292
346
|
} catch (err) {
|
|
293
347
|
// reset()/dispose() aborts the in-flight prompt; the rejection is the
|
|
294
348
|
// reset itself, not a transient advisor failure. Drop the stale batch
|
|
295
349
|
// (reset already cleared #pending and rewound the cursor) instead of
|
|
296
350
|
// requeuing it into the post-reset conversation.
|
|
297
351
|
if (this.#epoch !== epoch) continue;
|
|
352
|
+
this.#rollbackFailedTurn(messageSnapshot);
|
|
298
353
|
logger.debug("advisor turn failed", { err: String(err) });
|
|
299
354
|
this.#consecutiveFailures++;
|
|
300
355
|
if (this.#consecutiveFailures >= 3) {
|
|
301
356
|
logger.warn("advisor failed consecutively 3 times; dropping backlog to prevent stall");
|
|
357
|
+
if (!this.#failureNotified) {
|
|
358
|
+
this.#failureNotified = true;
|
|
359
|
+
try {
|
|
360
|
+
this.host.notifyFailure?.(err);
|
|
361
|
+
} catch (notifyErr) {
|
|
362
|
+
logger.warn("advisor failure notification failed", { err: String(notifyErr) });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
302
365
|
this.#consecutiveFailures = 0;
|
|
303
366
|
// The dropped batch may carry primary-context we never delivered; drop
|
|
304
367
|
// the seen-state too so the next turn re-expands it instead of marking
|
|
@@ -74,10 +74,10 @@ export async function classifyDifficulty(
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
|
|
77
|
-
const resolved = resolveRoleSelection(["smol"], deps.settings, deps.registry.getAvailable(), deps.registry);
|
|
77
|
+
const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable(), deps.registry);
|
|
78
78
|
const model = resolved?.model;
|
|
79
79
|
if (!model) {
|
|
80
|
-
throw new Error("auto-thinking: no smol model available for classification");
|
|
80
|
+
throw new Error("auto-thinking: no tiny/smol model available for classification");
|
|
81
81
|
}
|
|
82
82
|
const apiKey = await deps.registry.getApiKey(model, deps.sessionId);
|
|
83
83
|
if (!apiKey) {
|
|
@@ -882,10 +882,14 @@ function shouldInheritDefaultBeforePriority(role: ModelRole): boolean {
|
|
|
882
882
|
* list. The advisor — a second-opinion reviewer — defaults to the `slow`
|
|
883
883
|
* reasoning chain, but (unlike the `slow` role, see
|
|
884
884
|
* {@link shouldInheritDefaultBeforePriority}) never inherits the primary's
|
|
885
|
-
* model, so it stays a distinct strong model out of the box.
|
|
885
|
+
* model, so it stays a distinct strong model out of the box. The `tiny` role —
|
|
886
|
+
* the override for online title/memory/classifier tasks — reuses the `smol`
|
|
887
|
+
* fast chain so an unset tiny role auto-resolves to the same fast model smol
|
|
888
|
+
* would pick.
|
|
886
889
|
*/
|
|
887
890
|
const ROLE_PRIORITY_ALIAS: Partial<Record<ModelRole, keyof typeof MODEL_PRIO>> = {
|
|
888
891
|
advisor: "slow",
|
|
892
|
+
tiny: "smol",
|
|
889
893
|
};
|
|
890
894
|
|
|
891
895
|
/** Built-in priority patterns for a role, following {@link ROLE_PRIORITY_ALIAS}. */
|
|
@@ -13,7 +13,7 @@ export type ModelRole =
|
|
|
13
13
|
| "plan"
|
|
14
14
|
| "designer"
|
|
15
15
|
| "commit"
|
|
16
|
-
| "
|
|
16
|
+
| "tiny"
|
|
17
17
|
| "task"
|
|
18
18
|
| "advisor";
|
|
19
19
|
|
|
@@ -33,7 +33,7 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
|
33
33
|
plan: { tag: "PLAN", name: "Architect", color: "muted" },
|
|
34
34
|
designer: { tag: "DESIGNER", name: "Designer", color: "muted" },
|
|
35
35
|
commit: { tag: "COMMIT", name: "Commit", color: "dim" },
|
|
36
|
-
|
|
36
|
+
tiny: { tag: "TINY", name: "Tiny", color: "dim" },
|
|
37
37
|
task: { tag: "TASK", name: "Subtask", color: "muted" },
|
|
38
38
|
advisor: { tag: "ADVISOR", name: "Advisor", color: "accent" },
|
|
39
39
|
};
|
|
@@ -46,7 +46,7 @@ export const MODEL_ROLE_IDS: ModelRole[] = [
|
|
|
46
46
|
"plan",
|
|
47
47
|
"designer",
|
|
48
48
|
"commit",
|
|
49
|
-
"
|
|
49
|
+
"tiny",
|
|
50
50
|
"task",
|
|
51
51
|
"advisor",
|
|
52
52
|
];
|
|
@@ -1183,6 +1183,23 @@ export const SETTINGS_SCHEMA = {
|
|
|
1183
1183
|
},
|
|
1184
1184
|
},
|
|
1185
1185
|
|
|
1186
|
+
textVerbosity: {
|
|
1187
|
+
type: "enum",
|
|
1188
|
+
values: ["low", "medium", "high"] as const,
|
|
1189
|
+
default: "high",
|
|
1190
|
+
ui: {
|
|
1191
|
+
tab: "model",
|
|
1192
|
+
group: "Sampling",
|
|
1193
|
+
label: "Text Verbosity",
|
|
1194
|
+
description: "OpenAI Responses and Codex response verbosity (low, medium, or high)",
|
|
1195
|
+
options: [
|
|
1196
|
+
{ value: "low", label: "Low", description: "Prefer concise responses" },
|
|
1197
|
+
{ value: "medium", label: "Medium", description: "Balance brevity and detail" },
|
|
1198
|
+
{ value: "high", label: "High", description: "Prefer detailed responses (default)" },
|
|
1199
|
+
],
|
|
1200
|
+
},
|
|
1201
|
+
},
|
|
1202
|
+
|
|
1186
1203
|
serviceTier: {
|
|
1187
1204
|
type: "enum",
|
|
1188
1205
|
values: SERVICE_TIER_SETTING_VALUES,
|
|
@@ -2007,7 +2024,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
2007
2024
|
"anthropic",
|
|
2008
2025
|
"deepseek",
|
|
2009
2026
|
"harmony",
|
|
2010
|
-
"pi",
|
|
2011
2027
|
"qwen3",
|
|
2012
2028
|
"gemini",
|
|
2013
2029
|
"gemma",
|
|
@@ -2034,7 +2050,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
2034
2050
|
{ value: "anthropic", label: "Anthropic", description: "Use Anthropic-style in-band tool calls." },
|
|
2035
2051
|
{ value: "deepseek", label: "DeepSeek", description: "Use DeepSeek-style in-band tool calls." },
|
|
2036
2052
|
{ value: "harmony", label: "Harmony", description: "Use Harmony-style in-band tool calls." },
|
|
2037
|
-
{ value: "pi", label: "Pi", description: "Use the Pi owned dialect (compact sigil-delimited tool calls)." },
|
|
2038
2053
|
{ value: "qwen3", label: "Qwen3", description: "Use the Qwen3 owned dialect." },
|
|
2039
2054
|
{ value: "gemini", label: "Gemini", description: "Use the Gemini owned dialect." },
|
|
2040
2055
|
{ value: "gemma", label: "Gemma", description: "Use the Gemma owned dialect." },
|
|
@@ -2440,11 +2455,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
2440
2455
|
tab: "memory",
|
|
2441
2456
|
group: "Mnemopi",
|
|
2442
2457
|
label: "Mnemopi LLM Mode",
|
|
2443
|
-
description:
|
|
2458
|
+
description:
|
|
2459
|
+
"Use no LLM, the online tiny model (the TINY role from /models, else pi/smol), or a remote OpenAI-compatible endpoint",
|
|
2444
2460
|
condition: "mnemopiActive",
|
|
2445
2461
|
options: [
|
|
2446
2462
|
{ value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
|
|
2447
|
-
{
|
|
2463
|
+
{
|
|
2464
|
+
value: "smol",
|
|
2465
|
+
label: "Online (tiny)",
|
|
2466
|
+
description: "Use the online tiny model (the TINY role from /models, else pi/smol)",
|
|
2467
|
+
},
|
|
2448
2468
|
{ value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
|
|
2449
2469
|
],
|
|
2450
2470
|
},
|
|
@@ -4342,7 +4362,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
4342
4362
|
tab: "providers",
|
|
4343
4363
|
group: "Tiny Model",
|
|
4344
4364
|
label: "Tiny Model",
|
|
4345
|
-
description:
|
|
4365
|
+
description:
|
|
4366
|
+
"Session-title model: online (the TINY role from /models, else pi/smol) by default, or a local on-device model",
|
|
4346
4367
|
options: TINY_TITLE_MODEL_OPTIONS,
|
|
4347
4368
|
},
|
|
4348
4369
|
},
|
|
@@ -4381,7 +4402,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
4381
4402
|
group: "General",
|
|
4382
4403
|
label: "Memory Model",
|
|
4383
4404
|
description:
|
|
4384
|
-
"Mnemopi LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
|
|
4405
|
+
"Mnemopi LLM for fact extraction + consolidation: online (the TINY role from /models, else smol/remote) by default, or a local on-device model",
|
|
4385
4406
|
condition: "mnemopiActive",
|
|
4386
4407
|
options: TINY_MEMORY_MODEL_OPTIONS,
|
|
4387
4408
|
},
|
|
@@ -4396,7 +4417,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
4396
4417
|
group: "Thinking",
|
|
4397
4418
|
label: "Auto Thinking Model",
|
|
4398
4419
|
description:
|
|
4399
|
-
"Difficulty classifier for the `auto` thinking level: online smol by default, or a local on-device model",
|
|
4420
|
+
"Difficulty classifier for the `auto` thinking level: online (the TINY role from /models, else smol) by default, or a local on-device model",
|
|
4400
4421
|
condition: "autoThinkingActive",
|
|
4401
4422
|
options: AUTO_THINKING_MODEL_OPTIONS,
|
|
4402
4423
|
},
|
|
@@ -4420,7 +4441,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
4420
4441
|
tab: "providers",
|
|
4421
4442
|
group: "Tiny Model",
|
|
4422
4443
|
label: "Unexpected Stop Model",
|
|
4423
|
-
description:
|
|
4444
|
+
description:
|
|
4445
|
+
"Classifier for unexpected-stop detection: online (the TINY role from /models, else smol) by default, or a local on-device model.",
|
|
4424
4446
|
condition: "unexpectedStopDetection",
|
|
4425
4447
|
options: TINY_MEMORY_MODEL_OPTIONS,
|
|
4426
4448
|
},
|
|
@@ -22,6 +22,7 @@ import { readDirEntries, readFile } from "../capability/fs";
|
|
|
22
22
|
import type { LoadContext } from "../capability/types";
|
|
23
23
|
import { getEnabledPlugins } from "../extensibility/plugins/loader";
|
|
24
24
|
import { expandTilde } from "../tools/path-utils";
|
|
25
|
+
import { listClaudePluginRoots } from "./helpers";
|
|
25
26
|
|
|
26
27
|
/** A resolved extension package directory wired into the discovery surfaces. */
|
|
27
28
|
export interface OmpExtensionRoot {
|
|
@@ -123,9 +124,9 @@ async function isDirectory(p: string): Promise<boolean> {
|
|
|
123
124
|
* 1. CLI roots injected via {@link injectOmpExtensionCliRoots}
|
|
124
125
|
* 2. Project `<cwd>/.omp/settings.json#extensions`
|
|
125
126
|
* 3. User `~/.omp/agent/settings.json#extensions`
|
|
126
|
-
* 4. Enabled plugins installed under `<plugins>/node_modules/` (
|
|
127
|
-
* `omp install <pkg>` / `omp plugin install` / `omp plugin link`)
|
|
128
|
-
*
|
|
127
|
+
* 4. Enabled npm/link plugins installed under `<plugins>/node_modules/` (for
|
|
128
|
+
* `omp install <pkg>` / `omp plugin install` / `omp plugin link`). Marketplace
|
|
129
|
+
* installs are loaded by the `claude-plugins` provider and are excluded here.
|
|
129
130
|
* Only entries that resolve to a directory on disk are returned; file
|
|
130
131
|
* entrypoints contribute zero sub-discovery surface and are filtered out.
|
|
131
132
|
* Installed-plugin enumeration failures (missing lockfile, unreadable
|
|
@@ -167,20 +168,44 @@ export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtens
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
/**
|
|
170
|
-
* Enumerate every enabled
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
171
|
+
* Enumerate every enabled npm/link plugin's package directory so its conventional
|
|
172
|
+
* `skills/`, `hooks/`, `tools/`, `commands/`, `rules/`, `prompts/`, and
|
|
173
|
+
* `.mcp.json` are wired into discovery — mirrors how `getAllPluginExtensionPaths`
|
|
174
|
+
* already feeds the extension factory loader.
|
|
174
175
|
*
|
|
175
|
-
* Marketplace
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
176
|
+
* Marketplace installs also create runtime symlinks for enable-state persistence,
|
|
177
|
+
* but their resources are discovered through the `claude-plugins` provider.
|
|
178
|
+
* Filtering them here prevents `/status` from showing the same plugin under both
|
|
179
|
+
* "Claude Code Marketplace" and "OMP Extension Packages".
|
|
179
180
|
*/
|
|
181
|
+
async function realpathOrResolved(p: string): Promise<string> {
|
|
182
|
+
try {
|
|
183
|
+
return await fs.realpath(p);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (isEnoent(err)) return path.resolve(p);
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
180
190
|
async function listInstalledPluginRoots(ctx: LoadContext): Promise<InjectedRoot[]> {
|
|
181
191
|
try {
|
|
182
|
-
const plugins = await
|
|
183
|
-
|
|
192
|
+
const [plugins, marketplaceRoots] = await Promise.all([
|
|
193
|
+
getEnabledPlugins(ctx.cwd, { home: ctx.home }),
|
|
194
|
+
listClaudePluginRoots(ctx.home, ctx.cwd),
|
|
195
|
+
]);
|
|
196
|
+
const marketplaceRealpaths = new Set(
|
|
197
|
+
await Promise.all(marketplaceRoots.roots.map(root => realpathOrResolved(root.path))),
|
|
198
|
+
);
|
|
199
|
+
const installedRoots = await Promise.all(
|
|
200
|
+
plugins.map(async plugin => ({
|
|
201
|
+
path: plugin.path,
|
|
202
|
+
scope: plugin.scope,
|
|
203
|
+
realpath: await realpathOrResolved(plugin.path),
|
|
204
|
+
})),
|
|
205
|
+
);
|
|
206
|
+
return installedRoots
|
|
207
|
+
.filter(root => !marketplaceRealpaths.has(root.realpath))
|
|
208
|
+
.map(({ path: p, scope }) => ({ path: p, level: scope }));
|
|
184
209
|
} catch (err) {
|
|
185
210
|
logger.debug("listInstalledPluginRoots: enumeration failed", { error: String(err) });
|
|
186
211
|
return [];
|
package/src/edit/index.ts
CHANGED
|
@@ -397,6 +397,27 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
397
397
|
return EDIT_MODE_STRATEGIES[this.mode].matcherDigest(args);
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Project the streamed args onto their target file paths so path-scoped
|
|
402
|
+
* stream matchers (e.g. TTSR `tool:edit(*.ts)` globs) match hashline and
|
|
403
|
+
* apply_patch edits even though the path lives in the wire payload (a
|
|
404
|
+
* section header / envelope marker) rather than a top-level argument.
|
|
405
|
+
*/
|
|
406
|
+
matcherPaths(args: unknown): readonly string[] | undefined {
|
|
407
|
+
return EDIT_MODE_STRATEGIES[this.mode].matcherPaths(args);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Per-file projection of the streamed args, splitting multi-section
|
|
412
|
+
* hashline / multi-hunk apply_patch payloads into one (path, digest) entry
|
|
413
|
+
* per touched file. Path-scoped stream matchers (TTSR) then evaluate each
|
|
414
|
+
* file in isolation, so a `tool:edit(*.ts)` rule never fires on text that
|
|
415
|
+
* actually belongs to a sibling Markdown hunk.
|
|
416
|
+
*/
|
|
417
|
+
matcherEntries(args: unknown): readonly { path: string; digest: string }[] | undefined {
|
|
418
|
+
return EDIT_MODE_STRATEGIES[this.mode].matcherEntries(args);
|
|
419
|
+
}
|
|
420
|
+
|
|
400
421
|
async execute(
|
|
401
422
|
_toolCallId: string,
|
|
402
423
|
params: EditParams,
|