@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.1
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 +82 -0
- package/dist/cli.js +3785 -3695
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
- package/dist/types/advisor/emission-guard.d.ts +73 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +7 -0
- package/dist/types/advisor/watchdog.d.ts +13 -0
- package/dist/types/cli/flag-tables.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
- package/dist/types/collab/display-name.d.ts +3 -0
- package/dist/types/collab/host.d.ts +0 -2
- package/dist/types/commands/worktree.d.ts +0 -3
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +6 -2
- package/dist/types/config/model-resolver.d.ts +12 -0
- package/dist/types/config/models-config-schema.d.ts +29 -2
- package/dist/types/config/models-config.d.ts +22 -1
- package/dist/types/config/settings-schema.d.ts +91 -21
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/dap/config.d.ts +1 -1
- package/dist/types/edit/hashline/filesystem.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -17
- package/dist/types/extensibility/hooks/types.d.ts +10 -10
- package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
- package/dist/types/internal-urls/router.d.ts +1 -1
- package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
- package/dist/types/internal-urls/types.d.ts +19 -2
- package/dist/types/irc/bus.d.ts +6 -0
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/components/move-overlay.d.ts +23 -0
- package/dist/types/modes/components/plugin-selector.d.ts +2 -1
- package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
- package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +4 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/show-images-selector.d.ts +2 -1
- package/dist/types/modes/components/status-line/component.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/theme-selector.d.ts +2 -1
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/controllers/command-controller.d.ts +10 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -1
- package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
- package/dist/types/modes/running-subagent-badge.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +9 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +8 -1
- package/dist/types/session/blob-store.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +2 -0
- package/dist/types/ssh/file-transfer.d.ts +79 -0
- package/dist/types/ssh/utils.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +3 -0
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tiny/text.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +5 -1
- package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
- package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tools/path-utils.d.ts +29 -0
- package/dist/types/tools/read.d.ts +2 -2
- package/dist/types/tools/render-utils.d.ts +8 -0
- package/dist/types/tools/renderers.d.ts +11 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo.d.ts +0 -16
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/active-repo-context.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/utils/markit-cache.d.ts +23 -0
- package/dist/types/utils/markit.d.ts +5 -1
- package/dist/types/utils/prompt-path.d.ts +1 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
- package/dist/types/web/search/providers/xai.d.ts +13 -0
- package/dist/types/web/search/types.d.ts +18 -2
- package/package.json +30 -15
- package/scripts/build-binary.ts +9 -9
- package/scripts/bundle-dist.ts +2 -2
- package/src/advisor/__tests__/advisor.test.ts +61 -4
- package/src/advisor/__tests__/emission-guard.test.ts +147 -0
- package/src/advisor/advise-tool.ts +1 -1
- package/src/advisor/emission-guard.ts +172 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +11 -0
- package/src/advisor/watchdog.ts +27 -1
- package/src/cli/args.ts +5 -2
- package/src/cli/auth-broker-cli.ts +17 -0
- package/src/cli/config-cli.ts +4 -0
- package/src/cli/flag-tables.ts +7 -4
- package/src/cli/gallery-cli.ts +14 -2
- package/src/cli/gallery-fixtures/edit.ts +60 -0
- package/src/cli/gallery-fixtures/fs.ts +17 -17
- package/src/cli/gallery-fixtures/search.ts +4 -4
- package/src/cli/usage-cli.ts +20 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/collab/display-name.ts +13 -0
- package/src/collab/guest.ts +5 -1
- package/src/collab/host.ts +2 -13
- package/src/commands/worktree.ts +6 -0
- package/src/config/inline-tool-descriptors-mode.ts +27 -0
- package/src/config/model-discovery.ts +77 -8
- package/src/config/model-registry.ts +89 -11
- package/src/config/model-resolver.ts +39 -1
- package/src/config/models-config-schema.ts +41 -5
- package/src/config/models-config.ts +4 -1
- package/src/config/settings-schema.ts +85 -27
- package/src/config/settings.ts +172 -1
- package/src/cursor.ts +1 -1
- package/src/dap/config.ts +152 -8
- package/src/dap/session.ts +1 -1
- package/src/discovery/claude-plugins.ts +3 -2
- package/src/discovery/helpers.ts +3 -1
- package/src/edit/hashline/diff.ts +14 -3
- package/src/edit/hashline/execute.ts +42 -6
- package/src/edit/hashline/filesystem.ts +38 -2
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/patch.ts +11 -1
- package/src/edit/renderer.ts +140 -13
- package/src/eval/__tests__/agent-bridge.test.ts +101 -0
- package/src/eval/agent-bridge.ts +26 -3
- package/src/eval/js/tool-bridge.ts +2 -2
- package/src/export/html/index.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/export/html/tool-views.generated.js +20 -20
- package/src/extensibility/extensions/types.ts +22 -22
- package/src/extensibility/hooks/types.ts +11 -11
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
- package/src/extensibility/plugins/marketplace/manager.ts +22 -0
- package/src/extensibility/plugins/marketplace/types.ts +1 -0
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/internal-urls/docs-index.ts +2 -3
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/registry-helpers.ts +19 -4
- package/src/internal-urls/router.ts +5 -3
- package/src/internal-urls/ssh-protocol.ts +367 -0
- package/src/internal-urls/types.ts +19 -2
- package/src/irc/bus.ts +11 -3
- package/src/mcp/tool-bridge.ts +32 -2
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-agent.ts +14 -1
- package/src/modes/acp/acp-event-mapper.ts +91 -27
- package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
- package/src/modes/components/agent-hub.ts +3 -0
- package/src/modes/components/agent-transcript-viewer.ts +9 -7
- package/src/modes/components/custom-editor.ts +90 -5
- package/src/modes/components/move-overlay.ts +282 -0
- package/src/modes/components/plan-review-overlay.ts +35 -35
- package/src/modes/components/plugin-selector.ts +6 -1
- package/src/modes/components/queue-mode-selector.ts +6 -1
- package/src/modes/components/select-list-mouse-routing.ts +35 -0
- package/src/modes/components/session-selector.ts +11 -10
- package/src/modes/components/settings-defs.ts +14 -1
- package/src/modes/components/settings-selector.ts +196 -29
- package/src/modes/components/show-images-selector.ts +6 -1
- package/src/modes/components/status-line/component.ts +108 -28
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/theme-selector.ts +6 -1
- package/src/modes/components/thinking-selector.ts +6 -1
- package/src/modes/components/tool-execution.ts +25 -9
- package/src/modes/components/tree-selector.ts +5 -5
- package/src/modes/controllers/command-controller.ts +140 -47
- package/src/modes/controllers/event-controller.ts +59 -3
- package/src/modes/controllers/input-controller.ts +68 -2
- package/src/modes/controllers/selector-controller.ts +5 -2
- package/src/modes/controllers/streaming-reveal.ts +17 -0
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +116 -50
- package/src/modes/internal-url-autocomplete.ts +17 -2
- package/src/modes/prompt-action-autocomplete.ts +3 -1
- package/src/modes/running-subagent-badge.ts +13 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
- package/src/modes/setup-wizard/scenes/providers.ts +2 -1
- package/src/modes/setup-wizard/scenes/theme.ts +6 -12
- package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
- package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
- package/src/modes/theme/theme.ts +9 -1
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/interactive-context-helpers.ts +1 -1
- package/src/priority.json +15 -0
- package/src/prompts/advisor/active-repo-watchdog.md +6 -0
- package/src/prompts/advisor/context-files.md +8 -0
- package/src/prompts/advisor/system.md +21 -7
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/active-repo-context.md +4 -0
- package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
- package/src/prompts/system/plan-mode-active.md +3 -3
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -6
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/{find.md → glob.md} +1 -1
- package/src/prompts/tools/{search.md → grep.md} +3 -3
- package/src/prompts/tools/read.md +4 -2
- package/src/sdk.ts +65 -25
- package/src/session/agent-session.ts +519 -132
- package/src/session/blob-store.ts +24 -0
- package/src/session/session-history-format.ts +2 -2
- package/src/session/session-manager.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +138 -20
- package/src/slash-commands/helpers/usage-report.ts +23 -2
- package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
- package/src/ssh/connection-manager.ts +12 -15
- package/src/ssh/file-transfer.ts +209 -0
- package/src/ssh/utils.ts +24 -0
- package/src/system-prompt.ts +56 -28
- package/src/task/executor.ts +96 -36
- package/src/task/index.ts +3 -3
- package/src/task/render.ts +14 -13
- package/src/task/worktree.ts +38 -1
- package/src/tiny/text.ts +49 -4
- package/src/tiny/worker.ts +3 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/builtin-names.ts +26 -2
- package/src/tools/{find.ts → glob.ts} +48 -42
- package/src/tools/{search.ts → grep.ts} +311 -129
- package/src/tools/index.ts +13 -14
- package/src/tools/irc.ts +6 -2
- package/src/tools/path-utils.ts +64 -1
- package/src/tools/read.ts +35 -6
- package/src/tools/render-utils.ts +14 -0
- package/src/tools/renderers.ts +15 -4
- package/src/tools/ssh.ts +17 -2
- package/src/tools/todo.ts +4 -26
- package/src/tools/write.ts +25 -9
- package/src/utils/active-repo-context.ts +143 -0
- package/src/utils/image-resize.ts +88 -7
- package/src/utils/lang-from-path.ts +3 -3
- package/src/utils/markit-cache.ts +166 -0
- package/src/utils/markit.ts +86 -18
- package/src/utils/prompt-path.ts +3 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/provider.ts +54 -34
- package/src/web/search/providers/duckduckgo.ts +140 -0
- package/src/web/search/providers/firecrawl.ts +144 -0
- package/src/web/search/providers/tinyfish.ts +159 -0
- package/src/web/search/providers/xai.ts +292 -0
- package/src/web/search/providers/zai.ts +142 -56
- package/src/web/search/types.ts +6 -2
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-preserving remote file I/O over the shared SSH ControlMaster connection.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `executeSSH` (which truncates/sanitizes through an OutputSink) and
|
|
5
|
+
* `runSshCaptureSync` (which `.trim()`s output), these helpers move raw bytes so
|
|
6
|
+
* `ssh://` reads/writes round-trip exactly — leading/trailing whitespace, tabs,
|
|
7
|
+
* and final newlines are preserved.
|
|
8
|
+
*/
|
|
9
|
+
import { ptree } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
|
|
11
|
+
import { quotePosixPath } from "./utils";
|
|
12
|
+
|
|
13
|
+
/** Per-operation timeout for remote transfers (matches the ssh tool's grep window). */
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ensure the ControlMaster connection and restrict transfers to remotes whose
|
|
18
|
+
* *login* shell runs our POSIX snippets directly. OpenSSH hands each command to
|
|
19
|
+
* `$SHELL -c`, so the login shell must be POSIX: Windows (cmd/powershell) can't
|
|
20
|
+
* drive `head`/`cat`/`mv`, and csh/tcsh apply `!` history expansion to the
|
|
21
|
+
* command line. `ensureHostInfo` classifies those (and fish) as a non-sh shell,
|
|
22
|
+
* so accept only sh, bash, and zsh; anything else is refused here.
|
|
23
|
+
*/
|
|
24
|
+
async function ensurePosixRemote(target: SSHConnectionTarget): Promise<void> {
|
|
25
|
+
await ensureConnection(target);
|
|
26
|
+
const info = await ensureHostInfo(target);
|
|
27
|
+
if (info.os === "windows") {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`ssh://: ${target.name} is a Windows host; ssh:// supports POSIX remotes only (head/cat/mv) — use the ssh tool for Windows hosts`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (info.shell !== "sh" && info.shell !== "bash" && info.shell !== "zsh") {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`ssh://: ${target.name} uses a non-POSIX login shell (${info.shell}); ssh:// read/write needs sh, bash, or zsh — use the ssh tool for this host`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RemoteFileReadOptions {
|
|
40
|
+
/** Maximum bytes to materialize; the helper fetches one extra byte to detect truncation. */
|
|
41
|
+
maxBytes: number;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RemoteFileReadResult {
|
|
47
|
+
/** Raw file bytes, capped at `maxBytes`. */
|
|
48
|
+
bytes: Uint8Array;
|
|
49
|
+
/** True when the remote file was larger than `maxBytes` (`bytes` is the prefix). */
|
|
50
|
+
truncated: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RemoteFileWriteOptions {
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read a remote file's raw bytes. Fetches `maxBytes + 1` so the caller can
|
|
60
|
+
* distinguish an exactly-`maxBytes` file from a larger (truncated) one.
|
|
61
|
+
*
|
|
62
|
+
* Throws `ptree.NonZeroExitError` (carrying the remote stderr tail) when the
|
|
63
|
+
* file is missing/unreadable or the host is unreachable.
|
|
64
|
+
*/
|
|
65
|
+
export async function readRemoteFile(
|
|
66
|
+
target: SSHConnectionTarget,
|
|
67
|
+
remotePath: string,
|
|
68
|
+
opts: RemoteFileReadOptions,
|
|
69
|
+
): Promise<RemoteFileReadResult> {
|
|
70
|
+
await ensurePosixRemote(target);
|
|
71
|
+
const command = `head -c ${opts.maxBytes + 1} ${quotePosixPath(remotePath)}`;
|
|
72
|
+
const args = await buildRemoteCommand(target, command);
|
|
73
|
+
using child = ptree.spawn(["ssh", ...args], {
|
|
74
|
+
signal: ptree.combineSignals(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
75
|
+
});
|
|
76
|
+
// Drain stdout before awaiting exit so a full pipe can't deadlock the child.
|
|
77
|
+
const raw = await child.bytes();
|
|
78
|
+
await child.exitedCleanly;
|
|
79
|
+
const truncated = raw.length > opts.maxBytes;
|
|
80
|
+
return { bytes: truncated ? raw.subarray(0, opts.maxBytes) : raw, truncated };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Write `content` to a remote file byte-exact. Stdin is always staged first into
|
|
85
|
+
* a uniquely named temp in the destination directory (so the remote never blocks
|
|
86
|
+
* on an unread pipe and a dropped connection lands in the temp, never the
|
|
87
|
+
* destination). The destination then dictates the commit:
|
|
88
|
+
* - a directory — or a symlink to one, since the `-d` test follows links — is
|
|
89
|
+
* refused (a plain `mv tmp dir` would move the temp INTO it).
|
|
90
|
+
* - an existing non-symlink regular file is rewritten IN PLACE from the staged
|
|
91
|
+
* temp, preserving its inode and therefore its ordinary permission bits (a
|
|
92
|
+
* `0600` secret stays `0600` on overwrite), ACLs, xattrs, and hardlinks. The
|
|
93
|
+
* setuid/setgid bits may be cleared by the write (per POSIX). This commit is
|
|
94
|
+
* not fully atomic — a remote-side failure during the local temp->dest copy
|
|
95
|
+
* (e.g. the disk filling) can truncate the destination — but the slow network
|
|
96
|
+
* transfer has already landed in the temp, and the temp is removed on failure.
|
|
97
|
+
* It also needs write permission on the file itself (a read-only file is
|
|
98
|
+
* refused, not silently replaced).
|
|
99
|
+
* - an existing special file (FIFO/socket/device) is refused, not replaced.
|
|
100
|
+
* - anything else (a new path, a symlink to a non-directory, a dangling symlink)
|
|
101
|
+
* is committed with an atomic rename, which REPLACES a symlink with a regular
|
|
102
|
+
* file rather than writing through it (resolving the link target is not
|
|
103
|
+
* portable across the macOS/Linux hosts this stack supports).
|
|
104
|
+
* Throws `ptree.NonZeroExitError` when the remote path is unwritable or the host
|
|
105
|
+
* is unreachable.
|
|
106
|
+
*/
|
|
107
|
+
export async function writeRemoteFile(
|
|
108
|
+
target: SSHConnectionTarget,
|
|
109
|
+
remotePath: string,
|
|
110
|
+
content: Uint8Array,
|
|
111
|
+
opts: RemoteFileWriteOptions,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
await ensurePosixRemote(target);
|
|
114
|
+
if (remotePath.endsWith("/")) {
|
|
115
|
+
throw new Error("ssh://: destination is a directory path (trailing '/'); ssh:// write requires a file path");
|
|
116
|
+
}
|
|
117
|
+
const dest = quotePosixPath(remotePath);
|
|
118
|
+
const tmp = quotePosixPath(`${remotePath}.omp-tmp.${crypto.randomUUID()}`);
|
|
119
|
+
// Stage stdin into the temp first (so the remote never blocks on an unread
|
|
120
|
+
// pipe and a dropped connection lands in the temp, never the destination).
|
|
121
|
+
// An EXIT trap removes the staged temp on every exit path (staging failure,
|
|
122
|
+
// in-place success, refuse branches, or a failed rename). Commit by
|
|
123
|
+
// destination kind: a directory (or symlink to one; `-d` follows links) is
|
|
124
|
+
// refused; an existing non-symlink regular file is rewritten IN PLACE
|
|
125
|
+
// (preserving inode, permission bits, ACLs, xattrs, hardlinks; setuid/setgid
|
|
126
|
+
// may clear); an existing special file (FIFO/socket/device) is refused;
|
|
127
|
+
// anything else (a new path or a symlink to a non-directory) uses temp+rename,
|
|
128
|
+
// replacing such a symlink rather than writing through it.
|
|
129
|
+
const command =
|
|
130
|
+
`t=${tmp}; trap 'rm -f -- "$t"' 0; ` +
|
|
131
|
+
`mkdir -p -- "$(dirname "$t")" && ` +
|
|
132
|
+
`cat > "$t" && { ` +
|
|
133
|
+
`if [ -d ${dest} ]; then echo 'ssh://: destination is a directory' >&2; exit 1; ` +
|
|
134
|
+
`elif [ -f ${dest} ] && [ ! -L ${dest} ]; then cat "$t" > ${dest} || exit 1; ` +
|
|
135
|
+
`elif [ -e ${dest} ] && [ ! -L ${dest} ]; then echo 'ssh://: destination is a special file (not a regular file)' >&2; exit 1; ` +
|
|
136
|
+
`else mv "$t" ${dest}; fi; ` +
|
|
137
|
+
`}`;
|
|
138
|
+
const args = await buildRemoteCommand(target, command, { allowStdin: true });
|
|
139
|
+
using child = ptree.spawn(["ssh", ...args], {
|
|
140
|
+
stdin: content,
|
|
141
|
+
signal: ptree.combineSignals(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
142
|
+
});
|
|
143
|
+
await child.exitedCleanly;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Classification of a remote path, used by the read handler's directory dispatch. */
|
|
147
|
+
export type RemotePathKind = "file" | "directory" | "other" | "missing";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Classify a remote path with POSIX `test` (portable across Linux/BSD/macOS):
|
|
151
|
+
* `directory`, regular `file`, `other` (special file), or `missing`.
|
|
152
|
+
*/
|
|
153
|
+
export async function statRemotePath(
|
|
154
|
+
target: SSHConnectionTarget,
|
|
155
|
+
remotePath: string,
|
|
156
|
+
opts: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
157
|
+
): Promise<RemotePathKind> {
|
|
158
|
+
await ensurePosixRemote(target);
|
|
159
|
+
const p = quotePosixPath(remotePath);
|
|
160
|
+
const command = `if [ -d ${p} ]; then echo directory; elif [ -f ${p} ]; then echo file; elif [ -e ${p} ]; then echo other; else echo missing; fi`;
|
|
161
|
+
const args = await buildRemoteCommand(target, command);
|
|
162
|
+
using child = ptree.spawn(["ssh", ...args], {
|
|
163
|
+
signal: ptree.combineSignals(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
164
|
+
});
|
|
165
|
+
const out = new TextDecoder().decode(await child.bytes()).trim();
|
|
166
|
+
await child.exitedCleanly;
|
|
167
|
+
return out === "directory" || out === "file" || out === "other" ? out : "missing";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A single entry in a remote directory listing. */
|
|
171
|
+
export interface RemoteDirEntry {
|
|
172
|
+
/** Entry name (no path component), trailing `/` stripped. */
|
|
173
|
+
name: string;
|
|
174
|
+
/** True when the entry is a directory. */
|
|
175
|
+
isDirectory: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* List a remote directory one level deep with `ls -1Ap` (one per line; all
|
|
180
|
+
* entries incl. dotfiles but not `.`/`..`; trailing `/` marks directories).
|
|
181
|
+
* Plain `ls` (no `| head`) so a permission/race failure surfaces as a non-zero
|
|
182
|
+
* exit instead of being masked as an empty listing. Entries are returned in
|
|
183
|
+
* full, sorted directories-first then by name to mirror the local
|
|
184
|
+
* directory-resource contract, so the read tool can paginate the listing.
|
|
185
|
+
*/
|
|
186
|
+
export async function listRemoteDir(
|
|
187
|
+
target: SSHConnectionTarget,
|
|
188
|
+
remotePath: string,
|
|
189
|
+
opts: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
190
|
+
): Promise<RemoteDirEntry[]> {
|
|
191
|
+
await ensurePosixRemote(target);
|
|
192
|
+
const command = `LC_ALL=C ls -1Ap -- ${quotePosixPath(remotePath)}`;
|
|
193
|
+
const args = await buildRemoteCommand(target, command);
|
|
194
|
+
using child = ptree.spawn(["ssh", ...args], {
|
|
195
|
+
signal: ptree.combineSignals(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
196
|
+
});
|
|
197
|
+
const text = new TextDecoder().decode(await child.bytes());
|
|
198
|
+
await child.exitedCleanly;
|
|
199
|
+
const entries = text
|
|
200
|
+
.split("\n")
|
|
201
|
+
.filter(line => line.length > 0)
|
|
202
|
+
.map(line => {
|
|
203
|
+
const isDirectory = line.endsWith("/");
|
|
204
|
+
return { name: isDirectory ? line.slice(0, -1) : line, isDirectory };
|
|
205
|
+
});
|
|
206
|
+
// JS sort is the order contract (mirrors buildDirectoryResource): dirs first, then by name.
|
|
207
|
+
entries.sort((a, b) => Number(b.isDirectory) - Number(a.isDirectory) || a.name.localeCompare(b.name));
|
|
208
|
+
return entries;
|
|
209
|
+
}
|
package/src/ssh/utils.ts
CHANGED
|
@@ -4,5 +4,29 @@ export function sanitizeHostName(name: string): string {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
export function buildSshTarget(username: string | undefined, host: string): string {
|
|
7
|
+
// SSH treats a destination starting with "-" as an option, so a host/user of
|
|
8
|
+
// `-oProxyCommand=...` becomes local command execution. Reject before this
|
|
9
|
+
// string reaches any `ssh` argv (this is the single render chokepoint for
|
|
10
|
+
// every connection, transfer, and sshfs mount).
|
|
11
|
+
if (host.startsWith("-")) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`Invalid SSH host "${host}": an SSH destination must not begin with "-" (argument-injection guard)`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
if (username?.startsWith("-")) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Invalid SSH username "${username}": an SSH username must not begin with "-" (argument-injection guard)`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
7
21
|
return username ? `${username}@${host}` : host;
|
|
8
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Single-quote a path for a POSIX remote shell, escaping embedded single quotes.
|
|
26
|
+
* Mirrors the private `quoteRemotePath` in `tools/ssh.ts`; shared here for the
|
|
27
|
+
* `ssh://` file-transfer helpers.
|
|
28
|
+
*/
|
|
29
|
+
export function quotePosixPath(value: string): string {
|
|
30
|
+
if (value.length === 0) return "''";
|
|
31
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
32
|
+
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile
|
|
|
16
16
|
import { expandAtImports } from "./discovery/at-imports";
|
|
17
17
|
import { loadSkills, type Skill } from "./extensibility/skills";
|
|
18
18
|
import { hasObsidian } from "./internal-urls/vault-protocol";
|
|
19
|
+
import activeRepoContextTemplate from "./prompts/system/active-repo-context.md" with { type: "text" };
|
|
19
20
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
20
21
|
import defaultPersonality from "./prompts/system/personalities/default.md" with { type: "text" };
|
|
21
22
|
import friendlyPersonality from "./prompts/system/personalities/friendly.md" with { type: "text" };
|
|
@@ -23,6 +24,8 @@ import pragmaticPersonality from "./prompts/system/personalities/pragmatic.md" w
|
|
|
23
24
|
import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
|
|
24
25
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
25
26
|
import { shortenPath } from "./tools/render-utils";
|
|
27
|
+
import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
|
|
28
|
+
import { normalizePromptPath } from "./utils/prompt-path";
|
|
26
29
|
import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
27
30
|
|
|
28
31
|
/** Bundled personality specs, keyed by the `personality` setting value. */
|
|
@@ -90,6 +93,15 @@ function firstNonEmpty(...values: (string | undefined | null)[]): string | null
|
|
|
90
93
|
return null;
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
function renderActiveRepoContextPrompt(activeRepoContext: ActiveRepoContext | null): string {
|
|
97
|
+
if (!activeRepoContext) return "";
|
|
98
|
+
return prompt
|
|
99
|
+
.render(activeRepoContextTemplate, {
|
|
100
|
+
relativeRepoRoot: normalizePromptPath(activeRepoContext.relativeRepoRoot),
|
|
101
|
+
})
|
|
102
|
+
.trim();
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
function parseWmicTable(output: string, header: string): string | null {
|
|
94
106
|
const lines = output
|
|
95
107
|
.split("\n")
|
|
@@ -425,6 +437,8 @@ export interface BuildSystemPromptOptions {
|
|
|
425
437
|
includeWorkspaceTree?: boolean;
|
|
426
438
|
/** Whether Mermaid fenced blocks render as terminal ASCII diagrams. Default: true */
|
|
427
439
|
renderMermaid?: boolean;
|
|
440
|
+
/** Pre-resolved nested active repo context. Undefined resolves from cwd. */
|
|
441
|
+
activeRepoContext?: ActiveRepoContext | null;
|
|
428
442
|
}
|
|
429
443
|
|
|
430
444
|
/** Result of building provider-facing system prompt messages. */
|
|
@@ -467,6 +481,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
467
481
|
personality = "default",
|
|
468
482
|
includeWorkspaceTree = false,
|
|
469
483
|
renderMermaid = true,
|
|
484
|
+
activeRepoContext: providedActiveRepoContext,
|
|
470
485
|
} = options;
|
|
471
486
|
const inlineToolDescriptors = providedInlineToolDescriptors ?? false;
|
|
472
487
|
const resolvedCwd = cwd ?? getProjectDir();
|
|
@@ -484,6 +499,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
484
499
|
totalLines: 0,
|
|
485
500
|
agentsMdFiles: [],
|
|
486
501
|
} satisfies WorkspaceTree,
|
|
502
|
+
activeRepoContext: null as ActiveRepoContext | null,
|
|
487
503
|
};
|
|
488
504
|
|
|
489
505
|
const deadline = Bun.sleep(SYSTEM_PROMPT_PREP_TIMEOUT_MS).then(() => "__timeout__" as const);
|
|
@@ -546,34 +562,42 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
546
562
|
: skillsSettings?.enabled !== false
|
|
547
563
|
? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
|
|
548
564
|
: Promise.resolve([]);
|
|
565
|
+
const activeRepoContextPromise =
|
|
566
|
+
providedActiveRepoContext !== undefined
|
|
567
|
+
? Promise.resolve(providedActiveRepoContext)
|
|
568
|
+
: logger.time("resolveActiveRepoContext", () => resolveActiveRepoContext(resolvedCwd));
|
|
549
569
|
|
|
550
|
-
const [
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
570
|
+
const [
|
|
571
|
+
resolvedCustomPrompt,
|
|
572
|
+
resolvedAppendPrompt,
|
|
573
|
+
systemPromptCustomization,
|
|
574
|
+
contextFiles,
|
|
575
|
+
skills,
|
|
576
|
+
workspaceTree,
|
|
577
|
+
activeRepoContext,
|
|
578
|
+
] = await Promise.all([
|
|
579
|
+
withDeadline(
|
|
580
|
+
"customPrompt",
|
|
581
|
+
providedResolvedCustomPrompt !== undefined
|
|
582
|
+
? Promise.resolve(providedResolvedCustomPrompt)
|
|
583
|
+
: resolvePromptInput(customPrompt, "system prompt"),
|
|
584
|
+
prepDefaults.resolvedCustomPrompt,
|
|
585
|
+
),
|
|
586
|
+
withDeadline(
|
|
587
|
+
"appendSystemPrompt",
|
|
588
|
+
providedResolvedAppendPrompt !== undefined
|
|
589
|
+
? Promise.resolve(providedResolvedAppendPrompt)
|
|
590
|
+
: resolvePromptInput(appendSystemPrompt, "append system prompt"),
|
|
591
|
+
prepDefaults.resolvedAppendPrompt,
|
|
592
|
+
),
|
|
593
|
+
withDeadline("loadSystemPromptFiles", systemPromptCustomizationPromise, prepDefaults.systemPromptCustomization),
|
|
594
|
+
withDeadline("loadProjectContextFiles", contextFilesPromise, prepDefaults.contextFiles).then(
|
|
595
|
+
dedupeExactContextFiles,
|
|
596
|
+
),
|
|
597
|
+
withDeadline("loadSkills", skillsPromise, prepDefaults.skills),
|
|
598
|
+
withDeadline("buildWorkspaceTree", workspaceTreePromise, prepDefaults.workspaceTree),
|
|
599
|
+
withDeadline("resolveActiveRepoContext", activeRepoContextPromise, prepDefaults.activeRepoContext),
|
|
600
|
+
]);
|
|
577
601
|
const agentsMdFiles = Array.from(new Set(workspaceTree.agentsMdFiles)).sort().slice(0, AGENTS_MD_LIMIT);
|
|
578
602
|
|
|
579
603
|
if (timedOut.length > 0) {
|
|
@@ -598,7 +622,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
598
622
|
|
|
599
623
|
const date = new Date().toISOString().slice(0, 10);
|
|
600
624
|
const dateTime = date;
|
|
601
|
-
const promptCwd = shortenPath(resolvedCwd
|
|
625
|
+
const promptCwd = shortenPath(normalizePromptPath(resolvedCwd));
|
|
626
|
+
const activeRepoContextPrompt = renderActiveRepoContextPrompt(activeRepoContext);
|
|
602
627
|
|
|
603
628
|
// Build tool metadata for system prompt rendering.
|
|
604
629
|
// Priority: explicit list > tools map > conservative SDK fallback.
|
|
@@ -697,6 +722,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
697
722
|
if (projectPrompt) {
|
|
698
723
|
systemPrompt.push(projectPrompt);
|
|
699
724
|
}
|
|
725
|
+
if (activeRepoContextPrompt) {
|
|
726
|
+
systemPrompt.push(activeRepoContextPrompt);
|
|
727
|
+
}
|
|
700
728
|
|
|
701
729
|
return { systemPrompt };
|
|
702
730
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -421,6 +421,12 @@ export interface ExecutorOptions {
|
|
|
421
421
|
* passes its own `getAgentId()`).
|
|
422
422
|
*/
|
|
423
423
|
parentAgentId?: string;
|
|
424
|
+
/**
|
|
425
|
+
* Keep the finished subagent addressable in the registry for IRC/revival.
|
|
426
|
+
* Defaults to true. Eval bridge agents are programmatic one-shot helpers and
|
|
427
|
+
* set this false so disposal unregisters them instead of leaving idle peers.
|
|
428
|
+
*/
|
|
429
|
+
keepAlive?: boolean;
|
|
424
430
|
}
|
|
425
431
|
|
|
426
432
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -854,6 +860,8 @@ interface SubagentRunMonitor {
|
|
|
854
860
|
/** Whether the (attempted) abort counts as a cancelled run rather than an internal failure. */
|
|
855
861
|
isAbortedRun(): boolean;
|
|
856
862
|
requestAbort(reason: AbortReason): void;
|
|
863
|
+
abortActiveSession(): Promise<void>;
|
|
864
|
+
waitForActiveSessionAbort(): Promise<void>;
|
|
857
865
|
resolveSignalAbortReason(): string;
|
|
858
866
|
resolveAbortReasonText(): string;
|
|
859
867
|
setActiveSession(session: AgentSession | null): void;
|
|
@@ -923,6 +931,22 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
923
931
|
let budgetSteerSent = false;
|
|
924
932
|
let budgetLimitExceeded = false;
|
|
925
933
|
let lastAssistantSalvageText: string | undefined;
|
|
934
|
+
let activeSessionAbortPromise: Promise<void> | undefined;
|
|
935
|
+
|
|
936
|
+
const abortActiveSession = (): Promise<void> => {
|
|
937
|
+
const session = activeSession;
|
|
938
|
+
if (!session) return Promise.resolve();
|
|
939
|
+
activeSessionAbortPromise ??= session.abort().catch(error => {
|
|
940
|
+
logger.debug("Subagent session abort cleanup failed", {
|
|
941
|
+
error: error instanceof Error ? error.message : String(error),
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
return activeSessionAbortPromise;
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
const waitForActiveSessionAbort = async (): Promise<void> => {
|
|
948
|
+
if (activeSessionAbortPromise) await activeSessionAbortPromise;
|
|
949
|
+
};
|
|
926
950
|
|
|
927
951
|
const requestAbort = (reason: AbortReason) => {
|
|
928
952
|
if (reason === "timeout") {
|
|
@@ -941,9 +965,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
941
965
|
abortSent = true;
|
|
942
966
|
abortReason = reason;
|
|
943
967
|
abortController.abort();
|
|
944
|
-
|
|
945
|
-
void activeSession.abort();
|
|
946
|
-
}
|
|
968
|
+
void abortActiveSession();
|
|
947
969
|
};
|
|
948
970
|
|
|
949
971
|
// Handle abort signal
|
|
@@ -1423,6 +1445,8 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1423
1445
|
isAbortedRun: () =>
|
|
1424
1446
|
abortReason === "signal" || runtimeLimitExceeded || budgetLimitExceeded || abortReason === undefined,
|
|
1425
1447
|
requestAbort,
|
|
1448
|
+
abortActiveSession,
|
|
1449
|
+
waitForActiveSessionAbort,
|
|
1426
1450
|
resolveSignalAbortReason,
|
|
1427
1451
|
resolveAbortReasonText,
|
|
1428
1452
|
setActiveSession: session => {
|
|
@@ -1752,6 +1776,59 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1752
1776
|
};
|
|
1753
1777
|
}
|
|
1754
1778
|
|
|
1779
|
+
export async function finalizeSubagentLifecycle(args: {
|
|
1780
|
+
id: string;
|
|
1781
|
+
session: AgentSession;
|
|
1782
|
+
aborted: boolean;
|
|
1783
|
+
keepAlive: boolean;
|
|
1784
|
+
isolated: boolean;
|
|
1785
|
+
agentIdleTtlMs: number;
|
|
1786
|
+
reviveSession: (() => Promise<AgentSession>) | null;
|
|
1787
|
+
}): Promise<void> {
|
|
1788
|
+
const registry = AgentRegistry.global();
|
|
1789
|
+
const disposeSession = async (): Promise<void> => {
|
|
1790
|
+
try {
|
|
1791
|
+
await untilAborted(AbortSignal.timeout(5000), () => args.session.dispose());
|
|
1792
|
+
} catch {
|
|
1793
|
+
// Ignore cleanup errors
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
if (args.aborted) {
|
|
1798
|
+
// Hard abort (caller signal / wall-clock / budget): terminal teardown.
|
|
1799
|
+
registry.setStatus(args.id, "aborted");
|
|
1800
|
+
await disposeSession();
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
if (!args.keepAlive) {
|
|
1805
|
+
// One-shot helper: dispose and unregister. No IRC, no revival.
|
|
1806
|
+
await disposeSession();
|
|
1807
|
+
registry.unregister(args.id);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (args.isolated) {
|
|
1812
|
+
// Isolated run: the worktree is merged + cleaned after the run, so
|
|
1813
|
+
// the session is not resumable. Park the ref WITHOUT adopting — the
|
|
1814
|
+
// transcript stays reachable (history://), but ensureLive will throw.
|
|
1815
|
+
// Status must flip to "parked" before dispose so the sdk dispose
|
|
1816
|
+
// wrapper skips unregister.
|
|
1817
|
+
registry.setStatus(args.id, "parked");
|
|
1818
|
+
await disposeSession();
|
|
1819
|
+
registry.detachSession(args.id);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Keep-alive: finished and failed subagents both stay interrogable.
|
|
1824
|
+
// The lifecycle manager owns idle-TTL parking + revival from here on.
|
|
1825
|
+
registry.setStatus(args.id, "idle");
|
|
1826
|
+
AgentLifecycleManager.global().adopt(args.id, {
|
|
1827
|
+
idleTtlMs: args.agentIdleTtlMs,
|
|
1828
|
+
revive: args.reviveSession ?? undefined,
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1755
1832
|
/**
|
|
1756
1833
|
* Run a single agent in-process.
|
|
1757
1834
|
*/
|
|
@@ -2211,7 +2288,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2211
2288
|
abortSignal.addEventListener(
|
|
2212
2289
|
"abort",
|
|
2213
2290
|
() => {
|
|
2214
|
-
void
|
|
2291
|
+
void monitor.abortActiveSession();
|
|
2215
2292
|
},
|
|
2216
2293
|
{ once: true, signal: sessionAbortController.signal },
|
|
2217
2294
|
);
|
|
@@ -2219,7 +2296,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2219
2296
|
// the awaited setup above, the listener registration races the dispatch
|
|
2220
2297
|
// and may not observe the already-fired abort event. Mirror it manually.
|
|
2221
2298
|
if (abortSignal.aborted) {
|
|
2222
|
-
void
|
|
2299
|
+
void monitor.abortActiveSession();
|
|
2223
2300
|
}
|
|
2224
2301
|
|
|
2225
2302
|
const pendingExtensionMessages: Array<Promise<unknown>> = [];
|
|
@@ -2325,6 +2402,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2325
2402
|
providerSemaphoreAcquired = false;
|
|
2326
2403
|
}
|
|
2327
2404
|
sessionAbortController.abort();
|
|
2405
|
+
try {
|
|
2406
|
+
await untilAborted(AbortSignal.timeout(5000), () => monitor.waitForActiveSessionAbort());
|
|
2407
|
+
} catch {
|
|
2408
|
+
// Ignore abort cleanup timeouts/errors; terminal disposal below is still best-effort.
|
|
2409
|
+
}
|
|
2328
2410
|
if (unsubscribe) {
|
|
2329
2411
|
try {
|
|
2330
2412
|
unsubscribe();
|
|
@@ -2336,37 +2418,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2336
2418
|
const session = monitor.takeActiveSession();
|
|
2337
2419
|
if (session) {
|
|
2338
2420
|
monitor.captureSalvage(session);
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
} else if (worktree !== undefined) {
|
|
2349
|
-
// Isolated run: the worktree is merged + cleaned after the run, so
|
|
2350
|
-
// the session is not resumable. Park the ref WITHOUT adopting — the
|
|
2351
|
-
// transcript stays reachable (history://), but ensureLive will throw.
|
|
2352
|
-
// Status must flip to "parked" before dispose so the sdk dispose
|
|
2353
|
-
// wrapper skips unregister.
|
|
2354
|
-
registry.setStatus(id, "parked");
|
|
2355
|
-
try {
|
|
2356
|
-
await untilAborted(AbortSignal.timeout(5000), () => session.dispose());
|
|
2357
|
-
} catch {
|
|
2358
|
-
// Ignore cleanup errors
|
|
2359
|
-
}
|
|
2360
|
-
registry.detachSession(id);
|
|
2361
|
-
} else {
|
|
2362
|
-
// Keep-alive: finished and failed subagents both stay interrogable.
|
|
2363
|
-
// The lifecycle manager owns idle-TTL parking + revival from here on.
|
|
2364
|
-
registry.setStatus(id, "idle");
|
|
2365
|
-
AgentLifecycleManager.global().adopt(id, {
|
|
2366
|
-
idleTtlMs: agentIdleTtlMs,
|
|
2367
|
-
revive: reviveSession ?? undefined,
|
|
2368
|
-
});
|
|
2369
|
-
}
|
|
2421
|
+
await finalizeSubagentLifecycle({
|
|
2422
|
+
id,
|
|
2423
|
+
session,
|
|
2424
|
+
aborted,
|
|
2425
|
+
keepAlive: options.keepAlive !== false,
|
|
2426
|
+
isolated: worktree !== undefined,
|
|
2427
|
+
agentIdleTtlMs,
|
|
2428
|
+
reviveSession,
|
|
2429
|
+
});
|
|
2370
2430
|
}
|
|
2371
2431
|
}
|
|
2372
2432
|
|
package/src/task/index.ts
CHANGED
|
@@ -136,8 +136,8 @@ export {
|
|
|
136
136
|
// Fail-safe: any unknown tool makes the agent not read-only.
|
|
137
137
|
export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
138
138
|
"read",
|
|
139
|
-
"
|
|
140
|
-
"
|
|
139
|
+
"grep",
|
|
140
|
+
"glob",
|
|
141
141
|
"web_search",
|
|
142
142
|
"ast_grep",
|
|
143
143
|
"yield",
|
|
@@ -1076,7 +1076,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1076
1076
|
}
|
|
1077
1077
|
|
|
1078
1078
|
const planModeState = this.session.getPlanModeState?.();
|
|
1079
|
-
const planModeBaseTools = ["read", "
|
|
1079
|
+
const planModeBaseTools = ["read", "grep", "glob", "lsp", "web_search"];
|
|
1080
1080
|
const planModeTools = [
|
|
1081
1081
|
...planModeBaseTools,
|
|
1082
1082
|
...(agent.tools ?? []).filter(
|