@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.11
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 +95 -4
- package/dist/cli.js +23087 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/async/job-manager.d.ts +18 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/dry-balance-cli.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +1 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
- package/dist/types/cli/usage-cli.d.ts +72 -0
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/model-discovery.d.ts +55 -0
- package/dist/types/config/model-registry.d.ts +7 -219
- package/dist/types/config/model-resolver.d.ts +16 -10
- package/dist/types/config/model-roles.d.ts +28 -0
- package/dist/types/config/models-config-schema.d.ts +523 -42
- package/dist/types/config/models-config.d.ts +385 -0
- package/dist/types/config/settings-schema.d.ts +12 -7
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/eval/backend.d.ts +0 -2
- package/dist/types/eval/idle-timeout.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -3
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/index.d.ts +2 -2
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +3 -0
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/mnemopi/state.d.ts +11 -1
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +25 -6
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +1 -1
- package/examples/extensions/tools.ts +5 -4
- package/package.json +14 -11
- package/scripts/build-binary.ts +18 -23
- package/scripts/bundle-dist.ts +81 -0
- package/scripts/{dev-launch → omp} +1 -1
- package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
- package/src/async/job-manager.ts +57 -3
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/capability/fs.ts +10 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -3
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +1 -1
- package/src/cli/gallery-fixtures/types.ts +5 -1
- package/src/cli/list-models.ts +2 -1
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +69 -5
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/read.ts +6 -3
- package/src/commands/usage.ts +35 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/model-selection.ts +1 -1
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +231 -1019
- package/src/config/model-resolver.ts +113 -156
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +57 -8
- package/src/config/models-config.ts +129 -0
- package/src/config/settings-schema.ts +18 -4
- package/src/config/settings.ts +37 -1
- package/src/dap/client.ts +124 -37
- package/src/dap/session.ts +259 -158
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/raw-sse.ts +1 -1
- package/src/edit/diff.ts +47 -3
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +8 -2
- package/src/edit/index.ts +16 -1
- package/src/edit/modes/patch.ts +52 -0
- package/src/edit/modes/replace.ts +56 -22
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +36 -10
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/backend.ts +0 -2
- package/src/eval/completion-bridge.ts +2 -1
- package/src/eval/idle-timeout.ts +2 -9
- package/src/eval/js/context-manager.ts +6 -8
- package/src/eval/js/executor.ts +6 -2
- package/src/eval/js/index.ts +0 -2
- package/src/eval/js/shared/helpers.ts +5 -6
- package/src/eval/js/shared/local-module-loader.ts +1 -1
- package/src/eval/js/shared/prelude.txt +62 -1
- package/src/eval/js/shared/rewrite-imports.ts +40 -22
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +0 -2
- package/src/eval/py/kernel.ts +19 -0
- package/src/eval/py/runner.py +107 -3
- package/src/exec/bash-executor.ts +3 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/types.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +6 -1
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/internal-urls/issue-pr-protocol.ts +12 -5
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/lib/xai-http.ts +1 -1
- package/src/lsp/client.ts +118 -38
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/index.ts +31 -22
- package/src/lsp/render.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/lsp/utils.ts +28 -10
- package/src/main.ts +165 -17
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +2 -1
- package/src/mnemopi/backend.ts +25 -3
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +19 -13
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/copy-selector.ts +1 -1
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +1 -1
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-selector.ts +4 -2
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +258 -53
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +10 -3
- package/src/modes/controllers/event-controller.ts +73 -4
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +3 -9
- package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
- package/src/modes/setup-wizard/scenes/providers.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +1 -1
- package/src/modes/setup-wizard/scenes/types.ts +1 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +2 -1
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/librarian.md +1 -2
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +5 -5
- package/src/prompts/agents/task.md +5 -5
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/background-tan-dispatch.md +1 -1
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +2 -2
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/system/manual-continue.md +1 -1
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +9 -9
- package/src/prompts/system/plan-mode-active.md +4 -4
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +13 -24
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +5 -7
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +23 -10
- package/src/session/agent-session.ts +44 -10
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +23 -2
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/ssh/connection-manager.ts +27 -0
- package/src/task/commands.ts +2 -1
- package/src/task/executor.ts +61 -53
- package/src/task/index.ts +137 -60
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +2 -2
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +2 -1
- package/src/tiny/title-client.ts +26 -11
- package/src/tools/archive-reader.ts +30 -2
- package/src/tools/ask.ts +104 -21
- package/src/tools/ast-edit.ts +25 -5
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +27 -7
- package/src/tools/bash.ts +54 -13
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +4 -1
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +55 -16
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +5 -5
- package/src/tools/eval.ts +0 -2
- package/src/tools/fetch.ts +33 -10
- package/src/tools/gh-cache-invalidation.ts +63 -8
- package/src/tools/gh-renderer.ts +1 -1
- package/src/tools/gh.ts +172 -29
- package/src/tools/github-cache.ts +70 -6
- package/src/tools/image-gen.ts +3 -9
- package/src/tools/irc.ts +5 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/read.ts +202 -61
- package/src/tools/render-utils.ts +3 -3
- package/src/tools/resolve.ts +1 -1
- package/src/tools/search.ts +92 -29
- package/src/tools/sqlite-reader.ts +17 -5
- package/src/tools/ssh.ts +8 -8
- package/src/tools/todo.ts +38 -8
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/file-mentions.ts +2 -1
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +6 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/providers/codex.ts +2 -1
- package/src/web/search/providers/gemini.ts +2 -3
- package/src/web/search/render.ts +8 -6
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-id-affixes.d.ts +0 -12
- package/dist/types/config/model-provider-priority.d.ts +0 -1
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/src/config/model-equivalence.ts +0 -875
- package/src/config/model-id-affixes.ts +0 -81
- package/src/config/model-provider-priority.ts +0 -56
- package/src/exec/idle-timeout-watchdog.ts +0 -126
package/src/task/index.ts
CHANGED
|
@@ -43,7 +43,7 @@ import type { LocalProtocolOptions } from "../internal-urls";
|
|
|
43
43
|
import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
|
|
44
44
|
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
45
45
|
import * as git from "../utils/git";
|
|
46
|
-
import { discoverAgents, getAgent } from "./discovery";
|
|
46
|
+
import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
|
|
47
47
|
import { runSubprocess } from "./executor";
|
|
48
48
|
import { AgentOutputManager } from "./output-manager";
|
|
49
49
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
@@ -242,6 +242,88 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
|
|
|
242
242
|
return "task.simple is set to independent, so the task tool does not accept `context` or `schema`. Put all required background and output expectations inside each task assignment or the selected agent definition.";
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/** Sentinel for async jobs whose subagent finished with a failing result; batch counters are already updated. */
|
|
246
|
+
class TaskJobError extends Error {}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Validate task ids: every task needs a non-empty id and ids must be unique
|
|
250
|
+
* (case-insensitive). Returns a problem description, or undefined when valid.
|
|
251
|
+
*/
|
|
252
|
+
function validateTaskIds(tasks: TaskParams["tasks"]): string | undefined {
|
|
253
|
+
const missingTaskIndexes: number[] = [];
|
|
254
|
+
const idIndexes = new Map<string, number[]>();
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
257
|
+
const id = tasks[i]?.id;
|
|
258
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
259
|
+
missingTaskIndexes.push(i);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const normalizedId = id.toLowerCase();
|
|
263
|
+
const indexes = idIndexes.get(normalizedId);
|
|
264
|
+
if (indexes) {
|
|
265
|
+
indexes.push(i);
|
|
266
|
+
} else {
|
|
267
|
+
idIndexes.set(normalizedId, [i]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
|
|
272
|
+
for (const [normalizedId, indexes] of idIndexes.entries()) {
|
|
273
|
+
if (indexes.length > 1) {
|
|
274
|
+
duplicateIds.push({
|
|
275
|
+
id: tasks[indexes[0]]?.id ?? normalizedId,
|
|
276
|
+
indexes,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (missingTaskIndexes.length === 0 && duplicateIds.length === 0) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const problems: string[] = [];
|
|
286
|
+
if (missingTaskIndexes.length > 0) {
|
|
287
|
+
problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
|
|
288
|
+
}
|
|
289
|
+
if (duplicateIds.length > 0) {
|
|
290
|
+
const details = duplicateIds.map(entry => `${entry.id} (indexes ${entry.indexes.join(", ")})`).join("; ");
|
|
291
|
+
problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
|
|
292
|
+
}
|
|
293
|
+
return `Invalid tasks: ${problems.join(". ")}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Process-level memo for create-time agent discovery, keyed by resolved cwd.
|
|
298
|
+
*
|
|
299
|
+
* `TaskTool.create` runs for every (sub)agent session in this process and the
|
|
300
|
+
* walk-up + plugin-registry scan in `discoverAgents` is identical for a given
|
|
301
|
+
* cwd, so repeat creations reuse the first scan. Execution-time discovery
|
|
302
|
+
* (`#executeSync`) intentionally stays fresh. The memo also tracks the live
|
|
303
|
+
* `discoverAgents` binding: test spies swap that binding, which invalidates
|
|
304
|
+
* the memo automatically.
|
|
305
|
+
*/
|
|
306
|
+
const discoveryMemo = new Map<string, Promise<DiscoveryResult>>();
|
|
307
|
+
let discoveryMemoFn: typeof discoverAgents | undefined;
|
|
308
|
+
|
|
309
|
+
function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
310
|
+
const fn = discoverAgents;
|
|
311
|
+
if (discoveryMemoFn !== fn) {
|
|
312
|
+
discoveryMemoFn = fn;
|
|
313
|
+
discoveryMemo.clear();
|
|
314
|
+
}
|
|
315
|
+
const key = path.resolve(cwd);
|
|
316
|
+
let pending = discoveryMemo.get(key);
|
|
317
|
+
if (!pending) {
|
|
318
|
+
pending = fn(cwd);
|
|
319
|
+
discoveryMemo.set(key, pending);
|
|
320
|
+
pending.catch(() => {
|
|
321
|
+
if (discoveryMemo.get(key) === pending) discoveryMemo.delete(key);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return pending;
|
|
325
|
+
}
|
|
326
|
+
|
|
245
327
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
246
328
|
// Tool Class
|
|
247
329
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -325,7 +407,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
325
407
|
* Create a TaskTool instance with async agent discovery.
|
|
326
408
|
*/
|
|
327
409
|
static async create(session: ToolSession): Promise<TaskTool> {
|
|
328
|
-
const { agents } = await
|
|
410
|
+
const { agents } = await discoverAgentsForCreate(session.cwd);
|
|
329
411
|
return new TaskTool(session, agents);
|
|
330
412
|
}
|
|
331
413
|
|
|
@@ -363,6 +445,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
363
445
|
return this.#executeSync(_toolCallId, params, signal, onUpdate);
|
|
364
446
|
}
|
|
365
447
|
|
|
448
|
+
const taskIdProblem = validateTaskIds(taskItems);
|
|
449
|
+
if (taskIdProblem) {
|
|
450
|
+
return createTaskModeError(taskIdProblem);
|
|
451
|
+
}
|
|
452
|
+
|
|
366
453
|
const outputManager =
|
|
367
454
|
this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
|
|
368
455
|
const uniqueIds = await outputManager.allocateBatch(taskItems.map(t => t.id));
|
|
@@ -396,9 +483,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
396
483
|
let failedJobs = 0;
|
|
397
484
|
|
|
398
485
|
const getProgressSnapshot = (): AgentProgress[] => {
|
|
486
|
+
// Shallow copies: top-level fields are reassigned (never mutated in
|
|
487
|
+
// place) and the large nested payloads (extractedToolData) are
|
|
488
|
+
// immutable once attached — structuredClone here cost O(batch × payload)
|
|
489
|
+
// per progress event.
|
|
399
490
|
return Array.from(progressByTaskId.values())
|
|
400
491
|
.sort((a, b) => a.index - b.index)
|
|
401
|
-
.map(progress =>
|
|
492
|
+
.map(progress => ({ ...progress }));
|
|
402
493
|
};
|
|
403
494
|
|
|
404
495
|
const buildAsyncDetails = (state: "running" | "completed" | "failed", jobId: string): TaskToolDetails => ({
|
|
@@ -424,6 +515,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
424
515
|
const taskItem = taskItems[i];
|
|
425
516
|
if (signal?.aborted) {
|
|
426
517
|
failedSchedules.push(`${taskItem.id}: cancelled before scheduling`);
|
|
518
|
+
completedJobs += 1;
|
|
427
519
|
const progress = progressByTaskId.get(taskItem.id);
|
|
428
520
|
if (progress) {
|
|
429
521
|
progress.status = "aborted";
|
|
@@ -438,7 +530,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
438
530
|
const jobId = manager.register(
|
|
439
531
|
"task",
|
|
440
532
|
label,
|
|
441
|
-
async ({ signal: runSignal, reportProgress }) => {
|
|
533
|
+
async ({ signal: runSignal, reportProgress, markRunning }) => {
|
|
442
534
|
const startedAt = Date.now();
|
|
443
535
|
const progress = progressByTaskId.get(taskItem.id);
|
|
444
536
|
await semaphore.acquire();
|
|
@@ -447,8 +539,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
447
539
|
if (progress) {
|
|
448
540
|
progress.status = "aborted";
|
|
449
541
|
}
|
|
542
|
+
completedJobs += 1;
|
|
543
|
+
failedJobs += 1;
|
|
450
544
|
throw new Error("Aborted before execution");
|
|
451
545
|
}
|
|
546
|
+
markRunning();
|
|
452
547
|
if (progress) {
|
|
453
548
|
progress.status = "running";
|
|
454
549
|
}
|
|
@@ -462,12 +557,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
462
557
|
]);
|
|
463
558
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
464
559
|
const singleResult = result.details?.results[0];
|
|
560
|
+
// A missing per-task result means #executeSync failed at the
|
|
561
|
+
// tool level (results: []) — treat it as a failure, not success.
|
|
562
|
+
const resultFailed =
|
|
563
|
+
!singleResult || (singleResult.aborted ?? false) || singleResult.exitCode !== 0;
|
|
465
564
|
if (progress) {
|
|
466
|
-
progress.status = singleResult?.aborted
|
|
467
|
-
? "aborted"
|
|
468
|
-
: (singleResult?.exitCode ?? 0) === 0
|
|
469
|
-
? "completed"
|
|
470
|
-
: "failed";
|
|
565
|
+
progress.status = singleResult?.aborted ? "aborted" : resultFailed ? "failed" : "completed";
|
|
471
566
|
progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
|
|
472
567
|
progress.tokens = singleResult?.tokens ?? 0;
|
|
473
568
|
progress.contextTokens = singleResult?.contextTokens;
|
|
@@ -478,7 +573,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
478
573
|
progress.retryState = undefined;
|
|
479
574
|
}
|
|
480
575
|
completedJobs += 1;
|
|
481
|
-
if (
|
|
576
|
+
if (resultFailed) {
|
|
482
577
|
failedJobs += 1;
|
|
483
578
|
}
|
|
484
579
|
const remaining = taskItems.length - completedJobs;
|
|
@@ -498,8 +593,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
498
593
|
`Background task batch complete: ${completedJobs}/${taskItems.length} finished.`,
|
|
499
594
|
);
|
|
500
595
|
}
|
|
596
|
+
if (resultFailed) {
|
|
597
|
+
// Mark the job itself failed; counters above are already updated.
|
|
598
|
+
throw new TaskJobError(finalText);
|
|
599
|
+
}
|
|
501
600
|
return finalText;
|
|
502
601
|
} catch (error) {
|
|
602
|
+
if (error instanceof TaskJobError) {
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
503
605
|
if (progress) {
|
|
504
606
|
progress.status = "failed";
|
|
505
607
|
progress.durationMs = Math.max(0, Date.now() - startedAt);
|
|
@@ -530,6 +632,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
530
632
|
},
|
|
531
633
|
{
|
|
532
634
|
id: label,
|
|
635
|
+
queued: true,
|
|
533
636
|
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
534
637
|
onProgress: (text, details) => {
|
|
535
638
|
const progressDetails =
|
|
@@ -543,6 +646,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
543
646
|
} catch (error) {
|
|
544
647
|
const message = error instanceof Error ? error.message : String(error);
|
|
545
648
|
failedSchedules.push(`${taskItem.id}: ${message}`);
|
|
649
|
+
completedJobs += 1;
|
|
546
650
|
const progress = progressByTaskId.get(taskItem.id);
|
|
547
651
|
if (progress) {
|
|
548
652
|
progress.status = "failed";
|
|
@@ -734,45 +838,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
734
838
|
}
|
|
735
839
|
|
|
736
840
|
const tasks = params.tasks;
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
741
|
-
const id = tasks[i]?.id;
|
|
742
|
-
if (typeof id !== "string" || id.trim() === "") {
|
|
743
|
-
missingTaskIndexes.push(i);
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
const normalizedId = id.toLowerCase();
|
|
747
|
-
const indexes = idIndexes.get(normalizedId);
|
|
748
|
-
if (indexes) {
|
|
749
|
-
indexes.push(i);
|
|
750
|
-
} else {
|
|
751
|
-
idIndexes.set(normalizedId, [i]);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
|
|
756
|
-
for (const [normalizedId, indexes] of idIndexes.entries()) {
|
|
757
|
-
if (indexes.length > 1) {
|
|
758
|
-
duplicateIds.push({
|
|
759
|
-
id: tasks[indexes[0]]?.id ?? normalizedId,
|
|
760
|
-
indexes,
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
if (missingTaskIndexes.length > 0 || duplicateIds.length > 0) {
|
|
766
|
-
const problems: string[] = [];
|
|
767
|
-
if (missingTaskIndexes.length > 0) {
|
|
768
|
-
problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
|
|
769
|
-
}
|
|
770
|
-
if (duplicateIds.length > 0) {
|
|
771
|
-
const details = duplicateIds.map(entry => `${entry.id} (indexes ${entry.indexes.join(", ")})`).join("; ");
|
|
772
|
-
problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
|
|
773
|
-
}
|
|
841
|
+
const taskIdProblem = validateTaskIds(tasks);
|
|
842
|
+
if (taskIdProblem) {
|
|
774
843
|
return {
|
|
775
|
-
content: [{ type: "text", text:
|
|
844
|
+
content: [{ type: "text", text: taskIdProblem }],
|
|
776
845
|
details: {
|
|
777
846
|
projectAgentsDir,
|
|
778
847
|
results: [],
|
|
@@ -951,7 +1020,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
951
1020
|
}
|
|
952
1021
|
emitProgress();
|
|
953
1022
|
|
|
954
|
-
const runTask = async (
|
|
1023
|
+
const runTask = async (
|
|
1024
|
+
task: (typeof tasksWithUniqueIds)[number],
|
|
1025
|
+
index: number,
|
|
1026
|
+
workerSignal?: AbortSignal,
|
|
1027
|
+
) => {
|
|
955
1028
|
if (!isIsolated) {
|
|
956
1029
|
return runSubprocess({
|
|
957
1030
|
cwd: this.session.cwd,
|
|
@@ -973,12 +1046,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
973
1046
|
artifactsDir: effectiveArtifactsDir,
|
|
974
1047
|
contextFile: contextFilePath,
|
|
975
1048
|
enableLsp: subagentLspEnabled,
|
|
976
|
-
signal,
|
|
1049
|
+
signal: workerSignal ?? signal,
|
|
977
1050
|
eventBus: this.session.eventBus,
|
|
978
1051
|
onProgress: progress => {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1052
|
+
// Shallow snapshot; recentTools is mutated in place by the
|
|
1053
|
+
// executor, the rest is reassigned or immutable. A deep clone
|
|
1054
|
+
// here cost O(extractedToolData) per progress event.
|
|
1055
|
+
progressMap.set(index, { ...progress, recentTools: progress.recentTools.slice() });
|
|
982
1056
|
emitProgress();
|
|
983
1057
|
},
|
|
984
1058
|
authStorage: this.session.authStorage,
|
|
@@ -1034,12 +1108,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1034
1108
|
artifactsDir: effectiveArtifactsDir,
|
|
1035
1109
|
contextFile: contextFilePath,
|
|
1036
1110
|
enableLsp: subagentLspEnabled,
|
|
1037
|
-
signal,
|
|
1111
|
+
signal: workerSignal ?? signal,
|
|
1038
1112
|
eventBus: this.session.eventBus,
|
|
1039
1113
|
onProgress: progress => {
|
|
1040
|
-
progressMap.set(index, {
|
|
1041
|
-
...structuredClone(progress),
|
|
1042
|
-
});
|
|
1114
|
+
progressMap.set(index, { ...progress, recentTools: progress.recentTools.slice() });
|
|
1043
1115
|
emitProgress();
|
|
1044
1116
|
},
|
|
1045
1117
|
authStorage: this.session.authStorage,
|
|
@@ -1226,6 +1298,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1226
1298
|
const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
|
|
1227
1299
|
mergeSummary = `\n\n<system-notification>Branch merge failed. ${mergedPart}${failedPart}${conflictPart}\nUnmerged branches remain for manual resolution.</system-notification>`;
|
|
1228
1300
|
}
|
|
1301
|
+
if (mergeResult.stashConflict) {
|
|
1302
|
+
mergeSummary += `\n\n<system-notification>${mergeResult.stashConflict}</system-notification>`;
|
|
1303
|
+
}
|
|
1229
1304
|
}
|
|
1230
1305
|
|
|
1231
1306
|
// Clean up merged branches (keep failed ones for manual resolution)
|
|
@@ -1234,9 +1309,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1234
1309
|
await cleanupTaskBranches(repoRoot, allBranches);
|
|
1235
1310
|
}
|
|
1236
1311
|
} else {
|
|
1237
|
-
// Patch mode:
|
|
1238
|
-
|
|
1239
|
-
const
|
|
1312
|
+
// Patch mode: apply patches from successful tasks. Failed or
|
|
1313
|
+
// aborted siblings must not block completed work from landing.
|
|
1314
|
+
const successfulResults = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted);
|
|
1315
|
+
const patchesInOrder = successfulResults.map(result => result.patchPath).filter(Boolean) as string[];
|
|
1316
|
+
const missingPatch = successfulResults.some(result => !result.patchPath);
|
|
1240
1317
|
if (missingPatch) {
|
|
1241
1318
|
changesApplied = false;
|
|
1242
1319
|
hadAnyChanges = false;
|
package/src/task/parallel.ts
CHANGED
|
@@ -20,13 +20,13 @@ export interface ParallelResult<R> {
|
|
|
20
20
|
*
|
|
21
21
|
* @param items - Items to process
|
|
22
22
|
* @param concurrency - Maximum concurrent operations
|
|
23
|
-
* @param fn - Async function to execute for each item
|
|
23
|
+
* @param fn - Async function to execute for each item; receives a worker signal that fires on abort or fail-fast so in-flight siblings can cancel
|
|
24
24
|
* @param signal - Optional abort signal to stop scheduling new work
|
|
25
25
|
*/
|
|
26
26
|
export async function mapWithConcurrencyLimit<T, R>(
|
|
27
27
|
items: T[],
|
|
28
28
|
concurrency: number,
|
|
29
|
-
fn: (item: T, index: number) => Promise<R>,
|
|
29
|
+
fn: (item: T, index: number, signal: AbortSignal) => Promise<R>,
|
|
30
30
|
signal?: AbortSignal,
|
|
31
31
|
): Promise<ParallelResult<R>> {
|
|
32
32
|
const normalizedConcurrency = Number.isFinite(concurrency) ? Math.floor(concurrency) : items.length;
|
|
@@ -52,7 +52,7 @@ export async function mapWithConcurrencyLimit<T, R>(
|
|
|
52
52
|
const index = nextIndex++;
|
|
53
53
|
if (index >= items.length) return;
|
|
54
54
|
try {
|
|
55
|
-
results[index] = await fn(items[index], index);
|
|
55
|
+
results[index] = await fn(items[index], index, workerSignal);
|
|
56
56
|
} catch (error) {
|
|
57
57
|
// On abort, the fn itself handles it and returns a result
|
|
58
58
|
// Only propagate non-abort errors
|
package/src/task/render.ts
CHANGED
|
@@ -541,7 +541,7 @@ function renderTaskItemLines(tasks: TaskItem[] | undefined, expanded: boolean, t
|
|
|
541
541
|
* the merged result frame so the brief stays visible for the whole task
|
|
542
542
|
* lifecycle — not just until the first progress snapshot replaces the call view.
|
|
543
543
|
*/
|
|
544
|
-
type TaskRenderSection = { lines: string[] };
|
|
544
|
+
type TaskRenderSection = { lines: readonly string[] };
|
|
545
545
|
type ContextSectionRenderer = (width: number) => TaskRenderSection;
|
|
546
546
|
|
|
547
547
|
// Default output-block layout is: left border + one-cell content inset + right
|
|
@@ -578,7 +578,7 @@ export function renderCall(
|
|
|
578
578
|
const header = renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme);
|
|
579
579
|
const contextSectionRenderer = createContextSectionRenderer(args, theme);
|
|
580
580
|
return framedBlock(theme, width => {
|
|
581
|
-
const sections: Array<{ label?: string; lines: string[]; separator?: boolean }> = [];
|
|
581
|
+
const sections: Array<{ label?: string; lines: readonly string[]; separator?: boolean }> = [];
|
|
582
582
|
|
|
583
583
|
if (contextSectionRenderer) sections.push(contextSectionRenderer(width));
|
|
584
584
|
|
package/src/task/worktree.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import * as natives from "@oh-my-pi/pi-natives";
|
|
6
6
|
import { getWorktreeDir, hashPath, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import * as git from "../utils/git";
|
|
8
|
+
import { mapWithConcurrencyLimit } from "./parallel";
|
|
8
9
|
|
|
9
10
|
const { IsoBackendKind } = natives;
|
|
10
11
|
type IsoBackendKind = natives.IsoBackendKind;
|
|
@@ -82,16 +83,16 @@ async function discoverNestedRepos(repoRoot: string): Promise<string[]> {
|
|
|
82
83
|
async function captureUntrackedPatch(repoRoot: string, untracked: readonly string[]): Promise<string> {
|
|
83
84
|
if (untracked.length === 0) return "";
|
|
84
85
|
const nullPath = getGitNoIndexNullPath();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
),
|
|
86
|
+
// Bound concurrent git spawns; large untracked sets would otherwise fork one
|
|
87
|
+
// process per file at once.
|
|
88
|
+
const { results: untrackedDiffs } = await mapWithConcurrencyLimit([...untracked], 8, entry =>
|
|
89
|
+
git.diff(repoRoot, {
|
|
90
|
+
allowFailure: true,
|
|
91
|
+
binary: true,
|
|
92
|
+
noIndex: { left: nullPath, right: entry },
|
|
93
|
+
}),
|
|
93
94
|
);
|
|
94
|
-
return untrackedDiffs.filter(diff => diff
|
|
95
|
+
return untrackedDiffs.filter((diff): diff is string => !!diff?.trim()).join("\n");
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
async function captureRepoBaseline(repoRoot: string): Promise<RepoBaseline> {
|
|
@@ -427,6 +428,8 @@ export interface MergeBranchResult {
|
|
|
427
428
|
merged: string[];
|
|
428
429
|
failed: string[];
|
|
429
430
|
conflict?: string;
|
|
431
|
+
/** Set when cherry-picks landed on HEAD but restoring the stashed working tree failed. */
|
|
432
|
+
stashConflict?: string;
|
|
430
433
|
}
|
|
431
434
|
|
|
432
435
|
/**
|
|
@@ -438,64 +441,69 @@ export async function mergeTaskBranches(
|
|
|
438
441
|
repoRoot: string,
|
|
439
442
|
branches: Array<{ branchName: string; taskId: string; description?: string }>,
|
|
440
443
|
): Promise<MergeBranchResult> {
|
|
441
|
-
|
|
442
|
-
|
|
444
|
+
// Serialize against other in-process git mutations on this repo: concurrent
|
|
445
|
+
// background merges interleaving stash push/pop + cherry-pick would corrupt
|
|
446
|
+
// the working tree (lost uncommitted changes, mixed-up stash entries).
|
|
447
|
+
return git.withRepoLock(repoRoot, async () => {
|
|
448
|
+
const merged: string[] = [];
|
|
449
|
+
const failed: string[] = [];
|
|
443
450
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
451
|
+
// Stash dirty working tree so cherry-pick can operate on a clean HEAD.
|
|
452
|
+
// Without this, cherry-pick refuses to run when uncommitted changes exist.
|
|
453
|
+
const didStash = await git.stash.push(repoRoot, "omp-task-merge");
|
|
447
454
|
|
|
448
|
-
|
|
455
|
+
let conflictResult: MergeBranchResult | undefined;
|
|
449
456
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
await git.cherryPick(repoRoot, branchName);
|
|
454
|
-
} catch (err) {
|
|
457
|
+
try {
|
|
458
|
+
for (const { branchName } of branches) {
|
|
455
459
|
try {
|
|
456
|
-
await git.cherryPick
|
|
457
|
-
} catch {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
failed
|
|
470
|
-
conflict: `${branchName}: ${stderr}`,
|
|
471
|
-
};
|
|
472
|
-
break;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
merged.push(branchName);
|
|
476
|
-
}
|
|
477
|
-
} finally {
|
|
478
|
-
if (didStash) {
|
|
479
|
-
try {
|
|
480
|
-
await git.stash.pop(repoRoot, { index: true });
|
|
481
|
-
} catch {
|
|
482
|
-
// Stash-pop conflicts mean the replayed changes clash with the user's
|
|
483
|
-
// uncommitted edits. Treat this as a merge failure so the caller preserves
|
|
484
|
-
// recovery branches instead of reporting success and deleting them.
|
|
485
|
-
logger.warn("Failed to restore stashed changes after task merge; stash entry preserved");
|
|
486
|
-
if (!conflictResult) {
|
|
460
|
+
await git.cherryPick(repoRoot, branchName);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
try {
|
|
463
|
+
await git.cherryPick.abort(repoRoot);
|
|
464
|
+
} catch {
|
|
465
|
+
/* no state to abort */
|
|
466
|
+
}
|
|
467
|
+
const stderr =
|
|
468
|
+
err instanceof git.GitCommandError
|
|
469
|
+
? err.result.stderr.trim()
|
|
470
|
+
: err instanceof Error
|
|
471
|
+
? err.message
|
|
472
|
+
: String(err);
|
|
473
|
+
failed.push(branchName);
|
|
487
474
|
conflictResult = {
|
|
488
475
|
merged,
|
|
489
|
-
failed: merged,
|
|
490
|
-
conflict:
|
|
491
|
-
"stash pop: cherry-picked changes conflict with uncommitted edits. Run `git stash pop` and resolve manually.",
|
|
476
|
+
failed: [...failed, ...branches.slice(merged.length + failed.length).map(b => b.branchName)],
|
|
477
|
+
conflict: `${branchName}: ${stderr}`,
|
|
492
478
|
};
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
merged.push(branchName);
|
|
483
|
+
}
|
|
484
|
+
} finally {
|
|
485
|
+
if (didStash) {
|
|
486
|
+
try {
|
|
487
|
+
await git.stash.pop(repoRoot, { index: true });
|
|
488
|
+
} catch {
|
|
489
|
+
// Stash-pop conflicts mean the replayed changes clash with the user's
|
|
490
|
+
// uncommitted edits. The cherry-picked commits are already on HEAD, so
|
|
491
|
+
// the merged branches DID land — report them as merged and surface the
|
|
492
|
+
// stash conflict separately instead of claiming they are unmerged.
|
|
493
|
+
logger.warn("Failed to restore stashed changes after task merge; stash entry preserved");
|
|
494
|
+
const stashConflict =
|
|
495
|
+
"stash pop: cherry-picked changes conflict with uncommitted edits. The merged commits are on HEAD; run `git stash pop` and resolve manually.";
|
|
496
|
+
if (conflictResult) {
|
|
497
|
+
conflictResult.stashConflict = stashConflict;
|
|
498
|
+
} else {
|
|
499
|
+
conflictResult = { merged, failed: [], stashConflict };
|
|
500
|
+
}
|
|
493
501
|
}
|
|
494
502
|
}
|
|
495
503
|
}
|
|
496
|
-
}
|
|
497
504
|
|
|
498
|
-
|
|
505
|
+
return conflictResult ?? { merged, failed };
|
|
506
|
+
});
|
|
499
507
|
}
|
|
500
508
|
|
|
501
509
|
/** Clean up temporary task branches. */
|
package/src/thinking.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import { Effort, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { clampThinkingLevelForModel, getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Metadata used to render thinking selector values in the coding-agent UI.
|
package/src/tiny/title-client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { $env, isCompiledBinary, logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
4
|
import { settings } from "../config/settings";
|
|
5
5
|
import { tinyModelDeviceSettingToEnv } from "./device";
|
|
@@ -108,17 +108,28 @@ function tinyWorkerEnv(): Record<string, string> {
|
|
|
108
108
|
for (const key in overlay) merged[key] = overlay[key];
|
|
109
109
|
return merged;
|
|
110
110
|
}
|
|
111
|
+
interface TinyWorkerSpawnCommand {
|
|
112
|
+
cmd: string[];
|
|
113
|
+
cwd?: string;
|
|
114
|
+
}
|
|
111
115
|
|
|
112
116
|
/**
|
|
113
|
-
* Resolve the
|
|
114
|
-
* compiled binary the entry point is the binary itself
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
+
* Resolve the command used to relaunch the agent CLI into tiny-worker mode.
|
|
118
|
+
* In a compiled binary the entry point is the binary itself (no script arg).
|
|
119
|
+
* Otherwise re-enter the declared worker-host entry (source cli.ts or
|
|
120
|
+
* npm-bundle cli.js) with a cwd-relative script path — Bun's subprocess IPC
|
|
121
|
+
* is more reliable that way than with an absolute `.ts` entry under
|
|
122
|
+
* `bun test` — and fall back to this package's own `src/cli.ts` when no host
|
|
123
|
+
* entry is declared (bun test, SDK embedding).
|
|
117
124
|
*/
|
|
118
|
-
function tinyWorkerSpawnCmd():
|
|
119
|
-
if (isCompiledBinary()) return [process.execPath, TINY_WORKER_ARG];
|
|
120
|
-
const
|
|
121
|
-
|
|
125
|
+
function tinyWorkerSpawnCmd(): TinyWorkerSpawnCommand {
|
|
126
|
+
if (isCompiledBinary()) return { cmd: [process.execPath, TINY_WORKER_ARG] };
|
|
127
|
+
const hostEntry = workerHostEntry();
|
|
128
|
+
if (hostEntry) {
|
|
129
|
+
return { cmd: [process.execPath, path.basename(hostEntry), TINY_WORKER_ARG], cwd: path.dirname(hostEntry) };
|
|
130
|
+
}
|
|
131
|
+
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
132
|
+
return { cmd: [process.execPath, "src/cli.ts", TINY_WORKER_ARG], cwd: packageRoot };
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
interface SpawnedSubprocess {
|
|
@@ -143,8 +154,10 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess {
|
|
|
143
154
|
const inbound = new Set<(message: TinyTitleWorkerOutbound) => void>();
|
|
144
155
|
const errors = new Set<(error: Error) => void>();
|
|
145
156
|
const intentionalExit = { value: false };
|
|
157
|
+
const spawnCommand = tinyWorkerSpawnCmd();
|
|
146
158
|
const proc = Bun.spawn({
|
|
147
|
-
cmd:
|
|
159
|
+
cmd: spawnCommand.cmd,
|
|
160
|
+
cwd: spawnCommand.cwd,
|
|
148
161
|
env: tinyWorkerEnv(),
|
|
149
162
|
stdin: "ignore",
|
|
150
163
|
stdout: "ignore",
|
|
@@ -175,7 +188,9 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess {
|
|
|
175
188
|
});
|
|
176
189
|
// Don't keep the parent event loop alive on account of an idle worker; the
|
|
177
190
|
// agent dispose path calls `terminate()` explicitly when shutting down.
|
|
178
|
-
|
|
191
|
+
// Bun's test runner can starve IPC delivery for unref'd subprocesses, so
|
|
192
|
+
// keep it referenced only under tests that assert the ping/pong contract.
|
|
193
|
+
if (!isBunTestRuntime()) proc.unref();
|
|
179
194
|
return { proc, inbound, errors, intentionalExit };
|
|
180
195
|
}
|
|
181
196
|
|
|
@@ -6,6 +6,19 @@ import { inflateSync, strFromU8 } from "fflate";
|
|
|
6
6
|
import { formatBytes } from "./render-utils";
|
|
7
7
|
import { ToolError } from "./tool-errors";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Cap on the on-disk size of tar/tar.gz archives, which are loaded fully into
|
|
11
|
+
* memory (and decompressed by `Bun.Archive`) just to index entries. ZIP is
|
|
12
|
+
* exempt: it is read via ranged central-directory access.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_TAR_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
15
|
+
/**
|
|
16
|
+
* Cap on a single archive member's declared (uncompressed) size. The declared
|
|
17
|
+
* size is attacker-controlled metadata — a crafted ZIP entry can claim
|
|
18
|
+
* multi-GB sizes that would be allocated up front before any data inflates.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_ARCHIVE_MEMBER_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
|
|
9
22
|
export type ArchiveFormat = "zip" | "tar" | "tar.gz";
|
|
10
23
|
|
|
11
24
|
export interface ArchivePathCandidate {
|
|
@@ -646,6 +659,11 @@ export class ArchiveReader {
|
|
|
646
659
|
if (!entry.storage) {
|
|
647
660
|
throw new ToolError(`Archive file '${normalizedPath}' has no readable storage`);
|
|
648
661
|
}
|
|
662
|
+
if (entry.size > MAX_ARCHIVE_MEMBER_BYTES) {
|
|
663
|
+
throw new ToolError(
|
|
664
|
+
`Archive member '${normalizedPath}' is too large to extract in memory (${formatBytes(entry.size)} > ${formatBytes(MAX_ARCHIVE_MEMBER_BYTES)} limit)`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
649
667
|
|
|
650
668
|
const bytes =
|
|
651
669
|
entry.storage.type === "tar"
|
|
@@ -668,8 +686,18 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
|
|
|
668
686
|
throw new ToolError(`Unsupported archive format: ${filePath}`);
|
|
669
687
|
}
|
|
670
688
|
|
|
671
|
-
|
|
672
|
-
|
|
689
|
+
if (format === "zip") {
|
|
690
|
+
return new ArchiveReader(format, await readZipEntries(filePath));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const file = Bun.file(filePath);
|
|
694
|
+
const archiveSize = file.size;
|
|
695
|
+
if (archiveSize > MAX_TAR_ARCHIVE_BYTES) {
|
|
696
|
+
throw new ToolError(
|
|
697
|
+
`Archive is too large to read in memory (${formatBytes(archiveSize)} > ${formatBytes(MAX_TAR_ARCHIVE_BYTES)} limit)`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const entries = await readTarEntries(await file.bytes());
|
|
673
701
|
return new ArchiveReader(format, entries);
|
|
674
702
|
}
|
|
675
703
|
|