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
|
@@ -14,7 +14,7 @@ import { eventRole, readEvents } from "../events/event-read.js";
|
|
|
14
14
|
import { eventFeedItem, eventMatchesGrep } from "../events/event-format.js";
|
|
15
15
|
import { readRunDetail, runCommitSha, runMergedSha } from "../run/run-detail-data.js";
|
|
16
16
|
import { renderRunDetail, runDetailPatch, runDetailStat, runTranscriptLines } from "../run/run-detail-render.js";
|
|
17
|
-
import { resolveRevertTarget } from "../operator/revert.js";
|
|
17
|
+
import { resolveRevertTarget } from "../operator/operator-revert.js";
|
|
18
18
|
import { rolePayload } from "../roles/role-view.js";
|
|
19
19
|
import { clampCount, intQuery, rejectBadRole, sinceMsQuery, untilMsQuery } from "./gui-args.js";
|
|
20
20
|
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
@@ -124,36 +124,67 @@ function windowOrCount(q, res) {
|
|
|
124
124
|
return { kind: "count" };
|
|
125
125
|
}
|
|
126
126
|
export function handleHistory(q, res, root) {
|
|
127
|
+
const req = historyRequest(q, res);
|
|
128
|
+
if (req === null)
|
|
129
|
+
return;
|
|
130
|
+
const ctl = req.windowCtl;
|
|
131
|
+
if (ctl.kind === "window")
|
|
132
|
+
serveHistoryWindow(res, root, req, ctl.sinceMs, ctl.untilMs);
|
|
133
|
+
else
|
|
134
|
+
serveHistoryCount(q, res, root, req);
|
|
135
|
+
}
|
|
136
|
+
/** Parse the request prefix `GET /api/history` shares across both shapes: the `role`/`grep`
|
|
137
|
+
* filters, the `result` class list (resultClassesQuery, a 400 on an unknown class), the `model`
|
|
138
|
+
* selector list, the `summary` flag's mere presence, and the rival `sinceMs`-vs-`n` shape
|
|
139
|
+
* (windowOrCount, a 400 on both or a bad cap). Any 400 is already sent and yields null. */
|
|
140
|
+
function historyRequest(q, res) {
|
|
127
141
|
const role = q.get("role") || null; // "" and absent both read all roles
|
|
128
142
|
const grep = q.get("grep") || null; // "" and absent both filter nothing
|
|
129
143
|
// `model` is a comma list of exact selectors (trimmed, deduplicated); "", absent and a list
|
|
130
144
|
// with no non-empty piece all filter nothing, and an unknown selector keeps no rows.
|
|
131
145
|
const classes = resultClassesQuery(q, res);
|
|
132
146
|
if (classes === null)
|
|
133
|
-
return;
|
|
134
|
-
const inClasses = (rows) => (classes.length ? rows.filter((r) => classes.includes(historyClass(r.result))) : rows);
|
|
147
|
+
return null;
|
|
135
148
|
const modelPieces = splitCommaListUnique(q.get("model") ?? "");
|
|
136
|
-
const onModels = (rows) => (modelPieces.length ? runRowsOnModels(rows, modelPieces) : rows);
|
|
137
149
|
const summary = q.get("summary") !== null; // the flag's presence, whatever its value
|
|
138
150
|
const windowCtl = windowOrCount(q, res);
|
|
139
151
|
if (windowCtl === null)
|
|
140
|
-
return;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
return null;
|
|
153
|
+
return { role, grep, classes, modelPieces, summary, windowCtl };
|
|
154
|
+
}
|
|
155
|
+
/** The shared filter chain both shapes apply to the served rows: grep first, then the `result`
|
|
156
|
+
* outcome classes, then the `model` selectors — the CLI's `history --grep`, `--result` and
|
|
157
|
+
* `--model` rules, in that order. */
|
|
158
|
+
function historyRows(rows, req) {
|
|
159
|
+
const grepped = req.grep === null ? rows : runRowsMatchingGrep(rows, req.grep);
|
|
160
|
+
const classed = req.classes.length ? grepped.filter((r) => req.classes.includes(historyClass(r.result))) : grepped;
|
|
161
|
+
return req.modelPieces.length ? runRowsOnModels(classed, req.modelPieces) : classed;
|
|
162
|
+
}
|
|
163
|
+
/** The optional additive fold both answers share: when `summary` is present, `{ summary,
|
|
164
|
+
* modelSummary }` added to the 200 payload; absent, nothing (so the response is byte-for-byte
|
|
165
|
+
* today's). */
|
|
166
|
+
function historySummaryFields(rows, summary) {
|
|
167
|
+
return summary ? { summary: summarizeRunRows(rows), modelSummary: summarizeRunRowsByModel(rows) } : {};
|
|
168
|
+
}
|
|
169
|
+
/** Answer the trailing-window shape ({ rows, covered, note }): rows read through readRunRowsSince
|
|
170
|
+
* from `sinceMs` (untilMs drops the rows newer than now − untilMs when present, exactly as the
|
|
171
|
+
* CLI's `history --since … --until` window reads), then the shared historyRows filter, then the
|
|
172
|
+
* optional summary fold — key order rows, covered, note, summary, modelSummary. */
|
|
173
|
+
function serveHistoryWindow(res, root, req, sinceMs, untilMs) {
|
|
174
|
+
const { rows, covered } = readRunRowsSince(root, sinceMs, req.role, untilMs ?? null);
|
|
175
|
+
const matched = historyRows(rows, req);
|
|
176
|
+
sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...historySummaryFields(matched, req.summary) });
|
|
177
|
+
}
|
|
178
|
+
/** Answer the newest-`n` count shape: `n` parses through intQuery (non-negative, default
|
|
179
|
+
* HISTORY_DEFAULT_RUNS) and is clamped to [1, HISTORY_MAX_RUNS] — the GUI clamps where the CLI
|
|
180
|
+
* fails fast — then rows read through readRunRows, filtered and answered through the shared
|
|
181
|
+
* historyRows/historySummaryFields, key order rows, summary, modelSummary. */
|
|
182
|
+
function serveHistoryCount(q, res, root, req) {
|
|
148
183
|
const parsed = intQuery(q, res, "n", "non-negative", HISTORY_DEFAULT_RUNS);
|
|
149
184
|
if (parsed === null)
|
|
150
185
|
return;
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
if (grep !== null)
|
|
154
|
-
rows = runRowsMatchingGrep(rows, grep);
|
|
155
|
-
rows = onModels(inClasses(rows));
|
|
156
|
-
sendJson(res, 200, summary ? { rows, summary: summarizeRunRows(rows), modelSummary: summarizeRunRowsByModel(rows) } : { rows });
|
|
186
|
+
const rows = historyRows(readRunRows(root, clampCount(parsed, HISTORY_MAX_RUNS), req.role), req);
|
|
187
|
+
sendJson(res, 200, { rows, ...historySummaryFields(rows, req.summary) });
|
|
157
188
|
}
|
|
158
189
|
/** How many of the log's newest events `GET /api/logs` scans by default, and the most it will
|
|
159
190
|
* scan however large `n` asks: the CLI's `-n` shape (n bounds the scanned tail, not the printed
|
|
@@ -245,47 +276,64 @@ function transcriptText(root, role, run, detail, includePrompts, grep) {
|
|
|
245
276
|
* payload's `role` is that run's; `result`/`model` narrow it the same way, and a miss is a 404
|
|
246
277
|
* worded for any role. */
|
|
247
278
|
export async function handleRun(q, res, root) {
|
|
248
|
-
|
|
279
|
+
const target = runSelector(q, res, root);
|
|
280
|
+
if (target === null)
|
|
281
|
+
return;
|
|
282
|
+
const detail = readRunDetail(root, target.role, target.run);
|
|
283
|
+
if (detail === null) {
|
|
284
|
+
sendJson(res, 404, { error: runNotFoundMessage(target.role, target.run) });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
sendJson(res, 200, await runPayload(root, target.role, target.run, detail, q));
|
|
288
|
+
}
|
|
289
|
+
/** Resolve which run the request names — validating the role, then the numbered or `last` form. */
|
|
290
|
+
function runSelector(q, res, root) {
|
|
291
|
+
const role = q.get("role");
|
|
249
292
|
const bareLast = role === null && q.get("last") !== null;
|
|
250
293
|
if (!bareLast && rejectBadRole(root, res, role))
|
|
251
|
-
return;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (run === null)
|
|
260
|
-
return;
|
|
294
|
+
return null;
|
|
295
|
+
return q.get("last") !== null ? lastRunSelector(q, res, root, role) : numberedRunSelector(q, res, role);
|
|
296
|
+
}
|
|
297
|
+
/** Resolve the explicit `run=N` selector of /api/run (three 400s, the miss worded by intQuery). */
|
|
298
|
+
function numberedRunSelector(q, res, role) {
|
|
299
|
+
if (q.get("result") !== null) {
|
|
300
|
+
badRequest(res, "result needs last");
|
|
301
|
+
return null;
|
|
261
302
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const classes = resultClassesQuery(q, res);
|
|
266
|
-
if (classes === null)
|
|
267
|
-
return;
|
|
268
|
-
const models = splitCommaListUnique(q.get("model") ?? "");
|
|
269
|
-
const classFilter = classes.length ? classes : null;
|
|
270
|
-
const modelFilter = models.length ? models : null;
|
|
271
|
-
const row = newestRunRow(root, role, classFilter, modelFilter);
|
|
272
|
-
if (row !== null)
|
|
273
|
-
role = row.role;
|
|
274
|
-
run = row?.run ?? null;
|
|
275
|
-
if (run === null) {
|
|
276
|
-
sendJson(res, 404, { error: runNotFoundLastMessage(role, classFilter, modelFilter) });
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
303
|
+
if (q.get("model") !== null) {
|
|
304
|
+
badRequest(res, "model needs last");
|
|
305
|
+
return null;
|
|
279
306
|
}
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
307
|
+
const run = intQuery(q, res, "run", "positive");
|
|
308
|
+
if (run === null)
|
|
309
|
+
return null;
|
|
310
|
+
return { role: role, run };
|
|
311
|
+
}
|
|
312
|
+
/** Resolve the `last` selector, narrowed by `result`/`model`, to the role's newest run. */
|
|
313
|
+
function lastRunSelector(q, res, root, role) {
|
|
314
|
+
if (q.get("run") !== null) {
|
|
315
|
+
badRequest(res, "last cannot be combined with run (a number and the newest run are rival selectors)");
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const classes = resultClassesQuery(q, res);
|
|
319
|
+
if (classes === null)
|
|
320
|
+
return null;
|
|
321
|
+
const models = splitCommaListUnique(q.get("model") ?? "");
|
|
322
|
+
const classFilter = classes.length ? classes : null;
|
|
323
|
+
const modelFilter = models.length ? models : null;
|
|
324
|
+
const row = newestRunRow(root, role, classFilter, modelFilter);
|
|
325
|
+
if (row === null) {
|
|
326
|
+
sendJson(res, 404, { error: runNotFoundLastMessage(role, classFilter, modelFilter) });
|
|
327
|
+
return null;
|
|
284
328
|
}
|
|
329
|
+
return { role: row.role, run: row.run };
|
|
330
|
+
}
|
|
331
|
+
/** Assemble the 200 payload — the collector plus its rendered, stat and flag-gated fields. */
|
|
332
|
+
async function runPayload(root, role, run, detail, q) {
|
|
285
333
|
const grep = (q.get("grep") ?? "").trim();
|
|
286
334
|
const mergedAs = runMergedSha(detail);
|
|
287
335
|
const target = mergedAs === null ? null : await resolveRevertTarget(root, mergedAs);
|
|
288
|
-
|
|
336
|
+
return {
|
|
289
337
|
...detail,
|
|
290
338
|
text: renderRunDetail(detail),
|
|
291
339
|
stat: await runDetailStat(root, detail),
|
|
@@ -293,7 +341,7 @@ export async function handleRun(q, res, root) {
|
|
|
293
341
|
hasCommit: runCommitSha(detail) !== null,
|
|
294
342
|
...(q.get("patch") !== null ? { patch: await runDetailPatch(root, detail) } : {}),
|
|
295
343
|
...(q.get("transcript") !== null ? { transcript: transcriptText(root, role, run, detail, q.get("prompt") !== null, grep === "" ? undefined : grep) } : {}),
|
|
296
|
-
}
|
|
344
|
+
};
|
|
297
345
|
}
|
|
298
346
|
/** Handle GET /api/role?role=<id>: one role's inspection payload — rolePayload's object,
|
|
299
347
|
* the same collection behind `tumwater role <id>` — so the drawer's Notes and Next run
|
|
@@ -18,15 +18,15 @@ import { loadConfigSafe } from "../config/config.js";
|
|
|
18
18
|
import { saveRoleNote } from "../agent/role-notes-write.js";
|
|
19
19
|
import { roleNotesPath, MERGE_LOCK_WAIT_MS } from "../paths.js";
|
|
20
20
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
21
|
-
import { artifactPhrase, retireRole } from "../operator/retire.js";
|
|
22
|
-
import { previewRevert, revertFollowUp, revertMergedCommit } from "../operator/revert.js";
|
|
21
|
+
import { artifactPhrase, retireRole } from "../operator/operator-retire.js";
|
|
22
|
+
import { previewRevert, revertFollowUp, revertMergedCommit } from "../operator/operator-revert.js";
|
|
23
23
|
import { cancelQueuedMergeBySha } from "../merge/merge-cancel.js";
|
|
24
24
|
import { acceptPostReviewBySha } from "../review/post-merge-review-accept.js";
|
|
25
25
|
import { moveQueuedMergeToTopBySha } from "../merge/merge-top.js";
|
|
26
26
|
import { readMergeQueue } from "../merge/merge-queue.js";
|
|
27
27
|
import { mergeChanges } from "../merge/merge-slot.js";
|
|
28
28
|
import { queueInFlight } from "../status/status-polls.js";
|
|
29
|
-
import { orchestratorAlive } from "../team/orchestrator-info.js";
|
|
29
|
+
import { orchestratorAlive } from "../team/team-orchestrator-info.js";
|
|
30
30
|
import { shortSha } from "../text/format.js";
|
|
31
31
|
import { editBriefText } from "../brief/brief-edit.js";
|
|
32
32
|
import { requireStringField, validRoleIds } from "./gui-args.js";
|
|
@@ -34,6 +34,21 @@ import { badRequest, sendJson } from "./gui-http-body.js";
|
|
|
34
34
|
import { errorMessage, gotSuffix } from "../text/text.js";
|
|
35
35
|
import { readPostBody, readRoleBody, sendRequestResult } from "./gui-endpoint-common.js";
|
|
36
36
|
import { wakeInSecondsMs } from "./gui-team-commands.js";
|
|
37
|
+
/** The shared body of the dashboard's Health/Action buttons that run a domain function and
|
|
38
|
+
* answer its text lines joined with "; ": readPostBody (400/413 sent on a null return) plus the
|
|
39
|
+
* one sendJson built from the caller's `ok` predicate and line renderer. kill-orphans,
|
|
40
|
+
* drop-broken-pins, retire-leftovers and resync-install each restated this read-then-reply
|
|
41
|
+
* flow, differing only in what they run and how they say ok; this is the one home so the body
|
|
42
|
+
* discipline and the "; " joining cannot drift between them. commit-backlog and
|
|
43
|
+
* archive-backlog deliberately do not ride it (they carry extra payload fields or multi-branch
|
|
44
|
+
* replies), and neither does sendRequestResult's marker family (a different reply shape). */
|
|
45
|
+
async function runLinesAction(req, res, root, run, ok, lines) {
|
|
46
|
+
const body = await readPostBody(req, res, "{}");
|
|
47
|
+
if (!body)
|
|
48
|
+
return;
|
|
49
|
+
const report = await run(root);
|
|
50
|
+
sendJson(res, 200, { ok: ok(report), message: lines(report).join("; ") });
|
|
51
|
+
}
|
|
37
52
|
/** Handle POST /api/reset-counters: the dashboard's counter reset — the same marker-writing
|
|
38
53
|
* core `tumwater reset-counters` calls (requestResetCounters). Same body discipline and
|
|
39
54
|
* `{}` → every configured role default as /api/wake. */
|
|
@@ -97,22 +112,14 @@ export async function handleReclaim(req, res, root) {
|
|
|
97
112
|
* killOrphans `tumwater doctor --kill-orphans` runs, answering its lines joined with "; ". Same
|
|
98
113
|
* body discipline as /api/reclaim; `ok` is false when the scan failed or a signal was refused. */
|
|
99
114
|
export async function handleKillOrphans(req, res, root, kill = killOrphans) {
|
|
100
|
-
|
|
101
|
-
if (!body)
|
|
102
|
-
return;
|
|
103
|
-
const report = await kill(root);
|
|
104
|
-
sendJson(res, 200, { ok: !("error" in report) && report.failed.length === 0, message: orphanKillLines(report).join("; ") });
|
|
115
|
+
return runLinesAction(req, res, root, kill, (r) => !("error" in r) && r.failed.length === 0, orphanKillLines);
|
|
105
116
|
}
|
|
106
117
|
/** Handle POST /api/doctor-drop-broken-pins: the dashboard Health card's Drop broken pins
|
|
107
118
|
* button — the same dropBrokenPins `tumwater doctor --drop-broken-pins` runs, answering its
|
|
108
119
|
* lines joined with "; ". `ok` is false when any pin could not be dropped. Same body discipline
|
|
109
120
|
* as /api/kill-orphans; `drop` is injectable so a test need not touch git. */
|
|
110
121
|
export async function handleDropBrokenPins(req, res, root, drop = dropBrokenPins) {
|
|
111
|
-
|
|
112
|
-
if (!body)
|
|
113
|
-
return;
|
|
114
|
-
const result = await drop(root);
|
|
115
|
-
sendJson(res, 200, { ok: result.failed.length === 0, message: brokenPinDropLines(result).join("; ") });
|
|
122
|
+
return runLinesAction(req, res, root, drop, (r) => r.failed.length === 0, brokenPinDropLines);
|
|
116
123
|
}
|
|
117
124
|
/** Handle POST /api/doctor-retire-leftovers: the dashboard Health card's Retire leftovers
|
|
118
125
|
* button — the same retireLeftovers `tumwater doctor --retire-leftovers` runs (with the loaded
|
|
@@ -120,11 +127,7 @@ export async function handleDropBrokenPins(req, res, root, drop = dropBrokenPins
|
|
|
120
127
|
* retire was refused. Same body discipline as /api/kill-orphans; `retire` is injectable so a
|
|
121
128
|
* test need not touch git. */
|
|
122
129
|
export async function handleRetireLeftovers(req, res, root, retire = (r) => retireLeftovers(r, loadConfigSafe(r).config)) {
|
|
123
|
-
|
|
124
|
-
if (!body)
|
|
125
|
-
return;
|
|
126
|
-
const report = await retire(root);
|
|
127
|
-
sendJson(res, 200, { ok: report.failed.length === 0, message: leftoverRetireLines(report).join("; ") });
|
|
130
|
+
return runLinesAction(req, res, root, retire, (r) => r.failed.length === 0, leftoverRetireLines);
|
|
128
131
|
}
|
|
129
132
|
/** Handle POST /api/doctor-commit-backlog: the dashboard Health card's Commit backlog button —
|
|
130
133
|
* the same commitUncommittedBacklog `tumwater doctor --commit-backlog` runs, answering its
|
|
@@ -177,11 +180,7 @@ export async function handleArchiveBacklog(req, res, root, archive = (r) => arch
|
|
|
177
180
|
* hold the merge lock or touch the registry. resyncRootInstall never throws, so a lock/blog
|
|
178
181
|
* failure lands as the failure line, not a 500. */
|
|
179
182
|
export async function handleResyncInstall(req, res, root, resync = resyncRootInstall) {
|
|
180
|
-
|
|
181
|
-
if (!body)
|
|
182
|
-
return;
|
|
183
|
-
const r = await resync(root);
|
|
184
|
-
sendJson(res, 200, { ok: r.ok, message: rootInstallResyncLines(r).join("; ") });
|
|
183
|
+
return runLinesAction(req, res, root, resync, (r) => r.ok, rootInstallResyncLines);
|
|
185
184
|
}
|
|
186
185
|
/** Handle POST /api/abort: the dashboard's per-row abort control — the same marker-writing
|
|
187
186
|
* core `tumwater abort` calls (requestAbort). The role is required and validated like
|
|
@@ -139,14 +139,20 @@ export async function handlePromptRole(req, res, root) {
|
|
|
139
139
|
], "\n");
|
|
140
140
|
sendJson(res, 200, { ok: true, message });
|
|
141
141
|
}
|
|
142
|
-
/** The file-addressed prompt handlers' shared role prologue:
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
142
|
+
/** The file-addressed prompt handlers' shared role+file prologue: the optional role (400 on
|
|
143
|
+
* a present-but-invalid one, absent defaulting to the director's queue) and the queue-file
|
|
144
|
+
* name (400 on a bad one) validate exactly like /api/transcript and /api/prompt-cancel,
|
|
145
|
+
* with nothing read on disk, and return the resolved role and file — or null when a 400 was
|
|
146
|
+
* sent (the caller stops). handlePromptCancel, handlePromptEdit and handlePromptTop state the
|
|
147
|
+
* guard-plus-default pair once here instead of at each call site, so the three endpoints
|
|
148
|
+
* cannot drift on which role or file they accept. */
|
|
149
|
+
function promptFileTarget(body, res, root) {
|
|
147
150
|
if (body.role !== undefined && rejectBadRole(root, res, body.role))
|
|
148
151
|
return null;
|
|
149
|
-
|
|
152
|
+
const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
|
|
153
|
+
if (rejectBadQueueFileName(res, body.file))
|
|
154
|
+
return null;
|
|
155
|
+
return { role, file: body.file };
|
|
150
156
|
}
|
|
151
157
|
/** Handle POST /api/prompt-cancel: retract one queued prompt addressed by its queue file —
|
|
152
158
|
* the dashboard's per-row cancel affordance, the file-addressed twin of the CLI's
|
|
@@ -160,12 +166,10 @@ export async function handlePromptCancel(req, res, root) {
|
|
|
160
166
|
const body = await readPostBody(req, res, '{"role": "feature", "file": "<stamp>-<seq>-<pid>.md"}');
|
|
161
167
|
if (!body)
|
|
162
168
|
return;
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
169
|
+
const target = promptFileTarget(body, res, root);
|
|
170
|
+
if (target === null)
|
|
165
171
|
return;
|
|
166
|
-
|
|
167
|
-
return;
|
|
168
|
-
const outcome = cancelQueuedFile(root, role, body.file);
|
|
172
|
+
const outcome = cancelQueuedFile(root, target.role, target.file);
|
|
169
173
|
sendJson(res, 200, outcome.status === "cancelled"
|
|
170
174
|
? { ok: true, status: "cancelled", preview: promptPreview(outcome.text) }
|
|
171
175
|
: OK_GONE);
|
|
@@ -186,23 +190,21 @@ export async function handlePromptEdit(req, res, root) {
|
|
|
186
190
|
const body = await readPostBody(req, res, '{"role": "qa", "file": "<stamp>.md", "text": "..."}');
|
|
187
191
|
if (!body)
|
|
188
192
|
return;
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
193
|
+
const target = promptFileTarget(body, res, root);
|
|
194
|
+
if (target === null)
|
|
191
195
|
return;
|
|
192
|
-
|
|
193
|
-
return;
|
|
194
|
-
const fields = checkedPromptFields(res, body, role);
|
|
196
|
+
const fields = checkedPromptFields(res, body, target.role);
|
|
195
197
|
if (!fields)
|
|
196
198
|
return;
|
|
197
199
|
// Image-input gate (plans/image-input.md "Attachments", part 5/5): an image edited into
|
|
198
200
|
// the queued prompt answers 400 naming the fix when the target role cannot read it (and no
|
|
199
201
|
// tier reroutes it) — the same gate submitRolePrompt enforces — before anything is written.
|
|
200
202
|
// The edit handler does not flash a reroute notice, so it ignores the helper's return.
|
|
201
|
-
if (promptImageGate(res, root, role, (fields.images?.length ?? 0) > 0) === null)
|
|
203
|
+
if (promptImageGate(res, root, target.role, (fields.images?.length ?? 0) > 0) === null)
|
|
202
204
|
return;
|
|
203
205
|
let outcome;
|
|
204
206
|
try {
|
|
205
|
-
outcome = editQueuedFile(root, role,
|
|
207
|
+
outcome = editQueuedFile(root, target.role, target.file, fields.text, fields.images);
|
|
206
208
|
}
|
|
207
209
|
catch (err) {
|
|
208
210
|
badRequest(res, errorMessage(err));
|
|
@@ -225,14 +227,12 @@ export async function handlePromptTop(req, res, root) {
|
|
|
225
227
|
const body = await readPostBody(req, res, '{"role": "qa", "file": "<stamp>.md"}');
|
|
226
228
|
if (!body)
|
|
227
229
|
return;
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
230
|
-
return;
|
|
231
|
-
if (rejectBadQueueFileName(res, body.file))
|
|
230
|
+
const target = promptFileTarget(body, res, root);
|
|
231
|
+
if (target === null)
|
|
232
232
|
return;
|
|
233
233
|
let outcome;
|
|
234
234
|
try {
|
|
235
|
-
outcome = topQueuedFile(root, role,
|
|
235
|
+
outcome = topQueuedFile(root, target.role, target.file);
|
|
236
236
|
}
|
|
237
237
|
catch (err) {
|
|
238
238
|
badRequest(res, errorMessage(err));
|
|
@@ -9,7 +9,7 @@ import { collectReport } from "../report/report-data.js";
|
|
|
9
9
|
import { collectReportSince } from "../report/report-since-data.js";
|
|
10
10
|
import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
|
|
11
11
|
import { renderFailureMarkdown } from "../failure/failure-render.js";
|
|
12
|
-
import { renderSinceReportMarkdown } from "../report/report-render.js";
|
|
12
|
+
import { renderSinceReportMarkdown } from "../report/report-since-render.js";
|
|
13
13
|
import { loadQuarantine } from "../review/known-flakes.js";
|
|
14
14
|
import { quarantineRows, quarantineViewRows } from "../review/quarantine-render.js";
|
|
15
15
|
import { quarantineWarning } from "../baseline/main-guardrails.js";
|
|
@@ -179,58 +179,85 @@ export function runRowsOnModels(rows, models) {
|
|
|
179
179
|
* readEvents returns them), filtered to `role` when given, newest first. Pure: the CLI, the
|
|
180
180
|
* GUI, and the tests share this collector, and none of them writes anything. */
|
|
181
181
|
export function runRows(events, limit, role, join) {
|
|
182
|
-
const wanted =
|
|
183
|
-
const scoped =
|
|
184
|
-
|
|
185
|
-
// and the digest's fold cannot drift into different notions of a run's span.
|
|
186
|
-
const starts = runStartMap(scoped);
|
|
187
|
-
const models = runStartModelMap(scoped);
|
|
188
|
-
// The merge bookkeeping resolveQueuedResult joins over: per role, the merge_queued pins
|
|
189
|
-
// (oldest first) and the merge_done/merge_failed outcomes (oldest first). Both ride the role
|
|
190
|
-
// filter with their run, so the scoped events hold them exactly when they hold the row.
|
|
191
|
-
// A since-shaped scan hands a WIDER join read instead (readRunRowsSince): its cutoff cut
|
|
192
|
-
// the pins of runs that span it, so the maps read the unfiltered-by-cutoff evidence and
|
|
193
|
-
// resolveQueuedResult guards each claim on the row's run_start being whole in that read.
|
|
194
|
-
const joinScoped = join
|
|
195
|
-
? wanted === null
|
|
196
|
-
? join.events
|
|
197
|
-
: join.events.filter((e) => wanted.has(e.source))
|
|
198
|
-
: scoped;
|
|
199
|
-
const joinStarts = join ? runStartMap(joinScoped) : starts;
|
|
200
|
-
const joinModels = join ? runStartModelMap(joinScoped) : models;
|
|
201
|
-
const floorTs = join ? join.floorTs : null;
|
|
202
|
-
const { mergeQueuedByRunner, outcomeByRunner } = bucketMergeEvents(joinScoped);
|
|
203
|
-
const claimTop = new Map();
|
|
182
|
+
const wanted = roleWantSet(role);
|
|
183
|
+
const scoped = scopedEvents(events, wanted);
|
|
184
|
+
const evidence = runRowEvidence(scoped, join, wanted);
|
|
204
185
|
const rows = [];
|
|
205
186
|
for (let i = scoped.length - 1; i >= 0 && rows.length < limit; i--) {
|
|
206
187
|
const e = scoped[i];
|
|
207
188
|
if (e?.type !== "run_end")
|
|
208
189
|
continue;
|
|
209
|
-
|
|
210
|
-
const result = rawResult === "queued"
|
|
211
|
-
? (resolveQueuedResult(e, mergeQueuedByRunner, outcomeByRunner, claimTop, joinStarts, floorTs)
|
|
212
|
-
?.result ?? rawResult)
|
|
213
|
-
: rawResult;
|
|
214
|
-
const usage = eventUsage(e);
|
|
215
|
-
const peak = nonNegativeNumber(e.peakContextTokens, 0);
|
|
216
|
-
rows.push({
|
|
217
|
-
ts: e.ts,
|
|
218
|
-
time: formatTimestamp(e.ts),
|
|
219
|
-
tokens: usage.tokens,
|
|
220
|
-
costUsd: usage.costUsd,
|
|
221
|
-
...(peak > 0 ? { peakContextTokens: peak } : {}),
|
|
222
|
-
...(e.cutOff === true ? { cutOff: true } : {}),
|
|
223
|
-
role: eventRole(e),
|
|
224
|
-
run: eventRun(e),
|
|
225
|
-
result,
|
|
226
|
-
model: runStartModel(e, join ? joinModels : models),
|
|
227
|
-
durationMs: runSpanMs(e, starts),
|
|
228
|
-
usage: usageText(e),
|
|
229
|
-
detail: squash(textOr(e.summary, textOr(e.error, "")), DETAIL_MAX),
|
|
230
|
-
});
|
|
190
|
+
rows.push(runRowFromEnd(e, evidence));
|
|
231
191
|
}
|
|
232
192
|
return rows;
|
|
233
193
|
}
|
|
194
|
+
/** The role the scan is filtered to, as a direct set lookup: null means no filter (every
|
|
195
|
+
* event is wanted); otherwise the role list folded into a Set, so both the row scan and the
|
|
196
|
+
* merge-join read test membership in one `has`. */
|
|
197
|
+
function roleWantSet(role) {
|
|
198
|
+
return role === null ? null : new Set(asList(role));
|
|
199
|
+
}
|
|
200
|
+
/** The `events` held by the scan: all of them when `wanted` is null (no role filter), else
|
|
201
|
+
* those whose `source` the role set holds — the one role filter, used for both the row scan
|
|
202
|
+
* and the join read, so the two cannot drift. */
|
|
203
|
+
function scopedEvents(events, wanted) {
|
|
204
|
+
return wanted === null ? events : events.filter((e) => wanted.has(e.source));
|
|
205
|
+
}
|
|
206
|
+
/** Build the row scan's evidence from the scoped events and an optional wider join read. The
|
|
207
|
+
* start pairing is the shared helper (events.ts), so history's dash-on-unpaired rule and the
|
|
208
|
+
* digest's fold cannot drift into different notions of a run's span. The merge bookkeeping
|
|
209
|
+
* resolveQueuedResult joins over: per role, the merge_queued pins (oldest first) and the
|
|
210
|
+
* merge_done/merge_failed outcomes (oldest first). Both ride the role filter with their run,
|
|
211
|
+
* so the scoped events hold them exactly when they hold the row.
|
|
212
|
+
* A since-shaped scan hands a WIDER join read instead (readRunRowsSince): its cutoff cut the
|
|
213
|
+
* pins of runs that span it, so the maps read the unfiltered-by-cutoff evidence and
|
|
214
|
+
* resolveQueuedResult guards each claim on the row's run_start being whole in that read. */
|
|
215
|
+
function runRowEvidence(scoped, join, wanted) {
|
|
216
|
+
const starts = runStartMap(scoped);
|
|
217
|
+
const models = runStartModelMap(scoped);
|
|
218
|
+
const joinScoped = join ? scopedEvents(join.events, wanted) : scoped;
|
|
219
|
+
const { mergeQueuedByRunner, outcomeByRunner } = bucketMergeEvents(joinScoped);
|
|
220
|
+
return {
|
|
221
|
+
starts,
|
|
222
|
+
joinStarts: join ? runStartMap(joinScoped) : starts,
|
|
223
|
+
joinModels: join ? runStartModelMap(joinScoped) : models,
|
|
224
|
+
floorTs: join ? join.floorTs : null,
|
|
225
|
+
mergeQueuedByRunner,
|
|
226
|
+
outcomeByRunner,
|
|
227
|
+
claimTop: new Map(),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** One run_end's result label: the run's own unless it is `queued`, in which case the queued
|
|
231
|
+
* row is resolved through its change's merge evidence (resolveQueuedResult's conservative
|
|
232
|
+
* rule) and falls back to the raw `queued` label when the merge is still in the pipeline or
|
|
233
|
+
* the evidence is untrustworthy. */
|
|
234
|
+
function queuedRowResult(e, rawResult, evidence) {
|
|
235
|
+
if (rawResult !== "queued")
|
|
236
|
+
return rawResult;
|
|
237
|
+
return (resolveQueuedResult(e, evidence.mergeQueuedByRunner, evidence.outcomeByRunner, evidence.claimTop, evidence.joinStarts, evidence.floorTs)?.result ?? rawResult);
|
|
238
|
+
}
|
|
239
|
+
/** Assemble one row from its `run_end` and the shared evidence: the raw usage numbers folded
|
|
240
|
+
* into the payload, the positive-only peak and cut-off flags, the join-or-plain model map,
|
|
241
|
+
* the paired duration, and the detail squash. */
|
|
242
|
+
function runRowFromEnd(e, evidence) {
|
|
243
|
+
const usage = eventUsage(e);
|
|
244
|
+
const peak = nonNegativeNumber(e.peakContextTokens, 0);
|
|
245
|
+
return {
|
|
246
|
+
ts: e.ts,
|
|
247
|
+
time: formatTimestamp(e.ts),
|
|
248
|
+
tokens: usage.tokens,
|
|
249
|
+
costUsd: usage.costUsd,
|
|
250
|
+
...(peak > 0 ? { peakContextTokens: peak } : {}),
|
|
251
|
+
...(e.cutOff === true ? { cutOff: true } : {}),
|
|
252
|
+
role: eventRole(e),
|
|
253
|
+
run: eventRun(e),
|
|
254
|
+
result: queuedRowResult(e, outcomeText(e), evidence),
|
|
255
|
+
model: runStartModel(e, evidence.joinModels),
|
|
256
|
+
durationMs: runSpanMs(e, evidence.starts),
|
|
257
|
+
usage: usageText(e),
|
|
258
|
+
detail: squash(textOr(e.summary, textOr(e.error, "")), DETAIL_MAX),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
234
261
|
/** The last `limit` completed runs read from root's event log, filtered to `role` when
|
|
235
262
|
* given (newest first) — the scan cmdHistory and the GUI's handleHistory share, so the two
|
|
236
263
|
* surfaces cannot drift. A window twice the ask plus slack: run_start lines and unrelated
|
|
@@ -3,7 +3,7 @@ import { groupBy } from "../collections.js";
|
|
|
3
3
|
/** The merge bookkeeping resolveQueuedResult joins over: per role, the `merge_queued` pins
|
|
4
4
|
* and the `merge_done`/`merge_failed` outcomes, each list oldest-first (the scan order). Exported
|
|
5
5
|
* as one helper with exactly three call sites — readRunRows' history-row join, the failure
|
|
6
|
-
* digest's time-and-spend fold (time-spend.ts), and run-detail-data.ts — so the collectors
|
|
6
|
+
* digest's time-and-spend fold (failure-time-spend.ts), and run-detail-data.ts — so the collectors
|
|
7
7
|
* cannot disagree about which events feed the join or in what order they sit: a fourth
|
|
8
8
|
* merge-bookkeeping event
|
|
9
9
|
* type, or a changed order rule, is edited once here. Callers bucket only the events they
|
|
@@ -58,41 +58,63 @@ export function bucketMergeEvents(events) {
|
|
|
58
58
|
* text plus the pin and outcome of the attempt that produced it (and that pin's sha), or null
|
|
59
59
|
* to keep the raw label. Pure apart from the cursor it owns. */
|
|
60
60
|
export function resolveQueuedResult(end, mergeQueuedByRunner, outcomeByRunner, claimTop, joinStarts, floorTs) {
|
|
61
|
-
|
|
62
|
-
// enqueued nothing, so its queued label names the held commit, not a merge this run made.
|
|
63
|
-
// Returning null keeps that row's raw queued label and, crucially, leaves the earlier change's
|
|
64
|
-
// merge pins unclaimed, so the verdict that belongs to the queued entry is not credited to the
|
|
65
|
-
// held run. One guard covers all three call sites (history-data.ts, time-spend.ts,
|
|
66
|
-
// run-detail-data.ts).
|
|
67
|
-
if (end.heldOnBranch === true)
|
|
61
|
+
if (!queuedRowEvidenceWhole(end, joinStarts, floorTs))
|
|
68
62
|
return null;
|
|
69
|
-
if (floorTs !== null) {
|
|
70
|
-
const startTs = joinStarts.get(`${end.source}#${end.run}`);
|
|
71
|
-
if (startTs !== undefined && startTs < floorTs)
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
63
|
const queuedList = mergeQueuedByRunner.get(end.source);
|
|
75
64
|
if (!queuedList || queuedList.length === 0)
|
|
76
65
|
return null;
|
|
77
|
-
|
|
78
|
-
while (top >= 0 && queuedList[top].ts > end.ts)
|
|
79
|
-
top--;
|
|
66
|
+
const top = claimQueuedPin(end, queuedList, claimTop);
|
|
80
67
|
if (top < 0)
|
|
81
68
|
return null;
|
|
82
|
-
claimTop.set(end.source, top - 1);
|
|
83
69
|
const outcomes = outcomeByRunner.get(end.source) ?? [];
|
|
84
|
-
// A recovery run's row: the attempt it re-enqueued, not the change's final verdict, so the
|
|
85
|
-
// final verdict shows once (on the authoring row) and each retriable failure once (here).
|
|
86
70
|
const recovered = recoveredAttemptVerdict(queuedList, top, outcomes);
|
|
87
71
|
if (recovered !== null)
|
|
88
72
|
return recovered;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
73
|
+
return authoringRowVerdict(queuedList, outcomes, top);
|
|
74
|
+
}
|
|
75
|
+
/** True when the row's join evidence is whole enough to resolve it: neither the interlock-relief
|
|
76
|
+
* held-run guard nor the since-window floor keeps the row's raw queued label. Interlock relief
|
|
77
|
+
* (plans/interlock-relief.md): a held run's commit stayed on the branch and enqueued nothing, so
|
|
78
|
+
* its queued label names the held commit, not a merge this run made. Returning false keeps that
|
|
79
|
+
* row's raw queued label and, crucially, leaves the earlier change's merge pins unclaimed, so the
|
|
80
|
+
* verdict that belongs to the queued entry is not credited to the held run. One guard covers all
|
|
81
|
+
* three call sites (history-data.ts, failure-time-spend.ts, run-detail-data.ts). When `floorTs` (a
|
|
82
|
+
* since-window's cutoff, null in the count view) sits after the run_start's ts, the row's join
|
|
83
|
+
* evidence is not whole in the windowed read and the row keeps its raw queued label. A MISSING
|
|
84
|
+
* start needs no guard — it joins, because rotation drops a prefix, so every pin older than the
|
|
85
|
+
* lost start is lost with it and nothing can be mis-claimed (and a skipped run has no start at all
|
|
86
|
+
* but is never queued). */
|
|
87
|
+
function queuedRowEvidenceWhole(end, joinStarts, floorTs) {
|
|
88
|
+
if (end.heldOnBranch === true)
|
|
89
|
+
return false;
|
|
90
|
+
if (floorTs !== null) {
|
|
91
|
+
const startTs = joinStarts.get(`${end.source}#${end.run}`);
|
|
92
|
+
if (startTs !== undefined && startTs < floorTs)
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
/** Claim the newest unclaimed merge_queued pin at or before the row's end and consume it from the
|
|
98
|
+
* caller's `claimTop` cursor. The cursor (mutated in place) walks the pin list newest-first and
|
|
99
|
+
* hands each queued run the newest unclaimed pin at or before its end, so several queued runs
|
|
100
|
+
* cannot claim one pin. Returns the pin's index, or -1 when no pin is at or before the
|
|
101
|
+
* row's end. */
|
|
102
|
+
function claimQueuedPin(end, queuedList, claimTop) {
|
|
103
|
+
let top = claimTop.get(end.source) ?? queuedList.length - 1;
|
|
104
|
+
while (top >= 0 && queuedList[top].ts > end.ts)
|
|
105
|
+
top--;
|
|
106
|
+
if (top < 0)
|
|
107
|
+
return -1;
|
|
108
|
+
claimTop.set(end.source, top - 1);
|
|
109
|
+
return top;
|
|
110
|
+
}
|
|
111
|
+
/** The authoring row: walk forward while the next pin is a recovery re-enqueue and resolve to the
|
|
112
|
+
* last attempt's verdict. `findLast` picks the newest same-sha outcome so an un-rebased retry
|
|
113
|
+
* (recovery re-enqueues the identical sha, logged by both pins) does not re-match the first
|
|
114
|
+
* attempt's outcome inside the recovery pin's walk. The returned pin/outcome ride the result so a
|
|
115
|
+
* caller correlating sibling evidence (the failure digest's review-failure message) can key on
|
|
116
|
+
* THIS attempt's position, not a sha a pre-review rebase may have moved (BUGS.md 2026-10-10). */
|
|
117
|
+
function authoringRowVerdict(queuedList, outcomes, top) {
|
|
96
118
|
let idx = top;
|
|
97
119
|
let resolved = null;
|
|
98
120
|
for (;;) {
|
|
@@ -147,7 +169,7 @@ function recoveredAttemptVerdict(queuedList, idx, outcomes) {
|
|
|
147
169
|
return verdict === null || endsChange(verdict.result) ? null : verdict;
|
|
148
170
|
}
|
|
149
171
|
/** True when a pin re-enqueues the previous attempt's kept pin rather than starting a fresh
|
|
150
|
-
* change. `recoverLeftover` (
|
|
172
|
+
* change. `recoverLeftover` (runner-leftover.ts) is the one `merge_queued` producer whose
|
|
151
173
|
* summary starts with this label, so a fresh change's pin after a conflict-cap discard ends
|
|
152
174
|
* `resolveQueuedResult`'s walk. */
|
|
153
175
|
function isRecoveryPin(pin) {
|