@oh-my-pi/pi-coding-agent 17.3.4 → 17.3.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 +68 -0
- package/dist/{CHANGELOG-trcc215s.md → CHANGELOG-tt9k4jpr.md} +68 -0
- package/dist/cli.js +3146 -3153
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/config/model-discovery.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +7 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +22 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
- package/dist/types/mcp/config.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +3 -1
- package/dist/types/registry/agent-lifecycle.d.ts +11 -0
- package/dist/types/registry/agent-registry.d.ts +4 -0
- package/dist/types/session/agent-session-types.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/date-cwd-reminder.d.ts +22 -0
- package/dist/types/session/irc-bridge.d.ts +2 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-loader.d.ts +18 -11
- package/dist/types/session/session-maintenance.d.ts +6 -2
- package/dist/types/session/session-manager.d.ts +19 -3
- package/dist/types/session/session-tools.d.ts +0 -1
- package/dist/types/session/turn-recovery.d.ts +7 -7
- package/dist/types/tools/browser/attach.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +2 -2
- package/dist/types/tools/browser.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +6 -0
- package/dist/types/tools/hub/types.d.ts +6 -0
- package/dist/types/vibe/runtime.d.ts +3 -3
- package/package.json +13 -13
- package/src/auto-thinking/classifier.ts +37 -23
- package/src/cli/models-cli.ts +1 -1
- package/src/commit/analysis/conventional.ts +15 -9
- package/src/commit/analysis/summary.ts +15 -9
- package/src/commit/changelog/generate.ts +15 -9
- package/src/commit/map-reduce/map-phase.ts +3 -19
- package/src/commit/map-reduce/reduce-phase.ts +15 -9
- package/src/config/model-discovery.ts +3 -3
- package/src/config/model-registry.ts +29 -16
- package/src/config/settings-schema.ts +13 -0
- package/src/debug/report-bundle.ts +25 -59
- package/src/discovery/claude-plugins.ts +2 -1
- package/src/discovery/claude.ts +4 -2
- package/src/discovery/helpers.ts +5 -0
- package/src/edit/renderer.ts +48 -19
- package/src/extensibility/extensions/loader.ts +9 -5
- package/src/extensibility/extensions/runner.ts +124 -21
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +16 -10
- package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
- package/src/extensibility/plugins/marketplace/manager.ts +15 -18
- package/src/irc/bus.ts +1 -1
- package/src/launch/broker.ts +8 -1
- package/src/launch/presence.ts +77 -1
- package/src/lsp/client.ts +35 -3
- package/src/lsp/clients/biome-client.ts +47 -93
- package/src/lsp/servers.ts +17 -11
- package/src/lsp/tool.ts +2 -4
- package/src/lsp/workspace-diagnostics.ts +24 -2
- package/src/mcp/config.ts +50 -6
- package/src/memories/index.ts +29 -25
- package/src/mnemopi/backend.ts +13 -11
- package/src/modes/components/ask-dialog.ts +25 -5
- package/src/modes/components/tool-execution.ts +10 -6
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +52 -32
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +49 -10
- package/src/modes/theme/defaults/birch.json +2 -2
- package/src/prompts/system/checkpoint-active-notice.md +5 -0
- package/src/prompts/system/date-cwd-reminder.md +3 -0
- package/src/prompts/system/project-prompt.md +0 -1
- package/src/prompts/system/rewind-report.md +1 -3
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/rewind.md +1 -13
- package/src/registry/agent-lifecycle.ts +34 -0
- package/src/registry/agent-registry.ts +27 -2
- package/src/registry/persisted-agents.ts +40 -20
- package/src/sdk.ts +32 -2
- package/src/session/agent-session-types.ts +0 -2
- package/src/session/agent-session.ts +178 -46
- package/src/session/date-cwd-reminder.ts +77 -0
- package/src/session/irc-bridge.ts +5 -0
- package/src/session/messages.ts +5 -1
- package/src/session/session-loader.ts +106 -64
- package/src/session/session-maintenance.ts +189 -115
- package/src/session/session-manager.ts +142 -35
- package/src/session/session-persistence.ts +4 -4
- package/src/session/session-storage.ts +18 -5
- package/src/session/session-tools.ts +5 -10
- package/src/session/turn-recovery.ts +102 -20
- package/src/session/unexpected-stop-classifier.ts +33 -21
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +4 -15
- package/src/task/persisted-revive.ts +3 -6
- package/src/tools/ask.ts +10 -3
- package/src/tools/browser/attach.ts +80 -25
- package/src/tools/browser/launch.ts +44 -15
- package/src/tools/browser/relay/daemon.ts +3 -7
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/shared-daemon.ts +3 -7
- package/src/tools/browser.ts +5 -5
- package/src/tools/builtin-names.ts +7 -3
- package/src/tools/checkpoint.ts +1 -7
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +20 -3
- package/src/tools/hub/types.ts +6 -0
- package/src/tools/render-utils.ts +53 -21
- package/src/tools/think.ts +15 -3
- package/src/tts/speech-enhancer.ts +19 -14
- package/src/utils/commit-message-generator.ts +14 -12
- package/src/utils/title-generator.ts +23 -19
- package/src/vibe/runtime.ts +32 -17
package/src/discovery/claude.ts
CHANGED
|
@@ -406,8 +406,9 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
|
|
406
406
|
const userToolsDir = path.join(userBase, "tools");
|
|
407
407
|
|
|
408
408
|
const userResult = await loadFilesFromDir<CustomTool>(ctx, userToolsDir, PROVIDER_ID, "user", {
|
|
409
|
+
extensions: ["ts", "js"],
|
|
409
410
|
transform: (name, _content, path, source) => {
|
|
410
|
-
const toolName = name.replace(/\.(ts|js
|
|
411
|
+
const toolName = name.replace(/\.(ts|js)$/, "");
|
|
411
412
|
return {
|
|
412
413
|
name: toolName,
|
|
413
414
|
path,
|
|
@@ -425,8 +426,9 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
|
|
425
426
|
const projectToolsDir = path.join(projectBase, "tools");
|
|
426
427
|
|
|
427
428
|
const projectResult = await loadFilesFromDir<CustomTool>(ctx, projectToolsDir, PROVIDER_ID, "project", {
|
|
429
|
+
extensions: ["ts", "js"],
|
|
428
430
|
transform: (name, _content, path, source) => {
|
|
429
|
-
const toolName = name.replace(/\.(ts|js
|
|
431
|
+
const toolName = name.replace(/\.(ts|js)$/, "");
|
|
430
432
|
return {
|
|
431
433
|
name: toolName,
|
|
432
434
|
path,
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -512,6 +512,11 @@ export async function loadFilesFromDir<T>(
|
|
|
512
512
|
gitignore: true,
|
|
513
513
|
hidden: false,
|
|
514
514
|
fileType: FileType.File,
|
|
515
|
+
// Thread the caller's non-recursive intent explicitly: the native glob
|
|
516
|
+
// defaults `recursive` to true and rewrites `*.{ts,js}` -> `**/*.{ts,js}`,
|
|
517
|
+
// which would walk the entire subtree (e.g. a venv's site-packages under
|
|
518
|
+
// ~/.codex/tools) and import arbitrary frontend assets as tools (#8552).
|
|
519
|
+
recursive,
|
|
515
520
|
});
|
|
516
521
|
matches = result.matches;
|
|
517
522
|
} catch {
|
package/src/edit/renderer.ts
CHANGED
|
@@ -412,6 +412,42 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, _filePath?: string
|
|
|
412
412
|
}
|
|
413
413
|
return preview.trimEnd();
|
|
414
414
|
}
|
|
415
|
+
|
|
416
|
+
interface StreamingDiffTail {
|
|
417
|
+
content: string;
|
|
418
|
+
hidden: boolean;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Select the trailing physical lines that fit the live preview budget.
|
|
423
|
+
*
|
|
424
|
+
* Walk backward from the end instead of splitting the complete diff. This keeps
|
|
425
|
+
* allocation and scanning proportional to the visible suffix. One physical line
|
|
426
|
+
* may exceed the budget, but it is still kept so the newest change is visible.
|
|
427
|
+
*/
|
|
428
|
+
function sliceStreamingDiffTail(diff: string, innerWidth: number, budget: number): StreamingDiffTail {
|
|
429
|
+
let end = diff.length;
|
|
430
|
+
while (end > 0 && diff.charCodeAt(end - 1) === 10) end--;
|
|
431
|
+
if (end === 0) return { content: "", hidden: false };
|
|
432
|
+
|
|
433
|
+
const rowLimit = Math.max(1, budget);
|
|
434
|
+
let start = end;
|
|
435
|
+
let cursor = end;
|
|
436
|
+
let visualRows = 0;
|
|
437
|
+
while (cursor >= 0) {
|
|
438
|
+
const newline = cursor > 0 ? diff.lastIndexOf("\n", cursor - 1) : -1;
|
|
439
|
+
const lineStart = newline + 1;
|
|
440
|
+
const lineRows = Math.max(1, wrapTextWithAnsi(replaceTabs(diff.slice(lineStart, cursor)), innerWidth).length);
|
|
441
|
+
if (visualRows > 0 && visualRows + lineRows > rowLimit) break;
|
|
442
|
+
visualRows += lineRows;
|
|
443
|
+
start = lineStart;
|
|
444
|
+
if (newline < 0) break;
|
|
445
|
+
cursor = newline;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return { content: diff.slice(start, end), hidden: start > 0 };
|
|
449
|
+
}
|
|
450
|
+
|
|
415
451
|
function formatStreamingDiff(
|
|
416
452
|
diff: string,
|
|
417
453
|
rawPath: string,
|
|
@@ -442,26 +478,14 @@ function formatStreamingDiff(
|
|
|
442
478
|
// its Myers alignment is not monotonic in payload length, so a hunk-aware
|
|
443
479
|
// window stutters as rows move between hunks. Expanded widens the window
|
|
444
480
|
// to the viewport; the full diff appears once the result finalizes.
|
|
445
|
-
const
|
|
446
|
-
let visualUsed = 0;
|
|
447
|
-
let cut = allLines.length;
|
|
448
|
-
for (let i = allLines.length - 1; i >= 0; i--) {
|
|
449
|
-
const lineRows = Math.max(1, wrapTextWithAnsi(replaceTabs(allLines[i]!), innerWidth).length);
|
|
450
|
-
if (visualUsed + lineRows > budget && visualUsed > 0) break;
|
|
451
|
-
visualUsed += lineRows;
|
|
452
|
-
cut = i;
|
|
453
|
-
}
|
|
454
|
-
const hiddenLines = cut;
|
|
455
|
-
const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
|
|
481
|
+
const tail = sliceStreamingDiffTail(diff, innerWidth, budget);
|
|
456
482
|
let rendered = "\n\n";
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
remainder.push(`${hiddenLines} more lines`);
|
|
462
|
-
rendered += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
|
|
483
|
+
if (tail.hidden) {
|
|
484
|
+
// Exact hidden line/hunk counts require scanning the discarded prefix,
|
|
485
|
+
// which would make every streaming update scale with the complete diff.
|
|
486
|
+
rendered += `${uiTheme.fg("dim", "… (content above)")}\n`;
|
|
463
487
|
}
|
|
464
|
-
rendered += renderDiffColored(
|
|
488
|
+
rendered += renderDiffColored(tail.content, { filePath: rawPath });
|
|
465
489
|
return rendered;
|
|
466
490
|
});
|
|
467
491
|
// The animated glyph rides this trailing line — inside the transcript's
|
|
@@ -864,6 +888,7 @@ function renderSingleFileResult(
|
|
|
864
888
|
|
|
865
889
|
let diffSectionRenderDiffFn: ((t: string, o?: { filePath?: string }) => string) | undefined;
|
|
866
890
|
const diffSectionCache = createRenderedStringCache();
|
|
891
|
+
const statsSuffixCache = createRenderedStringCache();
|
|
867
892
|
|
|
868
893
|
return framedBlock(uiTheme, width => {
|
|
869
894
|
const { expanded, renderContext } = options;
|
|
@@ -887,7 +912,11 @@ function renderSingleFileResult(
|
|
|
887
912
|
// Change stats ride inline on the header bar next to the path.
|
|
888
913
|
const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
|
|
889
914
|
const headerDiff = isError ? undefined : details?.diff || previewDiff;
|
|
890
|
-
const statsSuffix = headerDiff
|
|
915
|
+
const statsSuffix = headerDiff
|
|
916
|
+
? cachedRenderedString(statsSuffixCache, uiTheme, false, "", headerDiff, () =>
|
|
917
|
+
formatDiffStatsSuffix(headerDiff, uiTheme),
|
|
918
|
+
)
|
|
919
|
+
: "";
|
|
891
920
|
const header = renderEditHeader(width, uiTheme, {
|
|
892
921
|
icon: isError ? "error" : "success",
|
|
893
922
|
iconOverride: !isError && !options.isPartial ? uiTheme.styledSymbol("tool.edit", "accent") : undefined,
|
|
@@ -613,6 +613,8 @@ async function discoverHooksInPackageRoot(root: string): Promise<string[]> {
|
|
|
613
613
|
export interface DiscoverExtensionPathOptions {
|
|
614
614
|
/** Include ambient native extensions, hooks, and installed plugins. */
|
|
615
615
|
ambient?: boolean;
|
|
616
|
+
/** Include ambient hook factories. Disable for read-only catalog commands. */
|
|
617
|
+
includeAmbientHooks?: boolean;
|
|
616
618
|
}
|
|
617
619
|
|
|
618
620
|
export async function discoverExtensionPaths(
|
|
@@ -665,11 +667,13 @@ export async function discoverExtensionPaths(
|
|
|
665
667
|
// scans only this invocation's configured package roots; it must not consult
|
|
666
668
|
// settings, installed packages, or process-global CLI injection state.
|
|
667
669
|
if (ambient) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
670
|
+
if (options.includeAmbientHooks !== false) {
|
|
671
|
+
const hooks = await loadCapability<Hook>(hookCapability.id, loadOptions);
|
|
672
|
+
for (const hookPath of hooks.items
|
|
673
|
+
.map(hook => hook.path)
|
|
674
|
+
.filter(hookPath => isExtensionFile(path.basename(hookPath)))) {
|
|
675
|
+
addPath(hookPath);
|
|
676
|
+
}
|
|
673
677
|
}
|
|
674
678
|
} else {
|
|
675
679
|
for (const configuredPath of configuredPaths) {
|
|
@@ -92,6 +92,10 @@ export function testSetExtensionHandlerTimeoutMs(timeoutMs: number): void {
|
|
|
92
92
|
extensionHandlerTimeoutMs = timeoutMs;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function normalizeHandlerTimeout(timeoutMs: number): number {
|
|
96
|
+
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : EXTENSION_HANDLER_TIMEOUT_MS;
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
/**
|
|
96
100
|
* Dedicated cap for `session_shutdown` handlers. The generic 30s budget is
|
|
97
101
|
* appropriate for events extensions can observe (e.g. `session_start`,
|
|
@@ -117,6 +121,11 @@ function handlerTimeoutForEvent(eventType: string): number {
|
|
|
117
121
|
const EXTENSION_HANDLER_TIMEOUT = Symbol("extensionHandlerTimeout");
|
|
118
122
|
const EXTENSION_HANDLER_ABORTED = Symbol("extensionHandlerAborted");
|
|
119
123
|
|
|
124
|
+
interface HandlerTimeoutBudget {
|
|
125
|
+
pause(): void;
|
|
126
|
+
resume(): void;
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
function attachHandlerSignal(
|
|
121
130
|
dialogOptions: ExtensionUIDialogOptions | undefined,
|
|
122
131
|
handlerSignal: AbortSignal,
|
|
@@ -127,22 +136,57 @@ function attachHandlerSignal(
|
|
|
127
136
|
return { ...dialogOptions, signal: AbortSignal.any([dialogOptions.signal, handlerSignal]) };
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
function createHandlerUIContext(
|
|
139
|
+
function createHandlerUIContext(
|
|
140
|
+
ui: ExtensionUIContext,
|
|
141
|
+
handlerSignal: AbortSignal,
|
|
142
|
+
timeoutBudget?: HandlerTimeoutBudget,
|
|
143
|
+
): ExtensionUIContext {
|
|
131
144
|
const askDialog = ui.askDialog;
|
|
145
|
+
const runDialog = async <T>(dialog: () => Promise<T>): Promise<T> => {
|
|
146
|
+
timeoutBudget?.pause();
|
|
147
|
+
try {
|
|
148
|
+
return await dialog();
|
|
149
|
+
} finally {
|
|
150
|
+
timeoutBudget?.resume();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
132
153
|
const dialogMethods = {
|
|
133
154
|
select: (title, options, dialogOptions) =>
|
|
134
|
-
ui.select(title, options, attachHandlerSignal(dialogOptions, handlerSignal)),
|
|
155
|
+
runDialog(() => ui.select(title, options, attachHandlerSignal(dialogOptions, handlerSignal))),
|
|
135
156
|
confirm: (title, message, dialogOptions) =>
|
|
136
|
-
ui.confirm(title, message, attachHandlerSignal(dialogOptions, handlerSignal)),
|
|
157
|
+
runDialog(() => ui.confirm(title, message, attachHandlerSignal(dialogOptions, handlerSignal))),
|
|
137
158
|
input: (title, placeholder, dialogOptions) =>
|
|
138
|
-
ui.input(title, placeholder, attachHandlerSignal(dialogOptions, handlerSignal)),
|
|
159
|
+
runDialog(() => ui.input(title, placeholder, attachHandlerSignal(dialogOptions, handlerSignal))),
|
|
139
160
|
askDialog: askDialog
|
|
140
161
|
? (questions, dialogOptions) =>
|
|
141
|
-
askDialog.call(ui, questions, attachHandlerSignal(dialogOptions, handlerSignal))
|
|
162
|
+
runDialog(() => askDialog.call(ui, questions, attachHandlerSignal(dialogOptions, handlerSignal)))
|
|
142
163
|
: undefined,
|
|
164
|
+
custom: async (factory, options) => {
|
|
165
|
+
let customSettled = false;
|
|
166
|
+
let componentReady = false;
|
|
167
|
+
try {
|
|
168
|
+
return await ui.custom(
|
|
169
|
+
async (...args) => {
|
|
170
|
+
const component = await factory(...args);
|
|
171
|
+
if (!customSettled) {
|
|
172
|
+
timeoutBudget?.pause();
|
|
173
|
+
componentReady = true;
|
|
174
|
+
}
|
|
175
|
+
return component;
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
...options,
|
|
179
|
+
signal: options?.signal ? AbortSignal.any([options.signal, handlerSignal]) : handlerSignal,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
} finally {
|
|
183
|
+
customSettled = true;
|
|
184
|
+
if (componentReady) timeoutBudget?.resume();
|
|
185
|
+
}
|
|
186
|
+
},
|
|
143
187
|
editor: (title, prefill, dialogOptions, editorOptions) =>
|
|
144
|
-
ui.editor(title, prefill, attachHandlerSignal(dialogOptions, handlerSignal), editorOptions),
|
|
145
|
-
} satisfies Pick<ExtensionUIContext, "select" | "confirm" | "input" | "askDialog" | "editor">;
|
|
188
|
+
runDialog(() => ui.editor(title, prefill, attachHandlerSignal(dialogOptions, handlerSignal), editorOptions)),
|
|
189
|
+
} satisfies Pick<ExtensionUIContext, "select" | "confirm" | "input" | "askDialog" | "custom" | "editor">;
|
|
146
190
|
const delegatedMethods = new Map<PropertyKey, unknown>();
|
|
147
191
|
|
|
148
192
|
return new Proxy(ui, {
|
|
@@ -167,10 +211,14 @@ function createHandlerUIContext(ui: ExtensionUIContext, handlerSignal: AbortSign
|
|
|
167
211
|
* `pi.setModel()` and then reading `ctx.model` would see a stale model.
|
|
168
212
|
* Prototype delegation keeps every getter live while overriding `ui`.
|
|
169
213
|
*/
|
|
170
|
-
function createHandlerContext(
|
|
214
|
+
function createHandlerContext(
|
|
215
|
+
ctx: ExtensionContext,
|
|
216
|
+
handlerSignal: AbortSignal,
|
|
217
|
+
timeoutBudget?: HandlerTimeoutBudget,
|
|
218
|
+
): ExtensionContext {
|
|
171
219
|
const scoped: ExtensionContext = Object.create(ctx);
|
|
172
220
|
Object.defineProperty(scoped, "ui", {
|
|
173
|
-
value: createHandlerUIContext(ctx.ui, handlerSignal),
|
|
221
|
+
value: createHandlerUIContext(ctx.ui, handlerSignal, timeoutBudget),
|
|
174
222
|
enumerable: true,
|
|
175
223
|
configurable: true,
|
|
176
224
|
});
|
|
@@ -190,7 +238,7 @@ function createHandlerContext(ctx: ExtensionContext, handlerSignal: AbortSignal)
|
|
|
190
238
|
* can `clearTimeout` on the winning branch.
|
|
191
239
|
*/
|
|
192
240
|
async function raceHandlerWithTimeout<T>(
|
|
193
|
-
work: (handlerSignal: AbortSignal) => Promise<T> | T,
|
|
241
|
+
work: (handlerSignal: AbortSignal, timeoutBudget: HandlerTimeoutBudget) => Promise<T> | T,
|
|
194
242
|
timeoutMs: number,
|
|
195
243
|
signal?: AbortSignal,
|
|
196
244
|
): Promise<T | typeof EXTENSION_HANDLER_TIMEOUT | typeof EXTENSION_HANDLER_ABORTED> {
|
|
@@ -203,13 +251,52 @@ async function raceHandlerWithTimeout<T>(
|
|
|
203
251
|
>();
|
|
204
252
|
const onAbort = () => resolveInterrupt(EXTENSION_HANDLER_ABORTED);
|
|
205
253
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
206
|
-
|
|
254
|
+
let timer: Timer | undefined;
|
|
255
|
+
let remainingMs = timeoutMs;
|
|
256
|
+
let activeSince = performance.now();
|
|
257
|
+
let pauseDepth = 0;
|
|
258
|
+
let settled = false;
|
|
259
|
+
const clearTimer = () => {
|
|
260
|
+
if (timer === undefined) return;
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
timer = undefined;
|
|
263
|
+
};
|
|
264
|
+
const expire = () => {
|
|
265
|
+
if (settled) return;
|
|
266
|
+
settled = true;
|
|
267
|
+
clearTimer();
|
|
207
268
|
timeoutController.abort(new DOMException(`Handler timed out after ${timeoutMs}ms`, "TimeoutError"));
|
|
208
269
|
resolveInterrupt(EXTENSION_HANDLER_TIMEOUT);
|
|
209
|
-
}
|
|
270
|
+
};
|
|
271
|
+
const armTimer = () => {
|
|
272
|
+
if (settled || pauseDepth > 0) return;
|
|
273
|
+
activeSince = performance.now();
|
|
274
|
+
timer = setTimeout(expire, Math.max(0, remainingMs));
|
|
275
|
+
};
|
|
276
|
+
const settle = () => {
|
|
277
|
+
if (settled) return;
|
|
278
|
+
settled = true;
|
|
279
|
+
clearTimer();
|
|
280
|
+
};
|
|
281
|
+
const timeoutBudget: HandlerTimeoutBudget = {
|
|
282
|
+
pause: () => {
|
|
283
|
+
if (settled) return;
|
|
284
|
+
pauseDepth++;
|
|
285
|
+
if (pauseDepth !== 1) return;
|
|
286
|
+
remainingMs = Math.max(0, remainingMs - (performance.now() - activeSince));
|
|
287
|
+
clearTimer();
|
|
288
|
+
if (remainingMs <= 0) expire();
|
|
289
|
+
},
|
|
290
|
+
resume: () => {
|
|
291
|
+
if (settled || pauseDepth === 0) return;
|
|
292
|
+
pauseDepth--;
|
|
293
|
+
if (pauseDepth === 0) armTimer();
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
armTimer();
|
|
210
297
|
try {
|
|
211
298
|
if (signal?.aborted) return EXTENSION_HANDLER_ABORTED;
|
|
212
|
-
const workPromise = Promise.resolve(work(handlerSignal));
|
|
299
|
+
const workPromise = Promise.resolve(work(handlerSignal, timeoutBudget));
|
|
213
300
|
const result = await Promise.race([workPromise, interruptPromise]);
|
|
214
301
|
if (result === EXTENSION_HANDLER_TIMEOUT) {
|
|
215
302
|
await Promise.race([
|
|
@@ -222,7 +309,7 @@ async function raceHandlerWithTimeout<T>(
|
|
|
222
309
|
}
|
|
223
310
|
return result;
|
|
224
311
|
} finally {
|
|
225
|
-
|
|
312
|
+
settle();
|
|
226
313
|
signal?.removeEventListener("abort", onAbort);
|
|
227
314
|
}
|
|
228
315
|
}
|
|
@@ -1043,23 +1130,33 @@ export class ExtensionRunner {
|
|
|
1043
1130
|
ext: Extension,
|
|
1044
1131
|
timeoutMs: number,
|
|
1045
1132
|
onFailure?: (kind: "timeout" | "error", message: string) => TResult,
|
|
1133
|
+
outerSignal?: AbortSignal,
|
|
1046
1134
|
): Promise<TResult | undefined> {
|
|
1047
|
-
|
|
1135
|
+
// `session_stop` carries its own signal on the event; `tool_call` receives
|
|
1136
|
+
// the outer dispatch signal (loop request or wrapper execute) so an abort
|
|
1137
|
+
// while a handler awaits a human dialog cancels the dialog and settles the
|
|
1138
|
+
// gate without executing the underlying tool. Compose whichever apply.
|
|
1139
|
+
const sessionStopSignal =
|
|
1048
1140
|
event.type === "session_stop" && "signal" in event && event.signal instanceof AbortSignal
|
|
1049
1141
|
? event.signal
|
|
1050
1142
|
: undefined;
|
|
1143
|
+
const signals = [outerSignal, sessionStopSignal].filter((s): s is AbortSignal => s !== undefined);
|
|
1144
|
+
const signal = signals.length === 0 ? undefined : signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
1051
1145
|
if (signal?.aborted) return undefined;
|
|
1052
1146
|
const registrationScope: ToolRegistrationScope = { pending: new Set(), closed: false };
|
|
1053
1147
|
let handlerResult: TResult | typeof EXTENSION_HANDLER_TIMEOUT | typeof EXTENSION_HANDLER_ABORTED | undefined;
|
|
1054
1148
|
let handlerFailure: { error: unknown } | undefined;
|
|
1055
1149
|
try {
|
|
1056
1150
|
handlerResult = await raceHandlerWithTimeout(
|
|
1057
|
-
async handlerSignal => {
|
|
1151
|
+
async (handlerSignal, budget) => {
|
|
1058
1152
|
registrationScope.signal = handlerSignal;
|
|
1059
1153
|
let result: TResult | undefined;
|
|
1060
1154
|
try {
|
|
1061
1155
|
result = await this.#toolRegistrationScope.run(registrationScope, () =>
|
|
1062
|
-
handler(
|
|
1156
|
+
handler(
|
|
1157
|
+
event,
|
|
1158
|
+
createHandlerContext(ctx, handlerSignal, event.type === "tool_call" ? budget : undefined),
|
|
1159
|
+
),
|
|
1063
1160
|
);
|
|
1064
1161
|
} catch (error) {
|
|
1065
1162
|
handlerFailure = { error };
|
|
@@ -1220,8 +1317,8 @@ export class ExtensionRunner {
|
|
|
1220
1317
|
/**
|
|
1221
1318
|
* Emit a `tool_call` event to every subscribed extension before the tool executes.
|
|
1222
1319
|
*
|
|
1223
|
-
* Each handler is bounded by `
|
|
1224
|
-
* matches the timeout policy already applied to `emitToolResult` and every
|
|
1320
|
+
* Each handler is bounded by `extensionHandlers.toolCallTimeoutMs` (default
|
|
1321
|
+
* 30s). This matches the timeout policy already applied to `emitToolResult` and every
|
|
1225
1322
|
* other handler routed through `#runHandlerWithTimeout`; without it a single
|
|
1226
1323
|
* hung extension (unresolved `await`, network call with no timeout) would
|
|
1227
1324
|
* park `ExtensionToolWrapper.execute` indefinitely and freeze tool
|
|
@@ -1232,9 +1329,11 @@ export class ExtensionRunner {
|
|
|
1232
1329
|
* pre-execution gate — an unresponsive extension MUST NOT be treated as
|
|
1233
1330
|
* silent consent to run the tool.
|
|
1234
1331
|
*/
|
|
1235
|
-
async emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {
|
|
1332
|
+
async emitToolCall(event: ToolCallEvent, signal?: AbortSignal): Promise<ToolCallEventResult | undefined> {
|
|
1236
1333
|
const ctx = this.createContext();
|
|
1237
|
-
const timeoutMs =
|
|
1334
|
+
const timeoutMs = normalizeHandlerTimeout(
|
|
1335
|
+
this.settings?.get("extensionHandlers.toolCallTimeoutMs") ?? extensionHandlerTimeoutMs,
|
|
1336
|
+
);
|
|
1238
1337
|
let result: ToolCallEventResult | undefined;
|
|
1239
1338
|
|
|
1240
1339
|
for (const ext of this.extensions) {
|
|
@@ -1255,6 +1354,7 @@ export class ExtensionRunner {
|
|
|
1255
1354
|
? `Extension ${ext.path} timed out after ${timeoutMs}ms`
|
|
1256
1355
|
: `Extension ${ext.path} failed: ${message}`,
|
|
1257
1356
|
}),
|
|
1357
|
+
signal,
|
|
1258
1358
|
);
|
|
1259
1359
|
|
|
1260
1360
|
if (handlerResult) {
|
|
@@ -1266,6 +1366,9 @@ export class ExtensionRunner {
|
|
|
1266
1366
|
}
|
|
1267
1367
|
}
|
|
1268
1368
|
|
|
1369
|
+
if (signal?.aborted) {
|
|
1370
|
+
return { block: true, reason: `Tool execution was cancelled while an extension handler was pending` };
|
|
1371
|
+
}
|
|
1269
1372
|
return result;
|
|
1270
1373
|
}
|
|
1271
1374
|
|
|
@@ -234,6 +234,8 @@ export interface ExtensionCustomOptions {
|
|
|
234
234
|
overlayOptions?: OverlayOptions | (() => OverlayOptions);
|
|
235
235
|
/** Invoked with the overlay handle once the overlay is created (overlay mode only). */
|
|
236
236
|
onHandle?: (handle: OverlayHandle) => void;
|
|
237
|
+
/** Abort the custom UI and reject its promise. */
|
|
238
|
+
signal?: AbortSignal;
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
/** Wrap the current autocomplete provider with additional behavior (pi-compatible). */
|
|
@@ -205,15 +205,18 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
205
205
|
let effectiveParams = params;
|
|
206
206
|
if (!loopEmittedToolCall && this.runner.hasHandlers("tool_call")) {
|
|
207
207
|
try {
|
|
208
|
-
const callResult = (await this.runner.emitToolCall(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
208
|
+
const callResult = (await this.runner.emitToolCall(
|
|
209
|
+
{
|
|
210
|
+
type: "tool_call",
|
|
211
|
+
toolName: this.tool.name,
|
|
212
|
+
toolCallId,
|
|
213
|
+
input: normalizeToolEventInput(
|
|
214
|
+
this.tool.name,
|
|
215
|
+
resolveToolEventInput(this.tool, toolEventArgs(params, context)),
|
|
216
|
+
),
|
|
217
|
+
},
|
|
218
|
+
signal,
|
|
219
|
+
)) as ToolCallEventResult | undefined;
|
|
217
220
|
|
|
218
221
|
if (callResult?.block) {
|
|
219
222
|
const reason = callResult.reason || "Tool execution was blocked by an extension";
|
|
@@ -265,7 +268,10 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
265
268
|
|
|
266
269
|
if (approvalCheck.required) {
|
|
267
270
|
const scheduledCall = context?.toolCall?.toolCalls[context.toolCall.index];
|
|
268
|
-
if (
|
|
271
|
+
if (
|
|
272
|
+
scheduledCall?.id === toolCallId &&
|
|
273
|
+
(scheduledCall.name === this.tool.name || scheduledCall.name === this.tool.customWireName)
|
|
274
|
+
) {
|
|
269
275
|
await untilAborted(signal, () => this.runner.waitForToolApprovalPreview(toolCallId));
|
|
270
276
|
}
|
|
271
277
|
|
|
@@ -134,6 +134,10 @@ export const getModels = getBundledModels;
|
|
|
134
134
|
*
|
|
135
135
|
* Legacy `/compat` callers pass {@link SimpleStreamOptions}; routing through
|
|
136
136
|
* `streamSimple` preserves option normalization before provider dispatch.
|
|
137
|
+
*
|
|
138
|
+
* Transient-failure retry (overload, rate-limit, 5xx) is the **caller's
|
|
139
|
+
* responsibility**. Oneshot callers that collect the full result before acting
|
|
140
|
+
* should wrap with `retryTransientCompletion` from `@oh-my-pi/pi-ai`.
|
|
137
141
|
*/
|
|
138
142
|
export function streamSimpleOpenAIResponses(
|
|
139
143
|
model: Model<"openai-responses">,
|
|
@@ -11,6 +11,7 @@ import * as os from "node:os";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
|
|
13
13
|
import { isEnoent, logger, pathIsWithin } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { expandTilde } from "../../../tools/path-utils";
|
|
14
15
|
import { normalizePluginRuntimeConfig } from "../runtime-config";
|
|
15
16
|
import type { PluginRuntimeConfig } from "../types";
|
|
16
17
|
|
|
@@ -107,12 +108,11 @@ export class MarketplaceManager {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
const sourceType = classifySource(source);
|
|
110
|
-
const normalizedSource =
|
|
111
|
-
sourceType === "local"
|
|
112
|
-
? path.resolve(source.startsWith("~/") ? path.join(os.homedir(), source.slice(2)) : source)
|
|
113
|
-
: source;
|
|
111
|
+
const normalizedSource = sourceType === "local" ? path.resolve(expandTilde(source)) : source;
|
|
114
112
|
|
|
115
|
-
const catalogPath = path.
|
|
113
|
+
const catalogPath = path.resolve(
|
|
114
|
+
expandTilde(path.join(this.#opts.marketplacesCacheDir, catalog.name, "marketplace.json")),
|
|
115
|
+
);
|
|
116
116
|
|
|
117
117
|
// Persist the fetched catalog so subsequent reads don't require re-fetching.
|
|
118
118
|
await Bun.write(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
@@ -174,11 +174,13 @@ export class MarketplaceManager {
|
|
|
174
174
|
await promoteCloneToCache(clonePath, this.#opts.marketplacesCacheDir, catalog.name);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
// Overwrite cached catalog
|
|
178
|
-
|
|
177
|
+
// Overwrite the cached catalog and migrate legacy home-relative registry entries.
|
|
178
|
+
const catalogPath = path.resolve(expandTilde(existing.catalogPath));
|
|
179
|
+
await Bun.write(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
179
180
|
|
|
180
181
|
const updatedEntry: MarketplaceRegistryEntry = {
|
|
181
182
|
...existing,
|
|
183
|
+
catalogPath,
|
|
182
184
|
updatedAt: new Date().toISOString(),
|
|
183
185
|
};
|
|
184
186
|
|
|
@@ -893,14 +895,13 @@ export class MarketplaceManager {
|
|
|
893
895
|
}
|
|
894
896
|
|
|
895
897
|
async #readCatalog(entry: MarketplaceRegistryEntry): Promise<MarketplaceCatalog> {
|
|
898
|
+
const catalogPath = path.resolve(expandTilde(entry.catalogPath));
|
|
896
899
|
try {
|
|
897
|
-
const content = await Bun.file(
|
|
898
|
-
return parseMarketplaceCatalog(content,
|
|
900
|
+
const content = await Bun.file(catalogPath).text();
|
|
901
|
+
return parseMarketplaceCatalog(content, catalogPath);
|
|
899
902
|
} catch (err) {
|
|
900
903
|
if (isEnoent(err)) {
|
|
901
|
-
throw new Error(
|
|
902
|
-
`Marketplace catalog not found at ${entry.catalogPath}. Try: /marketplace update ${entry.name}`,
|
|
903
|
-
);
|
|
904
|
+
throw new Error(`Marketplace catalog not found at ${catalogPath}. Try: /marketplace update ${entry.name}`);
|
|
904
905
|
}
|
|
905
906
|
throw err;
|
|
906
907
|
}
|
|
@@ -918,14 +919,10 @@ export class MarketplaceManager {
|
|
|
918
919
|
*/
|
|
919
920
|
#resolveMarketplaceRoot(entry: MarketplaceRegistryEntry): string {
|
|
920
921
|
if (entry.sourceType === "local") {
|
|
921
|
-
|
|
922
|
-
const expanded = entry.sourceUri.startsWith("~/")
|
|
923
|
-
? path.join(os.homedir(), entry.sourceUri.slice(2))
|
|
924
|
-
: entry.sourceUri;
|
|
925
|
-
return path.resolve(expanded);
|
|
922
|
+
return path.resolve(expandTilde(entry.sourceUri));
|
|
926
923
|
}
|
|
927
924
|
// For git/github/url sources, the catalog lives at <cloneDir>/marketplace.json
|
|
928
925
|
// under marketplacesCacheDir/<name>/; parent = <marketplacesCacheDir>/<name>/
|
|
929
|
-
return path.dirname(entry.catalogPath);
|
|
926
|
+
return path.dirname(path.resolve(expandTilde(entry.catalogPath)));
|
|
930
927
|
}
|
|
931
928
|
}
|
package/src/irc/bus.ts
CHANGED
|
@@ -276,7 +276,7 @@ export class IrcBus {
|
|
|
276
276
|
if (liveness) {
|
|
277
277
|
const { registry, senderId } = liveness;
|
|
278
278
|
const hasRunningSender = (from?: string): boolean =>
|
|
279
|
-
registry.listVisibleTo(senderId).some(ref => ref
|
|
279
|
+
registry.listVisibleTo(senderId).some(ref => registry.isRunning(ref) && (!from || ref.id === from));
|
|
280
280
|
const check = filter.from ? () => hasRunningSender(filter.from) : () => hasRunningSender();
|
|
281
281
|
unsubscribeLiveness = registry.onChange(() => {
|
|
282
282
|
if (!check()) {
|
package/src/launch/broker.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { hostHasInheritableConsole } from "../eval/py/spawn-options";
|
|
|
8
8
|
import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
|
|
9
9
|
import { workerEnvFromParent } from "../subprocess/worker-client";
|
|
10
10
|
import { daemonBrokerEndpoint } from "./paths";
|
|
11
|
-
import { hasLiveDaemonProjectPresence } from "./presence";
|
|
11
|
+
import { hasLiveDaemonProjectPresence, pruneDeadDaemonRuntimeDirs } from "./presence";
|
|
12
12
|
import {
|
|
13
13
|
DAEMON_IDLE_GRACE_ENV,
|
|
14
14
|
DAEMON_PROJECT_DIR_ENV,
|
|
@@ -1384,6 +1384,13 @@ export async function startDaemonBrokerFromEnvironment(options: DaemonBrokerStar
|
|
|
1384
1384
|
const lease = await acquireBrokerLease(runtimeDir);
|
|
1385
1385
|
if (!lease) return;
|
|
1386
1386
|
setProcessName("omp daemon broker");
|
|
1387
|
+
// Reclaim sibling daemon scopes left behind by dead brokers (issue #8674).
|
|
1388
|
+
// Detached and non-throwing so it never delays clients connecting to us.
|
|
1389
|
+
void pruneDeadDaemonRuntimeDirs(runtimeDir).catch(error => {
|
|
1390
|
+
logger.warn("Daemon runtime prune failed", {
|
|
1391
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1392
|
+
});
|
|
1393
|
+
});
|
|
1387
1394
|
const token = (await Bun.file(path.join(runtimeDir, TOKEN_FILE)).text()).trim();
|
|
1388
1395
|
if (!token) throw new Error("Daemon broker token is empty");
|
|
1389
1396
|
const broker = new DaemonBroker(projectDir, runtimeDir, token, idleGraceMs, restartBackoffBaseMs);
|