@mjasnikovs/pi-task 0.38.29 → 0.38.31
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/dist/config/config.d.ts +70 -70
- package/dist/config/config.js +26 -35
- package/dist/config/extension-list.d.ts +6 -5
- package/dist/config/extension-list.js +3 -2
- package/dist/config/reasoning-args.d.ts +9 -7
- package/dist/config/reasoning-args.js +12 -10
- package/dist/config/reasoning.d.ts +44 -105
- package/dist/config/reasoning.js +27 -704
- package/dist/config/register.d.ts +34 -48
- package/dist/config/register.js +41 -51
- package/dist/config/tool-list.d.ts +16 -16
- package/dist/config/tool-list.js +1 -1
- package/dist/index.js +2 -0
- package/dist/remote/bridge.d.ts +19 -10
- package/dist/remote/bridge.js +3 -2
- package/dist/remote/broadcast.js +3 -1
- package/dist/remote/events.js +12 -11
- package/dist/remote/history.d.ts +1 -1
- package/dist/remote/protocol.d.ts +6 -3
- package/dist/remote/protocol.js +2 -1
- package/dist/remote/push.d.ts +16 -16
- package/dist/remote/push.js +27 -27
- package/dist/remote/register.d.ts +3 -3
- package/dist/remote/register.js +17 -19
- package/dist/remote/server.d.ts +9 -8
- package/dist/remote/server.js +15 -14
- package/dist/remote/session-state.d.ts +5 -4
- package/dist/remote/session-state.js +8 -5
- package/dist/remote/sw.d.ts +7 -6
- package/dist/remote/sw.js +7 -6
- package/dist/remote/tailscale.d.ts +4 -2
- package/dist/remote/tailscale.js +4 -2
- package/dist/remote/ui-highlight.js +6 -5
- package/dist/remote/ui-render.js +4 -4
- package/dist/remote/ui-script.js +24 -24
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +10 -13
- package/dist/remote/ui-tools.js +9 -6
- package/dist/shared/child-extensions.d.ts +29 -17
- package/dist/shared/child-extensions.js +29 -17
- package/dist/shared/child-output.d.ts +30 -24
- package/dist/shared/child-output.js +25 -17
- package/dist/shared/child-process.d.ts +47 -40
- package/dist/shared/child-process.js +50 -59
- package/dist/shared/command-watchdog.d.ts +85 -16
- package/dist/shared/command-watchdog.js +115 -21
- package/dist/shared/fs-text.d.ts +16 -10
- package/dist/shared/fs-text.js +16 -10
- package/dist/shared/git-runner.d.ts +25 -25
- package/dist/shared/git-runner.js +25 -25
- package/dist/shared/leaked-tool-call.d.ts +17 -11
- package/dist/shared/leaked-tool-call.js +23 -15
- package/dist/shared/model-endpoint.d.ts +29 -16
- package/dist/shared/model-endpoint.js +33 -21
- package/dist/shared/pi-invocation.d.ts +7 -4
- package/dist/shared/pi-invocation.js +12 -7
- package/dist/shared/pkg-version.d.ts +13 -5
- package/dist/shared/pkg-version.js +13 -5
- package/dist/shared/reasoning-capability.d.ts +35 -24
- package/dist/shared/reasoning-capability.js +35 -24
- package/dist/shared/stream-watchdog.d.ts +60 -44
- package/dist/shared/stream-watchdog.js +62 -45
- package/dist/task/accept-debt.d.ts +41 -43
- package/dist/task/accept-debt.js +73 -65
- package/dist/task/api-synthesis.d.ts +24 -21
- package/dist/task/api-synthesis.js +32 -26
- package/dist/task/apis-contract.d.ts +32 -64
- package/dist/task/apis-contract.js +32 -64
- package/dist/task/artifact-closure.d.ts +27 -13
- package/dist/task/artifact-closure.js +95 -67
- package/dist/task/auto-commit.d.ts +46 -35
- package/dist/task/auto-commit.js +51 -38
- package/dist/task/auto-io.d.ts +45 -25
- package/dist/task/auto-io.js +57 -29
- package/dist/task/auto-orchestrator.d.ts +26 -24
- package/dist/task/auto-orchestrator.js +192 -165
- package/dist/task/auto-prompts.d.ts +36 -24
- package/dist/task/auto-prompts.js +40 -26
- package/dist/task/autofix-ledger.d.ts +27 -25
- package/dist/task/autofix-ledger.js +29 -26
- package/dist/task/batch-test-task.d.ts +20 -12
- package/dist/task/batch-test-task.js +67 -60
- package/dist/task/boot-probe.d.ts +60 -44
- package/dist/task/boot-probe.js +91 -72
- package/dist/task/cancel-input.d.ts +30 -16
- package/dist/task/cancel-input.js +20 -11
- package/dist/task/cancel-points.d.ts +27 -20
- package/dist/task/cancel-points.js +30 -22
- package/dist/task/child-runner.d.ts +124 -55
- package/dist/task/child-runner.js +298 -90
- package/dist/task/child-status.d.ts +23 -16
- package/dist/task/child-status.js +23 -16
- package/dist/task/clamp-output.js +12 -5
- package/dist/task/command-run.d.ts +31 -28
- package/dist/task/command-run.js +44 -35
- package/dist/task/command-shrink.d.ts +25 -18
- package/dist/task/command-shrink.js +37 -31
- package/dist/task/command-watchdog.d.ts +9 -6
- package/dist/task/command-watchdog.js +21 -15
- package/dist/task/context-attribution.d.ts +34 -26
- package/dist/task/context-attribution.js +34 -26
- package/dist/task/context-silence.d.ts +39 -29
- package/dist/task/context-silence.js +35 -25
- package/dist/task/context-usage.d.ts +16 -9
- package/dist/task/context-usage.js +16 -9
- package/dist/task/contracts.d.ts +8 -4
- package/dist/task/contracts.js +25 -17
- package/dist/task/coverage-loop.d.ts +22 -18
- package/dist/task/coverage-loop.js +35 -30
- package/dist/task/critique-probes.d.ts +13 -14
- package/dist/task/critique-probes.js +50 -39
- package/dist/task/debug-log.d.ts +13 -5
- package/dist/task/debug-log.js +32 -20
- package/dist/task/decompose-fidelity.d.ts +11 -9
- package/dist/task/decompose-fidelity.js +38 -33
- package/dist/task/decompose-granularity.d.ts +41 -38
- package/dist/task/decompose-granularity.js +41 -38
- package/dist/task/deep-render-check.d.ts +22 -14
- package/dist/task/deep-render-check.js +40 -31
- package/dist/task/dropped-input.d.ts +12 -7
- package/dist/task/dropped-input.js +5 -2
- package/dist/task/enforce-attribution.d.ts +38 -47
- package/dist/task/enforce-attribution.js +46 -52
- package/dist/task/enforce-guidelines.d.ts +31 -20
- package/dist/task/enforce-guidelines.js +32 -21
- package/dist/task/enrichment.d.ts +7 -2
- package/dist/task/enrichment.js +26 -14
- package/dist/task/env-notes.d.ts +16 -7
- package/dist/task/env-notes.js +48 -31
- package/dist/task/env-template-closure.d.ts +4 -4
- package/dist/task/env-template-closure.js +42 -34
- package/dist/task/external-context.d.ts +28 -21
- package/dist/task/external-context.js +17 -12
- package/dist/task/failure-classifier.d.ts +4 -5
- package/dist/task/failure-classifier.js +30 -8
- package/dist/task/file-inventory.d.ts +15 -11
- package/dist/task/file-inventory.js +25 -22
- package/dist/task/final-gate-fix.d.ts +74 -86
- package/dist/task/final-gate-fix.js +97 -116
- package/dist/task/final-gate-progress.d.ts +29 -46
- package/dist/task/final-gate-progress.js +40 -51
- package/dist/task/final-gate.d.ts +64 -97
- package/dist/task/final-gate.js +192 -199
- package/dist/task/fix-child.d.ts +21 -27
- package/dist/task/fix-child.js +21 -27
- package/dist/task/foreign-path.d.ts +6 -5
- package/dist/task/foreign-path.js +0 -0
- package/dist/task/frozen-conflict.d.ts +9 -10
- package/dist/task/frozen-conflict.js +61 -64
- package/dist/task/frozen-path-guard.d.ts +35 -14
- package/dist/task/frozen-path-guard.js +56 -39
- package/dist/task/gate-child.d.ts +27 -28
- package/dist/task/gate-child.js +36 -35
- package/dist/task/gate-deps.d.ts +34 -27
- package/dist/task/gate-deps.js +169 -159
- package/dist/task/gate-tally.d.ts +77 -80
- package/dist/task/gate-tally.js +65 -68
- package/dist/task/git-state-guard.d.ts +15 -11
- package/dist/task/git-state-guard.js +76 -66
- package/dist/task/impl-widget.d.ts +25 -16
- package/dist/task/impl-widget.js +27 -17
- package/dist/task/implementation-guards.d.ts +26 -0
- package/dist/task/implementation-guards.js +177 -0
- package/dist/task/implementation-thinking.d.ts +33 -31
- package/dist/task/implementation-thinking.js +5 -6
- package/dist/task/implementation-turn.d.ts +39 -31
- package/dist/task/implementation-turn.js +41 -28
- package/dist/task/inline-markdown.d.ts +20 -7
- package/dist/task/inline-markdown.js +15 -6
- package/dist/task/launch-config-gap.js +25 -39
- package/dist/task/launch-contract.d.ts +18 -21
- package/dist/task/launch-contract.js +28 -30
- package/dist/task/launch-manifest.d.ts +6 -2
- package/dist/task/launch-manifest.js +35 -34
- package/dist/task/ledger.js +16 -14
- package/dist/task/lint-fix.d.ts +6 -8
- package/dist/task/lint-fix.js +67 -69
- package/dist/task/loop-detector.d.ts +27 -8
- package/dist/task/loop-detector.js +38 -14
- package/dist/task/mid-run-input.d.ts +17 -15
- package/dist/task/mid-run-input.js +17 -15
- package/dist/task/orchestrator.d.ts +24 -28
- package/dist/task/orchestrator.js +89 -66
- package/dist/task/orientation.d.ts +18 -23
- package/dist/task/orientation.js +24 -31
- package/dist/task/owned-freeze-conflict.d.ts +21 -20
- package/dist/task/owned-freeze-conflict.js +52 -85
- package/dist/task/owned-freeze-reassign.d.ts +40 -60
- package/dist/task/owned-freeze-reassign.js +41 -61
- package/dist/task/parsers.d.ts +4 -2
- package/dist/task/parsers.js +4 -4
- package/dist/task/phases.d.ts +41 -48
- package/dist/task/phases.js +196 -252
- package/dist/task/plan-io.d.ts +6 -7
- package/dist/task/plan-io.js +6 -7
- package/dist/task/plan-orchestrator.d.ts +10 -8
- package/dist/task/plan-orchestrator.js +14 -10
- package/dist/task/plan-prompts.d.ts +6 -5
- package/dist/task/plan-prompts.js +6 -5
- package/dist/task/plan-readonly.d.ts +4 -5
- package/dist/task/plan-readonly.js +4 -5
- package/dist/task/plan-rounds.d.ts +17 -29
- package/dist/task/plan-rounds.js +21 -34
- package/dist/task/plan-session.d.ts +58 -72
- package/dist/task/plan-session.js +61 -83
- package/dist/task/probe-gaming.d.ts +28 -27
- package/dist/task/probe-gaming.js +0 -0
- package/dist/task/prohibition-probe.d.ts +14 -16
- package/dist/task/prompts.d.ts +3 -4
- package/dist/task/prompts.js +17 -26
- package/dist/task/qa-transcript.d.ts +15 -22
- package/dist/task/qa-transcript.js +15 -21
- package/dist/task/question-box.d.ts +17 -13
- package/dist/task/question-box.js +19 -15
- package/dist/task/question-dedup.d.ts +6 -7
- package/dist/task/question-dedup.js +13 -14
- package/dist/task/question-dialog.d.ts +22 -32
- package/dist/task/question-dialog.js +22 -32
- package/dist/task/question-source.d.ts +18 -44
- package/dist/task/question-source.js +22 -51
- package/dist/task/refuted-constraint.d.ts +11 -31
- package/dist/task/refuted-constraint.js +27 -51
- package/dist/task/regenerable-artifacts.d.ts +12 -31
- package/dist/task/regenerable-artifacts.js +12 -31
- package/dist/task/render-check.d.ts +11 -22
- package/dist/task/render-check.js +33 -46
- package/dist/task/repo-health-check.d.ts +10 -14
- package/dist/task/repo-health-check.js +17 -23
- package/dist/task/requirements.d.ts +38 -71
- package/dist/task/requirements.js +78 -126
- package/dist/task/research-fanout-budget.d.ts +51 -88
- package/dist/task/research-fanout-budget.js +51 -88
- package/dist/task/research-worker.d.ts +29 -39
- package/dist/task/research-worker.js +37 -61
- package/dist/task/resume-gap.d.ts +14 -15
- package/dist/task/root-cause-repair.d.ts +9 -9
- package/dist/task/root-cause-repair.js +28 -40
- package/dist/task/run-bracket.d.ts +10 -13
- package/dist/task/run-end.d.ts +12 -22
- package/dist/task/run-end.js +8 -16
- package/dist/task/run-final-gate.d.ts +19 -21
- package/dist/task/run-final-gate.js +62 -80
- package/dist/task/runner-globs.d.ts +12 -13
- package/dist/task/runner-globs.js +12 -13
- package/dist/task/runner-resolve.d.ts +9 -9
- package/dist/task/runner-resolve.js +22 -23
- package/dist/task/script-escape.d.ts +10 -12
- package/dist/task/script-escape.js +13 -14
- package/dist/task/serve-entry.d.ts +1 -1
- package/dist/task/serve-entry.js +22 -25
- package/dist/task/service-blocks.js +4 -2
- package/dist/task/shipped-source.d.ts +11 -29
- package/dist/task/shipped-source.js +11 -29
- package/dist/task/skip-escape.js +10 -14
- package/dist/task/spec-urls.d.ts +26 -65
- package/dist/task/spec-urls.js +26 -65
- package/dist/task/spec-validation.d.ts +17 -20
- package/dist/task/spec-validation.js +17 -20
- package/dist/task/stall-detector.d.ts +23 -30
- package/dist/task/stall-detector.js +23 -30
- package/dist/task/stream-watchdog.d.ts +14 -12
- package/dist/task/stream-watchdog.js +14 -12
- package/dist/task/substitution-probe.d.ts +17 -20
- package/dist/task/substitution-probe.js +17 -20
- package/dist/task/task-gates.d.ts +36 -41
- package/dist/task/task-gates.js +95 -106
- package/dist/task/task-io.d.ts +4 -4
- package/dist/task/task-io.js +4 -4
- package/dist/task/task-parsers.js +4 -3
- package/dist/task/task-provenance.d.ts +2 -2
- package/dist/task/task-provenance.js +11 -13
- package/dist/task/task-types.d.ts +4 -3
- package/dist/task/terminal-outcome.d.ts +14 -16
- package/dist/task/terminal-outcome.js +12 -14
- package/dist/task/test-assembly.d.ts +13 -20
- package/dist/task/test-assembly.js +13 -20
- package/dist/task/timings.d.ts +5 -3
- package/dist/task/timings.js +5 -3
- package/dist/task/title-label.d.ts +9 -4
- package/dist/task/title-label.js +9 -4
- package/dist/task/type-only-answer.d.ts +44 -52
- package/dist/task/type-only-answer.js +44 -52
- package/dist/task/unfailable-command.d.ts +18 -24
- package/dist/task/unfailable-command.js +21 -27
- package/dist/task/unknown-routing.d.ts +10 -4
- package/dist/task/unknown-routing.js +10 -4
- package/dist/task/user-directives.d.ts +5 -8
- package/dist/task/user-directives.js +5 -8
- package/dist/task/verify-quality.d.ts +18 -22
- package/dist/task/verify-quality.js +45 -46
- package/dist/task/verify-reconcile.d.ts +15 -10
- package/dist/task/verify-reconcile.js +45 -43
- package/dist/task/verify-resolution.d.ts +24 -20
- package/dist/task/verify-resolution.js +51 -50
- package/dist/task/verify-work.d.ts +59 -66
- package/dist/task/verify-work.js +101 -138
- package/dist/task/widget.d.ts +15 -14
- package/dist/task/widget.js +22 -17
- package/dist/task/wiring-claims.d.ts +25 -32
- package/dist/task/wiring-claims.js +30 -35
- package/dist/task/write-guard.d.ts +39 -39
- package/dist/task/write-guard.js +48 -51
- package/dist/task/yolo.d.ts +34 -30
- package/dist/task/yolo.js +42 -37
- package/dist/workers/abstention.d.ts +21 -41
- package/dist/workers/abstention.js +27 -48
- package/dist/workers/brave-search.d.ts +4 -3
- package/dist/workers/brave-search.js +5 -2
- package/dist/workers/brave-warning.d.ts +7 -4
- package/dist/workers/brave-warning.js +19 -7
- package/dist/workers/ddg-search.d.ts +6 -6
- package/dist/workers/ddg-search.js +18 -12
- package/dist/workers/docs-cache.js +5 -2
- package/dist/workers/docs-chunk.d.ts +30 -37
- package/dist/workers/docs-chunk.js +37 -41
- package/dist/workers/docs-core.d.ts +28 -44
- package/dist/workers/docs-core.js +25 -44
- package/dist/workers/docs-index.js +4 -3
- package/dist/workers/docs-lookup.d.ts +15 -22
- package/dist/workers/docs-lookup.js +12 -21
- package/dist/workers/docs-project.d.ts +15 -9
- package/dist/workers/docs-project.js +17 -10
- package/dist/workers/docs-resolve.d.ts +19 -20
- package/dist/workers/docs-resolve.js +35 -32
- package/dist/workers/docs-retrieve.d.ts +5 -6
- package/dist/workers/docs-retrieve.js +18 -15
- package/dist/workers/exa-search.d.ts +9 -6
- package/dist/workers/exa-search.js +23 -12
- package/dist/workers/fetch-core.d.ts +13 -16
- package/dist/workers/fetch-core.js +23 -23
- package/dist/workers/focused-extractor.d.ts +13 -12
- package/dist/workers/focused-extractor.js +27 -19
- package/dist/workers/html-clean.js +24 -14
- package/dist/workers/http-request.d.ts +28 -20
- package/dist/workers/http-request.js +22 -17
- package/dist/workers/npm-version.d.ts +28 -11
- package/dist/workers/npm-version.js +24 -15
- package/dist/workers/phantom-imports.d.ts +15 -12
- package/dist/workers/phantom-imports.js +30 -24
- package/dist/workers/pi-worker-core.d.ts +65 -96
- package/dist/workers/pi-worker-core.js +93 -181
- package/dist/workers/pi-worker-docs.d.ts +24 -19
- package/dist/workers/pi-worker-docs.js +67 -76
- package/dist/workers/pi-worker-fetch.d.ts +7 -3
- package/dist/workers/pi-worker-fetch.js +27 -19
- package/dist/workers/pi-worker-search.js +12 -8
- package/dist/workers/pi-worker.d.ts +9 -4
- package/dist/workers/pi-worker.js +21 -14
- package/dist/workers/reasoning-warning.d.ts +18 -17
- package/dist/workers/reasoning-warning.js +22 -20
- package/dist/workers/research-cache.js +50 -78
- package/dist/workers/search-core.js +7 -5
- package/dist/workers/search-types.d.ts +10 -9
- package/dist/workers/search-types.js +9 -8
- package/dist/workers/session-hint.d.ts +13 -14
- package/dist/workers/session-hint.js +8 -9
- package/dist/workers/shared.d.ts +21 -25
- package/dist/workers/shared.js +0 -0
- package/dist/workers/single-read-extension.d.ts +14 -7
- package/dist/workers/single-read-extension.js +14 -7
- package/dist/workers/single-read-guard.d.ts +27 -30
- package/dist/workers/single-read-guard.js +36 -36
- package/dist/workers/typeonly-log.d.ts +12 -9
- package/dist/workers/typeonly-log.js +29 -33
- package/dist/workers/worker-channels.d.ts +15 -23
- package/dist/workers/worker-channels.js +15 -23
- package/dist/workers/worker-failure.d.ts +38 -46
- package/dist/workers/worker-failure.js +31 -39
- package/dist/workers/worker-kill.d.ts +25 -26
- package/dist/workers/worker-kill.js +16 -19
- package/dist/workers/worker-profiles.d.ts +54 -56
- package/dist/workers/worker-profiles.js +63 -39
- package/package.json +10 -8
|
@@ -9,41 +9,37 @@ import { spawn } from 'node:child_process';
|
|
|
9
9
|
import { getPiInvocation } from '../shared/pi-invocation.js';
|
|
10
10
|
import { runChild as runChildUnified } from '../shared/child-process.js';
|
|
11
11
|
import { childBaseArgs } from '../shared/child-extensions.js';
|
|
12
|
-
import { LoopDetector } from './loop-detector.js';
|
|
12
|
+
import { LoopDetector, MAX_LOOP_RESTARTS } from './loop-detector.js';
|
|
13
13
|
import { StallDetector, formatStallHint } from './stall-detector.js';
|
|
14
14
|
import { detectLeakedToolCall, leakedToolCallHint, MAX_LEAK_RETRIES } from '../shared/leaked-tool-call.js';
|
|
15
15
|
import { readSection, setTaskSection } from './task-io.js';
|
|
16
16
|
import { streamStallCause } from '../shared/stream-watchdog.js';
|
|
17
|
+
import { commandCeilingForAttempt, commandTimeoutHint, commandWatch } from '../shared/command-watchdog.js';
|
|
18
|
+
import { discoverModelEndpoints, probeModelEndpoints } from '../shared/model-endpoint.js';
|
|
19
|
+
// VALUE import, and it is only safe because worker-profiles.ts reads its loop
|
|
20
|
+
// constants from loop-detector.ts. Point those back at this file and the graph
|
|
21
|
+
// closes into a TDZ ReferenceError that no compile step catches.
|
|
22
|
+
import { workerPolicy } from '../workers/worker-profiles.js';
|
|
17
23
|
import { getConfig } from '../config/config.js';
|
|
18
24
|
import { groupThinkingArgs } from '../config/reasoning-args.js';
|
|
19
25
|
import { reasoningGroupForChild } from '../config/reasoning.js';
|
|
20
|
-
// ─── Loop detection constants ────────────────────────────────────────────────
|
|
21
|
-
// Defined here (not in phases.ts) to avoid a circular dependency:
|
|
22
|
-
// phases.ts → child-runner.ts → phases.ts
|
|
23
|
-
export const LOOP_WINDOW = 20;
|
|
24
|
-
export const LOOP_THRESHOLD = 5;
|
|
25
|
-
export const MAX_LOOP_RESTARTS = 2; // 3 strikes total (initial attempt + 2 restarts)
|
|
26
|
-
// MAX_LEAK_RETRIES lives in shared/leaked-tool-call.ts (imported above).
|
|
27
26
|
// ─── Phase-child wall-clock cap ──────────────────────────────────────────────
|
|
28
27
|
/**
|
|
29
28
|
* Optional wall-clock bound on ONE spawn of a phase child. DEFAULT: OFF.
|
|
30
29
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* WHY OFF, AND NOT A NUMBER. A wall clock on a model child measures the
|
|
31
|
+
* MODEL'S SPEED, not its health. The same planning child that answers well in
|
|
32
|
+
* seconds on one backend takes many minutes on another, or on the same backend
|
|
33
|
+
* with thinking turned on — so any cap generous enough to be safe is too loose
|
|
34
|
+
* to catch anything, and any cap tight enough to catch a runaway kills healthy
|
|
35
|
+
* work. Assume a model that emits one token per second and the number has no
|
|
36
|
+
* defensible value at all.
|
|
34
37
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* pathology, it was measuring one model's speed on one day.
|
|
41
|
-
*
|
|
42
|
-
* The runaway it was there to catch — a decompose child that ran 16m23s at
|
|
43
|
-
* 117,370 of a 120,064-token window, forward-paging past the loop detector — is
|
|
44
|
-
* now caught by StallDetector (stall-detector.ts), which bounds NON-PROGRESS and
|
|
45
|
-
* CONTEXT CHURN instead of elapsed seconds. Both of those are properties of the
|
|
46
|
-
* pathology, so neither has to be re-tuned for a slower model or a bigger repo.
|
|
38
|
+
* The runaway it was there to catch — a child forward-paging through its whole
|
|
39
|
+
* context window, past the loop detector, never going to return — is caught by
|
|
40
|
+
* StallDetector (stall-detector.ts) instead. That bounds NON-PROGRESS and
|
|
41
|
+
* CONTEXT CHURN, both properties of the pathology itself, so neither has to be
|
|
42
|
+
* re-tuned for a slower model or a bigger repo.
|
|
47
43
|
*
|
|
48
44
|
* The value and the plumbing stay for a caller that genuinely wants a hard stop
|
|
49
45
|
* (tests inject a short one), but nothing sets it in production. Pass
|
|
@@ -108,6 +104,75 @@ export class PhaseTimeoutError extends Error {
|
|
|
108
104
|
this.name = 'PhaseTimeoutError';
|
|
109
105
|
}
|
|
110
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The terminal error for a guard kill, or null when the child was not killed.
|
|
109
|
+
*
|
|
110
|
+
* Both spawn paths must ask. A kill reports `exitCode: 0` (child-process.ts uses
|
|
111
|
+
* `code ?? 0`, and a signal gives null), so a path that tests the exit code
|
|
112
|
+
* instead returns the truncated text as the phase's answer.
|
|
113
|
+
*/
|
|
114
|
+
export function guardKillError(name, r, opts = {}) {
|
|
115
|
+
if (r.commandKill)
|
|
116
|
+
return new CommandTimeoutError(name, r.commandKill);
|
|
117
|
+
// A dead-backend verdict is only trusted once every attempt has produced it.
|
|
118
|
+
// `discoverModelEndpoints` reads EVERY provider in models.json, not the one
|
|
119
|
+
// this child's model uses, so a stopped local server can condemn a run against
|
|
120
|
+
// a healthy cloud backend. The asymmetry settles it: a backend that really is
|
|
121
|
+
// down costs three 5s probes, a wrong verdict costs the whole run.
|
|
122
|
+
if (r.stalled)
|
|
123
|
+
return opts.finalAttempt === false ? null : new BackendDownError(name);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The dead-backend probe killed a phase child on its LAST attempt.
|
|
128
|
+
*
|
|
129
|
+
* Reaching this means every attempt found no endpoint answering, not one. The
|
|
130
|
+
* single-probe verdict is not trusted on its own: `discoverModelEndpoints` reads
|
|
131
|
+
* every provider in models.json rather than the one this child's model uses, so a
|
|
132
|
+
* stopped local server can condemn a run against a healthy cloud backend. Three
|
|
133
|
+
* failed probes cost ~15s; one wrong verdict costs the run.
|
|
134
|
+
*/
|
|
135
|
+
export class BackendDownError extends Error {
|
|
136
|
+
childName;
|
|
137
|
+
constructor(childName) {
|
|
138
|
+
super(`${childName} child killed: no output for the stall window and the model `
|
|
139
|
+
+ `endpoint did not answer a probe`);
|
|
140
|
+
this.childName = childName;
|
|
141
|
+
this.name = 'BackendDownError';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A phase child spent every attempt on a command that never returned. Its own
|
|
146
|
+
* class because the fix is in the SPEC, not the model's exploration: a VERIFY
|
|
147
|
+
* block naming an unbounded `dev` command re-hangs every attempt.
|
|
148
|
+
*/
|
|
149
|
+
export class CommandTimeoutError extends Error {
|
|
150
|
+
childName;
|
|
151
|
+
kill;
|
|
152
|
+
constructor(childName, kill) {
|
|
153
|
+
super(`${childName} child ran \`${kill.toolName}\``
|
|
154
|
+
+ `${kill.detail ? ` (${kill.detail})` : ''} past its `
|
|
155
|
+
+ `${Math.round(kill.timeoutMs / 1000)}s ceiling on every attempt`);
|
|
156
|
+
this.childName = childName;
|
|
157
|
+
this.kill = kill;
|
|
158
|
+
this.name = 'CommandTimeoutError';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Causes a best-effort `catch` must NOT absorb.
|
|
163
|
+
*
|
|
164
|
+
* A phase child that merely answered badly should degrade — that is what those
|
|
165
|
+
* catches are for. These two are different in kind: the run is over either way,
|
|
166
|
+
* and swallowing them ships a half-built spec while every later phase dies
|
|
167
|
+
* against the same dead backend, or turns a user's ESC into silent progress.
|
|
168
|
+
* `failure-classifier.ts` has a verdict for both; a catch that eats them makes it
|
|
169
|
+
* unreachable.
|
|
170
|
+
*/
|
|
171
|
+
export function isFatalChildCause(e) {
|
|
172
|
+
if (e instanceof BackendDownError)
|
|
173
|
+
return true;
|
|
174
|
+
return e instanceof Error && e.message === USER_CANCELLED;
|
|
175
|
+
}
|
|
111
176
|
// ─── Connection-error retry ──────────────────────────────────────────────────
|
|
112
177
|
/**
|
|
113
178
|
* A connection-class model error is transient: a single dropped fetch to a live
|
|
@@ -123,9 +188,54 @@ export class PhaseTimeoutError extends Error {
|
|
|
123
188
|
* provider 5xx that names a real fault) still fails fast: re-spawning against
|
|
124
189
|
* the same request won't fix it, so burning the budget only delays the report.
|
|
125
190
|
*/
|
|
126
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Transport-level failures worth another attempt.
|
|
193
|
+
*
|
|
194
|
+
* SCOPE, and it is deliberate: connection classes only. pi's own
|
|
195
|
+
* `isRetryableAssistantError` (@earendil-works/pi-ai, `dist/utils/retry.js`) also
|
|
196
|
+
* retries the provider-LOAD family — `429`, `5xx`, `rate limit`, `overloaded` —
|
|
197
|
+
* which `does NOT match real, non-transient faults` in child-runner.test.ts
|
|
198
|
+
* explicitly rejects. That disagreement is real and OPEN; it is not settled here,
|
|
199
|
+
* because this backoff starts at 500ms and a 429 answered that fast is a retry
|
|
200
|
+
* storm, not a recovery.
|
|
201
|
+
*
|
|
202
|
+
* MEASURED against pi before widening: the transport entries added here — a bare
|
|
203
|
+
* `timed out`, `getaddrinfo ENOTFOUND`, `upstream connect`, `reset before
|
|
204
|
+
* headers`, a truncated Anthropic stream and a closed websocket — were all
|
|
205
|
+
* MISSES. Every one is a REMOTE-provider failure, which is why a local llama.cpp
|
|
206
|
+
* setup never surfaced the gap. The errno spellings are pi-task's own: a child
|
|
207
|
+
* reports them through stderr, and pi never sees them.
|
|
208
|
+
*
|
|
209
|
+
* pi's bare `timeout` is deliberately NOT reproduced. It matched a provider 400
|
|
210
|
+
* that merely echoed a `timeout` field back, turning a fail-fast into a full
|
|
211
|
+
* retry budget, and it caught nothing the `timed out` spellings above miss.
|
|
212
|
+
*/
|
|
213
|
+
const CONNECTION_ERROR_RE = /\b(?:connection error|connection (?:lost|closed|reset|refused|aborted)|econnreset|econnrefused|econnaborted|epipe|etimedout|enetunreach|enetdown|eai_again|socket hang up|socket connection was closed|fetch failed|network (?:error|timeout)|premature close|terminated|unreachable|getaddrinfo|enotfound|upstream.?connect|reset before headers|timed? out|ended without|stream ended before message_stop|websocket.?(?:closed|error))\b/i;
|
|
214
|
+
/**
|
|
215
|
+
* Provider LOAD, which is transient in a different way: the server is up and
|
|
216
|
+
* saying "not now". pi retries all of these; 53f0488 did not, but its own message
|
|
217
|
+
* names only "context overflow, bad request, auth" as the fail-fast set — a
|
|
218
|
+
* throttle was never argued for, it just rode along in a list written for a LOCAL
|
|
219
|
+
* server, where none of these can occur.
|
|
220
|
+
*
|
|
221
|
+
* Words carry no trailing \b (`overloaded_error` joins on `_`, which is a word
|
|
222
|
+
* character); the bare status codes carry one, or `500` matches inside `15000`.
|
|
223
|
+
*/
|
|
224
|
+
const PROVIDER_LOAD_RE = /(?:overloaded|rate.?limit|too many requests|service.?unavailable|server.?error|internal.?error|provider.?returned.?error)|\b(?:429|500|502|503|504|524)\b/i;
|
|
225
|
+
/**
|
|
226
|
+
* Account facts, not liveness: a budget does not refill on a retry. Checked FIRST,
|
|
227
|
+
* because these arrive worded as a throttle — `429 GoUsageLimitError` is a
|
|
228
|
+
* subscription limit, not a queue.
|
|
229
|
+
*/
|
|
230
|
+
const NON_RETRYABLE_RE = /\b(?:insufficient_quota|quota exceeded|out of budget|billing|usage limit reached|available balance|GoUsageLimitError|FreeUsageLimitError)\b/i;
|
|
231
|
+
/**
|
|
232
|
+
* Retry budget is three attempts at 500ms/1s/2s — three requests over 3.5s, which
|
|
233
|
+
* is not a storm even against a throttle. pi's own ladder is three at 2s/4s/8s.
|
|
234
|
+
*/
|
|
127
235
|
export function isConnectionError(cause) {
|
|
128
|
-
|
|
236
|
+
if (NON_RETRYABLE_RE.test(cause))
|
|
237
|
+
return false;
|
|
238
|
+
return CONNECTION_ERROR_RE.test(cause) || PROVIDER_LOAD_RE.test(cause);
|
|
129
239
|
}
|
|
130
240
|
/** Exponential backoff before a connection-error retry: 500ms, 1s, 2s, …, so a
|
|
131
241
|
* brief saturation window can drain before we re-issue the request. */
|
|
@@ -145,8 +255,8 @@ thinking = []) {
|
|
|
145
255
|
// `--mode json` puts the child into the structured event stream the
|
|
146
256
|
// unified runner parses in `mode: 'json-events'`. Without it the child
|
|
147
257
|
// emits plain text, every line fails JSON.parse, finalText stays empty,
|
|
148
|
-
// and every phase fails with "X child produced no output".
|
|
149
|
-
// dropped
|
|
258
|
+
// and every phase fails with "X child produced no output". A refactor has
|
|
259
|
+
// dropped it once already; do not remove it again.
|
|
150
260
|
//
|
|
151
261
|
// An empty `tools` string means "no tools at all" — emit `--no-tools`
|
|
152
262
|
// instead of `--tools ''` (which pi would reject). Used by pure-judgment
|
|
@@ -154,8 +264,9 @@ thinking = []) {
|
|
|
154
264
|
// hand them, never spend time reading the repo.
|
|
155
265
|
//
|
|
156
266
|
// The prompt is NOT an argv element: it goes to the child over stdin (see
|
|
157
|
-
// runChild below / getPiInvocation), so a large inlined-design prompt
|
|
158
|
-
//
|
|
267
|
+
// runChild below / getPiInvocation), so a large inlined-design prompt cannot
|
|
268
|
+
// exceed the OS argv ceiling — which fails the spawn outright rather than
|
|
269
|
+
// truncating (`E2BIG` on this platform).
|
|
159
270
|
//
|
|
160
271
|
// `extensions` are internal `-e` loads for in-run guards (the caller supplies
|
|
161
272
|
// the path). A no-tools child cannot make a tool call, so it never carries
|
|
@@ -167,30 +278,75 @@ thinking = []) {
|
|
|
167
278
|
// Sentinel error thrown when the user dismisses a grill-me dialog.
|
|
168
279
|
// Defined here (not in failure-classifier.ts) to avoid circular dependency.
|
|
169
280
|
export const USER_CANCELLED = '__user_cancelled__';
|
|
170
|
-
|
|
281
|
+
/**
|
|
282
|
+
* The `phase` row of WORKER_PROFILES, resolved with this machine's config.
|
|
283
|
+
*
|
|
284
|
+
* Read here rather than at module load so a /task-config change reaches the next
|
|
285
|
+
* child, the same contract childBaseArgs already keeps. Both spawn paths in this
|
|
286
|
+
* file go through it, so the degraded final attempt cannot drift from the ordinary
|
|
287
|
+
* one — the mislabel class runDegradedFinalAttempt's own comment warns about.
|
|
288
|
+
*/
|
|
289
|
+
export function phasePolicy() {
|
|
290
|
+
return workerPolicy('phase', {
|
|
291
|
+
commandTimeoutMs: getConfig().requestTimeoutMs,
|
|
292
|
+
streamInactivityMs: getConfig().streamInactivityMs
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
export async function runChild({ cwd, tools, prompt, signal, onLine, onContextUsage, onToolCall, spawn: spawnFn, extensions, onToolResult, contextWindow, thinking, commandCeilingMs }) {
|
|
171
296
|
const invocation = getPiInvocation(childArgs(tools, extensions, thinking), prompt);
|
|
172
297
|
let loopHit;
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
298
|
+
const guards = phasePolicy().guards;
|
|
299
|
+
// Null when the user set the ceiling to `off`. Why a phase child needs this at
|
|
300
|
+
// all is the `phase` row's `why` in worker-profiles.ts.
|
|
301
|
+
const cmdWatch = commandWatch(commandCeilingMs ?? guards['command-timeout']);
|
|
302
|
+
const childSignal = cmdWatch ? AbortSignal.any([signal, cmdWatch.signal]) : signal;
|
|
303
|
+
let result;
|
|
304
|
+
try {
|
|
305
|
+
result = await runChildUnified(spawnFn ?? spawn, invocation, cwd, childSignal, {
|
|
306
|
+
mode: 'json-events',
|
|
307
|
+
// A hung model stream reports nothing at all, so without this the
|
|
308
|
+
// phase child waits forever. The kill
|
|
309
|
+
// is reported below as a connection-class cause, which routes it into
|
|
310
|
+
// the retry/backoff path this file already has for a LOUD disconnect.
|
|
311
|
+
streamInactivityMs: guards['stream-stall'],
|
|
312
|
+
...(guards.stalled === false ?
|
|
313
|
+
{}
|
|
314
|
+
: {
|
|
315
|
+
stall: {
|
|
316
|
+
afterMs: guards.stalled.afterMs,
|
|
317
|
+
probe: guards.stalled.probe
|
|
318
|
+
?? (() => probeModelEndpoints(discoverModelEndpoints()))
|
|
319
|
+
}
|
|
320
|
+
}),
|
|
321
|
+
onLine,
|
|
322
|
+
onContextUsage,
|
|
323
|
+
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
324
|
+
// ALWAYS wired, never conditional on the caller wanting results:
|
|
325
|
+
// the sink emits a tool-execution-end only when a handler exists,
|
|
326
|
+
// and without that end the command watchdog's timer is never
|
|
327
|
+
// disarmed — every healthy tool call would then look hung.
|
|
328
|
+
onToolResult: r => {
|
|
329
|
+
cmdWatch?.onEnd(r.toolCallId);
|
|
330
|
+
onToolResult?.(r.text, r.isError);
|
|
331
|
+
},
|
|
332
|
+
onToolCall: call => {
|
|
333
|
+
// Before the detectors: a call they let through still needs its
|
|
334
|
+
// clock started.
|
|
335
|
+
cmdWatch?.onStart(call);
|
|
336
|
+
if (!onToolCall)
|
|
337
|
+
return null;
|
|
338
|
+
const hit = onToolCall(call);
|
|
339
|
+
if (hit && !loopHit) {
|
|
340
|
+
loopHit = hit;
|
|
341
|
+
}
|
|
342
|
+
return hit; // propagate to unified runner so it can kill
|
|
190
343
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
cmdWatch?.clear();
|
|
348
|
+
}
|
|
349
|
+
const commandKill = cmdWatch?.killed();
|
|
194
350
|
// Use `||` (not `??`) so an empty string from json-events mode falls
|
|
195
351
|
// back to raw stdout. Without this, a child that exits 0 but emits no
|
|
196
352
|
// assistant text (e.g. model API error swallowed in json mode) always
|
|
@@ -204,14 +360,16 @@ export async function runChild({ cwd, tools, prompt, signal, onLine, onContextUs
|
|
|
204
360
|
?? (result.streamStalled ? streamStallCause(result.streamStalled.idleMs) : undefined);
|
|
205
361
|
return {
|
|
206
362
|
text,
|
|
207
|
-
// WE killed this child, so its exit status
|
|
208
|
-
// the
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
exitCode: result.streamStalled ? 0 : result.exitCode,
|
|
363
|
+
// WE killed this child, so its exit status is our own SIGTERM. Report 0
|
|
364
|
+
// and let the named cause carry it. EVERY guard kill must be listed: one
|
|
365
|
+
// omitted here arrives as exit 0 with partial text, and triageChildResult
|
|
366
|
+
// returns it as a successful answer.
|
|
367
|
+
exitCode: result.streamStalled || result.stalled || commandKill ? 0 : result.exitCode,
|
|
212
368
|
stderr: result.stderr.trim(),
|
|
213
369
|
loopHit,
|
|
214
370
|
modelError,
|
|
371
|
+
...(commandKill ? { commandKill } : {}),
|
|
372
|
+
...(result.stalled ? { stalled: true } : {}),
|
|
215
373
|
// A tool call the model wrote as text (wrong dialect) never executed and
|
|
216
374
|
// sailed past the structured-event guards above; flag it so the wrappers
|
|
217
375
|
// can re-prompt instead of accepting the unexecuted call. Only meaningful
|
|
@@ -226,9 +384,8 @@ export async function runChild({ cwd, tools, prompt, signal, onLine, onContextUs
|
|
|
226
384
|
* verdict, so a fix to any rung lands in every caller at once.
|
|
227
385
|
*
|
|
228
386
|
* `attempt` is the caller's 0-based counter, `budget` the matching restart
|
|
229
|
-
* allowance
|
|
230
|
-
*
|
|
231
|
-
* loop now) — so a phase runs `budget + 1` attempts before a rung gives up.
|
|
387
|
+
* allowance — MAX_LEAK_RETRIES and MAX_LOOP_RESTARTS are both 2, and one loop
|
|
388
|
+
* spends the pair — so a phase runs `budget + 1` attempts before a rung gives up.
|
|
232
389
|
*
|
|
233
390
|
* `verb` names the restart in the debug log ("retry" by default, "restart" for
|
|
234
391
|
* refine and grill-gen). It is the only externally visible thing that differed
|
|
@@ -240,7 +397,11 @@ export async function runChild({ cwd, tools, prompt, signal, onLine, onContextUs
|
|
|
240
397
|
*/
|
|
241
398
|
async function triageChildResult(deps, name, r, attempt, budget, verb) {
|
|
242
399
|
if (r.exitCode !== 0) {
|
|
243
|
-
|
|
400
|
+
// The exit code is the only signal when stderr is empty, and pi exits
|
|
401
|
+
// silently on several paths (143 = SIGTERM/loop-kill, 137 = SIGKILL/OOM).
|
|
402
|
+
// Dropping it left "(no stderr)" as the whole diagnosis.
|
|
403
|
+
const why = r.stderr || `no stderr, exit ${r.exitCode}`;
|
|
404
|
+
throw new Error(`${name} child failed: ${why}`);
|
|
244
405
|
}
|
|
245
406
|
if (r.modelError) {
|
|
246
407
|
// The model/provider failed (pi exited 0 with a stopReason "error"
|
|
@@ -286,16 +447,16 @@ async function triageChildResult(deps, name, r, attempt, budget, verb) {
|
|
|
286
447
|
*
|
|
287
448
|
* THREE RUNAWAY GUARDS ride the same budget, because this is the runner every
|
|
288
449
|
* /task-auto planning child goes through (clarify, decompose, coverage,
|
|
289
|
-
* contract-extract) and
|
|
450
|
+
* contract-extract), and an unguarded planning child can burn a whole run:
|
|
290
451
|
* • a LoopDetector, so an identical repeated tool call is killed and
|
|
291
452
|
* re-prompted instead of being allowed to fill the context window;
|
|
292
453
|
* • a StallDetector, the backstop for the varied-args thrash the loop
|
|
293
|
-
* detector's short window cannot see —
|
|
294
|
-
*
|
|
454
|
+
* detector's short window cannot see — a child that keeps calling tools with
|
|
455
|
+
* different arguments, learns nothing, and is never going to return. It bounds
|
|
295
456
|
* consecutive no-new-ground calls and total context churn, NOT elapsed time;
|
|
296
|
-
* • PHASE_CHILD_TIMEOUT_MS, a hard wall clock, OFF by default
|
|
297
|
-
*
|
|
298
|
-
*
|
|
457
|
+
* • PHASE_CHILD_TIMEOUT_MS, a hard wall clock, OFF by default: a healthy
|
|
458
|
+
* reasoning-on planning child and a runaway one occupy the same range of
|
|
459
|
+
* elapsed times, so no threshold separates them. See its comment.
|
|
299
460
|
* All three are checked BEFORE the triage ladder: we killed the child, so its
|
|
300
461
|
* exit status describes our SIGTERM and says nothing about its verdict.
|
|
301
462
|
*/
|
|
@@ -345,17 +506,29 @@ export async function runPhaseChild(deps, name, tools, prompt, opts = {}) {
|
|
|
345
506
|
let hint = null;
|
|
346
507
|
const loopHistory = [];
|
|
347
508
|
const budgetMs = deps.timeoutMs ?? PHASE_CHILD_TIMEOUT_MS;
|
|
509
|
+
// From the `phase` row, not from literals here, so the table is the one place
|
|
510
|
+
// that answers "how may this child die". The row resolves to the same
|
|
511
|
+
// DEFAULT_LOOP_DETECTOR / DEFAULT_LOOP_PROGRESS this file used to hard-code.
|
|
512
|
+
const loopGuard = phasePolicy().guards.loop;
|
|
513
|
+
// Watchdog kills, NOT total strikes: a loop restart never saw the
|
|
514
|
+
// bound-your-command hint. Without the halving, three attempts at the default
|
|
515
|
+
// ceiling cost 45 minutes and nothing else bounds this path.
|
|
516
|
+
let hangKills = 0;
|
|
348
517
|
for (let attempt = 0; attempt <= MAX_LEAK_RETRIES; attempt++) {
|
|
349
518
|
// A cancel between attempts must not buy another spawn.
|
|
350
519
|
if (deps.signal.aborted)
|
|
351
520
|
throw new Error(USER_CANCELLED);
|
|
352
|
-
const detector =
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
521
|
+
const detector = loopGuard.detector === false ?
|
|
522
|
+
null
|
|
523
|
+
: new LoopDetector(loopGuard.detector.window, loopGuard.detector.threshold, loopGuard.detector.pathThreshold);
|
|
524
|
+
const stall = loopGuard.progress === false ?
|
|
525
|
+
null
|
|
526
|
+
: new StallDetector(loopGuard.progress.limit, loopGuard.progress.churnFactor);
|
|
527
|
+
// Arm the churn rule BEFORE the first tool call. pi's stream carries no
|
|
528
|
+
// context WINDOW, so a detector that waited to be told one would sit at 0,
|
|
529
|
+
// and the churn rule returns false on a non-positive window. The parent
|
|
530
|
+
// knows the value at spawn time — say it then, not later.
|
|
531
|
+
stall?.noteContext(deps.contextWindow ?? 0);
|
|
359
532
|
const clock = phaseTimeout(deps.signal, budgetMs);
|
|
360
533
|
let r;
|
|
361
534
|
try {
|
|
@@ -365,14 +538,16 @@ export async function runPhaseChild(deps, name, tools, prompt, opts = {}) {
|
|
|
365
538
|
signal: clock.signal,
|
|
366
539
|
thinking,
|
|
367
540
|
onContextUsage: snapshot => {
|
|
368
|
-
// Real window or nothing: noteContext ignores 0,
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
|
|
541
|
+
// Real window or nothing: `noteContext` ignores 0, which is
|
|
542
|
+
// why the parent's value must be supplied at spawn — a
|
|
543
|
+
// stream that only ever reports 0 leaves the churn rule
|
|
544
|
+
// permanently disarmed.
|
|
545
|
+
stall?.noteContext(snapshot.contextWindow);
|
|
372
546
|
deps.onContextUsage?.(snapshot);
|
|
373
547
|
},
|
|
374
|
-
|
|
375
|
-
|
|
548
|
+
commandCeilingMs: commandCeilingForAttempt(phasePolicy().guards['command-timeout'], hangKills),
|
|
549
|
+
onToolCall: call => detector?.record(call) ?? stall?.record(call) ?? null,
|
|
550
|
+
onToolResult: (text, isError) => stall?.noteResult(text, isError)
|
|
376
551
|
}));
|
|
377
552
|
}
|
|
378
553
|
finally {
|
|
@@ -402,6 +577,36 @@ export async function runPhaseChild(deps, name, tools, prompt, opts = {}) {
|
|
|
402
577
|
hint = r.loopHit.stall ? formatStallHint(r.loopHit.stall) : formatLoopHint(r.loopHit);
|
|
403
578
|
continue;
|
|
404
579
|
}
|
|
580
|
+
// Ordered after the loop rule and before the clock, matching RESTART_ORDER
|
|
581
|
+
// (worker-kill.ts): the loop hint names the offending call and is the more
|
|
582
|
+
// specific thing to tell a re-spawn, and a watchdog kill leaves the phase
|
|
583
|
+
// clock's own flag false, so the two cannot be confused.
|
|
584
|
+
if (r.commandKill) {
|
|
585
|
+
hangKills++;
|
|
586
|
+
if (attempt === MAX_LEAK_RETRIES) {
|
|
587
|
+
throw new CommandTimeoutError(name, r.commandKill);
|
|
588
|
+
}
|
|
589
|
+
deps.logDebug?.(`${name}: ${r.commandKill.toolName} outran `
|
|
590
|
+
+ `${Math.round(r.commandKill.timeoutMs / 1000)}s — `
|
|
591
|
+
+ `${verb} ${attempt + 1}/${MAX_LEAK_RETRIES}`);
|
|
592
|
+
// Tracks the TOOLS, not the phase: verify-tooling holds `read,bash`,
|
|
593
|
+
// and a half-written node_modules survives the kill.
|
|
594
|
+
hint = commandTimeoutHint(r.commandKill.toolName, r.commandKill.timeoutMs, {
|
|
595
|
+
...(r.commandKill.detail ? { commandDetail: r.commandKill.detail } : {}),
|
|
596
|
+
editsMayPersist: /\b(?:bash|edit|write)\b/.test(tools)
|
|
597
|
+
});
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
// A dead-backend kill spends a strike like the others; guardKillError says
|
|
601
|
+
// when the verdict has been earned.
|
|
602
|
+
const killed = guardKillError(name, r, { finalAttempt: attempt === MAX_LEAK_RETRIES });
|
|
603
|
+
if (killed)
|
|
604
|
+
throw killed;
|
|
605
|
+
if (r.stalled) {
|
|
606
|
+
deps.logDebug?.(`${name}: no output for the stall window and no endpoint answered — `
|
|
607
|
+
+ `${verb} ${attempt + 1}/${MAX_LEAK_RETRIES}`);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
405
610
|
if (clock.timedOut()) {
|
|
406
611
|
if (attempt === MAX_LEAK_RETRIES) {
|
|
407
612
|
throw new PhaseTimeoutError(name, budgetMs, MAX_LEAK_RETRIES + 1);
|
|
@@ -450,8 +655,8 @@ export function prependHint(hint, prompt) {
|
|
|
450
655
|
* Append one line to the task file's `loop events` section.
|
|
451
656
|
*
|
|
452
657
|
* Best-effort by contract: it runs for EVERY phase child now that there is one
|
|
453
|
-
* loop, and
|
|
454
|
-
*
|
|
658
|
+
* loop, and not every caller owns a task file on disk (a scripted harness, a
|
|
659
|
+
* bare unit deps bag). A trail
|
|
455
660
|
* that cannot be written must cost the phase nothing — the loop kill itself is
|
|
456
661
|
* already reported through the debug log and the thrown LoopExhaustedError.
|
|
457
662
|
*/
|
|
@@ -480,11 +685,9 @@ async function appendLoopEvent(cwd, taskId, phase, hit, strike, outcome) {
|
|
|
480
685
|
*/
|
|
481
686
|
async function runDegradedFinalAttempt(deps, name, prompt, hit, loopHistory) {
|
|
482
687
|
deps.logDebug?.(`${name}: loop budget exhausted — degrading to a no-tools final attempt`);
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
// positionals — so the one attempt made after a loop budget was spent was
|
|
487
|
-
// also the one attempt that could hang forever.
|
|
688
|
+
// This attempt runs under the same wall clock as the strikes that led here.
|
|
689
|
+
// Passing `deps.signal` raw instead would make the one attempt taken after a
|
|
690
|
+
// loop budget is spent the one attempt that can hang forever.
|
|
488
691
|
const clock = phaseTimeout(deps.signal, deps.timeoutMs ?? PHASE_CHILD_TIMEOUT_MS);
|
|
489
692
|
let r;
|
|
490
693
|
try {
|
|
@@ -501,12 +704,17 @@ async function runDegradedFinalAttempt(deps, name, prompt, hit, loopHistory) {
|
|
|
501
704
|
finally {
|
|
502
705
|
clock.cleanup();
|
|
503
706
|
}
|
|
707
|
+
// BEFORE the exit-code test, not inside it: a guard kill reports exit 0, so
|
|
708
|
+
// asking afterwards would already have returned the truncated text as this
|
|
709
|
+
// phase's deliverable.
|
|
710
|
+
const killed = guardKillError(name, r);
|
|
711
|
+
if (killed)
|
|
712
|
+
throw killed;
|
|
504
713
|
if (r.exitCode !== 0 || r.modelError || r.text.trim().length === 0) {
|
|
505
|
-
// A wall-clock kill is NOT a loop.
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
// the very path that clock was added to guard.
|
|
714
|
+
// A wall-clock kill is NOT a loop. Without this check a child that outran
|
|
715
|
+
// its budget is reported as "loop budget exhausted", carrying a loop
|
|
716
|
+
// history that did not cause it — the same mislabel class the worker-kill
|
|
717
|
+
// roster exists to prevent, on the very path the clock guards.
|
|
510
718
|
if (clock.timedOut()) {
|
|
511
719
|
throw new PhaseTimeoutError(name, deps.timeoutMs ?? PHASE_CHILD_TIMEOUT_MS, 1);
|
|
512
720
|
}
|
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
* ChildStatus — the live status of the child pi currently running under a
|
|
3
3
|
* status loader: its latest output line and its context usage.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Without it each spawn site keeps this state by hand — `let lastLine; let contextUsage;`
|
|
6
6
|
* plus two callbacks (`onChildOutput` writes the line, `onContextUsage` folds a
|
|
7
7
|
* snapshot through `resolveContextUsage` with the parent window), a reset before
|
|
8
|
-
* every child, and a loader whose every tick
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* every child, and a loader whose every tick reads both.
|
|
9
|
+
*
|
|
10
|
+
* Three sites share that ritual and are now this class: `/task-auto`'s planning
|
|
11
|
+
* `runChild`, `buildGateDeps`, and `/task-plan` — one `new ChildStatus` each, and
|
|
12
|
+
* no others. The single-task `TaskRunner` deliberately stays where it is: its
|
|
13
|
+
* state is the whole-run `WidgetState`, shared by reference with `PhaseContext`
|
|
14
|
+
* and written by the phases themselves (see `orchestrator.ts`).
|
|
14
15
|
*
|
|
15
16
|
* `track` is the loader ritual: reset, raise the loader reading this status on
|
|
16
17
|
* every tick, run, always stop. The status OUTLIVES a track — `buildGateDeps`
|
|
17
18
|
* shares one across every gate child, and the verify gate raises its own
|
|
18
|
-
* gate-wide loader over a child that renders none (`frame: null
|
|
19
|
-
* see the same object.
|
|
19
|
+
* gate-wide loader over a child that renders none (`frame: null`, reached when
|
|
20
|
+
* `deps.loader === false` in gate-child), so both must see the same object.
|
|
20
21
|
*/
|
|
21
22
|
import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
22
23
|
import type { ContextSnapshot } from '../shared/child-process.js';
|
|
@@ -55,12 +56,17 @@ export declare class ChildStatus {
|
|
|
55
56
|
};
|
|
56
57
|
/**
|
|
57
58
|
* Run `run` under the loader: reset, raise a loader whose every tick is
|
|
58
|
-
* `frame()`
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
59
|
+
* `frame()` spread OVER the live line and gauge, and stop it in a `finally`.
|
|
60
|
+
* All four behaviours were run against a fake loader:
|
|
61
|
+
* - the reset lands first, so the previous child's line never reaches the
|
|
62
|
+
* new loader's first tick;
|
|
63
|
+
* - `frame` wins on a clash, because it is spread last — which is how the
|
|
64
|
+
* verify gate shows its deterministic-stage label until the child has a
|
|
65
|
+
* line of its own;
|
|
66
|
+
* - `frame: null` raises NO loader at all (the caller already has one
|
|
67
|
+
* reading this status) but still resets;
|
|
68
|
+
* - a child that THROWS still stops the loader, so a failure never leaves
|
|
69
|
+
* the widget up.
|
|
64
70
|
*/
|
|
65
71
|
track<T>(ctx: ExtensionCommandContext, frame: (() => AutoLoaderState) | null, run: () => Promise<T>): Promise<T>;
|
|
66
72
|
}
|
|
@@ -98,6 +104,7 @@ export declare function runPlanningChild(opts: {
|
|
|
98
104
|
}): Promise<string>;
|
|
99
105
|
/**
|
|
100
106
|
* Wire a `ChildStatus` as a phase child's stream callbacks — plus the window the
|
|
101
|
-
* child must be TOLD, since pi's
|
|
107
|
+
* child must be TOLD, since pi's `--mode json` stream reports token counts but no
|
|
108
|
+
* context window.
|
|
102
109
|
*/
|
|
103
110
|
export declare function statusCallbacks(status: ChildStatus): Pick<PhaseDeps, 'onChildOutput' | 'onContextUsage' | 'contextWindow'>;
|