elanous 0.2.31 → 0.2.32
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1834-50625904996a8fa8.js → 1834-98b64172bc1efe77.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/69-0a2c8209c6d4a158.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3535a4a69f1fafaa.js → page-a9717a67c2eecbaf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5a503541353e60a8.js → page-c0f1ca371f710b06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-6025101384e361a5.js → page-46430d45a9e04eac.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a645f00a06483e9a.js → page-a27b1e5a0d1b308f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-e9bab7413b028b28.js → page-8ea49a4c6dd2c6e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-e547e6ce55f1fb33.js → page-0e5a41fac1c75396.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-d27e1b9850fd54bf.js → page-31aaf1ed37c6df9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-0fec29c7ba01894b.js → page-c69247e7134d329d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-339a07410cd6f36b.js → layout-66dc5cd3a0999abe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-2fd93ed8c9f16580.js → page-d256f5d5d9552823.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9b564dbff0fefc35.js → page-ce8f1907b8a6101c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-814ba51385225776.js → page-32866485913d6a04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-397d479626e5a459.js → not-found-7607cca9b84950c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-af1bba4e3251b728.js → page-eeab86176b03dc3b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-3ad3be5e7d3fdce9.js → page-5b93dbae3432b996.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4da93ad35d4a8d28.js → page-f47e768b5b242b5a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-3a658d5fa6d16a25.js → page-95306f892aa2d0ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-0157de6e85f8bb6b.js → page-940fb6c217f6b470.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0a07363b130182a1.js → page-8c66a3b24a745630.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-841d73d4ac44ed49.js → page-7b675dfd35c36fc9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-6f43a3d1146e6377.js → page-8bd0d3cf7665bcfc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-521ee23ac8c0abfa.js → page-c219f2ed171131be.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-a9992ee285efaa83.js → page-40f8fa21bdae955b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-3d48e85fa89ec04f.js → page-468f8313e4f1fbd9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-04d00ae9b0890349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-233c71cd5f68f0d7.js → page-4214b855d9233ea8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-70151d78de2f375e.js → page-936752c7aa58762e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-dcac33a3e2cb72c0.js → page-ec052ad359df1797.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0448145ccd5c45c1.js → page-fad650e4793adb65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +1 -0
- package/apps/pwa/out/_next/static/css/b5d88eba78a3d7bf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +17 -14
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/implement-loop.yaml +5 -0
- package/package.json +1 -1
- package/scripts/ci-typecheck-changed.ts +26 -1
- package/scripts/measure-si-task-cpu.ts +143 -0
- package/scripts/ratchets/test-interference-patterns.allowlist.txt +0 -2
- package/scripts/release-loop/prepare-node.ts +102 -4
- package/src/acp/core-turn-bridge.ts +16 -4
- package/src/acp/elanous-extensions.ts +3 -0
- package/src/acp/server.ts +3 -0
- package/src/acp/tool-trace.ts +186 -11
- package/src/agent/self-ambient-cache.ts +156 -0
- package/src/agent/self-ambient.ts +8 -1
- package/src/agent/task-notification.ts +5 -0
- package/src/agent-substrate/pr-reviewer.ts +3 -1
- package/src/ask-user-question/tool.ts +22 -0
- package/src/autopilot/build/llm-conflict-merge.ts +56 -10
- package/src/autopilot/pr-manager.ts +23 -5
- package/src/capture/frame-state-detect.ts +39 -0
- package/src/capture/tui-self-report.ts +134 -6
- package/src/chat/index.ts +61 -15
- package/src/chat/pickers/modal-runtime.ts +6 -0
- package/src/chat/pickers/modals.ts +8 -2
- package/src/cli/ask-bare.ts +1 -0
- package/src/cli/import-cli.ts +12 -0
- package/src/cli/log-event-names.ts +10 -0
- package/src/cli/msg-cli.ts +1 -0
- package/src/cli/pod-cli.ts +14 -3
- package/src/cli/pr-cli.ts +67 -32
- package/src/cli/pr-land-base-read.ts +34 -0
- package/src/cli/pr-land-heavy-remote.ts +81 -25
- package/src/cli/pr-land-overlap-evidence.ts +46 -2
- package/src/cli/pr-land-pod-shadow-job.ts +26 -3
- package/src/cli/pr-land-refusal.ts +20 -5
- package/src/cli/pty-takeover-cli.ts +88 -14
- package/src/cli/sim-cli.ts +18 -0
- package/src/context-bus/ops-events.ts +6 -2
- package/src/core-turn/run-core-turn.ts +1 -0
- package/src/core-turn/run-goal-loop.ts +21 -0
- package/src/core-turn/types.ts +3 -0
- package/src/dashboard/acp-active-model.ts +42 -1
- package/src/dashboard/background-jobs-status.ts +66 -0
- package/src/dashboard/index.ts +117 -44
- package/src/dashboard/input/agent-wake.ts +242 -0
- package/src/dashboard/input/chat-main-entry-runtime.ts +5 -0
- package/src/dashboard/input/external-input-host.ts +23 -9
- package/src/dashboard/input/mouse-wiring.ts +108 -11
- package/src/dashboard/log-click-runtime.ts +118 -2
- package/src/dashboard/turn-preamble.ts +116 -40
- package/src/dashboard/turn-session-layers.ts +4 -2
- package/src/dashboard/turn-start-segments.ts +222 -0
- package/src/display/prompt-row-owner.ts +146 -0
- package/src/display/resize-listener.ts +33 -2
- package/src/fold-stack.ts +4 -2
- package/src/git-fs/index.ts +263 -36
- package/src/harness/gate-reserve.ts +87 -0
- package/src/harness/harness-incidents.ts +13 -0
- package/src/harness/harness-queue.ts +15 -0
- package/src/index.ts +8 -6
- package/src/input/external-input-queue.ts +34 -9
- package/src/llm/route-decision.ts +36 -0
- package/src/llm/tool-loop-steered-rejection.ts +8 -0
- package/src/llm-request-shape.ts +209 -0
- package/src/llm.ts +166 -267
- package/src/loops/orchestrator/tick.ts +24 -2
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/mss/logging/render-categories.ts +5 -0
- package/src/ops/overview.ts +59 -2
- package/src/pr-tool/census.ts +19 -4
- package/src/pr-tool/inflight-registry.ts +143 -0
- package/src/pty-shell/pty-manifest.ts +12 -3
- package/src/pty-shell/tui-run-state.ts +34 -3
- package/src/seat-loop/traffic-signal.ts +2 -1
- package/src/self-dev/ask-launch-flow.ts +13 -0
- package/src/self-dev/draft-residue.ts +243 -24
- package/src/self-dev/shard-plan.ts +11 -3
- package/src/self-dev/shard-run.ts +103 -10
- package/src/self-implement/elanous-tui-spawn.ts +14 -0
- package/src/self-implement/follow-up-placement.ts +79 -0
- package/src/self-implement/gate-artifacts.ts +15 -4
- package/src/self-implement/gate-baseline.ts +96 -5
- package/src/self-implement/gate-cli.ts +7 -2
- package/src/self-implement/gate-remote.ts +26 -1
- package/src/self-implement/host-regate.ts +36 -4
- package/src/self-implement/orchestrator.ts +101 -11
- package/src/self-implement/run-ledger.ts +16 -14
- package/src/session-runtime/index.ts +22 -267
- package/src/shell-runner/background-surface.ts +15 -5
- package/src/skills/tools/agent.ts +39 -6
- package/src/skills/tools/list-dir.ts +11 -0
- package/src/skills/tools/schedule-wakeup.ts +408 -0
- package/src/skills/tools/shell.ts +245 -3
- package/src/status/bar.ts +26 -0
- package/src/steward/digest-delivery.ts +118 -0
- package/src/steward/hold-hygiene.ts +120 -0
- package/src/steward/triage.ts +10 -0
- package/src/surface/shell-runner-boot.ts +5 -2
- package/src/task-agent/task-hand.ts +24 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +65 -11
- package/src/task-orchestrator/surfaces/pod-lease.ts +98 -0
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-pool.ts +3 -1
- package/src/task-orchestrator/surfaces/pod-reuse-cleanup.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +525 -0
- package/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts +90 -0
- package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +156 -2
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +7 -5
- package/src/tui/chat/execution-badge.ts +5 -1
- package/src/tui/input-lag-probe.ts +230 -0
- package/src/tui.ts +4 -0
- package/src/ux-sim/scenario.ts +255 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8097-92f87bd108322f66.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-bf3634a117322df7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b9f51447f757fbf2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-2ebb8fd59b9461f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a2fc4e49a4fa4ba8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-f4b630cc8b5273cd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-61ac22cbab1ee4ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c992eafe028b642e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-805ac2e137b7a121.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-349f2752d8cd37ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-d5c6dcf31c25cecc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-62f4475d3c38fb9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-f219ccdbc0bf27df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-456666766e5ce6fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-7404e377e8adb8c5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-14456240ed2a36ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d2ced71132fac9b2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-f08358bdf251adb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-149dc1caba411bb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9cb0c444903da65d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-17e504c396e0e389.js +0 -1
- package/apps/pwa/out/_next/static/css/3f5fc6f96e2cf7b2.css +0 -1
- /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_ssgManifest.js +0 -0
|
@@ -10,14 +10,31 @@
|
|
|
10
10
|
// overlays catch up too. Both calls are wrapped in try/catch so a
|
|
11
11
|
// transient paint error doesn't kill the listener.
|
|
12
12
|
//
|
|
13
|
+
// (3) TUI-PICKER-RESIZE-SHRINK (#76 · 2026-10-17) — then ask the
|
|
14
|
+
// coordinator for one FORCED frame. Steps (1)+(2) bypass the
|
|
15
|
+
// coordinator flush, so the prompt chrome repaint in (2) writes the
|
|
16
|
+
// input divider straight over the bottom row of an open overlay (the
|
|
17
|
+
// slash picker's `╰` sits on the divider row) and nothing re-emits the
|
|
18
|
+
// overlay afterwards — the picker lost its bottom border until it was
|
|
19
|
+
// closed and reopened. A coordinator frame runs onRender → afterRender
|
|
20
|
+
// (chrome) → flushOverlay (modals on top), so the overlay lands last.
|
|
21
|
+
// `force` matters: the overlay byte-equality cache would otherwise
|
|
22
|
+
// skip a re-emit whose bytes match the last coordinator frame
|
|
23
|
+
// (e.g. 80 → 120 → 80 with no coordinator flush in between).
|
|
24
|
+
//
|
|
13
25
|
// Non-goals (stay out of IDX-1 territory):
|
|
14
|
-
// - Do NOT touch coordinator.flushOverlay cache state.
|
|
15
|
-
// - Do NOT recompute modal bounds (mount-time snapshot preserved).
|
|
26
|
+
// - Do NOT touch coordinator.flushOverlay cache state directly.
|
|
16
27
|
// - Do NOT reach into chat.ts textInput loop internals.
|
|
17
28
|
|
|
29
|
+
import { debug } from '../debug/log.js';
|
|
30
|
+
|
|
18
31
|
export interface ResizeListenerDeps {
|
|
19
32
|
draw: (opts?: { force?: boolean }) => void;
|
|
20
33
|
promptCtl: { repaint: () => void };
|
|
34
|
+
/** Request one forced coordinator frame so modal overlays are
|
|
35
|
+
* re-emitted on top of the chrome the prompt repaint just wrote.
|
|
36
|
+
* Optional: hosts without a coordinator keep the legacy two-step. */
|
|
37
|
+
requestOverlayFrame?: () => void;
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
/** Registers the resize listener and returns a dispose fn that
|
|
@@ -28,6 +45,20 @@ export function installResizeListener(deps: ResizeListenerDeps): () => void {
|
|
|
28
45
|
const handler = (): void => {
|
|
29
46
|
try { deps.draw({ force: true }); } catch { /* swallow — paint errors shouldn't break resize */ }
|
|
30
47
|
try { deps.promptCtl.repaint(); } catch { /* swallow */ }
|
|
48
|
+
let overlayFrame: 'requested' | 'absent' | 'failed' = 'absent';
|
|
49
|
+
if (deps.requestOverlayFrame) {
|
|
50
|
+
try {
|
|
51
|
+
deps.requestOverlayFrame();
|
|
52
|
+
overlayFrame = 'requested';
|
|
53
|
+
} catch {
|
|
54
|
+
overlayFrame = 'failed';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
debug.log('display.resize', 'repaint', {
|
|
58
|
+
cols: process.stdout.columns ?? null,
|
|
59
|
+
rows: process.stdout.rows ?? null,
|
|
60
|
+
overlayFrame,
|
|
61
|
+
});
|
|
31
62
|
};
|
|
32
63
|
process.stdout.on('resize', handler);
|
|
33
64
|
return (): void => {
|
package/src/fold-stack.ts
CHANGED
|
@@ -216,8 +216,9 @@ export class FoldStack {
|
|
|
216
216
|
/** Toggle the latest-pushed static target whose range contains
|
|
217
217
|
* `lineIndex`. Returns true when a target was toggled, false
|
|
218
218
|
* when none contained the line (live targets are never chosen).
|
|
219
|
-
* Reuses the single-target splice/cascade path.
|
|
220
|
-
|
|
219
|
+
* Reuses the single-target splice/cascade path. `logContext` (the
|
|
220
|
+
* click's screen frame) is merged into the expanded/recollapsed line. */
|
|
221
|
+
toggleAtLine(lineIndex: number, logContext: Record<string, unknown> = {}): boolean {
|
|
221
222
|
const t = this.findLatestStaticContaining(lineIndex);
|
|
222
223
|
if (!t) {
|
|
223
224
|
this.logToggleAtLineMiss(lineIndex);
|
|
@@ -236,6 +237,7 @@ export class FoldStack {
|
|
|
236
237
|
lineIndex,
|
|
237
238
|
lineStart,
|
|
238
239
|
lineEnd,
|
|
240
|
+
...logContext,
|
|
239
241
|
});
|
|
240
242
|
}
|
|
241
243
|
return toggled;
|
package/src/git-fs/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// on its own cadence (dashboard throttles to ~2s).
|
|
10
10
|
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
|
-
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
13
13
|
import { findGitDir, type LocatedGit } from './locate.js';
|
|
14
14
|
import { readGitHead } from './read-head.js';
|
|
15
15
|
import { GitFileWatcher } from './watcher.js';
|
|
@@ -31,13 +31,62 @@ export { probeDirty, probeAheadBehind, parsePorcelain } from './dirty.js';
|
|
|
31
31
|
const SNAPSHOT_TIMEOUT_MS = 5_000;
|
|
32
32
|
const SNAPSHOT_STATUS_MAX_CHARS = 2_000;
|
|
33
33
|
const GIT_SNAPSHOT_TTL_MS = 3_000;
|
|
34
|
+
/** A cached snapshot older than this is rebuilt in the background on the next periodic dirty tick. */
|
|
35
|
+
const GIT_SNAPSHOT_KEEP_WARM_MS = 30_000;
|
|
34
36
|
|
|
35
37
|
interface GitSnapshotCacheEntry {
|
|
36
38
|
at: number;
|
|
39
|
+
/** Monotonic store ordering — `at` can flip within the same millisecond. */
|
|
40
|
+
version: number;
|
|
37
41
|
value: string | null;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
const gitSnapshotCache = new Map<string, GitSnapshotCacheEntry>();
|
|
45
|
+
/** The entry an invalidation (known write · HEAD move) just evicted. Only the
|
|
46
|
+
* stale-while-revalidate turn path reads it, and only while it is younger than
|
|
47
|
+
* its window: the turn is served the last snapshot (its header states when it
|
|
48
|
+
* was taken) while the background rebuild runs, instead of rebuilding
|
|
49
|
+
* synchronously on the event loop. Any later store supersedes it. */
|
|
50
|
+
const gitSnapshotLastKnown = new Map<string, GitSnapshotCacheEntry>();
|
|
51
|
+
|
|
52
|
+
function storeGitSnapshot(key: string, entry: Omit<GitSnapshotCacheEntry, 'version'>): void {
|
|
53
|
+
gitSnapshotCache.set(key, { ...entry, version: bumpGitSnapshotVersion(key) });
|
|
54
|
+
gitSnapshotLastKnown.delete(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function evictGitSnapshot(key: string): boolean {
|
|
58
|
+
const entry = gitSnapshotCache.get(key);
|
|
59
|
+
if (!entry) return false;
|
|
60
|
+
gitSnapshotCache.delete(key);
|
|
61
|
+
gitSnapshotLastKnown.set(key, entry);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
/** One background snapshot rebuild per cwd and invalidation generation. */
|
|
65
|
+
const gitSnapshotInFlight = new Map<string, { generation: string; promise: Promise<string | null> }>();
|
|
66
|
+
|
|
67
|
+
/** Per-key monotonic store version: async rebuilds never overwrite a newer
|
|
68
|
+
* sync value, not even when both land in the same millisecond (`at`
|
|
69
|
+
* comparisons can flip). Generation guards invalidation races instead. */
|
|
70
|
+
const gitSnapshotVersion = new Map<string, number>();
|
|
71
|
+
|
|
72
|
+
function gitSnapshotVersionOf(key: string): number {
|
|
73
|
+
return gitSnapshotVersion.get(key) ?? 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function bumpGitSnapshotVersion(key: string): number {
|
|
77
|
+
const version = gitSnapshotVersionOf(key) + 1;
|
|
78
|
+
gitSnapshotVersion.set(key, version);
|
|
79
|
+
return version;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Test-only hold seam (production null): parks an async rebuild after its
|
|
83
|
+
* reads and before its store, so «a write during a rebuild wins» is provable
|
|
84
|
+
* deterministically instead of by racing timers. */
|
|
85
|
+
let gitSnapshotRebuildHold: (() => void | Promise<void>) | null = null;
|
|
86
|
+
|
|
87
|
+
export function __setGitSnapshotRebuildHoldForTest(hold: (() => void | Promise<void>) | null): void {
|
|
88
|
+
gitSnapshotRebuildHold = hold;
|
|
89
|
+
}
|
|
41
90
|
|
|
42
91
|
interface CacheEntry {
|
|
43
92
|
located: LocatedGit;
|
|
@@ -97,8 +146,15 @@ function ensureEntry(cwd: string): CacheEntry | null {
|
|
|
97
146
|
// Re-read HEAD + retarget watcher whenever a watched file changes.
|
|
98
147
|
// One handler covers all listeners so we don't race each subscriber.
|
|
99
148
|
watcher.subscribe(() => {
|
|
149
|
+
const before = entry.head;
|
|
100
150
|
entry.head = readGitHead(entry.located);
|
|
101
151
|
entry.watcher.retargetForHead(entry.head);
|
|
152
|
+
// A branch/ref move makes every cached snapshot's branch + log lines wrong. Only a real
|
|
153
|
+
// move: macOS fs.watch also replays the ref write that happened just before the watch was
|
|
154
|
+
// installed (measured: refs/heads/main «change» right after the first build), and that
|
|
155
|
+
// spurious event must not evict a fresh snapshot.
|
|
156
|
+
if (headMoved(before, entry.head)) invalidateAllGitSnapshots();
|
|
157
|
+
else debug.log('git-fs.snapshot', 'watch-event-head-unchanged', { root: entry.located.root });
|
|
102
158
|
for (const fn of entry.listeners) {
|
|
103
159
|
try { fn(); } catch { /* noop */ }
|
|
104
160
|
}
|
|
@@ -179,8 +235,8 @@ function viewOf(entry: CacheEntry): GitStatusView {
|
|
|
179
235
|
/** Non-blocking twin of `refreshDirty` for the TUI: the probes run as
|
|
180
236
|
* async subprocesses so a slow `git status` (large worktree, loaded
|
|
181
237
|
* host) never stalls key input. Same throttle; concurrent calls share
|
|
182
|
-
* one probe
|
|
183
|
-
*
|
|
238
|
+
* one probe. `force` also marks cached LLM snapshots stale and rebuilds
|
|
239
|
+
* them in the background — a known write happened. */
|
|
184
240
|
export function refreshDirtyAsync(
|
|
185
241
|
cwd: string,
|
|
186
242
|
opts: { minIntervalMs?: number; force?: boolean } = {},
|
|
@@ -188,26 +244,40 @@ export function refreshDirtyAsync(
|
|
|
188
244
|
let entry: CacheEntry | null;
|
|
189
245
|
try { entry = ensureEntry(cwd); } catch (error) { return Promise.reject(error); }
|
|
190
246
|
if (!entry) return Promise.resolve({ head: null, dirty: null, aheadBehind: null, lastDirtyProbeAt: 0 });
|
|
191
|
-
// An in-flight probe (possibly a forced, post-write one) is newer than the last stored view
|
|
247
|
+
// An in-flight probe (possibly a forced, post-write one) is newer than the last stored view —
|
|
248
|
+
// a throttled call joins it instead of returning the pre-write view (#27493 contract).
|
|
192
249
|
const running = dirtyInFlight.get(entry);
|
|
193
250
|
if (running && !opts.force) return running;
|
|
194
251
|
const min = opts.minIntervalMs ?? 2_000;
|
|
195
252
|
if (!opts.force && entry.lastDirtyProbeAt > 0 && Date.now() - entry.lastDirtyProbeAt < min) {
|
|
196
253
|
return Promise.resolve(viewOf(entry));
|
|
197
254
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
255
|
+
const probe = probeDirtyAsyncFor(entry, { fresh: !!opts.force });
|
|
256
|
+
// After the post-write probe is queued, so the snapshot rebuild joins it instead of an older one.
|
|
257
|
+
if (opts.force) invalidateGitSnapshots(cwd);
|
|
258
|
+
// Keep-warm: the dashboard's periodic (non-forced) dirty tick also re-warms a snapshot that is in
|
|
259
|
+
// use, so a turn typed after an idle stretch is served within the stale window instead of falling
|
|
260
|
+
// back to the synchronous build. Only snapshots already cached are kept warm.
|
|
261
|
+
else if (gitSnapshotCache.has(resolve(cwd)) || gitSnapshotLastKnown.has(resolve(cwd))) void refreshGitSnapshotAsync(cwd, { ifOlderThanMs: GIT_SNAPSHOT_KEEP_WARM_MS });
|
|
262
|
+
return probe;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Share the in-flight probe, unless the caller needs one that started after
|
|
266
|
+
* now (`fresh`: a known write just happened) — then queue a new probe behind it. */
|
|
267
|
+
function probeDirtyAsyncFor(entry: CacheEntry, opts: { fresh: boolean }): Promise<GitStatusView> {
|
|
268
|
+
const running = dirtyInFlight.get(entry);
|
|
269
|
+
if (running && !opts.fresh) return running;
|
|
270
|
+
const root = entry.located.root;
|
|
200
271
|
const start = (): Promise<GitStatusView> => Promise.all([probeDirtyAsync(root), probeAheadBehindAsync(root)])
|
|
201
272
|
.then(([dirty, aheadBehind]) => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return viewOf(
|
|
273
|
+
entry.dirty = dirty;
|
|
274
|
+
entry.aheadBehind = aheadBehind;
|
|
275
|
+
entry.lastDirtyProbeAt = Date.now();
|
|
276
|
+
return viewOf(entry);
|
|
206
277
|
});
|
|
207
|
-
// A forced probe queues behind the running one so it reads the post-write tree.
|
|
208
278
|
const probe = (running ? running.then(start, start) : start())
|
|
209
|
-
.finally(() => { if (dirtyInFlight.get(
|
|
210
|
-
dirtyInFlight.set(
|
|
279
|
+
.finally(() => { if (dirtyInFlight.get(entry) === probe) dirtyInFlight.delete(entry); });
|
|
280
|
+
dirtyInFlight.set(entry, probe);
|
|
211
281
|
return probe;
|
|
212
282
|
}
|
|
213
283
|
|
|
@@ -217,7 +287,7 @@ export function refreshDirtyAsync(
|
|
|
217
287
|
* fresh state after mutations. Returns null outside a git repo. */
|
|
218
288
|
export function buildGitSnapshot(
|
|
219
289
|
cwd: string,
|
|
220
|
-
opts: { now?: () => number; ttlMs?: number } = {},
|
|
290
|
+
opts: { now?: () => number; ttlMs?: number; staleWhileRevalidateMs?: number } = {},
|
|
221
291
|
): string | null {
|
|
222
292
|
const now = opts.now ?? Date.now;
|
|
223
293
|
const ttlMs = opts.ttlMs ?? GIT_SNAPSHOT_TTL_MS;
|
|
@@ -228,41 +298,157 @@ export function buildGitSnapshot(
|
|
|
228
298
|
debug.log('git-fs.snapshot', 'cache-hit', { cwd, ageMs: at - cached.at });
|
|
229
299
|
return cached.value;
|
|
230
300
|
}
|
|
301
|
+
// Stale-while-revalidate (TUI turn path): serve the recent snapshot now
|
|
302
|
+
// and rebuild it off the event loop for the next turn.
|
|
303
|
+
if (cached && opts.staleWhileRevalidateMs !== undefined && at - cached.at < opts.staleWhileRevalidateMs) {
|
|
304
|
+
debug.log('git-fs.snapshot', 'stale-hit', { cwd, ageMs: at - cached.at });
|
|
305
|
+
void refreshGitSnapshotAsync(cwd);
|
|
306
|
+
return cached.value;
|
|
307
|
+
}
|
|
308
|
+
// Invalidated (known write · HEAD move) and the background rebuild has not
|
|
309
|
+
// stored yet: serve the evicted snapshot rather than a synchronous rebuild —
|
|
310
|
+
// the TYPING-LAG this path exists to remove. Bounded by the same window.
|
|
311
|
+
const evicted = !cached ? gitSnapshotLastKnown.get(key) : undefined;
|
|
312
|
+
if (evicted && opts.staleWhileRevalidateMs !== undefined && at - evicted.at < opts.staleWhileRevalidateMs) {
|
|
313
|
+
debug.log('git-fs.snapshot', 'stale-after-invalidate', { cwd, ageMs: at - evicted.at, rebuilding: gitSnapshotInFlight.has(key) });
|
|
314
|
+
void refreshGitSnapshotAsync(cwd);
|
|
315
|
+
return evicted.value;
|
|
316
|
+
}
|
|
231
317
|
|
|
232
318
|
const view = refreshDirty(cwd, { force: true });
|
|
233
319
|
let value: string | null = null;
|
|
234
320
|
if (view.head) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
: status;
|
|
243
|
-
|
|
244
|
-
const parts = [
|
|
245
|
-
'This is the git status at the start of this turn. It is a snapshot and will not update during the turn.',
|
|
246
|
-
`Current branch: ${branch}${view.head.isWorktree ? ' (worktree)' : ''}`,
|
|
247
|
-
mainBranch ? `Main branch: ${mainBranch}` : '',
|
|
248
|
-
userName ? `Git user: ${userName}` : '',
|
|
249
|
-
view.aheadBehind
|
|
250
|
-
? `Upstream: ahead ${view.aheadBehind.ahead}, behind ${view.aheadBehind.behind}`
|
|
251
|
-
: '',
|
|
252
|
-
`Status:\n${truncatedStatus || '(clean)'}`,
|
|
253
|
-
`Recent commits:\n${recentLog || '(none)'}`,
|
|
254
|
-
].filter(Boolean);
|
|
255
|
-
value = parts.join('\n\n');
|
|
321
|
+
value = composeGitSnapshot(view, {
|
|
322
|
+
mainBranch: detectDefaultBranch(cwd),
|
|
323
|
+
status: runGit(cwd, ['status', '--short']),
|
|
324
|
+
recentLog: runGit(cwd, ['log', '--oneline', '-n', '5']),
|
|
325
|
+
userName: runGit(cwd, ['config', 'user.name']),
|
|
326
|
+
generatedAt: at,
|
|
327
|
+
});
|
|
256
328
|
}
|
|
257
329
|
|
|
258
|
-
|
|
330
|
+
storeGitSnapshot(key, { at, value });
|
|
259
331
|
debug.log('git-fs.snapshot', 'built', { cwd, ageMs: 0 });
|
|
260
332
|
return value;
|
|
261
333
|
}
|
|
262
334
|
|
|
335
|
+
function composeGitSnapshot(
|
|
336
|
+
view: GitStatusView,
|
|
337
|
+
git: { mainBranch: string | null; status: string; recentLog: string; userName: string; generatedAt: number },
|
|
338
|
+
): string {
|
|
339
|
+
const head = view.head!;
|
|
340
|
+
const branch = head.branch ?? `HEAD@${head.sha?.slice(0, 7) ?? 'unknown'}`;
|
|
341
|
+
const truncatedStatus = git.status.length > SNAPSHOT_STATUS_MAX_CHARS
|
|
342
|
+
? `${git.status.slice(0, SNAPSHOT_STATUS_MAX_CHARS)}\n... (truncated; run git status for more)`
|
|
343
|
+
: git.status;
|
|
344
|
+
const parts = [
|
|
345
|
+
`This is the git status as of ${new Date(git.generatedAt).toISOString()}. It is a snapshot and will not update during the turn.`,
|
|
346
|
+
`Current branch: ${branch}${head.isWorktree ? ' (worktree)' : ''}`,
|
|
347
|
+
git.mainBranch ? `Main branch: ${git.mainBranch}` : '',
|
|
348
|
+
git.userName ? `Git user: ${git.userName}` : '',
|
|
349
|
+
view.aheadBehind
|
|
350
|
+
? `Upstream: ahead ${view.aheadBehind.ahead}, behind ${view.aheadBehind.behind}`
|
|
351
|
+
: '',
|
|
352
|
+
`Status:\n${truncatedStatus || '(clean)'}`,
|
|
353
|
+
`Recent commits:\n${git.recentLog || '(none)'}`,
|
|
354
|
+
].filter(Boolean);
|
|
355
|
+
return parts.join('\n\n');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Rebuild the snapshot for `cwd` with async subprocesses and store it.
|
|
359
|
+
* Concurrent calls share one rebuild. Never throws. `ifOlderThanMs` turns it
|
|
360
|
+
* into a keep-warm tick: a cached snapshot younger than that is left alone. */
|
|
361
|
+
export function refreshGitSnapshotAsync(cwd: string, opts: { ifOlderThanMs?: number } = {}): Promise<string | null> {
|
|
362
|
+
const key = resolve(cwd);
|
|
363
|
+
const cached = gitSnapshotCache.get(key);
|
|
364
|
+
if (cached && opts.ifOlderThanMs !== undefined && Date.now() - cached.at < opts.ifOlderThanMs) {
|
|
365
|
+
return Promise.resolve(cached.value);
|
|
366
|
+
}
|
|
367
|
+
const generation = snapshotGeneration(key);
|
|
368
|
+
const running = gitSnapshotInFlight.get(key);
|
|
369
|
+
if (running && running.generation === generation) return running.promise;
|
|
370
|
+
const startedAt = Date.now();
|
|
371
|
+
const startVersion = gitSnapshotVersionOf(key);
|
|
372
|
+
const rebuild = (async (): Promise<string | null> => {
|
|
373
|
+
const entry = ensureEntry(cwd);
|
|
374
|
+
// Joins the in-flight probe; callers that need a post-write/post-move probe queue a fresh one first.
|
|
375
|
+
const view = entry ? await probeDirtyAsyncFor(entry, { fresh: false }) : getGitStatusView(cwd);
|
|
376
|
+
let value: string | null = null;
|
|
377
|
+
if (view.head) {
|
|
378
|
+
const [mainBranch, status, recentLog, userName] = await Promise.all([
|
|
379
|
+
detectDefaultBranchAsync(cwd),
|
|
380
|
+
runGitAsync(cwd, ['status', '--short']),
|
|
381
|
+
runGitAsync(cwd, ['log', '--oneline', '-n', '5']),
|
|
382
|
+
runGitAsync(cwd, ['config', 'user.name']),
|
|
383
|
+
]);
|
|
384
|
+
// «as of» = when the reads finished, not when the rebuild was queued.
|
|
385
|
+
value = composeGitSnapshot(view, { mainBranch, status, recentLog, userName, generatedAt: Date.now() });
|
|
386
|
+
}
|
|
387
|
+
// Test-only hold seam: reads are done, the store decision is next.
|
|
388
|
+
if (gitSnapshotRebuildHold) await gitSnapshotRebuildHold();
|
|
389
|
+
// An invalidation that landed mid-rebuild wins: keep the entry stale.
|
|
390
|
+
// A newer entry (e.g. a synchronous build that overlapped this rebuild) is
|
|
391
|
+
// kept — ordered by monotonic version, never by `at` (same-ms flips).
|
|
392
|
+
const current = gitSnapshotCache.get(key);
|
|
393
|
+
if (snapshotGeneration(key) === generation && !(current && current.version > startVersion)) {
|
|
394
|
+
storeGitSnapshot(key, { at: startedAt, value });
|
|
395
|
+
}
|
|
396
|
+
debug.log('git-fs.snapshot', 'rebuilt-async', { cwd, ms: Date.now() - startedAt });
|
|
397
|
+
return value;
|
|
398
|
+
})()
|
|
399
|
+
.catch(() => null)
|
|
400
|
+
.finally(() => { if (gitSnapshotInFlight.get(key)?.promise === rebuild) gitSnapshotInFlight.delete(key); });
|
|
401
|
+
gitSnapshotInFlight.set(key, { generation, promise: rebuild });
|
|
402
|
+
return rebuild;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Invalidation generations: a rebuild only stores its result when no
|
|
406
|
+
* invalidation (per cwd or global) landed while it was running. */
|
|
407
|
+
const gitSnapshotGeneration = new Map<string, number>();
|
|
408
|
+
let allGitSnapshotsGeneration = 0;
|
|
409
|
+
|
|
410
|
+
function snapshotGeneration(key: string): string {
|
|
411
|
+
return `${allGitSnapshotsGeneration}:${gitSnapshotGeneration.get(key) ?? 0}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function invalidateAllGitSnapshots(): void {
|
|
415
|
+
// Snapshots that were in use (cached or being rebuilt) are re-warmed for the next turn.
|
|
416
|
+
const inUse = new Set([...gitSnapshotCache.keys(), ...gitSnapshotInFlight.keys()]);
|
|
417
|
+
for (const key of [...gitSnapshotCache.keys()]) evictGitSnapshot(key);
|
|
418
|
+
allGitSnapshotsGeneration += 1;
|
|
419
|
+
// Probes already running read the old ref's upstream — queue one fresh probe per repo behind
|
|
420
|
+
// them, so the rebuilds below join it instead.
|
|
421
|
+
const entries = new Set<CacheEntry>();
|
|
422
|
+
for (const key of inUse) {
|
|
423
|
+
const entry = ensureEntry(key);
|
|
424
|
+
if (entry) entries.add(entry);
|
|
425
|
+
}
|
|
426
|
+
for (const entry of entries) void probeDirtyAsyncFor(entry, { fresh: true });
|
|
427
|
+
for (const key of inUse) void refreshGitSnapshotAsync(key);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** A known write happened: the cached snapshot must not be served again.
|
|
431
|
+
* When a snapshot was in use (cached or being rebuilt), rebuild it in the
|
|
432
|
+
* background so the next turn usually still hits. */
|
|
433
|
+
function invalidateGitSnapshots(cwd: string): void {
|
|
434
|
+
const key = resolve(cwd);
|
|
435
|
+
const wasCached = evictGitSnapshot(key);
|
|
436
|
+
const wasRebuilding = gitSnapshotInFlight.has(key);
|
|
437
|
+
gitSnapshotGeneration.set(key, (gitSnapshotGeneration.get(key) ?? 0) + 1);
|
|
438
|
+
// An in-flight rebuild of the old generation will be discarded — schedule its replacement.
|
|
439
|
+
if (wasCached || wasRebuilding) void refreshGitSnapshotAsync(cwd);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function headMoved(before: GitHeadInfo | null | undefined, after: GitHeadInfo | null | undefined): boolean {
|
|
443
|
+
if (!before || !after) return before !== after;
|
|
444
|
+
return before.branch !== after.branch || before.sha !== after.sha || before.detached !== after.detached;
|
|
445
|
+
}
|
|
446
|
+
|
|
263
447
|
/** Explicitly discard cached git snapshots, including cached non-repositories. */
|
|
264
448
|
export function clearGitSnapshotCache(): void {
|
|
265
449
|
gitSnapshotCache.clear();
|
|
450
|
+
gitSnapshotLastKnown.clear();
|
|
451
|
+
allGitSnapshotsGeneration += 1;
|
|
266
452
|
}
|
|
267
453
|
|
|
268
454
|
function detectDefaultBranch(cwd: string): string | null {
|
|
@@ -281,6 +467,34 @@ function detectDefaultBranch(cwd: string): string | null {
|
|
|
281
467
|
return null;
|
|
282
468
|
}
|
|
283
469
|
|
|
470
|
+
async function detectDefaultBranchAsync(cwd: string): Promise<string | null> {
|
|
471
|
+
const symbolic = await runGitAsync(cwd, ['symbolic-ref', 'refs/remotes/origin/HEAD']);
|
|
472
|
+
if (symbolic.startsWith('refs/remotes/origin/')) {
|
|
473
|
+
return symbolic.slice('refs/remotes/origin/'.length).trim() || null;
|
|
474
|
+
}
|
|
475
|
+
for (const candidate of ['main', 'master', 'develop']) {
|
|
476
|
+
if (await gitSucceeds(cwd, ['show-ref', '--verify', '--quiet', `refs/heads/${candidate}`])) return candidate;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function gitSucceeds(cwd: string, args: string[]): Promise<boolean> {
|
|
482
|
+
return new Promise((resolveOk) => {
|
|
483
|
+
try { execFile('git', args, { cwd, timeout: SNAPSHOT_TIMEOUT_MS }, (error) => resolveOk(!error)); }
|
|
484
|
+
catch { resolveOk(false); }
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function runGitAsync(cwd: string, args: string[]): Promise<string> {
|
|
489
|
+
return new Promise((resolveOut) => {
|
|
490
|
+
try {
|
|
491
|
+
execFile('git', args, { cwd, encoding: 'utf8', timeout: SNAPSHOT_TIMEOUT_MS, maxBuffer: 2 * 1024 * 1024 }, (error, stdout) => {
|
|
492
|
+
resolveOut(error ? '' : String(stdout ?? '').trim());
|
|
493
|
+
});
|
|
494
|
+
} catch { resolveOut(''); }
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
284
498
|
function runGit(cwd: string, args: string[]): string {
|
|
285
499
|
const res = spawnSync('git', args, {
|
|
286
500
|
cwd,
|
|
@@ -292,10 +506,23 @@ function runGit(cwd: string, args: string[]): string {
|
|
|
292
506
|
return String(res.stdout ?? '').trim();
|
|
293
507
|
}
|
|
294
508
|
|
|
509
|
+
/** Test helper — the cached snapshot for `cwd` without building one. */
|
|
510
|
+
export function __peekGitSnapshot(cwd: string): string | null | undefined {
|
|
511
|
+
return gitSnapshotCache.get(resolve(cwd))?.value;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Test helper — what a HEAD move does to cached snapshots. */
|
|
515
|
+
export function __invalidateAllGitSnapshotsForTest(): void {
|
|
516
|
+
invalidateAllGitSnapshots();
|
|
517
|
+
}
|
|
518
|
+
|
|
295
519
|
/** Test helper — drop the cache + dispose every watcher. */
|
|
296
520
|
export function __resetGitFsCache(): void {
|
|
297
521
|
for (const [, entry] of cache) entry.watcher.dispose();
|
|
298
522
|
cache.clear();
|
|
299
523
|
clearGitSnapshotCache();
|
|
524
|
+
gitSnapshotInFlight.clear();
|
|
300
525
|
dirtyInFlight.clear();
|
|
526
|
+
gitSnapshotGeneration.clear();
|
|
527
|
+
gitSnapshotVersion.clear();
|
|
301
528
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
6
|
+
import type { QueueSeat } from './harness-queue.js';
|
|
7
|
+
|
|
8
|
+
/** Shadow calculation only: reserve one of the configured per-seat gate slots while the release gate runs. */
|
|
9
|
+
export function gateReserve({ releaseGateActive, seat, configuredGateCap }: {
|
|
10
|
+
releaseGateActive: boolean; seat: QueueSeat; configuredGateCap: number;
|
|
11
|
+
}): { active: boolean; wouldCap: number } {
|
|
12
|
+
void seat;
|
|
13
|
+
return { active: releaseGateActive, wouldCap: releaseGateActive ? Math.max(0, configuredGateCap - 1) : configuredGateCap };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
const MAX_LEDGER_READS = 32;
|
|
18
|
+
const MAX_PROBE_MS = 1_500;
|
|
19
|
+
|
|
20
|
+
/** Read-only, bounded process and graph-run probe; unavailable or incomplete observation is inactive. */
|
|
21
|
+
export async function detectReleaseGateActive(deps: {
|
|
22
|
+
processes?: () => string | Promise<string>;
|
|
23
|
+
runsRoot?: string;
|
|
24
|
+
read?: (path: string) => string | Promise<string>;
|
|
25
|
+
list?: (path: string) => string[] | Promise<string[]>;
|
|
26
|
+
modifiedAt?: (path: string) => number | Promise<number>;
|
|
27
|
+
/** Test seam for the shared probe deadline; production defaults to 1.5 seconds. */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
} = {}): Promise<boolean> {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeoutMs = deps.timeoutMs ?? MAX_PROBE_MS;
|
|
32
|
+
const deadline = Date.now() + timeoutMs;
|
|
33
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
34
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
35
|
+
timeoutId = setTimeout(() => {
|
|
36
|
+
controller.abort();
|
|
37
|
+
reject(new Error('release gate observation timed out'));
|
|
38
|
+
}, timeoutMs);
|
|
39
|
+
});
|
|
40
|
+
const bounded = <T>(operation: T | Promise<T>): Promise<T> => Promise.race([Promise.resolve(operation), timeout]);
|
|
41
|
+
try {
|
|
42
|
+
const argv = await bounded((deps.processes ?? (async () => {
|
|
43
|
+
const result = await execFileAsync('ps', ['-eo', 'pid=,args='],
|
|
44
|
+
{ encoding: 'utf8', timeout: 1_000, maxBuffer: 4 * 1024 * 1024, signal: controller.signal });
|
|
45
|
+
return result.stdout;
|
|
46
|
+
}))());
|
|
47
|
+
if (Date.now() >= deadline) return false;
|
|
48
|
+
const pids = new Set(argv.split('\n').flatMap((line) => {
|
|
49
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
50
|
+
// Same release run/resume argv fingerprint as RAIL-SAFE, bound here to the graph-run owner's PID.
|
|
51
|
+
return match && /(?:^|\s)(?:\S*\/)?elanous\.mjs\s+(?:(?:--test(?:=\S+)?|--config-dir\s+\S+)\s+)*release\s+(?:run|resume)(?=\s|$)/.test(match[2]!)
|
|
52
|
+
? [Number(match[1])] : [];
|
|
53
|
+
}));
|
|
54
|
+
if (!pids.size) return false;
|
|
55
|
+
const dir = join(deps.runsRoot ?? releaseLedgerRoot(), 'graph-runs', 'release-loop');
|
|
56
|
+
let files: string[];
|
|
57
|
+
try { files = await bounded(deps.list ? deps.list(dir) : fs.readdir(dir)); }
|
|
58
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; throw error; }
|
|
59
|
+
// Inspect metadata for every ledger before choosing the newest: directory order is not recency.
|
|
60
|
+
// A slow or huge directory expires the common deadline, then fails open without scanning further.
|
|
61
|
+
const candidates = files.filter((file) => /^[A-Za-z0-9][A-Za-z0-9._-]*\.json$/.test(file));
|
|
62
|
+
const modifiedAt = deps.modifiedAt ?? (async (path: string) => (await fs.stat(path)).mtimeMs);
|
|
63
|
+
const recent: { file: string; at: number }[] = [];
|
|
64
|
+
for (let offset = 0; offset < candidates.length && Date.now() < deadline; offset += 32) {
|
|
65
|
+
const batch = candidates.slice(offset, offset + 32);
|
|
66
|
+
const times = await bounded(Promise.all(batch.map(async (file) => {
|
|
67
|
+
try { return await modifiedAt(join(dir, file)); } catch { return -1; }
|
|
68
|
+
})));
|
|
69
|
+
for (let i = 0; i < batch.length; i++) if (times[i]! >= 0) recent.push({ file: batch[i]!, at: times[i]! });
|
|
70
|
+
}
|
|
71
|
+
recent.sort((a, b) => b.at - a.at);
|
|
72
|
+
for (const { file } of recent.slice(0, MAX_LEDGER_READS)) {
|
|
73
|
+
if (Date.now() >= deadline) return false;
|
|
74
|
+
let run: {
|
|
75
|
+
pid?: number; status?: string; currentNode?: { nodeId: string }; path?: string[]; nodes?: { nodeId: string; ok: boolean }[];
|
|
76
|
+
};
|
|
77
|
+
try { run = JSON.parse(await bounded((deps.read ?? ((path: string) => fs.readFile(path,
|
|
78
|
+
{ encoding: 'utf8', signal: controller.signal })))(join(dir, file)))) as typeof run; }
|
|
79
|
+
catch { continue; }
|
|
80
|
+
if (run?.status === 'running' && run.pid && pids.has(run.pid)
|
|
81
|
+
&& run.currentNode?.nodeId === 'gate' && run.path?.at(-1) === 'gate'
|
|
82
|
+
&& Array.isArray(run.nodes) && run.path.length === run.nodes.length + 1) return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
} catch { return false; }
|
|
86
|
+
finally { clearTimeout(timeoutId); controller.abort(); }
|
|
87
|
+
}
|
|
@@ -180,6 +180,19 @@ export function detectBursts(rows: readonly RunExit[], { windowMs = 60_000, thre
|
|
|
180
180
|
return bursts.sort((a, b) => Date.parse(a.firstAt) - Date.parse(b.firstAt) || a.reason.localeCompare(b.reason));
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** Common-cause candidates: same hostname+reason exits within a 60-minute window, across seats and entrances; hostless rows stay unobserved. */
|
|
184
|
+
export function detectCommonCauseBursts(rows: readonly RunExit[]): IncidentBurst[] {
|
|
185
|
+
const unique = uniqueRunExits(rows).filter((row) => row.hostname);
|
|
186
|
+
const bursts: IncidentBurst[] = [];
|
|
187
|
+
for (const group of groupRunExits(unique, (row) => JSON.stringify([row.reason, row.hostname]))) {
|
|
188
|
+
const hostname = group[0]!.hostname;
|
|
189
|
+
for (const members of burstMemberWindows(group, HOSTNAME_BURST_WINDOW_MS, 3)) {
|
|
190
|
+
bursts.push(burstFromMembers(members, { ...(hostname ? { hostname } : {}) }));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return bursts.sort((a, b) => Date.parse(a.firstAt) - Date.parse(b.firstAt) || a.reason.localeCompare(b.reason));
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
export function recentBurst(root: string, now: Date | number, { lookbackMs = 15 * 60_000, seat, entrance }: {
|
|
184
197
|
lookbackMs?: number; seat?: string; entrance?: string;
|
|
185
198
|
} = {}): IncidentBurst | null {
|
|
@@ -21,6 +21,7 @@ import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js'
|
|
|
21
21
|
import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
22
22
|
import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
23
23
|
import { writeHarnessQueueReceipt } from './harness-queue-child.js';
|
|
24
|
+
import { detectReleaseGateActive, gateReserve } from './gate-reserve.js';
|
|
24
25
|
import { adviseLaunch, compareLaunchAdvice, type LaunchAdvice } from './launch-advice.js';
|
|
25
26
|
import { finishAdvice, measureFinish, recordFinishHistory, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
|
|
26
27
|
import { collectDraftMetrics, DRAFT_NEEDS_OWNER_SEAT_CAP, shouldHoldSeatNewLaunch, type DraftMetrics } from '../self-dev/draft-sweep.js';
|
|
@@ -85,6 +86,8 @@ export interface HarnessQueueDeps {
|
|
|
85
86
|
pool?: () => QueuePool;
|
|
86
87
|
/** Shadow-only advice seam; exceptions must not affect the queue decision. */
|
|
87
88
|
advice?: typeof adviseLaunch;
|
|
89
|
+
/** Gate-reserve observation only; never changes seatCapDetails or decideSpawn inputs. */
|
|
90
|
+
releaseGateActive?: () => boolean | Promise<boolean>;
|
|
88
91
|
launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
|
|
89
92
|
/** Test seam: runs after a direct CLI call enqueued its row and before that call's own tick (race tests). */
|
|
90
93
|
afterEnqueue?: (item: QueueItem) => void | Promise<void>;
|
|
@@ -1495,6 +1498,10 @@ async function tickHarnessQueueOnce(deps: HarnessQueueDeps, requestedId: string
|
|
|
1495
1498
|
}
|
|
1496
1499
|
return waiting();
|
|
1497
1500
|
}
|
|
1501
|
+
// Shadow measurement runs outside the approval path. One async probe serves all candidates in this tick.
|
|
1502
|
+
const gateActivePromise = !childUniverses?.unreadable.length
|
|
1503
|
+
? Promise.resolve().then(() => (deps.releaseGateActive ?? detectReleaseGateActive)()).catch(() => false)
|
|
1504
|
+
: undefined;
|
|
1498
1505
|
// NEEDS-OWNER-CENSUS: one draft census per tick, shared by every candidate. A full census pages all
|
|
1499
1506
|
// open drafts, the 48h closed cohort, and one claim-comment gh call per open draft — re-running it
|
|
1500
1507
|
// per candidate multiplied those calls by the seat count and raced the rate limit the seat-loop tick
|
|
@@ -1516,6 +1523,14 @@ async function tickHarnessQueueOnce(deps: HarnessQueueDeps, requestedId: string
|
|
|
1516
1523
|
const injectedCap = deps.cap?.(item.seat);
|
|
1517
1524
|
const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
|
|
1518
1525
|
const active = seatActive(item.seat);
|
|
1526
|
+
// An unreadable child universe makes the running count incomplete; do not emit a misleading cap sample.
|
|
1527
|
+
if (gateActivePromise) void gateActivePromise.then((gateActive) => {
|
|
1528
|
+
try {
|
|
1529
|
+
const reserve = gateReserve({ releaseGateActive: gateActive === true, seat: item.seat, configuredGateCap: gate?.[item.seat] ?? 4 });
|
|
1530
|
+
debug.log('harness.queue', 'gate-reserve-shadow', { seat: item.seat, active: reserve.active, wouldCap: reserve.wouldCap,
|
|
1531
|
+
cap: details.cap, running: active });
|
|
1532
|
+
} catch { /* Shadow observation cannot affect the decision. */ }
|
|
1533
|
+
});
|
|
1519
1534
|
if (active >= details.cap) { blocked.set(item.seat, { reason: seatLine(item.seat, active, details), active }); continue; }
|
|
1520
1535
|
if (!pool && !poolReason) {
|
|
1521
1536
|
try { pool = (deps.pool ?? readHarnessQueuePool)(); }
|