elanous 0.2.12 → 0.2.13
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/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/release-loop/gate-node.ts +53 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/test-deterministic.ts +53 -3
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +12 -7
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +29 -0
- package/src/cli/pod-cli.ts +10 -3
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +108 -28
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +15 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow.ts +64 -11
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +81 -17
- package/src/harness/harness-cli-command.ts +27 -1
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +4 -2
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +199 -7
- package/src/intake-plane/discord-seat-work.ts +13 -6
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +3 -3
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +47 -0
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +2 -1
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +90 -16
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +10 -4
- package/src/telegram.ts +5 -0
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +94 -10
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
|
|
3
|
+
import { dlopen, FFIType } from 'bun:ffi';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { listMemories, memoryRoot, saveMemory, type MemoryEntry } from '../memory.js';
|
|
6
|
+
import { surfaceEventsDbPath } from '../domains/surface-events.js';
|
|
7
|
+
import { DecisionLedger } from '../decisions/decision-ledger.js';
|
|
8
|
+
import { listCoordEvents } from './coord-events.js';
|
|
9
|
+
import { condenseContextWindowWithReport, type CondenseSkipped, type MemoryDeps, type MemoryItem, type MemorySource, type MemorySummary } from './long-term-memory.js';
|
|
10
|
+
|
|
11
|
+
const summarize = async (source: MemorySource): Promise<MemorySummary> =>
|
|
12
|
+
(await import('./nightly-condense.js')).summarize(source);
|
|
13
|
+
|
|
14
|
+
/** Reuse the context-bus reader against a strictly read-only SQLite handle. */
|
|
15
|
+
function readContextEvents(since: string) {
|
|
16
|
+
const path = surfaceEventsDbPath();
|
|
17
|
+
if (!existsSync(path)) return [];
|
|
18
|
+
const db = new Database(path, { readonly: true, strict: true });
|
|
19
|
+
try { return listCoordEvents({ since }, { db }); }
|
|
20
|
+
finally { db.close(); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CondenseReport {
|
|
24
|
+
since: string;
|
|
25
|
+
until: string;
|
|
26
|
+
cards: MemoryItem[];
|
|
27
|
+
conflicts: number;
|
|
28
|
+
skipped: CondenseSkipped;
|
|
29
|
+
folded: number;
|
|
30
|
+
markdown: string;
|
|
31
|
+
applied?: { written: number; retired: number; decisions: string[] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mdCell(value: string): string {
|
|
35
|
+
return value.replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderCondenseMarkdown(report: Pick<CondenseReport, 'since' | 'until' | 'cards' | 'conflicts' | 'skipped'>): string {
|
|
39
|
+
const lines = [
|
|
40
|
+
'# Long-term context memory candidates',
|
|
41
|
+
'',
|
|
42
|
+
`Window: ${report.since} → ${report.until} · Cards: ${report.cards.length} · Conflicts: ${report.conflicts}`,
|
|
43
|
+
`Skipped: ${JSON.stringify(report.skipped)}`,
|
|
44
|
+
'',
|
|
45
|
+
];
|
|
46
|
+
for (const card of report.cards) {
|
|
47
|
+
lines.push(`## ${mdCell(card.project)} · ${mdCell(card.seat)} · ${mdCell(card.topic)}`,
|
|
48
|
+
`- Updated: ${card.updatedAt}`,
|
|
49
|
+
`- Candidate: ${mdCell(card.summary)}`,
|
|
50
|
+
`- Source: ${card.source}`);
|
|
51
|
+
if (card.conflict) lines.push(`- 충돌 (${card.conflict.owner}): ${card.conflict.sources.join(' ↔ ')}`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
}
|
|
54
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type CondenseStore = {
|
|
58
|
+
list(): MemoryEntry[];
|
|
59
|
+
save(input: Parameters<typeof saveMemory>[0]): MemoryEntry;
|
|
60
|
+
raise(input: Parameters<DecisionLedger['raiseOnce']>[0], ref: string): string;
|
|
61
|
+
withLock<T>(action: () => T): T;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Persistent inode in the existing memory directory: all condense writers share one cross-process lock. */
|
|
65
|
+
function withMemoryLock<T>(dir: string, action: () => T): T {
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
const libc = process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : process.platform === 'linux' ? 'libc.so.6' : null;
|
|
68
|
+
if (!libc) throw new Error('context memory locking unavailable on this platform');
|
|
69
|
+
const lib = dlopen(libc, { flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 } });
|
|
70
|
+
const flock = lib.symbols.flock as (fd: number, op: number) => number;
|
|
71
|
+
const path = join(dir, '.context-condense.lock');
|
|
72
|
+
let fd: number | undefined;
|
|
73
|
+
let acquired = false;
|
|
74
|
+
try {
|
|
75
|
+
fd = openSync(path, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
|
|
76
|
+
const stat = fstatSync(fd);
|
|
77
|
+
const current = lstatSync(path);
|
|
78
|
+
if (!stat.isFile() || stat.ino !== current.ino || stat.dev !== current.dev || (stat.mode & 0o077) !== 0) {
|
|
79
|
+
throw new Error('context memory lock must be a private regular file');
|
|
80
|
+
}
|
|
81
|
+
const deadline = Date.now() + 10_000;
|
|
82
|
+
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
83
|
+
while (flock(fd, 2 | 4) !== 0) {
|
|
84
|
+
if (Date.now() >= deadline) throw new Error('context memory lock timed out');
|
|
85
|
+
Atomics.wait(pause, 0, 0, 10);
|
|
86
|
+
}
|
|
87
|
+
acquired = true;
|
|
88
|
+
const locked = lstatSync(path);
|
|
89
|
+
if (stat.ino !== locked.ino || stat.dev !== locked.dev) throw new Error('context memory lock changed');
|
|
90
|
+
return action();
|
|
91
|
+
} finally {
|
|
92
|
+
if (fd !== undefined) {
|
|
93
|
+
try { if (acquired) flock(fd, 8); } finally { closeSync(fd); }
|
|
94
|
+
}
|
|
95
|
+
lib.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function defaultStore(root?: string): CondenseStore {
|
|
100
|
+
const memoryDir = root ? join(root, 'elanous', 'memory') : memoryRoot();
|
|
101
|
+
const ledger = new DecisionLedger(root ? { stateDir: root } : {});
|
|
102
|
+
return {
|
|
103
|
+
list: () => listMemories({}, memoryDir),
|
|
104
|
+
save: input => saveMemory(input, memoryDir),
|
|
105
|
+
raise: (input, ref) => ledger.raiseOnce(input, ref).id,
|
|
106
|
+
withLock: action => withMemoryLock(memoryDir, action),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cardKey(card: Pick<MemoryItem, 'project' | 'seat' | 'topic'>): string {
|
|
111
|
+
return JSON.stringify([card.project, card.seat, card.topic]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function newestActive(entries: readonly MemoryEntry[]): MemoryEntry | undefined {
|
|
115
|
+
return entries.reduce<MemoryEntry | undefined>((best, entry) => {
|
|
116
|
+
const current = entry.contextCard!;
|
|
117
|
+
const previous = best?.contextCard;
|
|
118
|
+
return !previous || current.updatedAt > previous.updatedAt
|
|
119
|
+
|| (current.updatedAt === previous.updatedAt && current.source > previous.source)
|
|
120
|
+
|| (current.updatedAt === previous.updatedAt && current.source === previous.source && entry.id > best!.id)
|
|
121
|
+
? entry : best;
|
|
122
|
+
}, undefined);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Apply candidate cards through the existing curated memory and decision ledger paths. */
|
|
126
|
+
export function applyCondensedCards(cards: readonly MemoryItem[], store: CondenseStore): NonNullable<CondenseReport['applied']> {
|
|
127
|
+
const decisions: string[] = [];
|
|
128
|
+
let written = 0;
|
|
129
|
+
let retired = 0;
|
|
130
|
+
for (const card of cards) {
|
|
131
|
+
if (card.conflict) {
|
|
132
|
+
const ref = `context-condense:${JSON.stringify([card.project, card.seat, card.topic, card.source, card.updatedAt])}`;
|
|
133
|
+
decisions.push(store.raise({
|
|
134
|
+
title: `Context conflict: ${card.project} / ${card.seat} / ${card.topic}`.slice(0, 120),
|
|
135
|
+
category: 'scope',
|
|
136
|
+
scqa: { s: `Candidate (${card.updatedAt}): ${card.summary}`.slice(0, 220), c: `Conflicting sources: ${card.conflict.sources.join(' ↔ ')}`.slice(0, 220) },
|
|
137
|
+
options: [
|
|
138
|
+
{ key: 'a', label: 'Keep the existing claim', consequence: 'Candidate is not stored' },
|
|
139
|
+
{ key: 'b', label: 'Reconcile and apply the new claim', consequence: 'Resolve source disagreement before memory update' },
|
|
140
|
+
],
|
|
141
|
+
recommendation: { skipped: true, reason: 'Conflicting claims require OP judgment' },
|
|
142
|
+
raisedBy: { agent: 'context-condense', track: 'OP' },
|
|
143
|
+
refs: [ref, ...card.conflict.sources],
|
|
144
|
+
}, ref));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (card.status !== 'active') continue;
|
|
148
|
+
const applyTopic = () => {
|
|
149
|
+
const prior = store.list().filter(entry => entry.contextCard?.status === 'active' && cardKey(entry.contextCard) === cardKey(card));
|
|
150
|
+
const winner = newestActive(prior);
|
|
151
|
+
const alreadyPresent = prior.some(entry => entry.contextCard!.updatedAt === card.updatedAt && entry.contextCard!.source === card.source);
|
|
152
|
+
if (!alreadyPresent && (!winner || winner.contextCard!.updatedAt < card.updatedAt
|
|
153
|
+
|| (winner.contextCard!.updatedAt === card.updatedAt && winner.contextCard!.source < card.source))) {
|
|
154
|
+
store.save({ type: 'project', name: `${card.project} · ${card.seat} · ${card.topic}`,
|
|
155
|
+
description: card.summary, body: `${card.summary}\n\nSource: ${card.source}\nUpdated: ${card.updatedAt}\n`,
|
|
156
|
+
contextCard: { project: card.project, seat: card.seat, topic: card.topic, source: card.source, updatedAt: card.updatedAt, status: 'active' } });
|
|
157
|
+
written++;
|
|
158
|
+
}
|
|
159
|
+
const active = store.list().filter(entry => entry.contextCard?.status === 'active' && cardKey(entry.contextCard) === cardKey(card));
|
|
160
|
+
const keep = newestActive(active);
|
|
161
|
+
for (const old of active) {
|
|
162
|
+
if (old.id === keep?.id) continue;
|
|
163
|
+
store.save({ type: old.type, name: old.name, description: old.description, body: old.body, id: old.id,
|
|
164
|
+
pinned: false, priority: old.priority, contextCard: { ...old.contextCard!, status: 'retired' } });
|
|
165
|
+
retired++;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
store.withLock(applyTopic);
|
|
169
|
+
}
|
|
170
|
+
return { written, retired, decisions };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Produce a rolling-window candidate snapshot; no store is opened for writing in dry-run. */
|
|
174
|
+
export async function runContextCondense(options: {
|
|
175
|
+
hours: number; apply?: boolean; now?: Date; root?: string;
|
|
176
|
+
}, deps: MemoryDeps = { summarize }, store?: CondenseStore): Promise<CondenseReport> {
|
|
177
|
+
if (!Number.isSafeInteger(options.hours) || options.hours <= 0 || options.hours > 24 * 30) {
|
|
178
|
+
throw new Error('--since must be a positive number of hours (at most 720h)');
|
|
179
|
+
}
|
|
180
|
+
const now = options.now ?? new Date();
|
|
181
|
+
const until = now.toISOString();
|
|
182
|
+
const since = new Date(now.getTime() - options.hours * 3_600_000).toISOString();
|
|
183
|
+
const { cards, skipped, folded } = await condenseContextWindowWithReport(since, until, [], {
|
|
184
|
+
summarize: deps.summarize,
|
|
185
|
+
events: deps.events ?? readContextEvents,
|
|
186
|
+
decisions: deps.decisions ?? (() => new DecisionLedger(options.root ? { stateDir: options.root } : {}).list({ status: 'all' })
|
|
187
|
+
.filter(decision => decision.raisedBy.agent !== 'context-condense')),
|
|
188
|
+
}, now);
|
|
189
|
+
const conflicts = cards.filter(card => card.conflict !== undefined).length;
|
|
190
|
+
const report: CondenseReport = { since, until, cards, conflicts, skipped, folded,
|
|
191
|
+
markdown: renderCondenseMarkdown({ since, until, cards, conflicts, skipped }) };
|
|
192
|
+
if (options.apply === true) report.applied = applyCondensedCards(cards, store ?? defaultStore(options.root));
|
|
193
|
+
return report;
|
|
194
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { surfaceEventsDbPath } from '../domains/surface-events.js';
|
|
4
|
+
|
|
5
|
+
export const CONTEXT_DAY_SOURCES = ['OP', 'TC', 'MK', 'UX', 'claude-code', 'codex-agent-mission', 'harness-child'] as const;
|
|
6
|
+
export type ContextDaySource = typeof CONTEXT_DAY_SOURCES[number];
|
|
7
|
+
export type ContextDayKind = 'task-claimed' | 'done' | 'asked' | 'guidance-changed';
|
|
8
|
+
export interface ContextDayRow {
|
|
9
|
+
source: ContextDaySource;
|
|
10
|
+
counts: Record<ContextDayKind, number>;
|
|
11
|
+
lastAt: string | null;
|
|
12
|
+
status: '들어옴' | '안 들어옴';
|
|
13
|
+
}
|
|
14
|
+
export interface ContextDayReport { since: string; until: string; rows: ContextDayRow[] }
|
|
15
|
+
|
|
16
|
+
/** Reject malformed windows instead of silently changing the time range being judged. */
|
|
17
|
+
export function contextDayHours(since: string): number {
|
|
18
|
+
if (!/^[1-9]\d*h$/.test(since)) throw new Error('--since requires positive hours, e.g. 24h');
|
|
19
|
+
const hours = Number(since.slice(0, -1));
|
|
20
|
+
if (!Number.isSafeInteger(hours) || hours * 3_600_000 > 8.64e15) throw new Error('--since is out of range');
|
|
21
|
+
return hours;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function kindFor(surface: string, kind: string): ContextDayKind | null {
|
|
25
|
+
if (surface === 'context:external') {
|
|
26
|
+
if (kind === 'started' || kind === 'claimed') return 'task-claimed';
|
|
27
|
+
if (kind === 'finished' || kind === 'done') return 'done';
|
|
28
|
+
if (kind === 'guide-changed') return 'guidance-changed';
|
|
29
|
+
}
|
|
30
|
+
if (kind === 'task-claimed' || kind === 'claimed') return 'task-claimed';
|
|
31
|
+
if (kind === 'task-done' || kind === 'done') return 'done';
|
|
32
|
+
if (kind === 'asked' || (surface === 'coord:channel' && kind === '요청')) return 'asked';
|
|
33
|
+
if (kind === 'guidance-changed' || kind === 'guide-changed') return 'guidance-changed';
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read only the context journal, not recallEvents (which mutates recall counts) or the writable DB opener. */
|
|
38
|
+
export function contextDay(since = '24h', deps: { now?: () => Date; dbPath?: string } = {}): ContextDayReport {
|
|
39
|
+
const hours = contextDayHours(since);
|
|
40
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
41
|
+
const until = now.toISOString();
|
|
42
|
+
const from = new Date(now.getTime() - hours * 3_600_000).toISOString();
|
|
43
|
+
const rows: ContextDayRow[] = CONTEXT_DAY_SOURCES.map(source => ({
|
|
44
|
+
source, counts: { 'task-claimed': 0, done: 0, asked: 0, 'guidance-changed': 0 }, lastAt: null, status: '안 들어옴',
|
|
45
|
+
}));
|
|
46
|
+
const bySource = new Map(rows.map(row => [row.source, row]));
|
|
47
|
+
const path = deps.dbPath ?? surfaceEventsDbPath();
|
|
48
|
+
if (!existsSync(path)) return { since: from, until, rows };
|
|
49
|
+
const db = new Database(path, { readonly: true });
|
|
50
|
+
try {
|
|
51
|
+
const events = db.prepare(`SELECT surface, kind, ts, refs FROM events
|
|
52
|
+
WHERE surface IN ('coord:channel', 'context:session', 'context:external')
|
|
53
|
+
AND direction='outbound' AND ts>=? AND ts<=? ORDER BY ts ASC`).all(from, until) as Array<{
|
|
54
|
+
surface: string; kind: string; ts: string; refs: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
for (const event of events) {
|
|
57
|
+
let refs: { seat?: unknown; origin?: unknown };
|
|
58
|
+
try { refs = JSON.parse(event.refs ?? '{}') as typeof refs; } catch { continue; }
|
|
59
|
+
const source = event.surface === 'context:external' ? refs.origin : refs.seat;
|
|
60
|
+
if (typeof source !== 'string') continue;
|
|
61
|
+
const row = bySource.get(source as ContextDaySource);
|
|
62
|
+
if (!row) continue;
|
|
63
|
+
// Do not attribute an external hook to a seat (or a seat event to an external origin).
|
|
64
|
+
if ((event.surface === 'context:external') !== (rows.indexOf(row) >= 4)) continue;
|
|
65
|
+
row.lastAt = event.ts;
|
|
66
|
+
row.status = '들어옴';
|
|
67
|
+
const kind = kindFor(event.surface, event.kind);
|
|
68
|
+
if (kind) row.counts[kind]++;
|
|
69
|
+
}
|
|
70
|
+
} finally { db.close(); }
|
|
71
|
+
return { since: from, until, rows };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderContextDay(report: ContextDayReport): string {
|
|
75
|
+
return [
|
|
76
|
+
`맥락 이벤트 ${report.since} ~ ${report.until} (UTC)`,
|
|
77
|
+
'| 출처 | task-claimed | done | asked | guidance-changed | 마지막 시각 (UTC) | 상태 |',
|
|
78
|
+
'| --- | ---: | ---: | ---: | ---: | --- | --- |',
|
|
79
|
+
...report.rows.map(row => `| ${row.source} | ${row.counts['task-claimed']} | ${row.counts.done} | ${row.counts.asked} | ${row.counts['guidance-changed']} | ${row.lastAt ?? '—'} | ${row.status} |`),
|
|
80
|
+
'안 들어옴 = 이 시간 창에서 해당 출처 이벤트 0건 (Claude Code 훅 미설치 가능성; 설치 여부는 미확인). 카운트 열은 네 종류만 셈.',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
@@ -43,10 +43,23 @@ export function renderTelegramNow(answer: ContextNowAnswer): string {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function renderTuiNow(answer: ContextNowAnswer): string[] {
|
|
46
|
+
const shortText = (text: string) => {
|
|
47
|
+
const characters = Array.from(text);
|
|
48
|
+
return characters.length > 100 ? `${characters.slice(0, 100).join('')}…` : text;
|
|
49
|
+
};
|
|
50
|
+
const shortSource = (source: string) => {
|
|
51
|
+
if (/^https?:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+#issuecomment-/.test(source)) return '채널';
|
|
52
|
+
if (source.startsWith('elanous://release/')) return '원장';
|
|
53
|
+
return Array.from(source).slice(0, 40).join('');
|
|
54
|
+
};
|
|
55
|
+
const factLabels = { version: '판', cell: '칸', decision: '결정', seat: '자리' } as const;
|
|
56
|
+
const eventLabels: Record<string, string> = { report: '보고', dispatch: '발사', decision: '결정' };
|
|
57
|
+
const events = answer.events.slice().sort((a, b) => Date.parse(b.at) - Date.parse(a.at));
|
|
46
58
|
const rows = [
|
|
47
|
-
...answer.facts.map(f => [f.kind, factText(f), f.source]),
|
|
48
|
-
...
|
|
59
|
+
...answer.facts.map(f => [factLabels[f.kind], shortText(factText(f)), shortSource(f.source)]),
|
|
60
|
+
...events.slice(0, 8).map(e => [eventLabels[e.kind] ?? '소식', shortText(e.summary), shortSource(e.source)]),
|
|
49
61
|
...answer.guide.map(guide => ['안내', guide, '']),
|
|
62
|
+
...(events.length > 8 ? [['안내', `… 사건 ${events.length - 8}개 더(/now <주제> 로 좁히기)`, '']] : []),
|
|
50
63
|
];
|
|
51
64
|
return [
|
|
52
65
|
`지금${answer.topic ? ` · ${answer.topic}` : ''} (${answer.at})`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { redactSecretText } from '../debug/log.js';
|
|
1
|
+
import { debug, redactSecretText } from '../debug/log.js';
|
|
2
2
|
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
3
3
|
import { listCoordEvents, type CoordEvent } from './coord-events.js';
|
|
4
4
|
|
|
@@ -33,6 +33,14 @@ export interface MemoryDeps {
|
|
|
33
33
|
decisions?: () => DecisionEntry[];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export type CondenseSkipReason = 'no-seat' | 'no-project' | 'no-topic' | 'no-summary' | 'bad-link' | 'bad-claim' | 'summarize-failed';
|
|
37
|
+
export type CondenseSkipped = Partial<Record<CondenseSkipReason, number>>;
|
|
38
|
+
export interface CondenseWindowReport {
|
|
39
|
+
cards: MemoryItem[];
|
|
40
|
+
skipped: CondenseSkipped;
|
|
41
|
+
folded: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
const DAY_MS = 86_400_000;
|
|
37
45
|
const oneLine = (text: string) => redactSecretText(text.split(/\r?\n/, 1)[0] ?? '').trim();
|
|
38
46
|
|
|
@@ -43,40 +51,125 @@ export async function condenseContextDay(
|
|
|
43
51
|
if (!/^\d{4}-\d\d-\d\d$/.test(day) || new Date(`${day}T00:00:00.000Z`).toISOString().slice(0, 10) !== day) {
|
|
44
52
|
throw new Error('day must be a valid UTC YYYY-MM-DD');
|
|
45
53
|
}
|
|
46
|
-
|
|
54
|
+
return condenseContextWindow(`${day}T00:00:00.000Z`, new Date(Date.parse(`${day}T00:00:00.000Z`) + DAY_MS).toISOString(), previous, deps, now);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read-only condensation over an exact rolling UTC window; the caller owns any persistence. */
|
|
58
|
+
export async function condenseContextWindow(
|
|
59
|
+
since: string, until: string, previous: readonly MemoryItem[], deps: MemoryDeps, now: Date = new Date(),
|
|
60
|
+
): Promise<MemoryItem[]> {
|
|
61
|
+
return (await condenseContextWindowWithReport(since, until, previous, deps, now)).cards;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function condenseContextWindowWithReport(
|
|
65
|
+
since: string, until: string, previous: readonly MemoryItem[], deps: MemoryDeps, now: Date = new Date(),
|
|
66
|
+
): Promise<CondenseWindowReport> {
|
|
47
67
|
const start = Date.parse(since);
|
|
48
|
-
const
|
|
49
|
-
|
|
68
|
+
const end = Date.parse(until);
|
|
69
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) throw new Error('invalid context window');
|
|
70
|
+
// Fetch the previous local day as well: the store compares timestamp text rather than instants.
|
|
50
71
|
const events = (deps.events ?? (from => listCoordEvents({ since: from })))(new Date(start - DAY_MS).toISOString());
|
|
51
72
|
const decisions = (deps.decisions ?? (() => new DecisionLedger().list({ status: 'all' })))();
|
|
73
|
+
const inWindow = (at: string) => {
|
|
74
|
+
const time = Date.parse(at);
|
|
75
|
+
return Number.isFinite(time) && time >= start && time < end;
|
|
76
|
+
};
|
|
77
|
+
const harnessRuns: Array<{ events: CoordEvent[]; locators: Set<string> }> = [];
|
|
78
|
+
const earlierStarts: CoordEvent[] = [];
|
|
79
|
+
const otherEvents: CoordEvent[] = [];
|
|
80
|
+
let harnessEvents = 0;
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
if (!inWindow(event.at)) {
|
|
83
|
+
const time = Date.parse(event.at);
|
|
84
|
+
if (event.kind === 'started' && Number.isFinite(time) && time >= start - DAY_MS && time < start) {
|
|
85
|
+
earlierStarts.push(event);
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Harness lifecycle = started/finished, or any harness-child event that is not a report (review round 3).
|
|
90
|
+
const isLifecycle = event.kind === 'started' || event.kind === 'finished'
|
|
91
|
+
|| (event.refs.seat === 'harness-child' && event.kind !== '보고');
|
|
92
|
+
if (!isLifecycle) {
|
|
93
|
+
otherEvents.push(event);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
harnessEvents++;
|
|
97
|
+
const locators = new Set([event.refs.source, event.refs.url].filter((ref): ref is string => !!ref));
|
|
98
|
+
const group = { events: [event], locators };
|
|
99
|
+
// Either locator can identify the run, including a bridge through a third event.
|
|
100
|
+
for (let i = 0; i < harnessRuns.length;) {
|
|
101
|
+
const prior = harnessRuns[i]!;
|
|
102
|
+
if (locators.size && [...prior.locators].some(ref => locators.has(ref))) {
|
|
103
|
+
group.events.push(...prior.events);
|
|
104
|
+
for (const ref of prior.locators) locators.add(ref);
|
|
105
|
+
harnessRuns.splice(i, 1);
|
|
106
|
+
i = 0;
|
|
107
|
+
} else i++;
|
|
108
|
+
}
|
|
109
|
+
harnessRuns.push(group);
|
|
110
|
+
}
|
|
111
|
+
const eventSource = (event: CoordEvent): MemorySource => ({ kind: 'event', at: event.at, seat: event.refs.seat,
|
|
112
|
+
text: event.summary, source: event.refs.source ?? event.refs.url ?? `elanous://context/event/${encodeURIComponent(event.id)}` });
|
|
113
|
+
const foldedSources: MemorySource[] = [];
|
|
114
|
+
for (const group of harnessRuns) {
|
|
115
|
+
group.events.sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || a.id.localeCompare(b.id));
|
|
116
|
+
const finish = group.events.filter(event => event.kind === 'finished').at(-1);
|
|
117
|
+
const inWindowStart = group.events.find(event => event.kind === 'started');
|
|
118
|
+
const begin = inWindowStart ?? (finish ? earlierStarts.filter(event =>
|
|
119
|
+
(group.locators.has(event.refs.source ?? '') || group.locators.has(event.refs.url ?? '')))
|
|
120
|
+
.sort((a, b) => Date.parse(b.at) - Date.parse(a.at)).at(0) : undefined);
|
|
121
|
+
const winner = finish ?? inWindowStart ?? group.events.at(-1)!;
|
|
122
|
+
const frequency = (ref: string) => group.events.filter(event => event.refs.source === ref || event.refs.url === ref).length;
|
|
123
|
+
const run = [...group.locators].sort((a, b) => frequency(b) - frequency(a))[0] ?? eventSource(winner).source;
|
|
124
|
+
const duration = finish && begin && Date.parse(finish.at) >= Date.parse(begin.at)
|
|
125
|
+
? `${Math.floor((Date.parse(finish.at) - Date.parse(begin.at)) / 60_000)}m ${Math.floor(((Date.parse(finish.at) - Date.parse(begin.at)) % 60_000) / 1_000)}s`
|
|
126
|
+
: '알 수 없음';
|
|
127
|
+
const result = finish ? oneLine(finish.summary).replace(/^Harness child finished:\s*/i, '') || '종료'
|
|
128
|
+
: '진행 중';
|
|
129
|
+
foldedSources.push({ ...eventSource(winner), seat: winner.refs.seat || begin?.refs.seat || '',
|
|
130
|
+
source: run, text: `${run} ${result} · ${duration}` });
|
|
131
|
+
}
|
|
132
|
+
const folded = harnessEvents - foldedSources.length;
|
|
133
|
+
try { debug.log('context.condense', 'folded', { harnessEvents, kept: foldedSources.length }); }
|
|
134
|
+
catch { /* Observation must not stop condensation. */ }
|
|
52
135
|
const sources: MemorySource[] = [
|
|
53
|
-
...
|
|
54
|
-
text: event.summary, source: event.refs.source ?? event.refs.url ?? `elanous://context/event/${encodeURIComponent(event.id)}` })),
|
|
136
|
+
...otherEvents.map(eventSource), ...foldedSources,
|
|
55
137
|
...decisions.map(decision => ({ kind: 'decision' as const,
|
|
56
138
|
at: decision.decidedAt ?? decision.raisedAt ?? decision.importedAt ?? '',
|
|
57
139
|
seat: decision.raisedBy.track ?? 'OP',
|
|
58
140
|
text: decision.status === 'decided' ? `${decision.title}: ${decision.options.find(o => o.key === decision.choice)?.label ?? decision.note ?? decision.scqa.a ?? '결정됨'}` : `${decision.title}: ${decision.status}`,
|
|
59
141
|
source: `elanous://decisions/${encodeURIComponent(decision.id)}` })),
|
|
60
|
-
].filter(source =>
|
|
61
|
-
|
|
62
|
-
return Number.isFinite(at) && at >= start && at < until;
|
|
63
|
-
}).sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || a.source.localeCompare(b.source));
|
|
142
|
+
].filter(source => inWindow(source.at))
|
|
143
|
+
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || a.source.localeCompare(b.source));
|
|
64
144
|
|
|
65
145
|
const items = new Map<string, MemoryItem>();
|
|
146
|
+
const skipped: CondenseSkipped = {};
|
|
147
|
+
let kept = 0;
|
|
148
|
+
const skip = (reason: CondenseSkipReason, source: MemorySource) => {
|
|
149
|
+
skipped[reason] = (skipped[reason] ?? 0) + 1;
|
|
150
|
+
try { debug.log('context.condense', 'source-skipped', { reason, kind: source.kind, at: source.at }); }
|
|
151
|
+
catch { /* Observation must not stop condensation. */ }
|
|
152
|
+
};
|
|
153
|
+
const safeLine = (value: unknown) => typeof value === 'string' ? oneLine(value) : '';
|
|
66
154
|
const keyFor = (item: Pick<MemoryItem, 'project' | 'seat' | 'topic'>) => JSON.stringify([item.project, item.seat, item.topic]);
|
|
67
155
|
for (const item of previous) items.set(keyFor(item), { ...item });
|
|
68
156
|
for (const source of sources) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
157
|
+
const seat = safeLine(source.seat);
|
|
158
|
+
if (!seat) { skip('no-seat', source); continue; }
|
|
159
|
+
let summary: MemorySummary;
|
|
160
|
+
try { summary = await deps.summarize(source); }
|
|
161
|
+
catch { skip('summarize-failed', source); continue; }
|
|
162
|
+
const project = safeLine(summary?.project);
|
|
163
|
+
const topic = safeLine(summary?.topic);
|
|
164
|
+
const text = safeLine(summary?.summary);
|
|
165
|
+
const link = safeLine(source.source);
|
|
166
|
+
if (!project) { skip('no-project', source); continue; }
|
|
167
|
+
if (!topic) { skip('no-topic', source); continue; }
|
|
168
|
+
if (!text) { skip('no-summary', source); continue; }
|
|
169
|
+
if (!/^(?:https?:\/\/|elanous:\/\/)[^\s]+$/.test(link)) { skip('bad-link', source); continue; }
|
|
170
|
+
const claim = summary.claim && { key: safeLine(summary.claim.key), value: safeLine(summary.claim.value) };
|
|
171
|
+
if (claim && (!claim.key || !claim.value)) { skip('bad-claim', source); continue; }
|
|
172
|
+
kept++;
|
|
80
173
|
const key = keyFor({ project, seat, topic });
|
|
81
174
|
const old = items.get(key);
|
|
82
175
|
// Equal timestamps are resolved by the source link, so shuffled input does not change the winner.
|
|
@@ -103,6 +196,9 @@ export async function condenseContextDay(
|
|
|
103
196
|
finalItems[index] = { ...item, conflict: { owner: 'OP', sources: [conflicting.source, item.source] } };
|
|
104
197
|
}
|
|
105
198
|
}
|
|
106
|
-
|
|
199
|
+
const cards = finalItems.sort((a, b) => a.project.localeCompare(b.project) || a.seat.localeCompare(b.seat)
|
|
107
200
|
|| a.topic.localeCompare(b.topic));
|
|
201
|
+
try { debug.log('context.condense', 'window', { sources: sources.length, kept, skipped }); }
|
|
202
|
+
catch { /* Observation must not stop condensation. */ }
|
|
203
|
+
return { cards, skipped, folded };
|
|
108
204
|
}
|
|
@@ -64,7 +64,7 @@ function storedItems(root: string): MemoryItem[] {
|
|
|
64
64
|
return items;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
async function summarize(source: MemorySource, injected?: Record<string, MemorySummary>): Promise<MemorySummary> {
|
|
67
|
+
export async function summarize(source: MemorySource, injected?: Record<string, MemorySummary>): Promise<MemorySummary> {
|
|
68
68
|
if (injected) {
|
|
69
69
|
const result = injected[source.source];
|
|
70
70
|
if (!result) throw new Error(`missing test summary: ${source.source}`);
|
|
@@ -35,21 +35,27 @@ export function isIrreversible(entry: Pick<DecisionEntry, 'category'>): boolean
|
|
|
35
35
|
|
|
36
36
|
const kst = (at: string) => new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(at));
|
|
37
37
|
|
|
38
|
-
/** The card
|
|
38
|
+
/** The card opens with a short pitch; the full question and choice buttons remain below it. */
|
|
39
39
|
export function renderCardText(e: DecisionEntry, extra: { note?: string; confirm?: string } = {}): string {
|
|
40
|
+
const recommendedKey = 'option' in e.recommendation ? e.recommendation.option : undefined;
|
|
41
|
+
const recommended = e.options.find(o => o.key === recommendedKey);
|
|
42
|
+
const alternative = e.options.find(o => o.key === e.alternative);
|
|
40
43
|
const lines = [
|
|
41
44
|
`🗳 결정 요청 ${e.id}${isIrreversible(e) ? ' · ⚠️ 되돌릴 수 없음' : ''}`,
|
|
42
45
|
`«${e.title}» · 올린 자리: ${e.raisedBy.agent}${e.raisedBy.track ? `(${e.raisedBy.track})` : ''}`,
|
|
43
46
|
'',
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
`S: ${e.scqa.s.replace(/\s+/g, ' ')}`,
|
|
48
|
+
`C: ${e.scqa.c.replace(/\s+/g, ' ')}`,
|
|
49
|
+
`Q: ${(e.scqa.q ?? '(비움)').replace(/\s+/g, ' ')}`,
|
|
50
|
+
`A: ${(e.scqa.a ?? '(비움)').replace(/\s+/g, ' ')}`,
|
|
51
|
+
'skipped' in e.recommendation ? `권고 없음: ${e.recommendation.reason}` : `권고: ${recommended?.label ?? e.recommendation.option} — ${e.recommendation.why}`,
|
|
52
|
+
...(alternative ? [`대안: ${alternative.label} — ${alternative.consequence}`] : []),
|
|
53
|
+
e.crossCheck?.length ? `교차 확인: ${e.crossCheck.map(check => `${check.seat} ✓ ${check.note.replace(/\s+/g, ' ')}`).join(' · ')}` : `교차 확인 없음(${e.crossCheckSkipped ?? '미기재'})`,
|
|
54
|
+
...(e.dissent ? [`이견: ${e.dissent}`] : []),
|
|
55
|
+
...(e.dueAt ? [`기한: ${kst(e.dueAt)} KST`] : []),
|
|
46
56
|
...(e.pendingQuestion ? ['', `대기 질문 전문:\n${e.pendingQuestion}`] : []),
|
|
47
|
-
...(e.scqa.q ? [`질문: ${e.scqa.q}`] : []),
|
|
48
|
-
...(e.scqa.a ? [`제안: ${e.scqa.a}`] : []),
|
|
49
57
|
'',
|
|
50
58
|
...e.options.map((o) => `${o.key.toUpperCase()}) ${o.label} — ${o.consequence}`),
|
|
51
|
-
'skipped' in e.recommendation ? `권고 없음: ${e.recommendation.reason}` : `권고: ${e.recommendation.option.toUpperCase()} — ${e.recommendation.why}`,
|
|
52
|
-
...(e.dueAt ? [`기한: ${kst(e.dueAt)}`] : []),
|
|
53
59
|
...(extra.note ? ['', `📝 메모: ${extra.note}`] : []),
|
|
54
60
|
];
|
|
55
61
|
if (extra.confirm) {
|
|
@@ -22,11 +22,15 @@ export interface DecisionEntry {
|
|
|
22
22
|
pendingQuestion?: string;
|
|
23
23
|
/** DEC-TG — optional deadline (UTC). Cards remind the owner two hours before it. */
|
|
24
24
|
dueAt?: string;
|
|
25
|
+
crossCheck?: Array<{ seat: string; at: string; note: string }>;
|
|
26
|
+
crossCheckSkipped?: string;
|
|
27
|
+
alternative?: string;
|
|
28
|
+
dissent?: string;
|
|
25
29
|
decidedAt?: string; decidedBy?: DecisionActor; choice?: string; note?: string; versionAtDecision?: Versions;
|
|
26
30
|
withdrawnAt?: string; withdrawReason?: string;
|
|
27
31
|
history: Array<{ type: 'raised' | 'options-added' | 'decided' | 'withdrawn'; at?: string; by: string; version?: Versions; choice?: string; reason?: string }>;
|
|
28
32
|
}
|
|
29
|
-
export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt' | 'resume' | 'pendingQuestion'> & { raisedAt?: string };
|
|
33
|
+
export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt' | 'resume' | 'pendingQuestion' | 'crossCheck' | 'crossCheckSkipped' | 'alternative' | 'dissent'> & { raisedAt?: string };
|
|
30
34
|
type Event = { type: 'raised'; entry: DecisionEntry } | { type: 'options-added'; id: string; at: string; options: DecisionOption[]; by: string } | { type: 'decided'; id: string; at?: string; by: DecisionActor; choice?: string; version?: Versions; note?: string } | { type: 'withdrawn'; id: string; at: string; reason: string; version: Versions };
|
|
31
35
|
export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions; writeAnswer?: typeof writePendingQuestionAnswer }
|
|
32
36
|
|
|
@@ -89,13 +93,27 @@ function validate(input: RaiseInput, historical = false): RaiseInput {
|
|
|
89
93
|
? { skipped: true as const, reason: safe(single(input.recommendation.reason, 'skip reason')) }
|
|
90
94
|
: { option: input.recommendation.option, why: safe(single(input.recommendation.why, 'recommendation why')) };
|
|
91
95
|
if ('option' in recommendation && !options.some(o => o.key === recommendation.option)) throw new Error('recommended option not found');
|
|
96
|
+
const alternative = input.alternative === undefined ? undefined : single(input.alternative, 'alternative');
|
|
97
|
+
if (alternative && !options.some(o => o.key === alternative)) throw new Error('alternative option not found');
|
|
98
|
+
if (alternative && 'option' in recommendation && alternative === recommendation.option) throw new Error('alternative must differ from recommendation');
|
|
99
|
+
if (input.crossCheck?.length && input.crossCheckSkipped !== undefined) throw new Error('cross-check and skipped reason cannot coexist');
|
|
100
|
+
if (input.crossCheck !== undefined && !Array.isArray(input.crossCheck)) throw new Error('cross-check must be a list');
|
|
101
|
+
const crossCheck = input.crossCheck?.map(check => {
|
|
102
|
+
const seat = single(check.seat, 'cross-check seat');
|
|
103
|
+
if (!['OP', 'MK', 'TC', 'UX'].includes(seat)) throw new Error('invalid cross-check seat');
|
|
104
|
+
return { seat, at: utc(check.at), note: safe(single(check.note, 'cross-check note')) };
|
|
105
|
+
});
|
|
106
|
+
const crossCheckSkipped = input.crossCheckSkipped === undefined ? undefined : safe(single(input.crossCheckSkipped, 'cross-check skipped reason'));
|
|
107
|
+
const dissent = input.dissent === undefined ? undefined : safe(single(input.dissent, 'dissent'));
|
|
92
108
|
if (input.resume && !/^(?:auq:[0-9a-z]+:[0-9a-z]{5}|execution:[A-Za-z0-9_-]+:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?![\s\S])/.test(input.resume.questionId)) throw new Error('invalid resume questionId');
|
|
93
109
|
const resume = input.resume ? { questionId: input.resume.questionId,
|
|
94
110
|
...(input.resume.runId === undefined ? {} : { runId: single(input.resume.runId, 'resume runId') }) } : undefined;
|
|
95
111
|
if (input.pendingQuestion !== undefined) required(input.pendingQuestion, 'pending question');
|
|
96
112
|
return { ...input, title, scqa: { s, c, ...(q ? { q } : {}), ...(a ? { a } : {}) }, options, recommendation,
|
|
97
113
|
...(input.pendingQuestion === undefined ? {} : { pendingQuestion: safe(input.pendingQuestion) }),
|
|
98
|
-
...(resume ? { resume } : {}),
|
|
114
|
+
...(resume ? { resume } : {}), ...(crossCheck ? { crossCheck } : {}),
|
|
115
|
+
...(crossCheckSkipped !== undefined ? { crossCheckSkipped } : {}),
|
|
116
|
+
...(alternative !== undefined ? { alternative } : {}), ...(dissent !== undefined ? { dissent } : {}),
|
|
99
117
|
raisedBy: { agent: safe(single(input.raisedBy.agent, 'agent')), ...(input.raisedBy.track ? { track: input.raisedBy.track } : {}), ...(input.raisedBy.session ? { session: safe(single(input.raisedBy.session, 'session')) } : {}) },
|
|
100
118
|
...(input.refs ? { refs: input.refs.map(r => safe(single(r, 'ref'))) } : {}),
|
|
101
119
|
...(input.dueAt ? { dueAt: utc(input.dueAt) } : {}) };
|
|
@@ -206,21 +224,29 @@ export class DecisionLedger {
|
|
|
206
224
|
show(id: string): DecisionEntry { const entry = this.list({ status: 'all' }).find(e => e.id === id); if (!entry) throw new Error(`decision not found: ${id}`); return entry; }
|
|
207
225
|
raise(input: RaiseInput): DecisionEntry {
|
|
208
226
|
const clean = validate(input);
|
|
209
|
-
return this.locked(() =>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
return this.locked(() => this.raiseLocked(clean));
|
|
228
|
+
}
|
|
229
|
+
/** Reuse a card with the same source reference, including across competing raisers. */
|
|
230
|
+
raiseOnce(input: RaiseInput, ref: string): DecisionEntry {
|
|
231
|
+
const clean = validate(input);
|
|
232
|
+
if (!clean.refs?.includes(ref)) throw new Error('decision source reference required');
|
|
233
|
+
return this.locked(() => this.list({ status: 'all' }).find(entry => entry.refs?.includes(ref)) ?? this.raiseLocked(clean));
|
|
234
|
+
}
|
|
235
|
+
private raiseLocked(clean: RaiseInput): DecisionEntry {
|
|
236
|
+
const at = utc(clean.raisedAt ?? this.now().toISOString());
|
|
237
|
+
const day = at.slice(0, 10).replaceAll('-', '');
|
|
238
|
+
const prefix = `D-${day}-`;
|
|
239
|
+
let next = 1;
|
|
240
|
+
for (const entry of this.list({ status: 'all' })) {
|
|
241
|
+
if (entry.id.startsWith(prefix)) next = Math.max(next, Number(entry.id.slice(prefix.length)) + 1);
|
|
242
|
+
}
|
|
243
|
+
const id = `${prefix}${String(next).padStart(2, '0')}`;
|
|
244
|
+
const version = this.version(at);
|
|
245
|
+
const entry: DecisionEntry = { ...clean, id, raisedAt: at, version, status: 'open', history: [{ type: 'raised', at, by: clean.raisedBy.agent, version }] };
|
|
246
|
+
this.append({ type: 'raised', entry });
|
|
247
|
+
debug.log('decisions', 'raised', { id, category: entry.category, by: entry.raisedBy.agent });
|
|
248
|
+
debug.log('decisions', 'raised-crosscheck', { id, seats: entry.crossCheck?.length ?? 0, skipped: entry.crossCheckSkipped ?? null });
|
|
249
|
+
return entry;
|
|
224
250
|
}
|
|
225
251
|
/** Record the decision and say whether its answer reached the waiting run (null = the decision resumes nothing). */
|
|
226
252
|
decideWithDelivery(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): { entry: DecisionEntry; delivery: AnswerDelivery } {
|