@vellumai/assistant 0.11.3 → 0.11.4-staging.1
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/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -123,7 +123,9 @@ export async function indexMessageNow(
|
|
|
123
123
|
// memory_segments has no cross-file FK to messages, so this call must not
|
|
124
124
|
// leave pieces for a message with no row. Skip early when the source
|
|
125
125
|
// row is already gone, the common case of a backfill job running after the
|
|
126
|
-
// message was deleted.
|
|
126
|
+
// message was deleted. Any-state existence check, deliberately: a
|
|
127
|
+
// streaming row exists, and orphan prevention is about deletion, not
|
|
128
|
+
// completeness. A post-write re-check below closes the narrower window
|
|
127
129
|
// where the delete lands mid-transaction.
|
|
128
130
|
const sourceMessage = getDb()
|
|
129
131
|
.select({ id: messages.id })
|
|
@@ -52,18 +52,30 @@ export function isSkillCardMessage(row: { metadata: string | null }): boolean {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* `getMessagesAfter` minus skill-card
|
|
56
|
-
* new-message slice: a card-only tail yields an
|
|
57
|
-
* `no_new_messages` early return), and a mixed tail's
|
|
58
|
-
* last real message rather than the card.
|
|
55
|
+
* `getMessagesAfter` truncated at the first unfinalized row, minus skill-card
|
|
56
|
+
* rows. The retrospective job's new-message slice: a card-only tail yields an
|
|
57
|
+
* empty slice (the job's `no_new_messages` early return), and a mixed tail's
|
|
58
|
+
* cutoff lands on the last real message rather than the card.
|
|
59
|
+
*
|
|
60
|
+
* The slice STOPS BEFORE the first unfinalized row rather than filtering it
|
|
61
|
+
* out. The job's cutoff (and therefore `lastProcessedMessageId` on success)
|
|
62
|
+
* is chosen from this slice, while the fork the job reviews skips unfinalized
|
|
63
|
+
* rows, so the cursor must never move past one: filtering through a stale
|
|
64
|
+
* mid-slice `finalized = 0` row would advance the cursor beyond it, and once
|
|
65
|
+
* the row later finalizes its `(createdAt, id)` sits behind the cursor,
|
|
66
|
+
* unreviewed forever. Truncating parks the cursor before the unfinalized row;
|
|
67
|
+
* the rows behind it are reviewed by the retrospective that runs after the
|
|
68
|
+
* row resolves (finalizes, or is repaired away by crash recovery).
|
|
59
69
|
*/
|
|
60
70
|
export function getRetrospectiveMessagesAfter(
|
|
61
71
|
conversationId: string,
|
|
62
72
|
afterMessageId: string | null,
|
|
63
73
|
): MessageRow[] {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
const rows = getMessagesAfter(conversationId, afterMessageId);
|
|
75
|
+
const firstUnfinalized = rows.findIndex((row) => row.finalized !== 1);
|
|
76
|
+
const bounded =
|
|
77
|
+
firstUnfinalized === -1 ? rows : rows.slice(0, firstUnfinalized);
|
|
78
|
+
return bounded.filter((row) => !isSkillCardMessage(row));
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
/**
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `handleMemoryV3GateStats` in `memory-v3-routes.ts`.
|
|
3
|
+
*
|
|
4
|
+
* The handler aggregates gate telemetry from the telemetry DB and buckets it
|
|
5
|
+
* by concept page count. These tests cover:
|
|
6
|
+
* - empty DB → zero counts, all buckets present, scoredPassRate null
|
|
7
|
+
* - mixed rows land in the correct buckets by real_concept_page_count
|
|
8
|
+
* - scored vs unscored runs are counted separately
|
|
9
|
+
* - rows with no real_concept_page_count go to unknownPageCount
|
|
10
|
+
* - lookbackDays is clamped to [1, 90]
|
|
11
|
+
* - malformed JSON rows are skipped
|
|
12
|
+
* - null/absent DB returns a zero response
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Database } from "bun:sqlite";
|
|
16
|
+
import { describe, expect, test } from "bun:test";
|
|
17
|
+
|
|
18
|
+
import { handleMemoryV3GateStats } from "../memory-v3-routes.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Minimal fake SQLite DB that returns pre-set rows
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
type FakeRow = { payload: string };
|
|
25
|
+
|
|
26
|
+
function makePayload(detail: Record<string, unknown>): string {
|
|
27
|
+
return JSON.stringify({
|
|
28
|
+
type: "watchdog",
|
|
29
|
+
daemon_event_id: "test-id",
|
|
30
|
+
recorded_at: Date.now(),
|
|
31
|
+
check_name: "memory_v3_injection_gate",
|
|
32
|
+
value: 1,
|
|
33
|
+
detail,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fakeDb(rows: FakeRow[]): Database {
|
|
38
|
+
return {
|
|
39
|
+
query: (_sql: string) => ({
|
|
40
|
+
all: (..._args: unknown[]) => rows,
|
|
41
|
+
}),
|
|
42
|
+
} as unknown as Database;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Tests
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
describe("handleMemoryV3GateStats", () => {
|
|
50
|
+
test("returns zero counts with all buckets when DB is null", () => {
|
|
51
|
+
const result = handleMemoryV3GateStats(30, null);
|
|
52
|
+
expect(result.totalRuns).toBe(0);
|
|
53
|
+
expect(result.buckets).toHaveLength(4);
|
|
54
|
+
for (const bucket of result.buckets) {
|
|
55
|
+
expect(bucket.total).toBe(0);
|
|
56
|
+
expect(bucket.scored).toBe(0);
|
|
57
|
+
expect(bucket.passed).toBe(0);
|
|
58
|
+
expect(bucket.scoredPassRate).toBeNull();
|
|
59
|
+
expect(bucket.reasons).toEqual({});
|
|
60
|
+
}
|
|
61
|
+
expect(result.unknownPageCount).toEqual({
|
|
62
|
+
total: 0,
|
|
63
|
+
passed: 0,
|
|
64
|
+
reasons: {},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns zero counts with all buckets when DB returns no rows", () => {
|
|
69
|
+
const result = handleMemoryV3GateStats(30, fakeDb([]));
|
|
70
|
+
expect(result.totalRuns).toBe(0);
|
|
71
|
+
expect(result.buckets[0]!.pageCountRange).toBe("0–9");
|
|
72
|
+
expect(result.buckets[3]!.pageCountRange).toBe("200+");
|
|
73
|
+
for (const b of result.buckets) {
|
|
74
|
+
expect(b.scoredPassRate).toBeNull();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("buckets a dense_pass row into 10–49 bucket", () => {
|
|
79
|
+
const rows = [
|
|
80
|
+
{
|
|
81
|
+
payload: makePayload({
|
|
82
|
+
pass: true,
|
|
83
|
+
reason: "dense_pass",
|
|
84
|
+
scored: true,
|
|
85
|
+
real_concept_page_count: 25,
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
const result = handleMemoryV3GateStats(30, fakeDb(rows));
|
|
90
|
+
expect(result.totalRuns).toBe(1);
|
|
91
|
+
const bucket = result.buckets.find((b) => b.pageCountRange === "10–49")!;
|
|
92
|
+
expect(bucket.total).toBe(1);
|
|
93
|
+
expect(bucket.scored).toBe(1);
|
|
94
|
+
expect(bucket.passed).toBe(1);
|
|
95
|
+
expect(bucket.scoredPassRate).toBe(1);
|
|
96
|
+
expect(bucket.reasons).toEqual({ dense_pass: 1 });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("buckets a fail row into 200+ bucket", () => {
|
|
100
|
+
const rows = [
|
|
101
|
+
{
|
|
102
|
+
payload: makePayload({
|
|
103
|
+
pass: false,
|
|
104
|
+
reason: "fail_no_signal",
|
|
105
|
+
scored: true,
|
|
106
|
+
real_concept_page_count: 350,
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
const result = handleMemoryV3GateStats(30, fakeDb(rows));
|
|
111
|
+
const bucket = result.buckets.find((b) => b.pageCountRange === "200+")!;
|
|
112
|
+
expect(bucket.total).toBe(1);
|
|
113
|
+
expect(bucket.passed).toBe(0);
|
|
114
|
+
expect(bucket.scoredPassRate).toBe(0);
|
|
115
|
+
expect(bucket.reasons).toEqual({ fail_no_signal: 1 });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("unscored pass-open rows count in total but not scored", () => {
|
|
119
|
+
const rows = [
|
|
120
|
+
{
|
|
121
|
+
payload: makePayload({
|
|
122
|
+
pass: true,
|
|
123
|
+
reason: "dense_unavailable",
|
|
124
|
+
scored: false,
|
|
125
|
+
real_concept_page_count: 5,
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
const result = handleMemoryV3GateStats(30, fakeDb(rows));
|
|
130
|
+
const bucket = result.buckets.find((b) => b.pageCountRange === "0–9")!;
|
|
131
|
+
expect(bucket.total).toBe(1);
|
|
132
|
+
expect(bucket.scored).toBe(0);
|
|
133
|
+
expect(bucket.passed).toBe(1);
|
|
134
|
+
// No scored runs → scoredPassRate is null (unscored pass does not count)
|
|
135
|
+
expect(bucket.scoredPassRate).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("unscored pass mixed with scored fail does not inflate scoredPassRate", () => {
|
|
139
|
+
// Regression: before fix, pass-open + scored-fail in same bucket gave 100%
|
|
140
|
+
const rows = [
|
|
141
|
+
{
|
|
142
|
+
payload: makePayload({
|
|
143
|
+
pass: true,
|
|
144
|
+
reason: "dense_unavailable",
|
|
145
|
+
scored: false,
|
|
146
|
+
real_concept_page_count: 30,
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
payload: makePayload({
|
|
151
|
+
pass: false,
|
|
152
|
+
reason: "fail_no_signal",
|
|
153
|
+
scored: true,
|
|
154
|
+
real_concept_page_count: 40,
|
|
155
|
+
}),
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
const result = handleMemoryV3GateStats(30, fakeDb(rows));
|
|
159
|
+
const bucket = result.buckets.find((b) => b.pageCountRange === "10–49")!;
|
|
160
|
+
expect(bucket.total).toBe(2);
|
|
161
|
+
expect(bucket.scored).toBe(1);
|
|
162
|
+
expect(bucket.passed).toBe(1); // total passes (scored + unscored)
|
|
163
|
+
// Only the scored fail counts toward the rate — no scored passes
|
|
164
|
+
expect(bucket.scoredPassRate).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("rows with no real_concept_page_count go to unknownPageCount", () => {
|
|
168
|
+
const rows = [
|
|
169
|
+
{
|
|
170
|
+
payload: makePayload({
|
|
171
|
+
pass: false,
|
|
172
|
+
reason: "fail_no_signal",
|
|
173
|
+
scored: true,
|
|
174
|
+
}),
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
const result = handleMemoryV3GateStats(30, fakeDb(rows));
|
|
178
|
+
expect(result.unknownPageCount.total).toBe(1);
|
|
179
|
+
expect(result.unknownPageCount.passed).toBe(0);
|
|
180
|
+
expect(result.unknownPageCount.reasons).toEqual({ fail_no_signal: 1 });
|
|
181
|
+
for (const b of result.buckets) {
|
|
182
|
+
expect(b.total).toBe(0);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("mixed rows across multiple buckets are aggregated correctly", () => {
|
|
187
|
+
const rows: FakeRow[] = [
|
|
188
|
+
// 0–9 bucket: 1 unscored pass
|
|
189
|
+
{
|
|
190
|
+
payload: makePayload({
|
|
191
|
+
pass: true,
|
|
192
|
+
reason: "dense_disabled",
|
|
193
|
+
scored: false,
|
|
194
|
+
real_concept_page_count: 3,
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
// 10–49 bucket: 1 pass, 1 fail (both scored)
|
|
198
|
+
{
|
|
199
|
+
payload: makePayload({
|
|
200
|
+
pass: true,
|
|
201
|
+
reason: "dense_pass",
|
|
202
|
+
scored: true,
|
|
203
|
+
real_concept_page_count: 20,
|
|
204
|
+
}),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
payload: makePayload({
|
|
208
|
+
pass: false,
|
|
209
|
+
reason: "fail_no_signal",
|
|
210
|
+
scored: true,
|
|
211
|
+
real_concept_page_count: 45,
|
|
212
|
+
}),
|
|
213
|
+
},
|
|
214
|
+
// 50–199 bucket: 2 passes
|
|
215
|
+
{
|
|
216
|
+
payload: makePayload({
|
|
217
|
+
pass: true,
|
|
218
|
+
reason: "sparse_only_strong",
|
|
219
|
+
scored: true,
|
|
220
|
+
real_concept_page_count: 100,
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
payload: makePayload({
|
|
225
|
+
pass: true,
|
|
226
|
+
reason: "dense_pass",
|
|
227
|
+
scored: true,
|
|
228
|
+
real_concept_page_count: 150,
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
const result = handleMemoryV3GateStats(30, fakeDb(rows));
|
|
234
|
+
expect(result.totalRuns).toBe(5);
|
|
235
|
+
|
|
236
|
+
const b09 = result.buckets.find((b) => b.pageCountRange === "0–9")!;
|
|
237
|
+
expect(b09.total).toBe(1);
|
|
238
|
+
expect(b09.scored).toBe(0);
|
|
239
|
+
expect(b09.scoredPassRate).toBeNull();
|
|
240
|
+
|
|
241
|
+
const b1049 = result.buckets.find((b) => b.pageCountRange === "10–49")!;
|
|
242
|
+
expect(b1049.total).toBe(2);
|
|
243
|
+
expect(b1049.scored).toBe(2);
|
|
244
|
+
expect(b1049.passed).toBe(1);
|
|
245
|
+
expect(b1049.scoredPassRate).toBe(0.5);
|
|
246
|
+
expect(b1049.reasons).toEqual({ dense_pass: 1, fail_no_signal: 1 });
|
|
247
|
+
|
|
248
|
+
const b50199 = result.buckets.find((b) => b.pageCountRange === "50–199")!;
|
|
249
|
+
expect(b50199.scored).toBe(2);
|
|
250
|
+
expect(b50199.passed).toBe(2);
|
|
251
|
+
expect(b50199.scoredPassRate).toBe(1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("lookbackDays is clamped to 1–90", () => {
|
|
255
|
+
const r1 = handleMemoryV3GateStats(0, fakeDb([]));
|
|
256
|
+
expect(r1.lookbackDays).toBe(1);
|
|
257
|
+
const r2 = handleMemoryV3GateStats(999, fakeDb([]));
|
|
258
|
+
expect(r2.lookbackDays).toBe(90);
|
|
259
|
+
const r3 = handleMemoryV3GateStats(30, fakeDb([]));
|
|
260
|
+
expect(r3.lookbackDays).toBe(30);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("malformed JSON payload rows are skipped", () => {
|
|
264
|
+
const rows: FakeRow[] = [
|
|
265
|
+
{ payload: "not-valid-json" },
|
|
266
|
+
{
|
|
267
|
+
payload: makePayload({
|
|
268
|
+
pass: true,
|
|
269
|
+
reason: "dense_pass",
|
|
270
|
+
scored: true,
|
|
271
|
+
real_concept_page_count: 25,
|
|
272
|
+
}),
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
const result = handleMemoryV3GateStats(30, fakeDb(rows));
|
|
276
|
+
// Only the valid row is counted
|
|
277
|
+
expect(result.totalRuns).toBe(2); // totalRuns counts raw rows before parse
|
|
278
|
+
const bucket = result.buckets.find((b) => b.pageCountRange === "10–49")!;
|
|
279
|
+
expect(bucket.total).toBe(1);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -19,6 +19,7 @@ import { z } from "zod";
|
|
|
19
19
|
|
|
20
20
|
import { getConfig } from "../../../../config/loader.js";
|
|
21
21
|
import type { AssistantConfig } from "../../../../config/types.js";
|
|
22
|
+
import { getTelemetrySqlite } from "../../../../persistence/db-connection.js";
|
|
22
23
|
import {
|
|
23
24
|
ACTOR_PRINCIPALS,
|
|
24
25
|
type RoutePolicy,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
import type { RouteDefinition } from "../../../../runtime/routes/types.js";
|
|
27
28
|
import { getLogger } from "../logging.js";
|
|
28
29
|
import { backfillAllSections } from "../v3/maintain-job.js";
|
|
30
|
+
import { MEMORY_V3_INJECTION_GATE_CHECK_NAME } from "../v3/orchestrate.js";
|
|
29
31
|
import { invalidateLanes } from "../v3/shadow-plugin.js";
|
|
30
32
|
|
|
31
33
|
const log = getLogger("memory-v3-routes");
|
|
@@ -101,6 +103,211 @@ const WRITE_POLICY: RoutePolicy = {
|
|
|
101
103
|
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
102
104
|
};
|
|
103
105
|
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// gate-stats
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
const DEFAULT_LOOKBACK_DAYS = 30;
|
|
111
|
+
const MAX_LOOKBACK_DAYS = 90;
|
|
112
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
113
|
+
|
|
114
|
+
/** Corpus size buckets for gate-stats aggregation (in concept pages). */
|
|
115
|
+
const CORPUS_BUCKETS = [
|
|
116
|
+
{ label: "0–9", min: 0, max: 9 },
|
|
117
|
+
{ label: "10–49", min: 10, max: 49 },
|
|
118
|
+
{ label: "50–199", min: 50, max: 199 },
|
|
119
|
+
{ label: "200+", min: 200, max: Infinity },
|
|
120
|
+
] as const;
|
|
121
|
+
|
|
122
|
+
type BucketLabel = (typeof CORPUS_BUCKETS)[number]["label"];
|
|
123
|
+
|
|
124
|
+
interface BucketAcc {
|
|
125
|
+
total: number;
|
|
126
|
+
scored: number;
|
|
127
|
+
passed: number;
|
|
128
|
+
/** Passes that came from scored runs — the numerator for scoredPassRate. */
|
|
129
|
+
scoredPassed: number;
|
|
130
|
+
reasons: Record<string, number>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function corpusBucketLabel(pageCount: number): BucketLabel {
|
|
134
|
+
for (const b of CORPUS_BUCKETS) {
|
|
135
|
+
if (pageCount <= b.max) {
|
|
136
|
+
return b.label;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return "200+";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface GatePayloadDetail {
|
|
143
|
+
pass?: boolean;
|
|
144
|
+
reason?: string;
|
|
145
|
+
scored?: boolean;
|
|
146
|
+
real_concept_page_count?: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const GateStatsBucketSchema = z.object({
|
|
150
|
+
pageCountRange: z
|
|
151
|
+
.string()
|
|
152
|
+
.describe("Concept page count range (e.g. '10–49')"),
|
|
153
|
+
total: z
|
|
154
|
+
.number()
|
|
155
|
+
.describe("All gate runs in this bucket (scored + pass-open)"),
|
|
156
|
+
scored: z
|
|
157
|
+
.number()
|
|
158
|
+
.describe("Runs that weighed scores (dense lane available)"),
|
|
159
|
+
passed: z.number().describe("Runs that passed the gate"),
|
|
160
|
+
scoredPassRate: z
|
|
161
|
+
.number()
|
|
162
|
+
.nullable()
|
|
163
|
+
.describe(
|
|
164
|
+
"scoredPasses / scored (contested decisions only); null when no scored runs in bucket",
|
|
165
|
+
),
|
|
166
|
+
reasons: z
|
|
167
|
+
.record(z.string(), z.number())
|
|
168
|
+
.describe("Gate reason code → run count (dense_pass, fail_no_signal, …)"),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const GateStatsResponseSchema = z.object({
|
|
172
|
+
lookbackDays: z.number().describe("Lookback window applied"),
|
|
173
|
+
totalRuns: z.number().describe("Total gate runs found in the window"),
|
|
174
|
+
buckets: z
|
|
175
|
+
.array(GateStatsBucketSchema)
|
|
176
|
+
.describe("Stats by concept page count range, lean→large"),
|
|
177
|
+
unknownPageCount: z
|
|
178
|
+
.object({
|
|
179
|
+
total: z.number(),
|
|
180
|
+
passed: z.number(),
|
|
181
|
+
reasons: z.record(z.string(), z.number()),
|
|
182
|
+
})
|
|
183
|
+
.describe("Runs with no real_concept_page_count in telemetry detail"),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export type GateStatsResponse = z.infer<typeof GateStatsResponseSchema>;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Aggregate memory v3 injection gate runs from the telemetry outbox for the
|
|
190
|
+
* given lookback window. Groups runs by concept page count bucket and computes
|
|
191
|
+
* pass rates and reason distributions per bucket.
|
|
192
|
+
*
|
|
193
|
+
* Pass-open shortcuts (dense disabled/unavailable, gate threw) are included in
|
|
194
|
+
* `total` but not in `scored`, so `scoredPassRate` reflects only contested gate
|
|
195
|
+
* decisions — the signal that actually reveals whether the gate thresholds are
|
|
196
|
+
* calibrated correctly.
|
|
197
|
+
*/
|
|
198
|
+
export function handleMemoryV3GateStats(
|
|
199
|
+
lookbackDays: number = DEFAULT_LOOKBACK_DAYS,
|
|
200
|
+
db: ReturnType<typeof getTelemetrySqlite> = getTelemetrySqlite(),
|
|
201
|
+
): GateStatsResponse {
|
|
202
|
+
const days = Math.min(
|
|
203
|
+
Math.max(1, Math.floor(lookbackDays)),
|
|
204
|
+
MAX_LOOKBACK_DAYS,
|
|
205
|
+
);
|
|
206
|
+
const since = Date.now() - days * MS_PER_DAY;
|
|
207
|
+
|
|
208
|
+
const emptyBuckets = (): GateStatsResponse["buckets"] =>
|
|
209
|
+
CORPUS_BUCKETS.map((b) => ({
|
|
210
|
+
pageCountRange: b.label,
|
|
211
|
+
total: 0,
|
|
212
|
+
scored: 0,
|
|
213
|
+
passed: 0,
|
|
214
|
+
scoredPassRate: null,
|
|
215
|
+
reasons: {},
|
|
216
|
+
}));
|
|
217
|
+
if (!db) {
|
|
218
|
+
return {
|
|
219
|
+
lookbackDays: days,
|
|
220
|
+
totalRuns: 0,
|
|
221
|
+
buckets: emptyBuckets(),
|
|
222
|
+
unknownPageCount: { total: 0, passed: 0, reasons: {} },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const rows = db
|
|
227
|
+
.query(
|
|
228
|
+
/*sql*/ `
|
|
229
|
+
SELECT payload FROM telemetry_events
|
|
230
|
+
WHERE name = 'watchdog'
|
|
231
|
+
AND created_at >= ?
|
|
232
|
+
AND json_extract(payload, '$.check_name') = ?
|
|
233
|
+
`,
|
|
234
|
+
)
|
|
235
|
+
.all(since, MEMORY_V3_INJECTION_GATE_CHECK_NAME) as Array<{
|
|
236
|
+
payload: string;
|
|
237
|
+
}>;
|
|
238
|
+
|
|
239
|
+
const bucketMap = new Map<BucketLabel, BucketAcc>(
|
|
240
|
+
CORPUS_BUCKETS.map((b) => [
|
|
241
|
+
b.label,
|
|
242
|
+
{ total: 0, scored: 0, passed: 0, scoredPassed: 0, reasons: {} },
|
|
243
|
+
]),
|
|
244
|
+
);
|
|
245
|
+
const unknown: BucketAcc = {
|
|
246
|
+
total: 0,
|
|
247
|
+
scored: 0,
|
|
248
|
+
passed: 0,
|
|
249
|
+
scoredPassed: 0,
|
|
250
|
+
reasons: {},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
let detail: GatePayloadDetail;
|
|
255
|
+
try {
|
|
256
|
+
const parsed = JSON.parse(row.payload) as { detail?: GatePayloadDetail };
|
|
257
|
+
detail = parsed.detail ?? {};
|
|
258
|
+
} catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const pageCount =
|
|
263
|
+
typeof detail.real_concept_page_count === "number"
|
|
264
|
+
? detail.real_concept_page_count
|
|
265
|
+
: null;
|
|
266
|
+
|
|
267
|
+
const acc =
|
|
268
|
+
pageCount !== null
|
|
269
|
+
? (bucketMap.get(corpusBucketLabel(pageCount)) ?? unknown)
|
|
270
|
+
: unknown;
|
|
271
|
+
|
|
272
|
+
acc.total += 1;
|
|
273
|
+
if (detail.scored === true) {
|
|
274
|
+
acc.scored += 1;
|
|
275
|
+
if (detail.pass === true) {
|
|
276
|
+
acc.scoredPassed += 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (detail.pass === true) {
|
|
280
|
+
acc.passed += 1;
|
|
281
|
+
}
|
|
282
|
+
const reason =
|
|
283
|
+
typeof detail.reason === "string" ? detail.reason : "unknown";
|
|
284
|
+
acc.reasons[reason] = (acc.reasons[reason] ?? 0) + 1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const buckets = CORPUS_BUCKETS.map((b) => {
|
|
288
|
+
const acc = bucketMap.get(b.label)!;
|
|
289
|
+
return {
|
|
290
|
+
pageCountRange: b.label,
|
|
291
|
+
total: acc.total,
|
|
292
|
+
scored: acc.scored,
|
|
293
|
+
passed: acc.passed,
|
|
294
|
+
scoredPassRate: acc.scored > 0 ? acc.scoredPassed / acc.scored : null,
|
|
295
|
+
reasons: acc.reasons,
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
lookbackDays: days,
|
|
301
|
+
totalRuns: rows.length,
|
|
302
|
+
buckets,
|
|
303
|
+
unknownPageCount: {
|
|
304
|
+
total: unknown.total,
|
|
305
|
+
passed: unknown.passed,
|
|
306
|
+
reasons: unknown.reasons,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
104
311
|
export const ROUTES: RouteDefinition[] = [
|
|
105
312
|
{
|
|
106
313
|
operationId: "memory_v3_rebuild_index",
|
|
@@ -197,6 +197,7 @@ const { memoryV2ConsolidateJob, CONSOLIDATION_FAILURE_CHECKPOINT_KEY } =
|
|
|
197
197
|
const { CUTOFF_PLACEHOLDER, CONSOLIDATION_PROMPT } =
|
|
198
198
|
await import("../prompts/consolidation.js");
|
|
199
199
|
const { invalidatePageIndex } = await import("../page-index.js");
|
|
200
|
+
const { formatRememberEntry } = await import("../../buffer-format.js");
|
|
200
201
|
|
|
201
202
|
// The handler only reads `config.memory.enabled`, `config.memory.v2.enabled`,
|
|
202
203
|
// `config.memory.v2.consolidation_prompt_path`, and
|
|
@@ -437,6 +438,38 @@ describe("memoryV2ConsolidateJob — chunked cutoff (consolidation_max_entries_p
|
|
|
437
438
|
expect(outcome.cutoff).not.toBe("[meeting-notes]");
|
|
438
439
|
});
|
|
439
440
|
|
|
441
|
+
test("a fact whose body holds a canonical entry line counts once and never supplies the cutoff", async () => {
|
|
442
|
+
// Written through the real writer, so this covers the round trip rather
|
|
443
|
+
// than a hand-built buffer: the body line carries the exact shape an entry
|
|
444
|
+
// opening has. Counting it would both inflate the per-run budget and let
|
|
445
|
+
// the cutoff be drawn from inside a fact, deferring entries against a
|
|
446
|
+
// timestamp the user never filed anything at.
|
|
447
|
+
writeFileSync(
|
|
448
|
+
bufferPath(),
|
|
449
|
+
formatRememberEntry(
|
|
450
|
+
"Alice's rollout plan:\n- [Apr 28, 9:00 AM] cutover",
|
|
451
|
+
new Date(2026, 3, 27, 9, 0),
|
|
452
|
+
) +
|
|
453
|
+
formatRememberEntry(
|
|
454
|
+
"Bob takes his coffee black.",
|
|
455
|
+
new Date(2026, 3, 27, 9, 1),
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const outcome = await memoryV2ConsolidateJob(
|
|
460
|
+
makeJob(),
|
|
461
|
+
configWithMaxEntries(2),
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(outcome.kind).toBe("invoked");
|
|
465
|
+
if (outcome.kind !== "invoked") {
|
|
466
|
+
throw new Error("unreachable");
|
|
467
|
+
}
|
|
468
|
+
// Two facts, cap 2 → no chunking. Three would chunk and defer one.
|
|
469
|
+
expect(outcome.deferredEntries).toBe(0);
|
|
470
|
+
expect(outcome.cutoff).not.toBe("Apr 28, 9:00 AM");
|
|
471
|
+
});
|
|
472
|
+
|
|
440
473
|
test("multi-line entries under the cap → full-buffer cutoff even when raw line count exceeds it", async () => {
|
|
441
474
|
writeFileSync(
|
|
442
475
|
bufferPath(),
|