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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Read-only, point-in-time CPU measurement of si-task Pods. No Jobs or Pods are created. */
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
type Pod = {
|
|
7
|
+
metadata?: { name?: string; creationTimestamp?: string };
|
|
8
|
+
status?: { phase?: string; containerStatuses?: Array<{ state?: { terminated?: { finishedAt?: string } } }> };
|
|
9
|
+
spec?: { containers?: Array<{ name?: string; resources?: { requests?: { cpu?: string } } }> };
|
|
10
|
+
};
|
|
11
|
+
type PodMetric = { metadata?: { name?: string }; timestamp?: string; containers?: Array<{ name?: string; usage?: { cpu?: string } }> };
|
|
12
|
+
type KubeList<T> = { items?: T[] };
|
|
13
|
+
|
|
14
|
+
export type CpuObservation = {
|
|
15
|
+
dateKst: string;
|
|
16
|
+
context: string;
|
|
17
|
+
namespace: string;
|
|
18
|
+
measuredAt: string;
|
|
19
|
+
coverage: 'point-in-time-current-pods-only';
|
|
20
|
+
podsSeenToday: number;
|
|
21
|
+
unknownOverlapPods: number;
|
|
22
|
+
podsWithMetric: number;
|
|
23
|
+
podsWithoutMetric: number;
|
|
24
|
+
oldestMetricAgeSeconds: number | null;
|
|
25
|
+
cpuMillis: { p50: number | null; p95: number | null; max: number | null };
|
|
26
|
+
candidates: Array<{ request: string; requestMillis: number; p95OfRequest: number | null; observedComparison: string; decision: string }>;
|
|
27
|
+
pods: Array<{ name: string; phase: string; requestMillis: number | null; usageMillis: number | null; metricTimestamp: string | null; metricAgeSeconds: number | null }>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Kubernetes CPU quantity to millicores; null means unusable evidence. */
|
|
31
|
+
export function cpuMillis(value: unknown): number | null {
|
|
32
|
+
if (typeof value !== 'string') return null;
|
|
33
|
+
const match = /^(\d+(?:\.\d+)?)(n|u|m)?$/.exec(value);
|
|
34
|
+
if (!match) return null;
|
|
35
|
+
const factor = match[2] === 'n' ? 0.000001 : match[2] === 'u' ? 0.001 : match[2] === 'm' ? 1 : 1000;
|
|
36
|
+
const result = Number(match[1]) * factor;
|
|
37
|
+
return Number.isFinite(result) ? result : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function percentile(sorted: readonly number[], fraction: number): number | null {
|
|
41
|
+
if (!sorted.length) return null;
|
|
42
|
+
return Math.round(sorted[Math.ceil(fraction * sorted.length) - 1]! * 100) / 100;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function measureSiTaskCpu(
|
|
46
|
+
pods: KubeList<Pod>, metrics: KubeList<PodMetric>,
|
|
47
|
+
options: { dateKst: string; context: string; namespace: string; now?: Date },
|
|
48
|
+
): CpuObservation {
|
|
49
|
+
const start = Date.parse(`${options.dateKst}T00:00:00+09:00`);
|
|
50
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(options.dateKst) || !Number.isFinite(start)
|
|
51
|
+
|| new Date(start + 9 * 60 * 60 * 1000).toISOString().slice(0, 10) !== options.dateKst)
|
|
52
|
+
throw new Error(`invalid KST date: ${options.dateKst}`);
|
|
53
|
+
const end = start + 24 * 60 * 60 * 1000;
|
|
54
|
+
const now = options.now ?? new Date();
|
|
55
|
+
const byName = new Map((metrics.items ?? []).map((metric) => [metric.metadata?.name, metric]));
|
|
56
|
+
let unknownOverlapPods = 0;
|
|
57
|
+
const rows = (pods.items ?? []).filter((pod) => {
|
|
58
|
+
const name = pod.metadata?.name ?? '';
|
|
59
|
+
const created = Date.parse(pod.metadata?.creationTimestamp ?? '');
|
|
60
|
+
const finished = pod.status?.containerStatuses?.map((item) => Date.parse(item.state?.terminated?.finishedAt ?? ''))
|
|
61
|
+
.filter(Number.isFinite).sort((a, b) => b - a)[0];
|
|
62
|
+
const terminal = ['Succeeded', 'Failed'].includes(pod.status?.phase ?? '');
|
|
63
|
+
if (!name.startsWith('si-task-') || !Number.isFinite(created) || created >= end) return false;
|
|
64
|
+
if (created < start && terminal && finished === undefined) {
|
|
65
|
+
unknownOverlapPods++;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return !terminal || finished === undefined || finished >= start;
|
|
69
|
+
}).map((pod) => {
|
|
70
|
+
const name = pod.metadata!.name!;
|
|
71
|
+
const metric = byName.get(name);
|
|
72
|
+
const timestamp = Date.parse(metric?.timestamp ?? '');
|
|
73
|
+
const values = metric?.containers?.map((item) => cpuMillis(item.usage?.cpu)) ?? [];
|
|
74
|
+
const expected = pod.spec?.containers ?? [];
|
|
75
|
+
const namesMatch = expected.every((container) => !container.name || metric?.containers?.some((item) => item.name === container.name));
|
|
76
|
+
const containersComplete = expected.length > 0 && values.length === expected.length && namesMatch;
|
|
77
|
+
// Missing any container value is missing Pod usage, not a zero.
|
|
78
|
+
const ageSeconds = Math.round((now.getTime() - timestamp) / 1000);
|
|
79
|
+
const usageMillis = Number.isFinite(timestamp) && timestamp >= start && timestamp < end
|
|
80
|
+
&& ageSeconds >= -120 && ageSeconds <= 120
|
|
81
|
+
&& containersComplete && values.every((value) => value !== null)
|
|
82
|
+
? Math.round((values as number[]).reduce((sum, value) => sum + value, 0) * 100) / 100 : null;
|
|
83
|
+
const requests = pod.spec?.containers?.map((item) => cpuMillis(item.resources?.requests?.cpu)) ?? [];
|
|
84
|
+
return {
|
|
85
|
+
name, phase: pod.status?.phase ?? 'Unknown',
|
|
86
|
+
requestMillis: requests.length > 0 && requests.every((value) => value !== null)
|
|
87
|
+
? Math.round((requests as number[]).reduce((sum, value) => sum + value, 0) * 100) / 100 : null,
|
|
88
|
+
usageMillis, metricTimestamp: Number.isFinite(timestamp) ? metric!.timestamp! : null,
|
|
89
|
+
metricAgeSeconds: Number.isFinite(timestamp) ? ageSeconds : null,
|
|
90
|
+
};
|
|
91
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
92
|
+
const measured = rows.flatMap((row) => row.usageMillis === null ? [] : [row.usageMillis]).sort((a, b) => a - b);
|
|
93
|
+
const ages = rows.flatMap((row) => row.metricAgeSeconds === null ? [] : [row.metricAgeSeconds]);
|
|
94
|
+
const oldestMetricAgeSeconds = ages.length ? Math.max(...ages) : null;
|
|
95
|
+
const stale = rows.some((row) => row.metricTimestamp !== null && row.metricAgeSeconds !== null
|
|
96
|
+
&& (Date.parse(row.metricTimestamp) < start || Date.parse(row.metricTimestamp) >= end
|
|
97
|
+
|| row.metricAgeSeconds < -120 || row.metricAgeSeconds > 120));
|
|
98
|
+
const p95 = percentile(measured, 0.95);
|
|
99
|
+
const candidates = [250, 500, 1000].map((requestMillis) => ({
|
|
100
|
+
request: requestMillis === 1000 ? '1' : `${requestMillis}m`, requestMillis,
|
|
101
|
+
p95OfRequest: p95 === null ? null : Math.round(p95 / requestMillis * 1000) / 10,
|
|
102
|
+
observedComparison: p95 === null ? 'unmeasured' : p95 <= requestMillis ? 'observed-p95-within-request' : 'observed-p95-above-request',
|
|
103
|
+
// A single metrics-server snapshot cannot justify a request change, even with full current-Pod coverage.
|
|
104
|
+
decision: stale ? 'stale-snapshot' : p95 === null ? 'unmeasured'
|
|
105
|
+
: measured.length < rows.length || unknownOverlapPods > 0 ? 'incomplete-coverage' : 'needs-time-series',
|
|
106
|
+
}));
|
|
107
|
+
return {
|
|
108
|
+
dateKst: options.dateKst, context: options.context, namespace: options.namespace,
|
|
109
|
+
measuredAt: now.toISOString(), coverage: 'point-in-time-current-pods-only',
|
|
110
|
+
podsSeenToday: rows.length, unknownOverlapPods, podsWithMetric: measured.length, podsWithoutMetric: rows.length - measured.length,
|
|
111
|
+
oldestMetricAgeSeconds,
|
|
112
|
+
cpuMillis: { p50: percentile(measured, 0.5), p95, max: measured.at(-1) ?? null }, candidates, pods: rows,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function kubectl(args: string[]): unknown {
|
|
117
|
+
const result = spawnSync('kubectl', args, { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 20_000 });
|
|
118
|
+
if (result.error || result.status !== 0) throw new Error(`kubectl ${args.at(-1)} failed: ${result.error?.message ?? result.stderr?.trim() ?? result.status}`);
|
|
119
|
+
return JSON.parse(result.stdout);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (import.meta.main) {
|
|
123
|
+
const args = process.argv.slice(2);
|
|
124
|
+
const option = (flag: string): string | undefined => {
|
|
125
|
+
const index = args.indexOf(flag);
|
|
126
|
+
return index < 0 ? undefined : args[index + 1];
|
|
127
|
+
};
|
|
128
|
+
const usage = 'Usage: bun scripts/measure-si-task-cpu.ts [--context pool-node-b] [--namespace elanous-test] [--date YYYY-MM-DD] [--pods-json FILE --metrics-json FILE]';
|
|
129
|
+
const known = new Set(['--context', '--namespace', '--date', '--pods-json', '--metrics-json']);
|
|
130
|
+
if (args.includes('--help')) { console.log(usage); process.exit(0); }
|
|
131
|
+
if (args.some((arg, index) => index % 2 === 0 ? !known.has(arg) : arg.startsWith('--')) || args.length % 2) throw new Error(usage);
|
|
132
|
+
const podsPath = option('--pods-json');
|
|
133
|
+
const metricsPath = option('--metrics-json');
|
|
134
|
+
const offline = Boolean(podsPath || metricsPath);
|
|
135
|
+
const context = option('--context') ?? (offline ? 'unknown' : 'pool-node-b');
|
|
136
|
+
const namespace = option('--namespace') ?? (offline ? 'unknown' : 'elanous-test');
|
|
137
|
+
const dateKst = option('--date') ?? new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date());
|
|
138
|
+
if (Boolean(podsPath) !== Boolean(metricsPath)) throw new Error('both --pods-json and --metrics-json are required for offline input');
|
|
139
|
+
const pods = podsPath ? JSON.parse(readFileSync(podsPath, 'utf8')) : kubectl(['--context', context, '-n', namespace, 'get', 'pods', '-o', 'json']);
|
|
140
|
+
const metrics = metricsPath ? JSON.parse(readFileSync(metricsPath, 'utf8'))
|
|
141
|
+
: kubectl(['--context', context, 'get', '--raw', `/apis/metrics.k8s.io/v1beta1/namespaces/${encodeURIComponent(namespace)}/pods`]);
|
|
142
|
+
console.log(JSON.stringify(measureSiTaskCpu(pods, metrics, { dateKst, context, namespace }), null, 2));
|
|
143
|
+
}
|
|
@@ -77,7 +77,6 @@ body-dynamic-import src/nexus/index.test.ts 1
|
|
|
77
77
|
body-dynamic-import src/pty-shell/pty-control-stance.test.ts 12
|
|
78
78
|
body-dynamic-import src/pty-shell/tui-run-state.test.ts 1
|
|
79
79
|
body-dynamic-import src/registry/catalog-discovery-fold.test.ts 1
|
|
80
|
-
body-dynamic-import src/self-dev/ask-launch-flow.test.ts 2
|
|
81
80
|
body-dynamic-import src/self-dev/dev-cli.test.ts 1
|
|
82
81
|
body-dynamic-import src/self-dev/dev-pipeline.test.ts 1
|
|
83
82
|
body-dynamic-import src/self-dev/entrance-registry.test.ts 3
|
|
@@ -222,7 +221,6 @@ chdir-without-restore src/task-orchestrator/surfaces/dev-harness.test.ts 1
|
|
|
222
221
|
chdir-without-restore src/task-orchestrator/surfaces/pod-no-result.test.ts 2
|
|
223
222
|
chdir-without-restore src/task-orchestrator/surfaces/pod-ta-land-owner.test.ts 1
|
|
224
223
|
chdir-without-restore src/task-orchestrator/surfaces/self-implement-pod-oom-retry.test.ts 1
|
|
225
|
-
chdir-without-restore src/task-orchestrator/surfaces/self-implement-pod.test.ts 19
|
|
226
224
|
chdir-without-restore src/task-orchestrator/surfaces/self-implement.test.ts 1
|
|
227
225
|
chdir-without-restore test/llm-codex-dedup-guard.test.ts 1
|
|
228
226
|
chdir-without-restore test/oauth/codex-account-mirror.test.ts 1
|
|
@@ -1,19 +1,117 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { debug } from '../../src/debug/log.js';
|
|
4
5
|
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
5
6
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandRunner } from './node-verdict.js';
|
|
7
|
+
import { isReleaseVersion } from './release-version.js';
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
/** Apparent bytes of the preserved archive; never follow a symlink out of its private directory. */
|
|
10
|
+
function archiveBytes(path: string): number {
|
|
11
|
+
const entry = lstatSync(path);
|
|
12
|
+
if (!entry.isDirectory()) return entry.size;
|
|
13
|
+
return readdirSync(path).reduce((sum, name) => sum + archiveBytes(join(path, name)), 0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readRegularManifest(path: string): Buffer {
|
|
17
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
18
|
+
try {
|
|
19
|
+
if (!fstatSync(fd).isFile()) throw new Error(`prepared manifest is not a regular file: ${path}`);
|
|
20
|
+
return readFileSync(fd);
|
|
21
|
+
} finally { closeSync(fd); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runPrepare(run: CommandRunner = runCommand, move: typeof renameSync = renameSync) {
|
|
8
25
|
const context = readGraphContext();
|
|
9
26
|
const { version, previousVersion } = context.input;
|
|
27
|
+
if (!isReleaseVersion(version)) throw new Error(`invalid release version: ${version}`);
|
|
10
28
|
const commit = nodeOutput(context, 'version-release', 'commit');
|
|
11
29
|
for (const node of ['gate', 'pwa']) if (context.outputs[node]?.outcome !== 'ok') throw new Error(`${node} did not pass`);
|
|
12
|
-
const
|
|
13
|
-
|
|
30
|
+
const root = effectiveInstanceRoot();
|
|
31
|
+
const out = join(root, 'release', version, 'prepared');
|
|
32
|
+
if ([join(root, 'release'), join(root, 'release', version)]
|
|
33
|
+
.some((path) => lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink()))
|
|
34
|
+
throw new Error(`prepare output escapes release root: ${out}`);
|
|
35
|
+
let staleSource: string | undefined;
|
|
36
|
+
let staleManifestBytes: Buffer | undefined;
|
|
37
|
+
let staleParents: Array<{ path: string; dev: number; ino: number; real: string }> | undefined;
|
|
38
|
+
const occupied = lstatSync(out, { throwIfNoEntry: false });
|
|
39
|
+
if (occupied) {
|
|
40
|
+
// Only a tip refresh can prove that this archive belongs to the prior commit. An incomplete or unknown
|
|
41
|
+
// archive is left untouched for investigation; never erase it merely because the path is occupied.
|
|
42
|
+
const old = context.outputs['version-release']?.refreshedFrom;
|
|
43
|
+
const manifestPath = join(out, 'release.json');
|
|
44
|
+
if (typeof old !== 'string' || !/^[0-9a-f]{40}$/i.test(old) || old === commit
|
|
45
|
+
|| !occupied.isDirectory() || !existsSync(manifestPath) || !lstatSync(manifestPath).isFile())
|
|
46
|
+
throw new Error(`prepare output already exists with unverified source: ${out}`);
|
|
47
|
+
if (realpathSync(out) !== join(realpathSync(root), 'release', version, 'prepared'))
|
|
48
|
+
throw new Error(`prepare output escapes release root: ${out}`);
|
|
49
|
+
let manifest: { version?: unknown; sourceCommit?: unknown };
|
|
50
|
+
try { staleManifestBytes = readRegularManifest(manifestPath); manifest = JSON.parse(staleManifestBytes.toString('utf8')) as typeof manifest; }
|
|
51
|
+
catch { throw new Error(`prepare output already exists with unreadable manifest: ${out}`); }
|
|
52
|
+
if (manifest.version !== version || manifest.sourceCommit !== old)
|
|
53
|
+
throw new Error(`prepare output already exists for another source: ${out}`);
|
|
54
|
+
staleParents = [join(root, 'release'), join(root, 'release', version)].map((path) => {
|
|
55
|
+
const stat = lstatSync(path);
|
|
56
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`prepare output escapes release root: ${out}`);
|
|
57
|
+
return { path, dev: stat.dev, ino: stat.ino, real: realpathSync(path) };
|
|
58
|
+
});
|
|
59
|
+
staleSource = old;
|
|
60
|
+
}
|
|
14
61
|
const ref = `v${previousVersion}^{commit}`;
|
|
15
62
|
const prev = run('git', ['rev-parse', '--verify', ref]);
|
|
16
63
|
if (prev.status !== 0 || !/^[0-9a-f]{40}\s*$/i.test(prev.stdout)) throw new Error(`previous release ref unavailable: ${ref}`);
|
|
64
|
+
if (staleSource) {
|
|
65
|
+
const current = lstatSync(out, { throwIfNoEntry: false });
|
|
66
|
+
const manifestPath = join(out, 'release.json');
|
|
67
|
+
if (!current?.isDirectory() || current.dev !== occupied?.dev || current.ino !== occupied?.ino
|
|
68
|
+
|| !lstatSync(manifestPath, { throwIfNoEntry: false })?.isFile()
|
|
69
|
+
|| [join(root, 'release'), join(root, 'release', version)]
|
|
70
|
+
.some((path) => lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink()))
|
|
71
|
+
throw new Error(`prepared output changed before cleanup: ${out}`);
|
|
72
|
+
if (realpathSync(out) !== join(realpathSync(root), 'release', version, 'prepared'))
|
|
73
|
+
throw new Error(`prepare output escapes release root: ${out}`);
|
|
74
|
+
let manifestBytes: Buffer;
|
|
75
|
+
try { manifestBytes = readRegularManifest(manifestPath); }
|
|
76
|
+
catch { throw new Error(`prepared manifest changed before cleanup: ${out}`); }
|
|
77
|
+
if (!staleManifestBytes?.equals(manifestBytes))
|
|
78
|
+
throw new Error(`prepared manifest changed before cleanup: ${out}`);
|
|
79
|
+
// Rename is atomic on this filesystem. Preserve the moved inode even if it differs from the checked one;
|
|
80
|
+
// no later operation deletes a quarantine directory or follows a link inside the archive.
|
|
81
|
+
const quarantine = mkdtempSync(join(realpathSync(root), '.prepared-cleanup-'));
|
|
82
|
+
const moved = join(quarantine, 'prepared');
|
|
83
|
+
move(out, moved);
|
|
84
|
+
const movedStat = lstatSync(moved);
|
|
85
|
+
let movedManifestBytes: Buffer | undefined;
|
|
86
|
+
try { movedManifestBytes = readRegularManifest(join(moved, 'release.json')); }
|
|
87
|
+
catch { /* preserve the quarantined archive for inspection */ }
|
|
88
|
+
if (!movedStat.isDirectory() || movedStat.dev !== occupied?.dev || movedStat.ino !== occupied?.ino
|
|
89
|
+
|| !staleManifestBytes || !movedManifestBytes?.equals(staleManifestBytes))
|
|
90
|
+
throw new Error(`prepared output changed during cleanup; preserved at ${moved}`);
|
|
91
|
+
const receipt = join(quarantine, 'receipt.json');
|
|
92
|
+
const record = { version, old: staleSource, commit, from: out, to: moved, movedAt: new Date().toISOString(),
|
|
93
|
+
bytes: null as number | null, sizeStatus: 'pending' as 'pending' | 'measured' | 'unmeasured', sizeError: undefined as string | undefined };
|
|
94
|
+
try { writeFileSync(receipt, JSON.stringify(record, null, 2) + '\n', { flag: 'wx', mode: 0o600 }); }
|
|
95
|
+
catch (error) { throw new Error(`prepared archive preserved at ${moved}; receipt write failed: ${String(error)}`); }
|
|
96
|
+
try { record.bytes = archiveBytes(moved); record.sizeStatus = 'measured'; }
|
|
97
|
+
catch (error) { record.sizeStatus = 'unmeasured'; record.sizeError = String(error).slice(0, 200); }
|
|
98
|
+
const receiptNext = join(quarantine, 'receipt.next.json');
|
|
99
|
+
try {
|
|
100
|
+
writeFileSync(receiptNext, JSON.stringify(record, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
101
|
+
renameSync(receiptNext, receipt);
|
|
102
|
+
} catch (error) { throw new Error(`prepared archive preserved at ${moved}; receipt update failed: ${String(error)}`); }
|
|
103
|
+
debug.log('release-loop.prepare', 'stale-prepared-quarantined', { version, old: staleSource, commit,
|
|
104
|
+
from: out, to: moved, bytes: record.bytes, sizeStatus: record.sizeStatus, receipt });
|
|
105
|
+
const parentsStable = staleParents?.every(({ path, dev, ino, real }) => {
|
|
106
|
+
try {
|
|
107
|
+
const stat = lstatSync(path);
|
|
108
|
+
return stat.isDirectory() && !stat.isSymbolicLink() && stat.dev === dev && stat.ino === ino
|
|
109
|
+
&& realpathSync(path) === real;
|
|
110
|
+
} catch { return false; }
|
|
111
|
+
});
|
|
112
|
+
if (!parentsStable || lstatSync(out, { throwIfNoEntry: false }))
|
|
113
|
+
throw new Error(`prepare parent changed after quarantine; preserved at ${moved}`);
|
|
114
|
+
}
|
|
17
115
|
const command = run('bun', ['bin/elanous.mjs', 'release', 'prepare', '--version', version, '--source', commit, '--notes-from', prev.stdout.trim(), '--out', out, '--json']);
|
|
18
116
|
if (command.stderr) process.stderr.write(command.stderr);
|
|
19
117
|
const manifest = lastResult(command)?.manifest as { version?: string; sourceCommit?: string; distDir?: string } | undefined;
|
|
@@ -67,8 +67,9 @@ const QUICK_TOOLS: Record<QuickTuiRoute, readonly string[]> = {
|
|
|
67
67
|
'조회': ['decisions_pending', 'ops_seats', 'ops_status', 'release_status', 'logs_query', 'context_now', 'memory_recall', 'Read'],
|
|
68
68
|
'설정 변경': ['DashboardConfigGet', 'DashboardConfigSet', 'LlmListAvailableModels', 'prompt_getRuntimeConfig', 'prompt_setRuntimeConfig', 'view_getConfig', 'view_applyRuntimeConfig', 'view_saveConfig', 'memory_recall'],
|
|
69
69
|
// MsgList is a read-only ledger listing (never acks); MsgPost · MsgAck · RunShell · Bash stay out (TUI-QUICK-ROUTE-EXPLICIT-CMD).
|
|
70
|
-
'조사': ['WebSearch', 'OmniSearch', 'Grep', 'Glob', 'Read', 'fact_check', 'memory_recall', 'MsgList'],
|
|
71
|
-
|
|
70
|
+
'조사': ['WebSearch', 'OmniSearch', 'Grep', 'Glob', 'Read', 'fact_check', 'memory_recall', 'MsgList', 'Agent'],
|
|
71
|
+
// ScheduleWakeup (H5) wakes this TUI in-process; schedule_manage is OS cron — both belong to «상시 일».
|
|
72
|
+
'상시 일': ['schedule_manage', 'schedule_list', 'schedule_create', 'schedule_update', 'ScheduleWakeup', 'session_manage', 'ops_status', 'memory_recall'],
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
export function toolsForTuiRoute(tools: readonly LLMToolSpec[], route: QuickTuiRoute): LLMToolSpec[] {
|
|
@@ -248,7 +249,7 @@ export function bridgeCoreTurnToAcp(
|
|
|
248
249
|
})
|
|
249
250
|
: { route: '코딩·구현' as const, reason: harness ? 'harness' : 'legacy', goalLoop: true };
|
|
250
251
|
const tools = !route.goalLoop && route.route !== '코딩·구현' && route.route !== '읽기·분석'
|
|
251
|
-
? toolsForTuiRoute(availableTools, route.route)
|
|
252
|
+
? toolsForTuiRoute(availableTools, route.route) // bridgeCoreTurnToAcp applies route allowlists here.
|
|
252
253
|
: availableTools;
|
|
253
254
|
if (deps.tuiRouting) {
|
|
254
255
|
debug.log('chat.route', 'decided', { route: route.route, reason: route.reason, goalLoop: route.goalLoop, seat, seatKnown });
|
|
@@ -317,6 +318,7 @@ export function bridgeCoreTurnToAcp(
|
|
|
317
318
|
let finalSupplement: { answer: string; supplement: string; finalText: string } | null = null;
|
|
318
319
|
// turnFamily 는 관측용으로만 유지(goal loop 아밍 판정은 위 사실 질문 판정 앞에서 한다).
|
|
319
320
|
const turnModel = modelOverride ?? getUserConfig().llm.model;
|
|
321
|
+
let resolvedRoute: { provider: string; model: string } | null = null;
|
|
320
322
|
const turnFamily = getModelFamily(turnModel);
|
|
321
323
|
if (armGoalLoop) {
|
|
322
324
|
// 관측 — 어느 family/모델에서 across-turn goal-loop 가 아밍됐나(전-family 개방 검증).
|
|
@@ -421,8 +423,18 @@ export function bridgeCoreTurnToAcp(
|
|
|
421
423
|
void turnCtx.pushToolCall(call);
|
|
422
424
|
},
|
|
423
425
|
onToolResult: (call) => { void turnCtx.pushToolResult(call); },
|
|
426
|
+
// 실제로 응답한 공급자·모델을 기억한다 — 폴백 체인이 넘기면(예 codex → grok) 턴 모델(turnModel)과 달라진다.
|
|
427
|
+
// streamLLMWithTools 가 스트림을 (다시) 열 때마다 부른다 → 사용량 직전 값 = 그 사용량을 낸 쪽.
|
|
428
|
+
onResolvedProvider: (provider, model) => { resolvedRoute = { provider, model }; },
|
|
424
429
|
// 턴 모델(라우팅 결과)을 실어 보낸다 — 대시보드 턴 지표가 설정 모델로 ctx·단가를 잡지 않게.
|
|
425
|
-
|
|
430
|
+
// ⭐ 폴백이면 «넘어간» 모델·공급자를 싣는다(TUI-MODEL-BADGE-ACTUAL · 완료 배지가 읽는다 · 상태줄·/model 은 후속 TUI-MODEL-BADGE-SOURCE).
|
|
431
|
+
onUsage: (usage) => {
|
|
432
|
+
void turnCtx.pushUsage({
|
|
433
|
+
...usage,
|
|
434
|
+
model: usage.model ?? resolvedRoute?.model ?? turnModel,
|
|
435
|
+
...(resolvedRoute ? { routeProvider: resolvedRoute.provider } : {}),
|
|
436
|
+
});
|
|
437
|
+
},
|
|
426
438
|
// Codex Responses API reasoning summary stream. Currently
|
|
427
439
|
// surfaced via forensic only (debug.log) — acts as the
|
|
428
440
|
// "did the model actually emit reasoning?" verification
|
|
@@ -81,6 +81,9 @@ export interface ElanousUiUsagePayload {
|
|
|
81
81
|
provider?: 'anthropic' | 'openai';
|
|
82
82
|
/** 그 턴이 실제로 요청한 모델(라우팅 결과 — 설정 모델과 다를 수 있다). */
|
|
83
83
|
model?: string;
|
|
84
|
+
/** 이 사용량을 낸 «실제» 공급자(폴백 체인이 넘겼으면 넘어간 쪽 · 예 `grok`). `provider` 는 과금 계열 축이라 따로 둔다
|
|
85
|
+
* (TUI-MODEL-BADGE-ACTUAL · 10-16). */
|
|
86
|
+
routeProvider?: string;
|
|
84
87
|
inputTokens?: number;
|
|
85
88
|
outputTokens?: number;
|
|
86
89
|
cacheReadInputTokens?: number;
|
package/src/acp/server.ts
CHANGED
|
@@ -838,6 +838,8 @@ export interface AcpTurnContext {
|
|
|
838
838
|
pushUsage: (usage: {
|
|
839
839
|
provider?: 'anthropic' | 'openai';
|
|
840
840
|
model?: string;
|
|
841
|
+
/** 실제로 응답한 공급자(폴백이면 넘어간 쪽). */
|
|
842
|
+
routeProvider?: string;
|
|
841
843
|
inputTokens?: number;
|
|
842
844
|
outputTokens?: number;
|
|
843
845
|
cacheReadInputTokens?: number;
|
|
@@ -1839,6 +1841,7 @@ function wireAcpConnection(
|
|
|
1839
1841
|
id: `turn:${Date.now()}`,
|
|
1840
1842
|
...(usage.provider !== undefined ? { provider: usage.provider } : {}),
|
|
1841
1843
|
...(usage.model !== undefined ? { model: usage.model } : {}),
|
|
1844
|
+
...(usage.routeProvider !== undefined ? { routeProvider: usage.routeProvider } : {}),
|
|
1842
1845
|
...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
|
|
1843
1846
|
...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
|
|
1844
1847
|
...(usage.cacheReadInputTokens !== undefined
|
package/src/acp/tool-trace.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendMessage } from '../session/index.js';
|
|
1
|
+
import { appendMessage, onSessionCreated } from '../session/index.js';
|
|
2
2
|
// ⛔ 메시지 «모양»은 `session/chat.ts` 가 갖는다 — 여기서 베끼지 않는다.
|
|
3
3
|
// 같은 계약을 두 벌로 두면 한쪽만 자라고, 그때 「어느 표면의 기록인가」가 갈린다.
|
|
4
4
|
import { buildToolTraceMessage } from '../session/chat.js';
|
|
@@ -24,9 +24,13 @@ import { debug } from '../debug/log.js';
|
|
|
24
24
|
* Goodhart 시험이었다. ⇒ 주입 가능한 함수로 떼어 **실행으로** 문다. */
|
|
25
25
|
/** ⛔ 이 저장소 «밖»으로 열지 않는다 — 소비처가 이 파일과 그 시험뿐이다(리뷰 must-fix:
|
|
26
26
|
* 쓰이지 않는 공개 표면). 시험은 리터럴로 넘기므로 이름을 export 할 필요가 없다. */
|
|
27
|
+
type TraceMessage = ReturnType<typeof buildToolTraceMessage>;
|
|
28
|
+
type AppendFn = (sessionId: string, msg: TraceMessage) => unknown;
|
|
29
|
+
type FailureObserver = (info: { sessionId: string; tool: string; id: string; reason: string }) => void;
|
|
30
|
+
|
|
27
31
|
interface AcpToolTraceDeps {
|
|
28
32
|
/** 시험 주입점. 기본은 실제 세션 저장소. */
|
|
29
|
-
readonly append?:
|
|
33
|
+
readonly append?: AppendFn;
|
|
30
34
|
/** ⛔⭐ 메시지 «모양»을 만드는 자리 — 기본은 `session/chat.ts` 의 공유 빌더.
|
|
31
35
|
*
|
|
32
36
|
* 📏 무인 리뷰 must-fix(PR #11120): 산출 필드(`role`/`toolName`/…)만 검사하면
|
|
@@ -35,7 +39,166 @@ interface AcpToolTraceDeps {
|
|
|
35
39
|
* ⇒ 주입점으로 열어 **「공유 빌더를 쓴다」를 시험이 «실행으로» 확인**할 수 있게 한다. */
|
|
36
40
|
readonly buildMessage?: typeof buildToolTraceMessage;
|
|
37
41
|
/** 실패를 «이름으로» 남기는 자리. 기본은 관측 로그. */
|
|
38
|
-
readonly observeFailure?:
|
|
42
|
+
readonly observeFailure?: FailureObserver;
|
|
43
|
+
/** 지연·회수·상한을 남기는 자리(`trace-persist-deferred|flushed|dropped`). 기본은 관측 로그. */
|
|
44
|
+
readonly observe?: (event: TraceDeferEvent, data: Record<string, unknown>) => void;
|
|
45
|
+
/** 「세션이 원장에 생겼다」를 듣는 자리. 기본은 `session/index.ts` 의 `onSessionCreated`. */
|
|
46
|
+
readonly subscribeSessionCreated?: (listener: (meta: { readonly id: string }) => void) => () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type TraceDeferEvent = 'trace-persist-deferred' | 'trace-persist-flushed' | 'trace-persist-dropped';
|
|
50
|
+
|
|
51
|
+
/** ⛔⭐⭐ **「세션이 아직 원장에 없다」는 실패가 아니라 «순서»다 — 지연 기록한다.**
|
|
52
|
+
*
|
|
53
|
+
* 📏 2026-10-16 실측(TUI 결함 #30·#64 · `eln logs --category acp.tool` 12h):
|
|
54
|
+
* `trace-persist-failed «session not found: elanous-session-ndusg6»` 7건이 **13:12:03~15Z 한 턴**에 몰렸고,
|
|
55
|
+
* 그 세션 파일의 첫 줄은 **13:12:48Z**(턴 «끝»)였다.
|
|
56
|
+
* 🔑 원인(호출부): ACP 서버는 세션 id 를 «메모리에만» 민팅하고(`acpServerRegisterSession` · `server.ts`),
|
|
57
|
+
* 원장 채택은 **턴이 «끝난 뒤»** 에 일어난다 — `persistCompletedTuiAcpTurn`(서버 · origin 메타가 있을 때) 또는
|
|
58
|
+
* 대시보드의 `adoptChatSession`(SESSION-RESUME-ISOLATED · 턴 완료 뒤). 그런데 툴 흔적은 «턴 도중»
|
|
59
|
+
* `pushToolResult` → 이 함수에서 쓴다. ⇒ ***새 세션의 «첫 턴» 도구 흔적은 전부 잃었다***
|
|
60
|
+
* (`/session new`·ACP 갱신마다 새 id 라 첫 턴이 반복된다).
|
|
61
|
+
* ⇒ 그 오류면 메시지를 세션별로 붙잡아 두고, 세션이 원장에 생기는 순간(`onSessionCreated`) 또는
|
|
62
|
+
* 같은 세션의 다음 호출 때 «원래 시각(ts) 그대로» 붙인다. 경고는 세션당 «한 번»(지연) ⊕ 회수 때 «수»로.
|
|
63
|
+
* ⛔ 출처(`source`)를 짐작해 여기서 세션을 «만들지» 않는다 — 같은 ACP 경로를 PWA 도 타므로
|
|
64
|
+
* TUI 로 낙인찍으면 남의 세션 속성을 틀리게 박는다. 만드는 일은 원래 주인(턴 완료 채택)에게 둔다. */
|
|
65
|
+
/* ⚠️ 상한은 «메모리 바닥»만 지킨다 — 영영 채택되지 않는 세션(턴 도중 종료)이 쌓이지 않게.
|
|
66
|
+
* 세션당 100 을 넘으면 그 뒤 흔적은 버리고, 세션 32 를 넘으면 가장 오래 기다린 세션을 통째로 버린다.
|
|
67
|
+
* 어느 쪽이든 `trace-persist-dropped` 를 «한 줄» 남길 뿐 버린 «수»를 세거나 간격을 두지 않는다
|
|
68
|
+
* (버림 계수·보고 방식은 연결 골 `ASK-ACP-TRACE-EVICTION-ACCOUNTING` 이 정한다).
|
|
69
|
+
* 축출된 id 가 다시 오면 「한 번」 경고부터 다시 시작한다 — id 는 base36 무작위 6자라 드물다. */
|
|
70
|
+
const MAX_PENDING_PER_SESSION = 100;
|
|
71
|
+
const MAX_PENDING_SESSIONS = 32;
|
|
72
|
+
const SESSION_NOT_FOUND = /^session not found: /;
|
|
73
|
+
|
|
74
|
+
interface PendingSession {
|
|
75
|
+
readonly entries: Array<{
|
|
76
|
+
readonly append: AppendFn;
|
|
77
|
+
readonly msg: TraceMessage;
|
|
78
|
+
readonly tool: string;
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly observeFailure: FailureObserver | undefined;
|
|
81
|
+
}>;
|
|
82
|
+
/** 세션당 상한에 한 번 닿았나 — `trace-persist-dropped` 를 세션당 한 줄만 남기려고. */
|
|
83
|
+
capped: boolean;
|
|
84
|
+
readonly observe: (event: TraceDeferEvent, data: Record<string, unknown>) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const pendingBySession = new Map<string, PendingSession>();
|
|
88
|
+
let unsubscribeCreated: (() => void) | null = null;
|
|
89
|
+
|
|
90
|
+
const defaultObserve = (event: TraceDeferEvent, data: Record<string, unknown>): void => {
|
|
91
|
+
debug.log('acp.tool', event, data);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const safeObserve = (fn: () => void): void => {
|
|
95
|
+
// ⛔ 관측 자체가 던져도 턴을 깨지 않는다 — 그러면 fail-soft 가 무의미해진다.
|
|
96
|
+
try { fn(); } catch { /* 관측은 마지막 방어선이다 */ }
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const errorReason = (err: unknown): string =>
|
|
100
|
+
err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200);
|
|
101
|
+
|
|
102
|
+
function observeFailureOf(
|
|
103
|
+
sessionId: string,
|
|
104
|
+
call: { readonly tool: string; readonly id: string },
|
|
105
|
+
reason: string,
|
|
106
|
+
observeFailure: FailureObserver | undefined,
|
|
107
|
+
): void {
|
|
108
|
+
const observe = observeFailure
|
|
109
|
+
?? ((info) => { debug.log('acp.tool', 'trace-persist-failed', info); });
|
|
110
|
+
safeObserve(() => observe({ sessionId, tool: call.tool, id: call.id, reason }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 붙잡아 둔 흔적을 원래 순서대로 붙인다. 세션이 여전히 없으면 그대로 둔다(`'missing'`). */
|
|
114
|
+
function flushPending(sessionId: string): 'missing' | 'flushed' | 'none' {
|
|
115
|
+
const p = pendingBySession.get(sessionId);
|
|
116
|
+
if (!p) return 'none';
|
|
117
|
+
let flushed = 0;
|
|
118
|
+
let failed = 0;
|
|
119
|
+
while (p.entries.length > 0) {
|
|
120
|
+
const entry = p.entries[0]!;
|
|
121
|
+
try {
|
|
122
|
+
entry.append(sessionId, entry.msg);
|
|
123
|
+
flushed += 1;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const reason = errorReason(err);
|
|
126
|
+
// ⛔ 회수 «도중» 세션이 사라져도(이미 몇 행을 붙인 뒤라도) 그 행과 나머지는 «지연» 그대로 둔다 — 실패가 아니다.
|
|
127
|
+
if (SESSION_NOT_FOUND.test(reason)) {
|
|
128
|
+
// ⭐ 그때까지 붙인 행 수는 «지금» 남긴다 — 다음 회수 보고는 그 뒤의 행만 센다.
|
|
129
|
+
if (flushed > 0 || failed > 0) {
|
|
130
|
+
safeObserve(() => p.observe('trace-persist-flushed', { sessionId, flushed, failed, partial: true }));
|
|
131
|
+
}
|
|
132
|
+
return 'missing';
|
|
133
|
+
}
|
|
134
|
+
failed += 1;
|
|
135
|
+
// ⛔ 회수 중 실패도 «같은» `trace-persist-failed` 로 — `flushed.failed` 합계에만 묻으면 실패가 안 보인다.
|
|
136
|
+
observeFailureOf(sessionId, { tool: entry.tool, id: entry.id }, reason, entry.observeFailure);
|
|
137
|
+
}
|
|
138
|
+
p.entries.shift();
|
|
139
|
+
}
|
|
140
|
+
pendingBySession.delete(sessionId);
|
|
141
|
+
safeObserve(() => p.observe('trace-persist-flushed', { sessionId, flushed, failed }));
|
|
142
|
+
if (pendingBySession.size === 0 && unsubscribeCreated) {
|
|
143
|
+
const off = unsubscribeCreated;
|
|
144
|
+
unsubscribeCreated = null;
|
|
145
|
+
try { off(); } catch { /* 구독 해제 실패는 무해 — 다음 지연이 다시 건다 */ }
|
|
146
|
+
}
|
|
147
|
+
return 'flushed';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function deferTrace(
|
|
151
|
+
sessionId: string,
|
|
152
|
+
call: { readonly id: string; readonly name: string },
|
|
153
|
+
append: AppendFn,
|
|
154
|
+
msg: TraceMessage,
|
|
155
|
+
reason: string,
|
|
156
|
+
deps: AcpToolTraceDeps,
|
|
157
|
+
): void {
|
|
158
|
+
let p = pendingBySession.get(sessionId);
|
|
159
|
+
if (!p) {
|
|
160
|
+
if (pendingBySession.size >= MAX_PENDING_SESSIONS) {
|
|
161
|
+
// 가장 오래 기다린 세션부터 버린다 — 원장에 영영 안 생긴 세션(턴 도중 종료)이 쌓이지 않게.
|
|
162
|
+
const [oldestId, oldest] = pendingBySession.entries().next().value as [string, PendingSession];
|
|
163
|
+
pendingBySession.delete(oldestId);
|
|
164
|
+
safeObserve(() => oldest.observe('trace-persist-dropped', { sessionId: oldestId, reason: 'pending-session-cap' }));
|
|
165
|
+
}
|
|
166
|
+
p = { entries: [], capped: false, observe: deps.observe ?? defaultObserve };
|
|
167
|
+
pendingBySession.set(sessionId, p);
|
|
168
|
+
// ⭐ 경고는 세션당 «한 번» — 호출마다 같은 줄을 찍던 것이 결함 #64 의 다른 반쪽이다.
|
|
169
|
+
safeObserve(() => p!.observe('trace-persist-deferred', { sessionId, tool: call.name, id: call.id, reason }));
|
|
170
|
+
}
|
|
171
|
+
if (p.entries.length < MAX_PENDING_PER_SESSION) {
|
|
172
|
+
p.entries.push({ append, msg, tool: call.name, id: call.id, observeFailure: deps.observeFailure });
|
|
173
|
+
} else if (!p.capped) {
|
|
174
|
+
p.capped = true;
|
|
175
|
+
safeObserve(() => p!.observe('trace-persist-dropped', {
|
|
176
|
+
sessionId, tool: call.name, id: call.id, reason: 'pending-per-session-cap',
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
if (!unsubscribeCreated) {
|
|
180
|
+
const subscribe = deps.subscribeSessionCreated ?? onSessionCreated;
|
|
181
|
+
try {
|
|
182
|
+
unsubscribeCreated = subscribe((meta) => {
|
|
183
|
+
if (!pendingBySession.has(meta.id)) return;
|
|
184
|
+
// ⭐ 채택 «직후» 같은 동기 구간에서 user/assistant 가 이어 붙는다(`persistCompletedTuiAcpTurn`).
|
|
185
|
+
// 마이크로태스크로 미뤄 그 뒤에 붙인다 — 그래야 「마지막 user 뒤의 tool 행」(다음 턴 관찰)이 산다.
|
|
186
|
+
queueMicrotask(() => { try { flushPending(meta.id); } catch { /* fail-soft */ } });
|
|
187
|
+
});
|
|
188
|
+
} catch { unsubscribeCreated = null; }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 시험 전용 — 모듈 상태(지연 큐·구독)를 비운다. */
|
|
193
|
+
export function _resetAcpToolTraceStateForTest(): void {
|
|
194
|
+
pendingBySession.clear();
|
|
195
|
+
if (unsubscribeCreated) { try { unsubscribeCreated(); } catch { /* 무해 */ } }
|
|
196
|
+
unsubscribeCreated = null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 시험·관측용 — 세션별로 붙잡아 둔 흔적 수. */
|
|
200
|
+
export function pendingAcpToolTraceCount(sessionId: string): number {
|
|
201
|
+
return pendingBySession.get(sessionId)?.entries.length ?? 0;
|
|
39
202
|
}
|
|
40
203
|
|
|
41
204
|
/** 한 번의 툴 호출을 감사 기록으로 남긴다.
|
|
@@ -44,8 +207,9 @@ interface AcpToolTraceDeps {
|
|
|
44
207
|
* (`session/chat.ts` 가 세운 규율).
|
|
45
208
|
* ⚠️ **그러나 «조용히 삼키지 않는다»** — 실패하면 이름을 대고 관측에 남긴다.
|
|
46
209
|
* ***조용한 실패야말로 16차가 내내 잡던 형태고, 이 PR 초판이 또 만들었다.***
|
|
210
|
+
* ⭐ 「세션이 아직 원장에 없다」면 지연 기록한다(위 `MAX_PENDING_*` 주석) — 이때도 `false` 다.
|
|
47
211
|
*
|
|
48
|
-
* @returns
|
|
212
|
+
* @returns 지금 저장에 «성공»했으면 `true`. ⛔ 「안 했다」와 「했는데 실패했다」·「미뤘다」는 다른 값이다. */
|
|
49
213
|
export function persistAcpToolTrace(
|
|
50
214
|
sessionId: string,
|
|
51
215
|
call: { readonly id: string; readonly name: string; readonly result?: unknown },
|
|
@@ -56,14 +220,25 @@ export function persistAcpToolTrace(
|
|
|
56
220
|
// ⛔ 모양은 «공유 빌더»가 만든다 — 여기서 손으로 조립하지 않는다(계약을 두 벌로 두지 않는다).
|
|
57
221
|
const build = deps.buildMessage ?? buildToolTraceMessage;
|
|
58
222
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
223
|
+
const msg = build(call.name, args, call.result);
|
|
224
|
+
// 앞서 미뤄 둔 흔적이 있으면 먼저 붙인다(순서 보존). 여전히 세션이 없으면 이것도 뒤에 줄 세운다.
|
|
225
|
+
if (pendingBySession.has(sessionId) && flushPending(sessionId) === 'missing') {
|
|
226
|
+
deferTrace(sessionId, call, append, msg, 'session not found (pending)', deps);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
append(sessionId, msg);
|
|
231
|
+
return true;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const reason = errorReason(err);
|
|
234
|
+
if (SESSION_NOT_FOUND.test(reason)) {
|
|
235
|
+
deferTrace(sessionId, call, append, msg, reason, deps);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
61
240
|
} catch (err) {
|
|
62
|
-
|
|
63
|
-
const observe = deps.observeFailure
|
|
64
|
-
?? ((info) => { debug.log('acp.tool', 'trace-persist-failed', info); });
|
|
65
|
-
// ⛔ 관측 자체가 던져도 턴을 깨지 않는다 — 그러면 fail-soft 가 무의미해진다.
|
|
66
|
-
try { observe({ sessionId, tool: call.name, id: call.id, reason }); } catch { /* 관측은 마지막 방어선이다 */ }
|
|
241
|
+
observeFailureOf(sessionId, { tool: call.name, id: call.id }, errorReason(err), deps.observeFailure);
|
|
67
242
|
return false;
|
|
68
243
|
}
|
|
69
244
|
}
|