@bastani/atomic 0.9.5-alpha.8 → 0.9.5-alpha.9
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 +16 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +11 -0
- package/dist/builtin/mcp/README.md +6 -6
- package/dist/builtin/mcp/direct-tools.ts +41 -23
- package/dist/builtin/mcp/index.ts +31 -4
- package/dist/builtin/mcp/init.ts +4 -43
- package/dist/builtin/mcp/metadata-hydration.ts +37 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/mcp/proxy-info-modes.ts +67 -14
- package/dist/builtin/mcp/startup-warmup.ts +98 -0
- package/dist/builtin/mcp/tool-result-renderer.ts +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +12 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/index.ts +14 -25
- package/dist/builtin/subagents/src/extension/startup-maintenance.ts +89 -0
- package/dist/builtin/subagents/src/runs/background/async-execution-common.ts +23 -18
- package/dist/builtin/subagents/src/runs/background/result-watcher.ts +20 -10
- package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +13 -3
- package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +10 -2
- package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +28 -1
- package/dist/builtin/subagents/src/tui/render-widget.ts +29 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +18 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/builtin/goal-runner.ts +39 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +16 -13
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +1 -0
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.native.md +58 -0
- package/dist/builtin/workflows/skills/impeccable/reference/android.md +40 -0
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +2 -0
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +2 -0
- package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +139 -0
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +69 -62
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +21 -7
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +92 -0
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +29 -7
- package/dist/builtin/workflows/skills/impeccable/reference/ios.md +45 -0
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +25 -1
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +23 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +2 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +75 -14
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +31 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +64 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +118 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +16 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +66 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +42 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/fonts.mjs +30 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +4 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +660 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-before-edit.mjs +516 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +1764 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +61 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +4 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +13 -16
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +17 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +104 -37
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +9 -0
- package/dist/builtin/workflows/src/durable/backend.ts +12 -0
- package/dist/builtin/workflows/src/durable/dbos-backend.ts +17 -1
- package/dist/builtin/workflows/src/durable/resume-catalog.ts +12 -0
- package/dist/builtin/workflows/src/durable/resume-runtime.ts +5 -2
- package/dist/builtin/workflows/src/durable/types.ts +16 -0
- package/dist/builtin/workflows/src/engine/primitives/task.ts +1 -0
- package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +1 -2
- package/dist/builtin/workflows/src/engine/run-returned-status.ts +50 -16
- package/dist/builtin/workflows/src/engine/run.ts +20 -19
- package/dist/builtin/workflows/src/engine/runtime.ts +5 -0
- package/dist/builtin/workflows/src/extension/extension-factory.ts +3 -0
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +9 -2
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +137 -27
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +22 -6
- package/dist/builtin/workflows/src/extension/public-types.ts +1 -1
- package/dist/builtin/workflows/src/extension/workflow-command-completions.ts +13 -1
- package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +19 -4
- package/dist/builtin/workflows/src/extension/workflow-command-surfaces.ts +9 -0
- package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +48 -23
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +20 -8
- package/dist/builtin/workflows/src/extension/workflow-tool.ts +24 -8
- package/dist/builtin/workflows/src/runs/background/status.ts +4 -3
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +29 -8
- package/dist/builtin/workflows/src/runs/foreground/executor-hil.ts +2 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +3 -2
- package/dist/builtin/workflows/src/runs/shared/worktree-git.ts +155 -20
- package/dist/builtin/workflows/src/runs/shared/worktree-types.ts +6 -0
- package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -1
- package/dist/builtin/workflows/src/shared/returned-run-status.ts +116 -0
- package/dist/builtin/workflows/src/shared/workflow-failures-decisions.ts +2 -0
- package/dist/builtin/workflows/src/tui/graph-view-render-helpers.ts +21 -3
- package/dist/builtin/workflows/src/tui/graph-view-state.ts +3 -0
- package/dist/builtin/workflows/src/tui/graph-view-types.ts +3 -0
- package/dist/builtin/workflows/src/tui/status-list.ts +22 -15
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +4 -4
- package/dist/core/agent-session-auto-compaction.d.ts +23 -3
- package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-auto-compaction.js +129 -17
- package/dist/core/agent-session-auto-compaction.js.map +1 -1
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +7 -0
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +6 -3
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-prompt.d.ts.map +1 -1
- package/dist/core/agent-session-prompt.js +1 -1
- package/dist/core/agent-session-prompt.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.d.ts +8 -1
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +2 -1
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +6 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +72 -29
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/openai-responses-payload-sanitizer.d.ts +1 -0
- package/dist/core/openai-responses-payload-sanitizer.d.ts.map +1 -1
- package/dist/core/openai-responses-payload-sanitizer.js +24 -9
- package/dist/core/openai-responses-payload-sanitizer.js.map +1 -1
- package/dist/core/slash-commands.d.ts +2 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +141 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main-deferred-startup.d.ts.map +1 -1
- package/dist/main-deferred-startup.js +2 -2
- package/dist/main-deferred-startup.js.map +1 -1
- package/dist/main-early-input.d.ts.map +1 -1
- package/dist/main-early-input.js +7 -0
- package/dist/main-early-input.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +0 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +1 -0
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-autocomplete.js +21 -9
- package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-deferred-startup.js +41 -28
- package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-hotkeys-debug.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-hotkeys-debug.js +1 -0
- package/dist/modes/interactive/interactive-hotkeys-debug.js.map +1 -1
- package/dist/modes/interactive/interactive-input-handling.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-input-handling.js +41 -6
- package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.js +3 -0
- package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.js +1 -1
- package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -2
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-model-routing.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-model-routing.js +3 -1
- package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-process-lifecycle.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-process-lifecycle.js +0 -3
- package/dist/modes/interactive/interactive-process-lifecycle.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +15 -1
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive/interactive-session-routing.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-session-routing.js +5 -1
- package/dist/modes/interactive/interactive-session-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +20 -21
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive/theme/global-theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/global-theme.js +26 -2
- package/dist/modes/interactive/theme/global-theme.js.map +1 -1
- package/dist/utils/fs-watch.d.ts +24 -1
- package/dist/utils/fs-watch.d.ts.map +1 -1
- package/dist/utils/fs-watch.js +60 -5
- package/dist/utils/fs-watch.js.map +1 -1
- package/docs/compaction.md +5 -1
- package/docs/development.md +4 -0
- package/docs/extensions.md +5 -0
- package/docs/json.md +1 -1
- package/docs/quickstart.md +1 -1
- package/docs/settings.md +1 -1
- package/docs/subagents.md +4 -0
- package/docs/windows.md +4 -0
- package/docs/workflows.md +8 -6
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { McpExtensionState } from "./state.ts";
|
|
2
|
+
import { getMissingConfiguredDirectToolServers } from "./direct-tools.ts";
|
|
3
|
+
import { loadMetadataCache } from "./metadata-cache.ts";
|
|
4
|
+
import { buildToolMetadata } from "./tool-metadata.ts";
|
|
5
|
+
import { parallelLimit } from "./utils.ts";
|
|
6
|
+
import { logger } from "./logger.ts";
|
|
7
|
+
import { updateMetadataCache, updateStatusBar } from "./init.ts";
|
|
8
|
+
|
|
9
|
+
export interface McpStartupWarmupOptions {
|
|
10
|
+
shouldContinue?: () => boolean;
|
|
11
|
+
onDirectToolsChanged?: () => void | Promise<void>;
|
|
12
|
+
onSettled?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface McpStartupWarmupHandle {
|
|
16
|
+
readonly promise: Promise<void>;
|
|
17
|
+
cancel(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function deferToMacrotask(): Promise<void> {
|
|
21
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function scheduleMcpStartupWarmup(
|
|
25
|
+
state: McpExtensionState,
|
|
26
|
+
options: McpStartupWarmupOptions = {},
|
|
27
|
+
): McpStartupWarmupHandle {
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
const shouldContinue = (): boolean => !cancelled && (options.shouldContinue?.() ?? true);
|
|
30
|
+
|
|
31
|
+
const promise = (async () => {
|
|
32
|
+
await deferToMacrotask();
|
|
33
|
+
if (!shouldContinue()) return;
|
|
34
|
+
|
|
35
|
+
const envDirect = process.env.MCP_DIRECT_TOOLS;
|
|
36
|
+
if (envDirect === "__none__") return;
|
|
37
|
+
|
|
38
|
+
const envDirectTools = envDirect?.split(",").map((item) => item.trim()).filter(Boolean);
|
|
39
|
+
const missingCacheServers = getMissingConfiguredDirectToolServers(state.config, loadMetadataCache(), envDirectTools)
|
|
40
|
+
.filter((name) => state.manager.getConnection(name)?.status !== "connected");
|
|
41
|
+
if (missingCacheServers.length === 0) return;
|
|
42
|
+
|
|
43
|
+
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
44
|
+
const results = await parallelLimit(missingCacheServers, 10, async (name) => {
|
|
45
|
+
if (!shouldContinue()) return { name, ok: false };
|
|
46
|
+
const definition = state.config.mcpServers[name];
|
|
47
|
+
if (!definition) return { name, ok: false };
|
|
48
|
+
try {
|
|
49
|
+
const connection = await state.manager.connect(name, definition);
|
|
50
|
+
if (!shouldContinue()) {
|
|
51
|
+
if (state.manager.getConnection(name) === connection) {
|
|
52
|
+
await state.manager.close(name);
|
|
53
|
+
}
|
|
54
|
+
return { name, ok: false };
|
|
55
|
+
}
|
|
56
|
+
if (connection.status === "needs-auth") return { name, ok: false };
|
|
57
|
+
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
|
|
58
|
+
state.toolMetadata.set(name, metadata);
|
|
59
|
+
updateMetadataCache(state, name);
|
|
60
|
+
return { name, ok: true };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
logger.debug(`MCP: background direct-tools warmup failed for ${name}: ${message}`);
|
|
64
|
+
return { name, ok: false };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!shouldContinue()) return;
|
|
69
|
+
const warmed = results.filter((result) => result.ok).map((result) => result.name);
|
|
70
|
+
if (warmed.length === 0) return;
|
|
71
|
+
updateStatusBar(state);
|
|
72
|
+
if (!shouldContinue()) return;
|
|
73
|
+
await options.onDirectToolsChanged?.();
|
|
74
|
+
if (!shouldContinue()) return;
|
|
75
|
+
if (state.ui) {
|
|
76
|
+
state.ui.notify(`MCP: direct tools for ${warmed.join(", ")} are now available`, "info");
|
|
77
|
+
}
|
|
78
|
+
})()
|
|
79
|
+
.catch((error) => {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
logger.debug(`MCP: background startup warmup failed: ${message}`);
|
|
82
|
+
})
|
|
83
|
+
.finally(() => {
|
|
84
|
+
try {
|
|
85
|
+
options.onSettled?.();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
logger.debug(`MCP: background startup warmup settled callback failed: ${message}`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
promise,
|
|
94
|
+
cancel() {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { keyText, type AgentToolResult, type ToolRenderResultOptions } from "@bastani/atomic";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
|
-
type McpToolResultDetails = Record<string, unknown> & { error?: unknown };
|
|
4
|
+
export type McpToolResultDetails = Record<string, unknown> & { error?: unknown };
|
|
5
5
|
type McpToolContentBlock = AgentToolResult<McpToolResultDetails>["content"][number];
|
|
6
6
|
|
|
7
7
|
interface RenderTheme {
|
|
@@ -58,7 +58,7 @@ export function renderMcpToolResult(
|
|
|
58
58
|
.map((line) => line === "…" ? theme.fg("muted", line) : theme.fg("toolOutput", line))
|
|
59
59
|
.join("\n");
|
|
60
60
|
const hint = display.truncated && !options.expanded
|
|
61
|
-
? `\n${theme.fg("muted", "
|
|
61
|
+
? `\n${theme.fg("muted", `(${keyText("app.tools.expand")} Expand)`)}`
|
|
62
62
|
: "";
|
|
63
63
|
|
|
64
64
|
return new Text(`${output}${hint}`, 0, 0);
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.5-alpha.9] - 2026-07-09
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Improved subagent extension startup by deferring old-run cleanup scans, result watcher startup, and existing-result priming to macrotasks while preserving immediate `subagent` tool and slash-command registration.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed background async result watching on Windows to use Atomic's safe watcher path strategy and fall back to polling when native `fs.watch` would receive an unsafe or non-canonical watcher path.
|
|
14
|
+
- Fixed async/background subagent status visibility while the fullscreen workflow graph is active by publishing the live async summary through the shared status surface in addition to the below-editor widget.
|
|
15
|
+
- Fixed foreground and background subagent startup diagnostics so missing or invalid child `cwd` values are reported as working-directory problems before spawn, while runtime spawn failures include the attempted command and cwd instead of ambiguous `ENOENT` output.
|
|
16
|
+
|
|
5
17
|
## [0.9.5-alpha.8] - 2026-07-08
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/subagents",
|
|
3
|
-
"version": "0.9.5-alpha.
|
|
3
|
+
"version": "0.9.5-alpha.9",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
|
|
6
6
|
"contributors": [
|
|
@@ -7,21 +7,18 @@ import { APP_NAME, getEnvValue } from "@bastani/atomic";
|
|
|
7
7
|
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@bastani/atomic";
|
|
8
8
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
9
9
|
import { discoverAgents } from "../agents/agents.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { getArtifactsDir } from "../shared/artifacts.ts";
|
|
11
11
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
12
|
-
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
13
12
|
import { advanceResultPulseFrame, renderLiveSubagentResult, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, type SubagentResultRenderState } from "../tui/render.ts";
|
|
14
13
|
import { SubagentParams } from "./schemas.ts";
|
|
15
14
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
16
15
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
17
|
-
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
18
16
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
19
17
|
import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
|
|
20
18
|
import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
|
|
21
19
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
22
20
|
import { inspectSubagentStatus } from "../runs/background/run-status.ts";
|
|
23
21
|
import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
|
|
24
|
-
import { cleanupOldNestedRuntimeDirs } from "../runs/shared/nested-events.ts";
|
|
25
22
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
26
23
|
import registerFanoutChildSubagentExtension from "./fanout-child.ts";
|
|
27
24
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
@@ -29,6 +26,7 @@ import { loadConfig } from "./config.ts";
|
|
|
29
26
|
import { DEFAULT_PROMPT_GUIDANCE } from "./prompt-guidance.ts";
|
|
30
27
|
import { type Details, type SubagentState, ASYNC_DIR, DEFAULT_ARTIFACT_CONFIG, RESULTS_DIR, SLASH_RESULT_TYPE, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_CONTROL_EVENT, WIDGET_KEY } from "../shared/types.ts";
|
|
31
28
|
import { clearPendingForegroundControlNotices, formatSubagentControlNotice, handleSubagentControlNotice, SUBAGENT_CONTROL_MESSAGE_TYPE, type SubagentControlMessageDetails } from "./control-notices.ts";
|
|
29
|
+
import { createSubagentStartupMaintenance } from "./startup-maintenance.ts";
|
|
32
30
|
export { loadConfig } from "./config.ts";
|
|
33
31
|
function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
34
32
|
if (parentSessionFile) {
|
|
@@ -171,12 +169,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
171
169
|
}
|
|
172
170
|
ensureAccessibleDir(RESULTS_DIR);
|
|
173
171
|
ensureAccessibleDir(ASYNC_DIR);
|
|
174
|
-
cleanupOldChainDirs();
|
|
175
172
|
const config = loadConfig();
|
|
176
173
|
const asyncByDefault = config.asyncByDefault === true;
|
|
177
174
|
const tempArtifactsDir = getArtifactsDir(null);
|
|
178
|
-
cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
|
|
179
|
-
cleanupOldNestedRuntimeDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
|
|
180
175
|
const state: SubagentState = {
|
|
181
176
|
baseCwd: "",
|
|
182
177
|
currentSessionId: null,
|
|
@@ -197,16 +192,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
197
192
|
clear: () => {},
|
|
198
193
|
},
|
|
199
194
|
};
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
|
|
195
|
+
const maintenance = createSubagentStartupMaintenance(pi, state, {
|
|
196
|
+
resultsDir: RESULTS_DIR,
|
|
197
|
+
artifactCleanupDays: DEFAULT_ARTIFACT_CONFIG.cleanupDays,
|
|
198
|
+
resultTtlMs: 10 * 60 * 1000,
|
|
199
|
+
});
|
|
200
|
+
maintenance.scheduleStartupCleanup();
|
|
201
|
+
maintenance.startResultWatcherDeferred();
|
|
202
|
+
maintenance.primeExistingResultsDeferred();
|
|
208
203
|
const runtimeCleanup = () => {
|
|
209
|
-
|
|
204
|
+
maintenance.stop();
|
|
210
205
|
stopWidgetAnimation();
|
|
211
206
|
stopResultAnimations();
|
|
212
207
|
clearPendingForegroundControlNotices(state);
|
|
@@ -430,13 +425,7 @@ DIAGNOSTICS:
|
|
|
430
425
|
if (state.asyncJobs.size > 0) ensurePoller();
|
|
431
426
|
});
|
|
432
427
|
const cleanupSessionArtifacts = (ctx: ExtensionContext) => {
|
|
433
|
-
|
|
434
|
-
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
435
|
-
if (sessionFile) {
|
|
436
|
-
cleanupOldArtifacts(getArtifactsDir(sessionFile), DEFAULT_ARTIFACT_CONFIG.cleanupDays);
|
|
437
|
-
}
|
|
438
|
-
} catch {
|
|
439
|
-
}
|
|
428
|
+
maintenance.cleanupSessionArtifactsDeferred(ctx);
|
|
440
429
|
};
|
|
441
430
|
const resetSessionState = (ctx: ExtensionContext) => {
|
|
442
431
|
state.baseCwd = ctx.cwd;
|
|
@@ -447,7 +436,7 @@ DIAGNOSTICS:
|
|
|
447
436
|
resetJobs(ctx);
|
|
448
437
|
hydrateActiveJobs(ctx);
|
|
449
438
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
450
|
-
|
|
439
|
+
maintenance.primeExistingResultsDeferred();
|
|
451
440
|
};
|
|
452
441
|
pi.on("session_start", (_event, ctx) => {
|
|
453
442
|
resetSessionState(ctx);
|
|
@@ -462,7 +451,7 @@ DIAGNOSTICS:
|
|
|
462
451
|
if (globalStore[eventUnsubscribeStoreKey] === eventUnsubscribes) {
|
|
463
452
|
delete globalStore[eventUnsubscribeStoreKey];
|
|
464
453
|
}
|
|
465
|
-
|
|
454
|
+
maintenance.stop();
|
|
466
455
|
stopWidgetAnimation();
|
|
467
456
|
stopResultAnimations();
|
|
468
457
|
if (state.poller) clearInterval(state.poller);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@bastani/atomic";
|
|
2
|
+
import type { SubagentState } from "../shared/types.ts";
|
|
3
|
+
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
4
|
+
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
5
|
+
import { cleanupOldNestedRuntimeDirs } from "../runs/shared/nested-events.ts";
|
|
6
|
+
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
7
|
+
|
|
8
|
+
export interface SubagentStartupMaintenance {
|
|
9
|
+
scheduleStartupCleanup(): void;
|
|
10
|
+
startResultWatcherDeferred(): void;
|
|
11
|
+
primeExistingResultsDeferred(): void;
|
|
12
|
+
cleanupSessionArtifactsDeferred(ctx: ExtensionContext): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function scheduleMacrotask(task: () => void): () => void {
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
const handle = setImmediate(() => {
|
|
19
|
+
if (!cancelled) task();
|
|
20
|
+
});
|
|
21
|
+
handle.unref?.();
|
|
22
|
+
return () => {
|
|
23
|
+
cancelled = true;
|
|
24
|
+
clearImmediate(handle);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function swallowCleanup(task: () => void): void {
|
|
29
|
+
try {
|
|
30
|
+
task();
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createSubagentStartupMaintenance(
|
|
36
|
+
pi: ExtensionAPI,
|
|
37
|
+
state: SubagentState,
|
|
38
|
+
options: {
|
|
39
|
+
resultsDir: string;
|
|
40
|
+
artifactCleanupDays: number;
|
|
41
|
+
resultTtlMs: number;
|
|
42
|
+
},
|
|
43
|
+
): SubagentStartupMaintenance {
|
|
44
|
+
const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
|
|
45
|
+
pi,
|
|
46
|
+
state,
|
|
47
|
+
options.resultsDir,
|
|
48
|
+
options.resultTtlMs,
|
|
49
|
+
);
|
|
50
|
+
const cancelTasks = new Set<() => void>();
|
|
51
|
+
const schedule = (task: () => void): void => {
|
|
52
|
+
const cancel = scheduleMacrotask(() => {
|
|
53
|
+
cancelTasks.delete(cancel);
|
|
54
|
+
task();
|
|
55
|
+
});
|
|
56
|
+
cancelTasks.add(cancel);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
scheduleStartupCleanup() {
|
|
61
|
+
schedule(() => {
|
|
62
|
+
swallowCleanup(cleanupOldChainDirs);
|
|
63
|
+
swallowCleanup(() => cleanupAllArtifactDirs(options.artifactCleanupDays));
|
|
64
|
+
swallowCleanup(() => cleanupOldNestedRuntimeDirs(options.artifactCleanupDays));
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
startResultWatcherDeferred() {
|
|
68
|
+
schedule(startResultWatcher);
|
|
69
|
+
},
|
|
70
|
+
primeExistingResultsDeferred() {
|
|
71
|
+
schedule(primeExistingResults);
|
|
72
|
+
},
|
|
73
|
+
cleanupSessionArtifactsDeferred(ctx) {
|
|
74
|
+
let sessionFile: string | null | undefined;
|
|
75
|
+
try {
|
|
76
|
+
sessionFile = ctx.sessionManager.getSessionFile();
|
|
77
|
+
} catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!sessionFile) return;
|
|
81
|
+
schedule(() => swallowCleanup(() => cleanupOldArtifacts(getArtifactsDir(sessionFile), options.artifactCleanupDays)));
|
|
82
|
+
},
|
|
83
|
+
stop() {
|
|
84
|
+
for (const cancel of cancelTasks) cancel();
|
|
85
|
+
cancelTasks.clear();
|
|
86
|
+
stopResultWatcher();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { APP_NAME } from "@bastani/atomic";
|
|
7
|
-
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
7
|
+
import { formatPiSpawnError, resolvePiPackageRoot, validatePiSpawnCwd } from "../shared/pi-spawn.ts";
|
|
8
8
|
import { getAsyncConfigPath, TEMP_ROOT_DIR, type SubagentRunMode } from "../../shared/types.ts";
|
|
9
9
|
import type { AsyncExecutionResult, AsyncSpawnResult } from "./async-execution-types.ts";
|
|
10
10
|
|
|
@@ -85,33 +85,38 @@ export function writeAsyncRunnerConfig(cfg: object, suffix: string): string {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function spawnRunner(cfg: object, suffix: string, cwd: string): AsyncSpawnResult {
|
|
88
|
+
const cwdValidation = validatePiSpawnCwd(cwd);
|
|
89
|
+
if (!cwdValidation.ok) return { error: cwdValidation.error };
|
|
90
|
+
|
|
88
91
|
if (!jitiCliPath) {
|
|
89
92
|
return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
try {
|
|
93
|
-
const cwdStats = fs.statSync(cwd);
|
|
94
|
-
if (!cwdStats.isDirectory()) {
|
|
95
|
-
return { error: `cwd is not a directory: ${cwd}` };
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
return { error: `cwd does not exist: ${cwd}` };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
95
|
const cfgPath = writeAsyncRunnerConfig(cfg, suffix);
|
|
102
96
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
103
97
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
const spawnSpec = { command: process.execPath, args: [jitiCliPath, runner, cfgPath] };
|
|
99
|
+
let proc: ReturnType<typeof spawn>;
|
|
100
|
+
try {
|
|
101
|
+
proc = spawn(spawnSpec.command, spawnSpec.args, {
|
|
102
|
+
cwd,
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: "ignore",
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const spawnError = error instanceof Error ? error : new Error(String(error));
|
|
109
|
+
return { error: formatPiSpawnError(spawnError, spawnSpec, cwd) };
|
|
110
|
+
}
|
|
110
111
|
proc.on("error", (error) => {
|
|
111
|
-
console.error(`[${APP_NAME}-subagents] async spawn failed: ${error
|
|
112
|
+
console.error(`[${APP_NAME}-subagents] async spawn failed: ${formatPiSpawnError(error, spawnSpec, cwd)}`);
|
|
112
113
|
});
|
|
113
114
|
if (typeof proc.pid !== "number") {
|
|
114
|
-
|
|
115
|
+
const noPidError = Object.assign(
|
|
116
|
+
new Error(`spawn ${spawnSpec.command} failed before assigning a pid`),
|
|
117
|
+
fs.existsSync(spawnSpec.command) ? {} : { code: "ENOENT" },
|
|
118
|
+
);
|
|
119
|
+
return { error: formatPiSpawnError(noPidError, spawnSpec, cwd) };
|
|
115
120
|
}
|
|
116
121
|
proc.unref();
|
|
117
122
|
return { pid: proc.pid };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { isSafeFsWatchPathError, watchWithErrorHandler } from "@bastani/atomic";
|
|
3
4
|
import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
4
5
|
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
5
6
|
import {
|
|
@@ -21,7 +22,9 @@ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-
|
|
|
21
22
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
22
23
|
const POLL_INTERVAL_MS = 3000;
|
|
23
24
|
|
|
24
|
-
type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "watch"
|
|
25
|
+
type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "watch"> & {
|
|
26
|
+
realpathSync?: typeof fs.realpathSync;
|
|
27
|
+
};
|
|
25
28
|
|
|
26
29
|
type ResultWatcherTimers = {
|
|
27
30
|
setTimeout: typeof setTimeout;
|
|
@@ -30,9 +33,12 @@ type ResultWatcherTimers = {
|
|
|
30
33
|
clearInterval: typeof clearInterval;
|
|
31
34
|
};
|
|
32
35
|
|
|
36
|
+
type ResultWatcherSafeWatch = typeof watchWithErrorHandler;
|
|
37
|
+
|
|
33
38
|
type ResultWatcherDeps = {
|
|
34
39
|
fs?: ResultWatcherFs;
|
|
35
40
|
timers?: ResultWatcherTimers;
|
|
41
|
+
safeWatch?: ResultWatcherSafeWatch;
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
type ResultFileChild = {
|
|
@@ -88,7 +94,7 @@ function isNotFoundError(error: unknown): boolean {
|
|
|
88
94
|
|
|
89
95
|
function shouldFallBackToPolling(error: unknown): boolean {
|
|
90
96
|
const code = getErrorCode(error);
|
|
91
|
-
return code === "EMFILE" || code === "ENOSPC";
|
|
97
|
+
return code === "EMFILE" || code === "ENOSPC" || isSafeFsWatchPathError(error);
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
export function createResultWatcher(
|
|
@@ -104,6 +110,7 @@ export function createResultWatcher(
|
|
|
104
110
|
} {
|
|
105
111
|
const fsApi = deps.fs ?? fs;
|
|
106
112
|
const timers = deps.timers ?? { setTimeout, clearTimeout, setInterval, clearInterval };
|
|
113
|
+
const safeWatch = deps.safeWatch ?? watchWithErrorHandler;
|
|
107
114
|
|
|
108
115
|
const handleResult = async (file: string) => {
|
|
109
116
|
const resultPath = path.join(resultsDir, file);
|
|
@@ -264,13 +271,7 @@ export function createResultWatcher(
|
|
|
264
271
|
state.watcherRestartTimer = null;
|
|
265
272
|
}
|
|
266
273
|
try {
|
|
267
|
-
|
|
268
|
-
if (ev !== "rename" || !file) return;
|
|
269
|
-
const fileName = file.toString();
|
|
270
|
-
if (!fileName.endsWith(".json")) return;
|
|
271
|
-
state.resultFileCoalescer.schedule(fileName);
|
|
272
|
-
});
|
|
273
|
-
state.watcher.on("error", (error) => {
|
|
274
|
+
const handleWatcherError = (error: Error) => {
|
|
274
275
|
if (shouldFallBackToPolling(error)) {
|
|
275
276
|
startPollingFallback(error);
|
|
276
277
|
return;
|
|
@@ -279,8 +280,17 @@ export function createResultWatcher(
|
|
|
279
280
|
state.watcher?.close();
|
|
280
281
|
state.watcher = null;
|
|
281
282
|
scheduleRestart();
|
|
283
|
+
};
|
|
284
|
+
state.watcher = safeWatch(resultsDir, (ev, file) => {
|
|
285
|
+
if (ev !== "rename" || !file) return;
|
|
286
|
+
const fileName = file.toString();
|
|
287
|
+
if (!fileName.endsWith(".json")) return;
|
|
288
|
+
state.resultFileCoalescer.schedule(fileName);
|
|
289
|
+
}, handleWatcherError, {
|
|
290
|
+
watch: fsApi.watch,
|
|
291
|
+
realpathSyncNative: fsApi.realpathSync?.native,
|
|
282
292
|
});
|
|
283
|
-
state.watcher
|
|
293
|
+
state.watcher?.unref?.();
|
|
284
294
|
} catch (error) {
|
|
285
295
|
if (shouldFallBackToPolling(error)) {
|
|
286
296
|
startPollingFallback(error);
|
|
@@ -5,7 +5,7 @@ import { appendJsonl } from "../../shared/artifacts.ts";
|
|
|
5
5
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
6
6
|
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
|
|
7
7
|
import { getSubagentDepthEnv } from "../../shared/types.ts";
|
|
8
|
-
import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
|
|
8
|
+
import { formatPiSpawnError, getPiSpawnCommand, validatePiSpawnCwd } from "../shared/pi-spawn.ts";
|
|
9
9
|
import {
|
|
10
10
|
assistantStopReason,
|
|
11
11
|
isAssistantFailureStopReason,
|
|
@@ -29,6 +29,17 @@ export function runPiStreaming(
|
|
|
29
29
|
registerInterrupt?: (interrupt: (() => void) | undefined) => void,
|
|
30
30
|
onChildEvent?: (event: ChildEvent) => void,
|
|
31
31
|
): Promise<RunPiStreamingResult> {
|
|
32
|
+
const cwdValidation = validatePiSpawnCwd(cwd);
|
|
33
|
+
if (!cwdValidation.ok) {
|
|
34
|
+
return Promise.resolve({
|
|
35
|
+
stderr: "",
|
|
36
|
+
exitCode: 1,
|
|
37
|
+
messages: [],
|
|
38
|
+
usage: emptyUsage(),
|
|
39
|
+
error: cwdValidation.error,
|
|
40
|
+
finalOutput: "",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
32
43
|
return new Promise((resolve) => {
|
|
33
44
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
34
45
|
const spawnEnv = {
|
|
@@ -273,8 +284,7 @@ export function runPiStreaming(
|
|
|
273
284
|
attemptWatchdog.clear();
|
|
274
285
|
outputStream.end();
|
|
275
286
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
276
|
-
const
|
|
277
|
-
const finalError = error ?? assistantError ?? spawnErrorMessage;
|
|
287
|
+
const finalError = error ?? assistantError ?? formatPiSpawnError(spawnError, spawnSpec, cwd);
|
|
278
288
|
resolve({
|
|
279
289
|
stderr,
|
|
280
290
|
exitCode: 1,
|
|
@@ -14,7 +14,7 @@ import { extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "
|
|
|
14
14
|
import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
|
|
15
15
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
16
16
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
|
|
17
|
-
import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
|
|
17
|
+
import { formatPiSpawnError, getPiSpawnCommand, validatePiSpawnCwd } from "../shared/pi-spawn.ts";
|
|
18
18
|
import { assistantStopReason, isAssistantFailureStopReason, shouldStartSubagentFinalDrain } from "../shared/final-drain.ts";
|
|
19
19
|
import { modelFailureMessage } from "../shared/model-fallback.ts";
|
|
20
20
|
import { createAttemptWatchdog } from "../shared/attempt-watchdog.ts";
|
|
@@ -124,6 +124,14 @@ export async function runSingleAttempt(
|
|
|
124
124
|
workflowStageSubagentGuard: options.workflowStageSubagentGuard,
|
|
125
125
|
}),
|
|
126
126
|
};
|
|
127
|
+
const cwdValidation = validatePiSpawnCwd(runCwd);
|
|
128
|
+
if (!cwdValidation.ok) {
|
|
129
|
+
cleanupTempDir(tempDir);
|
|
130
|
+
result.error = cwdValidation.error;
|
|
131
|
+
return finalizeSingleAttempt({
|
|
132
|
+
result, progress, exitCode: 1, interruptedByControl, allControlEvents: controlRuntime.allControlEvents, options, shared, startTime,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
127
135
|
const exitCode = await new Promise<number>((resolve) => {
|
|
128
136
|
const spawnSpec = getPiSpawnCommand(args);
|
|
129
137
|
const proc = spawn(spawnSpec.command, spawnSpec.args, {
|
|
@@ -418,7 +426,7 @@ export async function runSingleAttempt(
|
|
|
418
426
|
attemptWatchdog.clear();
|
|
419
427
|
void jsonlWriter.close().catch(() => undefined);
|
|
420
428
|
cleanupTempDir(tempDir);
|
|
421
|
-
if (!result.error) result.error = error
|
|
429
|
+
if (!result.error) result.error = formatPiSpawnError(error, spawnSpec, runCwd);
|
|
422
430
|
finish(1);
|
|
423
431
|
});
|
|
424
432
|
|
|
@@ -42,9 +42,36 @@ export interface PiSpawnDeps {
|
|
|
42
42
|
resolvePackageJson?: () => string;
|
|
43
43
|
resolvePackageEntry?: () => string;
|
|
44
44
|
piPackageRoot?: string;
|
|
45
|
+
statSync?: (filePath: string) => fs.Stats;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
export type PiSpawnCwdValidationResult = { ok: true } | { ok: false; error: string };
|
|
49
|
+
|
|
50
|
+
export function validatePiSpawnCwd(cwd: string, deps: PiSpawnDeps = {}): PiSpawnCwdValidationResult {
|
|
51
|
+
const statSync = deps.statSync ?? fs.statSync;
|
|
52
|
+
try {
|
|
53
|
+
const cwdStats = statSync(cwd);
|
|
54
|
+
if (!cwdStats.isDirectory()) return { ok: false, error: `cwd is not a directory: ${cwd}` };
|
|
55
|
+
return { ok: true };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const fsError = error as NodeJS.ErrnoException;
|
|
58
|
+
if (fsError.code === "ENOENT") return { ok: false, error: `cwd does not exist: ${cwd}` };
|
|
59
|
+
if (fsError.code === "ENOTDIR") return { ok: false, error: `cwd path contains a non-directory component: ${cwd}` };
|
|
60
|
+
const details = fsError.message ? ` (${fsError.message})` : "";
|
|
61
|
+
return { ok: false, error: `cwd is not accessible: ${cwd}${details}` };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatPiSpawnError(error: Error, spawnSpec: PiSpawnCommand, cwd: string): string {
|
|
66
|
+
const spawnError = error as NodeJS.ErrnoException;
|
|
67
|
+
const message = error.message || String(error);
|
|
68
|
+
if (spawnError.code === "ENOENT") {
|
|
69
|
+
return `failed to spawn subagent runtime '${spawnSpec.command}' from cwd '${cwd}': runtime executable was not found or could not be launched (${message})`;
|
|
70
|
+
}
|
|
71
|
+
return `failed to spawn subagent runtime '${spawnSpec.command}' from cwd '${cwd}': ${message}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PiSpawnCommand {
|
|
48
75
|
command: string;
|
|
49
76
|
args: string[];
|
|
50
77
|
}
|
|
@@ -137,6 +137,28 @@ function getWidgetOwnerKey(ctx: ExtensionContext): string {
|
|
|
137
137
|
return `${sessionOwner}|cwd:${cwdOwner}`;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function widgetStatusText(jobs: AsyncJobState[]): string | undefined {
|
|
141
|
+
if (jobs.length === 0) return undefined;
|
|
142
|
+
const counts = new Map<string, number>();
|
|
143
|
+
for (const job of jobs) counts.set(job.status, (counts.get(job.status) ?? 0) + 1);
|
|
144
|
+
const ordered = ["running", "queued", "complete", "failed", "paused"];
|
|
145
|
+
const parts = ordered
|
|
146
|
+
.map((status) => {
|
|
147
|
+
const count = counts.get(status) ?? 0;
|
|
148
|
+
return count > 0 ? `${count} ${status}` : undefined;
|
|
149
|
+
})
|
|
150
|
+
.filter((part): part is string => part !== undefined);
|
|
151
|
+
return `Async agents: ${parts.join(", ")}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function setWidgetStatus(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
155
|
+
try {
|
|
156
|
+
ctx.ui.setStatus?.(WIDGET_KEY, widgetStatusText(jobs));
|
|
157
|
+
} catch {
|
|
158
|
+
// Status mirroring must never prevent the primary widget render path.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
140
162
|
function requestWidgetRender(ctx: ExtensionContext): void {
|
|
141
163
|
(ctx as RenderRequestingContext).ui.requestRender?.();
|
|
142
164
|
}
|
|
@@ -150,6 +172,11 @@ function unmountWidgetBestEffort(ctx: ExtensionContext | undefined): void {
|
|
|
150
172
|
// reload/session rebinding, but local state still needs to move on so the
|
|
151
173
|
// next status update can mount cleanly on the active UI context.
|
|
152
174
|
}
|
|
175
|
+
try {
|
|
176
|
+
ctx.ui.setStatus?.(WIDGET_KEY, undefined);
|
|
177
|
+
} catch {
|
|
178
|
+
// Status mirroring cleanup must never block primary widget teardown.
|
|
179
|
+
}
|
|
153
180
|
}
|
|
154
181
|
|
|
155
182
|
function widgetSnapshotKey(jobs: AsyncJobState[]): string {
|
|
@@ -253,6 +280,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
253
280
|
// host session disposal owns cross-owner empty-session teardown.
|
|
254
281
|
return;
|
|
255
282
|
}
|
|
283
|
+
if (ctx.hasUI) setWidgetStatus(ctx, []);
|
|
256
284
|
stopWidgetAnimation();
|
|
257
285
|
return;
|
|
258
286
|
}
|
|
@@ -271,6 +299,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
271
299
|
mountedWidgetOwnerKey = undefined;
|
|
272
300
|
widgetMounted = false;
|
|
273
301
|
}
|
|
302
|
+
setWidgetStatus(ctx, jobs);
|
|
274
303
|
if (!widgetMounted) {
|
|
275
304
|
// belowEditor keeps the live async widget pinned to the bottom viewport,
|
|
276
305
|
// matching the workflow companion widget's placement (#1109). The pulse frame
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.9.5-alpha.
|
|
3
|
+
"version": "0.9.5-alpha.9",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
|
|
6
6
|
"contributors": [
|