@vellumai/assistant 0.11.3 → 0.11.4-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +139 -37
- 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 +17 -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 +177 -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__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- 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/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -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/api/surfaces.ts +7 -3
- 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 +19 -3
- 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/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- 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 +83 -12
- 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 +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- 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/plugin-api/vision-support.test.ts +1 -1
- 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-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -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/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- 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/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -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
|
@@ -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(),
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - Reads the four files in canonical order and joins them under headings.
|
|
6
6
|
* - Skips empty / missing files.
|
|
7
7
|
* - Returns null when every file is empty or missing.
|
|
8
|
+
* - Caps the Buffer section at `consolidation_max_buffer_lines`.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -33,12 +34,19 @@ mock.module("../../../../../util/logger.js", () => ({
|
|
|
33
34
|
|
|
34
35
|
let configMemoryV2Enabled = true;
|
|
35
36
|
let configMemoryEnabled = true;
|
|
37
|
+
// Mirrors the `memory.v2.consolidation_max_buffer_lines` schema default; the
|
|
38
|
+
// Buffer-cap tests move it.
|
|
39
|
+
let configMaxBufferLines: number | null = 100;
|
|
36
40
|
|
|
37
|
-
// static-context reads its gates
|
|
41
|
+
// static-context reads its gates and the substrate tuning through the
|
|
42
|
+
// plugin's own config accessor.
|
|
38
43
|
mock.module("../../config.js", () => ({
|
|
39
44
|
getMemoryConfig: () => ({
|
|
40
45
|
enabled: configMemoryEnabled,
|
|
41
|
-
v2: {
|
|
46
|
+
v2: {
|
|
47
|
+
enabled: configMemoryV2Enabled,
|
|
48
|
+
consolidation_max_buffer_lines: configMaxBufferLines,
|
|
49
|
+
},
|
|
42
50
|
}),
|
|
43
51
|
}));
|
|
44
52
|
|
|
@@ -70,6 +78,7 @@ describe("readMemoryV2StaticContent", () => {
|
|
|
70
78
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
71
79
|
configMemoryV2Enabled = true;
|
|
72
80
|
configMemoryEnabled = true;
|
|
81
|
+
configMaxBufferLines = 100;
|
|
73
82
|
});
|
|
74
83
|
|
|
75
84
|
afterEach(() => {
|
|
@@ -165,6 +174,194 @@ describe("readMemoryV2StaticContent", () => {
|
|
|
165
174
|
});
|
|
166
175
|
});
|
|
167
176
|
|
|
177
|
+
describe("readMemoryV2StaticContent Buffer cap", () => {
|
|
178
|
+
/** `n` timestamped buffer entries, oldest first, in `remember()`'s shape. */
|
|
179
|
+
function bufferEntries(n: number): string {
|
|
180
|
+
return Array.from(
|
|
181
|
+
{ length: n },
|
|
182
|
+
(_, i) => `- [Jan 1, 9:00 AM] entry-${i}`,
|
|
183
|
+
).join("\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
188
|
+
configMemoryV2Enabled = true;
|
|
189
|
+
configMemoryEnabled = true;
|
|
190
|
+
configMaxBufferLines = 10;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
afterEach(() => {
|
|
194
|
+
cleanupMemoryDir();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("passes a buffer at or under the cap through byte-identically", () => {
|
|
198
|
+
const buffer = bufferEntries(10);
|
|
199
|
+
writeMemoryFile("buffer.md", buffer);
|
|
200
|
+
|
|
201
|
+
expect(readMemoryV2StaticContent()).toBe(`## Buffer\n\n${buffer}`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("keeps the newest entries and drops the oldest when over the cap", () => {
|
|
205
|
+
writeMemoryFile("buffer.md", bufferEntries(30));
|
|
206
|
+
|
|
207
|
+
const text = readMemoryV2StaticContent()!;
|
|
208
|
+
expect(text).toContain(
|
|
209
|
+
"(Older entries trimmed. Read memory/buffer.md for the full backlog.)",
|
|
210
|
+
);
|
|
211
|
+
// The cap counts non-empty lines, matching the scheduler's
|
|
212
|
+
// `countBufferLines`, so exactly the newest 10 entries survive.
|
|
213
|
+
const kept = text
|
|
214
|
+
.split("\n")
|
|
215
|
+
.filter((line) => line.startsWith("- ["))
|
|
216
|
+
.map((line) => line.slice(line.lastIndexOf(" ") + 1));
|
|
217
|
+
expect(kept).toEqual(
|
|
218
|
+
Array.from({ length: 10 }, (_, i) => `entry-${i + 20}`),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("never opens mid-entry when the cap lands inside a multiline fact", () => {
|
|
223
|
+
// The 5-line fact straddles the cap: counting back 10 non-empty lines
|
|
224
|
+
// lands on one of its continuation lines. Injecting from there would show
|
|
225
|
+
// orphan bullets with no timestamp and no opening clause.
|
|
226
|
+
const multiline = [
|
|
227
|
+
"- [Jan 1, 9:00 AM] the straddling fact",
|
|
228
|
+
" - [ ] a checklist item inside the fact",
|
|
229
|
+
" continuation prose",
|
|
230
|
+
" - another bullet",
|
|
231
|
+
" closing line",
|
|
232
|
+
].join("\n");
|
|
233
|
+
writeMemoryFile(
|
|
234
|
+
"buffer.md",
|
|
235
|
+
`${bufferEntries(20)}\n${multiline}\n${bufferEntries(8)}`,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const text = readMemoryV2StaticContent()!;
|
|
239
|
+
const body = text.slice(text.indexOf("buffer.md for the full backlog.)\n"));
|
|
240
|
+
const firstLine = body.split("\n")[1]!;
|
|
241
|
+
expect(firstLine).toMatch(/^- \[Jan 1, 9:00 AM\]/);
|
|
242
|
+
// The partial entry is dropped whole: none of its continuation lines
|
|
243
|
+
// survive on their own.
|
|
244
|
+
expect(text).not.toContain("continuation prose");
|
|
245
|
+
expect(text).not.toContain("the straddling fact");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("keeps the newest entry attributable when its body alone exceeds the cap", () => {
|
|
249
|
+
// The newest entry has no successor to fall back to: counting back 10
|
|
250
|
+
// non-empty lines lands inside its body and there is no later timestamped
|
|
251
|
+
// entry to open on. Dropping it would leave the section with no facts at
|
|
252
|
+
// all, so the entry's opening line is kept and its head elided.
|
|
253
|
+
writeMemoryFile(
|
|
254
|
+
"buffer.md",
|
|
255
|
+
[
|
|
256
|
+
...Array.from(
|
|
257
|
+
{ length: 20 },
|
|
258
|
+
(_, i) => `- [Jan 1, 9:00 AM] entry-${i}`,
|
|
259
|
+
),
|
|
260
|
+
"- [Jan 2, 9:00 AM] the oversized fact",
|
|
261
|
+
...Array.from({ length: 14 }, (_, i) => ` body line ${i}`),
|
|
262
|
+
].join("\n"),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const text = readMemoryV2StaticContent()!;
|
|
266
|
+
const body = text
|
|
267
|
+
.slice(text.indexOf("full backlog.)\n") + "full backlog.)\n".length)
|
|
268
|
+
.split("\n")
|
|
269
|
+
.filter((line) => line.trim().length > 0);
|
|
270
|
+
|
|
271
|
+
// Opens on the entry's own timestamped line, never on an orphan body line.
|
|
272
|
+
expect(body[0]).toBe("- [Jan 2, 9:00 AM] the oversized fact");
|
|
273
|
+
expect(body[1]).toBe(
|
|
274
|
+
"(This entry's body was trimmed. Read memory/buffer.md for the rest of it.)",
|
|
275
|
+
);
|
|
276
|
+
// The retained tail is the newest end of the body, not its head.
|
|
277
|
+
expect(body.at(-1)).toBe(" body line 13");
|
|
278
|
+
expect(body).toContain(" body line 4");
|
|
279
|
+
expect(body).not.toContain(" body line 3");
|
|
280
|
+
// Bounded at the cap plus exactly the opening line and the marker, so the
|
|
281
|
+
// oversized entry cannot reintroduce an unbounded injection.
|
|
282
|
+
expect(body).toHaveLength(12);
|
|
283
|
+
// The older entries are still dropped.
|
|
284
|
+
expect(text).not.toContain("entry-19");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("does not claim a trim when the newest entry's body exactly fills the cap", () => {
|
|
288
|
+
// The cut lands on the line right after the opening, so nothing of the
|
|
289
|
+
// entry was elided and the marker would be a lie.
|
|
290
|
+
writeMemoryFile(
|
|
291
|
+
"buffer.md",
|
|
292
|
+
[
|
|
293
|
+
...Array.from(
|
|
294
|
+
{ length: 20 },
|
|
295
|
+
(_, i) => `- [Jan 1, 9:00 AM] entry-${i}`,
|
|
296
|
+
),
|
|
297
|
+
"- [Jan 2, 9:00 AM] the exact-fit fact",
|
|
298
|
+
...Array.from({ length: 10 }, (_, i) => ` body line ${i}`),
|
|
299
|
+
].join("\n"),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const text = readMemoryV2StaticContent()!;
|
|
303
|
+
const body = text
|
|
304
|
+
.slice(text.indexOf("full backlog.)\n") + "full backlog.)\n".length)
|
|
305
|
+
.split("\n")
|
|
306
|
+
.filter((line) => line.trim().length > 0);
|
|
307
|
+
|
|
308
|
+
expect(body[0]).toBe("- [Jan 2, 9:00 AM] the exact-fit fact");
|
|
309
|
+
expect(text).not.toContain("This entry's body was trimmed");
|
|
310
|
+
// The entry survives whole: opening line plus its full 10-line body.
|
|
311
|
+
expect(body).toHaveLength(11);
|
|
312
|
+
expect(body.at(-1)).toBe(" body line 9");
|
|
313
|
+
expect(body).toContain(" body line 0");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("keeps a multiline entry intact when it sits fully inside the cap", () => {
|
|
317
|
+
const multiline = [
|
|
318
|
+
"- [Jan 1, 9:00 AM] a fact with a body",
|
|
319
|
+
" second line of the same fact",
|
|
320
|
+
].join("\n");
|
|
321
|
+
writeMemoryFile("buffer.md", `${bufferEntries(20)}\n${multiline}`);
|
|
322
|
+
|
|
323
|
+
const text = readMemoryV2StaticContent()!;
|
|
324
|
+
expect(text).toContain("a fact with a body");
|
|
325
|
+
expect(text).toContain("second line of the same fact");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("falls back to the line cut for a buffer with no timestamped entries", () => {
|
|
329
|
+
// A hand-written buffer that never went through `remember()` has no entry
|
|
330
|
+
// structure to preserve, so the line-based cut stands rather than dropping
|
|
331
|
+
// everything.
|
|
332
|
+
const handWritten = Array.from(
|
|
333
|
+
{ length: 30 },
|
|
334
|
+
(_, i) => `just a line ${i}`,
|
|
335
|
+
).join("\n");
|
|
336
|
+
writeMemoryFile("buffer.md", handWritten);
|
|
337
|
+
|
|
338
|
+
const text = readMemoryV2StaticContent()!;
|
|
339
|
+
expect(text).toContain("just a line 29");
|
|
340
|
+
expect(text).not.toContain("just a line 0");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("leaves the buffer unbounded when the size trigger is disabled", () => {
|
|
344
|
+
configMaxBufferLines = null;
|
|
345
|
+
const buffer = bufferEntries(30);
|
|
346
|
+
writeMemoryFile("buffer.md", buffer);
|
|
347
|
+
|
|
348
|
+
expect(readMemoryV2StaticContent()).toBe(`## Buffer\n\n${buffer}`);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("caps only the Buffer section, never the curated views", () => {
|
|
352
|
+
const long = Array.from({ length: 30 }, (_, i) => `line-${i}`).join("\n");
|
|
353
|
+
writeMemoryFile("essentials.md", long);
|
|
354
|
+
writeMemoryFile("threads.md", long);
|
|
355
|
+
writeMemoryFile("recent.md", long);
|
|
356
|
+
writeMemoryFile("buffer.md", bufferEntries(30));
|
|
357
|
+
|
|
358
|
+
const lines = readMemoryV2StaticContent()!.split("\n");
|
|
359
|
+
expect(lines.filter((line) => line === "line-0")).toHaveLength(3);
|
|
360
|
+
expect(lines.filter((line) => line === "line-29")).toHaveLength(3);
|
|
361
|
+
expect(lines.some((line) => line.endsWith("entry-0"))).toBe(false);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
168
365
|
describe("shouldExposePersonalMemory", () => {
|
|
169
366
|
test("allows guardian-trusted local conversations", () => {
|
|
170
367
|
expect(
|
|
@@ -91,7 +91,10 @@ import {
|
|
|
91
91
|
type MemoryJobType,
|
|
92
92
|
} from "../../../../persistence/jobs-store.js";
|
|
93
93
|
import { runBackgroundJob } from "../../../../runtime/background-job-runner.js";
|
|
94
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
formatBufferTimestamp,
|
|
96
|
+
matchBufferEntryStart,
|
|
97
|
+
} from "../buffer-format.js";
|
|
95
98
|
import { getLogger } from "../logging.js";
|
|
96
99
|
import { getWorkspaceDir } from "../paths.js";
|
|
97
100
|
import {
|
|
@@ -597,32 +600,36 @@ function readBufferContent(bufferPath: string): string {
|
|
|
597
600
|
|
|
598
601
|
/**
|
|
599
602
|
* Extract the bracketed timestamp from a `buffer.md` entry line
|
|
600
|
-
* (`- [Mon D, h:mm AM/PM] …`, see
|
|
601
|
-
* it can serve directly as a consolidation cutoff
|
|
602
|
-
* "timestamp
|
|
603
|
-
* shape.
|
|
603
|
+
* (`- [Mon D, h:mm AM/PM] …`, see {@link formatRememberEntry}). Returned
|
|
604
|
+
* verbatim so it can serve directly as a consolidation cutoff: both sides of
|
|
605
|
+
* the agent's "timestamp >= cutoff" comparison then share the exact
|
|
606
|
+
* {@link formatBufferTimestamp} shape.
|
|
604
607
|
*
|
|
605
|
-
*
|
|
606
|
-
* continuation lines
|
|
607
|
-
*
|
|
608
|
-
* would inflate the per-run budget
|
|
609
|
-
*
|
|
610
|
-
* timestamped entry start.
|
|
608
|
+
* Recognition is delegated to the shared matcher, so a remembered fact's
|
|
609
|
+
* continuation lines never register as entries. That matters here beyond
|
|
610
|
+
* tidiness: counting a fact's `- [ ] …` checklist lines or an indented
|
|
611
|
+
* entry-shaped body line as entries would inflate the per-run budget, or hand
|
|
612
|
+
* the agent a cutoff drawn from the middle of a fact.
|
|
611
613
|
*/
|
|
612
614
|
function extractBufferEntryTimestamp(line: string): string | null {
|
|
613
|
-
|
|
614
|
-
line,
|
|
615
|
-
);
|
|
616
|
-
return match ? match[1] : null;
|
|
615
|
+
return matchBufferEntryStart(line)?.timestamp ?? null;
|
|
617
616
|
}
|
|
618
617
|
|
|
619
618
|
/**
|
|
620
619
|
* Count non-empty lines in `memory/buffer.md`. Used by the scheduler to
|
|
621
620
|
* implement the size-based consolidation trigger. Missing file → 0.
|
|
622
621
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
622
|
+
* Lines, deliberately, not entries. A multiline `remember()` fact is one
|
|
623
|
+
* entry spread over several lines, so the two counts diverge and each answers
|
|
624
|
+
* a different question. This trigger and the injected-Buffer cap that reuses
|
|
625
|
+
* it (`capBufferSection` in `static-context.ts`) both care about how much
|
|
626
|
+
* context the buffer costs, which scales with lines. The per-run budget
|
|
627
|
+
* `consolidation_max_entries_per_run` cares about how many facts the agent
|
|
628
|
+
* must file, so it counts entry-start lines instead.
|
|
629
|
+
*
|
|
630
|
+
* Do not "fix" this to count entries: a single 200-line fact is a context
|
|
631
|
+
* problem the trigger should fire on, even though it is one entry. Blank
|
|
632
|
+
* lines and trailing newlines don't inflate the count.
|
|
626
633
|
*/
|
|
627
634
|
export function countBufferLines(bufferPath: string): number {
|
|
628
635
|
return countNonEmptyLines(readBufferContent(bufferPath));
|
|
@@ -22,6 +22,13 @@ export interface SkillCapabilityInput {
|
|
|
22
22
|
*/
|
|
23
23
|
export const ALWAYS_CANDIDATE_CARD_CHARS = 900;
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Character budget for an ordinary skill's capability statement. A statement
|
|
27
|
+
* that overruns is hard-truncated, dropping its trailing activation hints and
|
|
28
|
+
* its entire avoid-when list from what the selector sees.
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_CARD_CHARS = 500;
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* Render the prose-style capability statement embedded into the unified
|
|
27
34
|
* `memory_v2_concept_pages` Qdrant collection (under the `skills/<id>` slug
|
|
@@ -32,7 +39,7 @@ export const ALWAYS_CANDIDATE_CARD_CHARS = 900;
|
|
|
32
39
|
*/
|
|
33
40
|
export function buildSkillContent(
|
|
34
41
|
input: SkillCapabilityInput,
|
|
35
|
-
maxChars =
|
|
42
|
+
maxChars: number = DEFAULT_CARD_CHARS,
|
|
36
43
|
): string {
|
|
37
44
|
const list = maxChars > 500;
|
|
38
45
|
let content = `The "${input.displayName}" skill (${input.id}) is available. ${input.description}.`;
|