@selesai/code 0.8.6 → 0.8.7
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/core/agent-session.js +13 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/system-prompt.js +2 -8
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/schema-prune.d.ts +16 -0
- package/dist/core/tools/schema-prune.js +39 -0
- package/dist/core/tools/schema-prune.test.d.ts +1 -0
- package/dist/core/tools/schema-prune.test.js +111 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/model-prompt-injector/config.json +10 -0
- package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
- package/dist/extensions/model-prompt-injector/index.ts +151 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
- package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
- package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/agents/worker.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
- package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
- package/dist/extensions/pi-subagents/docs/missions.md +5 -3
- package/dist/extensions/pi-subagents/docs/models.md +1 -1
- package/dist/extensions/pi-subagents/docs/observability.md +42 -2
- package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
- package/dist/extensions/pi-subagents/install.mjs +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
- package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
- package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
- package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
- package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
- package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
- package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
- package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
- package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
- package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
- package/dist/extensions/question/index.ts +21 -6
- package/dist/extensions/question/question-list.ts +7 -1
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/exp.test.ts +70 -0
- package/dist/extensions/question/tests/exp2.test.ts +59 -0
- package/dist/extensions/question/tests/exp3.test.ts +47 -0
- package/dist/extensions/question/tests/helpers.test.ts +7 -0
- package/dist/extensions/question/tests/question-list.test.ts +140 -0
- package/dist/extensions/question/tests/row-layout.test.ts +13 -0
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/docs/settings.md +6 -0
- package/package.json +3 -3
|
@@ -22,6 +22,23 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
|
|
|
22
22
|
const CURSOR_FILE_MUTATION_THINKING =
|
|
23
23
|
/(?:^|\n)\s*Cursor (?:edit|write)\s*:/i;
|
|
24
24
|
|
|
25
|
+
const REVIVED_TASK_PREFIX = /^You are reviving a previous subagent conversation\.\n\nOriginal run: .+\nOriginal agent: .+(?:\nOriginal session file: .+)?\n\nUse the stored session context as background\. Answer the orchestrator's follow-up below\. Do not assume the original child process is still alive\.\n\nFollow-up:\n/;
|
|
26
|
+
const IMPLEMENTATION_CHALLENGE_FOLLOW_UP_PATTERN = /^(?:Run )?implementation challenge pass (?:one|two|\d+)(?:\s+and implement any better current[- ]scope change\.?|\s+for the accepted candidate\.\s+Reconsider it and implement any better current[- ]scope change\.?)?$/i;
|
|
27
|
+
const NO_BETTER_CHANGE_NEEDED_PATTERN = /^\s*no (?:better|further|additional) (?:current[- ]scope )?(?:code |source |file )?(?:change|changes|edit|edits|patch|patches) (?:is|are) needed\b/i;
|
|
28
|
+
const KEPT_CURRENT_IMPLEMENTATION_PATTERN = /\b(?:kept (?:the )?current (?:implementation|candidate|shape)|(?:the )?current (?:implementation|candidate|shape) was kept)\b/i;
|
|
29
|
+
const NO_CHANGE_MADE_PATTERN = /\bno (?:new )?(?:(?:code|source|file|test)(?:\s*(?:,\s*or|,|\/|or)\s*(?:code|source|file|test))* changes?|changes?) (?:were|was) made\b/i;
|
|
30
|
+
const NO_BETTER_CHANGE_QUALIFIER_PATTERN = /\b(?:do\s+not|don't|dont|not|never|cannot|can't|cant|unable|uncertain|unsure|unclear|disagree|wrong|false|reject|rejected|rejecting|maybe|might|may|\w+n['’]t)\b/i;
|
|
31
|
+
const REPORT_SENTENCE_PATTERN = /[^.!?]+(?:[.!?]+(?=(?:["”'’]?\s)|["”'’]?$)|$)/g;
|
|
32
|
+
const QUOTED_CLAIM_PATTERN = /["“]([^"”]+)["”]([.!?]?\s*[^.!?]*)/g;
|
|
33
|
+
const QUOTED_CLAIM_REFERENCE_PATTERN = /\b(?:that|this|it|claim|report|message|disagree)\b/i;
|
|
34
|
+
const CLAIM_CONTRADICTION_PATTERN = /\bbut\b[^.!?]*\b(?:uncertain|unsure|unclear|disagree|wrong|false|reject|rejected|rejecting)\b/i;
|
|
35
|
+
const IMPLEMENTATION_RETRACTION_PATTERN = /\b(?:found|identified)\s+(?:(?:a|an|the)\s+)?(?:required|necessary|additional|better)?\s*(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:(?:implementation|code|source|file|test)\s+)?work\s+(?:remains?|is\s+(?:needed|required))\b|\b(?:(?:a|an|the)\s+)?(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\s+(?:are|is)\s+(?:needed|required)\b|\b(?:required|necessary|additional|better)\s+(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:I|we)?\s*need\s+(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:need|needs|require|requires)\s+(?:to\s+)?(?:implement|make|apply)\b/i;
|
|
36
|
+
const IMPLEMENTATION_RETRACTION_GLOBAL_PATTERN = new RegExp(IMPLEMENTATION_RETRACTION_PATTERN.source, "gi");
|
|
37
|
+
const DIRECT_CLAIM_RETRACTION_PATTERN = /^\s*(?:I\s+)?(?:disagree|reject|retract)\b|\b(?:disagree|retract|retracted|retracting|reject|rejected|rejecting)\b[^.!?]*\b(?:that|this|it|claim|report|message)\b|\b(?:that|this|it|claim|report|message)\b[^.!?]*\b(?:is|was)\s+(?:rejected|retracted)\b/i;
|
|
38
|
+
const NEGATED_RETRACTION_PREFIX_PATTERN = /\b(?:no|not|\w+n['’]t)\b(?:\s+\w+){0,2}\s*$/i;
|
|
39
|
+
const REPORTED_CLAIM_PREFIX_PATTERN = /["“]|\b(?:prior|previous)\s+(?:message|report)\b|\b(?:said|stated|reported)\b/i;
|
|
40
|
+
const POST_CLAIM_SEPARATOR_PATTERN = /^[\s,;:.—–'"“‘’-]+/;
|
|
41
|
+
|
|
25
42
|
interface CompletionMutationGuardInput {
|
|
26
43
|
agent: string;
|
|
27
44
|
task: string;
|
|
@@ -36,6 +53,11 @@ interface CompletionMutationGuardResult {
|
|
|
36
53
|
triggered: boolean;
|
|
37
54
|
}
|
|
38
55
|
|
|
56
|
+
interface ReportSentence {
|
|
57
|
+
text: string;
|
|
58
|
+
offset: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
export function hasMutationToolCapability(tools: string[] | undefined, mcpDirectTools: string[] | undefined): boolean {
|
|
40
62
|
if ((mcpDirectTools?.length ?? 0) > 0) return true;
|
|
41
63
|
if (tools === undefined) return true;
|
|
@@ -82,14 +104,98 @@ export function hasMutationToolCall(messages: Message[]): boolean {
|
|
|
82
104
|
return false;
|
|
83
105
|
}
|
|
84
106
|
|
|
107
|
+
function hasImplementationRetraction(text: string): boolean {
|
|
108
|
+
return Array.from(text.matchAll(IMPLEMENTATION_RETRACTION_GLOBAL_PATTERN)).some((retraction) =>
|
|
109
|
+
!NEGATED_RETRACTION_PREFIX_PATTERN.test(text.slice(0, retraction.index!)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasDirectClaimRetraction(text: string): boolean {
|
|
113
|
+
return DIRECT_CLAIM_RETRACTION_PATTERN.test(text.replace(POST_CLAIM_SEPARATOR_PATTERN, ""));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isInsideQuotedText(report: string, index: number): boolean {
|
|
117
|
+
let quote: "double" | "single" | undefined;
|
|
118
|
+
for (const char of report.slice(0, index)) {
|
|
119
|
+
if (quote === "double") {
|
|
120
|
+
if (char === "\"" || char === "”") quote = undefined;
|
|
121
|
+
} else if (quote === "single") {
|
|
122
|
+
if (char === "’") quote = undefined;
|
|
123
|
+
} else if (char === "\"" || char === "“") {
|
|
124
|
+
quote = "double";
|
|
125
|
+
} else if (char === "‘") {
|
|
126
|
+
quote = "single";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return quote !== undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function unqualifiedClaimIndex(report: string, sentences: ReportSentence[], pattern: RegExp): number {
|
|
133
|
+
return sentences.findIndex((sentence) => {
|
|
134
|
+
const claim = sentence.text.match(pattern);
|
|
135
|
+
if (claim === null) return false;
|
|
136
|
+
const before = sentence.text.slice(0, claim.index!);
|
|
137
|
+
const after = sentence.text.slice(claim.index! + claim[0].length);
|
|
138
|
+
const previous = before.trimEnd().at(-1);
|
|
139
|
+
return previous !== "'"
|
|
140
|
+
&& previous !== "‘"
|
|
141
|
+
&& !isInsideQuotedText(report, sentence.offset + claim.index!)
|
|
142
|
+
&& !REPORTED_CLAIM_PREFIX_PATTERN.test(before)
|
|
143
|
+
&& !NO_BETTER_CHANGE_QUALIFIER_PATTERN.test(before)
|
|
144
|
+
&& !CLAIM_CONTRADICTION_PATTERN.test(after)
|
|
145
|
+
&& !hasImplementationRetraction(after)
|
|
146
|
+
&& !hasDirectClaimRetraction(after);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasLaterClaimRetraction(sentences: ReportSentence[], claimIndex: number): boolean {
|
|
151
|
+
return sentences.slice(claimIndex + 1).some((sentence) => hasImplementationRetraction(sentence.text)
|
|
152
|
+
|| hasDirectClaimRetraction(sentence.text));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function explicitlyRejectsQuotedClaim(report: string): boolean {
|
|
156
|
+
for (const [, claim, response] of report.matchAll(QUOTED_CLAIM_PATTERN)) {
|
|
157
|
+
if ((KEPT_CURRENT_IMPLEMENTATION_PATTERN.test(claim!) || NO_CHANGE_MADE_PATTERN.test(claim!))
|
|
158
|
+
&& NO_BETTER_CHANGE_QUALIFIER_PATTERN.test(response!)
|
|
159
|
+
&& QUOTED_CLAIM_REFERENCE_PATTERN.test(response!)) return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isImplementationChallengeTask(task: string): boolean {
|
|
165
|
+
const followUp = task.replace(REVIVED_TASK_PREFIX, "");
|
|
166
|
+
return followUp !== task && IMPLEMENTATION_CHALLENGE_FOLLOW_UP_PATTERN.test(followUp.trim());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function reportsNoBetterChallengeChange(messages: Message[]): boolean {
|
|
170
|
+
const report = messages
|
|
171
|
+
.filter((message) => message.role === "assistant")
|
|
172
|
+
.flatMap((message) => message.content)
|
|
173
|
+
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
174
|
+
.join("\n");
|
|
175
|
+
if (explicitlyRejectsQuotedClaim(report)) return false;
|
|
176
|
+
const sentences = Array.from(report.matchAll(REPORT_SENTENCE_PATTERN), (match) => ({
|
|
177
|
+
text: match[0],
|
|
178
|
+
offset: match.index!,
|
|
179
|
+
}));
|
|
180
|
+
const noBetterIndex = unqualifiedClaimIndex(report, sentences, NO_BETTER_CHANGE_NEEDED_PATTERN);
|
|
181
|
+
if (noBetterIndex >= 0) return !hasLaterClaimRetraction(sentences, noBetterIndex);
|
|
182
|
+
const keptIndex = unqualifiedClaimIndex(report, sentences, KEPT_CURRENT_IMPLEMENTATION_PATTERN);
|
|
183
|
+
const noChangeIndex = unqualifiedClaimIndex(report, sentences, NO_CHANGE_MADE_PATTERN);
|
|
184
|
+
return keptIndex >= 0
|
|
185
|
+
&& noChangeIndex >= 0
|
|
186
|
+
&& !hasLaterClaimRetraction(sentences, Math.min(keptIndex, noChangeIndex));
|
|
187
|
+
}
|
|
188
|
+
|
|
85
189
|
export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
|
|
86
190
|
const expectedMutation = hasMutationToolCapability(input.tools, input.mcpDirectTools)
|
|
87
191
|
? expectsImplementationMutation(input.agent, input.task)
|
|
88
192
|
: false;
|
|
89
193
|
const attemptedMutation = hasMutationToolCall(input.messages);
|
|
194
|
+
const noEditChallengeComplete = isImplementationChallengeTask(input.task)
|
|
195
|
+
&& reportsNoBetterChallengeChange(input.messages);
|
|
90
196
|
return {
|
|
91
197
|
expectedMutation,
|
|
92
198
|
attemptedMutation,
|
|
93
|
-
triggered: expectedMutation && !attemptedMutation,
|
|
199
|
+
triggered: expectedMutation && !attemptedMutation && !noEditChallengeComplete,
|
|
94
200
|
};
|
|
95
201
|
}
|
|
@@ -35,6 +35,8 @@ export function runExternalCli(input: {
|
|
|
35
35
|
timeoutMessage?: string;
|
|
36
36
|
stopMessage?: string;
|
|
37
37
|
onProcess?: (process: ExternalProcessStatus) => void;
|
|
38
|
+
onStdout?: (chunk: Buffer) => void;
|
|
39
|
+
onStderr?: (chunk: Buffer) => void;
|
|
38
40
|
}): Promise<ExternalCliRunResult> {
|
|
39
41
|
return new Promise((resolve, reject) => {
|
|
40
42
|
const startedAt = Date.now();
|
|
@@ -65,11 +67,13 @@ export function runExternalCli(input: {
|
|
|
65
67
|
input.onProcess?.(initialProcess);
|
|
66
68
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
67
69
|
stdoutStream.write(chunk);
|
|
70
|
+
input.onStdout?.(chunk);
|
|
68
71
|
stdoutTail = Buffer.concat([stdoutTail, chunk]);
|
|
69
72
|
if (stdoutTail.length > MAX_OUTPUT_TAIL_BYTES) stdoutTail = stdoutTail.subarray(stdoutTail.length - MAX_OUTPUT_TAIL_BYTES);
|
|
70
73
|
});
|
|
71
74
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
72
75
|
stderrStream.write(chunk);
|
|
76
|
+
input.onStderr?.(chunk);
|
|
73
77
|
stderrTail = Buffer.concat([stderrTail, chunk]);
|
|
74
78
|
if (stderrTail.length > MAX_ERROR_TAIL_BYTES) stderrTail = stderrTail.subarray(stderrTail.length - MAX_ERROR_TAIL_BYTES);
|
|
75
79
|
});
|
|
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { Agent, type AgentTool, type StreamFn } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import { convertToLlm, type ExtensionContext } from "@selesai/code";
|
|
4
4
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
5
|
+
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
5
6
|
import { Type, type Static } from "typebox";
|
|
7
|
+
import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* LLM intent arbiter for the completion mutation guard.
|
|
@@ -28,6 +30,10 @@ export type TaskMutationArbiter = (task: string) => Promise<TaskMutationVerdict>
|
|
|
28
30
|
const DecisionParams = Type.Object(
|
|
29
31
|
{
|
|
30
32
|
classification: Type.String({ enum: ["read_only", "implementation"] }),
|
|
33
|
+
confidence: Type.String({
|
|
34
|
+
enum: ["low", "medium", "high"],
|
|
35
|
+
description: "Confidence in the classification. Only read_only with high confidence rescues a failed run.",
|
|
36
|
+
}),
|
|
31
37
|
reason: Type.String({ description: "One concise reason for this classification." }),
|
|
32
38
|
},
|
|
33
39
|
{ additionalProperties: false },
|
|
@@ -35,6 +41,15 @@ const DecisionParams = Type.Object(
|
|
|
35
41
|
|
|
36
42
|
type DecisionParams = Static<typeof DecisionParams>;
|
|
37
43
|
|
|
44
|
+
/** Map a model decision to a verdict. Only a high-confidence read_only rescues. */
|
|
45
|
+
export function mapArbiterDecision(
|
|
46
|
+
decision: { classification?: string; confidence?: string } | undefined,
|
|
47
|
+
): TaskMutationVerdict {
|
|
48
|
+
if (!decision) return "unavailable";
|
|
49
|
+
if (decision.classification === "read_only" && decision.confidence === "high") return "read-only";
|
|
50
|
+
return "implementation";
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
interface ArbiterRuntime {
|
|
39
54
|
model: NonNullable<RegistryModel>;
|
|
40
55
|
baseStreamFn: StreamFn;
|
|
@@ -43,7 +58,7 @@ interface ArbiterRuntime {
|
|
|
43
58
|
|
|
44
59
|
interface ArbiterAuth {
|
|
45
60
|
apiKey?: string;
|
|
46
|
-
headers?:
|
|
61
|
+
headers?: ProviderHeaders;
|
|
47
62
|
env?: Record<string, string>;
|
|
48
63
|
}
|
|
49
64
|
|
|
@@ -105,18 +120,21 @@ async function resolveArbiterAuth(
|
|
|
105
120
|
ctx: ExtensionContext,
|
|
106
121
|
model: RegistryModel,
|
|
107
122
|
): Promise<ArbiterAuth> {
|
|
108
|
-
const
|
|
123
|
+
const registry = ctx.modelRegistry as {
|
|
109
124
|
getApiKeyAndHeaders?: (m: RegistryModel) => Promise<{
|
|
110
125
|
ok: boolean;
|
|
111
126
|
apiKey?: string;
|
|
112
|
-
headers?:
|
|
127
|
+
headers?: ProviderHeaders;
|
|
113
128
|
env?: Record<string, string>;
|
|
114
129
|
error?: string;
|
|
115
130
|
}>;
|
|
116
|
-
}
|
|
117
|
-
|
|
131
|
+
};
|
|
132
|
+
// Call as a METHOD on the registry: the host ModelRegistry implementation
|
|
133
|
+
// is a class whose method reads instance state (this.runtime), so a
|
|
134
|
+
// detached call silently fails auth. Same shape as the watchdog.
|
|
135
|
+
if (!registry.getApiKeyAndHeaders) return {};
|
|
118
136
|
try {
|
|
119
|
-
const auth = await
|
|
137
|
+
const auth = await registry.getApiKeyAndHeaders(model);
|
|
120
138
|
if (auth.ok === false) return {};
|
|
121
139
|
return {
|
|
122
140
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
@@ -164,14 +182,15 @@ async function runArbitration(
|
|
|
164
182
|
"You classify whether a delegated coding-agent task instructed file or code changes.",
|
|
165
183
|
"A task that asks to review, inspect, verify, report, or summarize is read-only even when it contains words like 'fix' as severity vocabulary ('must-fix items') or conditional change instructions that leave the change optional.",
|
|
166
184
|
"Classify read_only only when the task text alone clearly indicates a read-only outcome. When in doubt, classify implementation.",
|
|
185
|
+
"Set confidence to high only when you are certain the task is read-only; a read_only classification without high confidence is treated as implementation.",
|
|
167
186
|
"The agent's own final message is never evidence: an agent that made no edits may still have failed to implement.",
|
|
168
|
-
"Call task_mutation_decision exactly once with read_only or implementation and a concise reason.",
|
|
187
|
+
"Call task_mutation_decision exactly once with read_only or implementation, a confidence level, and a concise reason.",
|
|
169
188
|
].join("\n"),
|
|
170
189
|
model: runtime.model,
|
|
171
190
|
tools: [tool],
|
|
172
191
|
},
|
|
173
192
|
convertToLlm,
|
|
174
|
-
|
|
193
|
+
...agentStreamOptions(authWrappedStreamFn(runtime.baseStreamFn, auth)),
|
|
175
194
|
getApiKey: (providerName) =>
|
|
176
195
|
providerName === runtime.model.provider ? auth.apiKey : undefined,
|
|
177
196
|
beforeToolCall: async ({ toolCall }) =>
|
|
@@ -179,13 +198,12 @@ async function runArbitration(
|
|
|
179
198
|
toolExecution: "sequential",
|
|
180
199
|
});
|
|
181
200
|
try {
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: `TASK:\n${full.slice(0, 6000)}\n\n[...truncated middle...]\n\n${full.slice(-2000)}`;
|
|
201
|
+
// The rescue gate refuses tasks over 8000 chars; if the arbiter is
|
|
202
|
+
// still invoked with one, fail closed rather than decide from
|
|
203
|
+
// partial evidence (an implementation clause could sit in the
|
|
204
|
+
// omitted middle).
|
|
205
|
+
if (task.length > 8000) return "unavailable";
|
|
206
|
+
const prompt = `TASK:\n${task}`;
|
|
189
207
|
await Promise.race([
|
|
190
208
|
agent.prompt(prompt),
|
|
191
209
|
new Promise<never>((_, reject) => {
|
|
@@ -197,7 +215,7 @@ async function runArbitration(
|
|
|
197
215
|
}),
|
|
198
216
|
]);
|
|
199
217
|
if (!decision) return "unavailable";
|
|
200
|
-
return decision
|
|
218
|
+
return mapArbiterDecision(decision);
|
|
201
219
|
} catch {
|
|
202
220
|
return "unavailable";
|
|
203
221
|
}
|
|
@@ -228,13 +246,6 @@ export function isCompletionGuardFailure(result: { error?: string }): boolean {
|
|
|
228
246
|
return result.error?.startsWith(COMPLETION_GUARD_ERROR_PREFIX) === true;
|
|
229
247
|
}
|
|
230
248
|
|
|
231
|
-
/**
|
|
232
|
-
* Decision helper for the runSync completion-guard arbitration: given the raw
|
|
233
|
-
* guard verdict, decide whether the arbiter rescues the run. Pure and
|
|
234
|
-
* unit-testable; runSync calls this BEFORE any failure side effect is
|
|
235
|
-
* published. Only a confident read-only verdict rescues; every error,
|
|
236
|
-
* timeout, or other verdict keeps the guard's original behavior.
|
|
237
|
-
*/
|
|
238
249
|
export async function arbitrateCompletionGuardRescue(input: {
|
|
239
250
|
guardTriggered: boolean;
|
|
240
251
|
task: string;
|
|
@@ -243,6 +254,11 @@ export async function arbitrateCompletionGuardRescue(input: {
|
|
|
243
254
|
if (!input.guardTriggered || !input.arbiter) {
|
|
244
255
|
return { triggered: input.guardTriggered, rescued: false };
|
|
245
256
|
}
|
|
257
|
+
// Never decide from partial evidence: refuse tasks over 8000 chars before
|
|
258
|
+
// even consulting the model.
|
|
259
|
+
if (input.task.length > 8000) {
|
|
260
|
+
return { triggered: true, rescued: false };
|
|
261
|
+
}
|
|
246
262
|
try {
|
|
247
263
|
const verdict = await input.arbiter(input.task);
|
|
248
264
|
if (verdict === "read-only") return { triggered: false, rescued: true };
|
|
@@ -163,6 +163,20 @@ export function resolveModelCandidate(
|
|
|
163
163
|
return model;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
function resolveRequiredSubagentModelCandidate(
|
|
167
|
+
model: string,
|
|
168
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
169
|
+
preferredProvider?: string,
|
|
170
|
+
): string {
|
|
171
|
+
if (!availableModels || availableModels.length === 0) return model;
|
|
172
|
+
const resolvedWhole = resolveBaseModelCandidate(model, availableModels, preferredProvider);
|
|
173
|
+
if (resolvedWhole) return resolvedWhole;
|
|
174
|
+
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
175
|
+
const resolvedBase = thinkingSuffix ? resolveBaseModelCandidate(baseModel, availableModels, preferredProvider) : undefined;
|
|
176
|
+
if (resolvedBase) return `${resolvedBase}${thinkingSuffix}`;
|
|
177
|
+
throw new Error(`Unknown subagent model '${model}' in the active Pi model registry.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
export interface ResolveSubagentModelOverrideOptions {
|
|
167
181
|
/** When set with `enforce: true`, out-of-scope models are rejected. */
|
|
168
182
|
scope?: ModelScopeConfig;
|
|
@@ -207,7 +221,7 @@ export function resolveSubagentModelOverride(
|
|
|
207
221
|
if (explicit === undefined) {
|
|
208
222
|
resolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
209
223
|
} else {
|
|
210
|
-
resolved =
|
|
224
|
+
resolved = resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
211
225
|
}
|
|
212
226
|
if (resolved && options?.scope?.enforce) {
|
|
213
227
|
const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
|
|
@@ -264,7 +278,7 @@ export function buildModelCandidates(
|
|
|
264
278
|
for (let index = 0; index < rawCandidates.length; index++) {
|
|
265
279
|
const raw = rawCandidates[index];
|
|
266
280
|
if (!raw) continue;
|
|
267
|
-
const normalized =
|
|
281
|
+
const normalized = resolveRequiredSubagentModelCandidate(raw.trim(), availableModels, preferredProvider);
|
|
268
282
|
if (!normalized || seen.has(normalized)) continue;
|
|
269
283
|
if ((index > 0 || options?.scope?.strict === true) && options?.scope?.enforce) {
|
|
270
284
|
const violation = checkModelScope(normalized, options.scope, "inherited");
|
|
@@ -34,6 +34,7 @@ import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
|
34
34
|
export const NESTED_EVENTS_DIR = path.join(TEMP_ROOT_DIR, "nested-subagent-events");
|
|
35
35
|
const ROUTE_FILE = "route.json";
|
|
36
36
|
const REGISTRY_FILE = "registry.json";
|
|
37
|
+
const ROUTE_INDEX_DIR = ".route-index";
|
|
37
38
|
const MAX_EVENT_BYTES = 64 * 1024;
|
|
38
39
|
const MAX_STEPS = 12;
|
|
39
40
|
const MAX_CHILDREN = 16;
|
|
@@ -105,6 +106,18 @@ function commonRouteRoot(route: Pick<NestedRoute, "eventSink" | "controlInbox">)
|
|
|
105
106
|
return path.dirname(path.resolve(route.eventSink));
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
function routeIndexRoot(): string {
|
|
110
|
+
return path.join(NESTED_EVENTS_DIR, ROUTE_INDEX_DIR, "roots");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function routeIndexDir(rootRunId: string): string {
|
|
114
|
+
return path.join(routeIndexRoot(), encodeURIComponent(rootRunId));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function routeIndexPath(rootRunId: string, capabilityToken: string): string {
|
|
118
|
+
return path.join(routeIndexDir(rootRunId), `${encodeURIComponent(capabilityToken)}.json`);
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
function validateRouteShape(route: NestedRoute): void {
|
|
109
122
|
assertSafeId("rootRunId", route.rootRunId);
|
|
110
123
|
assertSafeId("capabilityToken", route.capabilityToken);
|
|
@@ -121,7 +134,10 @@ export function createNestedRoute(rootRunId: string): NestedRoute {
|
|
|
121
134
|
const controlInbox = path.join(routeRoot, "controls");
|
|
122
135
|
fs.mkdirSync(eventSink, { recursive: true, mode: 0o700 });
|
|
123
136
|
fs.mkdirSync(controlInbox, { recursive: true, mode: 0o700 });
|
|
124
|
-
|
|
137
|
+
const createdAt = Date.now();
|
|
138
|
+
fs.mkdirSync(routeIndexDir(rootRunId), { recursive: true, mode: 0o700 });
|
|
139
|
+
writeAtomicJson(routeIndexPath(rootRunId, capabilityToken), { rootRunId, capabilityToken, routeRoot: path.basename(routeRoot), createdAt });
|
|
140
|
+
writeAtomicJson(path.join(routeRoot, ROUTE_FILE), { rootRunId, capabilityToken, createdAt });
|
|
125
141
|
return { rootRunId, eventSink, controlInbox, capabilityToken };
|
|
126
142
|
}
|
|
127
143
|
|
|
@@ -515,68 +531,57 @@ function registryPath(route: NestedRoute): string {
|
|
|
515
531
|
return path.join(commonRouteRoot(route), REGISTRY_FILE);
|
|
516
532
|
}
|
|
517
533
|
|
|
534
|
+
function routeFromIndexEntry(rootRunId: string, filePath: string): NestedRoute | undefined {
|
|
535
|
+
try {
|
|
536
|
+
const metadata = JSON.parse(fs.readFileSync(filePath, "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown; routeRoot?: unknown; createdAt?: unknown };
|
|
537
|
+
if (metadata.rootRunId !== rootRunId || typeof metadata.capabilityToken !== "string" || typeof metadata.routeRoot !== "string") return undefined;
|
|
538
|
+
const routeRoot = path.join(NESTED_EVENTS_DIR, path.basename(metadata.routeRoot));
|
|
539
|
+
const route = {
|
|
540
|
+
rootRunId,
|
|
541
|
+
eventSink: path.join(routeRoot, "events"),
|
|
542
|
+
controlInbox: path.join(routeRoot, "controls"),
|
|
543
|
+
capabilityToken: metadata.capabilityToken,
|
|
544
|
+
};
|
|
545
|
+
validateRouteShape(route);
|
|
546
|
+
if (!fs.statSync(route.eventSink).isDirectory() || !fs.statSync(route.controlInbox).isDirectory()) return undefined;
|
|
547
|
+
const routeFilePath = path.join(routeRoot, ROUTE_FILE);
|
|
548
|
+
try {
|
|
549
|
+
const routeFile = JSON.parse(fs.readFileSync(routeFilePath, "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
550
|
+
if (routeFile.rootRunId !== rootRunId || routeFile.capabilityToken !== metadata.capabilityToken) return undefined;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return undefined;
|
|
553
|
+
try {
|
|
554
|
+
writeAtomicJson(routeFilePath, { rootRunId, capabilityToken: metadata.capabilityToken, createdAt: typeof metadata.createdAt === "number" ? metadata.createdAt : Date.now() });
|
|
555
|
+
} catch {
|
|
556
|
+
// The route index already carries enough data for reload lookup.
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return route;
|
|
560
|
+
} catch {
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
518
565
|
export function findNestedRouteForRootId(rootRunId: string): NestedRoute | undefined {
|
|
519
566
|
assertSafeId("rootRunId", rootRunId);
|
|
520
567
|
let entries: string[];
|
|
521
568
|
try {
|
|
522
|
-
entries = fs.readdirSync(
|
|
569
|
+
entries = fs.readdirSync(routeIndexDir(rootRunId));
|
|
523
570
|
} catch (error) {
|
|
524
571
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
525
572
|
throw error;
|
|
526
573
|
}
|
|
527
574
|
for (const entry of entries) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
try {
|
|
531
|
-
const metadata = JSON.parse(fs.readFileSync(path.join(routeRoot, ROUTE_FILE), "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
532
|
-
if (metadata.rootRunId !== rootRunId || typeof metadata.capabilityToken !== "string") continue;
|
|
533
|
-
const route = {
|
|
534
|
-
rootRunId,
|
|
535
|
-
eventSink: path.join(routeRoot, "events"),
|
|
536
|
-
controlInbox: path.join(routeRoot, "controls"),
|
|
537
|
-
capabilityToken: metadata.capabilityToken,
|
|
538
|
-
};
|
|
539
|
-
validateRouteShape(route);
|
|
540
|
-
return route;
|
|
541
|
-
} catch {
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
575
|
+
const route = routeFromIndexEntry(rootRunId, path.join(routeIndexDir(rootRunId), entry));
|
|
576
|
+
if (route) return route;
|
|
544
577
|
}
|
|
545
578
|
return undefined;
|
|
546
579
|
}
|
|
547
580
|
|
|
548
|
-
/**
|
|
549
|
-
* Scan the nested-events directory once and index every route by its root run
|
|
550
|
-
* id. Use this when resolving routes for many runs (e.g. listAsyncRuns) so the
|
|
551
|
-
* cost is O(routes) total instead of O(runs * routes) from calling
|
|
552
|
-
* findNestedRouteForRootId per run.
|
|
553
|
-
*/
|
|
554
581
|
export function buildNestedRouteIndex(): Map<string, NestedRoute> {
|
|
555
|
-
let entries: string[];
|
|
556
|
-
try {
|
|
557
|
-
entries = fs.readdirSync(NESTED_EVENTS_DIR);
|
|
558
|
-
} catch (error) {
|
|
559
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return new Map();
|
|
560
|
-
throw error;
|
|
561
|
-
}
|
|
562
582
|
const index = new Map<string, NestedRoute>();
|
|
563
|
-
for (const
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
const metadata = JSON.parse(fs.readFileSync(path.join(routeRoot, ROUTE_FILE), "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
567
|
-
if (typeof metadata.rootRunId !== "string" || typeof metadata.capabilityToken !== "string") continue;
|
|
568
|
-
if (index.has(metadata.rootRunId)) continue;
|
|
569
|
-
const route: NestedRoute = {
|
|
570
|
-
rootRunId: metadata.rootRunId,
|
|
571
|
-
eventSink: path.join(routeRoot, "events"),
|
|
572
|
-
controlInbox: path.join(routeRoot, "controls"),
|
|
573
|
-
capabilityToken: metadata.capabilityToken,
|
|
574
|
-
};
|
|
575
|
-
validateRouteShape(route);
|
|
576
|
-
index.set(metadata.rootRunId, route);
|
|
577
|
-
} catch {
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
583
|
+
for (const route of listNestedRoutes()) {
|
|
584
|
+
if (!index.has(route.rootRunId)) index.set(route.rootRunId, route);
|
|
580
585
|
}
|
|
581
586
|
return index;
|
|
582
587
|
}
|
|
@@ -630,27 +635,26 @@ function collectScopedNestedRuns(children: NestedRunSummary[] | undefined, scope
|
|
|
630
635
|
}
|
|
631
636
|
|
|
632
637
|
function listNestedRoutes(): NestedRoute[] {
|
|
633
|
-
let
|
|
638
|
+
let rootEntries: string[];
|
|
634
639
|
try {
|
|
635
|
-
|
|
640
|
+
rootEntries = fs.readdirSync(routeIndexRoot());
|
|
636
641
|
} catch (error) {
|
|
637
642
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
638
643
|
throw error;
|
|
639
644
|
}
|
|
640
645
|
const routes: NestedRoute[] = [];
|
|
641
|
-
for (const
|
|
642
|
-
|
|
646
|
+
for (const rootEntry of rootEntries) {
|
|
647
|
+
let rootRunId: string;
|
|
643
648
|
try {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
routes.push(route);
|
|
649
|
+
rootRunId = decodeURIComponent(rootEntry);
|
|
650
|
+
} catch {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
for (const entry of fs.readdirSync(path.join(routeIndexRoot(), rootEntry))) {
|
|
655
|
+
const route = routeFromIndexEntry(rootRunId, path.join(routeIndexRoot(), rootEntry, entry));
|
|
656
|
+
if (route) routes.push(route);
|
|
657
|
+
}
|
|
654
658
|
} catch {
|
|
655
659
|
continue;
|
|
656
660
|
}
|