tumwater 0.4.2 → 0.4.3
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/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
package/dist/src/pi/pi-models.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { enabledRoleIds, loadConfigSafe } from "../config/config.js";
|
|
4
4
|
import { configForRole, resolverConfig, resolvedBackendConfig, reviewConfig } from "../config/config-views.js";
|
|
5
5
|
import { fallbackPair } from "../config/config-fallbacks.js";
|
|
6
|
-
import { seamAgent } from "../config/model-selector.js";
|
|
6
|
+
import { seamAgent } from "../config/config-model-selector.js";
|
|
7
7
|
import { cachedByStat } from "../files/stat-cache.js";
|
|
8
8
|
import { isJsonObject } from "../files/json-object.js";
|
|
9
9
|
import { readJsonFile } from "../files/json-files.js";
|
|
@@ -107,40 +107,54 @@ export function seamContextWindow(seam, projectRoot, modelsPath = piModelsPath()
|
|
|
107
107
|
const backend = seamAgent(seam);
|
|
108
108
|
switch (backend) {
|
|
109
109
|
case "pi":
|
|
110
|
-
|
|
111
|
-
if (!provider || !model) {
|
|
112
|
-
const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), projectRoot);
|
|
113
|
-
provider = piDefault?.provider;
|
|
114
|
-
model = piDefault?.model;
|
|
115
|
-
}
|
|
116
|
-
return declaredWindow(resolveModelDef(readPiProviders(modelsPath), provider, model)?.contextWindow);
|
|
110
|
+
return piSeamContextWindow(seam, projectRoot, modelsPath);
|
|
117
111
|
case "opencode":
|
|
118
|
-
|
|
119
|
-
// the window part 6a/7 parsed from `limit.context`, keyed by the gateway-qualified id the
|
|
120
|
-
// run actually invokes (opencodeModelKey) — the same key and entry the free-ness check
|
|
121
|
-
// prices by, so a seam's window never disagrees with the model it runs.
|
|
122
|
-
if (seam.model === undefined)
|
|
123
|
-
return null;
|
|
124
|
-
return declaredWindow(readOpencodeCache(projectRoot).models[opencodeModelKey(seam.model)]?.context);
|
|
112
|
+
return opencodeSeamContextWindow(seam, projectRoot);
|
|
125
113
|
case "codex":
|
|
126
|
-
|
|
127
|
-
// `context_window × effective_context_window_percent / 100`, read from the team's codex
|
|
128
|
-
// home (backends.codex.configDir) at this project root — the same home the runs use. An
|
|
129
|
-
// unknown model, an absent home or a missing catalog reads null (declaredWindow's safe
|
|
130
|
-
// answer for an unknown window).
|
|
131
|
-
if (seam.model === undefined)
|
|
132
|
-
return null;
|
|
133
|
-
{
|
|
134
|
-
const { config } = loadConfigSafe(projectRoot);
|
|
135
|
-
const home = codexHome(resolvedBackendConfig(config ?? {}, "codex"));
|
|
136
|
-
if (home === undefined)
|
|
137
|
-
return null;
|
|
138
|
-
return declaredWindow(readCodexCatalog(home).get(seam.model)?.contextWindow);
|
|
139
|
-
}
|
|
114
|
+
return codexSeamContextWindow(seam, projectRoot);
|
|
140
115
|
default:
|
|
141
116
|
return null;
|
|
142
117
|
}
|
|
143
118
|
}
|
|
119
|
+
/** The window a pi seam declares, or null when it cannot be known: reads the pair's
|
|
120
|
+
* `contextWindow` from pi's models.json, running on pi's saved default
|
|
121
|
+
* (readPiDefaultModel) when the seam names no provider/model — exactly the model pi will
|
|
122
|
+
* start on. An unresolvable pair or a missing/unusable window reads null. */
|
|
123
|
+
function piSeamContextWindow(seam, projectRoot, modelsPath) {
|
|
124
|
+
let { provider, model } = seam;
|
|
125
|
+
if (!provider || !model) {
|
|
126
|
+
const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), projectRoot);
|
|
127
|
+
provider = piDefault?.provider;
|
|
128
|
+
model = piDefault?.model;
|
|
129
|
+
}
|
|
130
|
+
return declaredWindow(resolveModelDef(readPiProviders(modelsPath), provider, model)?.contextWindow);
|
|
131
|
+
}
|
|
132
|
+
/** The window an opencode seam declares, or null when it cannot be known. The models.dev cache
|
|
133
|
+
* (readOpencodeCache, an empty cache when the file is missing) holds the window part 6a/7
|
|
134
|
+
* parsed from `limit.context`, keyed by the gateway-qualified id the run actually invokes
|
|
135
|
+
* (opencodeModelKey) — the same key and entry the free-ness check prices by, so a seam's
|
|
136
|
+
* window never disagrees with the model it runs. A missing cache, an unknown model or an
|
|
137
|
+
* unusable `context` reads null. */
|
|
138
|
+
function opencodeSeamContextWindow(seam, projectRoot) {
|
|
139
|
+
if (seam.model === undefined)
|
|
140
|
+
return null;
|
|
141
|
+
return declaredWindow(readOpencodeCache(projectRoot).models[opencodeModelKey(seam.model)]?.context);
|
|
142
|
+
}
|
|
143
|
+
/** The window a codex seam declares, or null when it cannot be known. The model catalog
|
|
144
|
+
* (codex-catalog.ts) holds the window part 8/9 parsed from
|
|
145
|
+
* `context_window × effective_context_window_percent / 100`, read from the team's codex home
|
|
146
|
+
* (backends.codex.configDir) at this project root — the same home the runs use. An unknown
|
|
147
|
+
* model, an absent home or a missing catalog reads null (declaredWindow's safe answer for an
|
|
148
|
+
* unknown window). */
|
|
149
|
+
function codexSeamContextWindow(seam, projectRoot) {
|
|
150
|
+
if (seam.model === undefined)
|
|
151
|
+
return null;
|
|
152
|
+
const { config } = loadConfigSafe(projectRoot);
|
|
153
|
+
const home = codexHome(resolvedBackendConfig(config ?? {}, "codex"));
|
|
154
|
+
if (home === undefined)
|
|
155
|
+
return null;
|
|
156
|
+
return declaredWindow(readCodexCatalog(home).get(seam.model)?.contextWindow);
|
|
157
|
+
}
|
|
144
158
|
/** The resolved primary seam's declared context window for a role — `configForRole` resolves
|
|
145
159
|
* the role's seam (the view the role page, status table/drawer and run history all print), then
|
|
146
160
|
* `seamContextWindow` names the window that seam declares, or null when it cannot be known. The
|
|
@@ -50,37 +50,60 @@ export function readPiDefaultModel(settingsPath, projectRoot) {
|
|
|
50
50
|
return cachedByStat(settingsCache, settingsPath, // Keyed by path so distinct roots and test files never collide.
|
|
51
51
|
settingsPath, () => readPiDefaultModelUncached(settingsPath, projectRoot), (defaultModel) => ({ ...defaultModel }));
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const doc = readJsonFile(settingsPath);
|
|
57
|
-
if (!doc)
|
|
58
|
-
return null;
|
|
53
|
+
/** The saved default pair, or null when either half is missing or empty — pi uses a saved
|
|
54
|
+
* default only as a pair. */
|
|
55
|
+
function savedDefaultPair(doc) {
|
|
59
56
|
const { defaultProvider, defaultModel } = doc;
|
|
60
57
|
if (typeof defaultProvider !== "string" || defaultProvider === "")
|
|
61
58
|
return null;
|
|
62
59
|
if (typeof defaultModel !== "string" || defaultModel === "")
|
|
63
60
|
return null;
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
return { provider: defaultProvider, model: defaultModel };
|
|
62
|
+
}
|
|
63
|
+
/** Whether pi starts on the first scoped model instead of the saved default — a pattern
|
|
64
|
+
* tumwater does not resolve, so the reader shows nothing. */
|
|
65
|
+
function startsWithScopedModels(doc) {
|
|
66
|
+
return Array.isArray(doc.enabledModels) && doc.enabledModels.length > 0;
|
|
67
|
+
}
|
|
68
|
+
/** Whether the project's own `.pi/settings.json` names any default key, which makes pi's final
|
|
69
|
+
* choice depend on whether it trusts the project — unknowable to tumwater, so show nothing. */
|
|
70
|
+
function projectOverridesSavedDefault(projectRoot) {
|
|
66
71
|
// A project-local override that names any default key means pi's final choice depends on
|
|
67
72
|
// whether it trusts the project — unknowable to tumwater, so show nothing.
|
|
68
73
|
const proj = readJsonFile(path.join(projectRoot, ".pi", "settings.json"));
|
|
69
|
-
if (proj)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
if (!proj)
|
|
75
|
+
return false;
|
|
76
|
+
for (const key of DEFAULT_KEYS) {
|
|
77
|
+
if (proj[key] !== undefined)
|
|
78
|
+
return true;
|
|
74
79
|
}
|
|
75
|
-
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
/** pi's thinking for the pair: `modelThinkingLevels[key]` first, else `defaultThinkingLevel`. */
|
|
83
|
+
function savedThinkingLevel(doc, key) {
|
|
76
84
|
const levels = doc.modelThinkingLevels;
|
|
77
85
|
const level = isJsonObject(levels) ? levels[key] : undefined;
|
|
78
|
-
let thinking;
|
|
79
86
|
if (typeof level === "string")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
return level;
|
|
88
|
+
if (typeof doc.defaultThinkingLevel === "string")
|
|
89
|
+
return doc.defaultThinkingLevel;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
function readPiDefaultModelUncached(settingsPath, projectRoot) {
|
|
93
|
+
// readJsonFile is the shared "missing or torn reads as no data" policy for a JSON object
|
|
94
|
+
// file; the nested shapes get the same isJsonObject guard.
|
|
95
|
+
const doc = readJsonFile(settingsPath);
|
|
96
|
+
if (!doc)
|
|
97
|
+
return null;
|
|
98
|
+
const pair = savedDefaultPair(doc);
|
|
99
|
+
if (!pair)
|
|
100
|
+
return null;
|
|
101
|
+
if (startsWithScopedModels(doc))
|
|
102
|
+
return null;
|
|
103
|
+
if (projectOverridesSavedDefault(projectRoot))
|
|
104
|
+
return null;
|
|
105
|
+
const thinking = savedThinkingLevel(doc, `${pair.provider}/${pair.model}`);
|
|
83
106
|
return thinking !== undefined
|
|
84
|
-
? { provider:
|
|
85
|
-
: { provider:
|
|
107
|
+
? { provider: pair.provider, model: pair.model, thinking }
|
|
108
|
+
: { provider: pair.provider, model: pair.model };
|
|
86
109
|
}
|
package/dist/src/pi/pi-stream.js
CHANGED
|
@@ -251,6 +251,17 @@ export class PiStreamParser {
|
|
|
251
251
|
* treat a `rejected` window as the transient rate limit it is (with the resetsAt-derived
|
|
252
252
|
* wait the translator attached). */
|
|
253
253
|
applyRateLimit(event) {
|
|
254
|
+
const reading = this.readRateWindow(event);
|
|
255
|
+
this.upsertRateWindow(reading);
|
|
256
|
+
if (reading.status === "rejected") {
|
|
257
|
+
this.transientRateLimit = true;
|
|
258
|
+
this.setRetryAfterSeconds(event.retryAfterSeconds);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Build one `RateWindow` reading from a canonical `rate_limit` event: derive the `type`
|
|
262
|
+
* (falling back to `unknown` when pi names none), default the `status` to "" when absent,
|
|
263
|
+
* and copy through the four optional fields that the event carries. */
|
|
264
|
+
readRateWindow(event) {
|
|
254
265
|
const type = typeof event.rateLimitType === "string" && event.rateLimitType ? event.rateLimitType : "unknown";
|
|
255
266
|
const reading = { type, status: typeof event.status === "string" ? event.status : "" };
|
|
256
267
|
if (typeof event.resetsAt === "number" && Number.isFinite(event.resetsAt))
|
|
@@ -261,9 +272,16 @@ export class PiStreamParser {
|
|
|
261
272
|
reading.overageStatus = event.overageStatus;
|
|
262
273
|
if (event.isUsingOverage === true)
|
|
263
274
|
reading.isUsingOverage = true;
|
|
264
|
-
|
|
275
|
+
return reading;
|
|
276
|
+
}
|
|
277
|
+
/** Insert one reading, keeping the newest per type: replace the window whose `type` matches
|
|
278
|
+
* and, when the reading is status-less (a non-binding window's utilization), carry the last
|
|
279
|
+
* known status of the window it replaces; note overage so a caller can see a rate limit hit
|
|
280
|
+
* even before it is rejected. */
|
|
281
|
+
upsertRateWindow(reading) {
|
|
282
|
+
const existing = this.rateWindows.findIndex((w) => w.type === reading.type);
|
|
265
283
|
// A reading without a status (a non-binding window's utilization) keeps the last known one.
|
|
266
|
-
if (reading.status === ""
|
|
284
|
+
if (reading.status === "")
|
|
267
285
|
reading.status = this.rateWindows[existing]?.status ?? "";
|
|
268
286
|
if (existing >= 0)
|
|
269
287
|
this.rateWindows[existing] = reading;
|
|
@@ -271,10 +289,6 @@ export class PiStreamParser {
|
|
|
271
289
|
this.rateWindows.push(reading);
|
|
272
290
|
if (reading.isUsingOverage === true)
|
|
273
291
|
this.usedOverage = true;
|
|
274
|
-
if (reading.status === "rejected") {
|
|
275
|
-
this.transientRateLimit = true;
|
|
276
|
-
this.setRetryAfterSeconds(event.retryAfterSeconds);
|
|
277
|
-
}
|
|
278
292
|
}
|
|
279
293
|
/** Fold a provider Retry-After hint into `retryAfterSeconds`: ignore a non-number or
|
|
280
294
|
* non-finite value, and clamp a negative or fractional one to whole seconds. */
|
|
@@ -494,6 +508,12 @@ export class PiStreamParser {
|
|
|
494
508
|
}
|
|
495
509
|
applyAssistantMessage(msg) {
|
|
496
510
|
const text = messageText(msg);
|
|
511
|
+
this.applyAssistantReply(msg, text);
|
|
512
|
+
this.applyAssistantRefusal(text);
|
|
513
|
+
this.applyAssistantUsage(msg);
|
|
514
|
+
this.applyAssistantStop(msg);
|
|
515
|
+
}
|
|
516
|
+
applyAssistantReply(msg, text) {
|
|
497
517
|
this.finalMessageContentless = !messageContent(msg).some((c) => c.type === "toolCall" ||
|
|
498
518
|
(c.type === "text" && typeof c.text === "string" && Boolean(c.text.trim())));
|
|
499
519
|
if (!this.finalMessageContentless)
|
|
@@ -502,6 +522,8 @@ export class PiStreamParser {
|
|
|
502
522
|
this.finalText = text;
|
|
503
523
|
if (isNothingToDo(text))
|
|
504
524
|
this.declaredNothingToDo = true;
|
|
525
|
+
}
|
|
526
|
+
applyAssistantRefusal(text) {
|
|
505
527
|
// The refusal comes only from an anchored `TUMWATER_REFUSED: <reason>` line: a bare
|
|
506
528
|
// sentinel and a mid-sentence mention are not refusals, and a negating reason ("none",
|
|
507
529
|
// "(n/a …)") clears one — BUGS.md 2026-09-23, four runs ended ordinary replies with
|
|
@@ -521,6 +543,8 @@ export class PiStreamParser {
|
|
|
521
543
|
}
|
|
522
544
|
if (hasVerdictLine(text))
|
|
523
545
|
this.verdictText = text;
|
|
546
|
+
}
|
|
547
|
+
applyAssistantUsage(msg) {
|
|
524
548
|
this.turns += 1;
|
|
525
549
|
this.outputTokens += usageNumber(msg.usage?.output);
|
|
526
550
|
const promptTokens = usageNumber(msg.usage?.input) + usageNumber(msg.usage?.cacheRead) + usageNumber(msg.usage?.cacheWrite);
|
|
@@ -536,6 +560,8 @@ export class PiStreamParser {
|
|
|
536
560
|
}
|
|
537
561
|
this.peakContextTokens = Math.max(this.peakContextTokens, usageNumber(msg.usage?.totalTokens));
|
|
538
562
|
this.costUsd += usageNumber(msg.usage?.cost?.total);
|
|
563
|
+
}
|
|
564
|
+
applyAssistantStop(msg) {
|
|
539
565
|
this.stopReason = msg.stopReason;
|
|
540
566
|
if (msg.errorMessage)
|
|
541
567
|
this.errorMessage = msg.errorMessage;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** The run's watchdog clocks, split out of pi.ts's runPi (2026-10-04, organize): the run
|
|
2
2
|
* deadline and the quiet watchdog (quiet-kill plus the stalled-tool-call warning) — the
|
|
3
3
|
* timing policy of one pi run, which changes for different reasons than the spawn, stream,
|
|
4
|
-
* and result-distillation wiring that drives it (src/agent/run
|
|
4
|
+
* and result-distillation wiring that drives it (src/agent/agent-run.ts keeps those). The
|
|
5
5
|
* factory owns all the mutable state the two timers kept in runPi's closure (the progress
|
|
6
6
|
* window, the zero-byte grace, the warned-call set) and exposes the flags the run's close
|
|
7
|
-
* handler reads back; run
|
|
7
|
+
* handler reads back; agent-run.ts feeds the parser live through the deps accessors, so no
|
|
8
8
|
* notification path is needed when events land. */
|
|
9
9
|
import { bufferedCommandStallMs, callBuffersOutput } from "./command-shape.js";
|
|
10
10
|
/** The run-deadline clock: kills the process tree when it fires, after giving a
|
|
@@ -41,7 +41,9 @@ function startDeadlineWatch(state, deps) {
|
|
|
41
41
|
// discard (BUGS.md 2026-10-26).
|
|
42
42
|
const progressGraceMs = 250;
|
|
43
43
|
let extended = false;
|
|
44
|
-
let graceArmed = false; // one grace
|
|
44
|
+
let graceArmed = false; // one grace for the whole run, so a genuinely stalled run is never granted two
|
|
45
|
+
// (see the deadline comment below: a run reaches this branch at most once, so per-run and
|
|
46
|
+
// per-deadline scope grant the same single short re-check)
|
|
45
47
|
state.timeoutBudgetMs = deps.runTimeoutMs;
|
|
46
48
|
const fireDeadline = () => {
|
|
47
49
|
if (deps.extendOnProgress && !extended && deps.progressCount() > checkpointProgress) {
|
|
@@ -65,8 +67,13 @@ function startDeadlineWatch(state, deps) {
|
|
|
65
67
|
// count and a progressing run looks stalled (BUGS.md 2026-10-26). A run that has begun
|
|
66
68
|
// producing structured progress therefore gets ONE short grace to drain queued output and
|
|
67
69
|
// re-prove itself; only a run still flat after the grace — or one that never progressed —
|
|
68
|
-
// keeps the discard path. The grace is one
|
|
69
|
-
//
|
|
70
|
+
// keeps the discard path. The grace is one short beat, armed once for the whole run:
|
|
71
|
+
// graceArmed is a closure local of startDeadlineWatch, one per run, and the branch is
|
|
72
|
+
// reached at most once because a gate run's earlier base deadline is handled by its
|
|
73
|
+
// one-time extension (the block above) while an authoring run has a single deadline — so
|
|
74
|
+
// "once per run" and "once per deadline" grant the very same single re-check, and a
|
|
75
|
+
// progressing gate run is never left without one while a genuinely stalled run is never
|
|
76
|
+
// granted two.
|
|
70
77
|
if (!graceArmed && deps.progressCount() > 0) {
|
|
71
78
|
graceArmed = true;
|
|
72
79
|
state.timeout = setTimeout(fireDeadline, progressGraceMs);
|
package/dist/src/pi/pi.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/** The pi-side facade over the backend-neutral agent runner (src/agent/run
|
|
1
|
+
/** The pi-side facade over the backend-neutral agent runner (src/agent/agent-run.ts) and the
|
|
2
2
|
* pi adapter (src/pi/pi-backend.ts). `runPi` is a thin alias for `runAgent` so every existing
|
|
3
3
|
* call site, fake, and test keeps importing the historical name; the stderr-match constants and
|
|
4
4
|
* the run-kind/backend-failure types stay here for the same reason. The seam itself lives in
|
|
5
5
|
* src/agent/agent-backend.ts, and `hasResumableSession` now lives beside the ledger it reads
|
|
6
6
|
* (src/agent/session-ledger.ts) and is re-exported here so existing importers are unchanged. */
|
|
7
|
-
export { runAgent, runPi } from "../agent/run
|
|
7
|
+
export { runAgent, runPi } from "../agent/agent-run.js";
|
|
8
8
|
export { MODEL_FALLBACK_CLONE, TRANSIENT_PI_CRASH } from "./pi-backend.js";
|
|
9
9
|
export { hasResumableSession } from "../agent/session-ledger.js";
|
|
@@ -130,32 +130,22 @@ function isCandidate(msg, newerTokens, protectedTokens) {
|
|
|
130
130
|
return false;
|
|
131
131
|
return true;
|
|
132
132
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
* bulky tool results. Empty unless the run is at or above the first pass's percent, something
|
|
136
|
-
* outside the newest protected slice is reclaimable, and the pass clears the reclaim floor
|
|
137
|
-
* together — all three from `shakeLimits` for the usage's window. Deterministic and
|
|
138
|
-
* filesystem-free except the injected
|
|
139
|
-
* `writeFullOutput`, which is only called for a bash result pi has not already persisted.
|
|
140
|
-
*/
|
|
141
|
-
export function shakePlan(messages, usage, writeFullOutput) {
|
|
142
|
-
const percent = percentOf(usage);
|
|
143
|
-
const limits = shakeLimits(usage.contextWindow);
|
|
144
|
-
if (percent === null || percent < limits.percent)
|
|
145
|
-
return EMPTY_PLAN;
|
|
146
|
-
// Tokens newer than each message, so a candidate is one the model has already moved past.
|
|
133
|
+
/** Tokens newer than each message, so a candidate is one the model has already moved past. */
|
|
134
|
+
function newerTokenCounts(messages) {
|
|
147
135
|
const newerTokens = new Array(messages.length).fill(0);
|
|
148
136
|
let running = 0;
|
|
149
137
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
150
138
|
newerTokens[i] = running;
|
|
151
139
|
running += estimateTokens(messages[i]?.text ?? "");
|
|
152
140
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
141
|
+
return newerTokens;
|
|
142
|
+
}
|
|
143
|
+
/** The messages the model has already moved past and can reclaim space from. */
|
|
144
|
+
function reclaimableCandidates(messages, newerTokens, protectedTokens) {
|
|
145
|
+
return messages.filter((msg, i) => isCandidate(msg, newerTokens[i] ?? 0, protectedTokens));
|
|
146
|
+
}
|
|
147
|
+
/** The edits that reclaim space from one candidate list, and how much they reclaim together. */
|
|
148
|
+
function shakeEdits(candidates, writeFullOutput) {
|
|
159
149
|
const edits = [];
|
|
160
150
|
let reclaimedTokens = 0;
|
|
161
151
|
for (const msg of candidates) {
|
|
@@ -171,6 +161,28 @@ export function shakePlan(messages, usage, writeFullOutput) {
|
|
|
171
161
|
edits.push({ targetId: msg.entryId, toolName: msg.toolName, replacement, reclaimedTokens: reclaimed });
|
|
172
162
|
reclaimedTokens += reclaimed;
|
|
173
163
|
}
|
|
164
|
+
return { edits, reclaimedTokens };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Plan a shake pass: the `context_edit` replacements that reclaim the most space from old
|
|
168
|
+
* bulky tool results. Empty unless the run is at or above the first pass's percent, something
|
|
169
|
+
* outside the newest protected slice is reclaimable, and the pass clears the reclaim floor
|
|
170
|
+
* together — all three from `shakeLimits` for the usage's window. Deterministic and
|
|
171
|
+
* filesystem-free except the injected
|
|
172
|
+
* `writeFullOutput`, which is only called for a bash result pi has not already persisted.
|
|
173
|
+
*/
|
|
174
|
+
export function shakePlan(messages, usage, writeFullOutput) {
|
|
175
|
+
const percent = percentOf(usage);
|
|
176
|
+
const limits = shakeLimits(usage.contextWindow);
|
|
177
|
+
if (percent === null || percent < limits.percent)
|
|
178
|
+
return EMPTY_PLAN;
|
|
179
|
+
const candidates = reclaimableCandidates(messages, newerTokenCounts(messages), limits.protectedTokens);
|
|
180
|
+
// The pointer is far shorter than the result it replaces, so the full text's tokens are a
|
|
181
|
+
// safe upper bound on the reclaim; bail before touching the writer when even that is short.
|
|
182
|
+
const upperBound = candidates.reduce((sum, msg) => sum + estimateTokens(msg.text), 0);
|
|
183
|
+
if (upperBound < limits.minReclaimTokens)
|
|
184
|
+
return EMPTY_PLAN;
|
|
185
|
+
const { edits, reclaimedTokens } = shakeEdits(candidates, writeFullOutput);
|
|
174
186
|
if (reclaimedTokens < limits.minReclaimTokens)
|
|
175
187
|
return EMPTY_PLAN;
|
|
176
188
|
return { edits, reclaimedTokens, elidedCount: edits.length };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled pi extension: refuse a `bash` call that kills processes by pattern, next to the write
|
|
3
|
+
* guard (src/pi-extension/write-guard.ts). Loaded on every pi run via `-e <this file>` from
|
|
4
|
+
* src/pi/pi-args.ts.
|
|
5
|
+
*
|
|
6
|
+
* The run prompt already forbids pattern kills (KILL_BY_PID_RULE, src/prompt/prompt.ts): "Never
|
|
7
|
+
* kill processes by name or pattern; stop only a process you started, by its PID (`kill $!`)".
|
|
8
|
+
* But that rule is words, and BUGS.md 2026-10-28 records one agent answering a timed-out test run
|
|
9
|
+
* with `ps aux | grep test-runner | xargs kill` and a `for p in $(ps …) kill` loop, which reached
|
|
10
|
+
* every process-table row the pattern matched — the team's own in-flight checks included — and
|
|
11
|
+
* took them down mid-run. The process table is shared, so none of those kills is provably the
|
|
12
|
+
* runner's own; `kill $!` and the run's exit sweep are the sanctioned way to stop what it started.
|
|
13
|
+
*
|
|
14
|
+
* This is a tool-layer net beside the prompt rule, not a replacement for it. The same `tool_call`
|
|
15
|
+
* event pi fires for `edit`/`write` fires for `bash`; blocking a call when a handler returns
|
|
16
|
+
* `{ block: true, reason }` shows the model the reason as the tool's error. The detection is a
|
|
17
|
+
* heuristic for the common by-pattern shapes — `pkill`/`killall` as a command word, and `ps` or
|
|
18
|
+
* `pgrep` output fed into `kill` via `xargs` or a `$(ps|pgrep)` substitution — and fails open like
|
|
19
|
+
* the write guard does: a missed shape under load still leaves the prompt rule and the run's sweep,
|
|
20
|
+
* and an unexpected failure here lets the call through rather than stopping every bash command of
|
|
21
|
+
* the run (a broken guard cannot block the team's own checks).
|
|
22
|
+
*/
|
|
23
|
+
/** The refusal shown as the tool's error, naming the safe ways to stop a run's own process. */
|
|
24
|
+
export const PATTERN_KILL_REFUSAL = "kill by pattern is refused: `pkill`/`killall` and `ps`/`pgrep` output fed into `kill` reach every " +
|
|
25
|
+
"process on the host the pattern matches, the team's own checks included. Stop only a process you " +
|
|
26
|
+
"started, by its PID: `kill $!` right after backgrounding it — the run's own processes are reaped " +
|
|
27
|
+
"when the run ends.";
|
|
28
|
+
/** A `pkill` or `killall` spelled as a command word, skipping a line that only mentions them
|
|
29
|
+
* (built after `echo`/`grep`/`man` etc.). Preceded by a command boundary so `echo pkill x` and a
|
|
30
|
+
* quoted mention pass while `; pkill x` and `| killall x` are caught. */
|
|
31
|
+
const DIRECT_PATTERN_KILL = /(?:^|[;&|(\n])\s*(?:pkill|killall)(?:\s|$)/;
|
|
32
|
+
/** `ps` or `pgrep` that reads the process table. */
|
|
33
|
+
const READ_PROCESS_TABLE = /\b(?:ps|pgrep)\b/;
|
|
34
|
+
/** That table piped into `kill` through `xargs` (the incident's `ps … | … | xargs -r kill`). */
|
|
35
|
+
const XARGS_KILL = /\bxargs\b[^\n;]*\bkill\b/;
|
|
36
|
+
/** A `$(ps …)` / `$(pgrep …)` substitution that feeds ids to a `kill` (`kill -9 $(pgrep -f npm)`,
|
|
37
|
+
* a `for p in $(ps …)` loop whose body kills). */
|
|
38
|
+
const TABLE_SUBSTITUTION = /\$\(\s*(?:ps|pgrep)\b/;
|
|
39
|
+
/** The refusal for one `bash` command, or null to let it run. Exported for tests. */
|
|
40
|
+
export function patternKillRefusal(command) {
|
|
41
|
+
if (typeof command !== "string" || command.trim() === "")
|
|
42
|
+
return null;
|
|
43
|
+
if (DIRECT_PATTERN_KILL.test(command))
|
|
44
|
+
return PATTERN_KILL_REFUSAL;
|
|
45
|
+
const feedsTable = READ_PROCESS_TABLE.test(command);
|
|
46
|
+
const feedsKill = XARGS_KILL.test(command) || (TABLE_SUBSTITUTION.test(command) && /\bkill\b/.test(command));
|
|
47
|
+
if (feedsTable && feedsKill)
|
|
48
|
+
return PATTERN_KILL_REFUSAL;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/** The refusal for one `tool_call` event, or undefined to let it run. Exported for tests. */
|
|
52
|
+
export function guardKillToolCall(event) {
|
|
53
|
+
if (event.toolName !== "bash")
|
|
54
|
+
return undefined;
|
|
55
|
+
const input = event.input;
|
|
56
|
+
if (typeof input !== "object" || input === null)
|
|
57
|
+
return undefined;
|
|
58
|
+
try {
|
|
59
|
+
const reason = patternKillRefusal(input.command);
|
|
60
|
+
return reason === null ? undefined : { block: true, reason };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The pi extension entry point. */
|
|
67
|
+
export default function killGuard(pi) {
|
|
68
|
+
pi.on("tool_call", guardKillToolCall);
|
|
69
|
+
}
|
|
@@ -122,6 +122,13 @@ export function runScriptGroup(cmd, args, opts) {
|
|
|
122
122
|
env: withoutLaunchServicesCheckIn(process.env),
|
|
123
123
|
});
|
|
124
124
|
let settled = false;
|
|
125
|
+
let aborted = false;
|
|
126
|
+
// Abort-stop timers, the mirror of armGroupDeadline's — armed only while an abort fires
|
|
127
|
+
// before settle (never at spawn), SIGTERM first then SIGKILL after killGraceMs for a
|
|
128
|
+
// survivor, settling as soon as the whole group is gone — cleared on finish, the caller's
|
|
129
|
+
// single settle path.
|
|
130
|
+
let abortPoll;
|
|
131
|
+
let abortKillTimer;
|
|
125
132
|
// Captured output is capped at maxBuffer per stream (further chunks are dropped —
|
|
126
133
|
// classification reads the tail, so a chatty script can neither wedge the check nor balloon
|
|
127
134
|
// memory). The overflow is deliberately ignored here: truncated output still classifies.
|
|
@@ -134,11 +141,34 @@ export function runScriptGroup(cmd, args, opts) {
|
|
|
134
141
|
killGraceMs: opts.killGraceMs,
|
|
135
142
|
onTimedOut: () => finish({ signal: "SIGTERM" }),
|
|
136
143
|
});
|
|
144
|
+
function stopForAbort() {
|
|
145
|
+
if (settled)
|
|
146
|
+
return;
|
|
147
|
+
aborted = true;
|
|
148
|
+
// The harness's own stop, signalled the way a timeout stops the tree: SIGTERM the whole
|
|
149
|
+
// group (grandchild included), then SIGKILL anything still alive after the grace. The
|
|
150
|
+
// run settles when the group is gone, so the caller never sees a live tree behind it.
|
|
151
|
+
signalTree(child, "SIGTERM");
|
|
152
|
+
abortPoll = setInterval(() => {
|
|
153
|
+
if (!groupAlive(child.pid))
|
|
154
|
+
finish({ aborted: true });
|
|
155
|
+
}, GROUP_POLL_MS);
|
|
156
|
+
abortKillTimer = setTimeout(() => {
|
|
157
|
+
signalTree(child, "SIGKILL");
|
|
158
|
+
finish({ aborted: true });
|
|
159
|
+
}, opts.killGraceMs);
|
|
160
|
+
}
|
|
137
161
|
function finish(result) {
|
|
138
162
|
if (settled)
|
|
139
163
|
return;
|
|
140
164
|
settled = true;
|
|
141
165
|
deadline.dispose();
|
|
166
|
+
if (abortPoll)
|
|
167
|
+
clearInterval(abortPoll);
|
|
168
|
+
if (abortKillTimer)
|
|
169
|
+
clearTimeout(abortKillTimer);
|
|
170
|
+
if (opts.signal)
|
|
171
|
+
opts.signal.removeEventListener("abort", stopForAbort);
|
|
142
172
|
const timedOut = deadline.fired();
|
|
143
173
|
if (timedOut) {
|
|
144
174
|
// Nothing more is read once the deadline has fired: release the handles so a pipe
|
|
@@ -148,16 +178,30 @@ export function runScriptGroup(cmd, args, opts) {
|
|
|
148
178
|
const run = { spawnedAt, settledAt: Date.now(), timeoutMs: opts.timeoutMs };
|
|
149
179
|
if (timedOut)
|
|
150
180
|
run.deadlineLateMs = deadline.lateMs();
|
|
151
|
-
resolve({ ...result, timedOut, stdout: captured.stdout, stderr: captured.stderr, run });
|
|
181
|
+
resolve({ ...result, aborted, timedOut, stdout: captured.stdout, stderr: captured.stderr, run });
|
|
152
182
|
}
|
|
183
|
+
// An abort signal stops the tree mid-check — the harness's own stop, so the outcome says
|
|
184
|
+
// the harness aborted it, distinct from a timeout and from an external killed signal. The
|
|
185
|
+
// listener is removed as soon as the run settles. An already-aborted signal is deliberately
|
|
186
|
+
// NOT acted on at spawn: the swap happens mid-run (a user abort or a shutdown fires while
|
|
187
|
+
// the check is running), and a signal already aborted before the check started is caught by
|
|
188
|
+
// the caller's `aborted` check when the run returns — a fast failing check under a
|
|
189
|
+
// pre-aborted signal still reaches its own verdict first (the pre-existing shutdown-test
|
|
190
|
+
// behavior).
|
|
191
|
+
if (opts.signal)
|
|
192
|
+
opts.signal.addEventListener("abort", stopForAbort, { once: true });
|
|
153
193
|
child.on("error", (err) => {
|
|
154
|
-
// Before
|
|
155
|
-
// only be a teardown signal that could not be delivered
|
|
156
|
-
//
|
|
157
|
-
if (!deadline.fired())
|
|
194
|
+
// Before a stop this is the spawn failing (npm missing from PATH). After the deadline or
|
|
195
|
+
// an abort it can only be a teardown signal that could not be delivered — the stop's own
|
|
196
|
+
// group probe or grace timer settles the run.
|
|
197
|
+
if (!deadline.fired() && !aborted)
|
|
158
198
|
finish({ spawnError: err });
|
|
159
199
|
});
|
|
160
200
|
child.on("close", (code, signal) => {
|
|
201
|
+
// During an abort-stop the run settles on the whole group being gone (the probe or the
|
|
202
|
+
// grace timer), never on the leader's close alone. Otherwise the timeout rule stands.
|
|
203
|
+
if (aborted)
|
|
204
|
+
return;
|
|
161
205
|
// Before the deadline the leader's close settles the run. After it, settle only once the
|
|
162
206
|
// whole group is gone: something in the group may outlive the leader, and the poll
|
|
163
207
|
// settles the run when it goes, or the grace timer SIGKILLs it and settles then.
|
|
@@ -3,10 +3,10 @@ import { promisify } from "node:util";
|
|
|
3
3
|
/** Child-process plumbing: the harness's one execFile helper (execFileAsync), the pid-liveness
|
|
4
4
|
* probe every place that decides whether a recorded pid still belongs to a live process shares
|
|
5
5
|
* (the merge lock's stale-holder check in lock.ts, the orchestrator-alive status in
|
|
6
|
-
* team/orchestrator-info.ts), the group-signal helpers behind every teardown, the
|
|
7
|
-
* race helper the spawns and merge's shutdown wiring share, and the child
|
|
8
|
-
* that keeps the harness's Node processes out of LaunchServices on macOS. Reading the
|
|
9
|
-
* process table and the per-run TUMWATER_RUN marks and sweep built on it live in
|
|
6
|
+
* team/team-orchestrator-info.ts), the group-signal helpers behind every teardown, the
|
|
7
|
+
* register/abort race helper the spawns and merge's shutdown wiring share, and the child
|
|
8
|
+
* environment that keeps the harness's Node processes out of LaunchServices on macOS. Reading the
|
|
9
|
+
* host's process table and the per-run TUMWATER_RUN marks and sweep built on it live in
|
|
10
10
|
* process-table.ts, on top of the exec helper here. */
|
|
11
11
|
const execFileRaw = promisify(execFile);
|
|
12
12
|
/** The output ceiling every execFile the harness runs shares (git, tsc, the build check, the
|
|
@@ -24,7 +24,7 @@ export function execFileAsync(file, args, options = {}) {
|
|
|
24
24
|
* added after the abort event never fires, so `addEventListener` alone would leave the caller
|
|
25
25
|
* waiting on a shutdown that has come and gone. A missing signal (no shutdown wiring)
|
|
26
26
|
* registers nothing. Shared by spawnSupervised (supervisor.ts), runPi's child spawn
|
|
27
|
-
* (src/agent/
|
|
27
|
+
* (src/agent/agent-spawn.ts), sleepInterruptible (run-timing.ts), and abortOnShutdown
|
|
28
28
|
* (merge-pipeline.ts).
|
|
29
29
|
*
|
|
30
30
|
* Returns a disposer that detaches `fn`. A `{ once: true }` listener detaches itself when the
|
|
@@ -113,7 +113,7 @@ export function terminateChild(child) {
|
|
|
113
113
|
* reached a live process: false when nothing was left to receive it — ESRCH from a group that
|
|
114
114
|
* died with its leader, the normal case once the child has exited. Never throws: a process
|
|
115
115
|
* that died between the caller's decision and this call is the normal case. Shared by runPi's
|
|
116
|
-
* teardown (src/agent/run
|
|
116
|
+
* teardown (src/agent/agent-run.ts) and the detached process-group runner (runScriptGroup,
|
|
117
117
|
* process-group.ts) — the same "signal the tree, not just the direct child" guarantee in one
|
|
118
118
|
* home, this module. */
|
|
119
119
|
export function signalTree(child, signal) {
|
|
@@ -186,8 +186,8 @@ export function captureCappedOutput(child, maxBuffer, onOverflow) {
|
|
|
186
186
|
// ── Child environments ───────────────────────────────────────────────────────────────────
|
|
187
187
|
/** The child-environment policy every spawn that hands a child the harness's environment shares:
|
|
188
188
|
* start from `process.env` and apply `overrides` on top. The explicit spawn sites
|
|
189
|
-
* (supervisor.ts, self-reload.ts, notify.ts, git-run.ts, doctor-model-checks.ts) all want
|
|
190
|
-
* this, so the spread lives here once instead of in each spawn's options. */
|
|
189
|
+
* (supervisor.ts, self-reload.ts, event-notify.ts, git-run.ts, doctor-model-checks.ts) all want
|
|
190
|
+
* exactly this, so the spread lives here once instead of in each spawn's options. */
|
|
191
191
|
export function childEnv(overrides = {}) {
|
|
192
192
|
return { ...process.env, ...overrides };
|
|
193
193
|
}
|