mixdog 0.9.62 → 0.9.64
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/README.md +3 -0
- package/package.json +9 -2
- package/scripts/code-graph-description-contract.mjs +3 -3
- package/scripts/verify-release-assets.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +11 -14
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +19 -124
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -6
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +2 -2
- package/src/runtime/agent/orchestrator/internal-agents.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +333 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -277
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +196 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +460 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +14 -593
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -280
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +357 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +216 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +2 -188
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +18 -26
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +260 -0
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +72 -0
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +163 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -561
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -154
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/cwd-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-locks.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +519 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +5 -449
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +212 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +1 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +9 -36
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +429 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -395
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/mutation-planner.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/next-call-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +72 -9
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -444
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +505 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +1 -1
- package/src/runtime/channels/lib/backend-dispatch.mjs +20 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +1 -1
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +4 -4
- package/src/runtime/channels/tool-defs.mjs +2 -3
- package/src/runtime/memory/index.mjs +1 -5
- package/src/runtime/memory/lib/core-memory-candidates.mjs +357 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +7 -357
- package/src/runtime/memory/lib/cycle-signatures.mjs +1 -1
- package/src/runtime/memory/lib/embedding-model-config.mjs +1 -1
- package/src/runtime/memory/lib/embedding-provider.mjs +1 -5
- package/src/runtime/memory/lib/embedding-worker.mjs +1 -5
- package/src/runtime/memory/lib/ko-morph.mjs +2 -2
- package/src/runtime/memory/lib/memory-config-flags.mjs +2 -5
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +3 -7
- package/src/runtime/memory/lib/memory-cycle1.mjs +4 -8
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +2 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -5
- package/src/runtime/memory/lib/memory-embed.mjs +2 -6
- package/src/runtime/memory/lib/memory-log.mjs +8 -0
- package/src/runtime/memory/lib/memory-ops-policy.mjs +4 -8
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-store.mjs +2 -193
- package/src/runtime/memory/lib/memory-score.mjs +1 -1
- package/src/runtime/memory/lib/memory-text-utils.mjs +2 -2
- package/src/runtime/memory/lib/memory.mjs +2 -6
- package/src/runtime/memory/lib/pg/adapter.mjs +2 -6
- package/src/runtime/memory/lib/pg/process.mjs +1 -5
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -5
- package/src/runtime/memory/lib/recall-format.mjs +2 -2
- package/src/runtime/memory/lib/recall-scoring.mjs +195 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -5
- package/src/runtime/memory/lib/trace-store.mjs +2 -6
- package/src/runtime/search/lib/http-fetch.mjs +6 -6
- package/src/runtime/search/lib/web-tools.mjs +2 -2
- package/src/runtime/shared/buffered-appender.mjs +1 -1
- package/src/runtime/shared/child-guardian.mjs +1 -1
- package/src/runtime/shared/child-spawn-gate.mjs +1 -1
- package/src/runtime/shared/config.mjs +4 -4
- package/src/runtime/shared/launcher-control.mjs +9 -10
- package/src/runtime/shared/llm/index.mjs +5 -20
- package/src/runtime/shared/markdown-frontmatter.mjs +1 -1
- package/src/runtime/shared/memory-snapshot.mjs +4 -4
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/process-lifecycle.mjs +0 -1
- package/src/runtime/shared/process-shutdown.mjs +1 -1
- package/src/runtime/shared/resource-admission.mjs +2 -2
- package/src/runtime/shared/schedule-session-run.mjs +1 -1
- package/src/runtime/shared/schedules-db.mjs +1 -1
- package/src/runtime/shared/service-discovery.mjs +2 -2
- package/src/runtime/shared/staged-update.mjs +6 -6
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-primitives.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +2 -2
- package/src/runtime/shared/update-checker.mjs +1 -1
- package/src/runtime/shared/user-cwd.mjs +3 -3
- package/src/runtime/shared/user-data-guard.mjs +4 -4
- package/src/runtime/shared/workspace-router.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +2 -3
- package/src/session-runtime/config-lifecycle.mjs +1 -1
- package/src/session-runtime/hitch-profile.mjs +1 -1
- package/src/session-runtime/lifecycle-api.mjs +6 -4
- package/src/session-runtime/model-recency.mjs +5 -5
- package/src/session-runtime/plugin-mcp.mjs +1 -1
- package/src/session-runtime/provider-request-snapshot.mjs +338 -0
- package/src/session-runtime/session-text.mjs +4 -4
- package/src/session-runtime/tool-catalog-schema.mjs +197 -0
- package/src/session-runtime/tool-catalog.mjs +6 -483
- package/src/session-runtime/tool-defs.mjs +1 -1
- package/src/standalone/agent-task-status.mjs +1 -1
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/channel-admin.mjs +4 -4
- package/src/standalone/explore-tool.mjs +3 -3
- package/src/standalone/hook-bus/config.mjs +1 -1
- package/src/standalone/hook-bus/constants.mjs +1 -1
- package/src/standalone/hook-bus/rules.mjs +1 -1
- package/src/standalone/projects.mjs +1 -1
- package/src/standalone/provider-admin.mjs +2 -2
- package/src/tui/app/app-format.mjs +2 -2
- package/src/tui/app/clipboard.mjs +3 -3
- package/src/tui/app/input-parsers.mjs +1 -1
- package/src/tui/app/text-layout.mjs +1 -1
- package/src/tui/app/transcript-row-estimate.mjs +360 -0
- package/src/tui/app/transcript-window.mjs +5 -347
- package/src/tui/app/use-mouse-input.mjs +1 -1
- package/src/tui/app/use-prompt-handlers.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +9 -9
- package/src/tui/components/tool-execution/text-format.mjs +2 -2
- package/src/tui/components/tool-output-format.mjs +7 -7
- package/src/tui/display-width.mjs +4 -4
- package/src/tui/dist/index.mjs +63 -97
- package/src/tui/engine/agent-envelope.mjs +5 -5
- package/src/tui/engine/notification-plan.mjs +1 -1
- package/src/tui/engine/queue-helpers.mjs +3 -3
- package/src/tui/engine/render-timing.mjs +4 -5
- package/src/tui/engine/session-api-ext.mjs +3 -3
- package/src/tui/engine/tool-result-status.mjs +5 -5
- package/src/tui/engine/tool-result-text.mjs +1 -1
- package/src/tui/engine/transcript-spill.mjs +585 -0
- package/src/tui/engine/tui-steering-persist.mjs +1 -1
- package/src/tui/engine.mjs +2 -474
- package/src/tui/lib/voice-setup.mjs +1 -1
- package/src/tui/markdown/format-token.mjs +1 -1
- package/src/tui/markdown/render-ansi.mjs +1 -1
- package/src/tui/markdown/table-layout.mjs +4 -4
- package/src/tui/paste-attachments.mjs +1 -1
- package/src/tui/prompt-history-store.mjs +3 -3
- package/src/tui/statusline-ansi-bridge.mjs +2 -2
- package/src/tui/theme.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +2 -2
- package/src/ui/ansi.mjs +0 -9
- package/src/ui/statusline-format.mjs +2 -4
- package/scripts/_bench-cwc.json +0 -20
- package/scripts/_jitter-fuzz.jsx +0 -42
- package/scripts/_jitter-fuzz2.jsx +0 -30
- package/scripts/_jitter-probe.jsx +0 -35
- package/scripts/_jp2.jsx +0 -16
- package/scripts/_smoke_wd.mjs +0 -7
- package/scripts/abort-recovery-test.mjs +0 -175
- package/scripts/agent-dispatch-abort-compose-test.mjs +0 -31
- package/scripts/agent-live-arg-guard-smoke.mjs +0 -20
- package/scripts/agent-live-path-suffix-smoke.mjs +0 -34
- package/scripts/agent-live-toolcall-args-smoke.mjs +0 -45
- package/scripts/agent-loop-policy-test.mjs +0 -37
- package/scripts/agent-model-liveness-test.mjs +0 -763
- package/scripts/agent-parallel-smoke.mjs +0 -435
- package/scripts/agent-route-batch-test.mjs +0 -40
- package/scripts/agent-tag-reuse-smoke.mjs +0 -441
- package/scripts/agent-terminal-reap-test.mjs +0 -252
- package/scripts/agent-trace-io-test.mjs +0 -133
- package/scripts/ansi-color-capability-test.mjs +0 -90
- package/scripts/anthropic-admission-retry-integration-test.mjs +0 -119
- package/scripts/anthropic-maxtokens-test.mjs +0 -119
- package/scripts/anthropic-oauth-refresh-race-test.mjs +0 -397
- package/scripts/anthropic-transport-policy-test.mjs +0 -726
- package/scripts/apply-patch-edit-smoke.mjs +0 -71
- package/scripts/arg-guard-test.mjs +0 -93
- package/scripts/async-notify-settlement-test.mjs +0 -99
- package/scripts/atomic-lock-tryonce-test.mjs +0 -125
- package/scripts/background-task-meta-smoke.mjs +0 -38
- package/scripts/bench-run.mjs +0 -508
- package/scripts/boot-smoke.mjs +0 -137
- package/scripts/build-runtime-windows.ps1 +0 -242
- package/scripts/channel-daemon-smoke.mjs +0 -1103
- package/scripts/code-graph-aggregate-cwd-test.mjs +0 -158
- package/scripts/code-graph-disk-hit-test.mjs +0 -290
- package/scripts/code-graph-dispatch-test.mjs +0 -96
- package/scripts/code-graph-root-federation-test.mjs +0 -273
- package/scripts/compact-active-turn-test.mjs +0 -68
- package/scripts/compact-file-reattach-test.mjs +0 -88
- package/scripts/compact-pressure-test.mjs +0 -788
- package/scripts/compact-prior-context-flatten-test.mjs +0 -252
- package/scripts/compact-recall-digest-test.mjs +0 -57
- package/scripts/compact-smoke.mjs +0 -1017
- package/scripts/compact-trigger-migration-smoke.mjs +0 -261
- package/scripts/compacted-placeholder-scrub-test.mjs +0 -63
- package/scripts/context-mcp-metering-test.mjs +0 -1406
- package/scripts/debounced-skills-async-save-test.mjs +0 -57
- package/scripts/deferred-tool-loading-test.mjs +0 -250
- package/scripts/desktop-session-bridge-test.mjs +0 -1013
- package/scripts/dispatch-persist-recovery-test.mjs +0 -141
- package/scripts/embedding-worker-exit-test.mjs +0 -76
- package/scripts/execution-completion-dedup-test.mjs +0 -205
- package/scripts/execution-pending-resume-kick-test.mjs +0 -151
- package/scripts/execution-resume-esc-integration-test.mjs +0 -176
- package/scripts/explore-bench-tmp.mjs +0 -36
- package/scripts/explore-bench.mjs +0 -248
- package/scripts/explore-prompt-policy-test.mjs +0 -256
- package/scripts/explore-timeout-cancel-test.mjs +0 -345
- package/scripts/find-fuzzy-hidden-test.mjs +0 -267
- package/scripts/forwarder-rebind-tail-test.mjs +0 -67
- package/scripts/freevar-smoke.mjs +0 -98
- package/scripts/gemini-provider-test.mjs +0 -1448
- package/scripts/generate-oc-icons.mjs +0 -11
- package/scripts/grok-oauth-refresh-race-test.mjs +0 -273
- package/scripts/headless-pristine-execution-test.mjs +0 -614
- package/scripts/hook-bus-test.mjs +0 -552
- package/scripts/ingest-pure-conversation-smoke.mjs +0 -175
- package/scripts/internal-comms-bench-test.mjs +0 -226
- package/scripts/internal-comms-bench.mjs +0 -853
- package/scripts/internal-comms-smoke.mjs +0 -277
- package/scripts/internal-tools-normalization-test.mjs +0 -62
- package/scripts/interrupted-turn-history-test.mjs +0 -399
- package/scripts/lead-workflow-smoke.mjs +0 -592
- package/scripts/lifecycle-api-test.mjs +0 -137
- package/scripts/live-share-test.mjs +0 -148
- package/scripts/live-worker-smoke.mjs +0 -333
- package/scripts/log-writer-guard-smoke.mjs +0 -131
- package/scripts/maintenance-default-routes-test.mjs +0 -164
- package/scripts/max-output-recovery-persist-test.mjs +0 -81
- package/scripts/max-output-recovery-test.mjs +0 -368
- package/scripts/mcp-client-normalization-test.mjs +0 -45
- package/scripts/mcp-grace-deferred-test.mjs +0 -225
- package/scripts/memory-core-input-test.mjs +0 -167
- package/scripts/memory-cycle-routing-test.mjs +0 -111
- package/scripts/memory-meta-concurrency-test.mjs +0 -20
- package/scripts/memory-pg-recovery-test.mjs +0 -59
- package/scripts/memory-rule-contract-test.mjs +0 -93
- package/scripts/model-list-sanitize-test.mjs +0 -37
- package/scripts/mouse-tracking-restore-smoke.mjs +0 -107
- package/scripts/native-edit-wire-test.mjs +0 -152
- package/scripts/notify-completion-mirror-test.mjs +0 -73
- package/scripts/openai-oauth-refresh-race-test.mjs +0 -120
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +0 -954
- package/scripts/openai-ws-early-settle-test.mjs +0 -216
- package/scripts/output-style-bench.mjs +0 -292
- package/scripts/output-style-smoke.mjs +0 -145
- package/scripts/parent-abort-link-test.mjs +0 -90
- package/scripts/patch-binary-cache-test.mjs +0 -181
- package/scripts/path-suffix-test.mjs +0 -57
- package/scripts/pending-completion-drop-test.mjs +0 -239
- package/scripts/pending-messages-lock-nonblocking-test.mjs +0 -62
- package/scripts/pretool-ask-runtime-test.mjs +0 -54
- package/scripts/process-lifecycle-test.mjs +0 -461
- package/scripts/prompt-immediate-render-test.mjs +0 -89
- package/scripts/prompt-input-parity-test.mjs +0 -145
- package/scripts/provider-admission-scheduler-test.mjs +0 -681
- package/scripts/provider-contract-test.mjs +0 -632
- package/scripts/provider-stream-stall-test.mjs +0 -276
- package/scripts/provider-toolcall-test.mjs +0 -3063
- package/scripts/reactive-compact-persist-smoke.mjs +0 -203
- package/scripts/recall-bench.mjs +0 -359
- package/scripts/resource-admission-test.mjs +0 -792
- package/scripts/result-classification-test.mjs +0 -75
- package/scripts/rg-runner-test.mjs +0 -240
- package/scripts/routing-corpus-test.mjs +0 -349
- package/scripts/sanitize-tool-pairs-test.mjs +0 -260
- package/scripts/session-bench-cache-break-test.mjs +0 -102
- package/scripts/session-bench.mjs +0 -1708
- package/scripts/session-context-bench.mjs +0 -344
- package/scripts/session-heartbeat-lifecycle-test.mjs +0 -68
- package/scripts/session-ingest-compaction-smoke.mjs +0 -279
- package/scripts/session-ingest-smoke.mjs +0 -177
- package/scripts/session-orphan-sweep-test.mjs +0 -109
- package/scripts/set-effort-config-test.mjs +0 -41
- package/scripts/shell-failure-diagnostics-test.mjs +0 -280
- package/scripts/shell-hardening-test.mjs +0 -209
- package/scripts/shell-jobs-windows-hide-test.mjs +0 -164
- package/scripts/ship-mode-test.mjs +0 -98
- package/scripts/smoke-loop-failure-summary-test.mjs +0 -38
- package/scripts/smoke-loop-failure-summary.mjs +0 -16
- package/scripts/smoke-loop-report.mjs +0 -221
- package/scripts/smoke-loop.mjs +0 -202
- package/scripts/smoke-runtime-negative.ps1 +0 -106
- package/scripts/statusline-quota-hysteresis-test.mjs +0 -62
- package/scripts/steering-drain-buckets-test.mjs +0 -467
- package/scripts/steering-fold-provenance-test.mjs +0 -71
- package/scripts/streaming-tail-window-test.mjs +0 -175
- package/scripts/submit-commandbusy-race-test.mjs +0 -257
- package/scripts/task-bench.mjs +0 -205
- package/scripts/terminal-bench-isolation-guards-test.mjs +0 -102
- package/scripts/termio-input-smoke.mjs +0 -199
- package/scripts/tool-result-hook-test.mjs +0 -48
- package/scripts/tool-smoke.mjs +0 -2453
- package/scripts/tool-tui-presentation-test.mjs +0 -286
- package/scripts/toolcall-args-test.mjs +0 -239
- package/scripts/tui-ambiguous-width-test.mjs +0 -113
- package/scripts/tui-background-failure-smoke.mjs +0 -73
- package/scripts/tui-perf-run.ps1 +0 -26
- package/scripts/tui-render-smoke.mjs +0 -90
- package/scripts/tui-runtime-load-bench-entry.jsx +0 -608
- package/scripts/tui-runtime-load-bench.mjs +0 -50
- package/scripts/tui-store-frame-batch-test.mjs +0 -99
- package/scripts/tui-transcript-jitter-harness-entry.jsx +0 -383
- package/scripts/tui-transcript-perf-test.mjs +0 -934
- package/scripts/usage-metrics-epoch-smoke.mjs +0 -114
- package/scripts/verify-release-assets-test.mjs +0 -281
- package/scripts/web-fetch-routing-test.mjs +0 -158
- package/scripts/webhook-smoke.mjs +0 -201
- package/scripts/windows-hide-spawn-options-test.mjs +0 -19
- package/scripts/worker-notify-rejection-test.mjs +0 -51
- package/scripts/write-backpressure-test.mjs +0 -147
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Compaction-append collision smoke for the session-ingest RUNTIME (not just
|
|
3
|
-
// the pure helpers). Reproduces the silent-drop bug: after compaction removes an
|
|
4
|
-
// earlier identical untimestamped message from the in-memory array, a newly
|
|
5
|
-
// APPENDED identical message used to reproduce an already-persisted ordinal →
|
|
6
|
-
// same source_ref → INSERT ... ON CONFLICT DO NOTHING silently dropped it.
|
|
7
|
-
//
|
|
8
|
-
// Proves, against an in-memory fake DB that enforces the source_ref uniqueness
|
|
9
|
-
// the real ON CONFLICT relies on:
|
|
10
|
-
// (1) post-compaction appended untimestamped identical msg → NEW row;
|
|
11
|
-
// (2) full identical re-ingest in a FRESH runtime (rows already in DB) → 0 dups;
|
|
12
|
-
// (3) subset re-ingest reproduces the same source_refs as a full re-ingest.
|
|
13
|
-
import { createSessionIngestRuntime } from '../src/runtime/memory/lib/session-ingest-runtime.mjs'
|
|
14
|
-
|
|
15
|
-
function assert(condition, message) {
|
|
16
|
-
if (!condition) throw new Error(message)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Minimal fake DB. Only the two statements the ingest loop issues are modelled:
|
|
20
|
-
// • SELECT COALESCE(MAX(source_turn) ...) → per-session max
|
|
21
|
-
// • INSERT INTO entries(...) ON CONFLICT DO NOTHING → dedupe on source_ref
|
|
22
|
-
// Any other query (the post-ingest raw-embedding flush's schema calls) returns
|
|
23
|
-
// an empty result; flushRawEmbeddings then hits `db._pool` (undefined) and its
|
|
24
|
-
// error is swallowed by the runtime, so the smoke never blocks on embeddings.
|
|
25
|
-
function createFakeDb() {
|
|
26
|
-
const rows = [] // { ts, role, content, source_ref, session_id, source_turn, project_id }
|
|
27
|
-
const bySourceRef = new Set()
|
|
28
|
-
let nextId = 1
|
|
29
|
-
return {
|
|
30
|
-
rows,
|
|
31
|
-
async query(sql, params = []) {
|
|
32
|
-
if (/MAX\(source_turn\)/.test(sql)) {
|
|
33
|
-
const sessionId = params[0]
|
|
34
|
-
let max = 0
|
|
35
|
-
for (const r of rows) if (r.session_id === sessionId && r.source_turn > max) max = r.source_turn
|
|
36
|
-
return { rows: [{ max_turn: max }] }
|
|
37
|
-
}
|
|
38
|
-
if (/INSERT INTO entries/.test(sql)) {
|
|
39
|
-
const [ts, role, content, source_ref, session_id, source_turn, project_id] = params
|
|
40
|
-
if (bySourceRef.has(source_ref)) return { rowCount: 0 } // ON CONFLICT DO NOTHING
|
|
41
|
-
bySourceRef.add(source_ref)
|
|
42
|
-
rows.push({ id: nextId++, ts, role, content, source_ref, session_id, source_turn, project_id })
|
|
43
|
-
return { rowCount: 1 }
|
|
44
|
-
}
|
|
45
|
-
return { rows: [] }
|
|
46
|
-
},
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Durable high-water store fake (mirrors the DB `meta` kv the facade wires). A
|
|
51
|
-
// SHARED store passed to two runtime instances simulates the state file
|
|
52
|
-
// surviving a process restart; an absent store simulates a deleted state file.
|
|
53
|
-
function makeRuntime(db, durable = new Map()) {
|
|
54
|
-
return createSessionIngestRuntime({
|
|
55
|
-
getDb: () => db,
|
|
56
|
-
log: () => {},
|
|
57
|
-
parseTsToMs: (v) => (typeof v === 'number' ? v : Number(v) || 0),
|
|
58
|
-
loadOrdinalHighWater: async (sessionId) => durable.get(sessionId) ?? null,
|
|
59
|
-
saveOrdinalHighWater: (sessionId, obj) => { durable.set(sessionId, obj); },
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const SESSION = 'compaction-smoke-sess'
|
|
64
|
-
// Untimestamped identical turns (no ts/timestamp → ordinal is what disambiguates).
|
|
65
|
-
const mk = (role, content) => ({ role, content })
|
|
66
|
-
|
|
67
|
-
// ── Scenario: live process ingests, compaction drops an earlier copy, append ──
|
|
68
|
-
const db = createFakeDb()
|
|
69
|
-
const rt = makeRuntime(db)
|
|
70
|
-
|
|
71
|
-
// Distinct object identities so compaction survival is by-reference (mirrors the
|
|
72
|
-
// runtime's immutable-transcript assumption).
|
|
73
|
-
const other1 = mk('assistant', 'sure, working on it')
|
|
74
|
-
const dupA = mk('user', 'run it again') // 1st identical untimestamped turn
|
|
75
|
-
const other2 = mk('assistant', 'done')
|
|
76
|
-
const dupB = mk('user', 'run it again') // 2nd identical untimestamped turn (distinct object)
|
|
77
|
-
|
|
78
|
-
// Call 1 (COLD): full array with two identical untimestamped user turns.
|
|
79
|
-
const arr1 = [other1, dupA, other2, dupB]
|
|
80
|
-
let r1 = await rt.ingestSessionMessages({ sessionId: SESSION, messages: arr1, limit: 5000 })
|
|
81
|
-
const userRows1 = db.rows.filter(r => r.role === 'user' && r.content === 'run it again')
|
|
82
|
-
assert(userRows1.length === 2, `cold ingest must persist BOTH identical untimestamped turns (got ${userRows1.length})`)
|
|
83
|
-
const refDupA = userRows1[0].source_ref
|
|
84
|
-
const refDupB = userRows1[1].source_ref
|
|
85
|
-
assert(refDupA !== refDupB, 'the two identical untimestamped turns must persist under distinct source_refs')
|
|
86
|
-
|
|
87
|
-
// Compaction: drop the FIRST identical copy (dupA) and some other rows; KEEP the
|
|
88
|
-
// second identical copy object (dupB) — this is what makes the array position of
|
|
89
|
-
// the surviving/new identical turn shift down onto an already-persisted ordinal.
|
|
90
|
-
// Then APPEND a genuinely new identical turn (distinct object).
|
|
91
|
-
const dupC = mk('user', 'run it again') // NEW appended identical turn
|
|
92
|
-
const arr2 = [dupB, mk('assistant', 'ok next'), dupC]
|
|
93
|
-
|
|
94
|
-
// Call 2 (WARM, same runtime/process): the appended dupC must persist as a NEW row.
|
|
95
|
-
let r2 = await rt.ingestSessionMessages({ sessionId: SESSION, messages: arr2, limit: 5000 })
|
|
96
|
-
const userRows2 = db.rows.filter(r => r.role === 'user' && r.content === 'run it again')
|
|
97
|
-
assert(
|
|
98
|
-
userRows2.length === 3,
|
|
99
|
-
`INVARIANT 1: post-compaction appended identical turn must persist as a NEW row (expected 3 total, got ${userRows2.length}) — this is the silent-drop bug`,
|
|
100
|
-
)
|
|
101
|
-
const refDupC = userRows2.find(r => r.source_ref !== refDupA && r.source_ref !== refDupB)?.source_ref
|
|
102
|
-
assert(refDupC, 'appended identical turn must have a fresh source_ref above the persisted high-water')
|
|
103
|
-
// dupB survived: it must have deduped (reused its recorded ordinal), NOT minted a new row.
|
|
104
|
-
assert(db.rows.filter(r => r.source_ref === refDupB).length === 1, 'survived identical turn must dedupe, not duplicate')
|
|
105
|
-
|
|
106
|
-
// ── INVARIANT 2: fresh runtime (new process), full identical re-ingest ────────
|
|
107
|
-
// Rows already in DB; re-ingesting the CURRENT array from start=0 must create
|
|
108
|
-
// ZERO new rows.
|
|
109
|
-
const rowCountBefore = db.rows.length
|
|
110
|
-
const rtFresh = makeRuntime(db) // cold state for this "process"
|
|
111
|
-
await rtFresh.ingestSessionMessages({ sessionId: SESSION, messages: arr2, limit: 5000 })
|
|
112
|
-
assert(
|
|
113
|
-
db.rows.length === rowCountBefore,
|
|
114
|
-
`INVARIANT 2: full identical re-ingest in a fresh process must create 0 duplicates (before=${rowCountBefore} after=${db.rows.length})`,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
// ── INVARIANT 3: subset re-ingest reproduces the same source_refs as full ─────
|
|
118
|
-
// Fresh DB + fresh runtime; ingest the full array, capture refs; then a NEW
|
|
119
|
-
// fresh DB/runtime ingesting the same array via a small window (subset, seeded
|
|
120
|
-
// from the prefix) must reproduce the identical source_refs.
|
|
121
|
-
function refsFor(messages, opts) {
|
|
122
|
-
const d = createFakeDb()
|
|
123
|
-
const r = makeRuntime(d)
|
|
124
|
-
return { d, r }
|
|
125
|
-
}
|
|
126
|
-
{
|
|
127
|
-
const fullMsgs = [mk('user', 'x'), mk('assistant', 'y'), mk('user', 'x'), mk('user', 'x'), mk('assistant', 'y')]
|
|
128
|
-
const { d: dFull, r: rFull } = refsFor()
|
|
129
|
-
await rFull.ingestSessionMessages({ sessionId: 'subset-sess', messages: fullMsgs, limit: 5000 })
|
|
130
|
-
const fullRefs = dFull.rows.filter(r => r.session_id === 'subset-sess').map(r => r.source_ref).sort()
|
|
131
|
-
|
|
132
|
-
// Subset: fresh process, window = last 2 messages, prefix seeded internally.
|
|
133
|
-
const { d: dSub, r: rSub } = refsFor()
|
|
134
|
-
await rSub.ingestSessionMessages({ sessionId: 'subset-sess', messages: fullMsgs, limit: 2 })
|
|
135
|
-
const subRefs = dSub.rows.filter(r => r.session_id === 'subset-sess').map(r => r.source_ref)
|
|
136
|
-
for (const ref of subRefs) {
|
|
137
|
-
assert(fullRefs.includes(ref), `INVARIANT 3: subset re-ingest source_ref ${ref} must match a full re-ingest ref`)
|
|
138
|
-
}
|
|
139
|
-
// The subset window's last two messages are `x`(3rd occurrence) and `y`(2nd);
|
|
140
|
-
// their refs must equal the full re-ingest's 3rd `x` and 2nd `y` refs.
|
|
141
|
-
assert(subRefs.length === 2, `subset window should ingest exactly its 2 messages (got ${subRefs.length})`)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ── INVARIANT 1 across RESTART: fresh runtime (new process), same DB ──────────
|
|
145
|
-
// A restart = a NEW runtime instance (empty in-memory ordinal state AND empty
|
|
146
|
-
// WeakMap) ingesting a reloaded transcript against the SAME DB that already
|
|
147
|
-
// holds prior rows. When the reloaded (compacted) array still RETAINS the
|
|
148
|
-
// identical copies, positional ordinals reproduce each survivor's OWN live row,
|
|
149
|
-
// and a freshly appended identical turn takes a free ordinal and persists NEW.
|
|
150
|
-
{
|
|
151
|
-
const rdb = createFakeDb()
|
|
152
|
-
const S = 'restart-sess'
|
|
153
|
-
const rtA = makeRuntime(rdb)
|
|
154
|
-
await rtA.ingestSessionMessages({ sessionId: S, messages: [mk('user', 'ping'), mk('assistant', 'pong'), mk('user', 'ping')], limit: 5000 })
|
|
155
|
-
const pingBefore = rdb.rows.filter(r => r.content === 'ping')
|
|
156
|
-
assert(pingBefore.length === 2, `pre-restart should persist 2 identical pings (got ${pingBefore.length})`)
|
|
157
|
-
const beforeIds = new Set(pingBefore.map(r => r.id))
|
|
158
|
-
|
|
159
|
-
// Restart: brand-new runtime (fresh WeakMap + ordinal state), same DB. The
|
|
160
|
-
// reloaded compacted array RETAINS both ping copies (compaction dropped only
|
|
161
|
-
// the non-identical `pong`); append a genuinely new identical ping. The
|
|
162
|
-
// reloaded objects are DISTINCT references from the pre-restart ones.
|
|
163
|
-
const rtB = makeRuntime(rdb)
|
|
164
|
-
await rtB.ingestSessionMessages({ sessionId: S, messages: [mk('user', 'ping'), mk('user', 'ping'), mk('user', 'ping')], limit: 5000 })
|
|
165
|
-
const pingAfter = rdb.rows.filter(r => r.content === 'ping')
|
|
166
|
-
assert(pingAfter.length === 3, `RESTART: appended identical turn must persist as a NEW row (expected 3, got ${pingAfter.length})`)
|
|
167
|
-
// Row-identity (not ref-membership) check: the survivor re-ingest must DEDUPE
|
|
168
|
-
// onto the 2 pre-existing live rows (same ids, no re-insert) and add EXACTLY
|
|
169
|
-
// one NEW row (the append) — proving survivors mapped to live rows, not dropped.
|
|
170
|
-
const survivorRows = pingAfter.filter(r => beforeIds.has(r.id))
|
|
171
|
-
const newRows = pingAfter.filter(r => !beforeIds.has(r.id))
|
|
172
|
-
assert(survivorRows.length === 2, `RESTART: survivors must reuse the 2 live rows (got ${survivorRows.length})`)
|
|
173
|
-
assert(newRows.length === 1, `RESTART: exactly one NEW appended row expected (got ${newRows.length})`)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ── INVARIANT 1 across RESTART, LATER-WARM append (durable high-water) ────────
|
|
177
|
-
// The path the reviewer disproved: after restart the cold replay of a COMPACTED
|
|
178
|
-
// array seeds occNext = survivor-count T, but the DB holds K>T copies. A NEW
|
|
179
|
-
// untimestamped identical turn arriving in a LATER (warm) call is distinguishable
|
|
180
|
-
// (it is genuinely new, not one of the cold-replay survivors) and MUST persist —
|
|
181
|
-
// only the durable per-identity high-water K makes that possible.
|
|
182
|
-
{
|
|
183
|
-
const durable = new Map() // survives the "restart" (shared across runtimes)
|
|
184
|
-
const rdb = createFakeDb() // DB rows survive the restart too
|
|
185
|
-
const S = 'restart-warm-sess'
|
|
186
|
-
|
|
187
|
-
// Pre-restart process: build K=3 persisted copies of an identical untimestamped
|
|
188
|
-
// turn (cold [X,other,X] → 2 copies; then in-process compaction+append → 3rd).
|
|
189
|
-
const rtA = makeRuntime(rdb, durable)
|
|
190
|
-
const xa1 = mk('user', 'again'); const xa2 = mk('user', 'again')
|
|
191
|
-
await rtA.ingestSessionMessages({ sessionId: S, messages: [xa1, mk('assistant', 'ok'), xa2], limit: 5000 })
|
|
192
|
-
const xa3 = mk('user', 'again') // in-process appended (compaction kept xa2)
|
|
193
|
-
await rtA.ingestSessionMessages({ sessionId: S, messages: [xa2, mk('assistant', 'ok2'), xa3], limit: 5000 })
|
|
194
|
-
assert(rdb.rows.filter(r => r.content === 'again').length === 3, 'pre-restart should hold K=3 identical copies')
|
|
195
|
-
assert(durable.has(S), 'durable high-water must have been persisted for the duplicate identity')
|
|
196
|
-
|
|
197
|
-
// Restart: fresh runtime (empty WeakMap + ordinal state), SAME db + durable.
|
|
198
|
-
// Reloaded COMPACTED array retains only ONE copy (T=1).
|
|
199
|
-
const rtB = makeRuntime(rdb, durable)
|
|
200
|
-
const rx = mk('user', 'again') // reloaded survivor (distinct object)
|
|
201
|
-
await rtB.ingestSessionMessages({ sessionId: S, messages: [rx], limit: 5000 }) // COLD replay (T=1)
|
|
202
|
-
assert(rdb.rows.filter(r => r.content === 'again').length === 3, 'cold replay of survivor must not add rows (dedupe)')
|
|
203
|
-
|
|
204
|
-
// LATER warm call on rtB appends a genuinely new identical turn.
|
|
205
|
-
const rxNew = mk('user', 'again')
|
|
206
|
-
const rowsBeforeAppend = rdb.rows.length
|
|
207
|
-
await rtB.ingestSessionMessages({ sessionId: S, messages: [rx, rxNew], limit: 5000 })
|
|
208
|
-
const againAfter = rdb.rows.filter(r => r.content === 'again')
|
|
209
|
-
assert(
|
|
210
|
-
againAfter.length === 4,
|
|
211
|
-
`LATER-WARM RESTART: appended identical turn must persist as a NEW row via durable K (expected 4, got ${againAfter.length})`,
|
|
212
|
-
)
|
|
213
|
-
assert(rdb.rows.length === rowsBeforeAppend + 1, 'exactly one new row from the later-warm append')
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ── Zero-dup guarantee when a compacted reload DROPPED an identical copy ──────
|
|
217
|
-
// If the reloaded array is missing an earlier identical untimestamped copy AND
|
|
218
|
-
// the WeakMap survivor signal is gone (restart), the appended identical turn is
|
|
219
|
-
// information-theoretically indistinguishable from the survivors (see the
|
|
220
|
-
// DURABILITY note in session-ingest-runtime.mjs). Contract in that corner: NEVER
|
|
221
|
-
// mint a duplicate survivor row (ON CONFLICT dedupes). The append may collapse;
|
|
222
|
-
// it must not duplicate.
|
|
223
|
-
{
|
|
224
|
-
const rdb = createFakeDb()
|
|
225
|
-
const S = 'restart-drop-sess'
|
|
226
|
-
const rtA = makeRuntime(rdb)
|
|
227
|
-
await rtA.ingestSessionMessages({ sessionId: S, messages: [mk('user', 'echo'), mk('user', 'echo')], limit: 5000 })
|
|
228
|
-
const before = rdb.rows.filter(r => r.content === 'echo').length
|
|
229
|
-
assert(before === 2, `pre-restart should persist 2 identical echoes (got ${before})`)
|
|
230
|
-
const rowsBefore = rdb.rows.length
|
|
231
|
-
|
|
232
|
-
// Restart with a compacted reload that DROPPED one echo, then appended one
|
|
233
|
-
// (net 2 copies again). Must not mint a duplicate; the append collapses.
|
|
234
|
-
const rtB = makeRuntime(rdb)
|
|
235
|
-
await rtB.ingestSessionMessages({ sessionId: S, messages: [mk('user', 'echo'), mk('user', 'echo')], limit: 5000 })
|
|
236
|
-
const after = rdb.rows.filter(r => r.content === 'echo').length
|
|
237
|
-
assert(after === before, `RESTART+drop: must not mint a duplicate survivor row (before=${before} after=${after})`)
|
|
238
|
-
assert(rdb.rows.length === rowsBefore, 'RESTART+drop: no duplicate rows created')
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ── WARM runtime + JSON-cloned replay (desktop/session resume) ───────────────
|
|
242
|
-
// The memory daemon can outlive a surface that reloads the same session JSON.
|
|
243
|
-
// Every transcript message is then a new object while the ingest runtime's
|
|
244
|
-
// WeakMaps remain warm. The replay must reuse prior source_refs, and only a
|
|
245
|
-
// genuinely appended suffix may insert.
|
|
246
|
-
{
|
|
247
|
-
const rdb = createFakeDb()
|
|
248
|
-
const S = 'warm-json-clone-sess'
|
|
249
|
-
const rt = makeRuntime(rdb)
|
|
250
|
-
const original = [
|
|
251
|
-
mk('user', 'optimize session switching'),
|
|
252
|
-
mk('assistant', 'I will inspect the transition path'),
|
|
253
|
-
mk('user', 'go ahead'),
|
|
254
|
-
mk('assistant', 'working'),
|
|
255
|
-
]
|
|
256
|
-
await rt.ingestSessionMessages({ sessionId: S, messages: original, limit: 5000 })
|
|
257
|
-
const beforeReplay = rdb.rows.length
|
|
258
|
-
|
|
259
|
-
const clonedReplay = JSON.parse(JSON.stringify(original))
|
|
260
|
-
await rt.ingestSessionMessages({ sessionId: S, messages: clonedReplay, limit: 5000 })
|
|
261
|
-
assert(
|
|
262
|
-
rdb.rows.length === beforeReplay,
|
|
263
|
-
`WARM CLONE: JSON-reloaded transcript must insert 0 duplicates (before=${beforeReplay} after=${rdb.rows.length})`,
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
// Reload the whole array again and append text identical to an older row.
|
|
267
|
-
// Snapshot reconciliation reuses the cloned prefix while the suffix consumes
|
|
268
|
-
// the historical occurrence high-water.
|
|
269
|
-
const clonedWithAppend = JSON.parse(JSON.stringify(original))
|
|
270
|
-
clonedWithAppend.push(mk('assistant', 'working'))
|
|
271
|
-
await rt.ingestSessionMessages({ sessionId: S, messages: clonedWithAppend, limit: 5000 })
|
|
272
|
-
assert(rdb.rows.length === beforeReplay + 1, 'WARM CLONE: exactly one genuinely appended row must insert')
|
|
273
|
-
|
|
274
|
-
const clonedAgain = JSON.parse(JSON.stringify(clonedWithAppend))
|
|
275
|
-
await rt.ingestSessionMessages({ sessionId: S, messages: clonedAgain, limit: 5000 })
|
|
276
|
-
assert(rdb.rows.length === beforeReplay + 1, 'WARM CLONE: replay after append must remain idempotent')
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
process.stdout.write('session ingest compaction-append smoke passed \u2713\n')
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
normalizeIngestRole,
|
|
4
|
-
stableSessionSourceRef,
|
|
5
|
-
redactToolArgString,
|
|
6
|
-
redactToolArgValue,
|
|
7
|
-
sessionMessageContent,
|
|
8
|
-
createIngestTurnAllocator,
|
|
9
|
-
} from '../src/runtime/memory/lib/session-ingest.mjs';
|
|
10
|
-
|
|
11
|
-
function assert(condition, message) {
|
|
12
|
-
if (!condition) throw new Error(message);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// --- Role normalization ---
|
|
16
|
-
assert(normalizeIngestRole('human') === 'user', 'human should normalize to user');
|
|
17
|
-
assert(normalizeIngestRole('AI') === 'assistant', 'AI should normalize to assistant');
|
|
18
|
-
assert(normalizeIngestRole('tool_result') === null, 'tool_result is not ingested (maps to tool, dropped)');
|
|
19
|
-
assert(normalizeIngestRole('developer') === null, 'developer is not ingested');
|
|
20
|
-
assert(normalizeIngestRole('mystery') === null, 'unknown role should be dropped');
|
|
21
|
-
|
|
22
|
-
// --- Stable source_ref across index/time for untimestamped messages ---
|
|
23
|
-
const plain = { role: 'user', content: 'fix the bug in compact.mjs' };
|
|
24
|
-
const refA = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
|
|
25
|
-
const refB = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
|
|
26
|
-
assert(refA === refB, 'same untimestamped message must yield the same source_ref regardless of call');
|
|
27
|
-
assert(!/#\d+$/.test(refA), 'source_ref must not carry a volatile #index suffix');
|
|
28
|
-
|
|
29
|
-
// Two textually identical untimestamped plain messages dedupe to one ref.
|
|
30
|
-
const dupA = stableSessionSourceRef('sess1', { role: 'user', content: 'ok' }, 'user', 'ok');
|
|
31
|
-
const dupB = stableSessionSourceRef('sess1', { role: 'user', content: 'ok' }, 'user', 'ok');
|
|
32
|
-
assert(dupA === dupB, 'identical untimestamped plain messages should share a stable ref (dedupe preferred)');
|
|
33
|
-
|
|
34
|
-
// A synthesized Date.now() fallback ts must NOT change identity: the helper
|
|
35
|
-
// only reads m.ts/m.timestamp, so two calls for the same untimestamped message
|
|
36
|
-
// at different wall-clock times still match.
|
|
37
|
-
const t1 = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
|
|
38
|
-
const t2 = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
|
|
39
|
-
assert(t1 === t2, 'synthesized timestamps must not enter identity');
|
|
40
|
-
|
|
41
|
-
// Original timestamp, when present, DOES contribute to identity.
|
|
42
|
-
const tsMsg = { role: 'user', content: 'same text', ts: 1700000000000 };
|
|
43
|
-
const tsRef = stableSessionSourceRef('sess1', tsMsg, 'user', 'same text');
|
|
44
|
-
const noTsRef = stableSessionSourceRef('sess1', { role: 'user', content: 'same text' }, 'user', 'same text');
|
|
45
|
-
assert(tsRef !== noTsRef, 'an original ts should differentiate identity from an untimestamped twin');
|
|
46
|
-
|
|
47
|
-
// Tool-id identity: different tool ids => different refs; same id => same ref.
|
|
48
|
-
const toolMsg1 = { role: 'assistant', content: 'x', toolCalls: [{ id: 'call_1', name: 'apply_patch' }] };
|
|
49
|
-
const toolMsg2 = { role: 'assistant', content: 'x', toolCalls: [{ id: 'call_2', name: 'apply_patch' }] };
|
|
50
|
-
const toolRef1 = stableSessionSourceRef('sess1', toolMsg1, 'assistant', 'x');
|
|
51
|
-
const toolRef2 = stableSessionSourceRef('sess1', toolMsg2, 'assistant', 'x');
|
|
52
|
-
assert(toolRef1 !== toolRef2, 'different tool-call ids should produce different source_refs');
|
|
53
|
-
const toolRef1b = stableSessionSourceRef('sess1', { ...toolMsg1 }, 'assistant', 'x');
|
|
54
|
-
assert(toolRef1 === toolRef1b, 'same tool-call id + content should produce the same source_ref');
|
|
55
|
-
const resultRef = stableSessionSourceRef('sess1', { role: 'tool', content: 'done', toolCallId: 'call_1' }, 'tool', 'done');
|
|
56
|
-
assert(typeof resultRef === 'string' && resultRef.startsWith('session:sess1:'), 'tool_result ref should be well formed');
|
|
57
|
-
|
|
58
|
-
// --- Sensitive redaction (object/JSON args) ---
|
|
59
|
-
const jsonArgs = JSON.stringify({ url: 'https://x', api_key: 'sk-secret-123', nested: { password: 'p@ss word' } });
|
|
60
|
-
const redactedJson = redactToolArgString(jsonArgs);
|
|
61
|
-
assert(!redactedJson.includes('sk-secret-123'), 'api_key value must be redacted from JSON args');
|
|
62
|
-
assert(!redactedJson.includes('p@ss word'), 'nested password must be redacted from JSON args');
|
|
63
|
-
assert(redactedJson.includes('https://x'), 'non-sensitive url should be preserved');
|
|
64
|
-
assert(redactedJson.includes('[redacted]'), 'redaction marker should be present');
|
|
65
|
-
|
|
66
|
-
const objRedaction = redactToolArgValue({ authorization: 'Bearer abc.def', keep: 'value' });
|
|
67
|
-
assert(objRedaction.authorization === '[redacted]', 'authorization key should collapse to [redacted]');
|
|
68
|
-
assert(objRedaction.keep === 'value', 'non-sensitive key should be preserved');
|
|
69
|
-
|
|
70
|
-
// --- Sensitive redaction (raw non-JSON strings with spaces / Bearer / cookies) ---
|
|
71
|
-
const rawCases = [
|
|
72
|
-
'authorization: Bearer abc.def',
|
|
73
|
-
'password="abc def"',
|
|
74
|
-
"api_key='abc def'",
|
|
75
|
-
'cookie: a=b; c=d',
|
|
76
|
-
];
|
|
77
|
-
for (const raw of rawCases) {
|
|
78
|
-
const out = redactToolArgString(raw);
|
|
79
|
-
assert(out.includes('[redacted]'), `raw secret should be redacted: ${raw}`);
|
|
80
|
-
}
|
|
81
|
-
const bearerOut = redactToolArgString('authorization: Bearer abc.def');
|
|
82
|
-
assert(!bearerOut.includes('abc.def'), 'Bearer token fragment must not leak');
|
|
83
|
-
assert(!bearerOut.includes('Bearer'), 'Bearer scheme word must be consumed by redaction');
|
|
84
|
-
const pwSpaceOut = redactToolArgString('password="abc def"');
|
|
85
|
-
assert(!pwSpaceOut.includes('abc def'), 'quoted password with spaces must not leak');
|
|
86
|
-
const apiSpaceOut = redactToolArgString("api_key='abc def'");
|
|
87
|
-
assert(!apiSpaceOut.includes('abc def'), 'single-quoted api_key with spaces must not leak');
|
|
88
|
-
const cookieOut = redactToolArgString('cookie: a=b; c=d');
|
|
89
|
-
assert(!/a=b/.test(cookieOut), 'cookie value (incl. ;-separated pairs) must not leak');
|
|
90
|
-
|
|
91
|
-
// Non-sensitive raw args stay readable.
|
|
92
|
-
const plainArgs = redactToolArgString('file=compact.mjs lines=10');
|
|
93
|
-
assert(plainArgs.includes('compact.mjs'), 'non-sensitive raw args should be preserved');
|
|
94
|
-
assert(!plainArgs.includes('[redacted]'), 'non-sensitive raw args should not be redacted');
|
|
95
|
-
|
|
96
|
-
// --- Prefixed sensitive key variants (raw) ---
|
|
97
|
-
const prefixedRawCases = [
|
|
98
|
-
['access_token=abc.def', 'abc.def'],
|
|
99
|
-
['access-token=abc.def', 'abc.def'],
|
|
100
|
-
['x-api-key=sk-supersecret-123', 'sk-supersecret-123'],
|
|
101
|
-
['bearer_token: Bearer bear-tok-456', 'bear-tok-456'],
|
|
102
|
-
];
|
|
103
|
-
for (const [raw, frag] of prefixedRawCases) {
|
|
104
|
-
const out = redactToolArgString(raw);
|
|
105
|
-
assert(out.includes('[redacted]'), `prefixed key variant should be redacted: ${raw}`);
|
|
106
|
-
assert(!out.includes(frag), `prefixed key variant must not leak its secret fragment: ${raw}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// --- JSON/object string VALUES carrying embedded raw secrets ---
|
|
110
|
-
// Non-sensitive keys (headers/env) whose string value embeds a secret pair must
|
|
111
|
-
// still be redacted (defense-in-depth), while non-sensitive content stays.
|
|
112
|
-
const embeddedCases = [
|
|
113
|
-
[{ headers: 'authorization: Bearer abc.def' }, 'abc.def'],
|
|
114
|
-
[{ headers: 'cookie: a=b; c=d' }, 'a=b'],
|
|
115
|
-
[{ env: 'x-api-key=sk-supersecret-123' }, 'sk-supersecret-123'],
|
|
116
|
-
];
|
|
117
|
-
for (const [obj, frag] of embeddedCases) {
|
|
118
|
-
const viaString = redactToolArgString(JSON.stringify(obj));
|
|
119
|
-
assert(!viaString.includes(frag), `JSON string value secret must not leak: ${JSON.stringify(obj)}`);
|
|
120
|
-
assert(viaString.includes('[redacted]'), `JSON string value secret should be redacted: ${JSON.stringify(obj)}`);
|
|
121
|
-
// Object path (redactToolArgValue) must redact the embedded secret too.
|
|
122
|
-
const viaObject = JSON.stringify(redactToolArgValue(obj));
|
|
123
|
-
assert(!viaObject.includes(frag), `object string value secret must not leak: ${JSON.stringify(obj)}`);
|
|
124
|
-
}
|
|
125
|
-
// Non-sensitive sibling values remain readable.
|
|
126
|
-
const mixedObj = redactToolArgValue({ url: 'https://x', headers: 'authorization: Bearer abc.def', note: 'keep me' });
|
|
127
|
-
assert(mixedObj.url === 'https://x', 'non-sensitive url should be preserved alongside redacted header value');
|
|
128
|
-
assert(mixedObj.note === 'keep me', 'non-sensitive note should be preserved');
|
|
129
|
-
assert(!JSON.stringify(mixedObj).includes('abc.def'), 'embedded header secret must not leak from mixed object');
|
|
130
|
-
|
|
131
|
-
// --- sessionMessageContent shaping ---
|
|
132
|
-
const asstMsg = {
|
|
133
|
-
role: 'assistant',
|
|
134
|
-
content: 'running tool',
|
|
135
|
-
toolCalls: [{ id: 'call_9', name: 'http_get', arguments: '{"url":"https://x","authorization":"Bearer zzz"}' }],
|
|
136
|
-
};
|
|
137
|
-
const asstShaped = sessionMessageContent(asstMsg);
|
|
138
|
-
assert(asstShaped.includes('running tool'), 'assistant text should be preserved');
|
|
139
|
-
assert(asstShaped.includes('http_get'), 'tool name should be readable');
|
|
140
|
-
assert(asstShaped.includes('id=call_9'), 'tool id should be readable');
|
|
141
|
-
assert(asstShaped.includes('https://x'), 'non-sensitive tool arg should be readable');
|
|
142
|
-
assert(!asstShaped.includes('zzz'), 'sensitive tool arg should be redacted in shaped content');
|
|
143
|
-
|
|
144
|
-
const toolResultShaped = sessionMessageContent({ role: 'tool', content: 'patch applied', toolCallId: 'call_9' });
|
|
145
|
-
assert(toolResultShaped.includes('[tool_result id=call_9]'), 'tool result should be tagged with its pairing id');
|
|
146
|
-
assert(toolResultShaped.includes('patch applied'), 'tool result body should be preserved');
|
|
147
|
-
|
|
148
|
-
const rawToolMsg = sessionMessageContent({
|
|
149
|
-
role: 'assistant',
|
|
150
|
-
content: '',
|
|
151
|
-
toolCalls: [{ id: 'call_x', name: 'curl', arguments: 'authorization: Bearer abc.def' }],
|
|
152
|
-
});
|
|
153
|
-
assert(!/abc\.def/.test(rawToolMsg), 'session message redaction should not leak raw Bearer token text');
|
|
154
|
-
|
|
155
|
-
// --- Monotonic source_turn allocator (post-compaction ordering) ---
|
|
156
|
-
// Seed from the current MAX(source_turn) for the session; every newly inserted
|
|
157
|
-
// row must get a strictly increasing turn ABOVE all prior rows, independent of
|
|
158
|
-
// the (post-compaction) array index. Re-ingested rows do not advance the count.
|
|
159
|
-
const alloc = createIngestTurnAllocator(42);
|
|
160
|
-
assert(alloc.peekNext() === 43, 'allocator should peek the next turn above the prior max');
|
|
161
|
-
assert(alloc.peekNext() === 43, 'peek must be idempotent until next() is called');
|
|
162
|
-
assert(alloc.next() === 43, 'first inserted row should take prevMax+1');
|
|
163
|
-
assert(alloc.next() === 44, 'second inserted row should take prevMax+2');
|
|
164
|
-
assert(alloc.current() === 44, 'current should track the last consumed turn');
|
|
165
|
-
// Simulate: pre-compact rows already at turns 1..3 (prevMax=3); after
|
|
166
|
-
// compaction a later message is appended with a LOW array index — it must STILL
|
|
167
|
-
// get turn 4 (continuation), not a low/old turn that would sort before them.
|
|
168
|
-
const post = createIngestTurnAllocator(3);
|
|
169
|
-
const appendedTurn = post.next();
|
|
170
|
-
assert(appendedTurn === 4, 'a post-compaction appended row must continue after the prior max source_turn');
|
|
171
|
-
assert(appendedTurn > 3, 'post-compaction row turn must sort AFTER older pre-compaction rows');
|
|
172
|
-
// A conflicting re-ingest (row already present) must not consume a turn.
|
|
173
|
-
const seeded = createIngestTurnAllocator(10);
|
|
174
|
-
const peek = seeded.peekNext();
|
|
175
|
-
assert(peek === 11 && seeded.current() === 10, 're-ingest peek must not advance the counter');
|
|
176
|
-
|
|
177
|
-
process.stdout.write('session ingest smoke passed \u2713\n');
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-orphan-sweep-test.mjs — proves the periodic sweep reclaims mature
|
|
3
|
-
* closed tombstones that exist ON DISK but are ABSENT from the summary index.
|
|
4
|
-
*
|
|
5
|
-
* Regression guard for the pre-fix bug where sweepStaleSessions iterated only
|
|
6
|
-
* listStoredSessionSummaries() rows, so on-disk session files missing from a
|
|
7
|
-
* stale index were never visited and their tombstones accumulated forever.
|
|
8
|
-
*/
|
|
9
|
-
import { test } from 'node:test';
|
|
10
|
-
import assert from 'node:assert/strict';
|
|
11
|
-
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
import { tmpdir } from 'node:os';
|
|
14
|
-
|
|
15
|
-
test('mature closed on-disk orphan absent from summary index gets tombstone-swept', async () => {
|
|
16
|
-
const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-orphan-sweep-'));
|
|
17
|
-
process.env.MIXDOG_DATA_DIR = dataDir;
|
|
18
|
-
try {
|
|
19
|
-
const store = await import('../src/runtime/agent/orchestrator/session/store.mjs');
|
|
20
|
-
const { sweepStaleSessions, _sessionSummary, _writeSummaryIndex } = store;
|
|
21
|
-
|
|
22
|
-
const sessionsDir = join(dataDir, 'sessions');
|
|
23
|
-
mkdirSync(sessionsDir, { recursive: true });
|
|
24
|
-
|
|
25
|
-
const now = Date.now();
|
|
26
|
-
const TWO_HOURS = 2 * 60 * 60 * 1000;
|
|
27
|
-
const ONE_HOUR = 60 * 60 * 1000;
|
|
28
|
-
|
|
29
|
-
// (1) An INDEXED, fresh, open agent session — present in the summary
|
|
30
|
-
// index so the index is non-empty (blocking any rebuild) and must
|
|
31
|
-
// survive the sweep.
|
|
32
|
-
const indexed = {
|
|
33
|
-
id: 'sess_indexed_keep',
|
|
34
|
-
closed: false,
|
|
35
|
-
status: 'idle',
|
|
36
|
-
owner: 'agent:test',
|
|
37
|
-
updatedAt: now,
|
|
38
|
-
createdAt: now,
|
|
39
|
-
lastHeartbeatAt: now,
|
|
40
|
-
messages: [],
|
|
41
|
-
};
|
|
42
|
-
writeFileSync(join(sessionsDir, `${indexed.id}.json`), JSON.stringify(indexed));
|
|
43
|
-
|
|
44
|
-
// (2) An ORPHAN closed+mature tombstone — on disk, NOT in the index,
|
|
45
|
-
// closed >1h ago. Only reachable if the sweep reconciles the index
|
|
46
|
-
// candidate set with a direct directory scan.
|
|
47
|
-
const orphan = {
|
|
48
|
-
id: 'sess_orphan_tombstone',
|
|
49
|
-
closed: true,
|
|
50
|
-
status: 'closed',
|
|
51
|
-
owner: 'agent:test',
|
|
52
|
-
updatedAt: now - TWO_HOURS,
|
|
53
|
-
createdAt: now - TWO_HOURS,
|
|
54
|
-
messages: [],
|
|
55
|
-
};
|
|
56
|
-
writeFileSync(join(sessionsDir, `${orphan.id}.json`), JSON.stringify(orphan));
|
|
57
|
-
|
|
58
|
-
// (3) A shared-store tombstone whose id still has local in-flight work.
|
|
59
|
-
// It must be excluded before unlink, otherwise that work's late save
|
|
60
|
-
// could recreate an open session after the tombstone disappears.
|
|
61
|
-
const locallyLive = {
|
|
62
|
-
...orphan,
|
|
63
|
-
id: 'sess_shared_tombstone_locally_live',
|
|
64
|
-
};
|
|
65
|
-
writeFileSync(join(sessionsDir, `${locallyLive.id}.json`), JSON.stringify(locallyLive));
|
|
66
|
-
|
|
67
|
-
// Index contains ONLY the indexed session — the orphan is absent.
|
|
68
|
-
const rows = _writeSummaryIndex([_sessionSummary(indexed)]);
|
|
69
|
-
assert.equal(rows.length, 1, 'index seeded with exactly one row');
|
|
70
|
-
assert.ok(!rows.some((r) => r.id === orphan.id), 'orphan is absent from the summary index');
|
|
71
|
-
|
|
72
|
-
// Tombstone-only pass (no idle sweep) with a 1h maturity threshold.
|
|
73
|
-
const result = sweepStaleSessions({
|
|
74
|
-
sweepIdle: false,
|
|
75
|
-
tombstoneMaxAgeMs: ONE_HOUR,
|
|
76
|
-
isSessionLive: (id) => id === locallyLive.id,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
assert.equal(result.tombstonesCleaned, 1, 'the orphan tombstone was reclaimed');
|
|
80
|
-
assert.ok(
|
|
81
|
-
result.tombstoneDetails.some((d) => d.id === orphan.id),
|
|
82
|
-
'sweep reports the orphan id among tombstone deletions',
|
|
83
|
-
);
|
|
84
|
-
assert.ok(
|
|
85
|
-
!existsSync(join(sessionsDir, `${orphan.id}.json`)),
|
|
86
|
-
'orphan session file was unlinked from disk',
|
|
87
|
-
);
|
|
88
|
-
assert.ok(
|
|
89
|
-
existsSync(join(sessionsDir, `${indexed.id}.json`)),
|
|
90
|
-
'fresh open indexed session was preserved',
|
|
91
|
-
);
|
|
92
|
-
assert.ok(
|
|
93
|
-
existsSync(join(sessionsDir, `${locallyLive.id}.json`)),
|
|
94
|
-
'locally live shared-store tombstone was excluded before unlink',
|
|
95
|
-
);
|
|
96
|
-
const afterLocalWork = sweepStaleSessions({
|
|
97
|
-
sweepIdle: false,
|
|
98
|
-
tombstoneMaxAgeMs: ONE_HOUR,
|
|
99
|
-
isSessionLive: () => false,
|
|
100
|
-
});
|
|
101
|
-
assert.ok(
|
|
102
|
-
afterLocalWork.tombstoneDetails.some((d) => d.id === locallyLive.id),
|
|
103
|
-
'protected tombstone is reclaimed once local work settles',
|
|
104
|
-
);
|
|
105
|
-
} finally {
|
|
106
|
-
try { rmSync(dataDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
107
|
-
delete process.env.MIXDOG_DATA_DIR;
|
|
108
|
-
}
|
|
109
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { readFileSync } from 'node:fs';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { dirname, resolve } from 'node:path';
|
|
7
|
-
import { __saveModelSettingsForTest } from '../src/mixdog-session-runtime.mjs';
|
|
8
|
-
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const RUNTIME_SRC = resolve(__dirname, '../src/mixdog-session-runtime.mjs');
|
|
11
|
-
|
|
12
|
-
test('setEffort persists through adoptConfig with in-memory baseConfig', () => {
|
|
13
|
-
const src = readFileSync(RUNTIME_SRC, 'utf8');
|
|
14
|
-
const block = src.match(/async setEffort\(value\) \{[\s\S]*?\n \},/)?.[0] || '';
|
|
15
|
-
assert.match(block, /adoptConfig\(saveModelSettings\(cfgMod, route, \{ fastCapable, baseConfig: config \}\)/);
|
|
16
|
-
assert.doesNotMatch(block, /config = saveModelSettings/);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('saveModelSettings preserves baseConfig fields not present on disk', () => {
|
|
20
|
-
const saved = [];
|
|
21
|
-
const cfgMod = {
|
|
22
|
-
loadConfig: () => ({ profile: 'from-disk', modelSettings: {}, fastModels: {} }),
|
|
23
|
-
saveConfig: (next) => {
|
|
24
|
-
saved.push(next);
|
|
25
|
-
return next;
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
const baseConfig = {
|
|
29
|
-
profile: 'in-memory-only',
|
|
30
|
-
modelSettings: {},
|
|
31
|
-
fastModels: {},
|
|
32
|
-
autoClear: { enabled: true },
|
|
33
|
-
};
|
|
34
|
-
const route = { provider: 'openai', model: 'gpt-5.4', effort: 'high', fast: false };
|
|
35
|
-
const result = __saveModelSettingsForTest(cfgMod, route, { fastCapable: true, baseConfig });
|
|
36
|
-
assert.equal(result.profile, 'in-memory-only');
|
|
37
|
-
assert.equal(result.autoClear?.enabled, true);
|
|
38
|
-
assert.equal(result.modelSettings['openai/gpt-5.4']?.effort, 'high');
|
|
39
|
-
assert.equal(saved.length, 1);
|
|
40
|
-
assert.equal(saved[0].profile, 'in-memory-only');
|
|
41
|
-
});
|