@iowarp/clio-coder 0.5.0 → 0.5.2
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/.claude-plugin/marketplace.json +4 -4
- package/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +9 -0
- package/README.md +12 -9
- package/ROADMAP.md +30 -5
- package/dist/{acp-KS7ARGV6.js → acp-F6PGOFZ4.js} +2 -2
- package/dist/{agents-YOGJ2YEJ.js → agents-ZHP4R6EX.js} +22 -21
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-CEDCO6O6.js → auth-N75U2H3Z.js} +4 -4
- package/dist/{chunk-D3KWFOUT.js → chunk-23UIXH6Z.js} +3 -3
- package/dist/{chunk-ZR5YA6ZF.js → chunk-36XVK5P5.js} +5 -5
- package/dist/{chunk-AVSR2HKI.js → chunk-3DZ4LPLS.js} +6 -6
- package/dist/{chunk-LYJJ6546.js → chunk-3GSPCWFX.js} +12 -12
- package/dist/{chunk-CRHPCPZP.js → chunk-4KC4CHKJ.js} +2 -2
- package/dist/{chunk-4VDHLCF5.js → chunk-4WJBWKMI.js} +2 -2
- package/dist/{chunk-KBUTSJSR.js → chunk-4XBYHXGK.js} +2 -2
- package/dist/{chunk-6W75D37F.js → chunk-53P775GE.js} +6 -3
- package/dist/{chunk-XZWSP67B.js → chunk-5APYAXCB.js} +2 -2
- package/dist/{chunk-D65E25JG.js → chunk-67HPYNTJ.js} +2 -2
- package/dist/{chunk-5JQR5B4D.js → chunk-6KV4KCQZ.js} +4 -4
- package/dist/{chunk-V4XJWDHR.js → chunk-6UKFRTXI.js} +2 -1
- package/dist/{chunk-NDPF2N2L.js → chunk-7DW7CJW2.js} +2 -2
- package/dist/{chunk-5KQEUIWM.js → chunk-7LUEQODV.js} +4 -4
- package/dist/{chunk-LNOXCGN2.js → chunk-7UFDIOBT.js} +3 -3
- package/dist/{chunk-DXSETN72.js → chunk-7WUGNN43.js} +2 -2
- package/dist/{chunk-V35KZDUZ.js → chunk-AA3GRZWS.js} +2 -2
- package/dist/{chunk-X4CEFOSG.js → chunk-ADO3OQIC.js} +4 -4
- package/dist/{chunk-UX2L5CUD.js → chunk-AI36V3ON.js} +3 -3
- package/dist/{chunk-B7GY6QER.js → chunk-B26OUMJW.js} +2 -2
- package/dist/{chunk-LAR7DX5E.js → chunk-B5TASTCK.js} +153 -62
- package/dist/{chunk-TGWZXJ7L.js → chunk-BEMQK5Q7.js} +92 -84
- package/dist/{chunk-RNDCROSG.js → chunk-CNXU7TR2.js} +3 -3
- package/dist/{chunk-3JLTRPN7.js → chunk-CVMUXT64.js} +35 -66
- package/dist/{chunk-733ZBT5P.js → chunk-E74MJF6G.js} +4 -4
- package/dist/{chunk-T2MDZES3.js → chunk-EBMTBE76.js} +3 -3
- package/dist/{chunk-OZ76TLGU.js → chunk-EEITU7S6.js} +2 -2
- package/dist/{chunk-GRHOXDKB.js → chunk-ELDJWJKM.js} +97 -14
- package/dist/{chunk-GCQFN7YI.js → chunk-EMYEQLWO.js} +2 -2
- package/dist/{chunk-LTWH2ACR.js → chunk-EQPFEDFV.js} +222 -127
- package/dist/{chunk-TDGYTWM6.js → chunk-F4I3H56H.js} +2 -2
- package/dist/chunk-F6KCJO3U.js +523 -0
- package/dist/{chunk-FGSQHUCK.js → chunk-F6VKWTTY.js} +5 -5
- package/dist/{chunk-UJD5MPP6.js → chunk-FLNXQQ5B.js} +2 -2
- package/dist/{chunk-EX57VVWW.js → chunk-FOSRJ2VZ.js} +4 -4
- package/dist/{chunk-ZARZ4POR.js → chunk-G5ZDQQE3.js} +3 -3
- package/dist/{chunk-HQVN7G4F.js → chunk-G62JBPXN.js} +7 -7
- package/dist/{chunk-ZJPORZTC.js → chunk-GIAI7A2K.js} +2 -2
- package/dist/chunk-GPTR6OTU.js +2551 -0
- package/dist/{chunk-EZD2BXGP.js → chunk-GUFQPTQK.js} +2 -2
- package/dist/{chunk-OPD7GL6F.js → chunk-H6FQVNZC.js} +8 -7
- package/dist/chunk-H7REJ7L5.js +80 -0
- package/dist/{chunk-EYTLV3W3.js → chunk-HTWTORF7.js} +2 -2
- package/dist/{chunk-DF6PI6GN.js → chunk-I6UKQPGS.js} +3 -3
- package/dist/{chunk-ISHUS7HC.js → chunk-JRW236JT.js} +3 -3
- package/dist/{chunk-DE2LU267.js → chunk-KKAJGT3B.js} +3 -2
- package/dist/{chunk-OV62D6KO.js → chunk-KQMYL5GR.js} +3 -3
- package/dist/{chunk-QDUUOZ3S.js → chunk-KWMFC7FN.js} +3 -3
- package/dist/{chunk-R7FGRZNF.js → chunk-LXYHCCIZ.js} +9 -9
- package/dist/{chunk-NFSK2VNU.js → chunk-LZMTVVN4.js} +2 -2
- package/dist/{chunk-62UK7AGW.js → chunk-M5PMRZSR.js} +397 -44
- package/dist/chunk-MMZMM6SW.js +131 -0
- package/dist/{chunk-FFYTC2KQ.js → chunk-MNABNEBX.js} +2 -2
- package/dist/{chunk-XMDBTYCH.js → chunk-N3CWOGVF.js} +3 -3
- package/dist/{chunk-VYRQHORQ.js → chunk-NMQSO6Z6.js} +4 -3
- package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
- package/dist/{chunk-4SYHGVDE.js → chunk-OQNCLZI4.js} +4 -4
- package/dist/{chunk-EG4ARXPC.js → chunk-OX6QTA4N.js} +3 -3
- package/dist/{chunk-P4GNZPXO.js → chunk-OYUS3UZP.js} +2 -2
- package/dist/{chunk-OQ2EOAHA.js → chunk-PF36KOGI.js} +291 -112
- package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
- package/dist/{chunk-CBAQPTDU.js → chunk-PIU6BXXW.js} +2 -2
- package/dist/{chunk-EKV4UBCM.js → chunk-PZBQUJ2F.js} +2 -2
- package/dist/{chunk-7RET77VB.js → chunk-Q2HKY32Y.js} +2 -2
- package/dist/{chunk-U6YTUVOX.js → chunk-Q4RMNWMZ.js} +4 -4
- package/dist/chunk-Q6FRR3AQ.js +48 -0
- package/dist/{chunk-6RJMYYIE.js → chunk-QJSIAJBY.js} +3 -3
- package/dist/{chunk-DOUISWEE.js → chunk-QOJ4ERMU.js} +5 -5
- package/dist/{chunk-IFPDAD7R.js → chunk-RWSRKXLT.js} +3 -3
- package/dist/{chunk-T33IYTZM.js → chunk-SAASVMIY.js} +3 -3
- package/dist/{chunk-2DTIWSAW.js → chunk-SEQDH6JC.js} +3 -3
- package/dist/{chunk-BRCOSE7O.js → chunk-SZUO5BSE.js} +2 -2
- package/dist/{chunk-HOEDA42N.js → chunk-T7HZ7KJF.js} +2 -2
- package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
- package/dist/{chunk-UCINIRHW.js → chunk-TPCZXWLT.js} +3 -3
- package/dist/{chunk-ZUKUCZYZ.js → chunk-TQ2KTH4A.js} +2 -2
- package/dist/{chunk-4WOOTKFD.js → chunk-U3FETNQB.js} +341 -20
- package/dist/{chunk-5B3RTYAK.js → chunk-UGDSB4AN.js} +2 -2
- package/dist/{chunk-VKHLUZNO.js → chunk-UZ7YBL43.js} +3 -3
- package/dist/{chunk-QNUQ7K7D.js → chunk-VKMSEO7Y.js} +4 -4
- package/dist/{chunk-IN7DGBVS.js → chunk-VLX5VZ35.js} +5 -5
- package/dist/{chunk-SJGS3GDI.js → chunk-WAGBMMNX.js} +5 -5
- package/dist/{chunk-BFOSV5EZ.js → chunk-WX2YCH7F.js} +2 -2
- package/dist/{chunk-3LT34CAM.js → chunk-XBIGUILU.js} +32 -17
- package/dist/{chunk-XDHUDE5K.js → chunk-XKYBFRWR.js} +5 -5
- package/dist/{chunk-U5QU5ZOD.js → chunk-XYPWFSU5.js} +3 -3
- package/dist/{chunk-YBUECLAF.js → chunk-YEJQDODA.js} +2 -2
- package/dist/{chunk-YQ6XEFVK.js → chunk-Z74OGONW.js} +39 -7
- package/dist/{chunk-I4ELN5BX.js → chunk-ZTNOEJRI.js} +2 -2
- package/dist/cli/index.js +26 -26
- package/dist/{clio-K2PBVCEM.js → clio-4IKFE25F.js} +2 -2
- package/dist/{clio-context-tools-GR4OHFSO.js → clio-context-tools-OHJXYX4R.js} +25 -23
- package/dist/{code-nav-4X3OI6DK.js → code-nav-XPM3MY5I.js} +7 -7
- package/dist/{config-Q5J7UBAB.js → config-343YS25T.js} +43 -41
- package/dist/{config-graph-P3555574.js → config-graph-WI263KDX.js} +43 -41
- package/dist/{configure-KIJ32ZO6.js → configure-GZDG4HER.js} +21 -21
- package/dist/{context-LFBQOSXT.js → context-4OFO7N7Y.js} +12 -12
- package/dist/{context-WBH3KSVR.js → context-JLK2RLOG.js} +42 -40
- package/dist/{context-NNLBES2Y.js → context-O6DIWCF6.js} +23 -21
- package/dist/{context-clear-YOHMQ6SQ.js → context-clear-MR7KOGQI.js} +42 -40
- package/dist/{context-working-set-MRAEVDNV.js → context-working-set-YZAD3KWO.js} +14 -13
- package/dist/{data-tool-JJPBGMQJ.js → data-tool-EKD7DFHG.js} +7 -7
- package/dist/{detail-RHJOTGN4.js → detail-YZPHFEVJ.js} +43 -41
- package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-VKVXVTWJ.js} +44 -42
- package/dist/{doctor-S5KZ2GTN.js → doctor-53AVQXAO.js} +17 -16
- package/dist/{doctor-deep-2EJQSHIZ.js → doctor-deep-BLRB2NCG.js} +5 -5
- package/dist/{eval-3D6M36N7.js → eval-P3BQSLQE.js} +21 -20
- package/dist/{evidence-XNVPPLVK.js → evidence-6LLI6SZH.js} +44 -42
- package/dist/{evidence-6IYN52ET.js → evidence-HTN3JLUO.js} +42 -40
- package/dist/{evidence-JVECPI3F.js → evidence-NJRE3R5H.js} +42 -40
- package/dist/{evolve-TLPUDJDM.js → evolve-WWZHJVJP.js} +42 -40
- package/dist/{fleet-WKVBF5SZ.js → fleet-GGZ4BVJE.js} +64 -62
- package/dist/{fleet-6RJWIOEO.js → fleet-QG3IIWWM.js} +44 -42
- package/dist/{fleet-commands-L6WFT7KZ.js → fleet-commands-J2ZIKISQ.js} +7 -7
- package/dist/{fleet-decisions-BTQMWU5A.js → fleet-decisions-Y2Q7375Q.js} +8 -8
- package/dist/{fleet-graph-553DZ67Q.js → fleet-graph-TCBCCCBP.js} +11 -11
- package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-KOLKBC56.js} +44 -42
- package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-44UNCC4Y.js} +27 -25
- package/dist/{fleet-validate-7NLAE26R.js → fleet-validate-K2FXRZVA.js} +13 -13
- package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-AS444WDW.js} +42 -40
- package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-X4JU4J6O.js} +43 -41
- package/dist/gui/ops-worker.js +9 -9
- package/dist/gui/reads-worker.js +9 -8
- package/dist/{init-DV5B6HQG.js → init-VSOWFHVL.js} +54 -52
- package/dist/{interactive-OXGYZYAS.js → interactive-JRCBENLZ.js} +1733 -577
- package/dist/{interop-J7UOBOSF.js → interop-JR6BCY7R.js} +10 -10
- package/dist/{inventory-OEGIL6PY.js → inventory-3JUGLVJI.js} +43 -41
- package/dist/{library-ROZAMUIL.js → library-7BOH3Y7N.js} +15 -15
- package/dist/{library-B3UPGY4V.js → library-7PFONXUI.js} +9 -9
- package/dist/{library-MXQA5DLT.js → library-TUYZMFIA.js} +7 -7
- package/dist/{library-import-2LTWOVBD.js → library-import-TJVXKLDK.js} +10 -10
- package/dist/{library-inventory-LJ3XHLWO.js → library-inventory-MD3AYTP4.js} +9 -9
- package/dist/{library-validation-DARHHUNW.js → library-validation-ZDLITEA3.js} +7 -7
- package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
- package/dist/{memory-J3STUUIK.js → memory-Q2YVOKK5.js} +42 -40
- package/dist/{models-Y3GMFJ2V.js → models-M7BOCW7F.js} +17 -16
- package/dist/{monitor-LHNH577J.js → monitor-2HD3MC67.js} +47 -45
- package/dist/{orchestrator-DTAPXULA.js → orchestrator-OUW3ZN6Y.js} +1894 -226
- package/dist/{panes-A743ONZ5.js → panes-TTPPSYCL.js} +3 -3
- package/dist/{preload-OJBMAIXF.js → preload-A47C2NUF.js} +42 -40
- package/dist/{providers-CMETPZNS.js → providers-T43W7EL4.js} +4 -4
- package/dist/{resources-FY6XYFN2.js → resources-TP6X2V6W.js} +20 -11
- package/dist/{run-H64MEVTA.js → run-LDSBPMCS.js} +61 -59
- package/dist/{share-CAFMMQLM.js → share-URHKKMHR.js} +9 -9
- package/dist/{skills-ERKQUC7L.js → skills-E477MOOO.js} +13 -11
- package/dist/{skills-eval-CQ23MYQK.js → skills-eval-AIBR2L7H.js} +583 -93
- package/dist/{skills-inventory-TVUYJDVN.js → skills-inventory-HLMKY3R5.js} +13 -11
- package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-LIIZKA5Y.js} +28 -26
- package/dist/{startup-background-KDWCQMWT.js → startup-background-JR2KDK6R.js} +43 -41
- package/dist/{steer-KKYXJSH4.js → steer-D7RTAPCM.js} +3 -3
- package/dist/{system-ZDW44ZGX.js → system-X2P5N4IY.js} +14 -13
- package/dist/{targets-AJWA6E7Z.js → targets-VFFAKUFM.js} +25 -24
- package/dist/{tasks-6L2JJRC5.js → tasks-ERAWZA22.js} +6 -6
- package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
- package/dist/{trace-LA5MVR4Y.js → trace-MA5BT7XY.js} +3 -3
- package/dist/{usage-XSILDHKF.js → usage-WYFY2L6M.js} +50 -48
- package/dist/{verifiers-ECA5GIHL.js → verifiers-TYIW2XKF.js} +7 -7
- package/dist/{verify-R6QQCCEB.js → verify-242UFYTP.js} +6 -6
- package/dist/{web-fetch-PVZNMBOB.js → web-fetch-LRAGYU6Y.js} +3 -3
- package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-OFKOX2T6.js} +53 -51
- package/dist/worker/entry.js +31 -29
- package/docs/architecture/architecture.md +1 -0
- package/docs/architecture/context-engine.md +4 -2
- package/docs/architecture/observability.md +6 -4
- package/docs/architecture/prompt-envelope-and-tools.md +1 -1
- package/docs/architecture/session-lifecycle.md +1 -1
- package/docs/architecture/tui-design.md +12 -9
- package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
- package/docs/guide/commands-and-modes.md +91 -5
- package/docs/guide/configuration-and-targets.md +2 -2
- package/docs/guide/context-continuity.md +49 -0
- package/docs/guide/proactive-memory.md +6 -3
- package/library/registry.yaml +292 -32
- package/library/skills/README.md +27 -2
- package/library/skills/context/context-handoff/SKILL.md +10 -4
- package/library/skills/meta/clio-coder-dev/SKILL.md +92 -88
- package/library/skills/meta/clio-coder-dev/evals.md +51 -46
- package/library/skills/meta/clio-coder-dev/plugin.json +2 -2
- package/library/skills/meta/clio-coder-dev/references/change-map.md +49 -0
- package/library/skills/meta/clio-coder-test/SKILL.md +89 -142
- package/library/skills/meta/clio-coder-test/evals.md +58 -62
- package/library/skills/meta/clio-coder-test/plugin.json +2 -2
- package/library/skills/meta/clio-coder-test/references/harness.md +3 -3
- package/library/skills/meta/clio-coder-test/references/lifecycle-validation.md +30 -0
- package/library/skills/meta/clio-coder-test/references/test-map.md +60 -89
- package/library/skills/registry.yaml +5 -5
- package/library/skills/skill-marketplace.json +12 -14
- package/package.json +1 -1
- package/src/cli/skills-eval.ts +1064 -54
- package/src/cli/usage.ts +2 -2
- package/src/core/clio-repo.ts +3 -0
- package/src/core/tool-names.ts +1 -0
- package/src/domains/context/budget/inspection.ts +9 -0
- package/src/domains/context/budget/live-view.ts +418 -0
- package/src/domains/context/budget/pressure.ts +393 -0
- package/src/domains/context/budget/request-fit.ts +15 -0
- package/src/domains/evidence/build.ts +24 -0
- package/src/domains/gateway/mcp/client.ts +1 -1
- package/src/domains/memory/commit-state.ts +160 -0
- package/src/domains/memory/operations.ts +18 -10
- package/src/domains/memory/prompt-cache.ts +95 -0
- package/src/domains/memory/prompt-section.ts +50 -7
- package/src/domains/memory/relevance.ts +93 -0
- package/src/domains/memory/restoration.ts +78 -0
- package/src/domains/memory/store.ts +39 -1
- package/src/domains/middleware/memory-intervention.ts +116 -24
- package/src/domains/observability/background-memory-usage.ts +1 -1
- package/src/domains/observability/cost.ts +3 -3
- package/src/domains/observability/extension.ts +1 -1
- package/src/domains/observability/metrics.ts +1 -1
- package/src/domains/prompts/compiler.ts +1 -1
- package/src/domains/prompts/extension.ts +38 -1
- package/src/domains/quota/anthropic-max-provider.ts +134 -0
- package/src/domains/quota/anthropic-usage.ts +220 -0
- package/src/domains/quota/antigravity-provider.ts +339 -0
- package/src/domains/quota/cache.ts +87 -0
- package/src/domains/quota/claude-code-provider.ts +169 -0
- package/src/domains/quota/codex-provider.ts +237 -0
- package/src/domains/quota/presentation.ts +240 -0
- package/src/domains/quota/registry.ts +23 -0
- package/src/domains/quota/service.ts +93 -0
- package/src/domains/quota/summary-feed.ts +86 -0
- package/src/domains/quota/types.ts +84 -0
- package/src/domains/resources/index.ts +11 -0
- package/src/domains/resources/skills/catalog-view.ts +571 -0
- package/src/domains/resources/skills/lexical-match.ts +136 -0
- package/src/domains/resources/skills/loader.ts +38 -0
- package/src/domains/resources/skills/promotion.ts +1 -55
- package/src/domains/resources/skills/provenance-pin.ts +50 -20
- package/src/domains/safety/action-classifier.ts +1 -0
- package/src/domains/session/compaction/branch-summary.ts +4 -1
- package/src/domains/session/compaction/compact.ts +18 -2
- package/src/domains/session/compaction/cut-point.ts +20 -1
- package/src/domains/session/compaction/tokens.ts +48 -3
- package/src/domains/session/context-accounting.ts +8 -1
- package/src/domains/session/continuity/carry.ts +59 -0
- package/src/domains/session/continuity/contract.ts +592 -0
- package/src/domains/session/continuity/evidence.ts +290 -0
- package/src/domains/session/continuity/fold.ts +1075 -0
- package/src/domains/session/continuity/note.ts +79 -0
- package/src/domains/session/continuity/operator-request.ts +104 -0
- package/src/domains/session/continuity/persistence.ts +408 -0
- package/src/domains/session/continuity/ports.ts +231 -0
- package/src/domains/session/continuity/projection.ts +538 -0
- package/src/domains/session/continuity/validate.ts +352 -0
- package/src/domains/session/entries.ts +50 -3
- package/src/domains/session/index.ts +48 -0
- package/src/domains/session/migrations/index.ts +7 -3
- package/src/domains/session/tree/fork.ts +15 -1
- package/src/domains/session/usage.ts +2 -2
- package/src/engine/acp/commands.ts +1 -1
- package/src/engine/agent.ts +133 -9
- package/src/engine/session.ts +13 -6
- package/src/entry/orchestrator.ts +116 -33
- package/src/interactive/chat-loop-messages.ts +2 -2
- package/src/interactive/chat-loop.ts +219 -63
- package/src/interactive/chat-renderer.ts +99 -8
- package/src/interactive/context-overlay.ts +24 -3
- package/src/interactive/continuity-controller.ts +526 -0
- package/src/interactive/dispatch-board.ts +21 -3
- package/src/interactive/footer/dashboard.ts +25 -1
- package/src/interactive/footer/key-hints.ts +2 -2
- package/src/interactive/footer/pages.ts +70 -27
- package/src/interactive/footer/widgets.ts +32 -7
- package/src/interactive/footer-panel.ts +1 -1
- package/src/interactive/interactive-application.ts +7 -2
- package/src/interactive/interactive-input-runtime.ts +2 -2
- package/src/interactive/interactive-presentation.ts +15 -0
- package/src/interactive/interactive-slash-runtime.ts +45 -8
- package/src/interactive/interactive-tickers.ts +7 -1
- package/src/interactive/model-session-replay.ts +130 -3
- package/src/interactive/output-reserve.ts +35 -0
- package/src/interactive/overlay-general-openers.ts +12 -8
- package/src/interactive/overlay-key-routing.ts +6 -9
- package/src/interactive/overlay-lifecycle.ts +10 -6
- package/src/interactive/overlay-session-lifecycle.ts +21 -14
- package/src/interactive/quota-view.ts +229 -0
- package/src/interactive/session-last-turn.ts +1 -1
- package/src/interactive/session-transcript.ts +2 -1
- package/src/interactive/session-usage-reseed.ts +2 -2
- package/src/interactive/side-question.ts +2 -2
- package/src/interactive/slash-commands.ts +28 -9
- package/src/interactive/turn-context.ts +686 -63
- package/src/interactive/turn-middleware.ts +39 -6
- package/src/interactive/turn-persistence.ts +15 -2
- package/src/interactive/turn-prewarm.ts +1 -1
- package/src/interactive/turn-runtime.ts +48 -5
- package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
- package/src/interactive/welcome-dashboard.ts +26 -3
- package/src/tools/agent-tools.ts +3 -1
- package/src/tools/bootstrap.ts +6 -0
- package/src/tools/builtin-tool-catalog.ts +9 -0
- package/src/tools/context/index.ts +148 -113
- package/src/tools/context/surface.ts +11 -7
- package/src/tools/core-bootstrap.ts +4 -0
- package/src/tools/observation.ts +8 -2
- package/src/tools/policy.ts +3 -0
- package/src/tools/self-compact.ts +31 -0
- package/src/tools/surface.ts +1 -0
- package/src/tools/tasks.ts +1 -1
- package/dist/chunk-CTFPFW3H.js +0 -44
- package/dist/chunk-EOJPDNUP.js +0 -1325
- package/dist/chunk-I4Y4WKZR.js +0 -386
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact UTF-8 note admission (CONTRACTS.md §4).
|
|
3
|
+
*
|
|
4
|
+
* The operator's note is stored as the bytes they wrote. Whitespace is tested
|
|
5
|
+
* for emptiness only: nothing here trims, normalizes Unicode, truncates, or
|
|
6
|
+
* regenerates the accepted value, because replay must reproduce the decoded
|
|
7
|
+
* string exactly and JSONL escaping already changes the wire representation.
|
|
8
|
+
*
|
|
9
|
+
* Validation runs before any append or lock, takes no lock itself, and writes
|
|
10
|
+
* nothing. A rejection is typed so the caller can explain it without guessing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import {
|
|
15
|
+
type AcceptedNote,
|
|
16
|
+
type AcceptedNoteVerification,
|
|
17
|
+
HANDOFF_NOTE_MAX_BYTES,
|
|
18
|
+
type NoteValidation,
|
|
19
|
+
} from "./contract.js";
|
|
20
|
+
|
|
21
|
+
function sha256Utf8(value: string): string {
|
|
22
|
+
return createHash("sha256").update(Buffer.from(value, "utf8")).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Admit a note, or say precisely why not.
|
|
27
|
+
*
|
|
28
|
+
* The UTF-8 round trip is the check that rejects a lone surrogate: encoding
|
|
29
|
+
* `"\uD800"` and decoding it back yields U+FFFD, so the decoded string differs
|
|
30
|
+
* from the input and the note is refused rather than silently stored as a
|
|
31
|
+
* replacement character the operator never typed. The byte bound is measured on
|
|
32
|
+
* the UTF-8 encoding, not on `String.length`, so an emoji-heavy note is bounded
|
|
33
|
+
* by what the provider will actually carry.
|
|
34
|
+
*/
|
|
35
|
+
export function validateContinuityNote(note: unknown): NoteValidation {
|
|
36
|
+
if (typeof note !== "string") return { ok: false, reason: "not_a_string", noteBytes: null };
|
|
37
|
+
if (note.trim().length === 0) return { ok: false, reason: "blank", noteBytes: null };
|
|
38
|
+
if (note.includes("\u0000")) return { ok: false, reason: "contains_nul", noteBytes: null };
|
|
39
|
+
|
|
40
|
+
const encoded = Buffer.from(note, "utf8");
|
|
41
|
+
if (encoded.toString("utf8") !== note) {
|
|
42
|
+
return { ok: false, reason: "not_utf8_round_trip", noteBytes: encoded.byteLength };
|
|
43
|
+
}
|
|
44
|
+
if (encoded.byteLength > HANDOFF_NOTE_MAX_BYTES) {
|
|
45
|
+
return { ok: false, reason: "exceeds_max_bytes", noteBytes: encoded.byteLength };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
accepted: { note, noteBytes: encoded.byteLength, noteSha256: sha256Utf8(note) },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Verify a note read back from a record.
|
|
55
|
+
*
|
|
56
|
+
* Self-consistency first: the text is re-admitted, because a record whose note
|
|
57
|
+
* no longer admits is not usable evidence whatever its digest says, and the
|
|
58
|
+
* stored byte count and hash must match that re-encoding. Byte count is
|
|
59
|
+
* compared before the hash so a truncated readback reports the truncation
|
|
60
|
+
* rather than an opaque digest mismatch.
|
|
61
|
+
*
|
|
62
|
+
* `expected` compares two copies of the same transaction, which is how a
|
|
63
|
+
* commit's note and a later summary carry's note are held to exact decoded
|
|
64
|
+
* equality across records.
|
|
65
|
+
*/
|
|
66
|
+
export function verifyAcceptedNote(accepted: AcceptedNote, expected?: AcceptedNote): AcceptedNoteVerification {
|
|
67
|
+
const revalidated = validateContinuityNote(accepted.note);
|
|
68
|
+
if (!revalidated.ok) return { ok: false, reason: revalidated.reason };
|
|
69
|
+
if (revalidated.accepted.noteBytes !== accepted.noteBytes) return { ok: false, reason: "byte_count_mismatch" };
|
|
70
|
+
if (revalidated.accepted.noteSha256 !== accepted.noteSha256) return { ok: false, reason: "hash_mismatch" };
|
|
71
|
+
if (!expected) return { ok: true };
|
|
72
|
+
// Decoded text first: when two copies of one transaction disagree, the
|
|
73
|
+
// useful report is that the text differs, not a digest that differs because
|
|
74
|
+
// the text does.
|
|
75
|
+
if (expected.note !== accepted.note) return { ok: false, reason: "decoded_text_mismatch" };
|
|
76
|
+
if (expected.noteBytes !== accepted.noteBytes) return { ok: false, reason: "byte_count_mismatch" };
|
|
77
|
+
if (expected.noteSha256 !== accepted.noteSha256) return { ok: false, reason: "hash_mismatch" };
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable operator control request behind `operatorRequestEntryId`
|
|
3
|
+
* (CONTRACTS.md §3.1).
|
|
4
|
+
*
|
|
5
|
+
* Clio's existing control commands are display-only echoes: `/context compact
|
|
6
|
+
* [instructions]` runs a compaction and `/resume` opens session navigation, and
|
|
7
|
+
* neither leaves a durable record that means "resume this handoff". So the
|
|
8
|
+
* authority a `resumed` event cites cannot be a conversation turn. It is a
|
|
9
|
+
* reserved `CustomEntry` subtype, written by an operator surface, carrying the
|
|
10
|
+
* exact binding the fold checks: which handoff, which action, which paused or
|
|
11
|
+
* failed head, which session and which branch.
|
|
12
|
+
*
|
|
13
|
+
* This module owns the wire shape and its strict validation. It deliberately
|
|
14
|
+
* owns nothing else: the `/context recover <handoffId> <reduce|deliver>` command
|
|
15
|
+
* that writes one is packet 03/05, and nothing here or in 02B executes a
|
|
16
|
+
* recovery. An opaque `custom` entry is not authority, which is why
|
|
17
|
+
* `isSessionEntry` routes this `customType` through the strict check below
|
|
18
|
+
* rather than accepting any data under the reserved name.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { CustomEntry } from "../entries.js";
|
|
22
|
+
|
|
23
|
+
/** Reserved `customType`. A record under this name must satisfy the strict shape. */
|
|
24
|
+
export const HANDOFF_RECOVERY_REQUEST_CUSTOM_TYPE = "contextHandoffRecoveryRequest";
|
|
25
|
+
|
|
26
|
+
export interface HandoffRecoveryRequestData {
|
|
27
|
+
version: 1;
|
|
28
|
+
requestKind: "handoff_recovery";
|
|
29
|
+
handoffId: string;
|
|
30
|
+
action: "reduce" | "deliver";
|
|
31
|
+
/** The session the operator made the request in. */
|
|
32
|
+
sessionId: string;
|
|
33
|
+
/** The handoff's immutable branch anchor, as the request understood it. */
|
|
34
|
+
branchAnchorTurnId: string | null;
|
|
35
|
+
/** The live selected leaf when the request was made. */
|
|
36
|
+
selectedLeafTurnId: string | null;
|
|
37
|
+
/** The exact head this request answers. */
|
|
38
|
+
pausedOrFailedEntryId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type HandoffRecoveryRequestEntry = CustomEntry<HandoffRecoveryRequestData> & {
|
|
42
|
+
customType: typeof HANDOFF_RECOVERY_REQUEST_CUSTOM_TYPE;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
50
|
+
return typeof value === "string" && value.length > 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isNullableString(value: unknown): value is string | null {
|
|
54
|
+
return value === null || isNonEmptyString(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Strict validation of the reserved subtype's data.
|
|
59
|
+
*
|
|
60
|
+
* Every field is required, including the two nullable ones: an absent
|
|
61
|
+
* `branchAnchorTurnId` and an explicit `null` are different claims, and a
|
|
62
|
+
* request that simply omits the binding must not read as a request that
|
|
63
|
+
* asserted "no anchor". A request missing any part of its binding is not a
|
|
64
|
+
* weaker request, it is not a request at all.
|
|
65
|
+
*/
|
|
66
|
+
export function isHandoffRecoveryRequestData(value: unknown): value is HandoffRecoveryRequestData {
|
|
67
|
+
if (!isRecord(value)) return false;
|
|
68
|
+
return (
|
|
69
|
+
value.version === 1 &&
|
|
70
|
+
value.requestKind === "handoff_recovery" &&
|
|
71
|
+
isNonEmptyString(value.handoffId) &&
|
|
72
|
+
(value.action === "reduce" || value.action === "deliver") &&
|
|
73
|
+
isNonEmptyString(value.sessionId) &&
|
|
74
|
+
Object.hasOwn(value, "branchAnchorTurnId") &&
|
|
75
|
+
isNullableString(value.branchAnchorTurnId) &&
|
|
76
|
+
Object.hasOwn(value, "selectedLeafTurnId") &&
|
|
77
|
+
isNullableString(value.selectedLeafTurnId) &&
|
|
78
|
+
isNonEmptyString(value.pausedOrFailedEntryId)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether a ledger entry is a valid operator recovery request.
|
|
84
|
+
*
|
|
85
|
+
* Takes the entry rather than the data so a caller cannot reach authority by
|
|
86
|
+
* validating loose data it assembled itself: the record must really be a
|
|
87
|
+
* `custom` entry under the reserved `customType`.
|
|
88
|
+
*/
|
|
89
|
+
export function isHandoffRecoveryRequestEntry(value: unknown): value is HandoffRecoveryRequestEntry {
|
|
90
|
+
if (!isRecord(value)) return false;
|
|
91
|
+
return (
|
|
92
|
+
value.kind === "custom" &&
|
|
93
|
+
value.customType === HANDOFF_RECOVERY_REQUEST_CUSTOM_TYPE &&
|
|
94
|
+
isNonEmptyString(value.turnId) &&
|
|
95
|
+
isNullableString(value.parentTurnId) &&
|
|
96
|
+
isNonEmptyString(value.timestamp) &&
|
|
97
|
+
// §3.1 specifies `display: false`. A control record that renders is a
|
|
98
|
+
// different thing from the one the contract adopted, and the flag is part
|
|
99
|
+
// of the adopted envelope rather than a presentation preference: an
|
|
100
|
+
// authority carrier must not also be transcript text.
|
|
101
|
+
value.display === false &&
|
|
102
|
+
isHandoffRecoveryRequestData(value.data)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded persistence of an ordered continuity group (CONTRACTS.md §5, §6).
|
|
3
|
+
*
|
|
4
|
+
* Everything is injected: append, an exact payload-comparing readback, the
|
|
5
|
+
* barrier, removed-state and origin checks, and a bounded retry schedule. This
|
|
6
|
+
* module performs no execution of its own and knows nothing about models,
|
|
7
|
+
* runtimes or replay. No failure is logged by recursively persisting more
|
|
8
|
+
* records; a blocked group is reported to its caller and stops there.
|
|
9
|
+
*
|
|
10
|
+
* Rules that drive the shape:
|
|
11
|
+
*
|
|
12
|
+
* - An accepted append is never repeated because a later barrier failed. A
|
|
13
|
+
* failed barrier is retried with the same ids, within a finite bound.
|
|
14
|
+
* - A throwing append proves neither success nor absence. Only an
|
|
15
|
+
* authoritative `absent` readback licenses a re-append, under the original
|
|
16
|
+
* id; `conflicting` and `unresolved`, including a readback that itself
|
|
17
|
+
* throws, stop the group, because malformed or contradictory data cannot
|
|
18
|
+
* prove absence.
|
|
19
|
+
* - Removed state and origin are rechecked at every boundary that can be
|
|
20
|
+
* crossed by a wait or an await, not only at the ends of the group.
|
|
21
|
+
* - The supplied clock is treated as monotonic within one call. A reading
|
|
22
|
+
* that stops being a number, or that jumps backwards between two
|
|
23
|
+
* boundaries, makes the window unjudgeable rather than optimistic.
|
|
24
|
+
* - `durable` means the barrier reported success. Under §1's filesystem
|
|
25
|
+
* ceiling that is not a power-loss guarantee, and only a usable clock that
|
|
26
|
+
* stayed inside the window confirms it: persistence is not permission for
|
|
27
|
+
* dependent execution.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
type ContinuityAcceptedProgress,
|
|
32
|
+
type ContinuityAnomaly,
|
|
33
|
+
type ContinuityAppendable,
|
|
34
|
+
type ContinuityPersistBlockedReason,
|
|
35
|
+
type ContinuityPersistencePorts,
|
|
36
|
+
type ContinuityPersistRequest,
|
|
37
|
+
type ContinuityPersistResult,
|
|
38
|
+
type ContinuityReconcileResult,
|
|
39
|
+
type ContinuityRetrySchedule,
|
|
40
|
+
HANDOFF_MAX_FLUSH_RETRIES,
|
|
41
|
+
} from "./contract.js";
|
|
42
|
+
import { canonicalJson } from "./validate.js";
|
|
43
|
+
|
|
44
|
+
/** The digest a progress token carries, so an id can never stand in for a payload. */
|
|
45
|
+
export function continuityRecordDigest(entry: ContinuityAppendable): string {
|
|
46
|
+
return canonicalJson(entry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function blocked(
|
|
50
|
+
reason: ContinuityPersistBlockedReason,
|
|
51
|
+
accepted: ReadonlyArray<ContinuityAcceptedProgress>,
|
|
52
|
+
barrierAttempts: number,
|
|
53
|
+
anomalies: ReadonlyArray<ContinuityAnomaly>,
|
|
54
|
+
): ContinuityPersistResult {
|
|
55
|
+
return { status: "uncertain", reason, accepted: [...accepted], barrierAttempts, anomalies };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readback(entry: ContinuityAppendable, ports: ContinuityPersistencePorts) {
|
|
59
|
+
try {
|
|
60
|
+
return ports.readExact(entry);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// A readback that throws read nothing authoritative. It is unresolved,
|
|
63
|
+
// never absence.
|
|
64
|
+
return { status: "unresolved" as const, detail: error instanceof Error ? error.message : String(error) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Establish what actually happened to one record whose append was ambiguous.
|
|
70
|
+
*
|
|
71
|
+
* This is a **read-only** classification. The expected record goes to the port
|
|
72
|
+
* so the comparison is on payload, not on the id: a stored record with the same
|
|
73
|
+
* id and different bytes is `conflicting`, and re-appending over it would
|
|
74
|
+
* create a second contradictory copy. Writing is deliberately left to
|
|
75
|
+
* `persistContinuityGroup`, which re-checks removed state, origin and the
|
|
76
|
+
* deadline before it re-appends; a reconciler that wrote on its own would
|
|
77
|
+
* bypass exactly the guards a readback can invalidate.
|
|
78
|
+
*/
|
|
79
|
+
export function reconcileContinuityEntry(
|
|
80
|
+
entry: ContinuityAppendable,
|
|
81
|
+
ports: ContinuityPersistencePorts,
|
|
82
|
+
): ContinuityReconcileResult {
|
|
83
|
+
const result = readback(entry, ports);
|
|
84
|
+
if (result.status === "matching") return { status: "accepted" };
|
|
85
|
+
if (result.status === "conflicting") {
|
|
86
|
+
return { status: "conflicting", ...(result.detail === undefined ? {} : { detail: result.detail }) };
|
|
87
|
+
}
|
|
88
|
+
if (result.status === "unresolved") {
|
|
89
|
+
return { status: "unresolved", ...(result.detail === undefined ? {} : { detail: result.detail }) };
|
|
90
|
+
}
|
|
91
|
+
return { status: "absent" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A schedule that cannot bound the work is refused rather than run. */
|
|
95
|
+
function invalidSchedule(retry: ContinuityRetrySchedule): string | null {
|
|
96
|
+
if (!Number.isSafeInteger(retry.limit) || retry.limit < 0 || retry.limit > HANDOFF_MAX_FLUSH_RETRIES) {
|
|
97
|
+
return `retry limit ${retry.limit} is not an integer in 0..${HANDOFF_MAX_FLUSH_RETRIES}`;
|
|
98
|
+
}
|
|
99
|
+
if (!Number.isFinite(retry.deadlineAtMs)) return "retry deadline is not finite";
|
|
100
|
+
const now = retry.now();
|
|
101
|
+
if (!Number.isFinite(now)) return "retry clock is not finite";
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A clock reading that is usable for this operation.
|
|
107
|
+
*
|
|
108
|
+
* The supplied clock is read many times across appends, awaits and a barrier.
|
|
109
|
+
* Finiteness alone is not enough: a clock that jumps backwards mid-operation
|
|
110
|
+
* cannot be used to judge a deadline either, and neither can one that stops
|
|
111
|
+
* being a number. The guard is monotonic within one call and answers `null` the
|
|
112
|
+
* moment it stops being usable; every caller then treats the window as
|
|
113
|
+
* unconfirmed rather than assuming the optimistic reading.
|
|
114
|
+
*/
|
|
115
|
+
interface ClockGuard {
|
|
116
|
+
read(): number | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createClockGuard(retry: ContinuityRetrySchedule): ClockGuard {
|
|
120
|
+
let observed = Number.NEGATIVE_INFINITY;
|
|
121
|
+
let broken = false;
|
|
122
|
+
return {
|
|
123
|
+
read(): number | null {
|
|
124
|
+
if (broken) return null;
|
|
125
|
+
const now = retry.now();
|
|
126
|
+
if (!Number.isFinite(now) || now < observed) {
|
|
127
|
+
broken = true;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
observed = now;
|
|
131
|
+
return now;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type BarrierOutcome =
|
|
137
|
+
| { ok: true; attempts: number; confirmedWithinDeadline: boolean }
|
|
138
|
+
| { ok: false; attempts: number; reason: ContinuityPersistBlockedReason; detail: string };
|
|
139
|
+
|
|
140
|
+
async function runBarrier(
|
|
141
|
+
request: ContinuityPersistRequest,
|
|
142
|
+
ports: ContinuityPersistencePorts,
|
|
143
|
+
retry: ContinuityRetrySchedule,
|
|
144
|
+
clock: ClockGuard,
|
|
145
|
+
): Promise<BarrierOutcome> {
|
|
146
|
+
const flushing = request.barrier.kind === "flush";
|
|
147
|
+
const port = flushing ? ports.flushAppends : ports.checkpoint;
|
|
148
|
+
if (!port) {
|
|
149
|
+
// A SessionContract without the barrier is an unsupported persistence
|
|
150
|
+
// port. Treating a missing barrier as success would report durability
|
|
151
|
+
// that was never attempted.
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
attempts: 0,
|
|
155
|
+
reason: "unsupported_barrier_port",
|
|
156
|
+
detail: `${request.barrier.kind} port is not available`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let attempts = 0;
|
|
161
|
+
let detail = "";
|
|
162
|
+
for (let round = 0; round <= retry.limit; round += 1) {
|
|
163
|
+
// Read before every attempt, round 0 included: the appends that just ran
|
|
164
|
+
// can have taken the clock past the deadline or broken it outright. A late
|
|
165
|
+
// barrier still runs, because it can establish persistence for records
|
|
166
|
+
// already accepted; what it cannot do is confirm the window.
|
|
167
|
+
let before = clock.read();
|
|
168
|
+
if (round > 0) {
|
|
169
|
+
if (before === null || before >= retry.deadlineAtMs) break;
|
|
170
|
+
try {
|
|
171
|
+
await retry.wait(round);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
attempts,
|
|
176
|
+
reason: "barrier_failed",
|
|
177
|
+
detail: `retry wait rejected: ${error instanceof Error ? error.message : String(error)}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// The wait is an await, so every fact can have changed across it,
|
|
181
|
+
// including the clock going backwards or stopping being a number.
|
|
182
|
+
before = clock.read();
|
|
183
|
+
if (before === null) {
|
|
184
|
+
return { ok: false, attempts, reason: "invalid_retry_schedule", detail: "retry clock became unusable" };
|
|
185
|
+
}
|
|
186
|
+
if (ports.isStateRemoved()) return { ok: false, attempts, reason: "state_removed", detail: "state removed" };
|
|
187
|
+
if (!ports.isOriginCurrent()) return { ok: false, attempts, reason: "origin_changed", detail: "origin changed" };
|
|
188
|
+
if (before >= retry.deadlineAtMs) break;
|
|
189
|
+
}
|
|
190
|
+
attempts += 1;
|
|
191
|
+
try {
|
|
192
|
+
if (request.barrier.kind === "flush") {
|
|
193
|
+
(port as () => void)();
|
|
194
|
+
} else {
|
|
195
|
+
await (port as (reason: string) => Promise<void>)(request.barrier.reason);
|
|
196
|
+
}
|
|
197
|
+
// The barrier itself can be an await. Confirmation needs a clock that
|
|
198
|
+
// is still usable afterwards and still inside the window.
|
|
199
|
+
const after = clock.read();
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
attempts,
|
|
203
|
+
confirmedWithinDeadline: before !== null && after !== null && after < retry.deadlineAtMs,
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
detail = error instanceof Error ? error.message : String(error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { ok: false, attempts, reason: "barrier_failed", detail: detail || "deadline reached before the barrier ran" };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Append an ordered group and run one barrier after the complete group.
|
|
214
|
+
*
|
|
215
|
+
* `alreadyAccepted` resumes a partial attempt at this exact group, matched by
|
|
216
|
+
* payload digest so an id cannot carry a different record than the one that was
|
|
217
|
+
* accepted. A previously accepted entry is skipped, never re-appended: only a
|
|
218
|
+
* failing append inside this call reconciles.
|
|
219
|
+
*/
|
|
220
|
+
export async function persistContinuityGroup(
|
|
221
|
+
request: ContinuityPersistRequest,
|
|
222
|
+
ports: ContinuityPersistencePorts,
|
|
223
|
+
retry: ContinuityRetrySchedule,
|
|
224
|
+
): Promise<ContinuityPersistResult> {
|
|
225
|
+
const anomalies: ContinuityAnomaly[] = [];
|
|
226
|
+
const accepted: ContinuityAcceptedProgress[] = [];
|
|
227
|
+
|
|
228
|
+
// One record per id, compared by payload before anything is written.
|
|
229
|
+
const wanted = new Map<string, string>();
|
|
230
|
+
for (const entry of request.entries) {
|
|
231
|
+
const digest = continuityRecordDigest(entry);
|
|
232
|
+
const previous = wanted.get(entry.turnId);
|
|
233
|
+
if (previous !== undefined && previous !== digest) {
|
|
234
|
+
anomalies.push({
|
|
235
|
+
kind: "duplicate_conflict",
|
|
236
|
+
entryId: entry.turnId,
|
|
237
|
+
detail: "the group contains two different records under one entry id",
|
|
238
|
+
});
|
|
239
|
+
return blocked("duplicate_group_entry", accepted, 0, anomalies);
|
|
240
|
+
}
|
|
241
|
+
wanted.set(entry.turnId, digest);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Progress must belong to this request: an id whose recorded digest is not
|
|
245
|
+
// the record this group carries is a different group. Repeats of one id are
|
|
246
|
+
// folded, so duplicated progress cannot stand in for a record that is still
|
|
247
|
+
// missing.
|
|
248
|
+
const acceptedIds = new Set<string>();
|
|
249
|
+
for (const progress of request.alreadyAccepted ?? []) {
|
|
250
|
+
const expected = wanted.get(progress.entryId);
|
|
251
|
+
if (expected === undefined || expected !== progress.payloadDigest) {
|
|
252
|
+
anomalies.push({
|
|
253
|
+
kind: "duplicate_conflict",
|
|
254
|
+
entryId: progress.entryId,
|
|
255
|
+
detail: "reported progress does not match this group's record for that id",
|
|
256
|
+
});
|
|
257
|
+
return blocked("progress_mismatch", [], 0, anomalies);
|
|
258
|
+
}
|
|
259
|
+
if (acceptedIds.has(progress.entryId)) continue;
|
|
260
|
+
acceptedIds.add(progress.entryId);
|
|
261
|
+
accepted.push({ entryId: progress.entryId, payloadDigest: progress.payloadDigest });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Validated progress is established before the schedule is judged, so a
|
|
265
|
+
// refusal that writes nothing still hands back what an earlier attempt
|
|
266
|
+
// really got accepted. Losing it would invite a caller to append it twice.
|
|
267
|
+
const scheduleProblem = invalidSchedule(retry);
|
|
268
|
+
if (scheduleProblem) {
|
|
269
|
+
anomalies.push({ kind: "malformed_record", entryId: null, detail: scheduleProblem });
|
|
270
|
+
return blocked("invalid_retry_schedule", accepted, 0, anomalies);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// One monotonic clock reading for the whole call. Every boundary goes
|
|
274
|
+
// through it, so a clock that jumps backwards between two of them is caught
|
|
275
|
+
// rather than believed.
|
|
276
|
+
const clock = createClockGuard(retry);
|
|
277
|
+
|
|
278
|
+
const remaining = () => request.entries.filter((entry) => !acceptedIds.has(entry.turnId));
|
|
279
|
+
// A group whose window has already closed is not started. A late barrier can
|
|
280
|
+
// still establish persistence for work already accepted, but new records are
|
|
281
|
+
// not written into an expired transaction.
|
|
282
|
+
const startedAt = clock.read();
|
|
283
|
+
if (startedAt === null) {
|
|
284
|
+
anomalies.push({ kind: "malformed_record", entryId: null, detail: "the retry clock is not usable" });
|
|
285
|
+
return blocked("invalid_retry_schedule", accepted, 0, anomalies);
|
|
286
|
+
}
|
|
287
|
+
if (startedAt >= retry.deadlineAtMs && remaining().length > 0) {
|
|
288
|
+
anomalies.push({
|
|
289
|
+
kind: "malformed_record",
|
|
290
|
+
entryId: null,
|
|
291
|
+
detail: "the deadline passed before the group was written",
|
|
292
|
+
});
|
|
293
|
+
return blocked("deadline_expired", accepted, 0, anomalies);
|
|
294
|
+
}
|
|
295
|
+
if (ports.isStateRemoved()) return blocked("state_removed", accepted, 0, anomalies);
|
|
296
|
+
if (!ports.isOriginCurrent()) return blocked("origin_changed", accepted, 0, anomalies);
|
|
297
|
+
|
|
298
|
+
/** Every guard a write must clear, rechecked immediately before it. */
|
|
299
|
+
const writeBlocked = (): ContinuityPersistBlockedReason | null => {
|
|
300
|
+
const now = clock.read();
|
|
301
|
+
if (now === null) return "invalid_retry_schedule";
|
|
302
|
+
if (now >= retry.deadlineAtMs) return "deadline_expired";
|
|
303
|
+
if (ports.isStateRemoved()) return "state_removed";
|
|
304
|
+
if (!ports.isOriginCurrent()) return "origin_changed";
|
|
305
|
+
return null;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
for (const entry of request.entries) {
|
|
309
|
+
if (acceptedIds.has(entry.turnId)) continue;
|
|
310
|
+
// Rechecked per record: a slow append can cross the deadline, and a long
|
|
311
|
+
// group can straddle a removal or a branch switch.
|
|
312
|
+
const guard = writeBlocked();
|
|
313
|
+
if (guard) {
|
|
314
|
+
anomalies.push({ kind: "malformed_record", entryId: entry.turnId, detail: `write refused: ${guard}` });
|
|
315
|
+
return blocked(guard, accepted, 0, anomalies);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const digest = continuityRecordDigest(entry);
|
|
319
|
+
const accept = (): void => {
|
|
320
|
+
accepted.push({ entryId: entry.turnId, payloadDigest: digest });
|
|
321
|
+
acceptedIds.add(entry.turnId);
|
|
322
|
+
};
|
|
323
|
+
try {
|
|
324
|
+
ports.append(entry);
|
|
325
|
+
accept();
|
|
326
|
+
continue;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
anomalies.push({
|
|
329
|
+
kind: "malformed_record",
|
|
330
|
+
entryId: entry.turnId,
|
|
331
|
+
detail: `append threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const reconciled = reconcileContinuityEntry(entry, ports);
|
|
336
|
+
if (reconciled.status === "accepted") {
|
|
337
|
+
accept();
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (reconciled.status === "conflicting") {
|
|
341
|
+
anomalies.push({
|
|
342
|
+
kind: "duplicate_conflict",
|
|
343
|
+
entryId: entry.turnId,
|
|
344
|
+
detail: reconciled.detail ?? "stored record has this id and different payload",
|
|
345
|
+
});
|
|
346
|
+
return blocked("append_conflicting", accepted, 0, anomalies);
|
|
347
|
+
}
|
|
348
|
+
if (reconciled.status === "unresolved") {
|
|
349
|
+
return blocked("append_unresolved", accepted, 0, anomalies);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Proven absent, so the original id may be written once more. The readback
|
|
353
|
+
// itself can have observed or caused a removal, so the guards run again
|
|
354
|
+
// before the re-append.
|
|
355
|
+
const reguard = writeBlocked();
|
|
356
|
+
if (reguard) {
|
|
357
|
+
anomalies.push({ kind: "malformed_record", entryId: entry.turnId, detail: `reappend refused: ${reguard}` });
|
|
358
|
+
return blocked(reguard, accepted, 0, anomalies);
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
ports.append(entry);
|
|
362
|
+
accept();
|
|
363
|
+
} catch (error) {
|
|
364
|
+
const second = reconcileContinuityEntry(entry, ports);
|
|
365
|
+
if (second.status === "accepted") {
|
|
366
|
+
accept();
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
anomalies.push({
|
|
370
|
+
kind: "malformed_record",
|
|
371
|
+
entryId: entry.turnId,
|
|
372
|
+
detail: `reappend threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
373
|
+
});
|
|
374
|
+
return blocked(
|
|
375
|
+
second.status === "absent" ? "append_absent_after_retry" : "append_unresolved",
|
|
376
|
+
accepted,
|
|
377
|
+
0,
|
|
378
|
+
anomalies,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (ports.isStateRemoved()) return blocked("state_removed", accepted, 0, anomalies);
|
|
384
|
+
if (!ports.isOriginCurrent()) return blocked("origin_changed", accepted, 0, anomalies);
|
|
385
|
+
|
|
386
|
+
const barrier = await runBarrier(request, ports, retry, clock);
|
|
387
|
+
if (!barrier.ok) {
|
|
388
|
+
anomalies.push({
|
|
389
|
+
kind: "malformed_record",
|
|
390
|
+
entryId: null,
|
|
391
|
+
detail: `${request.barrier.kind} barrier did not complete: ${barrier.detail}`,
|
|
392
|
+
});
|
|
393
|
+
return blocked(barrier.reason, accepted, barrier.attempts, anomalies);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Both checks run again: an asynchronous checkpoint can resolve after the
|
|
397
|
+
// state root was removed or the origin moved, and resolution alone never
|
|
398
|
+
// licenses execution or recreation of deleted state.
|
|
399
|
+
if (ports.isStateRemoved()) return blocked("state_removed", accepted, barrier.attempts, anomalies);
|
|
400
|
+
if (!ports.isOriginCurrent()) return blocked("origin_changed", accepted, barrier.attempts, anomalies);
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
status: "durable",
|
|
404
|
+
accepted,
|
|
405
|
+
barrierAttempts: barrier.attempts,
|
|
406
|
+
confirmedWithinDeadline: barrier.confirmedWithinDeadline,
|
|
407
|
+
};
|
|
408
|
+
}
|