@vellumai/assistant 0.5.16 → 0.6.0
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 +1 -1
- package/Dockerfile +0 -3
- package/knip.json +2 -1
- package/openapi.yaml +660 -80
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +68 -0
- package/src/__tests__/agent-loop.test.ts +0 -32
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
- package/src/__tests__/anthropic-provider.test.ts +57 -3
- package/src/__tests__/app-compiler.test.ts +120 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/call-conversation-messages.test.ts +2 -6
- package/src/__tests__/call-domain.test.ts +2 -6
- package/src/__tests__/call-pointer-messages.test.ts +2 -14
- package/src/__tests__/call-recovery.test.ts +2 -6
- package/src/__tests__/call-routes-http.test.ts +2 -6
- package/src/__tests__/call-store.test.ts +2 -6
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
- package/src/__tests__/canonical-guardian-store.test.ts +2 -6
- package/src/__tests__/channel-delivery-store.test.ts +2 -6
- package/src/__tests__/channel-retry-sweep.test.ts +2 -6
- package/src/__tests__/checker.test.ts +25 -3
- package/src/__tests__/clawhub.test.ts +54 -24
- package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
- package/src/__tests__/cli-memory.test.ts +74 -69
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-set-platform-guard.test.ts +302 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
- package/src/__tests__/contacts-tools.test.ts +31 -0
- package/src/__tests__/context-overflow-reducer.test.ts +86 -0
- package/src/__tests__/context-token-estimator.test.ts +175 -10
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
- package/src/__tests__/conversation-agent-loop.test.ts +9 -0
- package/src/__tests__/conversation-attachments.test.ts +2 -6
- package/src/__tests__/conversation-attention-store.test.ts +2 -6
- package/src/__tests__/conversation-clear-safety.test.ts +2 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
- package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
- package/src/__tests__/conversation-disk-view.test.ts +2 -6
- package/src/__tests__/conversation-error.test.ts +33 -2
- package/src/__tests__/conversation-fork-crud.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
- package/src/__tests__/conversation-media-retry.test.ts +91 -0
- package/src/__tests__/conversation-starter-routes.test.ts +20 -11
- package/src/__tests__/conversation-store.test.ts +2 -6
- package/src/__tests__/conversation-usage.test.ts +2 -6
- package/src/__tests__/conversation-wipe.test.ts +11 -408
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +2 -0
- package/src/__tests__/followup-tools.test.ts +2 -6
- package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
- package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
- package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
- package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
- package/src/__tests__/guardian-action-store.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
- package/src/__tests__/guardian-dispatch.test.ts +2 -6
- package/src/__tests__/guardian-grant-minting.test.ts +2 -14
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
- package/src/__tests__/guardian-routing-state.test.ts +2 -6
- package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
- package/src/__tests__/injection-block.test.ts +154 -0
- package/src/__tests__/install-meta.test.ts +506 -0
- package/src/__tests__/install-skill-routing.test.ts +292 -0
- package/src/__tests__/invite-redemption-service.test.ts +2 -6
- package/src/__tests__/invite-routes-http.test.ts +2 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
- package/src/__tests__/list-messages-attachments.test.ts +2 -6
- package/src/__tests__/llm-context-route-provider.test.ts +2 -6
- package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
- package/src/__tests__/llm-usage-store.test.ts +2 -6
- package/src/__tests__/log-export-workspace.test.ts +2 -6
- package/src/__tests__/managed-store.test.ts +38 -11
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
- package/src/__tests__/memory-recall-log-store.test.ts +2 -6
- package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
- package/src/__tests__/non-member-access-request.test.ts +2 -6
- package/src/__tests__/notification-guardian-path.test.ts +2 -6
- package/src/__tests__/oauth-cli.test.ts +364 -2
- package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
- package/src/__tests__/outlook-attachments.test.ts +301 -0
- package/src/__tests__/outlook-automation-tools.test.ts +425 -0
- package/src/__tests__/outlook-categories.test.ts +212 -0
- package/src/__tests__/outlook-client-automation.test.ts +246 -0
- package/src/__tests__/outlook-compose-tools.test.ts +325 -0
- package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
- package/src/__tests__/outlook-email-watcher.test.ts +322 -0
- package/src/__tests__/outlook-follow-up.test.ts +196 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
- package/src/__tests__/outlook-trash.test.ts +77 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
- package/src/__tests__/platform-callback-registration.test.ts +4 -4
- package/src/__tests__/playbook-execution.test.ts +76 -80
- package/src/__tests__/playbook-tools.test.ts +5 -7
- package/src/__tests__/provider-error-scenarios.test.ts +21 -0
- package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/require-fresh-approval.test.ts +64 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
- package/src/__tests__/runtime-events-sse.test.ts +2 -6
- package/src/__tests__/schedule-store.test.ts +2 -6
- package/src/__tests__/schedule-tools.test.ts +2 -6
- package/src/__tests__/scheduler-recurrence.test.ts +1 -5
- package/src/__tests__/scoped-approval-grants.test.ts +2 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
- package/src/__tests__/search-skills-unified.test.ts +421 -0
- package/src/__tests__/secret-onetime-send.test.ts +2 -0
- package/src/__tests__/send-endpoint-busy.test.ts +2 -6
- package/src/__tests__/sequence-store.test.ts +2 -6
- package/src/__tests__/server-history-render.test.ts +2 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
- package/src/__tests__/skill-feature-flags.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +11 -11
- package/src/__tests__/skill-memory.test.ts +140 -98
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +1 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -6
- package/src/__tests__/task-compiler.test.ts +2 -6
- package/src/__tests__/task-management-tools.test.ts +2 -6
- package/src/__tests__/task-memory-cleanup.test.ts +173 -229
- package/src/__tests__/task-runner.test.ts +2 -6
- package/src/__tests__/task-scheduler.test.ts +2 -6
- package/src/__tests__/test-preload.ts +3 -0
- package/src/__tests__/tool-approval-handler.test.ts +2 -6
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
- package/src/__tests__/trust-store.test.ts +1 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -6
- package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
- package/src/__tests__/trusted-contact-verification.test.ts +2 -6
- package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
- package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
- package/src/__tests__/usage-routes.test.ts +2 -6
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-invite-redemption.test.ts +2 -6
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
- package/src/__tests__/voice-session-bridge.test.ts +2 -6
- package/src/__tests__/volume-security-guard.test.ts +2 -0
- package/src/__tests__/workspace-lifecycle.test.ts +29 -1
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
- package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
- package/src/__tests__/workspace-policy.test.ts +1 -1
- package/src/agent/attachments.ts +7 -2
- package/src/agent/image-optimize.ts +165 -0
- package/src/agent/loop.ts +1 -15
- package/src/bundler/app-compiler.ts +179 -2
- package/src/bundler/package-resolver.ts +3 -5
- package/src/cli/__tests__/notifications.test.ts +1 -2
- package/src/cli/cli-memory.ts +67 -64
- package/src/cli/commands/avatar.ts +3 -3
- package/src/cli/commands/config.ts +26 -13
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/memory.ts +41 -55
- package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
- package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
- package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
- package/src/cli/commands/oauth/connect.ts +11 -6
- package/src/cli/commands/oauth/mode.ts +7 -0
- package/src/cli/commands/oauth/shared.ts +39 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
- package/src/cli/commands/platform/index.ts +16 -16
- package/src/cli/commands/skills.ts +88 -16
- package/src/cli/commands/trust.ts +2 -2
- package/src/cli/lib/daemon-credential-client.ts +2 -3
- package/src/config/bundled-skills/acp/TOOLS.json +1 -1
- package/src/config/bundled-skills/contacts/SKILL.md +0 -1
- package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
- package/src/config/bundled-skills/gmail/SKILL.md +2 -10
- package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
- package/src/config/bundled-skills/messaging/SKILL.md +10 -18
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
- package/src/config/bundled-skills/outlook/SKILL.md +189 -0
- package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
- package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
- package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
- package/src/config/bundled-skills/slack/SKILL.md +1 -7
- package/src/config/bundled-tool-registry.ts +56 -4
- package/src/config/env-registry.ts +15 -8
- package/src/config/feature-flag-registry.json +21 -124
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/schemas/timeouts.ts +1 -1
- package/src/config/skills.ts +18 -7
- package/src/context/token-estimator.ts +25 -18
- package/src/context/window-manager.ts +6 -2
- package/src/credential-execution/process-manager.ts +3 -1
- package/src/daemon/context-overflow-reducer.ts +46 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
- package/src/daemon/conversation-agent-loop.ts +96 -61
- package/src/daemon/conversation-error.ts +31 -8
- package/src/daemon/conversation-lifecycle.ts +33 -0
- package/src/daemon/conversation-media-retry.ts +85 -7
- package/src/daemon/conversation-notifiers.ts +4 -1
- package/src/daemon/conversation-runtime-assembly.ts +5 -0
- package/src/daemon/conversation.ts +41 -2
- package/src/daemon/daemon-control.ts +8 -2
- package/src/daemon/handlers/shared.ts +22 -12
- package/src/daemon/handlers/skills.ts +416 -202
- package/src/daemon/lifecycle.ts +40 -1
- package/src/daemon/main.ts +5 -1
- package/src/daemon/message-types/conversations.ts +4 -1
- package/src/daemon/message-types/messages.ts +3 -1
- package/src/daemon/message-types/skills.ts +97 -36
- package/src/daemon/providers-setup.ts +5 -0
- package/src/daemon/server.ts +11 -2
- package/src/daemon/tool-side-effects.ts +27 -5
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/hooks/cli.ts +2 -2
- package/src/hooks/runner.ts +15 -38
- package/src/inbound/platform-callback-registration.ts +14 -14
- package/src/memory/admin.ts +11 -45
- package/src/memory/conversation-bootstrap.ts +2 -0
- package/src/memory/conversation-crud.ts +242 -348
- package/src/memory/conversation-group-migration.ts +157 -0
- package/src/memory/conversation-queries.ts +4 -2
- package/src/memory/db-init.ts +30 -3
- package/src/memory/embed.ts +73 -0
- package/src/memory/embedding-backend.ts +8 -14
- package/src/memory/embedding-runtime-manager.ts +12 -114
- package/src/memory/fingerprint.ts +2 -2
- package/src/memory/graph/bootstrap.ts +512 -0
- package/src/memory/graph/capability-seed.ts +297 -0
- package/src/memory/graph/consolidation.ts +691 -0
- package/src/memory/graph/conversation-graph-memory.ts +630 -0
- package/src/memory/graph/decay.test.ts +208 -0
- package/src/memory/graph/decay.ts +195 -0
- package/src/memory/graph/extraction-job.ts +69 -0
- package/src/memory/graph/extraction.test.ts +936 -0
- package/src/memory/graph/extraction.ts +1254 -0
- package/src/memory/graph/graph-search.ts +266 -0
- package/src/memory/graph/image-ref-utils.ts +29 -0
- package/src/memory/graph/injection.test.ts +513 -0
- package/src/memory/graph/injection.ts +439 -0
- package/src/memory/graph/inspect.ts +534 -0
- package/src/memory/graph/narrative.ts +267 -0
- package/src/memory/graph/pattern-scan.ts +269 -0
- package/src/memory/graph/retriever.ts +1008 -0
- package/src/memory/graph/scoring.test.ts +548 -0
- package/src/memory/graph/scoring.ts +232 -0
- package/src/memory/graph/serendipity.ts +65 -0
- package/src/memory/graph/store.test.ts +1050 -0
- package/src/memory/graph/store.ts +699 -0
- package/src/memory/graph/tool-handlers.ts +426 -0
- package/src/memory/graph/tools.ts +141 -0
- package/src/memory/graph/triggers.test.ts +487 -0
- package/src/memory/graph/triggers.ts +223 -0
- package/src/memory/graph/types.ts +271 -0
- package/src/memory/group-crud.ts +191 -0
- package/src/memory/indexer.ts +37 -19
- package/src/memory/job-handlers/cleanup.ts +0 -53
- package/src/memory/job-handlers/conversation-starters.ts +91 -53
- package/src/memory/job-handlers/embedding.ts +5 -31
- package/src/memory/job-handlers/index-maintenance.ts +23 -11
- package/src/memory/job-handlers/summarization.ts +32 -17
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +50 -70
- package/src/memory/jobs-worker.ts +147 -112
- package/src/memory/message-content.ts +1 -0
- package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
- package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
- package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
- package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-client.ts +44 -17
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/memory-graph.ts +139 -0
- package/src/memory/search/semantic.ts +47 -91
- package/src/memory/task-memory-cleanup.ts +28 -50
- package/src/messaging/providers/outlook/adapter.ts +8 -1
- package/src/messaging/providers/outlook/client.ts +299 -0
- package/src/messaging/providers/outlook/types.ts +118 -0
- package/src/notifications/adapters/macos.ts +1 -0
- package/src/notifications/copy-composer.ts +9 -0
- package/src/notifications/signal.ts +16 -0
- package/src/oauth/seed-providers.ts +2 -1
- package/src/permissions/checker.ts +24 -3
- package/src/permissions/defaults.ts +4 -4
- package/src/permissions/workspace-policy.ts +1 -1
- package/src/playbooks/playbook-compiler.ts +19 -18
- package/src/playbooks/types.ts +4 -3
- package/src/prompts/system-prompt.ts +3 -29
- package/src/providers/anthropic/client.ts +47 -19
- package/src/providers/gemini/client.ts +1 -1
- package/src/providers/openai/client.ts +1 -1
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +19 -3
- package/src/runtime/actor-trust-resolver.ts +5 -1
- package/src/runtime/auth/route-policy.ts +7 -0
- package/src/runtime/guardian-reply-router.ts +5 -1
- package/src/runtime/http-server.ts +23 -3
- package/src/runtime/middleware/auth.ts +20 -0
- package/src/runtime/routes/attachment-routes.test.ts +106 -0
- package/src/runtime/routes/attachment-routes.ts +106 -16
- package/src/runtime/routes/brain-graph-routes.ts +21 -22
- package/src/runtime/routes/btw-routes.ts +8 -0
- package/src/runtime/routes/conversation-management-routes.ts +2 -0
- package/src/runtime/routes/conversation-starter-routes.ts +2 -2
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/global-search-routes.ts +21 -19
- package/src/runtime/routes/group-routes.ts +207 -0
- package/src/runtime/routes/guardian-action-routes.ts +21 -10
- package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
- package/src/runtime/routes/inbound-message-handler.ts +19 -0
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
- package/src/runtime/routes/memory-item-routes.test.ts +2 -14
- package/src/runtime/routes/memory-item-routes.ts +341 -388
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/skills-routes.ts +103 -37
- package/src/runtime/routes/work-items-routes.test.ts +2 -6
- package/src/schedule/scheduler.ts +8 -1
- package/src/security/oauth2.ts +1 -1
- package/src/security/secure-keys.ts +4 -8
- package/src/shared/provider-env-vars.ts +19 -0
- package/src/skills/catalog-cache.ts +5 -0
- package/src/skills/catalog-install.ts +15 -14
- package/src/skills/clawhub.ts +134 -154
- package/src/skills/install-meta.ts +208 -0
- package/src/skills/managed-store.ts +27 -16
- package/src/skills/skill-memory.ts +152 -77
- package/src/skills/skillssh-registry.ts +19 -17
- package/src/tasks/task-runner.ts +3 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
- package/src/tools/browser/runtime-check.ts +3 -1
- package/src/tools/memory/register.ts +63 -46
- package/src/tools/permission-checker.ts +7 -1
- package/src/tools/shared/filesystem/image-read.ts +22 -85
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/tool-manifest.ts +3 -3
- package/src/util/browser.ts +25 -10
- package/src/util/bun-runtime.ts +172 -0
- package/src/watcher/providers/outlook-calendar.ts +343 -0
- package/src/watcher/providers/outlook.ts +198 -0
- package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
- package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
- package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/context-memory-e2e.test.ts +0 -415
- package/src/__tests__/journal-context.test.ts +0 -268
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
- package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
- package/src/__tests__/memory-query-builder.test.ts +0 -59
- package/src/__tests__/memory-recall-quality.test.ts +0 -1046
- package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
- package/src/__tests__/memory-regressions.test.ts +0 -3696
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
- package/src/daemon/conversation-memory.ts +0 -207
- package/src/memory/conversation-starters-cadence.ts +0 -74
- package/src/memory/items-extractor.ts +0 -860
- package/src/memory/job-handlers/batch-extraction.ts +0 -753
- package/src/memory/job-handlers/extraction.ts +0 -40
- package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
- package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
- package/src/memory/journal-memory.ts +0 -224
- package/src/memory/query-builder.ts +0 -47
- package/src/memory/query-expansion.ts +0 -83
- package/src/memory/retriever.test.ts +0 -1592
- package/src/memory/retriever.ts +0 -1331
- package/src/memory/search/formatting.test.ts +0 -140
- package/src/memory/search/formatting.ts +0 -262
- package/src/memory/search/mmr.ts +0 -139
- package/src/memory/search/ranking.ts +0 -15
- package/src/memory/search/staleness.ts +0 -40
- package/src/memory/search/tier-classifier.ts +0 -18
- package/src/memory/search/types.ts +0 -121
- package/src/prompts/journal-context.ts +0 -154
- package/src/tools/memory/definitions.ts +0 -69
- package/src/tools/memory/handlers.test.ts +0 -562
- package/src/tools/memory/handlers.ts +0 -434
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Memory Graph — Conversation-level memory integration
|
|
3
|
+
//
|
|
4
|
+
// Replaces the old `prepareMemoryContext` from conversation-memory.ts.
|
|
5
|
+
// Manages the InContextTracker lifecycle and dispatches to the correct
|
|
6
|
+
// retrieval mode based on conversation state.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import { and, desc, eq, ne } from "drizzle-orm";
|
|
10
|
+
|
|
11
|
+
import type { AssistantConfig } from "../../config/types.js";
|
|
12
|
+
import { estimateTextTokens } from "../../context/token-estimator.js";
|
|
13
|
+
import type { ServerMessage } from "../../daemon/message-protocol.js";
|
|
14
|
+
import type {
|
|
15
|
+
ContentBlock,
|
|
16
|
+
ImageContent,
|
|
17
|
+
Message,
|
|
18
|
+
} from "../../providers/types.js";
|
|
19
|
+
import { getLogger } from "../../util/logger.js";
|
|
20
|
+
import { getDb } from "../db.js";
|
|
21
|
+
import { memorySummaries } from "../schema.js";
|
|
22
|
+
import { conversations } from "../schema/conversations.js";
|
|
23
|
+
import {
|
|
24
|
+
assembleContextBlock,
|
|
25
|
+
assembleInjectionBlock,
|
|
26
|
+
InContextTracker,
|
|
27
|
+
MAX_CONTEXT_LOAD_IMAGES,
|
|
28
|
+
MAX_PER_TURN_IMAGES,
|
|
29
|
+
MAX_REFRESH_IMAGES,
|
|
30
|
+
type ResolvedImage,
|
|
31
|
+
resolveInjectionImages,
|
|
32
|
+
} from "./injection.js";
|
|
33
|
+
import {
|
|
34
|
+
loadContextMemory,
|
|
35
|
+
REFRESH_INTERVAL_TURNS,
|
|
36
|
+
refreshContextMemory,
|
|
37
|
+
retrieveForTurn,
|
|
38
|
+
} from "./retriever.js";
|
|
39
|
+
|
|
40
|
+
const log = getLogger("graph-conversation-memory");
|
|
41
|
+
|
|
42
|
+
const ESTIMATED_IMAGE_TOKENS = 1000;
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Per-conversation state
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Manages memory graph state for a single conversation.
|
|
50
|
+
* Create one per Conversation instance. Persists across turns.
|
|
51
|
+
*/
|
|
52
|
+
export class ConversationGraphMemory {
|
|
53
|
+
readonly tracker = new InContextTracker();
|
|
54
|
+
private turnCount = 0;
|
|
55
|
+
private initialized = false;
|
|
56
|
+
private lastCompactedAt: number | null = null;
|
|
57
|
+
private needsReload = false;
|
|
58
|
+
private scopeId: string;
|
|
59
|
+
private conversationId: string;
|
|
60
|
+
private lastInjectedBlock: string | null = null;
|
|
61
|
+
private lastInjectedNodeIds: string[] = [];
|
|
62
|
+
private lastInjectedImages: Map<string, ResolvedImage> = new Map();
|
|
63
|
+
|
|
64
|
+
constructor(scopeId: string, conversationId: string) {
|
|
65
|
+
this.scopeId = scopeId;
|
|
66
|
+
this.conversationId = conversationId;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fetch the most recent conversation summaries (excluding the current
|
|
71
|
+
* conversation, which won't have one yet at context-load time).
|
|
72
|
+
*
|
|
73
|
+
* Prioritizes user conversations (conversationType != "background"),
|
|
74
|
+
* allowing at most 1 background conversation summary so the retrieval
|
|
75
|
+
* signal is mostly from direct interactions.
|
|
76
|
+
*
|
|
77
|
+
* Returns up to 3 summary texts, most recent first.
|
|
78
|
+
*/
|
|
79
|
+
private fetchRecentSummaries(): string[] {
|
|
80
|
+
try {
|
|
81
|
+
const db = getDb();
|
|
82
|
+
const baseWhere = and(
|
|
83
|
+
eq(memorySummaries.scope, "conversation"),
|
|
84
|
+
eq(memorySummaries.scopeId, this.scopeId),
|
|
85
|
+
ne(memorySummaries.scopeKey, this.conversationId),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Fetch user conversations first (up to 3)
|
|
89
|
+
const userRows = db
|
|
90
|
+
.select({ summary: memorySummaries.summary })
|
|
91
|
+
.from(memorySummaries)
|
|
92
|
+
.innerJoin(
|
|
93
|
+
conversations,
|
|
94
|
+
eq(memorySummaries.scopeKey, conversations.id),
|
|
95
|
+
)
|
|
96
|
+
.where(and(baseWhere, ne(conversations.conversationType, "background")))
|
|
97
|
+
.orderBy(desc(memorySummaries.updatedAt))
|
|
98
|
+
.limit(3)
|
|
99
|
+
.all();
|
|
100
|
+
|
|
101
|
+
if (userRows.length >= 3) {
|
|
102
|
+
return userRows.map((r) => r.summary);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fill remaining slots with at most 1 background conversation
|
|
106
|
+
const remaining = Math.min(1, 3 - userRows.length);
|
|
107
|
+
const bgRows = db
|
|
108
|
+
.select({ summary: memorySummaries.summary })
|
|
109
|
+
.from(memorySummaries)
|
|
110
|
+
.innerJoin(
|
|
111
|
+
conversations,
|
|
112
|
+
eq(memorySummaries.scopeKey, conversations.id),
|
|
113
|
+
)
|
|
114
|
+
.where(and(baseWhere, eq(conversations.conversationType, "background")))
|
|
115
|
+
.orderBy(desc(memorySummaries.updatedAt))
|
|
116
|
+
.limit(remaining)
|
|
117
|
+
.all();
|
|
118
|
+
|
|
119
|
+
return [...userRows, ...bgRows].map((r) => r.summary);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
log.warn({ err }, "Failed to fetch recent conversation summaries");
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Notify that context compaction just happened.
|
|
128
|
+
* On the next turn, we'll re-run full context load.
|
|
129
|
+
*/
|
|
130
|
+
onCompacted(compactedMessageCount: number): void {
|
|
131
|
+
// Evict everything — compaction summarized all prior turns.
|
|
132
|
+
// The tracker can't know exactly which turns were compacted,
|
|
133
|
+
// so we conservatively clear everything and reload.
|
|
134
|
+
this.tracker.evictCompactedTurns(this.tracker.getTurn());
|
|
135
|
+
this.needsReload = true;
|
|
136
|
+
this.lastCompactedAt = Date.now();
|
|
137
|
+
log.info(
|
|
138
|
+
{ compactedMessageCount },
|
|
139
|
+
"Compaction detected — will reload context on next turn",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Re-inject the most recent memory block after context compaction.
|
|
145
|
+
* Synchronous — reuses the cached block from the last successful retrieval.
|
|
146
|
+
* Does NOT advance turn count or run new retrieval.
|
|
147
|
+
*/
|
|
148
|
+
reinjectCachedMemory(messages: Message[]): {
|
|
149
|
+
runMessages: Message[];
|
|
150
|
+
injectedTokens: number;
|
|
151
|
+
} {
|
|
152
|
+
if (!this.lastInjectedBlock) {
|
|
153
|
+
return { runMessages: messages, injectedTokens: 0 };
|
|
154
|
+
}
|
|
155
|
+
// Re-track node IDs since onCompacted evicted them
|
|
156
|
+
this.tracker.add(this.lastInjectedNodeIds);
|
|
157
|
+
// Strip any existing <memory __injected> blocks from the last user message
|
|
158
|
+
// before re-injecting, so compaction sites don't end up with duplicates.
|
|
159
|
+
const cleaned = stripExistingMemoryInjections(messages);
|
|
160
|
+
|
|
161
|
+
const injectedTokens =
|
|
162
|
+
estimateTextTokens(this.lastInjectedBlock) +
|
|
163
|
+
this.lastInjectedImages.size * ESTIMATED_IMAGE_TOKENS;
|
|
164
|
+
|
|
165
|
+
if (this.lastInjectedImages.size > 0) {
|
|
166
|
+
return {
|
|
167
|
+
runMessages: injectMemoryBlock(
|
|
168
|
+
cleaned,
|
|
169
|
+
this.lastInjectedBlock,
|
|
170
|
+
this.lastInjectedImages,
|
|
171
|
+
),
|
|
172
|
+
injectedTokens,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
runMessages: injectTextBlock(cleaned, this.lastInjectedBlock),
|
|
178
|
+
injectedTokens,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Main entry point — called on every turn before the LLM sees the messages.
|
|
184
|
+
*
|
|
185
|
+
* Dispatches to the appropriate retrieval mode:
|
|
186
|
+
* - Turn 1 (or after compaction): full context load
|
|
187
|
+
* - Every 5 turns: periodic refresh
|
|
188
|
+
* - Every other turn: per-turn injection
|
|
189
|
+
*
|
|
190
|
+
* Returns augmented messages with memory context prepended to the last
|
|
191
|
+
* user message, following the same injection pattern as the old system.
|
|
192
|
+
*/
|
|
193
|
+
async prepareMemory(
|
|
194
|
+
messages: Message[],
|
|
195
|
+
config: AssistantConfig,
|
|
196
|
+
abortSignal: AbortSignal,
|
|
197
|
+
onEvent: (msg: ServerMessage) => void,
|
|
198
|
+
): Promise<{
|
|
199
|
+
runMessages: Message[];
|
|
200
|
+
injectedTokens: number;
|
|
201
|
+
latencyMs: number;
|
|
202
|
+
mode: "context-load" | "refresh" | "per-turn" | "none";
|
|
203
|
+
/** The raw text content of the injected block (without XML wrapper), or null if nothing was injected. */
|
|
204
|
+
injectedBlockText: string | null;
|
|
205
|
+
}> {
|
|
206
|
+
this.turnCount++;
|
|
207
|
+
this.tracker.advanceTurn();
|
|
208
|
+
|
|
209
|
+
const noopResult = {
|
|
210
|
+
runMessages: messages,
|
|
211
|
+
injectedTokens: 0,
|
|
212
|
+
latencyMs: 0,
|
|
213
|
+
mode: "none" as const,
|
|
214
|
+
injectedBlockText: null as string | null,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Gate: skip for empty/tool-result-only messages — unless we need to
|
|
218
|
+
// reload after compaction (needsReload) or haven't initialized yet.
|
|
219
|
+
const lastMessage = messages[messages.length - 1];
|
|
220
|
+
if (!lastMessage || lastMessage.role !== "user") return noopResult;
|
|
221
|
+
const hasUserContent = lastMessage.content.some(
|
|
222
|
+
(block) => block.type === "text" && block.text.trim().length > 0,
|
|
223
|
+
);
|
|
224
|
+
if (!hasUserContent && this.initialized && !this.needsReload)
|
|
225
|
+
return noopResult;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Decide which retrieval mode to use
|
|
229
|
+
if (!this.initialized || this.needsReload) {
|
|
230
|
+
const recentSummaries = this.fetchRecentSummaries();
|
|
231
|
+
|
|
232
|
+
// Extract the first user message as an additional retrieval signal
|
|
233
|
+
// so context-load biases toward what the user is asking about
|
|
234
|
+
const firstUserText = extractUserText(lastMessage);
|
|
235
|
+
if (firstUserText) {
|
|
236
|
+
recentSummaries.unshift(firstUserText);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return await this.runContextLoad(
|
|
240
|
+
messages,
|
|
241
|
+
config,
|
|
242
|
+
recentSummaries,
|
|
243
|
+
abortSignal,
|
|
244
|
+
onEvent,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (this.turnCount % REFRESH_INTERVAL_TURNS === 0) {
|
|
249
|
+
return await this.runRefresh(messages, config, abortSignal);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return await this.runPerTurn(messages, config, abortSignal);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
log.warn(
|
|
255
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
256
|
+
"Memory retrieval failed (non-fatal)",
|
|
257
|
+
);
|
|
258
|
+
return noopResult;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Retrieval modes
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
private async runContextLoad(
|
|
267
|
+
messages: Message[],
|
|
268
|
+
config: AssistantConfig,
|
|
269
|
+
recentSummaries: string[],
|
|
270
|
+
signal: AbortSignal,
|
|
271
|
+
onEvent: (msg: ServerMessage) => void,
|
|
272
|
+
) {
|
|
273
|
+
const result = await loadContextMemory({
|
|
274
|
+
scopeId: this.scopeId,
|
|
275
|
+
recentSummaries,
|
|
276
|
+
config,
|
|
277
|
+
signal,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this.initialized = true;
|
|
281
|
+
this.needsReload = false;
|
|
282
|
+
|
|
283
|
+
if (result.nodes.length === 0) {
|
|
284
|
+
this.lastInjectedBlock = null;
|
|
285
|
+
this.lastInjectedNodeIds = [];
|
|
286
|
+
this.lastInjectedImages = new Map();
|
|
287
|
+
return {
|
|
288
|
+
runMessages: messages,
|
|
289
|
+
injectedTokens: 0,
|
|
290
|
+
latencyMs: result.latencyMs,
|
|
291
|
+
mode: "context-load" as const,
|
|
292
|
+
injectedBlockText: null,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Track loaded nodes (including serendipity)
|
|
297
|
+
this.tracker.add(result.nodes.map((n) => n.node.id));
|
|
298
|
+
this.tracker.add(result.serendipityNodes.map((n) => n.node.id));
|
|
299
|
+
|
|
300
|
+
// Assemble context block
|
|
301
|
+
const contextBlock = assembleContextBlock(result.nodes, {
|
|
302
|
+
serendipityNodes: result.serendipityNodes,
|
|
303
|
+
});
|
|
304
|
+
if (!contextBlock) {
|
|
305
|
+
return {
|
|
306
|
+
runMessages: messages,
|
|
307
|
+
injectedTokens: 0,
|
|
308
|
+
latencyMs: result.latencyMs,
|
|
309
|
+
mode: "context-load" as const,
|
|
310
|
+
injectedBlockText: null,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Resolve images from scored nodes
|
|
315
|
+
const images = await resolveInjectionImages(
|
|
316
|
+
[...result.nodes, ...result.serendipityNodes],
|
|
317
|
+
MAX_CONTEXT_LOAD_IMAGES,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const injectedTokens =
|
|
321
|
+
estimateTextTokens(contextBlock) + images.size * ESTIMATED_IMAGE_TOKENS;
|
|
322
|
+
|
|
323
|
+
onEvent({
|
|
324
|
+
type: "memory_status",
|
|
325
|
+
enabled: true,
|
|
326
|
+
degraded: false,
|
|
327
|
+
} as ServerMessage);
|
|
328
|
+
|
|
329
|
+
this.lastInjectedBlock = contextBlock;
|
|
330
|
+
this.lastInjectedNodeIds = [
|
|
331
|
+
...result.nodes.map((n) => n.node.id),
|
|
332
|
+
...result.serendipityNodes.map((n) => n.node.id),
|
|
333
|
+
];
|
|
334
|
+
this.lastInjectedImages = images;
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
runMessages: injectMemoryBlock(messages, contextBlock, images),
|
|
338
|
+
injectedTokens,
|
|
339
|
+
latencyMs: result.latencyMs,
|
|
340
|
+
mode: "context-load" as const,
|
|
341
|
+
injectedBlockText: contextBlock,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async runRefresh(
|
|
346
|
+
messages: Message[],
|
|
347
|
+
config: AssistantConfig,
|
|
348
|
+
signal: AbortSignal,
|
|
349
|
+
) {
|
|
350
|
+
// Build recent turns text from the last ~6 messages
|
|
351
|
+
const recentTurns = messages
|
|
352
|
+
.slice(-6)
|
|
353
|
+
.map((m) => {
|
|
354
|
+
const textBlocks = m.content.filter(
|
|
355
|
+
(b): b is Extract<typeof b, { type: "text" }> => b.type === "text",
|
|
356
|
+
);
|
|
357
|
+
if (textBlocks.length === 0) return "";
|
|
358
|
+
return `[${m.role}]: ${textBlocks.map((b) => b.text).join(" ")}`;
|
|
359
|
+
})
|
|
360
|
+
.filter((t) => t.length > 0)
|
|
361
|
+
.join("\n\n");
|
|
362
|
+
|
|
363
|
+
const result = await refreshContextMemory({
|
|
364
|
+
recentTurnsText: recentTurns,
|
|
365
|
+
scopeId: this.scopeId,
|
|
366
|
+
config,
|
|
367
|
+
tracker: this.tracker,
|
|
368
|
+
signal,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
if (result.nodes.length === 0) {
|
|
372
|
+
this.lastInjectedBlock = null;
|
|
373
|
+
this.lastInjectedNodeIds = [];
|
|
374
|
+
this.lastInjectedImages = new Map();
|
|
375
|
+
return {
|
|
376
|
+
runMessages: messages,
|
|
377
|
+
injectedTokens: 0,
|
|
378
|
+
latencyMs: result.latencyMs,
|
|
379
|
+
mode: "refresh" as const,
|
|
380
|
+
injectedBlockText: null,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Track new nodes
|
|
385
|
+
this.tracker.add(result.nodes.map((n) => n.node.id));
|
|
386
|
+
|
|
387
|
+
const injectionBlock = assembleInjectionBlock(result.nodes);
|
|
388
|
+
if (!injectionBlock) {
|
|
389
|
+
return {
|
|
390
|
+
runMessages: messages,
|
|
391
|
+
injectedTokens: 0,
|
|
392
|
+
latencyMs: result.latencyMs,
|
|
393
|
+
mode: "refresh" as const,
|
|
394
|
+
injectedBlockText: null,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Resolve images from scored nodes
|
|
399
|
+
const images = await resolveInjectionImages(
|
|
400
|
+
result.nodes,
|
|
401
|
+
MAX_REFRESH_IMAGES,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
this.lastInjectedBlock = injectionBlock;
|
|
405
|
+
this.lastInjectedNodeIds = result.nodes.map((n) => n.node.id);
|
|
406
|
+
this.lastInjectedImages = images;
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
runMessages: injectMemoryBlock(messages, injectionBlock, images),
|
|
410
|
+
injectedTokens:
|
|
411
|
+
estimateTextTokens(injectionBlock) +
|
|
412
|
+
images.size * ESTIMATED_IMAGE_TOKENS,
|
|
413
|
+
latencyMs: result.latencyMs,
|
|
414
|
+
mode: "refresh" as const,
|
|
415
|
+
injectedBlockText: injectionBlock,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async runPerTurn(
|
|
420
|
+
messages: Message[],
|
|
421
|
+
config: AssistantConfig,
|
|
422
|
+
signal: AbortSignal,
|
|
423
|
+
) {
|
|
424
|
+
// Extract last assistant and user messages as text
|
|
425
|
+
let assistantLast = "";
|
|
426
|
+
let userLast = "";
|
|
427
|
+
let userLastBlocks: ContentBlock[] = [];
|
|
428
|
+
|
|
429
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
430
|
+
const msg = messages[i];
|
|
431
|
+
const text = msg.content
|
|
432
|
+
.filter(
|
|
433
|
+
(b): b is Extract<typeof b, { type: "text" }> => b.type === "text",
|
|
434
|
+
)
|
|
435
|
+
.map((b) => b.text)
|
|
436
|
+
.join(" ");
|
|
437
|
+
|
|
438
|
+
if (msg.role === "user") {
|
|
439
|
+
if (userLastBlocks.length === 0) {
|
|
440
|
+
userLastBlocks = msg.content;
|
|
441
|
+
userLast = text;
|
|
442
|
+
}
|
|
443
|
+
} else if (msg.role === "assistant" && !assistantLast) {
|
|
444
|
+
assistantLast = text;
|
|
445
|
+
}
|
|
446
|
+
if (userLastBlocks.length > 0 && assistantLast) break;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const result = await retrieveForTurn({
|
|
450
|
+
assistantLastMessage: assistantLast,
|
|
451
|
+
userLastMessage: userLast,
|
|
452
|
+
userLastMessageBlocks: userLastBlocks,
|
|
453
|
+
scopeId: this.scopeId,
|
|
454
|
+
config,
|
|
455
|
+
tracker: this.tracker,
|
|
456
|
+
signal,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (result.nodes.length === 0) {
|
|
460
|
+
this.lastInjectedBlock = null;
|
|
461
|
+
this.lastInjectedNodeIds = [];
|
|
462
|
+
this.lastInjectedImages = new Map();
|
|
463
|
+
return {
|
|
464
|
+
runMessages: messages,
|
|
465
|
+
injectedTokens: 0,
|
|
466
|
+
latencyMs: result.latencyMs,
|
|
467
|
+
mode: "per-turn" as const,
|
|
468
|
+
injectedBlockText: null,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Track new nodes
|
|
473
|
+
this.tracker.add(result.nodes.map((n) => n.node.id));
|
|
474
|
+
|
|
475
|
+
const injectionBlock = assembleInjectionBlock(result.nodes);
|
|
476
|
+
if (!injectionBlock) {
|
|
477
|
+
return {
|
|
478
|
+
runMessages: messages,
|
|
479
|
+
injectedTokens: 0,
|
|
480
|
+
latencyMs: result.latencyMs,
|
|
481
|
+
mode: "per-turn" as const,
|
|
482
|
+
injectedBlockText: null,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Resolve images from scored nodes
|
|
487
|
+
const images = await resolveInjectionImages(
|
|
488
|
+
result.nodes,
|
|
489
|
+
MAX_PER_TURN_IMAGES,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
this.lastInjectedBlock = injectionBlock;
|
|
493
|
+
this.lastInjectedNodeIds = result.nodes.map((n) => n.node.id);
|
|
494
|
+
this.lastInjectedImages = images;
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
runMessages: injectMemoryBlock(messages, injectionBlock, images),
|
|
498
|
+
injectedTokens:
|
|
499
|
+
estimateTextTokens(injectionBlock) +
|
|
500
|
+
images.size * ESTIMATED_IMAGE_TOKENS,
|
|
501
|
+
latencyMs: result.latencyMs,
|
|
502
|
+
mode: "per-turn" as const,
|
|
503
|
+
injectedBlockText: injectionBlock,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// Injection helper — same pattern as old injectMemoryRecallAsUserBlock
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Remove all memory-injected blocks from the last user message.
|
|
514
|
+
*
|
|
515
|
+
* `injectMemoryBlock` always prepends blocks in this order:
|
|
516
|
+
* 1. `<memory __injected>…</memory>` text block
|
|
517
|
+
* 2. For each image: `<memory_image>…</memory_image>` text + `image` block
|
|
518
|
+
*
|
|
519
|
+
* We strip all leading blocks that match this pattern so that
|
|
520
|
+
* `reinjectCachedMemory` is idempotent — no duplicate images after compaction.
|
|
521
|
+
*/
|
|
522
|
+
function stripExistingMemoryInjections(messages: Message[]): Message[] {
|
|
523
|
+
if (messages.length === 0) return messages;
|
|
524
|
+
const last = messages[messages.length - 1];
|
|
525
|
+
if (!last || last.role !== "user") return messages;
|
|
526
|
+
|
|
527
|
+
// Walk from the front and skip all memory-injected blocks.
|
|
528
|
+
// The injection prefix is always contiguous at the start of content.
|
|
529
|
+
let firstNonMemory = 0;
|
|
530
|
+
const content = last.content;
|
|
531
|
+
while (firstNonMemory < content.length) {
|
|
532
|
+
const block = content[firstNonMemory];
|
|
533
|
+
if (
|
|
534
|
+
block.type === "text" &&
|
|
535
|
+
block.text.startsWith("<memory __injected>\n")
|
|
536
|
+
) {
|
|
537
|
+
firstNonMemory++;
|
|
538
|
+
} else if (
|
|
539
|
+
block.type === "text" &&
|
|
540
|
+
block.text.startsWith("<memory_image>")
|
|
541
|
+
) {
|
|
542
|
+
firstNonMemory++;
|
|
543
|
+
} else if (block.type === "image") {
|
|
544
|
+
firstNonMemory++;
|
|
545
|
+
} else {
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Nothing to strip
|
|
551
|
+
if (firstNonMemory === 0) return messages;
|
|
552
|
+
|
|
553
|
+
return [
|
|
554
|
+
...messages.slice(0, -1),
|
|
555
|
+
{ ...last, content: content.slice(firstNonMemory) },
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function injectTextBlock(messages: Message[], text: string): Message[] {
|
|
560
|
+
if (text.trim().length === 0) return messages;
|
|
561
|
+
if (messages.length === 0) return messages;
|
|
562
|
+
// Strip existing memory blocks from the last user message first to prevent
|
|
563
|
+
// duplicates when the message was loaded from DB with a persisted block.
|
|
564
|
+
const cleaned = stripExistingMemoryInjections(messages);
|
|
565
|
+
const userTail = cleaned[cleaned.length - 1];
|
|
566
|
+
if (!userTail || userTail.role !== "user") return messages;
|
|
567
|
+
return [
|
|
568
|
+
...cleaned.slice(0, -1),
|
|
569
|
+
{
|
|
570
|
+
...userTail,
|
|
571
|
+
content: [
|
|
572
|
+
{
|
|
573
|
+
type: "text" as const,
|
|
574
|
+
text: `<memory __injected>\n${text}\n</memory>`,
|
|
575
|
+
},
|
|
576
|
+
...userTail.content,
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function injectMemoryBlock(
|
|
583
|
+
messages: Message[],
|
|
584
|
+
text: string,
|
|
585
|
+
images: Map<string, ResolvedImage>,
|
|
586
|
+
): Message[] {
|
|
587
|
+
if (text.trim().length === 0 && images.size === 0) return messages;
|
|
588
|
+
if (messages.length === 0) return messages;
|
|
589
|
+
// Strip existing memory blocks from the last user message first to prevent
|
|
590
|
+
// duplicates when the message was loaded from DB with a persisted block.
|
|
591
|
+
const cleaned = stripExistingMemoryInjections(messages);
|
|
592
|
+
const userTail = cleaned[cleaned.length - 1];
|
|
593
|
+
if (!userTail || userTail.role !== "user") return messages;
|
|
594
|
+
|
|
595
|
+
const blocks: ContentBlock[] = [
|
|
596
|
+
{ type: "text" as const, text: `<memory __injected>\n${text}\n</memory>` },
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
for (const [_nodeId, img] of images) {
|
|
600
|
+
blocks.push({
|
|
601
|
+
type: "text" as const,
|
|
602
|
+
text: `<memory_image>${img.description}</memory_image>`,
|
|
603
|
+
});
|
|
604
|
+
blocks.push({
|
|
605
|
+
type: "image" as const,
|
|
606
|
+
source: {
|
|
607
|
+
type: "base64" as const,
|
|
608
|
+
media_type: img.mediaType,
|
|
609
|
+
data: img.base64Data,
|
|
610
|
+
},
|
|
611
|
+
} as ImageContent);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return [
|
|
615
|
+
...cleaned.slice(0, -1),
|
|
616
|
+
{ ...userTail, content: [...blocks, ...userTail.content] },
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Extract text content from a user message. */
|
|
621
|
+
function extractUserText(message: Message): string | null {
|
|
622
|
+
const texts = message.content
|
|
623
|
+
.filter((b): b is Extract<typeof b, { type: "text" }> => b.type === "text")
|
|
624
|
+
.map((b) => b.text.trim())
|
|
625
|
+
.filter((t) => t.length > 0);
|
|
626
|
+
if (texts.length === 0) return null;
|
|
627
|
+
const joined = texts.join(" ");
|
|
628
|
+
// Skip very short messages ("hi", "yes") — they produce vague embeddings
|
|
629
|
+
return joined.length > 10 ? joined : null;
|
|
630
|
+
}
|