@oh-my-pi/pi-coding-agent 17.0.6 → 17.0.8
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 +65 -0
- package/dist/cli.js +4967 -4948
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +16 -0
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/mcp/tool-bridge.d.ts +9 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +78 -2
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/package.json +12 -13
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/config/model-discovery.ts +3 -2
- package/src/config/model-registry.ts +53 -26
- package/src/config/model-resolver.ts +56 -31
- package/src/config/settings-schema.ts +5 -0
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +15 -5
- package/src/mcp/tool-bridge.ts +9 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +22 -5
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/sdk.ts +134 -48
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +300 -26
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +41 -27
- package/src/task/index.ts +11 -0
- package/src/task/prewalk.ts +6 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
package/src/task/executor.ts
CHANGED
|
@@ -30,7 +30,6 @@ import { getSessionSlashCommands } from "../extensibility/extensions/get-command
|
|
|
30
30
|
import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
|
|
31
31
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
32
32
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
33
|
-
import { callTool } from "../mcp/client";
|
|
34
33
|
import type { MCPManager } from "../mcp/manager";
|
|
35
34
|
import type { MnemopiSessionState } from "../mnemopi/state";
|
|
36
35
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
@@ -55,6 +54,7 @@ import type { EventBus } from "../utils/event-bus";
|
|
|
55
54
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
56
55
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
57
56
|
import { generateTaskLabel } from "./label";
|
|
57
|
+
import { resolveAgentPrewalkDefault } from "./prewalk";
|
|
58
58
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
59
59
|
import {
|
|
60
60
|
type AgentDefinition,
|
|
@@ -729,46 +729,54 @@ function getUsageTokens(usage: unknown): number {
|
|
|
729
729
|
|
|
730
730
|
/**
|
|
731
731
|
* Create proxy tools that reuse the parent's MCP connections.
|
|
732
|
+
*
|
|
733
|
+
* Each proxy delegates to the current source `MCPTool`/`DeferredMCPTool` rather
|
|
734
|
+
* than rebuilding a raw `tools/call` request, so the Task/subagent path shares
|
|
735
|
+
* the source tool's authoritative outbound boundary: harness-intent (`i`)
|
|
736
|
+
* stripping, optional-placeholder pruning, local-URL resolution, reconnect
|
|
737
|
+
* retry, abort handling, and result/provider metadata. The source tool is
|
|
738
|
+
* re-resolved on every call by raw MCP server/tool metadata (not the normalized
|
|
739
|
+
* display name), so a reconnect that swaps the instance in `getTools()` is
|
|
740
|
+
* always honored. The proxy adds only the Task-specific 60s call timeout,
|
|
741
|
+
* combining its abort signal with the caller's around source execution.
|
|
732
742
|
*/
|
|
733
743
|
export function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
|
|
734
744
|
return mcpManager.getTools().map(tool => {
|
|
735
|
-
const
|
|
745
|
+
const serverName = tool.mcpServerName ?? "";
|
|
746
|
+
const mcpToolName = tool.mcpToolName ?? "";
|
|
736
747
|
return {
|
|
737
748
|
name: tool.name,
|
|
738
749
|
label: tool.label ?? tool.name,
|
|
739
750
|
description: tool.description ?? "",
|
|
740
751
|
parameters: tool.parameters,
|
|
741
|
-
|
|
752
|
+
strict: tool.strict,
|
|
753
|
+
mcpServerName: serverName,
|
|
754
|
+
mcpToolName,
|
|
755
|
+
execute: async (toolCallId, params, onUpdate, ctx, signal) => {
|
|
742
756
|
if (signal?.aborted) {
|
|
743
757
|
throw new ToolAbortError();
|
|
744
758
|
}
|
|
745
|
-
|
|
746
|
-
|
|
759
|
+
// Re-resolve by raw MCP metadata so a reconnect that replaced the
|
|
760
|
+
// source instance is picked up; the display name alone is not enough.
|
|
761
|
+
const source = mcpManager
|
|
762
|
+
.getTools()
|
|
763
|
+
.find(t => t.mcpServerName === serverName && t.mcpToolName === mcpToolName);
|
|
764
|
+
if (!source?.execute) {
|
|
765
|
+
return {
|
|
766
|
+
content: [{ type: "text" as const, text: `MCP error: tool ${mcpToolName} no longer available` }],
|
|
767
|
+
details: { serverName, mcpToolName, isError: true },
|
|
768
|
+
};
|
|
769
|
+
}
|
|
747
770
|
try {
|
|
748
771
|
const timeoutController = new AbortController();
|
|
749
772
|
const timeoutSignal = timeoutController.signal;
|
|
750
773
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
751
|
-
|
|
752
|
-
(
|
|
753
|
-
const connection = await untilAborted(combinedSignal, () =>
|
|
754
|
-
mcpManager.waitForConnection(serverName),
|
|
755
|
-
);
|
|
756
|
-
return callTool(connection, mcpToolName, params as Record<string, unknown>, {
|
|
757
|
-
signal: combinedSignal,
|
|
758
|
-
});
|
|
759
|
-
})(),
|
|
774
|
+
return await withAbortTimeout(
|
|
775
|
+
Promise.resolve(source.execute(toolCallId, params, onUpdate, ctx, combinedSignal)),
|
|
760
776
|
MCP_CALL_TIMEOUT_MS,
|
|
761
777
|
signal,
|
|
762
778
|
timeoutController,
|
|
763
779
|
);
|
|
764
|
-
return {
|
|
765
|
-
content: (result.content ?? []).map(item =>
|
|
766
|
-
item.type === "text"
|
|
767
|
-
? { type: "text" as const, text: item.text ?? "" }
|
|
768
|
-
: { type: "text" as const, text: JSON.stringify(item) },
|
|
769
|
-
),
|
|
770
|
-
details: { serverName, mcpToolName, isError: result.isError },
|
|
771
|
-
};
|
|
772
780
|
} catch (error) {
|
|
773
781
|
if (error instanceof ToolAbortError) {
|
|
774
782
|
throw error;
|
|
@@ -1519,9 +1527,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1519
1527
|
scheduleProgress(flushProgress);
|
|
1520
1528
|
};
|
|
1521
1529
|
|
|
1522
|
-
const attach = (session: AgentSession): (() => void) =>
|
|
1523
|
-
session.
|
|
1530
|
+
const attach = (session: AgentSession): (() => void) => {
|
|
1531
|
+
let activeModel = session.model ? formatModelStringWithRouting(session.model) : undefined;
|
|
1532
|
+
return session.subscribe(event => {
|
|
1524
1533
|
emitSubagentEvent(event);
|
|
1534
|
+
const nextModel = session.model ? formatModelStringWithRouting(session.model) : undefined;
|
|
1535
|
+
if (nextModel && nextModel !== activeModel) {
|
|
1536
|
+
activeModel = nextModel;
|
|
1537
|
+
progress.resolvedModel = nextModel;
|
|
1538
|
+
scheduleProgress(true);
|
|
1539
|
+
}
|
|
1525
1540
|
if (event.type === "auto_retry_start") {
|
|
1526
1541
|
progress.retryState = {
|
|
1527
1542
|
attempt: event.attempt,
|
|
@@ -1572,6 +1587,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1572
1587
|
return;
|
|
1573
1588
|
}
|
|
1574
1589
|
});
|
|
1590
|
+
};
|
|
1575
1591
|
|
|
1576
1592
|
const captureSalvage = (session: AgentSession): void => {
|
|
1577
1593
|
// Best-effort salvage: capture the last assistant text so
|
|
@@ -2451,11 +2467,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2451
2467
|
// frontmatter default; the `task.prewalk` toggle (default off) arms it.
|
|
2452
2468
|
// Resolution failures skip prewalk instead of failing the spawn.
|
|
2453
2469
|
let prewalk: Prewalk | undefined;
|
|
2454
|
-
const genericTaskPrewalk =
|
|
2455
|
-
agent.source === "bundled" && agent.name === "task" && settings.get("task.prewalk") ? true : undefined;
|
|
2456
2470
|
const prewalkPattern = resolveAgentPrewalkPattern({
|
|
2457
2471
|
settingsOverride: settings.get("task.agentPrewalk")[agent.name],
|
|
2458
|
-
agentPrewalk: agent.prewalk
|
|
2472
|
+
agentPrewalk: resolveAgentPrewalkDefault(agent, settings.get("task.prewalk")),
|
|
2459
2473
|
});
|
|
2460
2474
|
if (prewalkPattern) {
|
|
2461
2475
|
const resolvedPrewalk = resolveModelOverride([prewalkPattern], modelRegistry, settings);
|
package/src/task/index.ts
CHANGED
|
@@ -520,6 +520,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
520
520
|
readonly summary = "Spawn subagents to complete delegated tasks";
|
|
521
521
|
readonly strict = false;
|
|
522
522
|
readonly loadMode = "essential";
|
|
523
|
+
// Arktype validates model calls against the active wire schema, but the flat
|
|
524
|
+
// single-spawn schema carries `"+": "delete"`: a batch `{ context, tasks[] }`
|
|
525
|
+
// payload has those keys stripped, then fails on the now-missing `task` with
|
|
526
|
+
// the misleading `task must be a string (was missing)`. That preempts the
|
|
527
|
+
// tool's own actionable shape checks (`validateShapeParams` /
|
|
528
|
+
// `validateSpawnParams`), which never run. Lenient validation forwards the
|
|
529
|
+
// raw args to `execute()` on any arktype failure so those checks surface the
|
|
530
|
+
// real reason ("enable task.batch, or use the flat `task` shape"). Valid
|
|
531
|
+
// calls still normalize through arktype; `execute()` resolves `agent`
|
|
532
|
+
// defaults independently, so the success path is unchanged.
|
|
533
|
+
readonly lenientArgValidation = true;
|
|
523
534
|
readonly renderResult = renderResult;
|
|
524
535
|
// Suppress the streaming call preview once a (partial or final) result exists
|
|
525
536
|
// so the task renders as ONE block that transitions in place — not a pending
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentDefinition } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Resolve an agent's prewalk default, including the bundled task opt-in. */
|
|
4
|
+
export function resolveAgentPrewalkDefault(agent: AgentDefinition, taskPrewalk: boolean): boolean | string | undefined {
|
|
5
|
+
return agent.prewalk ?? (taskPrewalk && agent.source === "bundled" && agent.name === "task" ? true : undefined);
|
|
6
|
+
}
|
package/src/task/worktree.ts
CHANGED
|
@@ -116,7 +116,16 @@ async function captureRepoBaseline(repoRoot: string): Promise<RepoBaseline> {
|
|
|
116
116
|
return { repoRoot, headCommit, staged, unstaged, untracked, untrackedPatch };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
interface SyntheticTreeOptions {
|
|
120
|
+
readonly threeWay?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function writeSyntheticTree(
|
|
124
|
+
repoDir: string,
|
|
125
|
+
baseTreeish: string,
|
|
126
|
+
patches: readonly string[],
|
|
127
|
+
options: SyntheticTreeOptions = {},
|
|
128
|
+
): Promise<string> {
|
|
120
129
|
const tempIndex = path.join(os.tmpdir(), `omp-task-index-${Snowflake.next()}`);
|
|
121
130
|
try {
|
|
122
131
|
await git.readTree(repoDir, baseTreeish, {
|
|
@@ -127,6 +136,7 @@ async function writeSyntheticTree(repoDir: string, baseTreeish: string, patches:
|
|
|
127
136
|
await git.patch.applyText(repoDir, patch, {
|
|
128
137
|
cached: true,
|
|
129
138
|
env: { GIT_INDEX_FILE: tempIndex },
|
|
139
|
+
threeWay: options.threeWay,
|
|
130
140
|
});
|
|
131
141
|
}
|
|
132
142
|
return await git.writeTree(repoDir, {
|
|
@@ -653,11 +663,12 @@ async function replayFilteredAgentCommits(opts: FilteredAgentReplayOptions): Pro
|
|
|
653
663
|
try {
|
|
654
664
|
await git.worktree.add(opts.repoRoot, tmpDir, opts.branchName);
|
|
655
665
|
const agentCommits = await git.revList.range(opts.isolationDir, baselineSha, opts.isolationHead);
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
666
|
+
const baselineWip = [opts.baseline.root.staged, opts.baseline.root.unstaged, opts.baseline.root.untrackedPatch];
|
|
667
|
+
// Seed the parent ODB with the dirty-side blobs needed by `git apply
|
|
668
|
+
// --3way`. Isolation repositories can read parent objects, but the parent
|
|
669
|
+
// cannot read objects created only inside isolation.
|
|
670
|
+
await writeSyntheticTree(opts.repoRoot, baselineSha, baselineWip);
|
|
671
|
+
const dirtyBaselineTree = await writeSyntheticTree(opts.isolationDir, baselineSha, baselineWip);
|
|
661
672
|
let previousFilteredTree = baselineSha;
|
|
662
673
|
let filteredCommitsApplied = 0;
|
|
663
674
|
|
|
@@ -666,7 +677,9 @@ async function replayFilteredAgentCommits(opts: FilteredAgentReplayOptions): Pro
|
|
|
666
677
|
allowFailure: true,
|
|
667
678
|
binary: true,
|
|
668
679
|
});
|
|
669
|
-
const currentFilteredTree = await writeSyntheticTree(opts.repoRoot, baselineSha, [taskStatePatch]
|
|
680
|
+
const currentFilteredTree = await writeSyntheticTree(opts.repoRoot, baselineSha, [taskStatePatch], {
|
|
681
|
+
threeWay: true,
|
|
682
|
+
});
|
|
670
683
|
const commitPatch = await git.diff.tree(opts.repoRoot, previousFilteredTree, currentFilteredTree, {
|
|
671
684
|
allowFailure: true,
|
|
672
685
|
binary: true,
|
|
@@ -699,10 +712,11 @@ async function replayFilteredAgentCommits(opts: FilteredAgentReplayOptions): Pro
|
|
|
699
712
|
await commitPatchToBranchWorktree(tmpDir, opts.taskId, opts.rootPatch, msg, undefined, opts.baseline.root);
|
|
700
713
|
}
|
|
701
714
|
} else {
|
|
702
|
-
// A filtered commit landed;
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
715
|
+
// A filtered commit landed; reconstruct the final HEAD-derived tree
|
|
716
|
+
// with the same dirty-side blobs and 3-way synthesis used above.
|
|
717
|
+
const finalFilteredTree = await writeSyntheticTree(opts.repoRoot, baselineSha, [opts.rootPatch], {
|
|
718
|
+
threeWay: true,
|
|
719
|
+
});
|
|
706
720
|
const leftoverPatch = await git.diff.tree(opts.repoRoot, previousFilteredTree, finalFilteredTree, {
|
|
707
721
|
allowFailure: true,
|
|
708
722
|
binary: true,
|
package/src/tools/ask.ts
CHANGED
|
@@ -81,6 +81,21 @@ const askSchema = arkType({
|
|
|
81
81
|
|
|
82
82
|
export type AskToolInput = typeof askSchema.infer;
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Recover a validated `questions` payload from a persisted `ask` toolCall's
|
|
86
|
+
* `arguments`. Used by `/tree` re-answer (issue #5642): selecting a past
|
|
87
|
+
* `ask` toolResult re-opens the picker with the *original* questions, so the
|
|
88
|
+
* new answer branches as a sibling instead of mutating the old one. Runs the
|
|
89
|
+
* same schema the live tool call validated against — legacy/corrupted
|
|
90
|
+
* persisted args fail closed (`undefined`) rather than feeding malformed
|
|
91
|
+
* data back into the picker.
|
|
92
|
+
*/
|
|
93
|
+
export function recoverAskQuestions(toolCallArguments: unknown): AskToolInput["questions"] | undefined {
|
|
94
|
+
const parsed = askSchema(toolCallArguments);
|
|
95
|
+
if (parsed instanceof arkType.errors) return undefined;
|
|
96
|
+
return parsed.questions;
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
/** Result for a single question */
|
|
85
100
|
export interface QuestionResult {
|
|
86
101
|
id: string;
|
package/src/tools/bash.ts
CHANGED
|
@@ -312,10 +312,17 @@ function extractPartialBashEnv(partialJson: string | undefined): Record<string,
|
|
|
312
312
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
function formatTimeoutClampNotice(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
function formatTimeoutClampNotice(
|
|
316
|
+
requestedTimeoutSec: number,
|
|
317
|
+
effectiveTimeoutSec: number,
|
|
318
|
+
maxTimeout: number,
|
|
319
|
+
): string | undefined {
|
|
320
|
+
if (requestedTimeoutSec === effectiveTimeoutSec) return undefined;
|
|
321
|
+
const cappedByGlobal = maxTimeout > 0 && effectiveTimeoutSec === maxTimeout && maxTimeout < TOOL_TIMEOUTS.bash.max;
|
|
322
|
+
const limit = cappedByGlobal
|
|
323
|
+
? `global tools.maxTimeout ceiling ${maxTimeout}s`
|
|
324
|
+
: `allowed range ${TOOL_TIMEOUTS.bash.min}-${TOOL_TIMEOUTS.bash.max}s`;
|
|
325
|
+
return `Timeout clamped to ${effectiveTimeoutSec}s (requested ${requestedTimeoutSec}s; ${limit}).`;
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
function formatWallTimeSeconds(wallTimeMs: number): string {
|
|
@@ -831,11 +838,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
831
838
|
// must still cancel the call or job, but OMP does not impose a deadline.
|
|
832
839
|
const requestedTimeoutSec = rawTimeout;
|
|
833
840
|
const timeoutDisabled = requestedTimeoutSec === 0;
|
|
834
|
-
const
|
|
841
|
+
const maxTimeout = this.session.settings.get("tools.maxTimeout");
|
|
842
|
+
const timeoutSec = timeoutDisabled ? undefined : clampTimeout("bash", requestedTimeoutSec, maxTimeout);
|
|
835
843
|
const timeoutMs = timeoutSec === undefined ? undefined : timeoutSec * 1000;
|
|
836
844
|
const pendingNotices: string[] = [];
|
|
837
845
|
if (timeoutSec !== undefined) {
|
|
838
|
-
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
|
|
846
|
+
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec, maxTimeout);
|
|
839
847
|
if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
|
|
840
848
|
}
|
|
841
849
|
|
package/src/tools/browser.ts
CHANGED
|
@@ -190,7 +190,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
190
190
|
): Promise<AgentToolResult<BrowserToolDetails>> {
|
|
191
191
|
try {
|
|
192
192
|
throwIfAborted(signal);
|
|
193
|
-
const timeoutSeconds = clampTimeout("browser", params.timeout);
|
|
193
|
+
const timeoutSeconds = clampTimeout("browser", params.timeout, this.session.settings.get("tools.maxTimeout"));
|
|
194
194
|
const timeoutMs = timeoutSeconds * 1000;
|
|
195
195
|
const name = params.name ?? DEFAULT_TAB_NAME;
|
|
196
196
|
const details: BrowserToolDetails = { action: params.action, name };
|
package/src/tools/debug.ts
CHANGED
|
@@ -729,7 +729,7 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
|
|
|
729
729
|
_onUpdate?: AgentToolUpdateCallback<DebugToolDetails>,
|
|
730
730
|
_context?: AgentToolContext,
|
|
731
731
|
): Promise<AgentToolResult<DebugToolDetails>> {
|
|
732
|
-
const timeoutSec = clampTimeout("debug", params.timeout);
|
|
732
|
+
const timeoutSec = clampTimeout("debug", params.timeout, this.session.settings.get("tools.maxTimeout"));
|
|
733
733
|
const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
|
|
734
734
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
735
735
|
const details: DebugToolDetails = { action: params.action, success: true };
|
package/src/tools/eval.ts
CHANGED
|
@@ -217,10 +217,6 @@ function detailsNotice(cells: ResolvedEvalCell[]): string | undefined {
|
|
|
217
217
|
return notices.length > 0 ? notices.join(" ") : undefined;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
function timeoutSecondsFromMs(timeoutMs: number): number {
|
|
221
|
-
return clampTimeout("eval", timeoutMs / 1000);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
220
|
async function resolveBackend(session: ToolSession, language: EvalLanguage): Promise<ResolvedBackend> {
|
|
225
221
|
const backends = resolveEvalBackends(session);
|
|
226
222
|
const allowPy = backends.python;
|
|
@@ -538,7 +534,10 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
538
534
|
// ordinary tool calls all count against the budget. The watchdog drives
|
|
539
535
|
// `combinedSignal`; we pass no wall-clock deadline downstream so the
|
|
540
536
|
// backends never arm a competing fixed timer.
|
|
541
|
-
const idleTimeoutMs =
|
|
537
|
+
const idleTimeoutMs =
|
|
538
|
+
cell.timeoutMs === 0
|
|
539
|
+
? undefined
|
|
540
|
+
: clampTimeout("eval", cell.timeoutMs / 1000, session.settings.get("tools.maxTimeout")) * 1000;
|
|
542
541
|
const idle = idleTimeoutMs === undefined ? undefined : new IdleTimeout(idleTimeoutMs);
|
|
543
542
|
const combinedSignal =
|
|
544
543
|
signal && idle
|
package/src/tools/fetch.ts
CHANGED
|
@@ -1622,7 +1622,7 @@ export async function fetchReadUrl(
|
|
|
1622
1622
|
): Promise<ReadUrlEntry> {
|
|
1623
1623
|
const { path: url, raw = false } = params;
|
|
1624
1624
|
|
|
1625
|
-
const effectiveTimeout = clampTimeout("fetch", 30);
|
|
1625
|
+
const effectiveTimeout = clampTimeout("fetch", 30, session.settings.get("tools.maxTimeout"));
|
|
1626
1626
|
|
|
1627
1627
|
if (signal?.aborted) {
|
|
1628
1628
|
throw new ToolAbortError();
|
package/src/tools/hub/launch.ts
CHANGED
|
@@ -74,6 +74,9 @@ const KEY_INPUT: Record<string, string> = {
|
|
|
74
74
|
LEFT: "\u001b[D",
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
/** Terminal daemon lifecycle states — the process is no longer running. */
|
|
78
|
+
const TERMINAL_STATES: Partial<Record<DaemonState, true>> = { exited: true, failed: true };
|
|
79
|
+
|
|
77
80
|
/** Structured launch state retained for compact TUI rendering. */
|
|
78
81
|
export interface LaunchToolDetails {
|
|
79
82
|
op: LaunchParams["op"];
|
|
@@ -229,6 +232,8 @@ function toolContent(result: DaemonRpcResult, params: LaunchParams): string {
|
|
|
229
232
|
lines.push(
|
|
230
233
|
`NOT ready — readiness timed out after ${params.ready?.timeout ?? 30}s${cause}. The process is still running (state: ${daemon.state}); follow its logs or stop it.`,
|
|
231
234
|
);
|
|
235
|
+
} else if (params.ready && daemon.readyAt === undefined && TERMINAL_STATES[daemon.state]) {
|
|
236
|
+
lines.push("Process exited before readiness was observed.");
|
|
232
237
|
}
|
|
233
238
|
return lines.join("\n");
|
|
234
239
|
}
|
|
@@ -437,6 +442,8 @@ export function launchRenderResult(
|
|
|
437
442
|
: "Readiness timed out; the process is still running.",
|
|
438
443
|
),
|
|
439
444
|
);
|
|
445
|
+
} else if (params.ready && daemon && daemon.readyAt === undefined && TERMINAL_STATES[daemon.state]) {
|
|
446
|
+
body.push(theme.fg("warning", "Process exited before readiness was observed."));
|
|
440
447
|
}
|
|
441
448
|
break;
|
|
442
449
|
}
|
|
@@ -21,10 +21,17 @@ export type ToolWithTimeout = keyof typeof TOOL_TIMEOUTS;
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Clamp a raw timeout to the allowed range for a tool.
|
|
24
|
-
*
|
|
24
|
+
*
|
|
25
|
+
* When `rawTimeout` is undefined the tool's `default` is used. A positive
|
|
26
|
+
* `maxTimeout` (the `tools.maxTimeout` global ceiling) caps the *resolved*
|
|
27
|
+
* value — including the default-fallback path — before the per-tool `min`/`max`
|
|
28
|
+
* floor and ceiling apply, so a configured global cap governs calls where the
|
|
29
|
+
* agent omits `timeout`, not only explicitly-passed values. `maxTimeout <= 0`
|
|
30
|
+
* means no global cap.
|
|
25
31
|
*/
|
|
26
|
-
export function clampTimeout(tool: ToolWithTimeout, rawTimeout?: number): number {
|
|
32
|
+
export function clampTimeout(tool: ToolWithTimeout, rawTimeout?: number, maxTimeout?: number): number {
|
|
27
33
|
const config = TOOL_TIMEOUTS[tool];
|
|
28
34
|
const timeout = rawTimeout ?? config.default;
|
|
29
|
-
|
|
35
|
+
const capped = maxTimeout !== undefined && maxTimeout > 0 ? Math.min(timeout, maxTimeout) : timeout;
|
|
36
|
+
return Math.max(config.min, Math.min(config.max, capped));
|
|
30
37
|
}
|
package/src/tools/write.ts
CHANGED
|
@@ -85,6 +85,36 @@ import { renderXdevCall, renderXdevResult, type XdevDispatch } from "./xdev";
|
|
|
85
85
|
|
|
86
86
|
const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/;
|
|
87
87
|
const EXECUTABLE_NOTICE = "[Notice: Made executable via chmod +x]";
|
|
88
|
+
const URI_LIKE_WRITE_PATH_RE = /^([a-z][a-z0-9+.-]*):\/{1,2}(.*)$/i;
|
|
89
|
+
const XD_MISSING_DELIMITER_RE = /^xd\/+(.*)$/i;
|
|
90
|
+
const XD_SCHEME_NEAR_MISSES: Record<string, true> = { dx: true, xdd: true, xdt: true };
|
|
91
|
+
|
|
92
|
+
function assertWriteTargetAddressable(target: string, router: InternalUrlRouter): void {
|
|
93
|
+
const trimmed = target.trim();
|
|
94
|
+
if (path.win32.isAbsolute(trimmed) || router.canHandle(trimmed)) return;
|
|
95
|
+
|
|
96
|
+
const missingDelimiter = trimmed.match(XD_MISSING_DELIMITER_RE);
|
|
97
|
+
if (missingDelimiter) {
|
|
98
|
+
throw new ToolError(
|
|
99
|
+
`Unknown URI-like write target '${trimmed}'. Did you mean 'xd://${missingDelimiter[1]}'? Prefix the path with './' to write it as a filesystem path.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const uriLike = trimmed.match(URI_LIKE_WRITE_PATH_RE);
|
|
104
|
+
if (!uriLike) return;
|
|
105
|
+
|
|
106
|
+
const scheme = uriLike[1]!.toLowerCase();
|
|
107
|
+
// conflict:// has no router handler but is spliced downstream by
|
|
108
|
+
// parseConflictUri (which emits its own precise id/scope errors); let it pass.
|
|
109
|
+
if (scheme === "conflict") return;
|
|
110
|
+
const canonicalScheme = router.getHandler(scheme) ? scheme : XD_SCHEME_NEAR_MISSES[scheme] ? "xd" : undefined;
|
|
111
|
+
const suggestion = canonicalScheme
|
|
112
|
+
? ` Did you mean '${canonicalScheme}://${uriLike[2]}'?`
|
|
113
|
+
: " Tool devices use 'xd://<tool>'.";
|
|
114
|
+
throw new ToolError(
|
|
115
|
+
`Unknown URI-like write target '${trimmed}'.${suggestion} Prefix the path with './' to write it as a filesystem path.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
88
118
|
|
|
89
119
|
const BULK_DIRECTIVE_RE = /^#?(\d+)\s*[:=]\s*(@ours|@theirs|@base|@both)$/;
|
|
90
120
|
/**
|
|
@@ -987,6 +1017,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
987
1017
|
// Strip hashline display prefixes ([PATH#HASH] + LINE:) if the model copied them from read output
|
|
988
1018
|
const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
|
|
989
1019
|
const internalRouter = InternalUrlRouter.instance();
|
|
1020
|
+
assertWriteTargetAddressable(path, internalRouter);
|
|
990
1021
|
if (internalRouter.canHandle(path)) {
|
|
991
1022
|
const parsed = parseInternalUrl(path);
|
|
992
1023
|
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
package/src/utils/changelog.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getLastChangelogVersionPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import bundledChangelog from "../../CHANGELOG.md" with { type: "text" };
|
|
2
3
|
|
|
3
4
|
export interface ChangelogEntry {
|
|
4
5
|
major: number;
|
|
@@ -29,73 +30,68 @@ export interface StartupChangelogSelection {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
* Parse changelog entries from
|
|
33
|
-
*
|
|
33
|
+
* Parse changelog entries from omp's package asset when available, falling back
|
|
34
|
+
* to the copy embedded in compiled binaries.
|
|
34
35
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* synthesize a fallback path from the host project's cwd; doing so caused issue
|
|
38
|
-
* #1423 (the host project's `CHANGELOG.md` was rendered as omp's).
|
|
36
|
+
* The embedded fallback keeps standalone binaries self-contained without
|
|
37
|
+
* resolving relative to the host project's cwd, which caused issue #1423.
|
|
39
38
|
*/
|
|
40
39
|
export async function parseChangelog(changelogPath: string | undefined): Promise<ChangelogEntry[]> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let currentLines: string[] = [];
|
|
50
|
-
let currentVersion: { major: number; minor: number; patch: number } | null = null;
|
|
51
|
-
|
|
52
|
-
for (const line of lines) {
|
|
53
|
-
// Check if this is a version header (## [x.y.z] ...)
|
|
54
|
-
if (line.startsWith("## ")) {
|
|
55
|
-
// Save previous entry if exists
|
|
56
|
-
if (currentVersion && currentLines.length > 0) {
|
|
57
|
-
entries.push({
|
|
58
|
-
...currentVersion,
|
|
59
|
-
content: currentLines.join("\n").trim(),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Try to parse version from this line
|
|
64
|
-
const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/);
|
|
65
|
-
if (versionMatch) {
|
|
66
|
-
currentVersion = {
|
|
67
|
-
major: Number.parseInt(versionMatch[1], 10),
|
|
68
|
-
minor: Number.parseInt(versionMatch[2], 10),
|
|
69
|
-
patch: Number.parseInt(versionMatch[3], 10),
|
|
70
|
-
};
|
|
71
|
-
currentLines = [line];
|
|
72
|
-
} else {
|
|
73
|
-
// Reset if we can't parse version
|
|
74
|
-
currentVersion = null;
|
|
75
|
-
currentLines = [];
|
|
76
|
-
}
|
|
77
|
-
} else if (currentVersion) {
|
|
78
|
-
// Collect lines for current version
|
|
79
|
-
currentLines.push(line);
|
|
40
|
+
let content = bundledChangelog;
|
|
41
|
+
if (changelogPath) {
|
|
42
|
+
try {
|
|
43
|
+
content = await Bun.file(changelogPath).text();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!isEnoent(error)) {
|
|
46
|
+
logger.error(`Warning: Could not parse changelog: ${error}`);
|
|
80
47
|
}
|
|
81
48
|
}
|
|
49
|
+
}
|
|
82
50
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
entries.push({
|
|
86
|
-
...currentVersion,
|
|
87
|
-
content: currentLines.join("\n").trim(),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
51
|
+
return parseChangelogContent(content);
|
|
52
|
+
}
|
|
90
53
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
54
|
+
function parseChangelogContent(content: string): ChangelogEntry[] {
|
|
55
|
+
const lines = content.split("\n");
|
|
56
|
+
const entries: ChangelogEntry[] = [];
|
|
57
|
+
|
|
58
|
+
let currentLines: string[] = [];
|
|
59
|
+
let currentVersion: { major: number; minor: number; patch: number } | null = null;
|
|
60
|
+
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (line.startsWith("## ")) {
|
|
63
|
+
if (currentVersion && currentLines.length > 0) {
|
|
64
|
+
entries.push({
|
|
65
|
+
...currentVersion,
|
|
66
|
+
content: currentLines.join("\n").trim(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/);
|
|
71
|
+
if (versionMatch) {
|
|
72
|
+
currentVersion = {
|
|
73
|
+
major: Number.parseInt(versionMatch[1], 10),
|
|
74
|
+
minor: Number.parseInt(versionMatch[2], 10),
|
|
75
|
+
patch: Number.parseInt(versionMatch[3], 10),
|
|
76
|
+
};
|
|
77
|
+
currentLines = [line];
|
|
78
|
+
} else {
|
|
79
|
+
currentVersion = null;
|
|
80
|
+
currentLines = [];
|
|
81
|
+
}
|
|
82
|
+
} else if (currentVersion) {
|
|
83
|
+
currentLines.push(line);
|
|
95
84
|
}
|
|
96
|
-
logger.error(`Warning: Could not parse changelog: ${error}`);
|
|
97
|
-
return [];
|
|
98
85
|
}
|
|
86
|
+
|
|
87
|
+
if (currentVersion && currentLines.length > 0) {
|
|
88
|
+
entries.push({
|
|
89
|
+
...currentVersion,
|
|
90
|
+
content: currentLines.join("\n").trim(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return entries;
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
/**
|