@oh-my-pi/pi-coding-agent 14.9.3 → 14.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/settings-schema.ts +1 -1
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/js/context-manager.ts +1 -38
- package/src/eval/js/prelude.txt +0 -2
- package/src/eval/py/executor.ts +24 -8
- package/src/eval/py/index.ts +1 -0
- package/src/eval/py/prelude.py +11 -80
- package/src/export/html/template.css +12 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +20 -2
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/internal-urls/agent-protocol.ts +63 -52
- package/src/internal-urls/artifact-protocol.ts +51 -51
- package/src/internal-urls/docs-index.generated.ts +33 -1
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +49 -7
- package/src/internal-urls/mcp-protocol.ts +2 -8
- package/src/internal-urls/memory-protocol.ts +89 -59
- package/src/internal-urls/router.ts +38 -22
- package/src/internal-urls/rule-protocol.ts +2 -20
- package/src/internal-urls/skill-protocol.ts +4 -27
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/project-prompt.md +10 -2
- package/src/prompts/system/subagent-system-prompt.md +8 -8
- package/src/prompts/system/system-prompt.md +13 -7
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -10
- package/src/prompts/tools/eval.md +1 -3
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +58 -20
- package/src/session/artifacts.ts +7 -4
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +14 -2
- package/src/task/index.ts +19 -5
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +15 -9
- package/src/tools/browser/tab-supervisor.ts +12 -2
- package/src/tools/eval.ts +48 -10
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/read.ts +5 -4
- package/src/tools/search.ts +3 -2
- package/src/tools/todo-write.ts +1 -1
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/internal-urls/jobs-protocol.ts +0 -120
- package/src/prompts/system/now-prompt.md +0 -7
package/src/sdk.ts
CHANGED
|
@@ -27,7 +27,7 @@ import chalk from "chalk";
|
|
|
27
27
|
import { AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
|
|
28
28
|
import { createAutoresearchExtension } from "./autoresearch";
|
|
29
29
|
import { loadCapability } from "./capability";
|
|
30
|
-
import { type Rule, ruleCapability } from "./capability/rule";
|
|
30
|
+
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
31
31
|
import { ModelRegistry } from "./config/model-registry";
|
|
32
32
|
import { formatModelString, parseModelPattern, parseModelString, resolveModelRoleValue } from "./config/model-resolver";
|
|
33
33
|
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
|
|
@@ -59,30 +59,22 @@ import {
|
|
|
59
59
|
type ToolDefinition,
|
|
60
60
|
wrapRegisteredTools,
|
|
61
61
|
} from "./extensibility/extensions";
|
|
62
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
loadSkills as loadSkillsInternal,
|
|
64
|
+
type Skill,
|
|
65
|
+
type SkillWarning,
|
|
66
|
+
setActiveSkills,
|
|
67
|
+
} from "./extensibility/skills";
|
|
63
68
|
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands";
|
|
64
69
|
import type { HindsightSessionState } from "./hindsight/state";
|
|
65
|
-
import {
|
|
66
|
-
AgentProtocolHandler,
|
|
67
|
-
ArtifactProtocolHandler,
|
|
68
|
-
InternalUrlRouter,
|
|
69
|
-
JobsProtocolHandler,
|
|
70
|
-
LocalProtocolHandler,
|
|
71
|
-
type LocalProtocolOptions,
|
|
72
|
-
McpProtocolHandler,
|
|
73
|
-
MemoryProtocolHandler,
|
|
74
|
-
PiProtocolHandler,
|
|
75
|
-
RuleProtocolHandler,
|
|
76
|
-
SkillProtocolHandler,
|
|
77
|
-
} from "./internal-urls";
|
|
70
|
+
import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
|
|
78
71
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
|
|
79
|
-
import { discoverAndLoadMCPTools,
|
|
72
|
+
import { discoverAndLoadMCPTools, MCPManager, type MCPToolsLoadResult } from "./mcp";
|
|
80
73
|
import {
|
|
81
74
|
collectDiscoverableMCPTools,
|
|
82
75
|
formatDiscoverableMCPToolServerSummary,
|
|
83
76
|
selectDiscoverableMCPToolNamesByServer,
|
|
84
77
|
} from "./mcp/discoverable-tool-metadata";
|
|
85
|
-
import { getMemoryRoot } from "./memories";
|
|
86
78
|
import { resolveMemoryBackend } from "./memory-backend";
|
|
87
79
|
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
|
|
88
80
|
import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
|
|
@@ -917,6 +909,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
917
909
|
let agent: Agent;
|
|
918
910
|
let session!: AgentSession;
|
|
919
911
|
let hasSession = false;
|
|
912
|
+
let hasRegistered = false;
|
|
920
913
|
const enableLsp = options.enableLsp ?? true;
|
|
921
914
|
const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
|
|
922
915
|
const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
|
|
@@ -942,34 +935,39 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
942
935
|
|
|
943
936
|
return preview;
|
|
944
937
|
};
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
938
|
+
// Only top-level sessions own an AsyncJobManager. Subagents reach the
|
|
939
|
+
// parent's manager via `AsyncJobManager.instance()` (set below), so creating
|
|
940
|
+
// a second instance here just to leave it orphaned wastes a constructor and
|
|
941
|
+
// risks accidental disposal of the parent's manager on subagent teardown.
|
|
942
|
+
const asyncJobManager =
|
|
943
|
+
backgroundJobsEnabled && !options.parentTaskPrefix
|
|
944
|
+
? new AsyncJobManager({
|
|
945
|
+
maxRunningJobs: asyncMaxJobs,
|
|
946
|
+
onJobComplete: async (jobId, result, job) => {
|
|
947
|
+
if (!session || asyncJobManager!.isDeliverySuppressed(jobId)) return;
|
|
948
|
+
const formattedResult = await formatAsyncResultForFollowUp(result);
|
|
949
|
+
if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
|
|
950
|
+
|
|
951
|
+
const message = prompt.render(asyncResultTemplate, { jobId, result: formattedResult });
|
|
952
|
+
const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
|
|
953
|
+
await session.sendCustomMessage(
|
|
954
|
+
{
|
|
955
|
+
customType: "async-result",
|
|
956
|
+
content: message,
|
|
957
|
+
display: true,
|
|
958
|
+
attribution: "agent",
|
|
959
|
+
details: {
|
|
960
|
+
jobId,
|
|
961
|
+
type: job?.type,
|
|
962
|
+
label: job?.label,
|
|
963
|
+
durationMs,
|
|
964
|
+
},
|
|
966
965
|
},
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
: undefined;
|
|
966
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
967
|
+
);
|
|
968
|
+
},
|
|
969
|
+
})
|
|
970
|
+
: undefined;
|
|
973
971
|
|
|
974
972
|
const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
|
|
975
973
|
const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
|
|
@@ -1055,44 +1053,27 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1055
1053
|
return {};
|
|
1056
1054
|
}
|
|
1057
1055
|
},
|
|
1056
|
+
getArtifactManager: () => sessionManager.getArtifactManager(),
|
|
1058
1057
|
settings,
|
|
1059
1058
|
authStorage,
|
|
1060
1059
|
modelRegistry,
|
|
1061
|
-
asyncJobManager,
|
|
1062
1060
|
};
|
|
1063
1061
|
|
|
1064
|
-
//
|
|
1065
|
-
|
|
1062
|
+
// Wire process-wide internal URL singletons owned by their real classes.
|
|
1063
|
+
// Top-level sessions install the active snapshots; subagents inherit them.
|
|
1064
|
+
// Artifact and agent-output URLs resolve via `AgentRegistry.global()` —
|
|
1065
|
+
// the protocol handlers walk each ref's `sessionManager.getArtifactsDir()`,
|
|
1066
|
+
// which collapses to the parent's dir for subagents (they adopt the
|
|
1067
|
+
// parent's ArtifactManager) so one lookup hits everything.
|
|
1066
1068
|
const getArtifactsDir = () => sessionManager.getArtifactsDir();
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
new LocalProtocolHandler(
|
|
1076
|
-
options.localProtocolOptions ?? {
|
|
1077
|
-
getArtifactsDir,
|
|
1078
|
-
getSessionId: () => sessionManager.getSessionId(),
|
|
1079
|
-
},
|
|
1080
|
-
),
|
|
1081
|
-
);
|
|
1082
|
-
internalRouter.register(
|
|
1083
|
-
new SkillProtocolHandler({
|
|
1084
|
-
getSkills: () => skills,
|
|
1085
|
-
}),
|
|
1086
|
-
);
|
|
1087
|
-
internalRouter.register(
|
|
1088
|
-
new RuleProtocolHandler({
|
|
1089
|
-
getRules: () => [...rulebookRules, ...alwaysApplyRules],
|
|
1090
|
-
}),
|
|
1091
|
-
);
|
|
1092
|
-
internalRouter.register(new PiProtocolHandler());
|
|
1093
|
-
internalRouter.register(new JobsProtocolHandler({ getAsyncJobManager: () => asyncJobManager }));
|
|
1094
|
-
internalRouter.register(new McpProtocolHandler({ getMcpManager: () => mcpManager }));
|
|
1095
|
-
toolSession.internalRouter = internalRouter;
|
|
1069
|
+
if (!options.parentTaskPrefix) {
|
|
1070
|
+
setActiveSkills(skills);
|
|
1071
|
+
setActiveRules([...rulebookRules, ...alwaysApplyRules]);
|
|
1072
|
+
if (asyncJobManager) AsyncJobManager.setInstance(asyncJobManager);
|
|
1073
|
+
}
|
|
1074
|
+
if (options.localProtocolOptions) {
|
|
1075
|
+
LocalProtocolHandler.setOverride(options.localProtocolOptions);
|
|
1076
|
+
}
|
|
1096
1077
|
toolSession.getArtifactsDir = getArtifactsDir;
|
|
1097
1078
|
toolSession.agentOutputManager = new AgentOutputManager(
|
|
1098
1079
|
getArtifactsDir,
|
|
@@ -1141,7 +1122,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1141
1122
|
customTools.push(...mcpResult.tools.map(loaded => loaded.tool));
|
|
1142
1123
|
}
|
|
1143
1124
|
}
|
|
1144
|
-
|
|
1125
|
+
// Only top-level sessions own the global MCPManager. Subagents already
|
|
1126
|
+
// receive the parent's manager via `options.mcpManager`, and reassigning
|
|
1127
|
+
// the singleton to the same value is a no-op \u2014 keep the gate explicit
|
|
1128
|
+
// to mirror the AsyncJobManager ownership rule.
|
|
1129
|
+
if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
|
|
1145
1130
|
|
|
1146
1131
|
// Add image tools when the active model or configured image providers can generate images.
|
|
1147
1132
|
const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
|
|
@@ -1552,6 +1537,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1552
1537
|
});
|
|
1553
1538
|
}
|
|
1554
1539
|
|
|
1540
|
+
// Pre-register in the global agent registry BEFORE building the system prompt,
|
|
1541
|
+
// so that subagents launched in the same parallel batch can see each other in
|
|
1542
|
+
// their initial `# IRC Peers` block (rendered inside `rebuildSystemPrompt`).
|
|
1543
|
+
// The session reference is attached after construction below.
|
|
1544
|
+
agentRegistry.register({
|
|
1545
|
+
id: resolvedAgentId,
|
|
1546
|
+
displayName: resolvedAgentDisplayName,
|
|
1547
|
+
kind: (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main",
|
|
1548
|
+
parentId: options.parentTaskPrefix,
|
|
1549
|
+
session: null,
|
|
1550
|
+
sessionFile: sessionManager.getSessionFile() ?? null,
|
|
1551
|
+
status: "running",
|
|
1552
|
+
});
|
|
1553
|
+
hasRegistered = true;
|
|
1554
|
+
|
|
1555
1555
|
const { systemPrompt } = await logger.time(
|
|
1556
1556
|
"buildSystemPrompt",
|
|
1557
1557
|
rebuildSystemPrompt,
|
|
@@ -1708,6 +1708,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1708
1708
|
sessionManager,
|
|
1709
1709
|
settings,
|
|
1710
1710
|
evalKernelOwnerId,
|
|
1711
|
+
// Defined only for top-level sessions (creation is gated above).
|
|
1712
|
+
// AgentSession uses this to decide whether it may dispose the global
|
|
1713
|
+
// AsyncJobManager on teardown; subagents inherit the parent's and
|
|
1714
|
+
// **MUST NOT** tear it down.
|
|
1715
|
+
ownedAsyncJobManager: asyncJobManager,
|
|
1711
1716
|
scopedModels: options.scopedModels,
|
|
1712
1717
|
promptTemplates,
|
|
1713
1718
|
slashCommands,
|
|
@@ -1744,24 +1749,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1744
1749
|
defaultSelectedMCPServerNames: [...discoveryDefaultServers],
|
|
1745
1750
|
ttsrManager,
|
|
1746
1751
|
obfuscator,
|
|
1747
|
-
asyncJobManager,
|
|
1748
1752
|
agentId: resolvedAgentId,
|
|
1749
1753
|
agentRegistry,
|
|
1750
1754
|
providerSessionId: options.providerSessionId,
|
|
1751
1755
|
});
|
|
1752
1756
|
hasSession = true;
|
|
1753
1757
|
|
|
1754
|
-
//
|
|
1755
|
-
//
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
displayName: resolvedAgentDisplayName,
|
|
1759
|
-
kind: (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main",
|
|
1760
|
-
parentId: options.parentTaskPrefix,
|
|
1761
|
-
session,
|
|
1762
|
-
sessionFile: sessionManager.getSessionFile() ?? null,
|
|
1763
|
-
status: "running",
|
|
1764
|
-
});
|
|
1758
|
+
// Attach the live session to the pre-registered ref so peers can route IRC
|
|
1759
|
+
// messages here. Refresh sessionFile in case it was unavailable at pre-register
|
|
1760
|
+
// time. The dispose wrapper below unregisters on teardown.
|
|
1761
|
+
agentRegistry.attachSession(resolvedAgentId, session, sessionManager.getSessionFile() ?? null);
|
|
1765
1762
|
{
|
|
1766
1763
|
const originalDispose = session.dispose.bind(session);
|
|
1767
1764
|
session.dispose = async () => {
|
|
@@ -1908,6 +1905,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1908
1905
|
if (hasSession) {
|
|
1909
1906
|
await session.dispose();
|
|
1910
1907
|
} else {
|
|
1908
|
+
if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
|
|
1911
1909
|
await disposeKernelSessionsByOwner(evalKernelOwnerId);
|
|
1912
1910
|
}
|
|
1913
1911
|
} catch (cleanupError) {
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
} from "@oh-my-pi/pi-ai";
|
|
56
56
|
import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
57
57
|
import { abortableSleep, getAgentDbPath, isEnoent, logger, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
58
|
-
import type
|
|
58
|
+
import { type AsyncJob, AsyncJobManager } from "../async";
|
|
59
59
|
import type { Rule } from "../capability/rule";
|
|
60
60
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
61
61
|
import {
|
|
@@ -225,8 +225,6 @@ export interface AgentSessionConfig {
|
|
|
225
225
|
agent: Agent;
|
|
226
226
|
sessionManager: SessionManager;
|
|
227
227
|
settings: Settings;
|
|
228
|
-
/** Async background jobs launched by tools */
|
|
229
|
-
asyncJobManager?: AsyncJobManager;
|
|
230
228
|
/** Models to cycle through with Ctrl+P (from --models flag) */
|
|
231
229
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
232
230
|
/** Initial session thinking selector. */
|
|
@@ -285,6 +283,12 @@ export interface AgentSessionConfig {
|
|
|
285
283
|
obfuscator?: SecretObfuscator;
|
|
286
284
|
/** Logical owner for retained Python kernels created by this session. */
|
|
287
285
|
evalKernelOwnerId?: string;
|
|
286
|
+
/**
|
|
287
|
+
* AsyncJobManager that this session installed as the process-global instance.
|
|
288
|
+
* Only set for top-level sessions; subagents inherit the parent's manager and
|
|
289
|
+
* **MUST NOT** dispose it on their own teardown.
|
|
290
|
+
*/
|
|
291
|
+
ownedAsyncJobManager?: AsyncJobManager;
|
|
288
292
|
/** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
|
|
289
293
|
agentId?: string;
|
|
290
294
|
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
@@ -507,7 +511,6 @@ export class AgentSession {
|
|
|
507
511
|
|
|
508
512
|
readonly configWarnings: string[] = [];
|
|
509
513
|
|
|
510
|
-
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
|
511
514
|
#scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
512
515
|
#thinkingLevel: ThinkingLevel | undefined;
|
|
513
516
|
#promptTemplates: PromptTemplate[];
|
|
@@ -558,6 +561,11 @@ export class AgentSession {
|
|
|
558
561
|
// Python execution state
|
|
559
562
|
#evalAbortControllers = new Set<AbortController>();
|
|
560
563
|
#evalKernelOwnerId: string;
|
|
564
|
+
/**
|
|
565
|
+
* AsyncJobManager owned by this session (top-level only). Subagents leave
|
|
566
|
+
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
567
|
+
*/
|
|
568
|
+
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
561
569
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
562
570
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
563
571
|
#evalExecutionDisposing = false;
|
|
@@ -704,8 +712,8 @@ export class AgentSession {
|
|
|
704
712
|
this.sessionManager = config.sessionManager;
|
|
705
713
|
this.settings = config.settings;
|
|
706
714
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
707
|
-
this.#asyncJobManager = config.asyncJobManager;
|
|
708
715
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
716
|
+
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
709
717
|
this.#scopedModels = config.scopedModels ?? [];
|
|
710
718
|
this.#thinkingLevel = config.thinkingLevel;
|
|
711
719
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
@@ -848,15 +856,16 @@ export class AgentSession {
|
|
|
848
856
|
}
|
|
849
857
|
|
|
850
858
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
851
|
-
|
|
852
|
-
|
|
859
|
+
const manager = AsyncJobManager.instance();
|
|
860
|
+
if (!manager) return null;
|
|
861
|
+
const running = manager.getRunningJobs().map(job => ({
|
|
853
862
|
id: job.id,
|
|
854
863
|
type: job.type,
|
|
855
864
|
status: job.status,
|
|
856
865
|
label: job.label,
|
|
857
866
|
startTime: job.startTime,
|
|
858
867
|
}));
|
|
859
|
-
const recent =
|
|
868
|
+
const recent = manager.getRecentJobs(options?.recentLimit ?? 5).map(job => ({
|
|
860
869
|
id: job.id,
|
|
861
870
|
type: job.type,
|
|
862
871
|
status: job.status,
|
|
@@ -866,6 +875,17 @@ export class AgentSession {
|
|
|
866
875
|
return { running, recent };
|
|
867
876
|
}
|
|
868
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Cancel async jobs registered by *this* agent only. Used by lifecycle
|
|
880
|
+
* transitions (newSession, switchSession, handoff, dispose) so a subagent
|
|
881
|
+
* cleans up its own background work without touching its parent's jobs.
|
|
882
|
+
* No-op when no manager is installed or this session has no agent id.
|
|
883
|
+
*/
|
|
884
|
+
#cancelOwnAsyncJobs(): void {
|
|
885
|
+
if (!this.#agentId) return;
|
|
886
|
+
AsyncJobManager.instance()?.cancelAll({ ownerId: this.#agentId });
|
|
887
|
+
}
|
|
888
|
+
|
|
869
889
|
// =========================================================================
|
|
870
890
|
// Event Subscription
|
|
871
891
|
// =========================================================================
|
|
@@ -1739,7 +1759,6 @@ export class AgentSession {
|
|
|
1739
1759
|
}
|
|
1740
1760
|
|
|
1741
1761
|
#preCacheStreamingEditFile(event: AgentEvent): void {
|
|
1742
|
-
if (!this.settings.get("edit.streamingAbort")) return;
|
|
1743
1762
|
if (this.#streamingEditAbortTriggered) return;
|
|
1744
1763
|
if (event.type !== "message_update") return;
|
|
1745
1764
|
|
|
@@ -1755,6 +1774,9 @@ export class AgentSession {
|
|
|
1755
1774
|
const streamingEdit = this.#getStreamingEditToolCall(event);
|
|
1756
1775
|
if (!streamingEdit) return;
|
|
1757
1776
|
|
|
1777
|
+
// The auto-generated guard runs unconditionally: editing a generated file
|
|
1778
|
+
// is never the user's intent, and the cost of a false-positive abort is one
|
|
1779
|
+
// wasted turn vs. silently corrupting a regenerated source.
|
|
1758
1780
|
const shouldCheckAutoGenerated =
|
|
1759
1781
|
!streamingEdit.toolCall.id || !this.#streamingEditPrecheckedToolCallIds.has(streamingEdit.toolCall.id);
|
|
1760
1782
|
if (shouldCheckAutoGenerated) {
|
|
@@ -1768,7 +1790,12 @@ export class AgentSession {
|
|
|
1768
1790
|
);
|
|
1769
1791
|
}
|
|
1770
1792
|
|
|
1771
|
-
|
|
1793
|
+
// File-cache priming feeds #maybeAbortStreamingEdit's removed-lines check,
|
|
1794
|
+
// which is the optional patch-preview verification gated by
|
|
1795
|
+
// edit.streamingAbort. Skip the read when the setting is off.
|
|
1796
|
+
if (this.settings.get("edit.streamingAbort")) {
|
|
1797
|
+
this.#ensureFileCache(streamingEdit.resolvedPath);
|
|
1798
|
+
}
|
|
1772
1799
|
}
|
|
1773
1800
|
|
|
1774
1801
|
#ensureFileCache(resolvedPath: string): void {
|
|
@@ -2149,10 +2176,21 @@ export class AgentSession {
|
|
|
2149
2176
|
}
|
|
2150
2177
|
await this.#cancelPostPromptTasks();
|
|
2151
2178
|
this.#clearTodoClearTimers();
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2179
|
+
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
2180
|
+
// leak its background bash/task work into the parent's manager. Only
|
|
2181
|
+
// the session that owns the manager goes on to dispose it (which itself
|
|
2182
|
+
// nukes any leftover jobs and pending deliveries).
|
|
2183
|
+
this.#cancelOwnAsyncJobs();
|
|
2184
|
+
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
2185
|
+
if (ownedAsyncManager) {
|
|
2186
|
+
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
2187
|
+
const deliveryState = ownedAsyncManager.getDeliveryState();
|
|
2188
|
+
if (drained === false && deliveryState) {
|
|
2189
|
+
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
2190
|
+
}
|
|
2191
|
+
if (AsyncJobManager.instance() === ownedAsyncManager) {
|
|
2192
|
+
AsyncJobManager.setInstance(undefined);
|
|
2193
|
+
}
|
|
2156
2194
|
}
|
|
2157
2195
|
const pythonExecutionsSettled = await this.#prepareEvalExecutionsForDispose();
|
|
2158
2196
|
if (!pythonExecutionsSettled) {
|
|
@@ -3948,7 +3986,7 @@ export class AgentSession {
|
|
|
3948
3986
|
|
|
3949
3987
|
this.#disconnectFromAgent();
|
|
3950
3988
|
await this.abort();
|
|
3951
|
-
this.#
|
|
3989
|
+
this.#cancelOwnAsyncJobs();
|
|
3952
3990
|
this.#closeAllProviderSessions("new session");
|
|
3953
3991
|
this.agent.reset();
|
|
3954
3992
|
if (options?.drop && previousSessionFile) {
|
|
@@ -4756,7 +4794,7 @@ export class AgentSession {
|
|
|
4756
4794
|
// Start a new session
|
|
4757
4795
|
const previousSessionFile = this.sessionFile;
|
|
4758
4796
|
await this.sessionManager.flush();
|
|
4759
|
-
this.#
|
|
4797
|
+
this.#cancelOwnAsyncJobs();
|
|
4760
4798
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
4761
4799
|
this.agent.reset();
|
|
4762
4800
|
this.#syncAgentSessionId();
|
|
@@ -6675,7 +6713,7 @@ export class AgentSession {
|
|
|
6675
6713
|
const incomingRecord: CustomMessage = {
|
|
6676
6714
|
role: "custom",
|
|
6677
6715
|
customType: "irc:incoming",
|
|
6678
|
-
content: `[IRC \`${args.from}\`
|
|
6716
|
+
content: `[IRC \`${args.from}\` → you]\n\n${args.message}`,
|
|
6679
6717
|
display: true,
|
|
6680
6718
|
details: { from: args.from, message: args.message },
|
|
6681
6719
|
attribution: "agent",
|
|
@@ -6707,7 +6745,7 @@ export class AgentSession {
|
|
|
6707
6745
|
const replyRecord: CustomMessage = {
|
|
6708
6746
|
role: "custom",
|
|
6709
6747
|
customType: "irc:autoreply",
|
|
6710
|
-
content: `[IRC you
|
|
6748
|
+
content: `[IRC you → \`${args.from}\` (auto)]\n\n${replyText}`,
|
|
6711
6749
|
display: true,
|
|
6712
6750
|
details: { to: args.from, reply: replyText },
|
|
6713
6751
|
attribution: "agent",
|
|
@@ -6746,7 +6784,7 @@ export class AgentSession {
|
|
|
6746
6784
|
const mainRef = registry.get(MAIN_AGENT_ID);
|
|
6747
6785
|
const mainSession = mainRef?.session;
|
|
6748
6786
|
if (!mainSession || mainSession === this) return;
|
|
6749
|
-
const arrow = args.kind === "reply" ? "
|
|
6787
|
+
const arrow = args.kind === "reply" ? "→ (auto)" : "→";
|
|
6750
6788
|
const relayRecord: CustomMessage = {
|
|
6751
6789
|
role: "custom",
|
|
6752
6790
|
customType: "irc:relay",
|
|
@@ -7149,7 +7187,7 @@ export class AgentSession {
|
|
|
7149
7187
|
|
|
7150
7188
|
// Flush pending writes before branching
|
|
7151
7189
|
await this.sessionManager.flush();
|
|
7152
|
-
this.#
|
|
7190
|
+
this.#cancelOwnAsyncJobs();
|
|
7153
7191
|
|
|
7154
7192
|
if (!selectedEntry.parentId) {
|
|
7155
7193
|
await this.sessionManager.newSession({ parentSession: previousSessionFile });
|
package/src/session/artifacts.ts
CHANGED
|
@@ -12,6 +12,10 @@ import * as path from "node:path";
|
|
|
12
12
|
*
|
|
13
13
|
* Artifacts are stored with sequential IDs in the session's artifact directory.
|
|
14
14
|
* The directory is created lazily on first write.
|
|
15
|
+
*
|
|
16
|
+
* Subagents do not own their own `ArtifactManager`. The parent's instance is
|
|
17
|
+
* adopted via `SessionManager.adoptArtifactManager`, so the whole parent +
|
|
18
|
+
* subagent tree shares one ID space and one directory.
|
|
15
19
|
*/
|
|
16
20
|
export class ArtifactManager {
|
|
17
21
|
#nextId = 0;
|
|
@@ -20,11 +24,10 @@ export class ArtifactManager {
|
|
|
20
24
|
#initialized = false;
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
|
-
* @param
|
|
27
|
+
* @param dir Directory that will hold artifact files. Created lazily on first save.
|
|
24
28
|
*/
|
|
25
|
-
constructor(
|
|
26
|
-
|
|
27
|
-
this.#dir = sessionFile.slice(0, -6);
|
|
29
|
+
constructor(dir: string) {
|
|
30
|
+
this.#dir = dir;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -275,6 +275,7 @@ export type ReadonlySessionManager = Pick<
|
|
|
275
275
|
| "getSessionFile"
|
|
276
276
|
| "getSessionName"
|
|
277
277
|
| "getArtifactsDir"
|
|
278
|
+
| "getArtifactManager"
|
|
278
279
|
| "allocateArtifactPath"
|
|
279
280
|
| "saveArtifact"
|
|
280
281
|
| "getArtifactPath"
|
|
@@ -1622,6 +1623,10 @@ export class SessionManager {
|
|
|
1622
1623
|
#persistErrorReported = false;
|
|
1623
1624
|
#artifactManager: ArtifactManager | null = null;
|
|
1624
1625
|
#artifactManagerSessionFile: string | null = null;
|
|
1626
|
+
// When set, take precedence over the lazily-derived per-session manager.
|
|
1627
|
+
// Subagents adopt the parent's manager so artifact IDs are unique across the
|
|
1628
|
+
// whole agent tree and all files land in the parent's artifacts dir.
|
|
1629
|
+
#adoptedArtifactManager: ArtifactManager | null = null;
|
|
1625
1630
|
// In-memory artifact fallback for non-persistent sessions (persist=false).
|
|
1626
1631
|
// Keyed by sequential numeric ID string; mirrors the file-based ArtifactManager ID scheme.
|
|
1627
1632
|
#inMemoryArtifacts: Map<string, string> | null = null;
|
|
@@ -1675,6 +1680,7 @@ export class SessionManager {
|
|
|
1675
1680
|
this.#persistErrorReported = false;
|
|
1676
1681
|
this.#artifactManager = null;
|
|
1677
1682
|
this.#artifactManagerSessionFile = null;
|
|
1683
|
+
this.#adoptedArtifactManager = null;
|
|
1678
1684
|
this.#buildIndex();
|
|
1679
1685
|
if (this.#sessionFile) {
|
|
1680
1686
|
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
@@ -2120,17 +2126,40 @@ export class SessionManager {
|
|
|
2120
2126
|
/**
|
|
2121
2127
|
* Returns the session artifacts directory path (session file path without .jsonl).
|
|
2122
2128
|
* Returns null when the session is not persisted to a file.
|
|
2129
|
+
* When this session has adopted an external ArtifactManager (subagent case),
|
|
2130
|
+
* returns that manager's directory so reads/writes land in the shared parent
|
|
2131
|
+
* dir instead of a private (non-existent) subdir.
|
|
2123
2132
|
*/
|
|
2124
2133
|
getArtifactsDir(): string | null {
|
|
2134
|
+
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager.dir;
|
|
2125
2135
|
const sessionFile = this.#sessionFile;
|
|
2126
2136
|
return sessionFile ? sessionFile.slice(0, -6) : null;
|
|
2127
2137
|
}
|
|
2128
2138
|
|
|
2139
|
+
/**
|
|
2140
|
+
* Adopt an externally-owned ArtifactManager. Used by subagents to share
|
|
2141
|
+
* the parent session's artifact directory and ID counter.
|
|
2142
|
+
*/
|
|
2143
|
+
adoptArtifactManager(manager: ArtifactManager): void {
|
|
2144
|
+
this.#adoptedArtifactManager = manager;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* Returns the ArtifactManager this session writes through. Lazily creates
|
|
2149
|
+
* one bound to the current session file unless an external manager was
|
|
2150
|
+
* adopted via `adoptArtifactManager`. Returns null only for non-persistent
|
|
2151
|
+
* sessions with no adopted manager.
|
|
2152
|
+
*/
|
|
2153
|
+
getArtifactManager(): ArtifactManager | null {
|
|
2154
|
+
return this.#getOrCreateArtifactManager();
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2129
2157
|
/**
|
|
2130
2158
|
* Returns an artifact manager bound to the current session file.
|
|
2131
2159
|
* Recreates the manager when the active session file changes.
|
|
2132
2160
|
*/
|
|
2133
2161
|
#getOrCreateArtifactManager(): ArtifactManager | null {
|
|
2162
|
+
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager;
|
|
2134
2163
|
const sessionFile = this.#sessionFile;
|
|
2135
2164
|
if (!sessionFile) {
|
|
2136
2165
|
this.#artifactManager = null;
|
|
@@ -2142,7 +2171,7 @@ export class SessionManager {
|
|
|
2142
2171
|
return this.#artifactManager;
|
|
2143
2172
|
}
|
|
2144
2173
|
|
|
2145
|
-
const manager = new ArtifactManager(sessionFile);
|
|
2174
|
+
const manager = new ArtifactManager(sessionFile.slice(0, -6));
|
|
2146
2175
|
this.#artifactManager = manager;
|
|
2147
2176
|
this.#artifactManagerSessionFile = sessionFile;
|
|
2148
2177
|
return manager;
|
|
@@ -15,6 +15,11 @@ export interface SSHConnectionTarget {
|
|
|
15
15
|
|
|
16
16
|
export type SSHHostOs = "windows" | "linux" | "macos" | "unknown";
|
|
17
17
|
export type SSHHostShell = "cmd" | "powershell" | "bash" | "zsh" | "sh" | "unknown";
|
|
18
|
+
export type SshPlatform = typeof process.platform;
|
|
19
|
+
|
|
20
|
+
export function supportsSshControlMaster(platform: SshPlatform = process.platform): boolean {
|
|
21
|
+
return platform !== "win32";
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
export interface SSHHostInfo {
|
|
20
25
|
version: number;
|
|
@@ -33,6 +38,10 @@ const activeHosts = new Map<string, SSHConnectionTarget>();
|
|
|
33
38
|
const pendingConnections = new Map<string, Promise<void>>();
|
|
34
39
|
const hostInfoCache = new Map<string, SSHHostInfo>();
|
|
35
40
|
|
|
41
|
+
interface SSHArgsOptions {
|
|
42
|
+
platform?: SshPlatform;
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function ensureControlDir() {
|
|
37
46
|
fs.mkdirSync(CONTROL_DIR, { recursive: true, mode: 0o700 });
|
|
38
47
|
try {
|
|
@@ -66,20 +75,14 @@ async function validateKeyPermissions(keyPath?: string): Promise<void> {
|
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
function buildCommonArgs(host: SSHConnectionTarget): string[] {
|
|
70
|
-
const args = [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"ControlMaster=auto",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"ControlPersist=3600",
|
|
78
|
-
"-o",
|
|
79
|
-
"BatchMode=yes",
|
|
80
|
-
"-o",
|
|
81
|
-
"StrictHostKeyChecking=accept-new",
|
|
82
|
-
];
|
|
78
|
+
function buildCommonArgs(host: SSHConnectionTarget, options?: SSHArgsOptions): string[] {
|
|
79
|
+
const args = ["-n"];
|
|
80
|
+
|
|
81
|
+
if (supportsSshControlMaster(options?.platform)) {
|
|
82
|
+
args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
args.push("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new");
|
|
83
86
|
|
|
84
87
|
if (host.port) {
|
|
85
88
|
args.push("-p", String(host.port));
|
|
@@ -357,9 +360,13 @@ export async function ensureHostInfo(host: SSHConnectionTarget): Promise<SSHHost
|
|
|
357
360
|
return probeHostInfo(host);
|
|
358
361
|
}
|
|
359
362
|
|
|
360
|
-
export async function buildRemoteCommand(
|
|
363
|
+
export async function buildRemoteCommand(
|
|
364
|
+
host: SSHConnectionTarget,
|
|
365
|
+
command: string,
|
|
366
|
+
options?: SSHArgsOptions,
|
|
367
|
+
): Promise<string[]> {
|
|
361
368
|
await validateKeyPermissions(host.keyPath);
|
|
362
|
-
return [...buildCommonArgs(host), buildSshTarget(host.username, host.host), command];
|
|
369
|
+
return [...buildCommonArgs(host, options), buildSshTarget(host.username, host.host), command];
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
let registered = false;
|
|
@@ -385,6 +392,14 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
385
392
|
}
|
|
386
393
|
|
|
387
394
|
const target = buildSshTarget(host.username, host.host);
|
|
395
|
+
if (!supportsSshControlMaster()) {
|
|
396
|
+
activeHosts.set(key, host);
|
|
397
|
+
if (!hostInfoCache.has(key) && !(await loadHostInfoFromDisk(host))) {
|
|
398
|
+
await probeHostInfo(host);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
388
403
|
const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
|
|
389
404
|
if (check.exitCode === 0) {
|
|
390
405
|
activeHosts.set(key, host);
|
|
@@ -415,6 +430,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
415
430
|
}
|
|
416
431
|
|
|
417
432
|
async function closeConnectionInternal(host: SSHConnectionTarget): Promise<void> {
|
|
433
|
+
if (!supportsSshControlMaster()) return;
|
|
418
434
|
const target = buildSshTarget(host.username, host.host);
|
|
419
435
|
await runSshSync(["-O", "exit", ...buildCommonArgs(host), target]);
|
|
420
436
|
}
|