pi-subagents 0.57.0 → 0.58.0
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 +39 -0
- package/docs/agents.md +13 -6
- package/docs/extension-api.md +39 -0
- package/docs/models.md +3 -3
- package/docs/tool-reference.md +3 -2
- package/docs/workflows.md +24 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +23 -0
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +38 -3
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +10 -2
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +26 -1
- package/src/extension/index.ts +15 -4
- package/src/extension/rpc.ts +35 -14
- package/src/extension/schemas.ts +2 -2
- package/src/extension/tool-description.ts +8 -4
- package/src/integrations/herdr-status.ts +5 -0
- package/src/runs/background/async-execution.ts +29 -12
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-status.ts +2 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-watcher.ts +27 -10
- package/src/runs/background/subagent-runner.ts +71 -21
- package/src/runs/foreground/execution.ts +20 -1
- package/src/runs/foreground/subagent-executor.ts +57 -26
- package/src/runs/foreground/workflow-detach-reconcile.ts +128 -17
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +12 -0
- package/src/runs/shared/model-fallback.ts +16 -4
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/formatters.ts +6 -0
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/types.ts +42 -1
- package/src/shared/utils.ts +6 -29
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/render.ts +14 -6
- package/src/workflows/scripted-workflow.ts +52 -5
- package/src/workflows/workflow-receipt.ts +39 -4
|
@@ -5,8 +5,9 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
|
5
5
|
|
|
6
6
|
const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
|
|
7
7
|
const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
8
|
+
const EXTERNAL_CLI_RUNNER_GUIDANCE = "External CLI agents (codex-exec, codex-exec-writer, claude-code, claude-code-writer, cursor-agent, cursor-agent-writer) use their own runner contract and do not support native Pi child options such as model override, structured output, acceptance/agent contract, tool budget, fast mode, fork context, skills, or native Pi tools unless the runner explicitly implements them.";
|
|
8
9
|
|
|
9
|
-
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, or workflowScriptPath to load a script from the request cwd. The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
10
|
+
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, or workflowScriptPath to load a script from the request cwd. The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
10
11
|
|
|
11
12
|
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
12
13
|
|
|
@@ -16,7 +17,8 @@ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
|
16
17
|
"workflowScript means exactly one top-level subagent tool call with async:true. Inside it, use runs.run/runs.all to launch children; do not make another top-level subagent call for those children.",
|
|
17
18
|
"For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all.",
|
|
18
19
|
"Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
|
|
19
|
-
"To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. openai-codex/gpt-5.6-sol); bare ids resolve only when unique in the registry, and agent names (gpt-pro, advisor) are not model ids.",
|
|
20
|
+
"To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. openai-codex/gpt-5.6-sol); bare ids resolve only when unique in the registry, and agent names (gpt-pro, advisor) are not model ids. Set per-run thinking with a suffix on the model string (e.g. openai-codex/gpt-5.6-sol:high; off/minimal/low/medium/high/xhigh/max); the suffix wins over the agent's thinking default. The thinking field only applies to action='watchdog.configure' and is ignored on dispatch.",
|
|
21
|
+
EXTERNAL_CLI_RUNNER_GUIDANCE,
|
|
20
22
|
"Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
|
|
21
23
|
];
|
|
22
24
|
|
|
@@ -32,8 +34,9 @@ export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
|
32
34
|
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for inline orchestration or { workflowScriptPath } to load it from the request cwd. The script inputs are mutually exclusive. Omit action for execution. Use action only for management/control actions.
|
|
33
35
|
|
|
34
36
|
EXECUTION:
|
|
37
|
+
• ${EXTERNAL_CLI_RUNNER_GUIDANCE}
|
|
35
38
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
36
|
-
• When passing an explicit model to a child (on the call or a runs.run/runs.all item), first call { action: "models" } and copy an exact provider/id; bare ids resolve only when unique in the registry, and agent names (e.g. gpt-pro, advisor) are not model ids.
|
|
39
|
+
• When passing an explicit model to a child (on the call or a runs.run/runs.all item), first call { action: "models" } and copy an exact provider/id; bare ids resolve only when unique in the registry, and agent names (e.g. gpt-pro, advisor) are not model ids. Set per-run thinking with a suffix on the model string (e.g. provider/id:high; off/minimal/low/medium/high/xhigh/max); the suffix wins over the agent's thinking default. The thinking field only applies to action='watchdog.configure' and is ignored on dispatch.
|
|
37
40
|
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one direct child. Fields such as model, context, cwd, worktree, output, budgets, acceptance, and async apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
|
|
38
41
|
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
39
42
|
• FILE SCRIPT: { workflowScriptPath:"workflows/review.js" }. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts. Do not combine this field with workflowScript.
|
|
@@ -52,8 +55,9 @@ ${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
|
52
55
|
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for inline orchestration or { workflowScriptPath } to load it from the request cwd. The script inputs are mutually exclusive. Omit action for execution. Use action only for management/control actions.
|
|
53
56
|
|
|
54
57
|
EXECUTE:
|
|
58
|
+
• ${EXTERNAL_CLI_RUNNER_GUIDANCE}
|
|
55
59
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
56
|
-
• Passing an explicit model? Call {action:"models"} first and copy an exact provider/id; bare ids resolve only when unique in the registry; agent names (e.g. gpt-pro, advisor) are not model ids.
|
|
60
|
+
• Passing an explicit model? Call {action:"models"} first and copy an exact provider/id; bare ids resolve only when unique in the registry; agent names (e.g. gpt-pro, advisor) are not model ids. Per-run thinking is a suffix on the model string (provider/id:high; off/minimal/low/medium/high/xhigh/max), and the suffix wins over the agent's thinking default; the thinking field only applies to action='watchdog.configure' and is ignored on dispatch.
|
|
57
61
|
• SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
|
|
58
62
|
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
|
|
59
63
|
• FILE SCRIPT {workflowScriptPath:"workflows/review.js"} loads the script on the host relative to the request cwd before sandbox execution. Do not combine it with workflowScript.
|
|
@@ -59,6 +59,7 @@ export interface HerdrStatusBridge {
|
|
|
59
59
|
* state. Also re-syncs runs that survived a reload/resume.
|
|
60
60
|
*/
|
|
61
61
|
sessionStarted(input: { hasUI: boolean; runs: Iterable<HerdrStatusRun> }): void;
|
|
62
|
+
syncRuns(): void;
|
|
62
63
|
agentStarted(): void;
|
|
63
64
|
flush(): Promise<void>;
|
|
64
65
|
dispose(): void;
|
|
@@ -377,6 +378,10 @@ export function registerHerdrStatusBridge(options: HerdrStatusBridgeOptions): He
|
|
|
377
378
|
rootSession = true;
|
|
378
379
|
replaceRuns(restoredRuns);
|
|
379
380
|
},
|
|
381
|
+
syncRuns() {
|
|
382
|
+
if (!enabled || !rootSession || disposed) return;
|
|
383
|
+
refresh();
|
|
384
|
+
},
|
|
380
385
|
async flush() {
|
|
381
386
|
while (draining || pendingReport) await drainPromise;
|
|
382
387
|
},
|
|
@@ -20,6 +20,7 @@ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveC
|
|
|
20
20
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
21
21
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
22
22
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
23
|
+
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
23
24
|
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
24
25
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
25
26
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
@@ -27,7 +28,7 @@ import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } fr
|
|
|
27
28
|
import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
28
29
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
29
30
|
import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
|
|
30
|
-
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
31
|
+
import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
31
32
|
import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
32
33
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
33
34
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
@@ -212,6 +213,7 @@ interface AsyncSingleParams {
|
|
|
212
213
|
recoveryAgentConfig?: AgentConfig;
|
|
213
214
|
ctx: AsyncExecutionContext;
|
|
214
215
|
cwd?: string;
|
|
216
|
+
requestedCwd?: string;
|
|
215
217
|
maxOutput?: MaxOutputConfig;
|
|
216
218
|
artifactsDir?: string;
|
|
217
219
|
artifactConfig: ArtifactConfig;
|
|
@@ -512,20 +514,14 @@ export function emitProcessTerminalEvent(ctx: AsyncExecutionContext, proof: unkn
|
|
|
512
514
|
}
|
|
513
515
|
}
|
|
514
516
|
|
|
515
|
-
function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void): SpawnRunnerResult {
|
|
517
|
+
function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, requestedCwd = cwd): SpawnRunnerResult {
|
|
518
|
+
const cwdError = preflightLaunchCwd(requestedCwd, cwd);
|
|
519
|
+
if (cwdError) return { error: cwdError };
|
|
520
|
+
|
|
516
521
|
if (!jitiCliPath) {
|
|
517
522
|
return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
|
|
518
523
|
}
|
|
519
524
|
|
|
520
|
-
try {
|
|
521
|
-
const cwdStats = fs.statSync(cwd);
|
|
522
|
-
if (!cwdStats.isDirectory()) {
|
|
523
|
-
return { error: `cwd is not a directory: ${cwd}` };
|
|
524
|
-
}
|
|
525
|
-
} catch {
|
|
526
|
-
return { error: `cwd does not exist: ${cwd}` };
|
|
527
|
-
}
|
|
528
|
-
|
|
529
525
|
fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
|
|
530
526
|
const cfgPath = getAsyncConfigPath(suffix);
|
|
531
527
|
const runnerProcessInstanceId = randomUUID();
|
|
@@ -868,6 +864,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
868
864
|
const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
|
|
869
865
|
const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
|
|
870
866
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
|
|
867
|
+
const contextLimit = model ? findModelInfo(model, availableModels, a.modelProvider ?? ctx.currentModelProvider)?.contextWindow : undefined;
|
|
871
868
|
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
872
869
|
params.thinkingCeiling,
|
|
873
870
|
a.maxThinking,
|
|
@@ -912,6 +909,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
912
909
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
913
910
|
agentName: a.name,
|
|
914
911
|
permissionRules,
|
|
912
|
+
runtimeSnapshotHost: ctx.pi,
|
|
915
913
|
});
|
|
916
914
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
917
915
|
if (externalRunner && permissionRules) {
|
|
@@ -946,11 +944,15 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
946
944
|
outputName: s.as,
|
|
947
945
|
structured: Boolean(s.outputSchema),
|
|
948
946
|
cwd: stepCwd,
|
|
947
|
+
requestedCwd: s.cwd ?? stepCwd,
|
|
949
948
|
model,
|
|
949
|
+
...(contextLimit !== undefined ? { contextLimit } : {}),
|
|
950
950
|
...(fast !== undefined ? { fast } : {}),
|
|
951
951
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
952
952
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
953
953
|
launchResolvedExtensions,
|
|
954
|
+
...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
|
|
955
|
+
...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
|
|
954
956
|
modelCandidates: externalRunner ? undefined : modelCandidates,
|
|
955
957
|
...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
|
|
956
958
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
@@ -958,10 +960,12 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
958
960
|
extensions: a.extensions,
|
|
959
961
|
subagentOnlyExtensions: a.subagentOnlyExtensions,
|
|
960
962
|
mcpDirectTools: a.mcpDirectTools,
|
|
963
|
+
mutationTools: a.mutationTools,
|
|
961
964
|
completionGuard: a.completionGuard,
|
|
962
965
|
systemPrompt,
|
|
963
966
|
systemPromptMode: a.systemPromptMode,
|
|
964
967
|
inheritProjectContext: a.inheritProjectContext,
|
|
968
|
+
inheritGlobalContext: a.inheritGlobalContext,
|
|
965
969
|
inheritSkills: a.inheritSkills,
|
|
966
970
|
skills: resolvedSkills.map((r) => r.name),
|
|
967
971
|
outputPath,
|
|
@@ -1098,6 +1102,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
1098
1102
|
index: params.attachRoot.index,
|
|
1099
1103
|
},
|
|
1100
1104
|
inheritProjectContext: false,
|
|
1105
|
+
inheritGlobalContext: false,
|
|
1101
1106
|
inheritSkills: false,
|
|
1102
1107
|
}, ...builtSteps]
|
|
1103
1108
|
: builtSteps;
|
|
@@ -1582,6 +1587,7 @@ export function executeAsyncSingle(
|
|
|
1582
1587
|
);
|
|
1583
1588
|
const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
|
|
1584
1589
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
|
|
1590
|
+
const contextLimit = model ? findModelInfo(model, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider)?.contextWindow : undefined;
|
|
1585
1591
|
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
1586
1592
|
params.thinkingCeiling,
|
|
1587
1593
|
agentConfig.maxThinking,
|
|
@@ -1649,6 +1655,7 @@ export function executeAsyncSingle(
|
|
|
1649
1655
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
1650
1656
|
agentName: agentConfig.name,
|
|
1651
1657
|
permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
|
|
1658
|
+
runtimeSnapshotHost: ctx.pi,
|
|
1652
1659
|
});
|
|
1653
1660
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
1654
1661
|
if (!externalRunner) {
|
|
@@ -1676,6 +1683,7 @@ export function executeAsyncSingle(
|
|
|
1676
1683
|
systemPrompt: effectiveSystemPrompt,
|
|
1677
1684
|
systemPromptMode: agentConfig.systemPromptMode,
|
|
1678
1685
|
inheritProjectContext: agentConfig.inheritProjectContext,
|
|
1686
|
+
inheritGlobalContext: agentConfig.inheritGlobalContext,
|
|
1679
1687
|
inheritSkills: agentConfig.inheritSkills,
|
|
1680
1688
|
skills: resolvedSkills.map((skill) => skill.name),
|
|
1681
1689
|
tools: toolPlan.effectiveToolAllowlist,
|
|
@@ -1718,9 +1726,11 @@ export function executeAsyncSingle(
|
|
|
1718
1726
|
...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
|
|
1719
1727
|
...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
|
|
1720
1728
|
...(recoveryAgentConfig.mcpDirectTools ? { mcpDirectTools: [...recoveryAgentConfig.mcpDirectTools] } : {}),
|
|
1729
|
+
...(recoveryAgentConfig.mutationTools ? { mutationTools: [...recoveryAgentConfig.mutationTools] } : {}),
|
|
1721
1730
|
...(recoveryAgentConfig.systemPrompt ? { systemPrompt: recoveryAgentConfig.systemPrompt } : {}),
|
|
1722
1731
|
systemPromptMode: recoveryAgentConfig.systemPromptMode,
|
|
1723
1732
|
inheritProjectContext: recoveryAgentConfig.inheritProjectContext,
|
|
1733
|
+
inheritGlobalContext: recoveryAgentConfig.inheritGlobalContext,
|
|
1724
1734
|
inheritSkills: recoveryAgentConfig.inheritSkills,
|
|
1725
1735
|
...(resolvedSkills.length ? { skills: resolvedSkills.map((skill) => skill.name) } : {}),
|
|
1726
1736
|
...(recoveryAgentConfig.skillPath ? { skillPath: [...recoveryAgentConfig.skillPath] } : {}),
|
|
@@ -1770,7 +1780,9 @@ export function executeAsyncSingle(
|
|
|
1770
1780
|
...(params.externalJobFollowUp ? { externalJobFollowUp: params.externalJobFollowUp } : {}),
|
|
1771
1781
|
...(params.context ? { context: params.context } : {}),
|
|
1772
1782
|
cwd: runnerCwd,
|
|
1783
|
+
requestedCwd: params.requestedCwd ?? runnerCwd,
|
|
1773
1784
|
model,
|
|
1785
|
+
...(contextLimit !== undefined ? { contextLimit } : {}),
|
|
1774
1786
|
...(params.fast ?? agentConfig.fast ? { fast: params.fast ?? agentConfig.fast } : {}),
|
|
1775
1787
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
1776
1788
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
@@ -1781,10 +1793,14 @@ export function executeAsyncSingle(
|
|
|
1781
1793
|
extensions: agentConfig.extensions,
|
|
1782
1794
|
subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
|
|
1783
1795
|
mcpDirectTools: agentConfig.mcpDirectTools,
|
|
1796
|
+
...(toolPlan.mcpConfig ? { mcpConfig: toolPlan.mcpConfig } : {}),
|
|
1797
|
+
...(toolPlan.runtimeServerNames ? { runtimeServerNames: toolPlan.runtimeServerNames } : {}),
|
|
1798
|
+
mutationTools: agentConfig.mutationTools,
|
|
1784
1799
|
completionGuard: agentConfig.completionGuard,
|
|
1785
1800
|
systemPrompt,
|
|
1786
1801
|
systemPromptMode: agentConfig.systemPromptMode,
|
|
1787
1802
|
inheritProjectContext: agentConfig.inheritProjectContext,
|
|
1803
|
+
inheritGlobalContext: agentConfig.inheritGlobalContext,
|
|
1788
1804
|
inheritSkills: agentConfig.inheritSkills,
|
|
1789
1805
|
skills: resolvedSkills.map((r) => r.name),
|
|
1790
1806
|
outputPath,
|
|
@@ -1861,10 +1877,11 @@ export function executeAsyncSingle(
|
|
|
1861
1877
|
lastUpdate: initialStatusAt,
|
|
1862
1878
|
currentStep: 0,
|
|
1863
1879
|
chainStepCount: 1,
|
|
1864
|
-
steps: [{ agent, status: "pending" }],
|
|
1880
|
+
steps: [{ agent, status: "pending", ...(model ? { model } : {}), ...(contextLimit !== undefined ? { contextLimit } : {}) }],
|
|
1865
1881
|
},
|
|
1866
1882
|
path.join(asyncDir, "status.json"),
|
|
1867
1883
|
(proof) => emitProcessTerminalEvent(ctx, proof),
|
|
1884
|
+
params.requestedCwd ?? runnerCwd,
|
|
1868
1885
|
);
|
|
1869
1886
|
} catch (error) {
|
|
1870
1887
|
params.activeAsyncCapacity?.rollback();
|
|
@@ -325,7 +325,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
325
325
|
const parsed = value as Record<string, unknown>;
|
|
326
326
|
const allowedFields = new Set([
|
|
327
327
|
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "fallbackModels", "thinking", "thinkingCeiling", "tools", "extensions",
|
|
328
|
-
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
328
|
+
"subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
329
329
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
330
330
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
331
331
|
"launchResolvedExtensions", "runFanoutBudget",
|
|
@@ -359,8 +359,10 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
359
359
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
360
360
|
if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
|
|
361
361
|
}
|
|
362
|
+
if (parsed.inheritGlobalContext === undefined) parsed.inheritGlobalContext = parsed.inheritProjectContext;
|
|
363
|
+
else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
|
|
362
364
|
if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
|
|
363
|
-
for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "skills", "skillPath"] as const) {
|
|
365
|
+
for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
|
|
364
366
|
const item = parsed[field];
|
|
365
367
|
if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
|
|
366
368
|
}
|
|
@@ -592,9 +594,11 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
|
|
|
592
594
|
extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
|
|
593
595
|
subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
|
|
594
596
|
mcpDirectTools: descriptor.mcpDirectTools ? [...descriptor.mcpDirectTools] : undefined,
|
|
597
|
+
mutationTools: descriptor.mutationTools ? [...descriptor.mutationTools] : undefined,
|
|
595
598
|
systemPrompt: descriptor.systemPrompt ?? agentConfig.systemPrompt,
|
|
596
599
|
systemPromptMode: descriptor.systemPromptMode,
|
|
597
600
|
inheritProjectContext: descriptor.inheritProjectContext,
|
|
601
|
+
inheritGlobalContext: descriptor.inheritGlobalContext,
|
|
598
602
|
inheritSkills: descriptor.inheritSkills,
|
|
599
603
|
skills: descriptor.skills ? [...descriptor.skills] : undefined,
|
|
600
604
|
skillPath: descriptor.skillPath ? [...descriptor.skillPath] : undefined,
|
|
@@ -48,6 +48,7 @@ interface AsyncRunStepSummary {
|
|
|
48
48
|
totalCost?: CostSummary;
|
|
49
49
|
skills?: string[];
|
|
50
50
|
model?: string;
|
|
51
|
+
contextLimit?: number;
|
|
51
52
|
thinking?: string;
|
|
52
53
|
attemptedModels?: string[];
|
|
53
54
|
sessionFile?: string;
|
|
@@ -287,6 +288,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
287
288
|
...(step.totalCost ? { totalCost: step.totalCost } : {}),
|
|
288
289
|
...(step.skills ? { skills: step.skills } : {}),
|
|
289
290
|
...(step.model ? { model: step.model } : {}),
|
|
291
|
+
...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
|
|
290
292
|
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
291
293
|
...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
|
|
292
294
|
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
@@ -172,6 +172,7 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
|
|
|
172
172
|
...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
|
|
173
173
|
skills: task.skills,
|
|
174
174
|
model: task.model,
|
|
175
|
+
...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
|
|
175
176
|
thinking: task.thinking,
|
|
176
177
|
attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
|
|
177
178
|
recentTools: [],
|
|
@@ -189,6 +190,7 @@ function statusStepsForRunnerStep(step: RunnerStep, stepIndex: number): StatusSt
|
|
|
189
190
|
label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
|
|
190
191
|
outputName: step.collect.as,
|
|
191
192
|
structured: Boolean(step.collect.outputSchema),
|
|
193
|
+
...(step.parallel.contextLimit !== undefined ? { contextLimit: step.parallel.contextLimit } : {}),
|
|
192
194
|
status: "pending",
|
|
193
195
|
recentTools: [],
|
|
194
196
|
recentOutput: [],
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./completion-batcher.ts";
|
|
17
17
|
import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type ScheduleOrigin, type SubagentState } from "../../shared/types.ts";
|
|
18
18
|
import { isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
19
|
+
import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
|
|
19
20
|
|
|
20
21
|
export interface SubagentNotifyDetails {
|
|
21
22
|
agent: string;
|
|
@@ -91,6 +92,7 @@ export interface RegisterSubagentNotifyOptions {
|
|
|
91
92
|
batchConfig?: CompletionBatchConfig;
|
|
92
93
|
timers?: NotifyTimerApi;
|
|
93
94
|
now?: () => number;
|
|
95
|
+
ownership?: Pick<ResultDeliveryOwnership, "owns">;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export interface CompletionNotifier {
|
|
@@ -228,6 +230,8 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
|
|
|
228
230
|
interface PendingCompletion {
|
|
229
231
|
key: string;
|
|
230
232
|
details: SubagentNotifyDetails;
|
|
233
|
+
sessionId: string;
|
|
234
|
+
completionOwnerId: unknown;
|
|
231
235
|
triggerTurn: boolean;
|
|
232
236
|
resolve(accepted: boolean): void;
|
|
233
237
|
}
|
|
@@ -335,6 +339,10 @@ export default function registerSubagentNotify(
|
|
|
335
339
|
const batchConfig = resolveCompletionBatchConfig(options.batchConfig);
|
|
336
340
|
const batchers = new Map<string, CompletionBatcher<PendingCompletion>>();
|
|
337
341
|
let disposed = false;
|
|
342
|
+
const ownsResult = options.ownership?.owns
|
|
343
|
+
?? ((sessionId: string, completionOwnerId: unknown) => sessionId === state.currentSessionId
|
|
344
|
+
&& typeof completionOwnerId === "string"
|
|
345
|
+
&& completionOwnerId === state.completionOwnerId);
|
|
338
346
|
|
|
339
347
|
const settle = (items: PendingCompletion[], accepted: boolean) => {
|
|
340
348
|
for (const item of items) {
|
|
@@ -343,7 +351,18 @@ export default function registerSubagentNotify(
|
|
|
343
351
|
item.resolve(accepted);
|
|
344
352
|
}
|
|
345
353
|
};
|
|
346
|
-
const emit = (items: PendingCompletion[]) =>
|
|
354
|
+
const emit = (items: PendingCompletion[]) => {
|
|
355
|
+
const accepted: PendingCompletion[] = [];
|
|
356
|
+
const rejected: PendingCompletion[] = [];
|
|
357
|
+
for (const item of items) {
|
|
358
|
+
const owned = item.details.source === "foreground"
|
|
359
|
+
? item.sessionId === state.currentSessionId
|
|
360
|
+
: ownsResult(item.sessionId, item.completionOwnerId);
|
|
361
|
+
(owned ? accepted : rejected).push(item);
|
|
362
|
+
}
|
|
363
|
+
settle(rejected, false);
|
|
364
|
+
settle(accepted, sendCompletion(pi, accepted));
|
|
365
|
+
};
|
|
347
366
|
const getBatcher = (result: CompletionNotification) => {
|
|
348
367
|
const key = completionBatchKey(result);
|
|
349
368
|
let batcher = batchers.get(key);
|
|
@@ -360,8 +379,10 @@ export default function registerSubagentNotify(
|
|
|
360
379
|
};
|
|
361
380
|
|
|
362
381
|
const deliver = (result: CompletionNotification): Promise<boolean> => {
|
|
363
|
-
if (disposed || typeof result.sessionId !== "string"
|
|
364
|
-
if (result.source
|
|
382
|
+
if (disposed || typeof result.sessionId !== "string") return Promise.resolve(false);
|
|
383
|
+
if (result.source === "foreground") {
|
|
384
|
+
if (result.sessionId !== state.currentSessionId) return Promise.resolve(false);
|
|
385
|
+
} else if (!ownsResult(result.sessionId, result.completionOwnerId)) return Promise.resolve(false);
|
|
365
386
|
if (result.intercomDelivered === true) return Promise.resolve(true);
|
|
366
387
|
const key = buildCompletionKey(result, "notify");
|
|
367
388
|
const seenAt = seen.get(key);
|
|
@@ -376,6 +397,8 @@ export default function registerSubagentNotify(
|
|
|
376
397
|
const item: PendingCompletion = {
|
|
377
398
|
key,
|
|
378
399
|
details,
|
|
400
|
+
sessionId: result.sessionId,
|
|
401
|
+
completionOwnerId: result.completionOwnerId,
|
|
379
402
|
triggerTurn: result.triggerTurn !== false,
|
|
380
403
|
resolve,
|
|
381
404
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SubagentState } from "../../shared/types.ts";
|
|
2
|
+
|
|
3
|
+
const MAX_CLAIMED_PREDECESSOR_SESSIONS = 8;
|
|
4
|
+
|
|
5
|
+
type ResultDeliveryState = Pick<SubagentState, "currentSessionId" | "completionOwnerId">;
|
|
6
|
+
|
|
7
|
+
export interface ResultDeliveryOwnership {
|
|
8
|
+
owns(sessionId: string, completionOwnerId: unknown): boolean;
|
|
9
|
+
claimedSessionIds(): readonly string[];
|
|
10
|
+
claimPredecessor(previousSessionFile: string | undefined, previousRuntimeSessionId: string | null): boolean;
|
|
11
|
+
clear(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createResultDeliveryOwnership(state: ResultDeliveryState): ResultDeliveryOwnership {
|
|
15
|
+
const claimed = new Map<string, string>();
|
|
16
|
+
|
|
17
|
+
const currentOwner = (): string | undefined => typeof state.completionOwnerId === "string" && state.completionOwnerId
|
|
18
|
+
? state.completionOwnerId
|
|
19
|
+
: undefined;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
owns(sessionId, completionOwnerId) {
|
|
23
|
+
const owner = currentOwner();
|
|
24
|
+
if (!owner || completionOwnerId !== owner) return false;
|
|
25
|
+
return sessionId === state.currentSessionId || claimed.get(sessionId) === owner;
|
|
26
|
+
},
|
|
27
|
+
claimedSessionIds() {
|
|
28
|
+
const owner = currentOwner();
|
|
29
|
+
if (!owner) return [];
|
|
30
|
+
return [...claimed].flatMap(([sessionId, claimedOwner]) => claimedOwner === owner ? [sessionId] : []);
|
|
31
|
+
},
|
|
32
|
+
claimPredecessor(previousSessionFile, previousRuntimeSessionId) {
|
|
33
|
+
const owner = currentOwner();
|
|
34
|
+
if (!owner || !previousSessionFile || !previousRuntimeSessionId) return false;
|
|
35
|
+
if (previousSessionFile !== previousRuntimeSessionId || state.currentSessionId !== previousRuntimeSessionId) return false;
|
|
36
|
+
claimed.delete(previousRuntimeSessionId);
|
|
37
|
+
claimed.set(previousRuntimeSessionId, owner);
|
|
38
|
+
while (claimed.size > MAX_CLAIMED_PREDECESSOR_SESSIONS) claimed.delete(claimed.keys().next().value!);
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
clear() {
|
|
42
|
+
claimed.clear();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -25,6 +25,7 @@ import { recordWaitCompletion } from "./wait-completions.ts";
|
|
|
25
25
|
import { MISSION_BINDING_FILE, syncMissionFromAsyncCompletion } from "../../missions/lifecycle.ts";
|
|
26
26
|
import { missionObserverResultCandidateFiles, promotePendingResultFile, removeMissionObserverIndex, removeResultIndex, resultCandidateFilesForSession, resultPayloadPathForIndexedRun, resultPayloadPathForMissionObserverRun, resultPayloadPathForSessionRun, writeAsyncResultFile, writeResultIndexForData } from "./result-files.ts";
|
|
27
27
|
import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
|
|
28
|
+
import type { ResultDeliveryOwnership } from "./result-delivery-ownership.ts";
|
|
28
29
|
|
|
29
30
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
30
31
|
const POLL_INTERVAL_MS = 3000;
|
|
@@ -60,6 +61,8 @@ type ResultWatcherDeps = {
|
|
|
60
61
|
/** Control how slow result-index scans are logged. Defaults to \"activity\". */
|
|
61
62
|
resultScanLogging?: "all" | "activity" | "off";
|
|
62
63
|
platform?: NodeJS.Platform;
|
|
64
|
+
/** Shared current/predecessor session ownership used by the notifier. */
|
|
65
|
+
ownership?: Pick<ResultDeliveryOwnership, "owns" | "claimedSessionIds">;
|
|
63
66
|
};
|
|
64
67
|
|
|
65
68
|
type ResultFileChild = {
|
|
@@ -200,6 +203,7 @@ export function createResultWatcher(
|
|
|
200
203
|
deps: ResultWatcherDeps = {},
|
|
201
204
|
): {
|
|
202
205
|
startResultWatcher: () => void;
|
|
206
|
+
transitionResultDelivery: () => void;
|
|
203
207
|
primeExistingResults: (options?: { triggerTurn?: boolean }) => void;
|
|
204
208
|
refreshResultDelivery: () => void;
|
|
205
209
|
stopResultWatcher: () => void;
|
|
@@ -218,14 +222,16 @@ export function createResultWatcher(
|
|
|
218
222
|
// The sole in-memory ownership lease. It is acquired for one active session
|
|
219
223
|
// and revoked before the watcher, queues, or callbacks are torn down.
|
|
220
224
|
let activeSessionId: string | null = null;
|
|
225
|
+
const ownsResult = deps.ownership?.owns
|
|
226
|
+
?? ((sessionId: string, completionOwnerId: unknown) => sessionId === state.currentSessionId
|
|
227
|
+
&& typeof completionOwnerId === "string"
|
|
228
|
+
&& completionOwnerId === state.completionOwnerId);
|
|
229
|
+
const claimedSessionIds = () => deps.ownership?.claimedSessionIds() ?? [];
|
|
221
230
|
|
|
222
231
|
const ownsCompletion = (sessionId: string, completionOwnerId: unknown, epoch: number) => {
|
|
223
232
|
if (!deliveryActive || epoch !== deliveryEpoch) return false;
|
|
224
233
|
if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
|
|
225
|
-
return activeSessionId === sessionId
|
|
226
|
-
&& state.currentSessionId === sessionId
|
|
227
|
-
&& typeof completionOwnerId === "string"
|
|
228
|
-
&& completionOwnerId === state.completionOwnerId;
|
|
234
|
+
return activeSessionId === state.currentSessionId && ownsResult(sessionId, completionOwnerId);
|
|
229
235
|
};
|
|
230
236
|
|
|
231
237
|
const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
|
|
@@ -246,8 +252,11 @@ export function createResultWatcher(
|
|
|
246
252
|
const resultPayloadPath = (file: string, observed?: ReadonlySet<string>): string | undefined => {
|
|
247
253
|
if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
|
|
248
254
|
const runId = file.replace(/\.json$/i, "");
|
|
249
|
-
const
|
|
250
|
-
|
|
255
|
+
for (const sessionId of [state.currentSessionId, ...claimedSessionIds()]) {
|
|
256
|
+
if (!sessionId) continue;
|
|
257
|
+
const sessionResult = resultPayloadPathForSessionRun(resultsDir, sessionId, runId);
|
|
258
|
+
if (sessionResult) return sessionResult;
|
|
259
|
+
}
|
|
251
260
|
const observerResult = resultPayloadPathForMissionObserverRun(resultsDir, runId);
|
|
252
261
|
if (observerResult) return observerResult;
|
|
253
262
|
if (observed?.has(runId)) {
|
|
@@ -308,7 +317,7 @@ export function createResultWatcher(
|
|
|
308
317
|
// Missing identity stays on the normal parser path so malformed or legacy
|
|
309
318
|
// files keep their existing diagnostics and compatibility behavior.
|
|
310
319
|
if (!identity.sessionId) return true;
|
|
311
|
-
if (identity.sessionId
|
|
320
|
+
if (ownsResult(identity.sessionId, identity.completionOwnerId)) return true;
|
|
312
321
|
if (identity.asyncDir && fsApi.existsSync(path.join(identity.asyncDir, MISSION_BINDING_FILE))) return true;
|
|
313
322
|
if (identity.runId && (observed ?? observedRunIds()).has(identity.runId)) return true;
|
|
314
323
|
return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
|
|
@@ -621,8 +630,9 @@ export function createResultWatcher(
|
|
|
621
630
|
};
|
|
622
631
|
const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
|
|
623
632
|
const files = new Set<string>();
|
|
624
|
-
|
|
625
|
-
|
|
633
|
+
for (const sessionId of [state.currentSessionId, ...claimedSessionIds()]) {
|
|
634
|
+
if (!sessionId) continue;
|
|
635
|
+
for (const file of resultCandidateFilesForSession(resultsDir, sessionId)) files.add(file);
|
|
626
636
|
}
|
|
627
637
|
for (const runId of state.asyncJobs.keys()) files.add(`${runId}.json`);
|
|
628
638
|
for (const file of missionObserverResultCandidateFiles(resultsDir)) files.add(file);
|
|
@@ -755,6 +765,13 @@ export function createResultWatcher(
|
|
|
755
765
|
}
|
|
756
766
|
};
|
|
757
767
|
|
|
768
|
+
const transitionResultDelivery = () => {
|
|
769
|
+
deliveryActive = true;
|
|
770
|
+
activeSessionId = state.currentSessionId;
|
|
771
|
+
deliveryEpoch += 1;
|
|
772
|
+
identityCache.clear();
|
|
773
|
+
};
|
|
774
|
+
|
|
758
775
|
const stopResultWatcher = () => {
|
|
759
776
|
deliveryActive = false;
|
|
760
777
|
activeSessionId = null;
|
|
@@ -769,5 +786,5 @@ export function createResultWatcher(
|
|
|
769
786
|
identityCache.clear();
|
|
770
787
|
};
|
|
771
788
|
|
|
772
|
-
return { startResultWatcher, primeExistingResults, stopResultWatcher, refreshResultDelivery: () => { primeExistingResults(); startDemandPolling(); } };
|
|
789
|
+
return { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher, refreshResultDelivery: () => { primeExistingResults(); startDemandPolling(); } };
|
|
773
790
|
}
|