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
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { join, resolve, sep } from 'node:path';
|
|
5
|
+
import { basename, dirname, join, parse, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { warmReferenceFromJobArtifacts } from '../../../scripts/release-loop/gate-warm-artifacts.js';
|
|
7
7
|
import { stateDirSourceForChild } from '../../agent/identity-env.js';
|
|
8
|
+
import { debug } from '../../debug/log.js';
|
|
9
|
+
import { decidePodReuseCleanup } from './pod-reuse-cleanup.js';
|
|
8
10
|
import type { WarmWorkerCommand, WarmWorkerFileResult, WarmWorkerReply } from './pod-warm-worker-protocol.js';
|
|
9
11
|
|
|
10
12
|
const token = (s: unknown): s is string => typeof s === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(s);
|
|
@@ -22,6 +24,76 @@ const withoutGitEnvironment = (): NodeJS.ProcessEnv => {
|
|
|
22
24
|
return env;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
function resolveThroughExistingAncestor(path: string): string | null {
|
|
28
|
+
let ancestor = resolve(path);
|
|
29
|
+
const suffix: string[] = [];
|
|
30
|
+
for (;;) {
|
|
31
|
+
try { return join(realpathSync(ancestor), ...suffix); }
|
|
32
|
+
catch {
|
|
33
|
+
const parent = dirname(ancestor);
|
|
34
|
+
if (parent === ancestor) return null;
|
|
35
|
+
suffix.unshift(basename(ancestor));
|
|
36
|
+
ancestor = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasCheckoutLocalSymlink(repoDir: string, path: string): boolean {
|
|
42
|
+
if (path.split(sep).includes('..')) return true;
|
|
43
|
+
const absolute = resolve(path);
|
|
44
|
+
let current = parse(absolute).root;
|
|
45
|
+
for (const part of absolute.slice(current.length).split(sep).filter(Boolean)) {
|
|
46
|
+
current = join(current, part);
|
|
47
|
+
try {
|
|
48
|
+
if (!lstatSync(current).isSymbolicLink()) continue;
|
|
49
|
+
const physicalParent = resolveThroughExistingAncestor(dirname(current));
|
|
50
|
+
if (!physicalParent) continue;
|
|
51
|
+
const physicalLink = join(physicalParent, basename(current));
|
|
52
|
+
if (physicalLink === repoDir || physicalLink.startsWith(`${repoDir}${sep}`)) return true;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runtimeDirectoryExclusions(repoDir: string, repoLexicalDir: string, directories: readonly { name: string; path: string }[]) {
|
|
59
|
+
const additionalCleanExclusions: Array<{ name: string; pattern: string }> = [];
|
|
60
|
+
const statusExclusions = new Set<string>();
|
|
61
|
+
for (const directory of directories) {
|
|
62
|
+
const lexical = resolve(directory.path);
|
|
63
|
+
if (hasCheckoutLocalSymlink(repoDir, directory.path)) return null;
|
|
64
|
+
const candidates = new Set([lexical]);
|
|
65
|
+
const resolvedThroughAncestor = resolveThroughExistingAncestor(directory.path);
|
|
66
|
+
if (resolvedThroughAncestor) candidates.add(resolvedThroughAncestor);
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
const root = candidate.startsWith(`${repoDir}${sep}`) ? repoDir
|
|
69
|
+
: candidate.startsWith(`${repoLexicalDir}${sep}`) ? repoLexicalDir : null;
|
|
70
|
+
if (candidate === repoDir || candidate === repoLexicalDir) return null;
|
|
71
|
+
if (!root) continue;
|
|
72
|
+
const path = relative(root, candidate).split(sep).join('/');
|
|
73
|
+
if (!path || path === '..' || path.startsWith('../')) return null;
|
|
74
|
+
const escaped = path.replace(/[\\*?\[\]]/g, '\\$&');
|
|
75
|
+
const pattern = `/${escaped}/`;
|
|
76
|
+
if (!additionalCleanExclusions.some(item => item.pattern === pattern)) {
|
|
77
|
+
additionalCleanExclusions.push({ name: directory.name, pattern });
|
|
78
|
+
}
|
|
79
|
+
statusExclusions.add(path);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { additionalCleanExclusions, statusExclusions: [...statusExclusions] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function remainingGitStatusPaths(statusPorcelainZ: string): string[] {
|
|
86
|
+
const records = statusPorcelainZ.split('\0').filter(Boolean);
|
|
87
|
+
const paths: string[] = [];
|
|
88
|
+
for (let i = 0; i < records.length; i++) {
|
|
89
|
+
const record = records[i]!;
|
|
90
|
+
if (!/^[ MADRCU?!][ MADRCU?!] /.test(record)) continue;
|
|
91
|
+
paths.push(record.slice(3));
|
|
92
|
+
if (/[RC]/.test(record.slice(0, 2))) i++;
|
|
93
|
+
}
|
|
94
|
+
return paths;
|
|
95
|
+
}
|
|
96
|
+
|
|
25
97
|
export interface WarmWorkerRunFile {
|
|
26
98
|
file: string;
|
|
27
99
|
cutSha: string;
|
|
@@ -33,6 +105,7 @@ export interface WarmWorkerRunFile {
|
|
|
33
105
|
deadlineAt: number;
|
|
34
106
|
}
|
|
35
107
|
export type WarmWorkerFileRunner = (request: WarmWorkerRunFile) => Promise<number>;
|
|
108
|
+
type WarmWorkerGitResult = { status: number | null; stdout?: string | null };
|
|
36
109
|
|
|
37
110
|
/** Recheck the source bytes before every request, not only at Pod init. */
|
|
38
111
|
export function warmWorkerSourceMatches(repoDir: string, cutSha: string, lockHash: string): boolean {
|
|
@@ -71,12 +144,13 @@ export class WarmWorkerRuntime {
|
|
|
71
144
|
artifactsRoot: string;
|
|
72
145
|
workerId: string;
|
|
73
146
|
runFile: WarmWorkerFileRunner;
|
|
147
|
+
runGit?: (args: readonly string[]) => WarmWorkerGitResult;
|
|
74
148
|
removeScratch?: (path: string) => void;
|
|
75
149
|
now?: () => number;
|
|
76
150
|
}) {}
|
|
77
151
|
|
|
78
152
|
handle(command: WarmWorkerCommand): Promise<WarmWorkerOutcome> {
|
|
79
|
-
const next = this.tail.then(() => this.execute(command)).catch((): WarmWorkerOutcome => {
|
|
153
|
+
const next = this.tail.then(() => this.execute(command)).catch((error): WarmWorkerOutcome => {
|
|
80
154
|
this.quarantined = true;
|
|
81
155
|
return { reply: null, quarantineWorker: true, reason: 'execution-error' };
|
|
82
156
|
});
|
|
@@ -131,6 +205,18 @@ export class WarmWorkerRuntime {
|
|
|
131
205
|
if (!artifactsDir.startsWith(`${artifactsRoot}${sep}`) || existsSync(artifactsDir)) {
|
|
132
206
|
return { reply: null, quarantineWorker: false, reason: 'invalid-command' };
|
|
133
207
|
}
|
|
208
|
+
const runtimeExclusions = runtimeDirectoryExclusions(repo, resolve(this.options.repoDir), [
|
|
209
|
+
{ name: 'worker scratch directory', path: this.options.scratchDir },
|
|
210
|
+
{ name: 'worker artifacts directory', path: artifactsRoot },
|
|
211
|
+
]);
|
|
212
|
+
if (!runtimeExclusions) {
|
|
213
|
+
this.quarantined = true;
|
|
214
|
+
try { debug.log('pod.reuse', 'clean-failed', {
|
|
215
|
+
runId: command.runId, attemptId: command.attemptId, workerId: command.workerId,
|
|
216
|
+
reason: 'unsafe-runtime-directory', remainingPaths: [],
|
|
217
|
+
}, { level: 'warn' }); } catch { /* observation cannot revive an unsafe worker */ }
|
|
218
|
+
return { reply: null, quarantineWorker: true, reason: 'cleanup-failed', artifactsDir };
|
|
219
|
+
}
|
|
134
220
|
mkdirSync(this.options.scratchDir, { recursive: true });
|
|
135
221
|
mkdirSync(artifactsRoot, { recursive: true });
|
|
136
222
|
mkdirSync(artifactsDir, { recursive: true, mode: 0o700 });
|
|
@@ -180,6 +266,74 @@ export class WarmWorkerRuntime {
|
|
|
180
266
|
if (reason !== 'complete') break;
|
|
181
267
|
}
|
|
182
268
|
if (now() >= command.deadlineAt) reason = 'execution-error';
|
|
269
|
+
const sourceMatchedBeforeCleanup = warmWorkerSourceMatches(repo, command.cutSha, command.key.lockHash);
|
|
270
|
+
// A worker accepts another request only after restoring the pinned source and removing prior-request artifacts.
|
|
271
|
+
const cleanupPlan = decidePodReuseCleanup({
|
|
272
|
+
base: command.cutSha,
|
|
273
|
+
resetExitCode: 0,
|
|
274
|
+
cleanExitCode: 0,
|
|
275
|
+
statusExitCode: 0,
|
|
276
|
+
statusPorcelain: '',
|
|
277
|
+
...runtimeExclusions,
|
|
278
|
+
});
|
|
279
|
+
const git = (args: readonly string[]): WarmWorkerGitResult => {
|
|
280
|
+
try {
|
|
281
|
+
return this.options.runGit?.(args) ?? spawnSync('git', [...args], {
|
|
282
|
+
cwd: repo, encoding: 'utf8', env: withoutGitEnvironment(), maxBuffer: 16 * 1024 * 1024,
|
|
283
|
+
});
|
|
284
|
+
} catch {
|
|
285
|
+
return { status: null, stdout: null };
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const rootMatches = () => {
|
|
289
|
+
const top = git(['rev-parse', '--show-toplevel']);
|
|
290
|
+
if (top.status !== 0 || !top.stdout?.trim()) return false;
|
|
291
|
+
try { return realpathSync(top.stdout.trim()) === repo; } catch { return false; }
|
|
292
|
+
};
|
|
293
|
+
const discard = (reason: string, remainingPaths: readonly string[] = []): WarmWorkerOutcome => {
|
|
294
|
+
this.quarantined = true;
|
|
295
|
+
try { debug.log('pod.reuse', 'clean-failed', {
|
|
296
|
+
runId: command.runId,
|
|
297
|
+
attemptId: command.attemptId,
|
|
298
|
+
workerId: command.workerId,
|
|
299
|
+
reason,
|
|
300
|
+
remainingPaths: remainingPaths.slice(0, 5),
|
|
301
|
+
}, { level: 'warn' }); } catch { /* observation cannot revive an unsafe worker */ }
|
|
302
|
+
return { reply: null, quarantineWorker: true, reason: 'cleanup-failed', artifactsDir };
|
|
303
|
+
};
|
|
304
|
+
if (!rootMatches()) return discard('checkout-root-mismatch');
|
|
305
|
+
const reset = git(cleanupPlan.resetArgs);
|
|
306
|
+
if (reset.status !== 0) {
|
|
307
|
+
const failed = decidePodReuseCleanup({ base: command.cutSha, resetExitCode: reset.status,
|
|
308
|
+
cleanExitCode: null, statusExitCode: null, statusPorcelain: '', ...runtimeExclusions });
|
|
309
|
+
return discard(failed.reason ?? 'reset-failed');
|
|
310
|
+
}
|
|
311
|
+
if (!rootMatches()) return discard('checkout-root-mismatch');
|
|
312
|
+
const clean = git(cleanupPlan.cleanArgs);
|
|
313
|
+
if (clean.status !== 0) {
|
|
314
|
+
const failed = decidePodReuseCleanup({ base: command.cutSha, resetExitCode: 0,
|
|
315
|
+
cleanExitCode: clean.status, statusExitCode: null, statusPorcelain: '', ...runtimeExclusions });
|
|
316
|
+
return discard(failed.reason ?? 'clean-failed');
|
|
317
|
+
}
|
|
318
|
+
if (!rootMatches()) return discard('checkout-root-mismatch');
|
|
319
|
+
const status = git(cleanupPlan.statusArgs);
|
|
320
|
+
if (status.status === 0 && typeof status.stdout !== 'string') return discard('status-output-missing');
|
|
321
|
+
const cleanup = decidePodReuseCleanup({
|
|
322
|
+
base: command.cutSha,
|
|
323
|
+
resetExitCode: reset.status,
|
|
324
|
+
cleanExitCode: clean.status,
|
|
325
|
+
statusExitCode: status.status,
|
|
326
|
+
statusPorcelain: status.stdout ?? '',
|
|
327
|
+
remainingPaths: remainingGitStatusPaths(status.stdout ?? ''),
|
|
328
|
+
...runtimeExclusions,
|
|
329
|
+
});
|
|
330
|
+
if (cleanup.outcome === 'discard') {
|
|
331
|
+
return discard(cleanup.reason ?? 'unknown', cleanup.remainingPaths);
|
|
332
|
+
}
|
|
333
|
+
if (!sourceMatchedBeforeCleanup) {
|
|
334
|
+
this.quarantined = true;
|
|
335
|
+
return { reply: null, quarantineWorker: true, reason: 'quarantined', artifactsDir };
|
|
336
|
+
}
|
|
183
337
|
if (reason !== 'complete') this.quarantined = true;
|
|
184
338
|
if (reason !== 'complete' || files.length !== command.files.length) return { reply: null, quarantineWorker: true, reason, artifactsDir };
|
|
185
339
|
if (!warmWorkerSourceMatches(repo, command.cutSha, command.key.lockHash)) {
|
|
@@ -43,8 +43,9 @@ export const POD_LOGS_KEEP_BYTES = 4_500_000;
|
|
|
43
43
|
* POD-NORESULT ⓑ (10-07 · 28 failed rc=2 Jobs read back): every child printed its result line, but one
|
|
44
44
|
* (si-task-71f68d57f6b7, image 0.2.15) had stderr lines (`[graph] collect fail`) after it, so the old
|
|
45
45
|
* «last line only» check skipped the flatten and the host kept `ok:false` with no stage or PR number.
|
|
46
|
-
* Scan back to
|
|
47
|
-
|
|
46
|
+
* Scan back to a terminal child result; an interim JSON row or successful row on rc≠0 cannot hide a later failure.
|
|
47
|
+
* Keep this shell command on one physical line: Job command extraction consumes it as one line. */
|
|
48
|
+
export const POD_RESULT_FLATTEN = `ELANOUS_CHILD_RC="$rc" bun -e ' const fs=require("fs"); const p="/tmp/si.out"; const lines=fs.readFileSync(p,"utf8").trimEnd().split("\\n"); const failed=Number(process.env.ELANOUS_CHILD_RC)!==0; let terminal=null; let appendTerminal=false; let successIndex=-1; for(let i=lines.length-1;i>=0;i--){ const l=lines[i].trim(); if(!l.startsWith("{"))continue; let o; try{o=JSON.parse(l)}catch{continue} if(!o||typeof o!=="object"||Array.isArray(o))continue; const nested=o.kind==="self"&&o.result&&typeof o.result==="object"&&!Array.isArray(o.result); const flat=nested ? {...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})} : o.kind===undefined?o:null; if(!flat||typeof flat.ok!=="boolean"||typeof flat.stage!=="string"||!flat.stage||flat.stage==="running")continue; if(failed&&flat.ok===true){successIndex=i;break;} terminal=flat; appendTerminal=i!==lines.length-1||flat!==o; break; } let reason=""; let reasonIndex=-1; if(failed){ for(let i=successIndex>=0?successIndex+1:0;i<lines.length;i++){ const l=lines[i].trim(); if(/골 완료 경계 누락|self-implement PR guard 실패|draft PR 생성 실패/u.test(l)){ reason=l;reasonIndex=i; }else if(/^(Error:|❌)/u.test(l)&&(!reason||lines.slice(reasonIndex+1,i).some(x=>x.trim().startsWith("[retry]")||x.trim().startsWith("재시도:")))){ reason=l;reasonIndex=i; } } } if(terminal){ if(appendTerminal)fs.appendFileSync(p,"\\n"+JSON.stringify(terminal)+"\\n"); }else if(failed){ if(reason)fs.appendFileSync(p,"\\n"+JSON.stringify({stage:reason.includes("골 완료 경계 누락")?"preflight-blocked":"error",ok:false,error:lines.slice(reasonIndex).join("\\n").trim().slice(0,4000)})+"\\n"); else if(successIndex>=0)fs.appendFileSync(p,"\\n"+JSON.stringify({stage:"error",ok:false,error:"child exited rc="+process.env.ELANOUS_CHILD_RC+" after a success result without a terminal failure result"})+"\\n"); }'`;
|
|
48
49
|
|
|
49
50
|
export const POD_JOB_DEADLINE_SECONDS = 10_800;
|
|
50
51
|
|
|
@@ -240,11 +241,11 @@ export interface PodSpawnOptions {
|
|
|
240
241
|
memoryAdvice?: () => PodMemoryAdvice;
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
export function defaultKubectl(args: readonly string[], input?: string): { status: number | null; stdout: string; stderr: string } {
|
|
244
|
+
export function defaultKubectl(args: readonly string[], input?: string, timeoutMs = 120_000): { status: number | null; stdout: string; stderr: string } {
|
|
244
245
|
// 셸 프록시가 kubectl 의 로컬 API 요청을 가로챈다(2026-09-25 실측) — 자식 env 에서 뺀다.
|
|
245
246
|
const env = { ...process.env };
|
|
246
247
|
for (const k of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy']) delete env[k];
|
|
247
|
-
const r = spawnSync('kubectl', [...args], { encoding: 'utf8', input, env, timeout:
|
|
248
|
+
const r = spawnSync('kubectl', [...args], { encoding: 'utf8', input, env, timeout: timeoutMs, maxBuffer: LEASE_KUBECTL_MAX_BUFFER });
|
|
248
249
|
const error = (r.error as NodeJS.ErrnoException | undefined)?.code === 'ENOBUFS'
|
|
249
250
|
? `kubectl 출력 너무 큼 (ENOBUFS; maxBuffer=${LEASE_KUBECTL_MAX_BUFFER} bytes)`
|
|
250
251
|
: r.error ? String(r.error) : '';
|
|
@@ -1080,7 +1081,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
|
|
|
1080
1081
|
? `echo "ELANOUS_AUTHOR_ON_POD started host=$(hostname) at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"; author_t0=$(date +%s); ${o.authorGrade === 'lite' || o.authorGrade === 'full' ? `ELANOUS_AUTHOR_GRADE=${o.authorGrade} ` : ''}elanous harness say --substrate local --json${quotedAsk}${quotedChildLlm} -- "$(cat /creds/feature)" > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; ${RUN_END}; echo "ELANOUS_AUTHOR_ON_POD finished host=$(hostname) rc=$rc seconds=$(( $(date +%s) - author_t0 ))"; ${POD_RESULT_FLATTEN}`
|
|
1081
1082
|
: goalPath
|
|
1082
1083
|
? `elanous harness ask ${goalPath} --json${quotedAsk}${quotedChildLlm} > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; ${RUN_END}; ${POD_RESULT_FLATTEN}`
|
|
1083
|
-
: `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; ${RUN_END}`,
|
|
1084
|
+
: `export ${OLD_DOOR_STAMP_ENV}=self-implement; elanous self implement "$(cat /creds/feature)" --json ${quoted} > /tmp/si.out 2>&1 & si_pid=$!; wait "$si_pid"; rc=$?; ${RUN_END}; ${POD_RESULT_FLATTEN}`,
|
|
1084
1085
|
'cat /tmp/si.out',
|
|
1085
1086
|
'[ -f scripts/usage-rollup.ts ] && bun scripts/usage-rollup.ts --since 12h || echo "ELANOUS_USAGE_ROLLUP {\"measured\":false,\"reason\":\"no rollup script\"}"',
|
|
1086
1087
|
// A long run's log export can pass the 5MB artifact limit; skipping it whole left no window into why a
|
|
@@ -1174,6 +1175,7 @@ if [ "$found" -eq 0 ]; then echo ELANOUS_RUN_LEDGER_NONE; fi`,
|
|
|
1174
1175
|
...(o.runId ? [{ name: 'ELANOUS_RUN_ID', value: o.runId }] : []),
|
|
1175
1176
|
...(o.parentRunId ? [{ name: 'ELANOUS_PARENT_RUN_ID', value: o.parentRunId }] : []),
|
|
1176
1177
|
...(o.workIdentity?.taskCard ? [{ name: 'ELANOUS_TASK_CARD_ID', value: o.workIdentity.taskCard }] : []),
|
|
1178
|
+
...(o.workIdentity?.slotId ? [{ name: 'ELANOUS_POD_SLOT_ID', value: o.workIdentity.slotId }] : []),
|
|
1177
1179
|
{ name: 'ELANOUS_SUBSTRATE', value: 'pod' },
|
|
1178
1180
|
...Object.entries(controlInboxEnv(POD_CONTROL_INBOX_DIR)).map(([name, value]) => ({ name, value })),
|
|
1179
1181
|
// ⭐ 런 출처(🅣 RFC run-origin §A3 · #20457/#20468 칸 이름) — Pod 는 자기 elanous_id 를 쓰지 않고 «띄운 감독기»의 hostId 를 물려받는다.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// completion-line presentation. dashboard/execution-badge.ts is now a
|
|
13
13
|
// re-export shim for backward compat until callers migrate.
|
|
14
14
|
|
|
15
|
-
import { currentRouteDecision } from '../../llm/route-decision.js';
|
|
15
|
+
import { currentRouteDecision, routeFallbackLabel } from '../../llm/route-decision.js';
|
|
16
16
|
import { getUserConfig } from '../../user-config.js';
|
|
17
17
|
import { effectiveReasoningLevel, modelSupportsReasoning } from '../../llm.js';
|
|
18
18
|
import { executionBadge } from '../../telegram-exec-footer.js';
|
|
@@ -24,6 +24,10 @@ import { executionBadge } from '../../telegram-exec-footer.js';
|
|
|
24
24
|
* execution footer uses, so both surfaces report the same truth. */
|
|
25
25
|
export function resolveDashboardExecutionBadge(): string | undefined {
|
|
26
26
|
const cfg = getUserConfig();
|
|
27
|
+
// 폴백(TUI-MODEL-BADGE-ACTUAL · 10-16) — 턴이 고른 모델과 «실제로 응답한» 모델이 다르면 «sol → grok».
|
|
28
|
+
// 생각 깊이는 결정 쪽 값이라 넘어간 공급자엔 맞지 않는다 → 폴백 배지엔 안 붙인다.
|
|
29
|
+
const fallback = routeFallbackLabel('dashboard');
|
|
30
|
+
if (fallback) return executionBadge({ model: fallback });
|
|
27
31
|
const decision = currentRouteDecision('dashboard');
|
|
28
32
|
const model = decision?.model || cfg.llm.model;
|
|
29
33
|
if (!model) return undefined;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// TUI-TYPING-LAG-OBS — 타이핑 지연을 «재는» 관측(#28063 이 «실물 불가» 로 착지한 빈칸).
|
|
2
|
+
//
|
|
3
|
+
// 계약(UX SCENARIO-MOCK-RIG 와 합의 · ⛔ 이름·칸을 바꾸지 않는다):
|
|
4
|
+
// debug.log('tui.input-lag', 'measured', {
|
|
5
|
+
// phase: 'keystroke' | 'turn-start', keyToPaintMs?, loopDelayMs: { p99, max }, turnId?, busy, sampled })
|
|
6
|
+
//
|
|
7
|
+
// - keystroke: stdin 키 수신 → 그 뒤 첫 화면 칠(render · typeahead echo) 의 write 호출까지.
|
|
8
|
+
// 이벤트 루프가 막힌 동안 온 키는 막힘이 풀린 뒤에야 'data' 로 배달되므로, 배달 시각만 보면
|
|
9
|
+
// 막힌 시간이 빠진다. ⇒ 20ms 심장박동으로 «배달 전 기다린 시간» 을 추정해 더한다(상한 추정 ·
|
|
10
|
+
// 오차 ≤ 심장박동 간격). 기본 표본 = 10키 중 1(`sampled: 0.1`) · 50ms 이상은 항상 남긴다.
|
|
11
|
+
// - turn-start: turn-preamble 을 감싼 창(시작 → 끝 + 박동 두 번)의 루프 지연(`sampled: 1`) · busy 는 창 끝 값.
|
|
12
|
+
// - loopDelayMs: 그 창의 루프 지연 p99/max(ms) — 같은 20ms 심장박동의 드리프트 표본(최근 ~20초 고리).
|
|
13
|
+
// ⚠️ perf_hooks.monitorEventLoopDelay 를 «안» 쓴다: Bun 1.4.2 에서 동작은 하지만 인스턴스들이
|
|
14
|
+
// «히스토그램 하나를 공유» 하고 한 인스턴스의 disable() 이 전부를 멈춘다(node-c 실측 · 라이브에서
|
|
15
|
+
// turn-start 창을 닫은 뒤 keystroke 창이 {p99:0,max:0} 만 찍었다). 창이 겹치는 이 프로브엔 못 쓴다.
|
|
16
|
+
//
|
|
17
|
+
// 키 경로 비용: 키 청크마다 시각 호출 1회 ⊕ 비교 몇 개 · 할당 0(대기 중 키는 숫자 둘로 쥔다).
|
|
18
|
+
// 화면 칠마다: 대기 키가 없으면 비교 1회. 로그 객체는 «남길 때만» 만든다.
|
|
19
|
+
// 프로브가 등록되지 않으면(대시보드 밖 · 시험) 모든 훅은 null 검사 한 번으로 끝난다.
|
|
20
|
+
|
|
21
|
+
export const INPUT_LAG_CATEGORY = 'tui.input-lag';
|
|
22
|
+
export const INPUT_LAG_EVENT = 'measured';
|
|
23
|
+
/** 표본 비율의 분모 — 측정한 키 N개 중 1개를 남긴다. */
|
|
24
|
+
export const INPUT_LAG_SAMPLE_EVERY = 10;
|
|
25
|
+
/** 이 ms 이상 걸린 키는 표본과 무관하게 항상 남긴다. */
|
|
26
|
+
export const INPUT_LAG_ALWAYS_MS = 50;
|
|
27
|
+
const HEARTBEAT_MS = 20;
|
|
28
|
+
/** 루프가 «안 막힌 채» 이만큼 지나도 칠이 없으면 그 키는 칠을 안 낳은 키(무시 키 등)로 보고 놓는다 —
|
|
29
|
+
* 안 그러면 한참 뒤 다른 칠이 그 키에 붙어 수 분짜리 가짜 지연이 찍힌다(라이브 실측 150,963ms). */
|
|
30
|
+
export const INPUT_LAG_ABANDON_MS = 1000;
|
|
31
|
+
/** 루프 지연 표본 고리 크기 — 20ms 박동이면 ~20초. */
|
|
32
|
+
const LOOP_RING = 1024;
|
|
33
|
+
|
|
34
|
+
export interface LoopDelayMs { p99: number; max: number }
|
|
35
|
+
|
|
36
|
+
export interface InputLagMeasured {
|
|
37
|
+
phase: 'keystroke' | 'turn-start';
|
|
38
|
+
keyToPaintMs?: number;
|
|
39
|
+
loopDelayMs: LoopDelayMs;
|
|
40
|
+
turnId?: string;
|
|
41
|
+
busy: boolean;
|
|
42
|
+
sampled: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TimerLike { unref?: () => void }
|
|
46
|
+
|
|
47
|
+
export interface InputLagProbeOptions {
|
|
48
|
+
log: (category: string, event: string, data: InputLagMeasured) => void;
|
|
49
|
+
isBusy: () => boolean;
|
|
50
|
+
now?: () => number;
|
|
51
|
+
sampleEvery?: number;
|
|
52
|
+
alwaysMs?: number;
|
|
53
|
+
heartbeatMs?: number;
|
|
54
|
+
setIntervalFn?: (fn: () => void, ms: number) => TimerLike;
|
|
55
|
+
clearIntervalFn?: (timer: TimerLike) => void;
|
|
56
|
+
setTimeoutFn?: (fn: () => void, ms: number) => TimerLike;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface InputLagProbe {
|
|
60
|
+
/** 키 `count` 개가 한 번에 도착했다(한 stdin 청크가 여러 키를 낳을 수 있다). */
|
|
61
|
+
noteKey(count?: number): void;
|
|
62
|
+
notePaint(): void;
|
|
63
|
+
measureTurnStart<T>(fn: () => T, turnId?: string): T;
|
|
64
|
+
/** 같은 심장박동을 다른 관측(턴 시작 덩어리 · `dashboard/turn-start-segments.ts`)이 읽는 창 — 시계·할당 0. */
|
|
65
|
+
beats: InputLagBeatView;
|
|
66
|
+
dispose(): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface InputLagBeatView {
|
|
70
|
+
heartbeatMs: number;
|
|
71
|
+
/** 마지막 박동 시각(같은 `now` 시계). */
|
|
72
|
+
lastBeatAt(): number;
|
|
73
|
+
/** `since` 뒤 박동들의 루프 지연 p99/max — 남길 때만 부른다. */
|
|
74
|
+
loopDelaySince(since: number): LoopDelayMs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const round1 = (ms: number): number => Math.round(Math.max(0, ms) * 10) / 10;
|
|
78
|
+
|
|
79
|
+
/** 드리프트 표본 고리에서 `since` «뒤»(초과) 표본의 p99/max. 남길 때만 부른다(그때만 할당). */
|
|
80
|
+
function readLoopDelay(drift: Float64Array, at: Float64Array, count: number, head: number, since: number): LoopDelayMs {
|
|
81
|
+
const picked: number[] = [];
|
|
82
|
+
for (let i = 0; i < count; i++) {
|
|
83
|
+
const idx = (head - 1 - i + LOOP_RING) % LOOP_RING;
|
|
84
|
+
if (at[idx]! <= since) break;
|
|
85
|
+
picked.push(drift[idx]!);
|
|
86
|
+
}
|
|
87
|
+
if (picked.length === 0) return { p99: 0, max: 0 };
|
|
88
|
+
picked.sort((x, y) => x - y);
|
|
89
|
+
return {
|
|
90
|
+
// 최근순위(nearest-rank) p99 — 짧은 창([0, 300])에서도 큰 막힘을 내림으로 잃지 않는다.
|
|
91
|
+
p99: round1(picked[Math.max(0, Math.ceil(picked.length * 0.99) - 1)]!),
|
|
92
|
+
max: round1(picked[picked.length - 1]!),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createInputLagProbe(opts: InputLagProbeOptions): InputLagProbe {
|
|
97
|
+
const now = opts.now ?? (() => performance.now());
|
|
98
|
+
const sampleEvery = Math.max(1, Math.floor(opts.sampleEvery ?? INPUT_LAG_SAMPLE_EVERY));
|
|
99
|
+
const alwaysMs = opts.alwaysMs ?? INPUT_LAG_ALWAYS_MS;
|
|
100
|
+
const heartbeatMs = opts.heartbeatMs ?? HEARTBEAT_MS;
|
|
101
|
+
const setIntervalFn = opts.setIntervalFn ?? ((fn, ms) => setInterval(fn, ms));
|
|
102
|
+
const clearIntervalFn = opts.clearIntervalFn ?? ((t) => clearInterval(t as ReturnType<typeof setInterval>));
|
|
103
|
+
const setTimeoutFn = opts.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
|
|
104
|
+
|
|
105
|
+
// 칠을 기다리는 첫 키(뒤따르는 키는 같은 칠을 기다리므로 첫 키가 최악값이다).
|
|
106
|
+
let pendingAt = -1;
|
|
107
|
+
let pendingWaitMs = 0;
|
|
108
|
+
// 심장박동 — 마지막 박동 시각 ⊕ 방금 끝난 막힘(같은 루프 회전에서 키가 배달되면 그만큼 기다렸다).
|
|
109
|
+
let lastBeatAt = now();
|
|
110
|
+
let stallMs = 0;
|
|
111
|
+
let stallEndedAt = -Infinity;
|
|
112
|
+
const ringDrift = new Float64Array(LOOP_RING);
|
|
113
|
+
const ringAt = new Float64Array(LOOP_RING);
|
|
114
|
+
let ringHead = 0;
|
|
115
|
+
let ringCount = 0;
|
|
116
|
+
let keyWindowStart = lastBeatAt;
|
|
117
|
+
const loopDelaySince = (since: number): LoopDelayMs => readLoopDelay(ringDrift, ringAt, ringCount, ringHead, since);
|
|
118
|
+
const beat = setIntervalFn(() => {
|
|
119
|
+
const t = now();
|
|
120
|
+
const drift = t - lastBeatAt - heartbeatMs;
|
|
121
|
+
ringDrift[ringHead] = Math.max(0, drift);
|
|
122
|
+
ringAt[ringHead] = t;
|
|
123
|
+
ringHead = (ringHead + 1) % LOOP_RING;
|
|
124
|
+
if (ringCount < LOOP_RING) ringCount += 1;
|
|
125
|
+
if (drift > heartbeatMs) { stallMs = drift; stallEndedAt = t; }
|
|
126
|
+
lastBeatAt = t;
|
|
127
|
+
if (pendingAt >= 0 && t - Math.max(pendingAt, stallEndedAt) > INPUT_LAG_ABANDON_MS) pendingAt = -1;
|
|
128
|
+
}, heartbeatMs);
|
|
129
|
+
beat.unref?.();
|
|
130
|
+
|
|
131
|
+
// 표본 줄 사이에 들어온 실제 키 수(측정 횟수가 아니다) — 표본 줄을 남기면 sampleEvery 의 나머지를 이월한다.
|
|
132
|
+
let keysSinceLine = 0;
|
|
133
|
+
let disposed = false;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
noteKey(count = 1): void {
|
|
137
|
+
if (disposed) return;
|
|
138
|
+
keysSinceLine += count;
|
|
139
|
+
if (pendingAt >= 0) return;
|
|
140
|
+
const t = now();
|
|
141
|
+
// 박동이 밀려 있으면 루프가 아직 막혀 있었다. 마지막 박동이 막힘을 끝낸 박동이면(그 뒤 평상 박동이
|
|
142
|
+
// 아직 없으면) 막힘 동안 쌓인 키가 이제 배달되는 중이다 — 막힘 ⊕ 그 뒤 흐른 시간(상한 추정).
|
|
143
|
+
const overdue = t - lastBeatAt - heartbeatMs;
|
|
144
|
+
const justStalled = stallEndedAt === lastBeatAt ? stallMs + (t - stallEndedAt) : 0;
|
|
145
|
+
pendingAt = t;
|
|
146
|
+
pendingWaitMs = Math.max(0, overdue, justStalled);
|
|
147
|
+
},
|
|
148
|
+
notePaint(): void {
|
|
149
|
+
if (pendingAt < 0) return;
|
|
150
|
+
const keyToPaintMs = now() - pendingAt + pendingWaitMs;
|
|
151
|
+
pendingAt = -1;
|
|
152
|
+
// 50ms↑ 강제 줄은 표본을 소비하지 않는다(표본이 때가 됐어도 다음 칠로 넘긴다) ·
|
|
153
|
+
// 표본 줄은 넘친 키를 다음 표본으로 이월한다.
|
|
154
|
+
const forced = keyToPaintMs >= alwaysMs;
|
|
155
|
+
if (!forced) {
|
|
156
|
+
if (keysSinceLine < sampleEvery) return;
|
|
157
|
+
keysSinceLine %= sampleEvery;
|
|
158
|
+
}
|
|
159
|
+
const t = now();
|
|
160
|
+
const loopDelayMs = loopDelaySince(keyWindowStart);
|
|
161
|
+
keyWindowStart = t;
|
|
162
|
+
opts.log(INPUT_LAG_CATEGORY, INPUT_LAG_EVENT, {
|
|
163
|
+
phase: 'keystroke',
|
|
164
|
+
keyToPaintMs: round1(keyToPaintMs),
|
|
165
|
+
loopDelayMs,
|
|
166
|
+
busy: opts.isBusy(),
|
|
167
|
+
sampled: 1 / sampleEvery,
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
measureTurnStart<T>(fn: () => T, turnId?: string): T {
|
|
171
|
+
if (disposed) return fn();
|
|
172
|
+
const windowStart = now();
|
|
173
|
+
const finish = (): void => {
|
|
174
|
+
// busy 는 창이 닫히는 «지금» 잡는다 — 박동 두 번 뒤 읽으면 그 사이 바뀐 상태가 창에 붙는다.
|
|
175
|
+
const busy = opts.isBusy();
|
|
176
|
+
// 동기 막힘은 루프가 풀린 «뒤» 첫 박동에 찍힌다 — 박동 두 번만큼 기다렸다 읽는다.
|
|
177
|
+
const timer = setTimeoutFn(() => {
|
|
178
|
+
if (disposed) return;
|
|
179
|
+
const loopDelayMs = loopDelaySince(windowStart);
|
|
180
|
+
opts.log(INPUT_LAG_CATEGORY, INPUT_LAG_EVENT, {
|
|
181
|
+
phase: 'turn-start',
|
|
182
|
+
loopDelayMs,
|
|
183
|
+
...(turnId !== undefined ? { turnId } : {}),
|
|
184
|
+
busy,
|
|
185
|
+
sampled: 1,
|
|
186
|
+
});
|
|
187
|
+
}, heartbeatMs * 2);
|
|
188
|
+
timer.unref?.();
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
return fn();
|
|
192
|
+
} finally {
|
|
193
|
+
finish();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
beats: {
|
|
197
|
+
heartbeatMs,
|
|
198
|
+
lastBeatAt: () => lastBeatAt,
|
|
199
|
+
loopDelaySince,
|
|
200
|
+
},
|
|
201
|
+
dispose(): void {
|
|
202
|
+
if (disposed) return;
|
|
203
|
+
disposed = true;
|
|
204
|
+
pendingAt = -1;
|
|
205
|
+
clearIntervalFn(beat);
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── 등록 자리 — 대시보드가 하나 등록하고, 저수준 훅(tui.ts · chat · turn-preamble)은 부르기만 한다. ──
|
|
211
|
+
let activeProbe: InputLagProbe | null = null;
|
|
212
|
+
|
|
213
|
+
export function setInputLagProbe(probe: InputLagProbe | null): void {
|
|
214
|
+
activeProbe = probe;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** stdin(또는 주입) 키 `count` 개 수신 — 키 경로 훅. 등록이 없으면 null 검사 한 번. */
|
|
218
|
+
export function noteInputLagKey(count = 1): void {
|
|
219
|
+
activeProbe?.noteKey(count);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 화면 칠(write 호출) 직후 — 칠 경로 훅. */
|
|
223
|
+
export function noteInputLagPaint(): void {
|
|
224
|
+
activeProbe?.notePaint();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** turn-preamble 창을 잰다. 등록이 없으면 fn 만 부른다. */
|
|
228
|
+
export function measureInputLagTurnStart<T>(fn: () => T, turnId?: string): T {
|
|
229
|
+
return activeProbe ? activeProbe.measureTurnStart(fn, turnId) : fn();
|
|
230
|
+
}
|
package/src/tui.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { StringDecoder } from 'node:string_decoder';
|
|
7
7
|
import { perf } from './perf-counters.js';
|
|
8
|
+
import { noteInputLagKey, noteInputLagPaint } from './tui/input-lag-probe.js';
|
|
8
9
|
import { debug } from './debug/log.js';
|
|
9
10
|
|
|
10
11
|
// ── ANSI escape sequences ──
|
|
@@ -911,6 +912,7 @@ export function readKey(source: KeyTraceSource = 'main'): Promise<Key> {
|
|
|
911
912
|
// split across chunk boundaries need a pending buffer that belongs
|
|
912
913
|
// to this stream alone.
|
|
913
914
|
const keys = _stdinParser.push(data);
|
|
915
|
+
if (keys.length > 0) noteInputLagKey(keys.length);
|
|
914
916
|
|
|
915
917
|
// 2026-04-30 — also drain `_keyQueue` here so an `injectKey`
|
|
916
918
|
// call that happened while `readKey` was already awaiting on
|
|
@@ -963,6 +965,7 @@ export function readKey(source: KeyTraceSource = 'main'): Promise<Key> {
|
|
|
963
965
|
* §8.6 — "(A) auto-Enter via injectKey" approach. */
|
|
964
966
|
export function injectKey(key: Key): boolean {
|
|
965
967
|
if (!_raw) return false;
|
|
968
|
+
noteInputLagKey();
|
|
966
969
|
_keyQueue.push(key);
|
|
967
970
|
_injectedKeys.add(key);
|
|
968
971
|
// Wake any in-flight readKey awaiting on stdin by emitting an empty
|
|
@@ -1109,6 +1112,7 @@ export function render(lines: string[], opts: RenderOptions = {}): void {
|
|
|
1109
1112
|
}
|
|
1110
1113
|
perf.recordStdoutWrite(buf.length, true);
|
|
1111
1114
|
perf.markDrawEnd();
|
|
1115
|
+
noteInputLagPaint();
|
|
1112
1116
|
|
|
1113
1117
|
_lastFrameLines = lines.slice(0, lineCount);
|
|
1114
1118
|
_lastFrameRows = rows;
|