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,408 @@
|
|
|
1
|
+
// ── 자기 예약 깨움 `ScheduleWakeup` (TUI-HOMEOSTASIS H5 · TUI-H5-SCHEDULE-WAKEUP · 0.2.31 · 2026-10-17) ─────
|
|
2
|
+
//
|
|
3
|
+
// 🚨 무엇을 푸는가: 자리 TUI 가 «다음 점검 13:20» 이라고 적고 턴을 끝내면, 13:20 에 아무것도 안 열린다
|
|
4
|
+
// (10-16 OPT 실측). TUI 안에 «이 대화로 돌아오는» 예약이 없었다 — `schedule_manage` 는 시스템 crontab 이라
|
|
5
|
+
// 프로세스 밖에서 명령을 돌릴 뿐 이 TUI 의 다음 턴을 못 연다.
|
|
6
|
+
// ⭐ 이 모듈은 TUI 프로세스 «안» 타이머다: 모델이 «HH:MM» 또는 «N초 뒤»(반복 가능)를 예약하면, 그 시각에
|
|
7
|
+
// H2 내부 턴 문(`startTurnIfIdle` · origin 'scheduled')으로 이 TUI 에 한 턴을 청한다. 문은 새로 짓지 않는다.
|
|
8
|
+
// ⛔ 입력칸 버퍼·`injectKey`·`hostControl.submit`·pty 주입 0(대표 비침투) — 거부 사유는 H2 가 낸다.
|
|
9
|
+
// ⛔ 조각 1 범위: 프로세스 안 타이머뿐 — TUI 가 재기동되면 예약은 사라진다(영속·재기동 복원은 후속).
|
|
10
|
+
//
|
|
11
|
+
// 규칙(grok-build scheduler 꼴): 최소 60초 · TUI 당 예약 상한 5 · 반복 7일 만료 · 직전 회차가 아직 돌면 건너뜀 ·
|
|
12
|
+
// 놓친 반복 틱은 하나로 합친다 · 발화 때 H2 가 거부하면(바쁨·사람 입력 중) 1분 뒤 «한 번» 다시 청하고,
|
|
13
|
+
// 그래도 거부면 반복은 다음 회차로, 일회성은 놓침으로 접는다.
|
|
14
|
+
//
|
|
15
|
+
// 세 얼굴:
|
|
16
|
+
// 관측 = debug.log('tui.wakeup', 'scheduled' | 'fired' | 'skipped' | 'rejected' | 'cancelled', { id, ... })
|
|
17
|
+
// 인지 = 예약 시각 도래 ∧ (직전 회차 진행 중 → 건너뜀 · 만료 → 소멸 · H2 거부 사유)
|
|
18
|
+
// 힐링 = H2 로 내부 턴(화면은 H2 의 «↻ 자동 재개(scheduled): …» 한 줄 · 예약 순간엔 «⏰ 예약 깨움 …» 한 줄) ·
|
|
19
|
+
// 거부 → 1분 뒤 1회 재시도 → 그래도 거부면 «⏰ 예약 깨움 놓침» 한 줄 ⊕ 로그(TF 가 본다)
|
|
20
|
+
//
|
|
21
|
+
// 🩹 TUI-H5-ONESHOT-SCHEMA(0.2.32 · 라이브 실측 두 번): 모델이 «한 번만»을 `repeatMinutes:0` 으로 불렀다 →
|
|
22
|
+
// «1 이상» 거부 → 1분 반복으로 재예약 → 취소 → 또 … 한 턴에 26회. ⇒ ① `repeatMinutes:0` = 일회성으로 받는다 ·
|
|
23
|
+
// 다른 틀린 값의 거부 문구는 «일회성은 repeatMinutes 생략» 을 가리킨다(재시도 고리를 끊는 안내) ·
|
|
24
|
+
// ② 한 턴(같은 턴 id · H2 `lastTurnId`) 안 schedule·cancel 합계가 `WAKEUP_MAX_CALLS_PER_TURN` 을 넘으면
|
|
25
|
+
// 거부 ⊕ debug.log('tui.wakeup','loop-capped',{turnId,count}).
|
|
26
|
+
|
|
27
|
+
import { debug } from '../../debug/log.js';
|
|
28
|
+
import type { LLMToolDef } from '../../plugins/core/types.js';
|
|
29
|
+
import type { InternalTurnGate, StartTurnIfIdleOptions, StartTurnIfIdleResult } from '../../dashboard/input/internal-turn.js';
|
|
30
|
+
|
|
31
|
+
export const SCHEDULE_WAKEUP_TOOL_NAME = 'ScheduleWakeup';
|
|
32
|
+
export const WAKEUP_MIN_DELAY_MS = 60_000;
|
|
33
|
+
export const WAKEUP_MAX_ENTRIES = 5;
|
|
34
|
+
export const WAKEUP_REPEAT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
35
|
+
export const WAKEUP_RETRY_DELAY_MS = 60_000;
|
|
36
|
+
/** 일회성 지연의 상한 — 반복 TTL 과 같은 7일(그보다 먼 일은 cron `schedule_manage` 몫). */
|
|
37
|
+
export const WAKEUP_MAX_DELAY_MS = WAKEUP_REPEAT_TTL_MS;
|
|
38
|
+
/** 한 턴 안 schedule·cancel 호출 합계 상한 — 넘으면 «다음 턴에» 로 거부(예약·취소 고리 차단). */
|
|
39
|
+
export const WAKEUP_MAX_CALLS_PER_TURN = 6;
|
|
40
|
+
/** 반복 간격이 틀렸을 때의 거부 문구 — 일회성으로 가는 길을 같이 준다(«하지 마라»만 주면 재시도 고리가 돈다). */
|
|
41
|
+
export const WAKEUP_REPEAT_INVALID_ERROR =
|
|
42
|
+
'repeatMinutes 는 반복 간격(분 · 1 이상)이다 — 한 번만 깨우려면 repeatMinutes 를 빼거나 0 으로 두고 delaySeconds 또는 at 만 줘라';
|
|
43
|
+
|
|
44
|
+
export type WakeupTimerHandle = unknown;
|
|
45
|
+
|
|
46
|
+
export interface WakeupEntry {
|
|
47
|
+
id: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
/** 다음 발화 시각(ms). */
|
|
50
|
+
dueAt: number;
|
|
51
|
+
/** 반복 간격(ms) — 없으면 일회성. */
|
|
52
|
+
repeatMs?: number;
|
|
53
|
+
/** 반복 만료 시각(예약 시각 + 7일). 일회성은 없음. */
|
|
54
|
+
expiresAt?: number;
|
|
55
|
+
/** 이번 회차에 이미 1회 재시도했나. */
|
|
56
|
+
retried: boolean;
|
|
57
|
+
/** 이 예약이 마지막으로 연 내부 턴 id(직전 회차 진행 판정용). */
|
|
58
|
+
lastTurnId?: string;
|
|
59
|
+
fireCount: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WakeupSchedulerDeps {
|
|
63
|
+
/** H2 문 — 대시보드의 `internalTurnGate.startTurnIfIdle`. */
|
|
64
|
+
startTurnIfIdle: (input: string, opts: StartTurnIfIdleOptions) => StartTurnIfIdleResult;
|
|
65
|
+
/** 그 내부 턴이 아직 도는가(직전 회차 건너뜀). 없으면 늘 false. */
|
|
66
|
+
isTurnActive?: (turnId: string) => boolean;
|
|
67
|
+
now?: () => number;
|
|
68
|
+
setTimer?: (fn: () => void, ms: number) => WakeupTimerHandle;
|
|
69
|
+
clearTimer?: (handle: WakeupTimerHandle) => void;
|
|
70
|
+
/** 화면 한 줄(입력칸 무접촉 — 채팅 줄에만). */
|
|
71
|
+
notify?: (line: string) => void;
|
|
72
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
73
|
+
maxEntries?: number;
|
|
74
|
+
/** 지금 도는 턴 id(사람·내부 · H2 `lastTurnId`). 주면 그 턴 안 schedule·cancel 합계에 상한을 건다. */
|
|
75
|
+
currentTurnId?: () => string | null;
|
|
76
|
+
/** 한 턴 호출 상한(기본 `WAKEUP_MAX_CALLS_PER_TURN`). */
|
|
77
|
+
maxCallsPerTurn?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type ScheduleWakeupRequest = {
|
|
81
|
+
/** «HH:MM»(로컬 시각 · 지났으면 내일). delaySeconds 와 둘 중 하나. */
|
|
82
|
+
at?: string;
|
|
83
|
+
delaySeconds?: number;
|
|
84
|
+
reason: string;
|
|
85
|
+
/** 반복 간격(분). 첫 발화는 at/delaySeconds — 둘 다 없으면 지금부터 repeatMinutes 뒤. 0 = 일회성(생략과 같다). */
|
|
86
|
+
repeatMinutes?: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type ScheduleWakeupResult =
|
|
90
|
+
| { ok: true; entry: WakeupEntry }
|
|
91
|
+
| { ok: false; error: string };
|
|
92
|
+
|
|
93
|
+
export type CancelWakeupResult = { ok: true } | { ok: false; error: string; capped?: boolean };
|
|
94
|
+
|
|
95
|
+
export interface WakeupScheduler {
|
|
96
|
+
schedule(req: ScheduleWakeupRequest): ScheduleWakeupResult;
|
|
97
|
+
list(): WakeupEntry[];
|
|
98
|
+
cancel(id: string): boolean;
|
|
99
|
+
/** cancel 과 같되 거부 사유(없는 id · 턴 상한)를 값으로 낸다 — LLM 도구가 쓴다. */
|
|
100
|
+
tryCancel(id: string): CancelWakeupResult;
|
|
101
|
+
/** 모든 타이머를 거둔다(TUI 종료). */
|
|
102
|
+
dispose(): void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function defaultSetTimer(fn: () => void, ms: number): WakeupTimerHandle {
|
|
106
|
+
const t = setTimeout(fn, ms);
|
|
107
|
+
(t as { unref?: () => void }).unref?.();
|
|
108
|
+
return t;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function defaultClearTimer(handle: WakeupTimerHandle): void {
|
|
112
|
+
clearTimeout(handle as ReturnType<typeof setTimeout>);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const HHMM = /^([01]?\d|2[0-3]):([0-5]\d)$/;
|
|
116
|
+
|
|
117
|
+
/** «HH:MM» → 다음 그 시각(로컬). 지금 이전·같은 시각이면 내일. 꼴이 틀리면 null. */
|
|
118
|
+
export function resolveWakeupAt(at: string, now: number): number | null {
|
|
119
|
+
const m = HHMM.exec(at.trim());
|
|
120
|
+
if (!m) return null;
|
|
121
|
+
const d = new Date(now);
|
|
122
|
+
d.setHours(Number(m[1]), Number(m[2]), 0, 0);
|
|
123
|
+
let t = d.getTime();
|
|
124
|
+
if (t <= now) {
|
|
125
|
+
d.setDate(d.getDate() + 1);
|
|
126
|
+
t = d.getTime();
|
|
127
|
+
}
|
|
128
|
+
return t;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function formatWakeupClock(ms: number): string {
|
|
132
|
+
const d = new Date(ms);
|
|
133
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 내부 턴에 실을 글 — 사람 발화처럼 보이지 않게 머리표를 단다. */
|
|
137
|
+
export function formatWakeupPrompt(entry: Pick<WakeupEntry, 'id' | 'reason' | 'repeatMs'>): string {
|
|
138
|
+
const repeat = entry.repeatMs ? ` · 반복 ${Math.round(entry.repeatMs / 60_000)}분` : '';
|
|
139
|
+
return `[예약 깨움 ${entry.id}${repeat}] ${entry.reason} — 스스로 예약한 점검 시각이다. 남은 일을 이어서 처리하라.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createWakeupScheduler(deps: WakeupSchedulerDeps): WakeupScheduler {
|
|
143
|
+
const now = deps.now ?? Date.now;
|
|
144
|
+
const setTimer = deps.setTimer ?? defaultSetTimer;
|
|
145
|
+
const clearTimer = deps.clearTimer ?? defaultClearTimer;
|
|
146
|
+
const log = deps.log ?? ((c, e, d) => debug.log(c, e, d));
|
|
147
|
+
const maxEntries = deps.maxEntries ?? WAKEUP_MAX_ENTRIES;
|
|
148
|
+
const entries = new Map<string, WakeupEntry>();
|
|
149
|
+
const timers = new Map<string, WakeupTimerHandle>();
|
|
150
|
+
let seq = 0;
|
|
151
|
+
const maxCallsPerTurn = deps.maxCallsPerTurn ?? WAKEUP_MAX_CALLS_PER_TURN;
|
|
152
|
+
let callTurnKey: string | null = null;
|
|
153
|
+
let callCount = 0;
|
|
154
|
+
|
|
155
|
+
/** 한 턴 호출 상한 — 넘었으면 거부 문구, 아니면 null. 턴 경계를 모르면(currentTurnId 없음) 걸지 않는다. */
|
|
156
|
+
const loopCapError = (action: 'schedule' | 'cancel'): string | null => {
|
|
157
|
+
if (!deps.currentTurnId) return null;
|
|
158
|
+
const turnId = deps.currentTurnId() ?? 'none';
|
|
159
|
+
if (turnId !== callTurnKey) { callTurnKey = turnId; callCount = 0; }
|
|
160
|
+
callCount += 1;
|
|
161
|
+
if (callCount <= maxCallsPerTurn) return null;
|
|
162
|
+
log('tui.wakeup', 'loop-capped', { turnId, count: callCount, max: maxCallsPerTurn, action });
|
|
163
|
+
return `이번 턴에 예약을 너무 많이 바꿨다(${maxCallsPerTurn}회 상한) — 지금 예약은 list 로 확인만 하고, 바꾸려면 다음 턴에`;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const tryCancel = (id: string): CancelWakeupResult => {
|
|
167
|
+
const capped = loopCapError('cancel');
|
|
168
|
+
if (capped) return { ok: false, error: capped, capped: true };
|
|
169
|
+
if (!entries.has(id)) return { ok: false, error: `no pending wakeup ${JSON.stringify(id)}` };
|
|
170
|
+
drop(id);
|
|
171
|
+
log('tui.wakeup', 'cancelled', { id });
|
|
172
|
+
return { ok: true };
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const arm = (entry: WakeupEntry): void => {
|
|
176
|
+
const prev = timers.get(entry.id);
|
|
177
|
+
if (prev !== undefined) clearTimer(prev);
|
|
178
|
+
timers.set(entry.id, setTimer(() => fire(entry.id), Math.max(0, entry.dueAt - now())));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const drop = (id: string): void => {
|
|
182
|
+
const handle = timers.get(id);
|
|
183
|
+
if (handle !== undefined) clearTimer(handle);
|
|
184
|
+
timers.delete(id);
|
|
185
|
+
entries.delete(id);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** 다음 회차로 — 놓친 틱은 하나로 합친다. 만료를 넘으면 소멸. */
|
|
189
|
+
const advance = (entry: WakeupEntry, t: number): void => {
|
|
190
|
+
if (!entry.repeatMs) { drop(entry.id); return; }
|
|
191
|
+
let next = entry.dueAt + entry.repeatMs;
|
|
192
|
+
let coalesced = 0;
|
|
193
|
+
while (next <= t) { next += entry.repeatMs; coalesced++; }
|
|
194
|
+
if (entry.expiresAt !== undefined && next > entry.expiresAt) {
|
|
195
|
+
log('tui.wakeup', 'skipped', { id: entry.id, reason: 'expired', fireCount: entry.fireCount });
|
|
196
|
+
drop(entry.id);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
entry.dueAt = next;
|
|
200
|
+
entry.retried = false;
|
|
201
|
+
if (coalesced > 0) log('tui.wakeup', 'skipped', { id: entry.id, reason: 'coalesced', ticks: coalesced });
|
|
202
|
+
arm(entry);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const fire = (id: string): void => {
|
|
206
|
+
const entry = entries.get(id);
|
|
207
|
+
timers.delete(id);
|
|
208
|
+
if (!entry) return;
|
|
209
|
+
const t = now();
|
|
210
|
+
if (entry.expiresAt !== undefined && t > entry.expiresAt) {
|
|
211
|
+
log('tui.wakeup', 'skipped', { id, reason: 'expired', fireCount: entry.fireCount });
|
|
212
|
+
drop(id);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (entry.lastTurnId && deps.isTurnActive?.(entry.lastTurnId)) {
|
|
216
|
+
log('tui.wakeup', 'skipped', { id, reason: 'previous-running', turnId: entry.lastTurnId });
|
|
217
|
+
advance(entry, t);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
let result: StartTurnIfIdleResult;
|
|
221
|
+
try {
|
|
222
|
+
result = deps.startTurnIfIdle(formatWakeupPrompt(entry), { origin: 'scheduled', reason: entry.reason });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
result = { opened: false, reason: 'NotIdle', detail: `threw:${String(error)}` };
|
|
225
|
+
}
|
|
226
|
+
if (result.opened) {
|
|
227
|
+
entry.lastTurnId = result.turnId;
|
|
228
|
+
entry.fireCount += 1;
|
|
229
|
+
log('tui.wakeup', 'fired', {
|
|
230
|
+
id, turnId: result.turnId, reason: entry.reason, retried: entry.retried, repeat: !!entry.repeatMs,
|
|
231
|
+
});
|
|
232
|
+
advance(entry, t);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
log('tui.wakeup', 'rejected', {
|
|
236
|
+
id, reason: result.reason, ...(result.detail ? { detail: result.detail } : {}), retried: entry.retried,
|
|
237
|
+
});
|
|
238
|
+
if (!entry.retried) {
|
|
239
|
+
// 바쁨·사람 입력 중 — 1분 뒤 한 번만 다시 청한다(같은 회차).
|
|
240
|
+
entry.retried = true;
|
|
241
|
+
timers.set(id, setTimer(() => fire(id), WAKEUP_RETRY_DELAY_MS));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (entry.repeatMs) {
|
|
245
|
+
log('tui.wakeup', 'skipped', { id, reason: 'retry-exhausted', next: 'repeat' });
|
|
246
|
+
advance(entry, t);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
log('tui.wakeup', 'skipped', { id, reason: 'retry-exhausted', next: 'dropped' });
|
|
250
|
+
deps.notify?.(`⏰ 예약 깨움 놓침(${id}): ${entry.reason} — ${result.reason}`);
|
|
251
|
+
drop(id);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
schedule(req) {
|
|
256
|
+
const t = now();
|
|
257
|
+
const reason = (req.reason ?? '').replace(/\s+/g, ' ').trim();
|
|
258
|
+
const reject = (error: string): ScheduleWakeupResult => {
|
|
259
|
+
log('tui.wakeup', 'rejected', { id: null, reason: 'invalid', error });
|
|
260
|
+
return { ok: false, error };
|
|
261
|
+
};
|
|
262
|
+
const capped = loopCapError('schedule');
|
|
263
|
+
if (capped) return { ok: false, error: capped };
|
|
264
|
+
if (!reason) return reject('reason 이 비었다 — 깨어나서 무엇을 할지 한 줄로 적어라');
|
|
265
|
+
if (entries.size >= maxEntries) return reject(`예약 상한 ${maxEntries}개 — list 로 보고 cancel 뒤 다시`);
|
|
266
|
+
const hasAt = typeof req.at === 'string' && req.at.trim() !== '';
|
|
267
|
+
const hasDelay = typeof req.delaySeconds === 'number';
|
|
268
|
+
if (hasAt && hasDelay) return reject('at 과 delaySeconds 는 둘 중 하나만');
|
|
269
|
+
let repeatMs: number | undefined;
|
|
270
|
+
// ⭐ 0 = 일회성(생략과 같다) — 모델이 «한 번만»을 0 으로 부른다(라이브 실측). 음수·NaN·1 미만 소수는 거부.
|
|
271
|
+
if (req.repeatMinutes !== undefined && req.repeatMinutes !== 0) {
|
|
272
|
+
if (typeof req.repeatMinutes !== 'number' || !Number.isFinite(req.repeatMinutes) || req.repeatMinutes * 60_000 < WAKEUP_MIN_DELAY_MS) {
|
|
273
|
+
return reject(WAKEUP_REPEAT_INVALID_ERROR);
|
|
274
|
+
}
|
|
275
|
+
repeatMs = Math.round(req.repeatMinutes * 60_000);
|
|
276
|
+
}
|
|
277
|
+
let dueAt: number;
|
|
278
|
+
if (hasAt) {
|
|
279
|
+
const resolved = resolveWakeupAt(req.at!, t);
|
|
280
|
+
if (resolved === null) return reject(`at 은 «HH:MM»(24시간) 꼴 — 받은 값 ${JSON.stringify(req.at)}`);
|
|
281
|
+
dueAt = resolved;
|
|
282
|
+
} else if (hasDelay) {
|
|
283
|
+
if (!Number.isFinite(req.delaySeconds)) return reject('delaySeconds 가 수가 아니다');
|
|
284
|
+
// 경계는 «받은 값»으로 잰다 — 반올림 뒤에 재면 59.9999초가 60초로 통과한다.
|
|
285
|
+
const rawMs = req.delaySeconds! * 1000;
|
|
286
|
+
if (rawMs < WAKEUP_MIN_DELAY_MS) return reject(`최소 ${WAKEUP_MIN_DELAY_MS / 1000}초 뒤부터 예약할 수 있다`);
|
|
287
|
+
if (rawMs > WAKEUP_MAX_DELAY_MS) return reject('7일보다 먼 예약은 schedule_manage(cron) 몫');
|
|
288
|
+
dueAt = t + Math.round(rawMs);
|
|
289
|
+
} else if (repeatMs !== undefined) {
|
|
290
|
+
dueAt = t + repeatMs;
|
|
291
|
+
} else {
|
|
292
|
+
return reject('at(«HH:MM») · delaySeconds · repeatMinutes 중 하나는 있어야 한다');
|
|
293
|
+
}
|
|
294
|
+
if (dueAt - t < WAKEUP_MIN_DELAY_MS) return reject(`최소 ${WAKEUP_MIN_DELAY_MS / 1000}초 뒤부터 예약할 수 있다`);
|
|
295
|
+
if (dueAt - t > WAKEUP_MAX_DELAY_MS) return reject('7일보다 먼 예약은 schedule_manage(cron) 몫');
|
|
296
|
+
const entry: WakeupEntry = {
|
|
297
|
+
id: `w${++seq}`,
|
|
298
|
+
reason,
|
|
299
|
+
dueAt,
|
|
300
|
+
retried: false,
|
|
301
|
+
fireCount: 0,
|
|
302
|
+
...(repeatMs !== undefined ? { repeatMs, expiresAt: t + WAKEUP_REPEAT_TTL_MS } : {}),
|
|
303
|
+
};
|
|
304
|
+
entries.set(entry.id, entry);
|
|
305
|
+
arm(entry);
|
|
306
|
+
log('tui.wakeup', 'scheduled', {
|
|
307
|
+
id: entry.id, dueAt, inMs: dueAt - t, reason, ...(repeatMs ? { repeatMs, expiresAt: entry.expiresAt } : {}),
|
|
308
|
+
});
|
|
309
|
+
const repeat = repeatMs ? ` · ${Math.round(repeatMs / 60_000)}분마다(7일)` : ' (한 번)';
|
|
310
|
+
deps.notify?.(`⏰ 예약 깨움 ${formatWakeupClock(dueAt)}${repeat}: ${reason}`);
|
|
311
|
+
return { ok: true, entry: { ...entry } };
|
|
312
|
+
},
|
|
313
|
+
list() {
|
|
314
|
+
return [...entries.values()].sort((a, b) => a.dueAt - b.dueAt).map(e => ({ ...e }));
|
|
315
|
+
},
|
|
316
|
+
cancel(id) {
|
|
317
|
+
return tryCancel(id).ok;
|
|
318
|
+
},
|
|
319
|
+
tryCancel,
|
|
320
|
+
dispose() {
|
|
321
|
+
for (const handle of timers.values()) clearTimer(handle);
|
|
322
|
+
timers.clear();
|
|
323
|
+
entries.clear();
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── LLM 도구 ───────────────────────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
const describeEntry = (e: WakeupEntry): Record<string, unknown> => ({
|
|
331
|
+
id: e.id,
|
|
332
|
+
at: formatWakeupClock(e.dueAt),
|
|
333
|
+
dueAt: new Date(e.dueAt).toISOString(),
|
|
334
|
+
reason: e.reason,
|
|
335
|
+
...(e.repeatMs ? { repeatMinutes: Math.round(e.repeatMs / 60_000), expiresAt: new Date(e.expiresAt!).toISOString() } : {}),
|
|
336
|
+
fireCount: e.fireCount,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
/** 자리 TUI 의 host tool — 대시보드가 자기 스케줄러를 넘겨 등록한다(하니스 자식·비 TUI 표면엔 없다). */
|
|
340
|
+
export function buildScheduleWakeupHostTool(scheduler: WakeupScheduler): LLMToolDef {
|
|
341
|
+
return {
|
|
342
|
+
name: SCHEDULE_WAKEUP_TOOL_NAME,
|
|
343
|
+
description:
|
|
344
|
+
'Wake THIS TUI later with an internal turn: when you would write «다음 점검 HH:MM» and end the turn, call this instead so the check actually happens. '
|
|
345
|
+
+ 'action=schedule (default) needs reason plus one of at ("HH:MM" local, past → tomorrow) / delaySeconds (≥60) / repeatMinutes (≥1; repeats expire after 7 days). '
|
|
346
|
+
+ 'ONE-SHOT (wake once): give only delaySeconds or at and OMIT repeatMinutes (0 also means one-shot). Set repeatMinutes only when you really want it to repeat. '
|
|
347
|
+
+ `action=list shows pending wakeups; action=cancel needs id. Limits: ≥60s ahead, ≤7 days, at most 5 pending, at most ${WAKEUP_MAX_CALLS_PER_TURN} schedule/cancel calls per turn. `
|
|
348
|
+
+ 'If the TUI is busy or the human is typing at that time it retries once a minute later, then gives up (repeats move to the next tick). '
|
|
349
|
+
+ 'In-process timer: lost when the TUI restarts. Not the same as schedule_manage — that is an OS cron job that runs a command outside this TUI and never opens a turn here.',
|
|
350
|
+
parameters: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: {
|
|
353
|
+
action: { type: 'string', enum: ['schedule', 'list', 'cancel'], description: 'Default schedule.' },
|
|
354
|
+
at: { type: 'string', description: 'Local wall-clock time "HH:MM" (24h). If already past today, schedules for tomorrow.' },
|
|
355
|
+
delaySeconds: { type: 'number', description: 'Seconds from now (≥60). Use instead of at.' },
|
|
356
|
+
repeatMinutes: { type: 'number', description: 'Repeat interval in minutes (≥1). Expires 7 days after scheduling. Omit (or 0) for a one-shot wakeup.' },
|
|
357
|
+
reason: { type: 'string', description: 'What to do when woken — shown on screen and given to the wakeup turn.' },
|
|
358
|
+
id: { type: 'string', description: 'Wakeup id for action=cancel (from schedule/list).' },
|
|
359
|
+
},
|
|
360
|
+
required: [],
|
|
361
|
+
},
|
|
362
|
+
handler: async (args) => {
|
|
363
|
+
const action = typeof args.action === 'string' ? args.action : 'schedule';
|
|
364
|
+
if (action === 'list') return { wakeups: scheduler.list().map(describeEntry) };
|
|
365
|
+
if (action === 'cancel') {
|
|
366
|
+
const id = typeof args.id === 'string' ? args.id : '';
|
|
367
|
+
const r = scheduler.tryCancel(id);
|
|
368
|
+
return r.ok ? { cancelled: id } : { error: r.error };
|
|
369
|
+
}
|
|
370
|
+
if (action !== 'schedule') return { error: `unknown action ${JSON.stringify(action)}` };
|
|
371
|
+
const result = scheduler.schedule({
|
|
372
|
+
reason: typeof args.reason === 'string' ? args.reason : '',
|
|
373
|
+
...(typeof args.at === 'string' ? { at: args.at } : {}),
|
|
374
|
+
...(typeof args.delaySeconds === 'number' ? { delaySeconds: args.delaySeconds } : {}),
|
|
375
|
+
...(typeof args.repeatMinutes === 'number' ? { repeatMinutes: args.repeatMinutes } : {}),
|
|
376
|
+
});
|
|
377
|
+
return result.ok ? { scheduled: describeEntry(result.entry) } : { error: result.error };
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── 대시보드 배선(실물 경로 = 시험이 같은 함수를 부른다) ─────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
/** 대시보드의 H2 문에 스케줄러를 잇는다 — 직전 회차 판정 = «그 예약이 연 내부 턴이 아직 도나». */
|
|
385
|
+
export function createDashboardWakeupScheduler(
|
|
386
|
+
gate: Pick<InternalTurnGate, 'startTurnIfIdle' | 'isInternalTurnActive' | 'lastTurnId'>,
|
|
387
|
+
deps: Omit<WakeupSchedulerDeps, 'startTurnIfIdle' | 'isTurnActive'> = {},
|
|
388
|
+
): WakeupScheduler {
|
|
389
|
+
return createWakeupScheduler({
|
|
390
|
+
...deps,
|
|
391
|
+
startTurnIfIdle: (text, opts) => gate.startTurnIfIdle(text, opts),
|
|
392
|
+
isTurnActive: (turnId) => gate.isInternalTurnActive() && gate.lastTurnId() === turnId,
|
|
393
|
+
// 한 턴 호출 상한의 턴 경계 = H2 가 아는 마지막 턴 id(사람 제출마다 human-N · 내부 턴마다 auto-N).
|
|
394
|
+
currentTurnId: () => gate.lastTurnId(),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 대시보드 host tool 로 등록한다 — 하니스 자식(`ELANOUS_HARNESS_ROLE` 있음)은 무인 런이라 싣지 않는다.
|
|
399
|
+
* host tool 은 대시보드 pluginHost 에만 있으므로 비 TUI 표면엔 애초에 없다. 등록했으면 true. */
|
|
400
|
+
export function registerScheduleWakeupHostTool(
|
|
401
|
+
host: { registerHostTool(tool: LLMToolDef): void },
|
|
402
|
+
scheduler: WakeupScheduler,
|
|
403
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
404
|
+
): boolean {
|
|
405
|
+
if (env.ELANOUS_HARNESS_ROLE?.trim()) return false;
|
|
406
|
+
host.registerHostTool(buildScheduleWakeupHostTool(scheduler));
|
|
407
|
+
return true;
|
|
408
|
+
}
|