@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.88
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/CHANGELOG.md +132 -0
- package/LICENSE +1 -1
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +3 -3
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +12 -12
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +293 -7
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +3 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +151 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +134 -16
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1295 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/symbol-tools.js +372 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2157 -214
- package/dist/core/repl/slash-commands.js +533 -40
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +298 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +36 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4203 -493
- package/dist/runtime/commands/agents.js +30 -30
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +73 -39
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +244 -13
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +288 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +624 -46
- package/dist/tools/brief.js +224 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-chips.js +257 -0
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +332 -54
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +124 -44
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +25 -7
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pugi CLI dual-write client — Memory Phase 1 (2026-05-27, ).
|
|
3
|
+
*
|
|
4
|
+
* Promotes the per-session NDJSON event log (Memory Phase 0, PR)
|
|
5
|
+
* to a best-effort Prisma row store via the admin-api endpoint:
|
|
6
|
+
*
|
|
7
|
+
* POST /api/pugi/sessions/:sessionId/events
|
|
8
|
+
*
|
|
9
|
+
* Design contract:
|
|
10
|
+
*
|
|
11
|
+
* 1. Local NDJSON is the SOURCE OF TRUTH. The dual-write client is
|
|
12
|
+
* fire-and-forget — a network failure NEVER blocks the local
|
|
13
|
+
* append path, and the operator never loses data because the
|
|
14
|
+
* .pugi/sessions/<id>/events.<n>.jsonl file is always written
|
|
15
|
+
* first by the SessionStore.
|
|
16
|
+
*
|
|
17
|
+
* 2. Async + debounced. Events are buffered in memory and flushed on
|
|
18
|
+
* a timer (default 250 ms) or when the buffer crosses the batch
|
|
19
|
+
* cap (default 50). The debounce + buffer keeps the network cost
|
|
20
|
+
* proportional to operator activity, not per-event.
|
|
21
|
+
*
|
|
22
|
+
* 3. Retry with backoff. A failed flush retries up to 3 times with
|
|
23
|
+
* exponential backoff (250 ms / 750 ms / 2250 ms). Failure beyond
|
|
24
|
+
* that drops the batch; the next flush brings in the backlog from
|
|
25
|
+
* the local NDJSON via the resume path.
|
|
26
|
+
*
|
|
27
|
+
* 4. Resume marker. The client tracks `lastSyncedSeq` per session in
|
|
28
|
+
* `~/.pugi/memory-sync/<sessionId>.json`. On `flushBacklog()` it
|
|
29
|
+
* reads any events from the local NDJSON whose seq > lastSyncedSeq
|
|
30
|
+
* and posts them in order. The marker is updated only on a
|
|
31
|
+
* successful POST so a crash mid-flush retries the unsent batch.
|
|
32
|
+
*
|
|
33
|
+
* 5. Kill switch. `PUGI_MEMORY_PHASE1_PRISMA_PERSIST_ENABLED=false`
|
|
34
|
+
* shuts the dual-write off entirely (env var, no settings file
|
|
35
|
+
* round-trip). The client is also a no-op when no credentials are
|
|
36
|
+
* available — anon CLI sessions never POST.
|
|
37
|
+
*
|
|
38
|
+
* 6. Schema contract. The kinds mirror
|
|
39
|
+
* `apps/admin-api/src/pugi-session-events/pugi-session-events.types.ts`
|
|
40
|
+
* PUGI_SESSION_EVENT_KINDS. The CLI's broader NDJSON kind set
|
|
41
|
+
* (`user`, `persona`, `rewind-marker`, etc.) is REMAPPED to the
|
|
42
|
+
* Phase-1 closed set inside `eventToPhase1()` — adding a new
|
|
43
|
+
* Phase-1 kind requires a coordinated CLI + server bump.
|
|
44
|
+
*
|
|
45
|
+
* The client carries no Nest / Prisma deps — pure node:fs + global
|
|
46
|
+
* fetch so it boots inside the CLI's REPL workflow without dragging in
|
|
47
|
+
* the admin-api graph.
|
|
48
|
+
*/
|
|
49
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
50
|
+
import { homedir } from 'node:os';
|
|
51
|
+
import { resolve } from 'node:path';
|
|
52
|
+
import { PUGI_SESSION_EVENT_KINDS, } from './phase1-kinds.js';
|
|
53
|
+
/* ------------------------------------------------------------------ */
|
|
54
|
+
/* Constants */
|
|
55
|
+
/* ------------------------------------------------------------------ */
|
|
56
|
+
export const DEFAULT_DEBOUNCE_MS = 250;
|
|
57
|
+
export const DEFAULT_MAX_BATCH_SIZE = 50;
|
|
58
|
+
export const DEFAULT_MAX_RETRIES = 3;
|
|
59
|
+
/** Server-side hard ceiling (mirror of admin-api MAX_BATCH_EVENTS). */
|
|
60
|
+
const SERVER_MAX_BATCH_EVENTS = 200;
|
|
61
|
+
/** Env kill-switch. Unset / 'true' / '1' = enabled. 'false' / '0' = off. */
|
|
62
|
+
export function isDualWriteEnabledFromEnv(env = process.env) {
|
|
63
|
+
const raw = env.PUGI_MEMORY_PHASE1_PRISMA_PERSIST_ENABLED;
|
|
64
|
+
if (raw === undefined || raw === '')
|
|
65
|
+
return true;
|
|
66
|
+
const v = raw.toLowerCase().trim();
|
|
67
|
+
return v !== 'false' && v !== '0' && v !== 'off' && v !== 'no';
|
|
68
|
+
}
|
|
69
|
+
/** Env override for debounce (ms). Falls back to DEFAULT_DEBOUNCE_MS. */
|
|
70
|
+
export function debounceMsFromEnv(env = process.env) {
|
|
71
|
+
const raw = env.PUGI_MEMORY_DUAL_WRITE_DEBOUNCE_MS;
|
|
72
|
+
if (!raw)
|
|
73
|
+
return DEFAULT_DEBOUNCE_MS;
|
|
74
|
+
const n = Number.parseInt(raw, 10);
|
|
75
|
+
if (!Number.isFinite(n) || n < 0 || n > 60_000)
|
|
76
|
+
return DEFAULT_DEBOUNCE_MS;
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
export function defaultStateDir(home = homedir()) {
|
|
80
|
+
return resolve(home, '.pugi', 'memory-sync');
|
|
81
|
+
}
|
|
82
|
+
export function syncStatePath(sessionId, stateDir) {
|
|
83
|
+
return resolve(stateDir, `${sessionId}.json`);
|
|
84
|
+
}
|
|
85
|
+
export function readSyncState(sessionId, stateDir = defaultStateDir()) {
|
|
86
|
+
const p = syncStatePath(sessionId, stateDir);
|
|
87
|
+
if (!existsSync(p))
|
|
88
|
+
return 0;
|
|
89
|
+
try {
|
|
90
|
+
const raw = readFileSync(p, 'utf-8');
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
if (parsed.schema === 1 &&
|
|
93
|
+
typeof parsed.lastSyncedSeq === 'number' &&
|
|
94
|
+
Number.isInteger(parsed.lastSyncedSeq) &&
|
|
95
|
+
parsed.lastSyncedSeq >= 0) {
|
|
96
|
+
return parsed.lastSyncedSeq;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Best effort — a corrupt state file means "resync from zero".
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
export function writeSyncState(sessionId, lastSyncedSeq, stateDir = defaultStateDir()) {
|
|
106
|
+
if (!existsSync(stateDir)) {
|
|
107
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
108
|
+
}
|
|
109
|
+
const p = syncStatePath(sessionId, stateDir);
|
|
110
|
+
const record = {
|
|
111
|
+
schema: 1,
|
|
112
|
+
sessionId,
|
|
113
|
+
lastSyncedSeq,
|
|
114
|
+
updatedAt: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
// Atomic write: temp + rename so a crash mid-write leaves either the
|
|
117
|
+
// old contents or the new — never a partial JSON document.
|
|
118
|
+
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
119
|
+
writeFileSync(tmp, JSON.stringify(record), { encoding: 'utf-8', mode: 0o600 });
|
|
120
|
+
renameSync(tmp, p);
|
|
121
|
+
}
|
|
122
|
+
/* ------------------------------------------------------------------ */
|
|
123
|
+
/* DualWriteClient */
|
|
124
|
+
/* ------------------------------------------------------------------ */
|
|
125
|
+
/**
|
|
126
|
+
* Compute the next backoff delay (ms). Exponential growth from the
|
|
127
|
+
* debounce base — 1× / 3× / 9× — keeps the retry storm bounded.
|
|
128
|
+
*/
|
|
129
|
+
export function nextBackoffMs(attempt, base) {
|
|
130
|
+
// attempt 1 -> base, attempt 2 -> 3×base, attempt 3 -> 9×base.
|
|
131
|
+
return base * Math.pow(3, Math.max(0, attempt - 1));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Tiny await-able sleeper. Public for tests that want a deterministic
|
|
135
|
+
* scheduler — production callers go through the internal debounce
|
|
136
|
+
* timer, not this helper.
|
|
137
|
+
*/
|
|
138
|
+
function sleep(ms, scheduler = setTimeout) {
|
|
139
|
+
return new Promise((res) => scheduler(res, Math.max(0, ms)));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Buffered, debounced, retry-capable dual-write client.
|
|
143
|
+
*
|
|
144
|
+
* One instance per active session. The CLI's SessionStore creates one
|
|
145
|
+
* at `openSession()` and disposes it at `archive()` / `process.exit()`.
|
|
146
|
+
* The instance retains its own buffer + flushing promise so concurrent
|
|
147
|
+
* `enqueue()` calls from multiple producers (REPL turn + subagent
|
|
148
|
+
* dispatcher) coalesce into the same batch.
|
|
149
|
+
*/
|
|
150
|
+
export class DualWriteClient {
|
|
151
|
+
cfg;
|
|
152
|
+
buffer = [];
|
|
153
|
+
flushTimer = null;
|
|
154
|
+
/**
|
|
155
|
+
* Active flush promise (null when idle). Re-using the in-flight
|
|
156
|
+
* promise lets `enqueue` callers tail-chain a pending flush instead
|
|
157
|
+
* of stacking N flushes when the CLI is in a hot loop.
|
|
158
|
+
*/
|
|
159
|
+
flushing = null;
|
|
160
|
+
/** Disposed clients reject further enqueues quietly. */
|
|
161
|
+
disposed = false;
|
|
162
|
+
/** Track the highest server-acknowledged seq. */
|
|
163
|
+
highestSyncedSeq;
|
|
164
|
+
constructor(config) {
|
|
165
|
+
const stateDir = config.stateDir ?? defaultStateDir();
|
|
166
|
+
this.cfg = {
|
|
167
|
+
apiUrl: config.apiUrl.replace(/\/+$/, ''),
|
|
168
|
+
apiKey: config.apiKey,
|
|
169
|
+
sessionId: config.sessionId,
|
|
170
|
+
debounceMs: config.debounceMs ?? debounceMsFromEnv(),
|
|
171
|
+
maxBatchSize: Math.min(config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE, SERVER_MAX_BATCH_EVENTS),
|
|
172
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
173
|
+
fetchImpl: config.fetchImpl ?? globalThis.fetch,
|
|
174
|
+
stateDir,
|
|
175
|
+
};
|
|
176
|
+
this.highestSyncedSeq = readSyncState(config.sessionId, stateDir);
|
|
177
|
+
}
|
|
178
|
+
/** Currently-known synced seq (read-through for callers). */
|
|
179
|
+
get lastSyncedSeq() {
|
|
180
|
+
return this.highestSyncedSeq;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Buffer an event for the next flush. Returns immediately — the
|
|
184
|
+
* actual POST happens on the debounce timer.
|
|
185
|
+
*
|
|
186
|
+
* The producer (SessionStore) is expected to have ALREADY written
|
|
187
|
+
* the local NDJSON line before calling this. The dual-write client
|
|
188
|
+
* never touches the NDJSON itself.
|
|
189
|
+
*/
|
|
190
|
+
enqueue(event) {
|
|
191
|
+
if (this.disposed)
|
|
192
|
+
return;
|
|
193
|
+
if (!isValidPhase1Kind(event.kind)) {
|
|
194
|
+
// Quietly ignore events the Phase-1 schema cannot represent. The
|
|
195
|
+
// local NDJSON keeps them; a future Phase-1.1 may widen the kind
|
|
196
|
+
// set.
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!Number.isInteger(event.seq) || event.seq <= 0)
|
|
200
|
+
return;
|
|
201
|
+
this.buffer.push(event);
|
|
202
|
+
// Crossed the batch cap — flush eagerly to bound buffer growth.
|
|
203
|
+
if (this.buffer.length >= this.cfg.maxBatchSize) {
|
|
204
|
+
this.scheduleFlush(0);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.scheduleFlush(this.cfg.debounceMs);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Force any pending events out. Used at session archive / shutdown
|
|
211
|
+
* so the trailing batch lands before the process exits.
|
|
212
|
+
*/
|
|
213
|
+
async flush() {
|
|
214
|
+
if (this.flushTimer) {
|
|
215
|
+
clearTimeout(this.flushTimer);
|
|
216
|
+
this.flushTimer = null;
|
|
217
|
+
}
|
|
218
|
+
return this.doFlush();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Mark the client disposed. Subsequent enqueues are no-ops. Pending
|
|
222
|
+
* flush is awaited so the caller can `await client.dispose()` to
|
|
223
|
+
* fence completion.
|
|
224
|
+
*/
|
|
225
|
+
async dispose() {
|
|
226
|
+
this.disposed = true;
|
|
227
|
+
if (this.flushTimer) {
|
|
228
|
+
clearTimeout(this.flushTimer);
|
|
229
|
+
this.flushTimer = null;
|
|
230
|
+
}
|
|
231
|
+
if (this.flushing) {
|
|
232
|
+
try {
|
|
233
|
+
await this.flushing;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// ignore
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Backlog catch-up. The caller passes the local NDJSON's events
|
|
242
|
+
* (already filtered to the session) — the client posts any whose
|
|
243
|
+
* seq > lastSyncedSeq, in order, in batches of maxBatchSize.
|
|
244
|
+
*
|
|
245
|
+
* Returns the aggregate FlushResult across all batches (sums
|
|
246
|
+
* persisted + duplicate counts, lastSeq is the max).
|
|
247
|
+
*/
|
|
248
|
+
async flushBacklog(allEvents) {
|
|
249
|
+
const pending = allEvents
|
|
250
|
+
.filter((e) => e.seq > this.highestSyncedSeq && isValidPhase1Kind(e.kind))
|
|
251
|
+
.sort((a, b) => a.seq - b.seq);
|
|
252
|
+
if (pending.length === 0) {
|
|
253
|
+
return {
|
|
254
|
+
persistedCount: 0,
|
|
255
|
+
duplicateCount: 0,
|
|
256
|
+
lastSeq: this.highestSyncedSeq,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
let totalPersisted = 0;
|
|
260
|
+
let totalDuplicates = 0;
|
|
261
|
+
let maxLastSeq = this.highestSyncedSeq;
|
|
262
|
+
for (let i = 0; i < pending.length; i += this.cfg.maxBatchSize) {
|
|
263
|
+
const batch = pending.slice(i, i + this.cfg.maxBatchSize);
|
|
264
|
+
const result = await this.postBatchWithRetry(batch);
|
|
265
|
+
if (!result)
|
|
266
|
+
break; // surrender — backlog stays for next session
|
|
267
|
+
totalPersisted += result.persistedCount;
|
|
268
|
+
totalDuplicates += result.duplicateCount;
|
|
269
|
+
maxLastSeq = Math.max(maxLastSeq, result.lastSeq);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
persistedCount: totalPersisted,
|
|
273
|
+
duplicateCount: totalDuplicates,
|
|
274
|
+
lastSeq: maxLastSeq,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/* ---------------- internal ---------------- */
|
|
278
|
+
scheduleFlush(delay) {
|
|
279
|
+
if (this.flushTimer)
|
|
280
|
+
clearTimeout(this.flushTimer);
|
|
281
|
+
this.flushTimer = setTimeout(() => {
|
|
282
|
+
this.flushTimer = null;
|
|
283
|
+
// doFlush handles its own error swallowing; we drop the floating
|
|
284
|
+
// promise on the floor on purpose so the timer callback returns
|
|
285
|
+
// synchronously (Node requires that).
|
|
286
|
+
void this.doFlush();
|
|
287
|
+
}, delay);
|
|
288
|
+
}
|
|
289
|
+
async doFlush() {
|
|
290
|
+
if (this.flushing)
|
|
291
|
+
return this.flushing;
|
|
292
|
+
if (this.buffer.length === 0)
|
|
293
|
+
return null;
|
|
294
|
+
const drained = this.buffer.splice(0, this.buffer.length);
|
|
295
|
+
this.flushing = this.postBatchWithRetry(drained);
|
|
296
|
+
try {
|
|
297
|
+
return await this.flushing;
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
this.flushing = null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async postBatchWithRetry(batch) {
|
|
304
|
+
if (batch.length === 0)
|
|
305
|
+
return null;
|
|
306
|
+
// Sort + within-batch dedup (defence in depth — the server also
|
|
307
|
+
// enforces monotonic seq + dedup, but enforcing here keeps the
|
|
308
|
+
// wire payload clean and avoids the 400 on a buggy producer).
|
|
309
|
+
const sorted = [...batch].sort((a, b) => a.seq - b.seq);
|
|
310
|
+
const deduped = [];
|
|
311
|
+
let prevSeq = -1;
|
|
312
|
+
for (const ev of sorted) {
|
|
313
|
+
if (ev.seq === prevSeq)
|
|
314
|
+
continue;
|
|
315
|
+
deduped.push(ev);
|
|
316
|
+
prevSeq = ev.seq;
|
|
317
|
+
}
|
|
318
|
+
for (let attempt = 1; attempt <= this.cfg.maxRetries; attempt++) {
|
|
319
|
+
try {
|
|
320
|
+
const result = await this.doPost(deduped);
|
|
321
|
+
// Persist the lastSyncedSeq marker so a crash before the next
|
|
322
|
+
// flush still rediscovers the high-water-mark on restart.
|
|
323
|
+
if (result.lastSeq > this.highestSyncedSeq) {
|
|
324
|
+
this.highestSyncedSeq = result.lastSeq;
|
|
325
|
+
try {
|
|
326
|
+
writeSyncState(this.cfg.sessionId, this.highestSyncedSeq, this.cfg.stateDir);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Best effort — a marker write failure does not invalidate
|
|
330
|
+
// the on-server data, only the resume hint.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
if (attempt === this.cfg.maxRetries) {
|
|
337
|
+
// Final surrender — drop the batch silently. Local NDJSON is
|
|
338
|
+
// authoritative; the next session resume can replay the gap.
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
await sleep(nextBackoffMs(attempt, this.cfg.debounceMs));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
async doPost(events) {
|
|
347
|
+
const url = `${this.cfg.apiUrl}/api/pugi/sessions/${encodeURIComponent(this.cfg.sessionId)}/events`;
|
|
348
|
+
const res = await this.cfg.fetchImpl(url, {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: {
|
|
351
|
+
authorization: `Bearer ${this.cfg.apiKey}`,
|
|
352
|
+
'content-type': 'application/json',
|
|
353
|
+
accept: 'application/json',
|
|
354
|
+
},
|
|
355
|
+
body: JSON.stringify({ events }),
|
|
356
|
+
});
|
|
357
|
+
if (!res.ok) {
|
|
358
|
+
throw new Error(`dual-write POST ${url} returned ${res.status} ${res.statusText}`);
|
|
359
|
+
}
|
|
360
|
+
const body = (await res.json());
|
|
361
|
+
return {
|
|
362
|
+
persistedCount: body.persistedCount ?? 0,
|
|
363
|
+
duplicateCount: body.duplicateCount ?? 0,
|
|
364
|
+
lastSeq: body.lastSeq ?? 0,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** Validate a kind string against the Phase-1 closed set. */
|
|
369
|
+
export function isValidPhase1Kind(value) {
|
|
370
|
+
return PUGI_SESSION_EVENT_KINDS.includes(value);
|
|
371
|
+
}
|
|
372
|
+
/* ------------------------------------------------------------------ */
|
|
373
|
+
/* Kind remap (CLI broad set -> Phase-1 closed set) */
|
|
374
|
+
/* ------------------------------------------------------------------ */
|
|
375
|
+
/**
|
|
376
|
+
* Map the CLI's broader NDJSON kind set to the Phase-1 server-side set.
|
|
377
|
+
* Returns `null` when the kind has no Phase-1 representation — the
|
|
378
|
+
* caller should drop that event from the dual-write (the local NDJSON
|
|
379
|
+
* still carries it for offline analysis).
|
|
380
|
+
*
|
|
381
|
+
* The CLI's kind set lives in
|
|
382
|
+
* `apps/pugi-cli/src/core/repl/store/types.ts`:
|
|
383
|
+
*
|
|
384
|
+
* 'user' -> 'turn.user'
|
|
385
|
+
* 'persona' -> 'turn.assistant'
|
|
386
|
+
* 'system' -> 'system'
|
|
387
|
+
* 'tool.start' -> 'tool.call'
|
|
388
|
+
* 'tool.result' -> 'tool.result'
|
|
389
|
+
* 'agent.spawned' -> 'dispatch.start'
|
|
390
|
+
* 'agent.completed'-> 'dispatch.end'
|
|
391
|
+
* 'compaction' -> 'compact.boundary'
|
|
392
|
+
* 'rewind-marker' -> null (Phase-1 has no analog yet)
|
|
393
|
+
*/
|
|
394
|
+
export function cliKindToPhase1(cliKind) {
|
|
395
|
+
switch (cliKind) {
|
|
396
|
+
case 'user':
|
|
397
|
+
return 'turn.user';
|
|
398
|
+
case 'persona':
|
|
399
|
+
return 'turn.assistant';
|
|
400
|
+
case 'system':
|
|
401
|
+
return 'system';
|
|
402
|
+
case 'tool.start':
|
|
403
|
+
return 'tool.call';
|
|
404
|
+
case 'tool.result':
|
|
405
|
+
return 'tool.result';
|
|
406
|
+
case 'agent.spawned':
|
|
407
|
+
return 'dispatch.start';
|
|
408
|
+
case 'agent.completed':
|
|
409
|
+
return 'dispatch.end';
|
|
410
|
+
case 'compaction':
|
|
411
|
+
return 'compact.boundary';
|
|
412
|
+
default:
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
//# sourceMappingURL=dual-write.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { estimateTokens } from '../compact/token-counter.js';
|
|
2
|
+
const VALID_KINDS = new Set([
|
|
3
|
+
'preference',
|
|
4
|
+
'decision',
|
|
5
|
+
'context',
|
|
6
|
+
'fact',
|
|
7
|
+
]);
|
|
8
|
+
const DEFAULT_MAX_ITEMS = 8;
|
|
9
|
+
const DEFAULT_MIN_CONFIDENCE = 0.6;
|
|
10
|
+
const DEFAULT_MAX_INPUT_TOKENS = 6000;
|
|
11
|
+
function genId() {
|
|
12
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
13
|
+
return `mem-${rand}`;
|
|
14
|
+
}
|
|
15
|
+
function truncateTurns(turns, maxInputTokens) {
|
|
16
|
+
if (turns.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
const kept = [];
|
|
19
|
+
let total = 0;
|
|
20
|
+
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
21
|
+
const turn = turns[i];
|
|
22
|
+
if (!turn)
|
|
23
|
+
continue;
|
|
24
|
+
const cost = estimateTokens(`${turn.role}: ${turn.content}`);
|
|
25
|
+
if (total + cost > maxInputTokens && kept.length > 0) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
kept.unshift(turn);
|
|
29
|
+
total += cost;
|
|
30
|
+
}
|
|
31
|
+
return kept;
|
|
32
|
+
}
|
|
33
|
+
function buildPrompt(turns) {
|
|
34
|
+
const dialog = turns
|
|
35
|
+
.map((t) => `[${t.id}] ${t.role}: ${t.content}`)
|
|
36
|
+
.join('\n');
|
|
37
|
+
return [
|
|
38
|
+
'You extract persistable memory items from a dialog between a user and an assistant.',
|
|
39
|
+
'Return strict JSON with shape:',
|
|
40
|
+
'{ "items": [ { "kind": "preference"|"decision"|"context"|"fact", "body": "1-2 sentences", "confidence": 0..1, "sourceTurn": "<turn id>" } ] }',
|
|
41
|
+
'Rules:',
|
|
42
|
+
'- Only include facts worth remembering across future sessions.',
|
|
43
|
+
'- Skip greetings, small talk, transient state, and tool noise.',
|
|
44
|
+
'- Confidence reflects how clearly the dialog supports the fact.',
|
|
45
|
+
'- Never invent facts that are not present in the dialog.',
|
|
46
|
+
'- If nothing is worth keeping, return { "items": [] }.',
|
|
47
|
+
'Dialog:',
|
|
48
|
+
dialog,
|
|
49
|
+
'Respond with JSON only.',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
function extractJsonObject(raw) {
|
|
53
|
+
const trimmed = raw.trim();
|
|
54
|
+
const start = trimmed.indexOf('{');
|
|
55
|
+
const end = trimmed.lastIndexOf('}');
|
|
56
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
57
|
+
throw new Error('no-json');
|
|
58
|
+
}
|
|
59
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
60
|
+
}
|
|
61
|
+
function isRecord(value) {
|
|
62
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
function validateItem(value) {
|
|
65
|
+
if (!isRecord(value))
|
|
66
|
+
return null;
|
|
67
|
+
const { kind, body, confidence, sourceTurn } = value;
|
|
68
|
+
if (typeof kind !== 'string' || !VALID_KINDS.has(kind)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (typeof body !== 'string' || body.trim().length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (typeof confidence !== 'number' || Number.isNaN(confidence)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (confidence < 0 || confidence > 1) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const out = {
|
|
81
|
+
kind: kind,
|
|
82
|
+
body: body.trim(),
|
|
83
|
+
confidence,
|
|
84
|
+
};
|
|
85
|
+
if (typeof sourceTurn === 'string' && sourceTurn.length > 0) {
|
|
86
|
+
out.sourceTurn = sourceTurn;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
export async function extractPassiveMemory(options) {
|
|
91
|
+
const { transport, dialogTurns, maxItems = DEFAULT_MAX_ITEMS, minConfidence = DEFAULT_MIN_CONFIDENCE, maxInputTokens = DEFAULT_MAX_INPUT_TOKENS, } = options;
|
|
92
|
+
if (dialogTurns.length === 0) {
|
|
93
|
+
return { items: [], usedFallback: false };
|
|
94
|
+
}
|
|
95
|
+
const truncated = truncateTurns(dialogTurns, maxInputTokens);
|
|
96
|
+
const prompt = buildPrompt(truncated);
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = await transport(prompt);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { items: [], usedFallback: true, reason: 'transport-error' };
|
|
103
|
+
}
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = extractJsonObject(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return { items: [], usedFallback: true, reason: 'parse-failed' };
|
|
110
|
+
}
|
|
111
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.items)) {
|
|
112
|
+
return { items: [], usedFallback: true, reason: 'parse-failed' };
|
|
113
|
+
}
|
|
114
|
+
const validated = [];
|
|
115
|
+
for (const candidate of parsed.items) {
|
|
116
|
+
const item = validateItem(candidate);
|
|
117
|
+
if (!item)
|
|
118
|
+
continue;
|
|
119
|
+
if (item.confidence < minConfidence)
|
|
120
|
+
continue;
|
|
121
|
+
validated.push({ id: genId(), ...item });
|
|
122
|
+
}
|
|
123
|
+
if (validated.length === 0) {
|
|
124
|
+
return { items: [], usedFallback: true, reason: 'no-signal' };
|
|
125
|
+
}
|
|
126
|
+
validated.sort((a, b) => b.confidence - a.confidence);
|
|
127
|
+
const capped = validated.slice(0, maxItems);
|
|
128
|
+
return { items: capped, usedFallback: false };
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=passive-extract.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-1 event kind closed set — mirror of
|
|
3
|
+
* `apps/admin-api/src/pugi-session-events/pugi-session-events.types.ts`
|
|
4
|
+
* PUGI_SESSION_EVENT_KINDS.
|
|
5
|
+
*
|
|
6
|
+
* Kept as a separate file so the CLI does not import from admin-api
|
|
7
|
+
* (would drag the whole Nest graph). The TWO lists must stay in
|
|
8
|
+
* lockstep — a new kind requires editing both files.
|
|
9
|
+
*/
|
|
10
|
+
export const PUGI_SESSION_EVENT_KINDS = [
|
|
11
|
+
'turn.user',
|
|
12
|
+
'turn.assistant',
|
|
13
|
+
'tool.call',
|
|
14
|
+
'tool.result',
|
|
15
|
+
'dispatch.start',
|
|
16
|
+
'dispatch.end',
|
|
17
|
+
'compact.boundary',
|
|
18
|
+
'system',
|
|
19
|
+
];
|
|
20
|
+
//# sourceMappingURL=phase1-kinds.js.map
|