tumwater 0.1.3 → 0.2.0
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 +63 -28
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-eligibility.js +332 -43
- package/dist/src/backlog/backlog-md.js +79 -17
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +178 -441
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +32 -30
- package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
- package/dist/src/{brief.js → brief/brief.js} +33 -8
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +13 -11
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +9 -7
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +43 -10
- package/dist/src/cli/cli-editor.js +13 -1
- package/dist/src/cli/cli-flag-specs.js +31 -6
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-query-commands.js +439 -56
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +124 -68
- package/dist/src/cli/completion.js +70 -12
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +264 -124
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +13 -224
- package/dist/src/cli.js +78 -36
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +107 -325
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +494 -68
- package/dist/src/doctor/doctor-checks.js +509 -58
- package/dist/src/doctor/doctor-holds.js +139 -78
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +2 -3
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +76 -25
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +219 -106
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +80 -30
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +6 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +79 -59
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -716
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +105 -54
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +25 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +179 -104
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +29 -70
- package/dist/src/inbox/inbox-attachments.js +66 -10
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +114 -73
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +104 -50
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +130 -13
- package/dist/src/loop/loop-tick.js +190 -88
- package/dist/src/loop/loop.js +68 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +113 -45
- package/dist/src/operator/operator-intent.js +74 -53
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +47 -28
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
- package/dist/src/orchestrator/orchestrator.js +149 -96
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +124 -38
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +4 -3
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +36 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +29 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +259 -254
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +235 -44
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +4 -3
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +55 -34
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +109 -131
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +16 -14
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +15 -8
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +28 -25
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +72 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +50 -39
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +142 -25
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +23 -22
- package/dist/src/tick/tick-detail.js +37 -24
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +67 -53
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +75 -10
- package/dist/src/ui/badges.js +113 -69
- package/dist/src/ui/gui/gui-client-boot.js +202 -29
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +94 -37
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +90 -47
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +60 -43
- package/dist/src/ui/gui/gui-client-operator.js +73 -43
- package/dist/src/ui/gui/gui-client-pending.js +34 -31
- package/dist/src/ui/gui/gui-client-report.js +330 -47
- package/dist/src/ui/gui/gui-client-settings.js +126 -18
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +18 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +176 -102
- package/dist/src/ui/status-payload.js +114 -36
- package/dist/src/ui/status-render.js +121 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +9 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +61 -57
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +44 -50
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +87 -1821
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +52 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +44 -26
- package/dist/src/verdict/fix-claim.js +56 -33
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +43 -11
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -56
- package/dist/src/landing/landing-slot.js +0 -256
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** Collection half of `tumwater history`: read the event log and distill the last N completed
|
|
2
3
|
* ticks into TickRow — one row per tick, newest first, with the paired duration and the raw
|
|
3
4
|
* usage numbers. The Markdown/table rendering of this data lives in history.ts, a pure
|
|
@@ -7,54 +8,70 @@
|
|
|
7
8
|
* which change for different reasons — and keeps core data collection out of the
|
|
8
9
|
* presentation layer, so a core consumer (as the GUI's /api/history already is) never
|
|
9
10
|
* forces a core→ui import. */
|
|
10
|
-
import { eventRole, eventTick, eventUsage, readEvents, tickSpanMs, tickStartMap } from "../events/event-read.js";
|
|
11
|
+
import { eventRole, eventTick, eventUsage, readEvents, tickSpanMs, tickStartMap, tickStartModel, tickStartModelMap } from "../events/event-read.js";
|
|
11
12
|
import { readEventsSinceJoined } from "../events/event-window.js";
|
|
12
13
|
import { formatTimestamp } from "../text/datetime.js";
|
|
14
|
+
import { eventTypeLabel } from "../events/event-labels.js";
|
|
15
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
13
16
|
import { squash, textOr } from "../text/text.js";
|
|
14
17
|
import { shortSpanPhrase } from "../text/phrases.js";
|
|
15
18
|
import { outcomeText, usageText } from "../events/event-format.js";
|
|
16
|
-
import { groupBy } from "../collections.js";
|
|
19
|
+
import { compareCodeUnits, groupBy } from "../collections.js";
|
|
17
20
|
/** `history`'s default row count and ceiling. The default shows a working hour of a quiet
|
|
18
|
-
*
|
|
21
|
+
* team; the ceiling bounds how much log one ask re-reads — more rows only re-read more log
|
|
19
22
|
* with no added signal, so an explicit flag above it fails fast (the report's --days pattern). */
|
|
20
23
|
export const HISTORY_DEFAULT_TICKS = 20;
|
|
21
24
|
export const HISTORY_MAX_TICKS = 200;
|
|
22
25
|
/** The outcome classes `tumwater history --result` and the dashboard's History filter both
|
|
23
|
-
* speak: Changes (the tick
|
|
24
|
-
* the operator aborted it),
|
|
26
|
+
* speak: Changes (the tick merged or queued work), No change (it did nothing on purpose, or
|
|
27
|
+
* the operator aborted it), Refused (a run that deliberately declined its entry), Problems
|
|
28
|
+
* (everything else, e.g. error/rejected). The one canonical
|
|
25
29
|
* rule; the browser's copy in gui-client-history.ts is pinned to it by
|
|
26
30
|
* test/gui-client-history.test.ts, so the two cannot drift. */
|
|
27
|
-
export const HISTORY_RESULT_CLASSES = ["changes", "none", "problems"];
|
|
31
|
+
export const HISTORY_RESULT_CLASSES = ["changes", "none", "refused", "problems"];
|
|
32
|
+
/** The spelling each class has on `--result` and in the TUI filter: `changes` reads `merged`. */
|
|
33
|
+
export const HISTORY_RESULT_FLAG_NAMES = { changes: "merged", none: "none", refused: "refused", problems: "problems" };
|
|
34
|
+
/** Parse a `--result` value: a flag name, or a class name (so `changes` stays a hidden alias). */
|
|
35
|
+
export function parseHistoryResultClass(raw) {
|
|
36
|
+
return HISTORY_RESULT_CLASSES.find((c) => raw === c || raw === HISTORY_RESULT_FLAG_NAMES[c]) ?? null;
|
|
37
|
+
}
|
|
28
38
|
/** Which outcome class a tick result falls in (the HISTORY_RESULT_CLASSES rule): `changed` and
|
|
29
|
-
* `queued` are `changes`; `no_change`, `skipped`, and `user_aborted` are `none`;
|
|
30
|
-
* result is `problems`. Pure; the canonical home the dashboard's
|
|
39
|
+
* `queued` are `changes`; `no_change`, `skipped`, and `user_aborted` are `none`; `refused` is
|
|
40
|
+
* `refused`; every other result is `problems`. Pure; the canonical home the dashboard's
|
|
41
|
+
* client-side filter mirrors. */
|
|
31
42
|
export function historyClass(result) {
|
|
32
43
|
if (result === "changed" || result === "queued")
|
|
33
44
|
return "changes";
|
|
34
45
|
if (result === "no_change" || result === "skipped" || result === "user_aborted")
|
|
35
46
|
return "none";
|
|
47
|
+
if (result === "refused")
|
|
48
|
+
return "refused";
|
|
36
49
|
return "problems";
|
|
37
50
|
}
|
|
38
51
|
/** Tally rows per loop by historyClass, each row counted once. Loops come back in plain string
|
|
39
52
|
* order (not localeCompare) so the order is deterministic; `total` sums every loop. Pure. */
|
|
40
53
|
export function summarizeTickRows(rows) {
|
|
41
|
-
const blank = (
|
|
54
|
+
const blank = (role) => ({ role, runs: 0, changes: 0, none: 0, refused: 0, problems: 0, tokens: 0, costUsd: 0, lastChangeTs: null });
|
|
42
55
|
const byLoop = new Map();
|
|
43
56
|
const total = blank("total");
|
|
44
57
|
for (const row of rows) {
|
|
45
|
-
let entry = byLoop.get(row.
|
|
58
|
+
let entry = byLoop.get(row.role);
|
|
46
59
|
if (!entry)
|
|
47
|
-
byLoop.set(row.
|
|
60
|
+
byLoop.set(row.role, (entry = blank(row.role)));
|
|
48
61
|
const cls = historyClass(row.result);
|
|
49
62
|
for (const t of [entry, total]) {
|
|
50
|
-
t.
|
|
63
|
+
t.runs += 1;
|
|
51
64
|
t[cls] += 1;
|
|
65
|
+
t.tokens += row.tokens;
|
|
66
|
+
t.costUsd += row.costUsd;
|
|
67
|
+
if (row.durationMs !== null)
|
|
68
|
+
t.durationMs = (t.durationMs ?? 0) + row.durationMs;
|
|
52
69
|
if (cls === "changes")
|
|
53
70
|
t.lastChangeTs = Math.max(t.lastChangeTs ?? row.ts, row.ts);
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
|
-
const
|
|
57
|
-
return {
|
|
73
|
+
const roles = [...byLoop.values()].sort((a, b) => compareCodeUnits(a.role, b.role));
|
|
74
|
+
return { roles, total };
|
|
58
75
|
}
|
|
59
76
|
/** How much of a tick's summary or error a row keeps. Long enough to name the work, short
|
|
60
77
|
* enough that the row stays one terminal line next to its other columns. */
|
|
@@ -70,7 +87,7 @@ const DETAIL_MAX = 72;
|
|
|
70
87
|
export const HISTORY_SCAN_MAX_EVENTS = 20_000;
|
|
71
88
|
/** How far behind a since-shaped scan's cutoff the queued-row join reads its evidence: the
|
|
72
89
|
* row set is ts-filtered to the cutoff, but a queued tick whose tick SPANS the cutoff logged
|
|
73
|
-
* its `
|
|
90
|
+
* its `merge_queued` pin before it (the pin is logged during the tick, the run_end after it)
|
|
74
91
|
* — so the join evidence needs its own read reaching further back. Six hours outlasts any
|
|
75
92
|
* tick the harness runs (every stage inside a tick is timeout-bounded); a tick that somehow
|
|
76
93
|
* queued later than that keeps the raw label, the conservative fallback. The wider read shares
|
|
@@ -79,25 +96,28 @@ export const HISTORY_SCAN_MAX_EVENTS = 20_000;
|
|
|
79
96
|
const TICK_ROW_JOIN_BACKSLACK_MS = 6 * 60 * 60 * 1000;
|
|
80
97
|
export function tickRowCells(row) {
|
|
81
98
|
return {
|
|
82
|
-
loop: row.
|
|
83
|
-
tick: String(row.
|
|
84
|
-
result: row.result,
|
|
99
|
+
loop: roleLabel(row.role),
|
|
100
|
+
tick: String(row.run),
|
|
101
|
+
result: eventTypeLabel(row.result),
|
|
102
|
+
model: row.model ?? "",
|
|
85
103
|
duration: row.durationMs === null ? "—" : shortSpanPhrase(row.durationMs),
|
|
86
104
|
usage: row.usage,
|
|
87
105
|
};
|
|
88
106
|
}
|
|
89
|
-
/** One tick row's line: its timestamp, the
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
107
|
+
/** One tick row's line: its timestamp, the six tickRowCells values (the model cell between
|
|
108
|
+
* the result and the duration), then the free-text detail, joined by two spaces and stripped
|
|
109
|
+
* of trailing padding. This is the one home for the row's column order and separator;
|
|
110
|
+
* renderRow (history-render.ts) passes a per-column padding function to align the CLI table,
|
|
111
|
+
* while tickRowGrepHaystack below passes none. The tick cell's "#" prefix rides outside the
|
|
112
|
+
* padding, so its column width measures the number alone. */
|
|
94
113
|
export function tickRowLine(row, pad = (_c, v) => v) {
|
|
95
114
|
const c = tickRowCells(row);
|
|
96
115
|
return [
|
|
97
116
|
row.time,
|
|
98
|
-
pad("loop", c.loop),
|
|
99
|
-
`#${pad("tick", c.tick)}`,
|
|
117
|
+
pad("loop", c.loop), // vocabulary-ok: column key, not shown
|
|
118
|
+
`#${pad("tick", c.tick)}`, // vocabulary-ok: column key, not shown
|
|
100
119
|
pad("result", c.result),
|
|
120
|
+
pad("model", c.model),
|
|
101
121
|
pad("duration", c.duration),
|
|
102
122
|
pad("usage", c.usage),
|
|
103
123
|
row.detail,
|
|
@@ -106,13 +126,15 @@ export function tickRowLine(row, pad = (_c, v) => v) {
|
|
|
106
126
|
.trimEnd();
|
|
107
127
|
}
|
|
108
128
|
/** The `--grep` haystack for one rendered history row: tickRowLine's canonical unpadded row
|
|
109
|
-
* line prefixed with the raw event type so stable ids (`
|
|
110
|
-
* --grep's haystack carries `e.type
|
|
111
|
-
* the
|
|
129
|
+
* line prefixed with the raw event type so stable ids (`run_end`) are greppable the way logs
|
|
130
|
+
* --grep's haystack carries `e.type`, and followed by the raw role id and result code, which
|
|
131
|
+
* the row shows as `feature #2` and `no change`, so a script's grep on the code still
|
|
132
|
+
* matches. Built per row because the filter runs before the widths of the rows actually shown
|
|
133
|
+
* are known, and shared by `tumwater history --grep` and the dashboard's
|
|
112
134
|
* /api/history?grep so the two cannot disagree on the rule. The row's cells and column order
|
|
113
135
|
* come from tickRowCells and tickRowLine, the one home for each. */
|
|
114
136
|
export function tickRowGrepHaystack(row) {
|
|
115
|
-
return `
|
|
137
|
+
return `run_end ${tickRowLine(row)} ${row.role} ${row.result}`;
|
|
116
138
|
}
|
|
117
139
|
/** The shared tick-grep rule: a case-insensitive substring match over tickRowGrepHaystack.
|
|
118
140
|
* Four call sites apply it — `tumwater history --grep`, /api/history's window and count
|
|
@@ -122,95 +144,145 @@ export function tickRowsMatchingGrep(rows, pattern) {
|
|
|
122
144
|
const needle = pattern.toLowerCase();
|
|
123
145
|
return rows.filter((r) => tickRowGrepHaystack(r).toLowerCase().includes(needle));
|
|
124
146
|
}
|
|
125
|
-
/** The
|
|
126
|
-
* and the `
|
|
147
|
+
/** The merge bookkeeping resolveQueuedResult joins over: per loop, the `merge_queued` pins
|
|
148
|
+
* and the `merge_done`/`merge_failed` outcomes, each list oldest-first (the scan order). Exported
|
|
127
149
|
* as one helper with exactly two call sites — readTickRows' history-row join and the failure
|
|
128
150
|
* digest's time-and-spend fold (time-spend.ts) — so the two collectors cannot disagree about
|
|
129
|
-
* which events feed the join or in what order they sit: a third
|
|
151
|
+
* which events feed the join or in what order they sit: a third merge-bookkeeping event
|
|
130
152
|
* type, or a changed order rule, is edited once here. Callers bucket only the events they
|
|
131
153
|
* need; the digest folds its `review_rejected` reasons with its own pass.
|
|
132
154
|
* Roles or cutoffs are the caller's concern: pass a role-scoped or wider-read event list and
|
|
133
155
|
* the maps carry exactly that list's evidence. */
|
|
134
|
-
export function
|
|
135
|
-
const
|
|
136
|
-
const outcomeByLoop = groupBy(events.filter((e) => e.type === "
|
|
137
|
-
return {
|
|
156
|
+
export function bucketMergeEvents(events) {
|
|
157
|
+
const mergeQueuedByLoop = groupBy(events.filter((e) => e.type === "merge_queued"), (e) => e.source);
|
|
158
|
+
const outcomeByLoop = groupBy(events.filter((e) => e.type === "merge_done" || e.type === "merge_failed"), (e) => e.source);
|
|
159
|
+
return { mergeQueuedByLoop, outcomeByLoop };
|
|
138
160
|
}
|
|
139
|
-
/** Resolve one queued tick row's eventual
|
|
140
|
-
* result until its
|
|
141
|
-
* tick's
|
|
142
|
-
*
|
|
161
|
+
/** Resolve one queued tick row's eventual merge verdict: a "queued" run_end carries no
|
|
162
|
+
* result until its merge batch merges or fails, so the row joins its `end` run_end to the
|
|
163
|
+
* tick's merge_queued pin and that pin's merge_done/merge_failed outcome (both from the
|
|
164
|
+
* bucketMergeEvents maps, oldest-first per loop). The claim cursor `claimTop` (mutated in
|
|
143
165
|
* place) walks the pin list newest-first and hands each queued tick the newest unclaimed pin
|
|
144
|
-
* at or before its end, so several queued ticks cannot claim one pin.
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
|
|
166
|
+
* at or before its end, so several queued ticks cannot claim one pin.
|
|
167
|
+
*
|
|
168
|
+
* One change can take several merge attempts: a retriable result (RETRIABLE_MERGE_RESULTS:
|
|
169
|
+
* review_error, merge_conflict, merge_blocked) keeps the pin, and the role's next run — a
|
|
170
|
+
* leftover-recovery run that does no work of its own and ends in about a second — re-enqueues
|
|
171
|
+
* it (isRecoveryPin). The authoring run and the recovery runs resolve by different rules, so
|
|
172
|
+
* every attempt's verdict shows on exactly one row:
|
|
173
|
+
* - The run that authored the change (its pin is not a re-enqueue of a kept attempt) takes
|
|
174
|
+
* the change's FINAL verdict, not its first attempt's: a retriable failure that recovery
|
|
175
|
+
* later merges reads "changed" (BUGS.md 2026-10-10). While a re-enqueued attempt is still
|
|
176
|
+
* pending the row keeps the raw "queued" label — the change is waiting to merge again, and
|
|
177
|
+
* the failure before it already shows on the recovery run's row.
|
|
178
|
+
* - A recovery run takes the verdict of the attempt it re-enqueued — the retriable failure
|
|
179
|
+
* that kept the pin and made the run happen (recoveredAttemptVerdict). So a change that
|
|
180
|
+
* timed out in review seven times and was then rejected reads "rejected" once (the
|
|
181
|
+
* authoring row) and "review failed" seven times (the authoring attempt's and six
|
|
182
|
+
* recoveries' timeouts, each on the recovery row that followed it), not "rejected" on all
|
|
183
|
+
* eight rows.
|
|
184
|
+
* When main moved before a retry the verification's syncPinToMain rebased the ref
|
|
185
|
+
* (merge-core.ts), so the recovery pin carries the rebased sha while each attempt's own
|
|
186
|
+
* merge_done/merge_failed logs its entry's sha (merge-slot.ts's entry.sha): the attempts' shas
|
|
187
|
+
* differ, so the authoring row's walk follows the pin sequence (isRecoveryPin), not sha
|
|
188
|
+
* equality alone. It stops at a pin that is not a recovery re-enqueue — a fresh change after a
|
|
189
|
+
* conflict-cap discard — so the row never credits a different change's verdict (BUGS.md
|
|
190
|
+
* 2026-10-10). A recovery pin whose previous attempt shows no retriable verdict (a crash
|
|
191
|
+
* left the commit unpinned or the merge unfinished, or that attempt sits outside the read) is
|
|
192
|
+
* the change's first visible attempt, so its row takes the authoring rule.
|
|
193
|
+
*
|
|
194
|
+
* `joinStarts` maps loop#tick to its run_start's ts; when `floorTs` (a since-window's cutoff,
|
|
195
|
+
* null in the count view) sits after that start, the row's join evidence is not whole in the
|
|
196
|
+
* windowed read and the row keeps its raw "queued" label (null). A MISSING start needs no
|
|
197
|
+
* guard — it joins, because rotation drops a prefix, so every pin older than the lost start is
|
|
198
|
+
* lost with it and nothing can be mis-claimed (and a skipped tick has no start at all but is
|
|
199
|
+
* never queued). A merge with no outcome event in the join set also keeps the raw label: the
|
|
200
|
+
* conservative fallback for a verdict simply not visible here. Returns the verdict's result
|
|
201
|
+
* text plus the pin and outcome of the attempt that produced it (and that pin's sha), or null
|
|
202
|
+
* to keep the raw label. Pure apart from the cursor it owns. */
|
|
203
|
+
export function resolveQueuedResult(end, mergeQueuedByLoop, outcomeByLoop, claimTop, joinStarts, floorTs) {
|
|
164
204
|
if (floorTs !== null) {
|
|
165
|
-
const startTs = joinStarts.get(`${end.
|
|
205
|
+
const startTs = joinStarts.get(`${end.source}#${end.run}`);
|
|
166
206
|
if (startTs !== undefined && startTs < floorTs)
|
|
167
207
|
return null;
|
|
168
208
|
}
|
|
169
|
-
const queuedList =
|
|
209
|
+
const queuedList = mergeQueuedByLoop.get(end.source);
|
|
170
210
|
if (!queuedList || queuedList.length === 0)
|
|
171
211
|
return null;
|
|
172
|
-
let top = claimTop.get(end.
|
|
212
|
+
let top = claimTop.get(end.source) ?? queuedList.length - 1;
|
|
173
213
|
while (top >= 0 && queuedList[top].ts > end.ts)
|
|
174
214
|
top--;
|
|
175
215
|
if (top < 0)
|
|
176
216
|
return null;
|
|
177
|
-
claimTop.set(end.
|
|
178
|
-
const outcomes = outcomeByLoop.get(end.
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// The
|
|
185
|
-
//
|
|
186
|
-
//
|
|
217
|
+
claimTop.set(end.source, top - 1);
|
|
218
|
+
const outcomes = outcomeByLoop.get(end.source) ?? [];
|
|
219
|
+
// A recovery run's row: the attempt it re-enqueued, not the change's final verdict, so the
|
|
220
|
+
// final verdict shows once (on the authoring row) and each retriable failure once (here).
|
|
221
|
+
const recovered = recoveredAttemptVerdict(queuedList, top, outcomes);
|
|
222
|
+
if (recovered !== null)
|
|
223
|
+
return recovered;
|
|
224
|
+
// The authoring row: walk forward while the next pin is a recovery re-enqueue and resolve to
|
|
225
|
+
// the last attempt's verdict. `findLast` picks the newest same-sha outcome so an un-rebased
|
|
226
|
+
// retry (recovery re-enqueues the identical sha, logged by both pins) does not re-match the
|
|
227
|
+
// first attempt's outcome inside the recovery pin's walk. The returned pin/outcome ride the
|
|
228
|
+
// result so a caller correlating sibling evidence (the failure digest's review-failure
|
|
229
|
+
// message) can key on THIS attempt's position, not a sha a pre-review rebase may have moved
|
|
230
|
+
// (BUGS.md 2026-10-10).
|
|
187
231
|
let idx = top;
|
|
188
232
|
let resolved = null;
|
|
189
233
|
for (;;) {
|
|
190
234
|
const pin = queuedList[idx];
|
|
191
235
|
const outcome = outcomes.findLast((o) => o.ts >= pin.ts && textOr(o.commit, "") === textOr(pin.commit, ""));
|
|
192
|
-
// No outcome on the tick's own pin
|
|
193
|
-
//
|
|
194
|
-
//
|
|
236
|
+
// No outcome on the tick's own pin, or on a recovery pin the walk reached: that attempt is
|
|
237
|
+
// still pending, so the change is waiting to merge — keep the raw label. The previous
|
|
238
|
+
// attempt's retriable verdict already shows on the recovery run's row.
|
|
195
239
|
if (!outcome)
|
|
240
|
+
return null;
|
|
241
|
+
const verdict = attemptVerdict(pin, outcome);
|
|
242
|
+
if (verdict === null)
|
|
196
243
|
return resolved;
|
|
197
|
-
|
|
198
|
-
if (!result)
|
|
199
|
-
return resolved;
|
|
200
|
-
const verdict = { result, sha: textOr(pin.commit, ""), pin, outcome };
|
|
201
|
-
// A landing or a rejection ends the change (the pin is discarded, so no recovery pin can
|
|
244
|
+
// A merge or a rejection ends the change (the pin is discarded, so no recovery pin can
|
|
202
245
|
// follow); any other verdict kept the pin, and the walk continues only if the next pin is
|
|
203
246
|
// the recovery re-enqueue that kept it. Otherwise this attempt's verdict is the final one
|
|
204
247
|
// visible.
|
|
205
|
-
|
|
206
|
-
if (terminal || !isRecoveryPin(queuedList[idx + 1]))
|
|
248
|
+
if (endsChange(verdict.result) || !isRecoveryPin(queuedList[idx + 1]))
|
|
207
249
|
return verdict;
|
|
208
250
|
resolved = verdict;
|
|
209
251
|
idx += 1;
|
|
210
252
|
}
|
|
211
253
|
}
|
|
254
|
+
/** The verdict one attempt's outcome carries, or null for a torn outcome with no result (a
|
|
255
|
+
* merge_failed whose result is missing or not text). A merge_done with no result is a merge. */
|
|
256
|
+
function attemptVerdict(pin, outcome) {
|
|
257
|
+
const result = textOr(outcome.result, outcome.type === "merge_done" ? "changed" : "");
|
|
258
|
+
return result ? { result, sha: textOr(pin.commit, ""), pin, outcome } : null;
|
|
259
|
+
}
|
|
260
|
+
/** True when a verdict ends its change: a merge or a rejection discards the pin, so no recovery
|
|
261
|
+
* re-enqueue can follow it. Every other verdict (the retriable ones) kept the pin. The one rule
|
|
262
|
+
* both directions of resolveQueuedResult's chain read — the authoring row's walk forward and a
|
|
263
|
+
* recovery row's step back — so the two cannot disagree on where a change ends. */
|
|
264
|
+
function endsChange(result) {
|
|
265
|
+
return result === "changed" || result === "rejected";
|
|
266
|
+
}
|
|
267
|
+
/** A recovery run's own row verdict: when the pin at `idx` is a recovery re-enqueue and the
|
|
268
|
+
* previous pin's attempt ended in a retriable verdict (the kept pin this run re-enqueued), that
|
|
269
|
+
* verdict, carrying the previous attempt's pin and outcome. The outcome is the newest one for
|
|
270
|
+
* the previous pin's sha logged between the two pins: recovery runs only after the previous
|
|
271
|
+
* attempt's outcome is written, and bounding at this pin keeps an un-rebased retry (both pins
|
|
272
|
+
* share a sha) from matching a LATER attempt's outcome. Null when the pin is not a recovery
|
|
273
|
+
* re-enqueue, or is one with no retriable attempt visible before it — a crash's unpinned or
|
|
274
|
+
* unfinished commit, or a previous pin outside the read — so the row takes the authoring rule. */
|
|
275
|
+
function recoveredAttemptVerdict(queuedList, idx, outcomes) {
|
|
276
|
+
const pin = queuedList[idx];
|
|
277
|
+
const prev = queuedList[idx - 1];
|
|
278
|
+
if (prev === undefined || !isRecoveryPin(pin))
|
|
279
|
+
return null;
|
|
280
|
+
const outcome = outcomes.findLast((o) => o.ts >= prev.ts && o.ts <= pin.ts && textOr(o.commit, "") === textOr(prev.commit, ""));
|
|
281
|
+
const verdict = outcome === undefined ? null : attemptVerdict(prev, outcome);
|
|
282
|
+
return verdict === null || endsChange(verdict.result) ? null : verdict;
|
|
283
|
+
}
|
|
212
284
|
/** True when a pin re-enqueues the previous attempt's kept pin rather than starting a fresh
|
|
213
|
-
* change. `recoverLeftover` (src/loop/leftover.ts) is the one `
|
|
285
|
+
* change. `recoverLeftover` (src/loop/leftover.ts) is the one `merge_queued` producer whose
|
|
214
286
|
* summary starts with this label, so a fresh change's pin after a conflict-cap discard ends
|
|
215
287
|
* `resolveQueuedResult`'s walk. */
|
|
216
288
|
function isRecoveryPin(pin) {
|
|
@@ -220,33 +292,35 @@ function isRecoveryPin(pin) {
|
|
|
220
292
|
* readEvents returns them), filtered to `role` when given, newest first. Pure: the CLI, the
|
|
221
293
|
* GUI, and the tests share this collector, and none of them writes anything. */
|
|
222
294
|
export function tickRows(events, limit, role, join) {
|
|
223
|
-
const scoped = role === null ? events : events.filter((e) => e.
|
|
295
|
+
const scoped = role === null ? events : events.filter((e) => e.source === role);
|
|
224
296
|
// The start pairing is the shared helper (events.ts), so history's dash-on-unpaired rule
|
|
225
297
|
// and the digest's fold cannot drift into different notions of a tick's span.
|
|
226
298
|
const starts = tickStartMap(scoped);
|
|
227
|
-
|
|
228
|
-
//
|
|
299
|
+
const models = tickStartModelMap(scoped);
|
|
300
|
+
// The merge bookkeeping resolveQueuedResult joins over: per loop, the merge_queued pins
|
|
301
|
+
// (oldest first) and the merge_done/merge_failed outcomes (oldest first). Both ride the role
|
|
229
302
|
// filter with their tick, so the scoped events hold them exactly when they hold the row.
|
|
230
303
|
// A since-shaped scan hands a WIDER join read instead (readTickRowsSince): its cutoff cut
|
|
231
304
|
// the pins of ticks that span it, so the maps read the unfiltered-by-cutoff evidence and
|
|
232
|
-
// resolveQueuedResult guards each claim on the row's
|
|
305
|
+
// resolveQueuedResult guards each claim on the row's run_start being whole in that read.
|
|
233
306
|
const joinScoped = join
|
|
234
307
|
? role === null
|
|
235
308
|
? join.events
|
|
236
|
-
: join.events.filter((e) => e.
|
|
309
|
+
: join.events.filter((e) => e.source === role)
|
|
237
310
|
: scoped;
|
|
238
311
|
const joinStarts = join ? tickStartMap(joinScoped) : starts;
|
|
312
|
+
const joinModels = join ? tickStartModelMap(joinScoped) : models;
|
|
239
313
|
const floorTs = join ? join.floorTs : null;
|
|
240
|
-
const {
|
|
314
|
+
const { mergeQueuedByLoop, outcomeByLoop } = bucketMergeEvents(joinScoped);
|
|
241
315
|
const claimTop = new Map();
|
|
242
316
|
const rows = [];
|
|
243
317
|
for (let i = scoped.length - 1; i >= 0 && rows.length < limit; i--) {
|
|
244
318
|
const e = scoped[i];
|
|
245
|
-
if (e?.type !== "
|
|
319
|
+
if (e?.type !== "run_end")
|
|
246
320
|
continue;
|
|
247
321
|
const rawResult = outcomeText(e);
|
|
248
322
|
const result = rawResult === "queued"
|
|
249
|
-
? (resolveQueuedResult(e,
|
|
323
|
+
? (resolveQueuedResult(e, mergeQueuedByLoop, outcomeByLoop, claimTop, joinStarts, floorTs)
|
|
250
324
|
?.result ?? rawResult)
|
|
251
325
|
: rawResult;
|
|
252
326
|
const usage = eventUsage(e);
|
|
@@ -255,9 +329,10 @@ export function tickRows(events, limit, role, join) {
|
|
|
255
329
|
time: formatTimestamp(e.ts),
|
|
256
330
|
tokens: usage.tokens,
|
|
257
331
|
costUsd: usage.costUsd,
|
|
258
|
-
|
|
259
|
-
|
|
332
|
+
role: eventRole(e),
|
|
333
|
+
run: eventTick(e),
|
|
260
334
|
result,
|
|
335
|
+
model: tickStartModel(e, join ? joinModels : models),
|
|
261
336
|
durationMs: tickSpanMs(e, starts),
|
|
262
337
|
usage: usageText(e),
|
|
263
338
|
detail: squash(textOr(e.summary, textOr(e.error, "")), DETAIL_MAX),
|
|
@@ -267,23 +342,23 @@ export function tickRows(events, limit, role, join) {
|
|
|
267
342
|
}
|
|
268
343
|
/** The last `limit` completed ticks read from root's event log, filtered to `role` when
|
|
269
344
|
* given (newest first) — the scan cmdHistory and the GUI's handleHistory share, so the two
|
|
270
|
-
* surfaces cannot drift. A window twice the ask plus slack:
|
|
271
|
-
* events interleave with the
|
|
345
|
+
* surfaces cannot drift. A window twice the ask plus slack: run_start lines and unrelated
|
|
346
|
+
* events interleave with the run_end rows scanned for, and a skipped tick's end rides a start
|
|
272
347
|
* that may sit outside any smaller window. Two shortfalls grow the window (and re-read) while
|
|
273
348
|
* the scan filled it — `events.length >= window` means the log may hold older events — under
|
|
274
349
|
* the same HISTORY_SCAN_MAX_EVENTS bound so one ask cannot scan without end:
|
|
275
350
|
* - Rows fall short. A role filter dilutes the window (every other loop's events occupy it
|
|
276
|
-
* too), so a quiet role in a busy
|
|
351
|
+
* too), so a quiet role in a busy team must still get its last `limit` ticks whenever the
|
|
277
352
|
* retained log holds them — never a bare "no ticks yet" for a role whose ticks sit just
|
|
278
353
|
* past the first window.
|
|
279
|
-
* - A row's
|
|
280
|
-
* block: every
|
|
354
|
+
* - A row's run_start went unpaired. The window boundary can cut inside a tick's own event
|
|
355
|
+
* block: every run_end of the ask sits within the window but the oldest one's run_start
|
|
281
356
|
* sits just before it, so the row renders a dash though the log holds the start. Every
|
|
282
|
-
*
|
|
357
|
+
* run_end the harness logs follows its run_start (tick() logs the start before runTick,
|
|
283
358
|
* skipped results included), so an unpaired start is either just past the window or lost
|
|
284
359
|
* to rotation — growing re-pairs the former, and the latter only costs the bounded scan.
|
|
285
|
-
* (A queued row's
|
|
286
|
-
* after its
|
|
360
|
+
* (A queued row's merge verdict needs no growth: the merge_done/merge_failed event sits
|
|
361
|
+
* after its run_end, so any tail window holding the row holds the verdict too — or there
|
|
287
362
|
* is none yet, the genuinely-pending case that keeps the raw label.) */
|
|
288
363
|
export function readTickRows(root, limit, role) {
|
|
289
364
|
const key = `${root}\u0000${role ?? ""}\u0000${limit}`;
|
|
@@ -325,8 +400,8 @@ const TICK_ROW_WINDOW_MEMO_MAX = 256;
|
|
|
325
400
|
* and coverage derived from the wide scan, so neither windowed surface can claim coverage the
|
|
326
401
|
* other would hedge or key its read differently. The rows read the cutoff-filtered events, but
|
|
327
402
|
* their queued-row join reads WIDER evidence (TICK_ROW_JOIN_BACKSLACK_MS): a queued tick whose
|
|
328
|
-
* tick spans the cutoff logged its
|
|
329
|
-
* read the row would either keep "Queued to land" after its change
|
|
403
|
+
* tick spans the cutoff logged its merge_queued pin before the cutoff, and without the wider
|
|
404
|
+
* read the row would either keep "Queued to land" after its change merged or misclaim an older
|
|
330
405
|
* tick's pin (BUGS.md 2026-09-30). The rows reuse the pure tickRows with limit =
|
|
331
406
|
* events.length, so pairing, role filtering, and the dash-on-unpaired rule stay exactly the
|
|
332
407
|
* count view's. Pure over root: reads the event log, writes nothing. */
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** Rendering half of `tumwater history`: format a `TickRow[]`/summary — collected by
|
|
3
|
+
* history-data.ts — into the aligned table or `--summary` text `cmdHistory` (history.ts)
|
|
4
|
+
* prints. The sibling observing view report does the same (src/report/report.ts prints
|
|
5
|
+
* src/report/report-render.ts's Markdown), and this split keeps the same separation
|
|
6
|
+
* "what happened / how it prints" that report and failure draw: the command owns argument
|
|
7
|
+
* parsing and stdout, this module owns column widths and cell wording. The rows themselves
|
|
8
|
+
* live in history-data.ts because the grep haystack shares the row line. */
|
|
9
|
+
import { formatTimestamp, humanSeconds } from "../text/datetime.js";
|
|
10
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
11
|
+
import { displayWidth, padToWidth } from "../text/text-width.js";
|
|
12
|
+
import { compactTokens, usd } from "../text/format.js";
|
|
13
|
+
import { tickRowCells, tickRowLine } from "./history-data.js";
|
|
14
|
+
/** Each column's width from its unpadded cell. Text cells measure terminal display columns
|
|
15
|
+
* (displayWidth), not UTF-16 code units: a loop name holding CJK or emoji renders two columns
|
|
16
|
+
* per code point, and a code-unit padEnd lets that row's later columns drift right of its
|
|
17
|
+
* ASCII neighbors. The tick cell measures before its "#" prefix is added (the prefix rides
|
|
18
|
+
* outside the pad), and duration is ASCII, so both take plain length. */
|
|
19
|
+
function widthsOf(cells) {
|
|
20
|
+
return {
|
|
21
|
+
loop: displayWidth(cells.loop),
|
|
22
|
+
tick: cells.tick.length,
|
|
23
|
+
result: displayWidth(cells.result),
|
|
24
|
+
model: displayWidth(cells.model),
|
|
25
|
+
duration: cells.duration.length,
|
|
26
|
+
usage: displayWidth(cells.usage),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** The per-column maxima over every row the table shows — the widths derive from the rows
|
|
30
|
+
* actually shown, so a single-row table has no padding gap. */
|
|
31
|
+
export function widestWidths(rows) {
|
|
32
|
+
const widths = { loop: 0, tick: 0, result: 0, model: 0, duration: 0, usage: 0 };
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
const w = widthsOf(tickRowCells(row));
|
|
35
|
+
widths.loop = Math.max(widths.loop, w.loop);
|
|
36
|
+
widths.tick = Math.max(widths.tick, w.tick);
|
|
37
|
+
widths.result = Math.max(widths.result, w.result);
|
|
38
|
+
widths.model = Math.max(widths.model, w.model);
|
|
39
|
+
widths.duration = Math.max(widths.duration, w.duration);
|
|
40
|
+
widths.usage = Math.max(widths.usage, w.usage);
|
|
41
|
+
}
|
|
42
|
+
return widths;
|
|
43
|
+
}
|
|
44
|
+
/** One aligned table line: fixed-width columns over the row set, then the free-text detail.
|
|
45
|
+
* Every cell is padded to its column's width even when empty — usage is the one cell that can
|
|
46
|
+
* be empty (a tick with neither tokens nor cost), and dropping it would pull the detail left,
|
|
47
|
+
* misaligning that row against its neighbors; tickRowLine strips only the trailing pad. The
|
|
48
|
+
* row's column order rides history-data.ts's tickRowLine, shared with the grep haystack so the
|
|
49
|
+
* two cannot disagree about which field sits where. */
|
|
50
|
+
export function renderRow(row, widths) {
|
|
51
|
+
return tickRowLine(row, (column, value) => padToWidth(value, widths[column]));
|
|
52
|
+
}
|
|
53
|
+
/** The `--summary` text: a header, one line per loop, then the total line. The loop column pads to
|
|
54
|
+
* display width; each number column right-aligns under its header; the `time` column is the sum of
|
|
55
|
+
* each role's run spans, `—` when none of the role's rows carried a span; the last column,
|
|
56
|
+
* `last change`, is the newest change's timestamp (`—` when none) and is left-aligned, unpadded. */
|
|
57
|
+
export function renderSummary(summary) {
|
|
58
|
+
const all = [...summary.roles, summary.total];
|
|
59
|
+
const loopWidth = Math.max(...["role", ...all.map((r) => roleLabel(r.role))].map(displayWidth));
|
|
60
|
+
const cols = ["runs", "changes", "none", "refused", "problems", "tokens", "cost", "time"];
|
|
61
|
+
const cell = (r, c) => c === "tokens"
|
|
62
|
+
? compactTokens(r.tokens)
|
|
63
|
+
: c === "cost"
|
|
64
|
+
? usd(r.costUsd)
|
|
65
|
+
: c === "time"
|
|
66
|
+
? r.durationMs === undefined
|
|
67
|
+
? "—"
|
|
68
|
+
: humanSeconds(r.durationMs / 1000)
|
|
69
|
+
: String(c === "runs" ? r.runs : r[c]);
|
|
70
|
+
const widths = cols.map((c) => Math.max(c.length, ...all.map((r) => cell(r, c).length)));
|
|
71
|
+
const line = (loop, cells, lastChange) => [padToWidth(loop, loopWidth), ...cells.map((c, i) => c.padStart(widths[i] ?? 0)), lastChange].join(" ").trimEnd();
|
|
72
|
+
return [
|
|
73
|
+
line("role", [...cols], "last change"),
|
|
74
|
+
...all.map((r) => line(roleLabel(r.role), cols.map((c) => cell(r, c)), r.lastChangeTs === null ? "—" : formatTimestamp(r.lastChangeTs))),
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|