@vellumai/assistant 0.5.5 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +4 -5
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +1 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +3 -3
- package/src/__tests__/context-window-manager.test.ts +78 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +117 -1
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +98 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/conversations.ts +0 -18
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +16 -16
- package/src/config/feature-flag-registry.json +48 -16
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -25
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/platform.ts +1 -1
- package/src/config/schemas/security.ts +4 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/context/window-manager.ts +53 -2
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +6 -4
- package/src/daemon/conversation-agent-loop.ts +0 -60
- package/src/daemon/conversation-memory.ts +0 -117
- package/src/daemon/conversation-runtime-assembly.ts +0 -2
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/conversations.ts +0 -11
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +229 -96
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/followups/followup-store.ts +5 -2
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-crud.ts +0 -236
- package/src/memory/conversation-title-service.ts +76 -11
- package/src/memory/db-init.ts +15 -11
- package/src/memory/indexer.ts +15 -106
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/job-handlers/embedding.ts +0 -79
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +30 -13
- package/src/memory/jobs-worker.ts +31 -27
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +5 -3
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-client.ts +4 -6
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/conversations.ts +0 -3
- package/src/memory/schema/index.ts +0 -2
- package/src/messaging/draft-store.ts +2 -2
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/defaults.ts +3 -3
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/trust-client.ts +2 -13
- package/src/permissions/trust-store.ts +8 -3
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +29 -1
- package/src/runtime/auth/token-service.ts +53 -15
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +29 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-management-routes.ts +0 -36
- package/src/runtime/routes/conversation-query-routes.ts +106 -2
- package/src/runtime/routes/conversation-routes.ts +4 -43
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +144 -4
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +175 -0
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/schedule/schedule-store.ts +0 -21
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/memory/handlers.ts +1 -129
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/load.ts +9 -2
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +8 -55
- package/src/util/xml.ts +8 -0
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/heartbeat-service.ts +5 -24
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/archive-recall.test.ts +0 -560
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
- package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
- package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
- package/src/__tests__/memory-brief-time.test.ts +0 -285
- package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
- package/src/__tests__/memory-chunk-archive.test.ts +0 -400
- package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
- package/src/__tests__/memory-episode-archive.test.ts +0 -370
- package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
- package/src/__tests__/memory-observation-archive.test.ts +0 -375
- package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
- package/src/__tests__/memory-reducer-job.test.ts +0 -538
- package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
- package/src/__tests__/memory-reducer-store.test.ts +0 -728
- package/src/__tests__/memory-reducer-types.test.ts +0 -707
- package/src/__tests__/memory-reducer.test.ts +0 -704
- package/src/__tests__/memory-simplified-config.test.ts +0 -281
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
- package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/memory-simplified.ts +0 -101
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/memory/archive-recall.ts +0 -516
- package/src/memory/archive-store.ts +0 -400
- package/src/memory/brief-formatting.ts +0 -33
- package/src/memory/brief-open-loops.ts +0 -266
- package/src/memory/brief-time.ts +0 -162
- package/src/memory/brief.ts +0 -75
- package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
- package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
- package/src/memory/migrations/185-memory-brief-state.ts +0 -52
- package/src/memory/migrations/186-memory-archive.ts +0 -109
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
- package/src/memory/reducer-scheduler.ts +0 -242
- package/src/memory/reducer-store.ts +0 -271
- package/src/memory/reducer-types.ts +0 -106
- package/src/memory/reducer.ts +0 -467
- package/src/memory/schema/memory-archive.ts +0 -121
- package/src/memory/schema/memory-brief.ts +0 -55
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
4
|
-
import { v4 as uuid } from "uuid";
|
|
5
|
-
|
|
6
|
-
import { estimateTextTokens } from "../context/token-estimator.js";
|
|
7
|
-
import { getLogger } from "../util/logger.js";
|
|
8
|
-
import { getDb, rawChanges } from "./db.js";
|
|
9
|
-
import { enqueueMemoryJob, type MemoryJobType } from "./jobs-store.js";
|
|
10
|
-
import {
|
|
11
|
-
memoryChunks,
|
|
12
|
-
memoryEpisodes,
|
|
13
|
-
memoryObservations,
|
|
14
|
-
} from "./schema.js";
|
|
15
|
-
|
|
16
|
-
const log = getLogger("memory-archive-store");
|
|
17
|
-
|
|
18
|
-
// ── Content hashing ─────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Compute a SHA-256 content hash for a chunk's content, scoped by scopeId.
|
|
22
|
-
* Used for idempotent upserts — if the hash already exists within the same
|
|
23
|
-
* scope, the chunk is skipped.
|
|
24
|
-
*/
|
|
25
|
-
export function computeChunkContentHash(
|
|
26
|
-
scopeId: string,
|
|
27
|
-
content: string,
|
|
28
|
-
): string {
|
|
29
|
-
return createHash("sha256").update(`${scopeId}|${content}`).digest("hex");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Compute a SHA-256 hash of the observation content, scoped by scopeId.
|
|
34
|
-
* Used for idempotent chunk deduplication.
|
|
35
|
-
*/
|
|
36
|
-
export function computeObservationContentHash(
|
|
37
|
-
scopeId: string,
|
|
38
|
-
content: string,
|
|
39
|
-
): string {
|
|
40
|
-
return createHash("sha256").update(`${scopeId}|${content}`).digest("hex");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ── Token estimation ────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Rough token count estimate based on character length.
|
|
47
|
-
* Uses the common ~4 chars/token heuristic for English text.
|
|
48
|
-
*/
|
|
49
|
-
export function estimateTokens(text: string): number {
|
|
50
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── Chunk upsert ────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
export interface UpsertChunkInput {
|
|
56
|
-
/** Scope for memory isolation. Defaults to "default". */
|
|
57
|
-
scopeId?: string;
|
|
58
|
-
/** FK to the parent observation. */
|
|
59
|
-
observationId: string;
|
|
60
|
-
/** The chunk text to embed and recall. */
|
|
61
|
-
content: string;
|
|
62
|
-
/** Optional pre-computed token estimate. If omitted, estimated from content length. */
|
|
63
|
-
tokenEstimate?: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface UpsertChunkResult {
|
|
67
|
-
chunkId: string;
|
|
68
|
-
/** True if a new row was inserted; false if an existing row matched the content hash. */
|
|
69
|
-
inserted: boolean;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Idempotently upsert a chunk into the archive. If a chunk with the same
|
|
74
|
-
* (scopeId, contentHash) already exists, the insert is skipped and the
|
|
75
|
-
* existing row's id is returned. Otherwise a new row is inserted and an
|
|
76
|
-
* `embed_chunk` job is enqueued.
|
|
77
|
-
*/
|
|
78
|
-
export function upsertChunk(input: UpsertChunkInput): UpsertChunkResult {
|
|
79
|
-
const scopeId = input.scopeId ?? "default";
|
|
80
|
-
const contentHash = computeChunkContentHash(scopeId, input.content);
|
|
81
|
-
const tokenEstimate = input.tokenEstimate ?? estimateTokens(input.content);
|
|
82
|
-
const db = getDb();
|
|
83
|
-
|
|
84
|
-
// Check for an existing chunk with the same content hash in this scope
|
|
85
|
-
const existing = db
|
|
86
|
-
.select({ id: memoryChunks.id })
|
|
87
|
-
.from(memoryChunks)
|
|
88
|
-
.where(eq(memoryChunks.contentHash, contentHash))
|
|
89
|
-
.get();
|
|
90
|
-
|
|
91
|
-
if (existing) {
|
|
92
|
-
log.debug(
|
|
93
|
-
{ scopeId, contentHash, existingId: existing.id },
|
|
94
|
-
"Chunk already exists, skipping insert",
|
|
95
|
-
);
|
|
96
|
-
return { chunkId: existing.id, inserted: false };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const chunkId = uuid();
|
|
100
|
-
const now = Date.now();
|
|
101
|
-
|
|
102
|
-
db.insert(memoryChunks)
|
|
103
|
-
.values({
|
|
104
|
-
id: chunkId,
|
|
105
|
-
scopeId,
|
|
106
|
-
observationId: input.observationId,
|
|
107
|
-
content: input.content,
|
|
108
|
-
tokenEstimate,
|
|
109
|
-
contentHash,
|
|
110
|
-
createdAt: now,
|
|
111
|
-
})
|
|
112
|
-
.run();
|
|
113
|
-
|
|
114
|
-
// Enqueue an embedding job for the new chunk
|
|
115
|
-
enqueueMemoryJob("embed_chunk", {
|
|
116
|
-
chunkId,
|
|
117
|
-
scopeId,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
log.debug(
|
|
121
|
-
{ chunkId, scopeId, contentHash },
|
|
122
|
-
"Inserted new chunk and enqueued embed_chunk job",
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
return { chunkId, inserted: true };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Upsert multiple chunks for a single observation. Returns results for
|
|
130
|
-
* each input in the same order.
|
|
131
|
-
*/
|
|
132
|
-
export function upsertChunks(inputs: UpsertChunkInput[]): UpsertChunkResult[] {
|
|
133
|
-
return inputs.map((input) => upsertChunk(input));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ── Chunk queries ───────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get a chunk by its ID.
|
|
140
|
-
*/
|
|
141
|
-
export function getChunkById(
|
|
142
|
-
chunkId: string,
|
|
143
|
-
): typeof memoryChunks.$inferSelect | undefined {
|
|
144
|
-
const db = getDb();
|
|
145
|
-
return db
|
|
146
|
-
.select()
|
|
147
|
-
.from(memoryChunks)
|
|
148
|
-
.where(eq(memoryChunks.id, chunkId))
|
|
149
|
-
.get();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Get all chunks for a given observation.
|
|
154
|
-
*/
|
|
155
|
-
export function getChunksByObservationId(
|
|
156
|
-
observationId: string,
|
|
157
|
-
): Array<typeof memoryChunks.$inferSelect> {
|
|
158
|
-
const db = getDb();
|
|
159
|
-
return db
|
|
160
|
-
.select()
|
|
161
|
-
.from(memoryChunks)
|
|
162
|
-
.where(eq(memoryChunks.observationId, observationId))
|
|
163
|
-
.all();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Episode insertion helpers ───────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
export interface InsertEpisodeParams {
|
|
169
|
-
scopeId?: string;
|
|
170
|
-
conversationId: string;
|
|
171
|
-
title: string;
|
|
172
|
-
summary: string;
|
|
173
|
-
tokenEstimate: number;
|
|
174
|
-
source?: string;
|
|
175
|
-
startAt: number;
|
|
176
|
-
endAt: number;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Insert an episode row produced by conversation compaction.
|
|
181
|
-
* Compaction episodes summarize a contiguous block of turns that was
|
|
182
|
-
* compressed to free context-window space.
|
|
183
|
-
*
|
|
184
|
-
* An `embed_episode` job is enqueued automatically so the episode
|
|
185
|
-
* becomes searchable via vector recall.
|
|
186
|
-
*/
|
|
187
|
-
export function insertCompactionEpisode(params: InsertEpisodeParams): {
|
|
188
|
-
episodeId: string;
|
|
189
|
-
jobId: string;
|
|
190
|
-
} {
|
|
191
|
-
return insertEpisodeAndEnqueue(params);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Insert an episode row produced by resolution (end-of-conversation)
|
|
196
|
-
* summarization. Resolution episodes capture the full narrative arc
|
|
197
|
-
* of a completed conversation.
|
|
198
|
-
*
|
|
199
|
-
* An `embed_episode` job is enqueued automatically so the episode
|
|
200
|
-
* becomes searchable via vector recall.
|
|
201
|
-
*/
|
|
202
|
-
export function insertResolutionEpisode(params: InsertEpisodeParams): {
|
|
203
|
-
episodeId: string;
|
|
204
|
-
jobId: string;
|
|
205
|
-
} {
|
|
206
|
-
return insertEpisodeAndEnqueue(params);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── Internal (episode) ──────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
function insertEpisodeAndEnqueue(params: InsertEpisodeParams): {
|
|
212
|
-
episodeId: string;
|
|
213
|
-
jobId: string;
|
|
214
|
-
} {
|
|
215
|
-
const db = getDb();
|
|
216
|
-
const episodeId = uuid();
|
|
217
|
-
const now = Date.now();
|
|
218
|
-
|
|
219
|
-
db.insert(memoryEpisodes)
|
|
220
|
-
.values({
|
|
221
|
-
id: episodeId,
|
|
222
|
-
scopeId: params.scopeId ?? "default",
|
|
223
|
-
conversationId: params.conversationId,
|
|
224
|
-
title: params.title,
|
|
225
|
-
summary: params.summary,
|
|
226
|
-
tokenEstimate: params.tokenEstimate,
|
|
227
|
-
source: params.source ?? null,
|
|
228
|
-
startAt: params.startAt,
|
|
229
|
-
endAt: params.endAt,
|
|
230
|
-
createdAt: now,
|
|
231
|
-
updatedAt: now,
|
|
232
|
-
})
|
|
233
|
-
.run();
|
|
234
|
-
|
|
235
|
-
const jobId = enqueueMemoryJob("embed_episode" satisfies MemoryJobType, {
|
|
236
|
-
episodeId,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
log.debug(
|
|
240
|
-
{ episodeId, jobId, conversationId: params.conversationId },
|
|
241
|
-
"Inserted episode and enqueued embed job",
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
return { episodeId, jobId };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ── Observation types ───────────────────────────────────────────────
|
|
248
|
-
|
|
249
|
-
export interface InsertObservationParams {
|
|
250
|
-
conversationId: string;
|
|
251
|
-
messageId?: string | null;
|
|
252
|
-
role: string;
|
|
253
|
-
content: string;
|
|
254
|
-
scopeId?: string;
|
|
255
|
-
modality?: string;
|
|
256
|
-
source?: string | null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export interface InsertedObservation {
|
|
260
|
-
observationId: string;
|
|
261
|
-
chunkId: string | null;
|
|
262
|
-
contentHash: string;
|
|
263
|
-
embeddingJobId: string | null;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ── Observation insert helpers ──────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Insert a raw observation row and its associated chunk. If a chunk with the
|
|
270
|
-
* same content hash already exists in the scope, the chunk insert is skipped
|
|
271
|
-
* (idempotent dual-write safety). An `embed_observation` job is enqueued when
|
|
272
|
-
* a new chunk is created.
|
|
273
|
-
*
|
|
274
|
-
* Returns the observation ID, chunk ID (null if deduplicated), content hash,
|
|
275
|
-
* and embedding job ID (null if no new chunk was created).
|
|
276
|
-
*/
|
|
277
|
-
export function insertObservation(
|
|
278
|
-
params: InsertObservationParams,
|
|
279
|
-
): InsertedObservation {
|
|
280
|
-
const db = getDb();
|
|
281
|
-
const now = Date.now();
|
|
282
|
-
const scopeId = params.scopeId ?? "default";
|
|
283
|
-
const modality = params.modality ?? "text";
|
|
284
|
-
|
|
285
|
-
const observationId = uuid();
|
|
286
|
-
const contentHash = computeObservationContentHash(scopeId, params.content);
|
|
287
|
-
|
|
288
|
-
// Insert the observation row
|
|
289
|
-
db.insert(memoryObservations)
|
|
290
|
-
.values({
|
|
291
|
-
id: observationId,
|
|
292
|
-
scopeId,
|
|
293
|
-
conversationId: params.conversationId,
|
|
294
|
-
messageId: params.messageId ?? null,
|
|
295
|
-
role: params.role,
|
|
296
|
-
content: params.content,
|
|
297
|
-
modality,
|
|
298
|
-
source: params.source ?? null,
|
|
299
|
-
createdAt: now,
|
|
300
|
-
})
|
|
301
|
-
.run();
|
|
302
|
-
|
|
303
|
-
// Attempt to insert the chunk — the unique index on (scope_id, content_hash)
|
|
304
|
-
// will cause a conflict if this content was already chunked. We use
|
|
305
|
-
// onConflictDoNothing to silently skip the duplicate.
|
|
306
|
-
const chunkId = uuid();
|
|
307
|
-
const tokenEstimate = estimateTextTokens(params.content);
|
|
308
|
-
|
|
309
|
-
db.insert(memoryChunks)
|
|
310
|
-
.values({
|
|
311
|
-
id: chunkId,
|
|
312
|
-
scopeId,
|
|
313
|
-
observationId,
|
|
314
|
-
content: params.content,
|
|
315
|
-
tokenEstimate,
|
|
316
|
-
contentHash,
|
|
317
|
-
createdAt: now,
|
|
318
|
-
})
|
|
319
|
-
.onConflictDoNothing({
|
|
320
|
-
target: [memoryChunks.scopeId, memoryChunks.contentHash],
|
|
321
|
-
})
|
|
322
|
-
.run();
|
|
323
|
-
|
|
324
|
-
// Drizzle bun:sqlite .run() returns void; use rawChanges() to detect no-ops
|
|
325
|
-
const chunkInserted = rawChanges() > 0;
|
|
326
|
-
|
|
327
|
-
let embeddingJobId: string | null = null;
|
|
328
|
-
if (chunkInserted) {
|
|
329
|
-
embeddingJobId = enqueueMemoryJob("embed_observation", {
|
|
330
|
-
observationId,
|
|
331
|
-
chunkId,
|
|
332
|
-
});
|
|
333
|
-
log.debug(
|
|
334
|
-
{ observationId, chunkId, contentHash, embeddingJobId },
|
|
335
|
-
"Inserted observation with new chunk, enqueued embed job",
|
|
336
|
-
);
|
|
337
|
-
} else {
|
|
338
|
-
log.debug(
|
|
339
|
-
{ observationId, contentHash },
|
|
340
|
-
"Inserted observation, chunk deduplicated by content hash",
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
observationId,
|
|
346
|
-
chunkId: chunkInserted ? chunkId : null,
|
|
347
|
-
contentHash,
|
|
348
|
-
embeddingJobId,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Insert multiple observations in a single transaction.
|
|
354
|
-
* Returns the results for each insertion.
|
|
355
|
-
*/
|
|
356
|
-
export function insertObservations(
|
|
357
|
-
observations: InsertObservationParams[],
|
|
358
|
-
): InsertedObservation[] {
|
|
359
|
-
const db = getDb();
|
|
360
|
-
const results: InsertedObservation[] = [];
|
|
361
|
-
db.transaction((tx) => {
|
|
362
|
-
// We don't use `tx` directly for individual inserts because
|
|
363
|
-
// insertObservation uses getDb() internally. Instead, the transaction
|
|
364
|
-
// wrapper ensures atomicity at the SQLite level.
|
|
365
|
-
void tx;
|
|
366
|
-
for (const obs of observations) {
|
|
367
|
-
results.push(insertObservation(obs));
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
return results;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Look up an observation by ID.
|
|
375
|
-
*/
|
|
376
|
-
export function getObservation(
|
|
377
|
-
observationId: string,
|
|
378
|
-
): typeof memoryObservations.$inferSelect | undefined {
|
|
379
|
-
const db = getDb();
|
|
380
|
-
return db
|
|
381
|
-
.select()
|
|
382
|
-
.from(memoryObservations)
|
|
383
|
-
.where(eq(memoryObservations.id, observationId))
|
|
384
|
-
.get();
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Look up a chunk by observation ID. Returns the first chunk linked to the
|
|
389
|
-
* given observation, or undefined if none exists.
|
|
390
|
-
*/
|
|
391
|
-
export function getChunkByObservationId(
|
|
392
|
-
observationId: string,
|
|
393
|
-
): typeof memoryChunks.$inferSelect | undefined {
|
|
394
|
-
const db = getDb();
|
|
395
|
-
return db
|
|
396
|
-
.select()
|
|
397
|
-
.from(memoryChunks)
|
|
398
|
-
.where(eq(memoryChunks.observationId, observationId))
|
|
399
|
-
.get();
|
|
400
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared formatting helpers for rendering capped markdown sections
|
|
3
|
-
* inside the `<memory_brief>` wrapper.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface BriefEntry {
|
|
7
|
-
/** One-line markdown bullet text (no leading `- `). */
|
|
8
|
-
text: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Render a titled markdown section with a capped number of bullet entries.
|
|
13
|
-
*
|
|
14
|
-
* Returns `null` when `entries` is empty so callers can easily omit absent
|
|
15
|
-
* sections. The output is a markdown string like:
|
|
16
|
-
*
|
|
17
|
-
* ```
|
|
18
|
-
* ### Time-Relevant Context
|
|
19
|
-
* - Meeting with Alice in 2 hours
|
|
20
|
-
* - Quarterly review deadline tomorrow
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export function renderBriefSection(
|
|
24
|
-
title: string,
|
|
25
|
-
entries: BriefEntry[],
|
|
26
|
-
maxEntries: number,
|
|
27
|
-
): string | null {
|
|
28
|
-
if (entries.length === 0) return null;
|
|
29
|
-
|
|
30
|
-
const capped = entries.slice(0, maxEntries);
|
|
31
|
-
const bullets = capped.map((e) => `- ${e.text}`).join("\n");
|
|
32
|
-
return `### ${title}\n${bullets}`;
|
|
33
|
-
}
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Open-loop brief compiler.
|
|
3
|
-
*
|
|
4
|
-
* Merges reducer-created open_loops rows with live task queue (work items)
|
|
5
|
-
* and follow-up state into a ranked, deduplicated list of bullets for the
|
|
6
|
-
* memory brief.
|
|
7
|
-
*
|
|
8
|
-
* Ranking tiers (highest first):
|
|
9
|
-
* 1. Overdue — dueAt in the past
|
|
10
|
-
* 2. Due ≤ 24 h — dueAt within the next 24 hours
|
|
11
|
-
* 3. Due ≤ 7 d — dueAt within the next 7 days
|
|
12
|
-
* 4. High-priority / blocked — work items at priority tier 0 or
|
|
13
|
-
* follow-ups with status "nudged"
|
|
14
|
-
* 5. Recently touched — updatedAt within the last 48 hours
|
|
15
|
-
*
|
|
16
|
-
* After ranked items, at most ONE low-salience loop is resurfaced via
|
|
17
|
-
* deterministic pseudo-random sampling seeded by `scopeId + userMessageId`.
|
|
18
|
-
* The resurfaced loop's `surfacedAt` is updated so it won't repeat
|
|
19
|
-
* immediately.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { and, eq } from "drizzle-orm";
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
type BriefFollowUp,
|
|
26
|
-
getPendingAndOverdueFollowUps,
|
|
27
|
-
} from "../followups/followup-store.js";
|
|
28
|
-
import {
|
|
29
|
-
type ActionableWorkItem,
|
|
30
|
-
getActionableWorkItems,
|
|
31
|
-
} from "../tasks/task-store.js";
|
|
32
|
-
import { getDb } from "./db.js";
|
|
33
|
-
import { updateLastSurfacedAt } from "./reducer-store.js";
|
|
34
|
-
import { openLoops } from "./schema.js";
|
|
35
|
-
|
|
36
|
-
// ── Constants ─────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
const MS_24H = 24 * 60 * 60 * 1000;
|
|
39
|
-
const MS_7D = 7 * 24 * 60 * 60 * 1000;
|
|
40
|
-
const MS_48H = 48 * 60 * 60 * 1000;
|
|
41
|
-
|
|
42
|
-
// ── Types ─────────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
export interface OpenLoopBullet {
|
|
45
|
-
/** Dedupe key — one of `loop:<id>`, `work:<id>`, or `followup:<id>`. */
|
|
46
|
-
key: string;
|
|
47
|
-
summary: string;
|
|
48
|
-
tier: number; // 1–5, lower = higher priority
|
|
49
|
-
source: "loop" | "work_item" | "followup";
|
|
50
|
-
sourceId: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface OpenLoopBriefResult {
|
|
54
|
-
/** Bullets ordered by tier then recency. */
|
|
55
|
-
bullets: OpenLoopBullet[];
|
|
56
|
-
/** ID of the resurfaced low-salience loop, if any. */
|
|
57
|
-
resurfacedLoopId: string | null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface OpenLoopRow {
|
|
61
|
-
id: string;
|
|
62
|
-
scopeId: string;
|
|
63
|
-
summary: string;
|
|
64
|
-
status: string;
|
|
65
|
-
source: string;
|
|
66
|
-
dueAt: number | null;
|
|
67
|
-
surfacedAt: number | null;
|
|
68
|
-
createdAt: number;
|
|
69
|
-
updatedAt: number;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Deterministic hash ────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Simple 32-bit FNV-1a hash for deterministic pseudo-random selection.
|
|
76
|
-
* Not cryptographic — only needs to be well-distributed for sampling.
|
|
77
|
-
*/
|
|
78
|
-
function fnv1a(input: string): number {
|
|
79
|
-
let h = 0x811c9dc5;
|
|
80
|
-
for (let i = 0; i < input.length; i++) {
|
|
81
|
-
h ^= input.charCodeAt(i);
|
|
82
|
-
h = Math.imul(h, 0x01000193);
|
|
83
|
-
}
|
|
84
|
-
return h >>> 0; // unsigned 32-bit
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── Tier assignment ───────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
function assignLoopTier(loop: OpenLoopRow, now: number): number {
|
|
90
|
-
if (loop.dueAt != null) {
|
|
91
|
-
if (loop.dueAt <= now) return 1; // overdue
|
|
92
|
-
if (loop.dueAt <= now + MS_24H) return 2; // due within 24h
|
|
93
|
-
if (loop.dueAt <= now + MS_7D) return 3; // due within 7d
|
|
94
|
-
}
|
|
95
|
-
if (now - loop.updatedAt <= MS_48H) return 5; // recently touched
|
|
96
|
-
return 6; // low salience — candidate for resurfacing
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function assignWorkItemTier(item: ActionableWorkItem, now: number): number {
|
|
100
|
-
if (item.priorityTier === 0) return 4; // high priority
|
|
101
|
-
if (item.status === "awaiting_review") return 4;
|
|
102
|
-
if (now - item.updatedAt <= MS_48H) return 5;
|
|
103
|
-
return 6;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function assignFollowUpTier(fu: BriefFollowUp, now: number): number {
|
|
107
|
-
if (fu.expectedResponseBy != null && fu.expectedResponseBy <= now) return 1;
|
|
108
|
-
if (fu.expectedResponseBy != null && fu.expectedResponseBy <= now + MS_24H)
|
|
109
|
-
return 2;
|
|
110
|
-
if (fu.expectedResponseBy != null && fu.expectedResponseBy <= now + MS_7D)
|
|
111
|
-
return 3;
|
|
112
|
-
if (fu.status === "nudged") return 4;
|
|
113
|
-
if (now - fu.updatedAt <= MS_48H) return 5;
|
|
114
|
-
return 6;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ── Compiler ──────────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Compile the open-loop section of the memory brief.
|
|
121
|
-
*
|
|
122
|
-
* @param scopeId Memory scope (e.g. assistant instance ID)
|
|
123
|
-
* @param userMessageId Current user message ID — used as part of the
|
|
124
|
-
* resurfacing seed so the selection is deterministic
|
|
125
|
-
* per turn but varies across turns.
|
|
126
|
-
* @param now Current epoch-ms timestamp (injectable for testing).
|
|
127
|
-
*/
|
|
128
|
-
export function compileOpenLoopBrief(
|
|
129
|
-
scopeId: string,
|
|
130
|
-
userMessageId: string,
|
|
131
|
-
now: number = Date.now(),
|
|
132
|
-
): OpenLoopBriefResult {
|
|
133
|
-
// 1. Gather data from all three sources
|
|
134
|
-
const loops = getOpenLoopsForScope(scopeId);
|
|
135
|
-
const workItems = getActionableWorkItems();
|
|
136
|
-
const followUps = getPendingAndOverdueFollowUps();
|
|
137
|
-
|
|
138
|
-
// 2. Convert to bullets with tier assignment
|
|
139
|
-
const bullets: OpenLoopBullet[] = [];
|
|
140
|
-
const seenKeys = new Set<string>();
|
|
141
|
-
|
|
142
|
-
// Loops first (they are the authoritative open-loop source)
|
|
143
|
-
for (const loop of loops) {
|
|
144
|
-
const key = `loop:${loop.id}`;
|
|
145
|
-
if (seenKeys.has(key)) continue;
|
|
146
|
-
seenKeys.add(key);
|
|
147
|
-
bullets.push({
|
|
148
|
-
key,
|
|
149
|
-
summary: loop.summary,
|
|
150
|
-
tier: assignLoopTier(loop, now),
|
|
151
|
-
source: "loop",
|
|
152
|
-
sourceId: loop.id,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Work items — skip if already represented by a loop with matching summary
|
|
157
|
-
const loopSummaries = new Set(loops.map((l) => l.summary.toLowerCase()));
|
|
158
|
-
for (const item of workItems) {
|
|
159
|
-
const key = `work:${item.id}`;
|
|
160
|
-
if (seenKeys.has(key)) continue;
|
|
161
|
-
// Deduplicate against loop summaries
|
|
162
|
-
if (loopSummaries.has(item.title.toLowerCase())) continue;
|
|
163
|
-
seenKeys.add(key);
|
|
164
|
-
bullets.push({
|
|
165
|
-
key,
|
|
166
|
-
summary: item.title,
|
|
167
|
-
tier: assignWorkItemTier(item, now),
|
|
168
|
-
source: "work_item",
|
|
169
|
-
sourceId: item.id,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Follow-ups — skip if already represented by a loop
|
|
174
|
-
for (const fu of followUps) {
|
|
175
|
-
const key = `followup:${fu.id}`;
|
|
176
|
-
if (seenKeys.has(key)) continue;
|
|
177
|
-
const fuSummary =
|
|
178
|
-
`Awaiting reply on ${fu.channel} (${fu.conversationId})`.toLowerCase();
|
|
179
|
-
if (loopSummaries.has(fuSummary)) continue;
|
|
180
|
-
seenKeys.add(key);
|
|
181
|
-
bullets.push({
|
|
182
|
-
key,
|
|
183
|
-
summary: `Awaiting reply on ${fu.channel} (${fu.conversationId})`,
|
|
184
|
-
tier: assignFollowUpTier(fu, now),
|
|
185
|
-
source: "followup",
|
|
186
|
-
sourceId: fu.id,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 3. Split into ranked (tiers 1–5) and low-salience (tier 6)
|
|
191
|
-
const ranked: OpenLoopBullet[] = [];
|
|
192
|
-
const lowSalience: OpenLoopBullet[] = [];
|
|
193
|
-
|
|
194
|
-
for (const b of bullets) {
|
|
195
|
-
if (b.tier <= 5) {
|
|
196
|
-
ranked.push(b);
|
|
197
|
-
} else {
|
|
198
|
-
lowSalience.push(b);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Sort ranked: by tier ascending, then by source priority (loop > work > followup)
|
|
203
|
-
ranked.sort((a, b) => {
|
|
204
|
-
if (a.tier !== b.tier) return a.tier - b.tier;
|
|
205
|
-
return sourcePriority(a.source) - sourcePriority(b.source);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// 4. Deterministic resurfacing of ONE low-salience loop
|
|
209
|
-
let resurfacedLoopId: string | null = null;
|
|
210
|
-
|
|
211
|
-
if (lowSalience.length > 0) {
|
|
212
|
-
// Only consider loops from the open_loops table for resurfacing
|
|
213
|
-
// (work items and follow-ups have their own lifecycle)
|
|
214
|
-
const resurfaceCandidates = lowSalience.filter((b) => b.source === "loop");
|
|
215
|
-
|
|
216
|
-
if (resurfaceCandidates.length > 0) {
|
|
217
|
-
// Sort candidates deterministically by key for stable ordering
|
|
218
|
-
resurfaceCandidates.sort((a, b) => a.key.localeCompare(b.key));
|
|
219
|
-
|
|
220
|
-
const seed = `${scopeId}:${userMessageId}`;
|
|
221
|
-
const hash = fnv1a(seed);
|
|
222
|
-
const idx = hash % resurfaceCandidates.length;
|
|
223
|
-
const picked = resurfaceCandidates[idx];
|
|
224
|
-
|
|
225
|
-
// Promote picked to tier 5 and add to ranked output
|
|
226
|
-
ranked.push({ ...picked, tier: 5 });
|
|
227
|
-
resurfacedLoopId = picked.sourceId;
|
|
228
|
-
|
|
229
|
-
// Update surfacedAt so it is deprioritised on subsequent turns
|
|
230
|
-
updateLastSurfacedAt(picked.sourceId, now);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Re-sort after potential resurfaced bullet insertion
|
|
235
|
-
ranked.sort((a, b) => {
|
|
236
|
-
if (a.tier !== b.tier) return a.tier - b.tier;
|
|
237
|
-
return sourcePriority(a.source) - sourcePriority(b.source);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
bullets: ranked,
|
|
242
|
-
resurfacedLoopId,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ── Internals ─────────────────────────────────────────────────────────
|
|
247
|
-
|
|
248
|
-
function sourcePriority(source: "loop" | "work_item" | "followup"): number {
|
|
249
|
-
switch (source) {
|
|
250
|
-
case "loop":
|
|
251
|
-
return 0;
|
|
252
|
-
case "work_item":
|
|
253
|
-
return 1;
|
|
254
|
-
case "followup":
|
|
255
|
-
return 2;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function getOpenLoopsForScope(scopeId: string): OpenLoopRow[] {
|
|
260
|
-
const db = getDb();
|
|
261
|
-
return db
|
|
262
|
-
.select()
|
|
263
|
-
.from(openLoops)
|
|
264
|
-
.where(and(eq(openLoops.scopeId, scopeId), eq(openLoops.status, "open")))
|
|
265
|
-
.all() as OpenLoopRow[];
|
|
266
|
-
}
|