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
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { errorStorm, errorStormKnob } from "./error-storm.js";
|
|
1
|
+
import { errorStorm, errorStormKnob } from "./team-error-storm.js";
|
|
2
2
|
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
3
3
|
import { teamHold, TEAM_OPEN } from "./team-hold.js";
|
|
4
|
-
import { claudeWindowUsedUp
|
|
4
|
+
import { claudeWindowUsedUp } from "../agent/claude/claude-account.js";
|
|
5
5
|
import { sortedRoles } from "../failure/failure-cluster.js";
|
|
6
|
-
import { FAILURE_SPREAD_WINDOW_MS, failureSpread } from "./failure-spread.js";
|
|
6
|
+
import { FAILURE_SPREAD_WINDOW_MS, failureSpread } from "./team-failure-spread.js";
|
|
7
7
|
import { groupBy } from "../collections.js";
|
|
8
|
-
import { eventsSince } from "../events/event-window.js";
|
|
9
|
-
import { DAY_MS } from "../text/datetime.js";
|
|
10
8
|
/** The HoldObservation list both team-wide failure polls step their reducer with, gathered
|
|
11
9
|
* from each runner's two episodic fields: lastRateLimit (stamped "rate-limit", carrying the
|
|
12
10
|
* server's retry-after when present) and lastBackendFailure. One copy of that flatMap so the
|
|
@@ -37,14 +35,13 @@ function holdObservations(runners) {
|
|
|
37
35
|
* heldProviders() (until non-null), never bare key presence: a lifted hold must never
|
|
38
36
|
* keep blocking. Events carry the provider when one is configured (an undefined provider
|
|
39
37
|
* — pi's default — omits it, so an unconfigured team's events render exactly as before).
|
|
40
|
-
* `
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
export function pollTeamHold(root, prev, runners, now, observations = [], windowTiers = {}) {
|
|
38
|
+
* `observations` is the per-subscription-backend hold
|
|
39
|
+
* observations (one per backend in use), each stamped by the caller from its account via
|
|
40
|
+
* windowObservation. The gate no longer passes them and no doctor row reads them (Subtract
|
|
41
|
+
* parts 5u/6 and 5w/6); the seam stays for the unit tests. Returns the new map for the
|
|
42
|
+
* caller to keep.
|
|
43
|
+
* Exported as a unit-test seam, like its sibling polls below. */
|
|
44
|
+
export function pollTeamHold(root, prev, runners, now, observations = []) {
|
|
48
45
|
const steps = [...holdObservations(runners), ...observations];
|
|
49
46
|
const byProvider = groupBy(steps, (o) => o.provider);
|
|
50
47
|
const next = new Map();
|
|
@@ -73,9 +70,6 @@ export function pollTeamHold(root, prev, runners, now, observations = [], window
|
|
|
73
70
|
...providerFields,
|
|
74
71
|
windowType: subObs?.windowType,
|
|
75
72
|
resetsAt: pNext.until,
|
|
76
|
-
// Where each moved tier's work went (the budget poll's window move); absent when
|
|
77
|
-
// nothing moved, so a no-fallback team's event is today's.
|
|
78
|
-
...(Object.keys(windowTiers).length > 0 ? { tiers: windowTiers } : {}),
|
|
79
73
|
});
|
|
80
74
|
};
|
|
81
75
|
if (pPrev.until !== null && pPrev.kind === "subscription-window" && pPrev.reserve === true &&
|
|
@@ -85,7 +79,7 @@ export function pollTeamHold(root, prev, runners, now, observations = [], window
|
|
|
85
79
|
}
|
|
86
80
|
else if (pPrev.until === null && pNext.until !== null) {
|
|
87
81
|
if (pNext.kind === "subscription-window" && pNext.reserve === true) {
|
|
88
|
-
// A reserve crossing logs
|
|
82
|
+
// A reserve crossing logs nothing here; the reserve event loop was removed (5u/6).
|
|
89
83
|
}
|
|
90
84
|
else if (pNext.kind === "subscription-window") {
|
|
91
85
|
limitedEvent();
|
|
@@ -143,107 +137,15 @@ export function windowObservation(backend, account, now, reserve) {
|
|
|
143
137
|
...(furthest.reserve === true ? { reserve: true } : {}),
|
|
144
138
|
};
|
|
145
139
|
}
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* in the event log, so a restart does not re-announce a window that is still open. Keyed by the
|
|
151
|
-
* event's own `backend` (falling back to `"claude"` for an event written before the window
|
|
152
|
-
* events carried one), so two backends' windows of the same type and reset instant keep their
|
|
153
|
-
* own keys. Events without a numeric `resetsAt` cannot name their window and are skipped. */
|
|
154
|
-
export function loggedWindowKeys(root, type, now) {
|
|
155
|
-
const keys = new Set();
|
|
156
|
-
for (const e of eventsSince(root, CLAUDE_WINDOW_LOOKBACK_MS, now, (ev) => ev.type === type)) {
|
|
157
|
-
if (typeof e.windowType === "string" && typeof e.resetsAt === "number" && e.resetsAt > now) {
|
|
158
|
-
keys.add(`${typeof e.backend === "string" ? e.backend : "claude"}:${e.windowType}:${e.resetsAt}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return keys;
|
|
162
|
-
}
|
|
163
|
-
/** Log each subscription-window event once per window key. The reserve and the warning polls
|
|
164
|
-
* (below) both walk every window of the account and log one event for each that crosses its mark
|
|
165
|
-
* this poll, deduplicated by the `backend:type:resetsAt` key so many polls across one window log
|
|
166
|
-
* once (a later window's crossing is its own key). They differ only in WHICH window marks (the
|
|
167
|
-
* reserve fraction vs the `allowed_warning` status) and the event's payload, so the traversal, the
|
|
168
|
-
* "reset instant already past" skip, the key build, the key dedup, and the returned-set contract
|
|
169
|
-
* live here and each caller supplies the mark-and-payload predicate (`eventFor` returns the event
|
|
170
|
-
* to log, or null to skip that window). The returned set drops keys whose reset instant has passed,
|
|
171
|
-
* and is `logged` itself when nothing was added or dropped. */
|
|
172
|
-
function logWindowEventOnce(root, logged, account, now, backend, eventFor) {
|
|
173
|
-
if (account?.billing !== "subscription")
|
|
174
|
-
return logged;
|
|
175
|
-
let next = null;
|
|
176
|
-
for (const key of logged) {
|
|
177
|
-
const suffix = key.slice(key.lastIndexOf(":") + 1);
|
|
178
|
-
if (suffix !== "" && Number(suffix) <= now)
|
|
179
|
-
(next ??= new Set(logged)).delete(key);
|
|
180
|
-
}
|
|
181
|
-
for (const [type, w] of Object.entries(account.windows)) {
|
|
182
|
-
const ev = eventFor(type, w);
|
|
183
|
-
if (ev === null)
|
|
184
|
-
continue;
|
|
185
|
-
// A window whose reset instant is in the past (or unreadable) no longer marks a crossing.
|
|
186
|
-
if (typeof w.resetsAt === "number" && w.resetsAt <= now)
|
|
187
|
-
continue;
|
|
188
|
-
// Keyed by this poll's backend (the `backend:windowType:resetsAt` shape loggedWindowKeys
|
|
189
|
-
// seeds), so a different backend's identical window keeps its own key. The backend rides
|
|
190
|
-
// the event, stamped here so each caller's event needs no backend of its own.
|
|
191
|
-
const key = `${backend}:${type}:${w.resetsAt ?? ""}`;
|
|
192
|
-
if ((next ?? logged).has(key))
|
|
193
|
-
continue;
|
|
194
|
-
logEventBestEffort(root, { ...ev, backend });
|
|
195
|
-
(next ??= new Set(logged)).add(key);
|
|
196
|
-
}
|
|
197
|
-
return next ?? logged;
|
|
198
|
-
}
|
|
199
|
-
/** One poll of the reserve crossing: each window at or past its windowReserve fraction (read
|
|
200
|
-
* through `windowReserveFor(config, "claude")`) logs `subscription_window_reserved` once per
|
|
201
|
-
* window key (`backend:type:resetsAt`, kept in `logged`), so several windows crossing in one
|
|
202
|
-
* poll each log their own event. Returns the keys to keep. */
|
|
203
|
-
export function pollClaudeWindowReserve(root, logged, account, now, backend, reserve) {
|
|
204
|
-
if (reserve === undefined)
|
|
205
|
-
return logged;
|
|
206
|
-
return logWindowEventOnce(root, logged, account, now, backend, (type, w) => {
|
|
207
|
-
const fraction = reserve[type];
|
|
208
|
-
if (!windowReserved(w, fraction) || typeof w.resetsAt !== "number")
|
|
209
|
-
return null;
|
|
210
|
-
return {
|
|
211
|
-
source: TUMWATER_SOURCE,
|
|
212
|
-
type: "subscription_window_reserved",
|
|
213
|
-
windowType: type,
|
|
214
|
-
utilization: w.utilization,
|
|
215
|
-
reserve: fraction,
|
|
216
|
-
resetsAt: w.resetsAt,
|
|
217
|
-
};
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
/** One poll of the subscription-window warning: each `allowed_warning` window logs
|
|
221
|
-
* `subscription_window_warning` once per window key (`backend:type:resetsAt`, kept in `logged`;
|
|
222
|
-
* carrying the window's utilization when the account reported one), so many polls across one
|
|
223
|
-
* window warn once and several windows warning in one poll each log their own event. Returns
|
|
224
|
-
* the keys to keep. */
|
|
225
|
-
export function pollClaudeWindowWarning(root, logged, account, now, backend) {
|
|
226
|
-
return logWindowEventOnce(root, logged, account, now, backend, (type, w) => {
|
|
227
|
-
if (w.status !== "allowed_warning")
|
|
228
|
-
return null;
|
|
229
|
-
return {
|
|
230
|
-
source: TUMWATER_SOURCE,
|
|
231
|
-
type: "subscription_window_warning",
|
|
232
|
-
windowType: type,
|
|
233
|
-
...(typeof w.utilization === "number" ? { utilization: w.utilization } : {}),
|
|
234
|
-
...(typeof w.resetsAt === "number" ? { resetsAt: w.resetsAt } : {}),
|
|
235
|
-
};
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
/** One poll of the team-wide error-storm warning (src/team/error-storm.ts): gather each runner's
|
|
239
|
-
* current error streak (RunnerState.consecutiveErrors/lastError — the same fields the per-role
|
|
240
|
-
* "consecutive run failures" warning reads, the director's included, since its failures are
|
|
241
|
-
* the team's evidence too), step the pure reducer, and log exactly one warning per episode,
|
|
140
|
+
/** One poll of the team-wide error-storm warning (src/team/team-error-storm.ts): gather each
|
|
141
|
+
* runner's current error streak (RunnerState.consecutiveErrors/lastError — the same fields the
|
|
142
|
+
* per-role "consecutive run failures" warning reads, the director's included, since its failures
|
|
143
|
+
* are the team's evidence too), step the pure reducer, and log exactly one warning per episode,
|
|
242
144
|
* like the per-role warning's once-per-streak-crossing shape: on the quiet→storm crossing,
|
|
243
|
-
* naming the roles, the shared normalized cause, and the config knob when the cause has one
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
145
|
+
* naming the roles, the shared normalized cause, and the config knob when the cause has one (a
|
|
146
|
+
* team timing out together means runTimeoutSeconds does not fit the serving model). Storms clear
|
|
147
|
+
* silently — the members' own recoveries already tell that story — and the reducer's `prev`
|
|
148
|
+
* pass-through keeps a held storm event-free. Returns the new storm for the
|
|
247
149
|
* caller to keep. Exported as a unit-test seam, like pollTeamHold above. */
|
|
248
150
|
export function pollErrorStorm(root, prev, runners) {
|
|
249
151
|
const observations = runners.map((r) => ({
|
|
@@ -267,15 +169,15 @@ export function pollErrorStorm(root, prev, runners) {
|
|
|
267
169
|
}
|
|
268
170
|
return next;
|
|
269
171
|
}
|
|
270
|
-
/** One poll of the team-wide wide-shallow storm alarm (src/team/failure-spread.ts): gather
|
|
271
|
-
* runner's latest provider-failure observations — the same two the team hold reads, the
|
|
272
|
-
* director's included, since its failures are the team's evidence too — step the pure
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
172
|
+
/** One poll of the team-wide wide-shallow storm alarm (src/team/team-failure-spread.ts): gather
|
|
173
|
+
* each runner's latest provider-failure observations — the same two the team hold reads, the
|
|
174
|
+
* director's included, since its failures are the team's evidence too — step the pure reducer,
|
|
175
|
+
* and log exactly one warning per episode: on the quiet→active crossing (and again when a
|
|
176
|
+
* DIFFERENT kind becomes the episode while one is sounding, the reducer's new-episode rule),
|
|
177
|
+
* naming the failure count, the kind, the window, and the roles behind it. The wiring keys its
|
|
178
|
+
* log off active/kind, not object identity — `recent` moves almost every poll, so unlike the
|
|
179
|
+
* hold's poll there is no prev pass-through to compare. Exported as a unit-test seam, like
|
|
180
|
+
* pollTeamHold above. */
|
|
279
181
|
export function pollFailureSpread(root, prev, runners, now) {
|
|
280
182
|
const observations = holdObservations(runners);
|
|
281
183
|
const next = failureSpread(prev, observations, now);
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* an interrupted run's uncommitted edits survive.
|
|
7
7
|
*
|
|
8
8
|
* The candidate list and the guard that keeps a clean off the primary checkout live here; the
|
|
9
|
-
* in-use half lives in src/git/worktree-use.ts and the hold half in src/gates/disk-
|
|
9
|
+
* in-use half lives in src/git/worktree-use.ts and the hold half in src/gates/disk-gates.ts. */
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { HOUR_MS } from "../text/datetime.js";
|
|
12
12
|
import { worktreesDir } from "../paths.js";
|
|
13
13
|
import { git, gitTry } from "../git/git-run.js";
|
|
14
14
|
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
15
15
|
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
16
|
-
import { BYTES_PER_GB, sampleFreeBytes } from "../gates/disk-
|
|
16
|
+
import { BYTES_PER_GB, sampleFreeBytes } from "../gates/disk-gates.js";
|
|
17
17
|
import { subdirectoryNames } from "../files/files.js";
|
|
18
18
|
import { claimForReclaim, isReclaimInProgress, isWorktreeInUse, readWorktreeUse, releaseReclaim, updateWorktreeUse, } from "../git/worktree-use.js";
|
|
19
19
|
/** Worktree basenames that pressure reclaim never touches: the self-hosting mirrors the
|
|
@@ -126,55 +126,85 @@ async function cleanIgnored(dir) {
|
|
|
126
126
|
const out = await git(dir, "clean", "-fdX");
|
|
127
127
|
return out.split("\n").some((line) => line.startsWith("Removing "));
|
|
128
128
|
}
|
|
129
|
-
/**
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
/** True when a pressure or manual pass should stop: the sampler reports free space at or
|
|
130
|
+
* above `reclaimGB`. A null sample (sampling failed) is not the floor, so it keeps reclaiming. */
|
|
131
|
+
function pressureFloorReached(free, reclaimGB) {
|
|
132
|
+
return free !== null && reclaimGB > 0 && free >= reclaimGB * BYTES_PER_GB;
|
|
133
|
+
}
|
|
134
|
+
/** Clean one candidate, treating a guard failure as a skip, not a reason to abort the pass. */
|
|
135
|
+
async function reclaimOneCandidate(root, candidate) {
|
|
136
|
+
try {
|
|
137
|
+
return await reclaimWorktree(root, candidate.dir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// A guard failure (a directory that stopped being a linked worktree) is not a reason to
|
|
141
|
+
// abort the pass; the remaining candidates are still safe to clean.
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Walk the candidates once, collecting the ones actually cleaned. Idle mode keeps only the
|
|
146
|
+
* `isIdleExpired` candidates and never re-samples; pressure and manual modes re-sample before
|
|
147
|
+
* each and stop once the pressure floor is reached. */
|
|
148
|
+
async function collectReclaimed(root, candidates, mode, idleHours, reclaimGB, sample, startedAt) {
|
|
137
149
|
const cleaned = [];
|
|
138
150
|
for (const candidate of candidates) {
|
|
139
151
|
if (mode === "idle") {
|
|
140
|
-
if (!isIdleExpired(candidate,
|
|
152
|
+
if (!isIdleExpired(candidate, idleHours, startedAt))
|
|
141
153
|
continue;
|
|
142
154
|
}
|
|
143
|
-
else {
|
|
144
|
-
|
|
145
|
-
if (free !== null && opts.reclaimGB > 0 && free >= opts.reclaimGB * BYTES_PER_GB)
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
if (await reclaimWorktree(root, candidate.dir))
|
|
150
|
-
cleaned.push(candidate.name);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// A guard failure (a directory that stopped being a linked worktree) is not a reason to
|
|
154
|
-
// abort the pass; the remaining candidates are still safe to clean.
|
|
155
|
+
else if (pressureFloorReached(sample(root), reclaimGB)) {
|
|
156
|
+
break;
|
|
155
157
|
}
|
|
158
|
+
if (await reclaimOneCandidate(root, candidate))
|
|
159
|
+
cleaned.push(candidate.name);
|
|
156
160
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
return cleaned;
|
|
162
|
+
}
|
|
163
|
+
/** Freed space as the before/after free-space difference, clamped at zero: another writer can
|
|
164
|
+
* consume space during the pass, and a negative "freed" would render as nonsense in the event
|
|
165
|
+
* and the digest. A null sample keeps the zero fallback. */
|
|
166
|
+
function reclaimFreedGB(before, after) {
|
|
167
|
+
return before !== null && after !== null ? Math.max(0, (after - before) / BYTES_PER_GB) : 0;
|
|
168
|
+
}
|
|
169
|
+
/** The pass result: the cleaned worktrees, the clamped freed and current free figures, and the
|
|
170
|
+
* pass duration. `freeGB` falls back to whichever sample we have when the other is null. */
|
|
171
|
+
function buildReclaimResult(cleaned, before, after, startedAt) {
|
|
160
172
|
const freeBytes = after ?? before ?? 0;
|
|
161
|
-
|
|
173
|
+
return {
|
|
162
174
|
worktrees: cleaned,
|
|
163
|
-
|
|
164
|
-
// would render as nonsense in the event and the digest.
|
|
165
|
-
freedGB: before !== null && after !== null ? Math.max(0, (after - before) / BYTES_PER_GB) : 0,
|
|
175
|
+
freedGB: reclaimFreedGB(before, after),
|
|
166
176
|
freeGB: freeBytes / BYTES_PER_GB,
|
|
167
177
|
durationMs: Date.now() - startedAt,
|
|
168
178
|
};
|
|
179
|
+
}
|
|
180
|
+
/** Log exactly one `disk_reclaim` event with the result's figures. */
|
|
181
|
+
function logReclaim(root, mode, result) {
|
|
169
182
|
logEventBestEffort(root, {
|
|
170
183
|
source: TUMWATER_SOURCE,
|
|
171
184
|
type: "disk_reclaim",
|
|
172
185
|
mode,
|
|
173
|
-
worktrees:
|
|
186
|
+
worktrees: result.worktrees,
|
|
174
187
|
freedGB: result.freedGB,
|
|
175
188
|
freeGB: result.freeGB,
|
|
176
189
|
durationMs: result.durationMs,
|
|
177
190
|
});
|
|
191
|
+
}
|
|
192
|
+
/** Clean pressure candidates until free space reaches `reclaimGB`, re-sampling after each, and
|
|
193
|
+
* log exactly one `disk_reclaim` when anything was cleaned. `idle` mode ignores the threshold
|
|
194
|
+
* and walks every candidate once (part 3/4). Returns null when nothing was reclaimed. */
|
|
195
|
+
export async function reclaimPass(root, mode, opts) {
|
|
196
|
+
const sample = opts.sample ?? sampleFreeBytes;
|
|
197
|
+
const candidates = opts.candidates ?? reclaimCandidates(root);
|
|
198
|
+
const startedAt = Date.now();
|
|
199
|
+
const before = sample(root);
|
|
200
|
+
// With no candidates the pass resolves synchronously (no re-sampling or cleaning to await),
|
|
201
|
+
// so a no-live-team Ctrl+E flashes "nothing to reclaim" within the caller's same turn.
|
|
202
|
+
const cleaned = candidates.length === 0 ? [] : await collectReclaimed(root, candidates, mode, opts.idleHours ?? 0, opts.reclaimGB, sample, startedAt);
|
|
203
|
+
if (cleaned.length === 0)
|
|
204
|
+
return null;
|
|
205
|
+
const after = sample(root);
|
|
206
|
+
const result = buildReclaimResult(cleaned, before, after, startedAt);
|
|
207
|
+
logReclaim(root, mode, result);
|
|
178
208
|
return result;
|
|
179
209
|
}
|
|
180
210
|
/** Dry run: every reclaim candidate with its idle age and what `git clean -ndX` would remove,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
3
2
|
import { finiteNumber, stringList } from "../files/json-object.js";
|
|
4
|
-
import { removeQuiet } from "../files/files.js";
|
|
3
|
+
import { removeIfPresent, removeQuiet } from "../files/files.js";
|
|
5
4
|
import { withStateLock } from "../concurrency/lock.js";
|
|
6
5
|
import { collapseWhitespace, truncate } from "../text/text.js";
|
|
7
6
|
import { pausedPath, pausedRolesLockPath, pausedRolesPath, scheduledStopPath } from "../paths.js";
|
|
@@ -128,15 +127,11 @@ export function pauseTeam(root, untilMs, reason) {
|
|
|
128
127
|
return true;
|
|
129
128
|
}
|
|
130
129
|
/** Resume the team by removing its marker (a no-op if absent); returns whether a marker was
|
|
131
|
-
* there to lift, mirroring pauseTeam's changed-state contract. Uses
|
|
130
|
+
* there to lift, mirroring pauseTeam's changed-state contract. Uses removeIfPresent's
|
|
132
131
|
* never-throws delete: a marker vanishing between the check and the unlink is success, not a
|
|
133
132
|
* failure worth surfacing from a toggle. */
|
|
134
133
|
export function resumeTeam(root) {
|
|
135
|
-
|
|
136
|
-
if (!fs.existsSync(marker))
|
|
137
|
-
return false;
|
|
138
|
-
removeQuiet(marker);
|
|
139
|
-
return true;
|
|
134
|
+
return removeIfPresent(pausedPath(root));
|
|
140
135
|
}
|
|
141
136
|
/** Schedule a graceful team stop at `untilMs` (epoch ms): `tumwater stop --in`. Overwrites the
|
|
142
137
|
* marker, so the last write wins. */
|
|
@@ -156,11 +151,7 @@ export function scheduledStopAt(root) {
|
|
|
156
151
|
}
|
|
157
152
|
/** Drop the scheduled stop; returns whether a marker was there (resumeTeam's contract). */
|
|
158
153
|
export function cancelScheduledStop(root) {
|
|
159
|
-
|
|
160
|
-
if (!fs.existsSync(marker))
|
|
161
|
-
return false;
|
|
162
|
-
removeQuiet(marker);
|
|
163
|
-
return true;
|
|
154
|
+
return removeIfPresent(scheduledStopPath(root));
|
|
164
155
|
}
|
|
165
156
|
/** The roles the operator has individually paused (`tumwater pause --role <id>`): a role in
|
|
166
157
|
* this set starts no new runs — in-flight ones finish, every other role keeps running, the
|
|
@@ -22,7 +22,7 @@ export function markdownTable(header, rows, align = []) {
|
|
|
22
22
|
* the `map((item, i) => `${i + 1}. ${item}`).join("\n")` step, shared by the review prompt's
|
|
23
23
|
* prior objections, its stage-fix findings, and the rejected/revision notes
|
|
24
24
|
* (prompt/gate-notes.ts), `tumwater questions`' open-question list
|
|
25
|
-
* (backlog/question-answer.ts) and `tumwater wake --list`'s wake lines
|
|
25
|
+
* (backlog/backlog-question-answer.ts) and `tumwater wake --list`'s wake lines
|
|
26
26
|
* (operator/operator-intent.ts `renderWakeList`), so the numbering shape cannot drift between
|
|
27
27
|
* those surfaces (the TUI queued-prompt rows and the merge queue rows number their own
|
|
28
28
|
* lines). `empty` is returned verbatim when `items` is empty
|
package/dist/src/text/phrases.js
CHANGED
|
@@ -25,8 +25,9 @@ export const PREFILL_REASON = "it rides into every run's prefill";
|
|
|
25
25
|
* drift across the four surfaces that show just that first reason: the review gate's returned
|
|
26
26
|
* detail (review.ts), the event feed's one-line reject (event-format.ts), the failure digest's
|
|
27
27
|
* rejection clusters (failure-data.ts), and the error a role's next run records (run-apply.ts).
|
|
28
|
-
* Sites that render the whole reason list, or intentionally omit the fallback
|
|
29
|
-
* first-reason example, review.ts's rejection event `reason`), keep
|
|
28
|
+
* Sites that render the whole reason list, or intentionally omit the fallback
|
|
29
|
+
* (failure-time-spend.ts's first-reason example, review.ts's rejection event `reason`), keep
|
|
30
|
+
* their own handling. */
|
|
30
31
|
export function firstReason(reasons) {
|
|
31
32
|
const first = reasons?.[0];
|
|
32
33
|
return typeof first === "string" ? first : NO_REASONS_GIVEN;
|
|
@@ -241,6 +242,21 @@ function withoutLeadingCd(command) {
|
|
|
241
242
|
rest = rest.replace(LEADING_CD, "");
|
|
242
243
|
return rest.trim() ? rest : command;
|
|
243
244
|
}
|
|
245
|
+
/** The first present candidate string in a tool call's args, or "" when none is a string.
|
|
246
|
+
* The read order — path, file_path, command, cmd, pattern, url — is the order INV-AE4 pins. */
|
|
247
|
+
function toolCallCandidate(args) {
|
|
248
|
+
const candidate = args.path ?? args.file_path ?? args.command ?? args.cmd ?? args.pattern ?? args.url;
|
|
249
|
+
return typeof candidate === "string" ? candidate : "";
|
|
250
|
+
}
|
|
251
|
+
/** How one candidate reads as a detail: a path-like key reduces to its file name, a
|
|
252
|
+
* command-like key drops a leading `cd`, and any other key shows verbatim. */
|
|
253
|
+
function toolCallDetail(candidate, args) {
|
|
254
|
+
if (candidate === args.path || candidate === args.file_path)
|
|
255
|
+
return path.basename(candidate);
|
|
256
|
+
if (candidate === args.command || candidate === args.cmd)
|
|
257
|
+
return withoutLeadingCd(candidate);
|
|
258
|
+
return candidate;
|
|
259
|
+
}
|
|
244
260
|
/** One-line description of a tool call from its name and args — shared by live progress data
|
|
245
261
|
* collection (LiveProgress.lastTool), transcript rendering, and the harness's stalled-tool-call
|
|
246
262
|
* warning (src/pi/pi-stream.ts builds the label src/pi/pi-watchdogs.ts renders). Path-like keys
|
|
@@ -248,19 +264,8 @@ function withoutLeadingCd(command) {
|
|
|
248
264
|
* is dropped from a command, because the run's cwd is implied and the path would use up the
|
|
249
265
|
* 32-character budget. */
|
|
250
266
|
export function describeToolCall(toolName, args) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const candidate = args.path ?? args.file_path ?? args.command ?? args.cmd ?? args.pattern ?? args.url;
|
|
254
|
-
if (typeof candidate === "string") {
|
|
255
|
-
detail =
|
|
256
|
-
candidate === args.path || candidate === args.file_path
|
|
257
|
-
? path.basename(candidate)
|
|
258
|
-
: candidate === args.command || candidate === args.cmd
|
|
259
|
-
? withoutLeadingCd(candidate)
|
|
260
|
-
: candidate;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
detail = squash(detail, 32);
|
|
267
|
+
const raw = isJsonObject(args) ? toolCallDetail(toolCallCandidate(args), args) : "";
|
|
268
|
+
const detail = squash(raw, 32);
|
|
264
269
|
// An empty name (pi omits toolName on some start events) yields the bare detail — no
|
|
265
270
|
// leading space in front of it.
|
|
266
271
|
return detail ? (toolName ? `${toolName} ${detail}` : detail) : toolName;
|
package/dist/src/text/text.js
CHANGED
|
@@ -94,9 +94,9 @@ export function gotSuffix(v) {
|
|
|
94
94
|
* and its legacy provider, exempt paths, custom-role tasks), pi-event-line.ts's tool-result content
|
|
95
95
|
* test, quiet-hours.ts's per-role window lookup, and inbox-attachments.ts's image-name check, so
|
|
96
96
|
* the blank-means-unset rule cannot drift per call site. Callers where a bare empty string means
|
|
97
|
-
* unset rather than blank text (notify.ts, version.ts, inbox-cancel.ts) keep their own
|
|
98
|
-
* check; typed-string callers (checkStringField's two-branch shape,
|
|
99
|
-
* report) spell the trim test inline. */
|
|
97
|
+
* unset rather than blank text (event-notify.ts, version.ts, inbox-cancel.ts) keep their own
|
|
98
|
+
* `=== ""` check; typed-string callers (checkStringField's two-branch shape,
|
|
99
|
+
* checkStringArray's blank-index report) spell the trim test inline. */
|
|
100
100
|
export function isNonBlankString(v) {
|
|
101
101
|
return typeof v === "string" && v.trim() !== "";
|
|
102
102
|
}
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import { openQuestionEntries, ageInDays } from "../backlog/backlog.js";
|
|
2
2
|
import { stripEntryStamp } from "../backlog/backlog-structure.js";
|
|
3
|
-
import { questionFilingDate } from "../backlog/question-answer.js";
|
|
3
|
+
import { questionFilingDate } from "../backlog/backlog-question-answer.js";
|
|
4
4
|
import { byDaysThenIndex } from "../collections.js";
|
|
5
|
-
import { eventsSince } from "../events/event-window.js";
|
|
6
5
|
import { STALE_QUESTION_DAYS } from "../doctor/doctor-backlog.js";
|
|
7
|
-
import { regressedFixCandidates, matchedRegressedFixes, regressedFixesPhrase } from "../doctor/doctor-backlog-bugs.js";
|
|
8
6
|
import { planDependencyProblem } from "../doctor/doctor-backlog-plans.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { configForStatus } from "../status/status-polls.js";
|
|
12
|
-
import { piModelsPath } from "../pi/pi-models.js";
|
|
13
|
-
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
7
|
+
import { findOrphans, orphanFailDetail } from "../doctor/doctor-orphans.js";
|
|
8
|
+
import { systemProcessProbe } from "../process/process-table.js";
|
|
14
9
|
/** This module is the alert-feed half of src/ui/status-payload.ts: each always-on team alert's
|
|
15
|
-
* data-sourcing function
|
|
10
|
+
* data-sourcing function lives here, a
|
|
16
11
|
* sibling of the src/ui/team-alert-*.ts builders that turn them into banners. status-payload.ts
|
|
17
12
|
* stays the one status-document builder, importing these feeds and folding each feed's result
|
|
18
13
|
* into the `alerts` field; status-render.ts reads the feeds directly for the TUI. */
|
|
@@ -46,70 +41,51 @@ export function staleOpenQuestions(root, now = Date.now()) {
|
|
|
46
41
|
export function planDependencyAlert(root) {
|
|
47
42
|
return planDependencyProblem(root);
|
|
48
43
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const verdict = resolve(eventsSince(root, windowMs, now, keep));
|
|
59
|
-
if (verdict.warn)
|
|
60
|
-
out.push({ name, detail: verdict.detail });
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// A read error skips this row, so the alert never cries wolf on an unreadable log.
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
maybeWarn("workspace slots", SLOT_WAIT_CHECK_WINDOW_MS, (ev) => ev.type === "slot_wait", (waits) => slotWaitContention(waits, readSlotsState(root).slots.length));
|
|
67
|
-
maybeWarn("provider slots", PROVIDER_WAIT_CHECK_WINDOW_MS, (ev) => ev.type === "provider_wait", (waits) => providerWaitContention(waits, configForStatus(root)));
|
|
68
|
-
maybeWarn("check slots", CHECK_WAIT_CHECK_WINDOW_MS, (e) => e.type === "check_wait", (waits) => checkWaitContention(waits, configForStatus(root)));
|
|
69
|
-
return out;
|
|
44
|
+
/** The orphan scan's alert shape, given a scan it is handed — the pure feed, so a test stages a
|
|
45
|
+
* fake probe's scan and gets exactly the row's message without a real host (the shared
|
|
46
|
+
* findOrphans/orphanFailDetail the doctor row reads, so the two cannot drift). `root` trims this
|
|
47
|
+
* repo's path from the commands, like the doctor row. */
|
|
48
|
+
export function orphanAlertFromScan(scan, root = "") {
|
|
49
|
+
if ("error" in scan || scan.orphans.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
const detail = orphanFailDetail(root, scan);
|
|
52
|
+
return detail === null ? null : { count: scan.orphans.length, detail };
|
|
70
53
|
}
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
* a
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
* has struck; this standing alert names the static misconfiguration itself, which the doctor
|
|
77
|
-
* row is the only other surface that otherwise exposes. Reads the same last-known-good config
|
|
78
|
-
* the snapshot uses (configForStatus) and audits it through the row's own
|
|
79
|
-
* providerConcurrencyProblems, so the alert and the row cannot drift on the trigger or the
|
|
80
|
-
* wording; `[]` when no cap is configured or the map is well-formed. */
|
|
81
|
-
export function providerCapProblems(root) {
|
|
82
|
-
return providerConcurrencyProblems(configForStatus(root), readPiDefaultModel(piSettingsPath(piModelsPath()), root));
|
|
54
|
+
/** Run the doctor's orphan scan and shape it into the standing alert. Async (it does the real
|
|
55
|
+
* process-table read); the always-on surfaces read it through getOrphanAlert's cache below, so
|
|
56
|
+
* a status frame never blocks on the host scan. */
|
|
57
|
+
export async function refreshOrphanAlert(root, probe = systemProcessProbe) {
|
|
58
|
+
return orphanAlertFromScan(await findOrphans(root, probe), root);
|
|
83
59
|
}
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
60
|
+
/** How stale a cached orphan scan may be before the next status read starts a fresh one:
|
|
61
|
+
* orphan leaks grow over days, so a sixty-second refresh is plenty and keeps the host scan
|
|
62
|
+
* (one `ps`, plus cwd reads only for the parentless candidates argv did not settle) off the
|
|
63
|
+
* hot render path. */
|
|
64
|
+
const ORPHAN_ALERT_REFRESH_MS = 60_000;
|
|
65
|
+
let orphanCache = null;
|
|
66
|
+
let orphanLoading = false;
|
|
67
|
+
/** The synchronous orphan feed the always-on surfaces (status-payload, status-render, the TUI)
|
|
68
|
+
* read each frame: the last scan's value while it is fresher than ORPHAN_ALERT_REFRESH_MS, else a
|
|
69
|
+
* background refresh is kicked (one at a time) and the previous value — null on the first ever
|
|
70
|
+
* read — is returned until that scan lands. The refresh runs off the render path, so a slow host
|
|
71
|
+
* scan never stalls a status frame. Per-process: each long-lived surface (the GUI server, the
|
|
72
|
+
* TUI) and the CLI each run their own throttled scan against the same doctor rule. */
|
|
73
|
+
export function getOrphanAlert(root, probe = systemProcessProbe, now = Date.now()) {
|
|
74
|
+
if (orphanCache === null || now - orphanCache.at >= ORPHAN_ALERT_REFRESH_MS) {
|
|
75
|
+
if (!orphanLoading) {
|
|
76
|
+
orphanLoading = true;
|
|
77
|
+
void (async () => {
|
|
78
|
+
try {
|
|
79
|
+
orphanCache = { value: await refreshOrphanAlert(root, probe), at: Date.now() };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
orphanCache = { value: null, at: Date.now() };
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
orphanLoading = false;
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return orphanCache === null ? null : orphanCache.value;
|
|
111
91
|
}
|
|
112
|
-
/** Re-export so the always-on alert's stale-pin selector stays in its one home
|
|
113
|
-
* (src/doctor/doctor-contention-checks.ts) while tui.tsx keeps importing every alert feed from
|
|
114
|
-
* this one module. */
|
|
115
|
-
export { staleWorkspacePins };
|