@vellumai/assistant 0.8.5 → 0.8.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/AGENTS.md +33 -1
- package/ARCHITECTURE.md +1 -1
- package/Dockerfile +1 -0
- package/bun.lock +11 -2
- package/bunfig.toml +6 -1
- package/docker-entrypoint.sh +8 -6
- package/docs/credential-execution-service.md +6 -6
- package/docs/plugins.md +67 -31
- package/examples/plugins/echo/register.ts +4 -7
- package/knip.json +1 -0
- package/node_modules/@vellumai/environments/bun.lock +24 -0
- package/node_modules/@vellumai/environments/package.json +18 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
- package/node_modules/@vellumai/environments/src/index.ts +11 -0
- package/node_modules/@vellumai/environments/src/seeds.ts +73 -0
- package/node_modules/@vellumai/environments/src/types.ts +70 -0
- package/node_modules/@vellumai/environments/tsconfig.json +20 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +11 -0
- package/node_modules/@vellumai/skill-host-contracts/src/client.ts +15 -17
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +10 -3
- package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
- package/openapi.yaml +5585 -469
- package/package.json +7 -3
- package/scripts/generate-openapi.ts +20 -13
- package/src/__tests__/actor-token-service.test.ts +3 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +42 -80
- package/src/__tests__/agent-loop-exit-reason.test.ts +336 -42
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +141 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +21 -33
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +6 -4
- package/src/__tests__/agent-loop-thinking.test.ts +17 -12
- package/src/__tests__/agent-loop.test.ts +207 -341
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +5 -2
- package/src/__tests__/agent-wake-override-profile.test.ts +23 -40
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
- package/src/__tests__/annotate-risk-options.test.ts +1 -0
- package/src/__tests__/anthropic-provider.test.ts +201 -55
- package/src/__tests__/app-builder-skill-instructions.test.ts +22 -0
- package/src/__tests__/app-control-flow.test.ts +5 -0
- package/src/__tests__/approval-cascade.test.ts +5 -11
- package/src/__tests__/approval-routes-http.test.ts +13 -15
- package/src/__tests__/assert-not-live-db.ts +79 -0
- package/src/__tests__/assistant-event.test.ts +15 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -27
- package/src/__tests__/audit-log-rotation.test.ts +2 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +6 -6
- package/src/__tests__/avatar-e2e.test.ts +7 -37
- package/src/__tests__/avatar-generator.test.ts +12 -42
- package/src/__tests__/avatar-identity-sync.test.ts +28 -3
- package/src/__tests__/background-shell-bash.test.ts +3 -7
- package/src/__tests__/background-workers-disk-pressure.test.ts +5 -8
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/btw-routes.test.ts +10 -14
- package/src/__tests__/call-controller.test.ts +3 -2
- package/src/__tests__/call-pointer-messages.test.ts +5 -3
- package/src/__tests__/call-site-routing-provider.test.ts +22 -40
- package/src/__tests__/catalog-files.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +51 -22
- package/src/__tests__/channel-approvals.test.ts +3 -1
- package/src/__tests__/channel-guardian.test.ts +3 -2
- package/src/__tests__/channel-invite-transport.test.ts +1 -5
- package/src/__tests__/channel-readiness-routes.test.ts +0 -4
- package/src/__tests__/channel-readiness-slack-remote.test.ts +170 -0
- package/src/__tests__/channel-reply-delivery.test.ts +35 -0
- package/src/__tests__/channel-retry-sweep.test.ts +388 -79
- package/src/__tests__/checker.test.ts +12 -12
- package/src/__tests__/circuit-breaker-pipeline.test.ts +3 -3
- package/src/__tests__/clawhub-files.test.ts +1 -0
- package/src/__tests__/compaction-events.test.ts +6 -17
- package/src/__tests__/compaction-pipeline.test.ts +1 -1
- package/src/__tests__/compaction-timeout-recovery.test.ts +37 -48
- package/src/__tests__/compaction-trail-store.test.ts +186 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +1 -0
- package/src/__tests__/compactor-image-manifest-trust.test.ts +112 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +1 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +7 -5
- package/src/__tests__/computer-use-tools.test.ts +14 -16
- package/src/__tests__/config-loader-backfill.test.ts +13 -28
- package/src/__tests__/config-loader-corrupt.test.ts +5 -5
- package/src/__tests__/config-loader-platform-defaults.test.ts +93 -26
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +3 -3
- package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -4
- package/src/__tests__/config-schema.test.ts +10 -10
- package/src/__tests__/config-watcher.test.ts +28 -0
- package/src/__tests__/connection-model-compat.test.ts +83 -0
- package/src/__tests__/contacts-tools.test.ts +3 -2
- package/src/__tests__/context-search-agent-runner.test.ts +6 -3
- package/src/__tests__/context-token-estimator.test.ts +56 -0
- package/src/__tests__/context-window-manager-compact-retry.test.ts +291 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +19 -7
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -27
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +464 -90
- package/src/__tests__/conversation-agent-loop.test.ts +1069 -64
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -3
- package/src/__tests__/conversation-app-control-instantiation.test.ts +29 -19
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -1
- package/src/__tests__/conversation-attention-store.test.ts +101 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +3 -2
- package/src/__tests__/conversation-clear-safety.test.ts +20 -10
- package/src/__tests__/conversation-confirmation-signals.test.ts +16 -45
- package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
- package/src/__tests__/conversation-disk-view.test.ts +10 -17
- package/src/__tests__/conversation-error.test.ts +30 -0
- package/src/__tests__/conversation-fork-crud.test.ts +132 -157
- package/src/__tests__/conversation-fork-route.test.ts +19 -16
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-inference-profile-list.test.ts +3 -2
- package/src/__tests__/conversation-inference-profile-route.test.ts +3 -2
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -6
- package/src/__tests__/conversation-lifecycle.test.ts +4 -2
- package/src/__tests__/conversation-list-source.test.ts +3 -2
- package/src/__tests__/conversation-load-history-repair.test.ts +5 -3
- package/src/__tests__/conversation-load-history-stripped.test.ts +2 -1
- package/src/__tests__/conversation-message-sync-tags.test.ts +3 -4
- package/src/__tests__/conversation-pairing.test.ts +87 -4
- package/src/__tests__/conversation-pre-run-repair.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +30 -7
- package/src/__tests__/conversation-process-callsite.test.ts +28 -30
- package/src/__tests__/conversation-provider-retry-repair.test.ts +58 -44
- package/src/__tests__/conversation-queue.test.ts +603 -455
- package/src/__tests__/conversation-routes-disk-view.test.ts +6 -20
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +35 -10
- package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +98 -22
- package/src/__tests__/conversation-runtime-workspace.test.ts +19 -1
- package/src/__tests__/conversation-skill-tools.test.ts +38 -142
- package/src/__tests__/conversation-slash-queue.test.ts +120 -62
- package/src/__tests__/conversation-slash-unknown.test.ts +18 -15
- package/src/__tests__/conversation-speed-override.test.ts +9 -22
- package/src/__tests__/conversation-stream-state.test.ts +484 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +52 -15
- package/src/__tests__/conversation-surfaces-app-control.test.ts +32 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +6 -3
- package/src/__tests__/conversation-surfaces-standalone.test.ts +6 -3
- package/src/__tests__/conversation-surfaces-state-update.test.ts +8 -5
- package/src/__tests__/conversation-surfaces-table-action.test.ts +13 -32
- package/src/__tests__/conversation-sync-tags.test.ts +128 -12
- package/src/__tests__/conversation-title-service.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +53 -11
- package/src/__tests__/conversation-unread-route.test.ts +14 -2
- package/src/__tests__/conversation-usage.test.ts +1 -2
- package/src/__tests__/conversation-wipe.test.ts +1 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +53 -22
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +32 -7
- package/src/__tests__/credential-broker-browser-fill.test.ts +3 -3
- package/src/__tests__/credential-broker-server-use.test.ts +5 -5
- package/src/__tests__/credential-execution-client.test.ts +72 -1
- package/src/__tests__/credential-execution-feature-gates.test.ts +10 -12
- package/src/__tests__/credential-execution-tools.test.ts +1 -2
- package/src/__tests__/credential-health-service.test.ts +252 -3
- package/src/__tests__/credential-security-invariants.test.ts +5 -6
- package/src/__tests__/credential-vault-unit.test.ts +19 -19
- package/src/__tests__/credential-vault.test.ts +5 -5
- package/src/__tests__/cross-provider-web-search.test.ts +61 -3
- package/src/__tests__/cu-unified-flow.test.ts +26 -1
- package/src/__tests__/db-connection-isolation.test.ts +7 -6
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +8 -10
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +7 -10
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +9 -15
- package/src/__tests__/db-schedule-syntax-migration.test.ts +11 -0
- package/src/__tests__/db-test-helpers.ts +58 -0
- package/src/__tests__/disk-pressure-guard.test.ts +119 -36
- package/src/__tests__/disk-pressure-lifecycle.test.ts +13 -10
- package/src/__tests__/disk-pressure-routes.test.ts +9 -35
- package/src/__tests__/disk-pressure-tools.test.ts +0 -4
- package/src/__tests__/dm-persistence.test.ts +33 -42
- package/src/__tests__/document-create-dedupe.test.ts +189 -0
- package/src/__tests__/document-find-replace.test.ts +3 -2
- package/src/__tests__/document-tool-security.test.ts +81 -2
- package/src/__tests__/dynamic-page-surface.test.ts +68 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +5 -4
- package/src/__tests__/edit-propagation.test.ts +1 -2
- package/src/__tests__/empty-response-pipeline.test.ts +127 -5
- package/src/__tests__/encrypted-store-test-helpers.ts +56 -0
- package/src/__tests__/encrypted-store.test.ts +11 -9
- package/src/__tests__/feature-flag-test-helpers.ts +53 -0
- package/src/__tests__/filing-service.test.ts +3 -2
- package/src/__tests__/first-greeting.test.ts +103 -12
- package/src/__tests__/gateway-flag-listener.test.ts +0 -1
- package/src/__tests__/gemini-inline-media.test.ts +78 -0
- package/src/__tests__/gemini-provider.test.ts +375 -26
- package/src/__tests__/guardian-action-sweep.test.ts +3 -2
- package/src/__tests__/guardian-outbound-http.test.ts +3 -2
- package/src/__tests__/guardian-routing-state.test.ts +60 -71
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +48 -3
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +10 -7
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +3 -1
- package/src/__tests__/helpers/mock-logger.ts +26 -0
- package/src/__tests__/history-repair-hook.test.ts +161 -0
- package/src/__tests__/history-repair-observability.test.ts +1 -1
- package/src/__tests__/history-repair.test.ts +2 -1
- package/src/__tests__/host-app-control-proxy.test.ts +2 -0
- package/src/__tests__/host-bash-routes.test.ts +1 -0
- package/src/__tests__/host-cu-proxy.test.ts +2 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +1 -0
- package/src/__tests__/host-file-edit-tool.test.ts +4 -2
- package/src/__tests__/host-file-proxy.test.ts +31 -0
- package/src/__tests__/host-file-read-tool.test.ts +4 -2
- package/src/__tests__/host-file-routes-targeted.test.ts +1 -0
- package/src/__tests__/host-file-write-tool.test.ts +9 -3
- package/src/__tests__/host-proxy-preactivation.test.ts +53 -14
- package/src/__tests__/host-shell-tool.test.ts +11 -5
- package/src/__tests__/host-transfer-routes-targeted.test.ts +1 -0
- package/src/__tests__/http-conversation-lineage.test.ts +3 -2
- package/src/__tests__/http-user-message-parity.test.ts +31 -9
- package/src/__tests__/identity-intro-cache.test.ts +154 -22
- package/src/__tests__/inbound-slack-persistence.test.ts +51 -74
- package/src/__tests__/inference-profile-reaper.test.ts +3 -2
- package/src/__tests__/inference-profile-session-ipc.test.ts +3 -2
- package/src/__tests__/injector-background-turn.test.ts +1 -1
- package/src/__tests__/injector-chain.test.ts +1 -1
- package/src/__tests__/injector-disk-pressure.test.ts +4 -18
- package/src/__tests__/injector-document-comments.test.ts +1 -1
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +1 -1
- package/src/__tests__/injector-v3-suppression.test.ts +220 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
- package/src/__tests__/list-messages-attachments.test.ts +7 -8
- package/src/__tests__/list-messages-hidden-metadata.test.ts +93 -11
- package/src/__tests__/list-messages-page-latest.test.ts +0 -1
- package/src/__tests__/list-messages-tool-merge.test.ts +36 -6
- package/src/__tests__/llm-call-pipeline.test.ts +21 -15
- package/src/__tests__/llm-context-normalization.test.ts +42 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +42 -86
- package/src/__tests__/llm-resolver.test.ts +346 -39
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/llm-usage-store.test.ts +45 -0
- package/src/__tests__/log-export-routes.test.ts +59 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +1 -8
- package/src/__tests__/manual-token-reconciliation.test.ts +76 -1
- package/src/__tests__/mcp-abort-signal.test.ts +14 -0
- package/src/__tests__/mcp-auth-routes.test.ts +15 -10
- package/src/__tests__/mcp-client-auth.test.ts +14 -0
- package/src/__tests__/mcp-health-check.test.ts +18 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +1 -1
- package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +9 -4
- package/src/__tests__/migration-export-http.test.ts +12 -12
- package/src/__tests__/migration-import-commit-http.test.ts +8 -8
- package/src/__tests__/migration-import-from-url.test.ts +3 -3
- package/src/__tests__/migration-import-preflight-http.test.ts +7 -7
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +18 -2
- package/src/__tests__/model-intents.test.ts +3 -3
- package/src/__tests__/native-web-search.test.ts +44 -22
- package/src/__tests__/notification-decision-identity.test.ts +9 -18
- package/src/__tests__/notification-decision-recipient-context.test.ts +3 -6
- package/src/__tests__/notification-deep-link.test.ts +62 -0
- package/src/__tests__/oauth-commands-routes.test.ts +38 -1
- package/src/__tests__/oauth-provider-visibility.test.ts +8 -8
- package/src/__tests__/oauth-store.test.ts +3 -2
- package/src/__tests__/onboarding-template-contract.test.ts +13 -2
- package/src/__tests__/openai-provider.test.ts +74 -79
- package/src/__tests__/openai-responses-provider.test.ts +90 -86
- package/src/__tests__/openrouter-provider-only.test.ts +27 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +48 -2
- package/src/__tests__/overflow-reduce-pipeline.test.ts +2 -4
- package/src/__tests__/parallel-tool.benchmark.test.ts +24 -36
- package/src/__tests__/persistence-pipeline.test.ts +154 -27
- package/src/__tests__/persistence-secret-redaction.test.ts +85 -13
- package/src/__tests__/pipeline-runner.test.ts +2 -3
- package/src/__tests__/plugin-bootstrap.test.ts +60 -36
- package/src/__tests__/plugin-route-contribution.test.ts +6 -16
- package/src/__tests__/plugin-skill-contribution.test.ts +7 -17
- package/src/__tests__/plugin-tool-contribution.test.ts +51 -64
- package/src/__tests__/plugin-types.test.ts +7 -14
- package/src/__tests__/prechat-onboarding-contract.test.ts +23 -0
- package/src/__tests__/process-message-background-slack.test.ts +38 -32
- package/src/__tests__/process-message-display-content.test.ts +49 -64
- package/src/__tests__/provider-catalog-visibility.test.ts +9 -9
- package/src/__tests__/provider-commit-message-generator.test.ts +19 -14
- package/src/__tests__/provider-error-scenarios.test.ts +7 -6
- package/src/__tests__/provider-platform-proxy-integration.test.ts +215 -8
- package/src/__tests__/provider-registry-ollama.test.ts +45 -22
- package/src/__tests__/provider-send-message-override-profile.test.ts +9 -25
- package/src/__tests__/provider-streaming.benchmark.test.ts +12 -22
- package/src/__tests__/provider-usage-tracking.test.ts +0 -6
- package/src/__tests__/ratelimit.test.ts +9 -4
- package/src/__tests__/recording-handler.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +82 -76
- package/src/__tests__/relay-server.test.ts +30 -23
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +5 -8
- package/src/__tests__/retry-thinking-tool-choice.test.ts +10 -13
- package/src/__tests__/retry-verbosity-normalization.test.ts +5 -8
- package/src/__tests__/runtime-attachment-metadata.test.ts +3 -2
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +353 -0
- package/src/__tests__/schedule-routes.test.ts +80 -10
- package/src/__tests__/schedule-store.test.ts +83 -1
- package/src/__tests__/schedule-tools.test.ts +125 -0
- package/src/__tests__/scheduler-reuse-conversation.test.ts +48 -3
- package/src/__tests__/secret-ingress-http.test.ts +7 -3
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +11 -7
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +11 -9
- package/src/__tests__/secret-response-routing.test.ts +13 -11
- package/src/__tests__/secure-keys.test.ts +3 -3
- package/src/__tests__/send-endpoint-busy.test.ts +83 -43
- package/src/__tests__/server-history-render.test.ts +4 -1
- package/src/__tests__/shell-observability.test.ts +249 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +19 -21
- package/src/__tests__/skill-feature-flags.test.ts +20 -22
- package/src/__tests__/skill-load-feature-flag.test.ts +15 -15
- package/src/__tests__/skill-projection-feature-flag.test.ts +44 -30
- package/src/__tests__/skill-projection.benchmark.test.ts +5 -7
- package/src/__tests__/skill-tool-factory.test.ts +96 -95
- package/src/__tests__/skills-files-catalog-fallback.test.ts +10 -0
- package/src/__tests__/skillssh-files.test.ts +1 -0
- package/src/__tests__/slack-channel-config.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +6 -6
- package/src/__tests__/strip-memory-injections.test.ts +102 -14
- package/src/__tests__/subagent-call-site-routing.test.ts +13 -5
- package/src/__tests__/subagent-disposal.test.ts +27 -8
- package/src/__tests__/subagent-fork-notifications.test.ts +24 -9
- package/src/__tests__/subagent-fork-spawn.test.ts +13 -4
- package/src/__tests__/subagent-manager-notify.test.ts +20 -8
- package/src/__tests__/subagent-notify-parent.test.ts +5 -4
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +58 -0
- package/src/__tests__/subagent-tools.test.ts +2 -1
- package/src/__tests__/suggestion-routes.test.ts +4 -3
- package/src/__tests__/sync-message-contract.test.ts +19 -16
- package/src/__tests__/system-prompt.test.ts +92 -0
- package/src/__tests__/terminal-tools.test.ts +3 -24
- package/src/__tests__/test-preload-verifier.ts +68 -0
- package/src/__tests__/test-preload.ts +32 -39
- package/src/__tests__/thread-backfill.test.ts +4 -9
- package/src/__tests__/title-generate-pipeline.test.ts +1 -1
- package/src/__tests__/token-estimate-pipeline.test.ts +2 -4
- package/src/__tests__/tool-error-pipeline.test.ts +2 -2
- package/src/__tests__/tool-execute-pipeline.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +20 -7
- package/src/__tests__/tool-executor.test.ts +55 -10
- package/src/__tests__/tool-preview-lifecycle.test.ts +14 -11
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +9 -12
- package/src/__tests__/tool-result-truncation.test.ts +3 -1
- package/src/__tests__/tools-audio-read.test.ts +113 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +44 -84
- package/src/__tests__/turn-events-store.test.ts +11 -7
- package/src/__tests__/twilio-routes.test.ts +3 -2
- package/src/__tests__/validate-input.test.ts +381 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +10 -7
- package/src/__tests__/voice-session-bridge.test.ts +50 -35
- package/src/__tests__/workspace-migration-090-memory-router-cost-optimized-profile.test.ts +326 -0
- package/src/__tests__/workspace-migration-091-retighten-migration-onboarding-thread.test.ts +166 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +143 -31
- package/src/acp/prepare-agent-env.ts +52 -11
- package/src/acp/session-manager.ts +5 -6
- package/src/agent/compaction-circuit.ts +140 -0
- package/src/agent/loop.ts +489 -85
- package/src/api/README.md +126 -2
- package/src/api/constants/call-sites.ts +27 -0
- package/src/api/constants/tool-execution.ts +21 -0
- package/src/api/events/assistant-activity-state.ts +75 -0
- package/src/api/events/assistant-outbound-attachment.ts +49 -0
- package/src/api/events/assistant-text-delta.ts +30 -0
- package/src/api/events/assistant-turn-start.ts +31 -0
- package/src/api/events/avatar-updated.ts +24 -0
- package/src/api/events/compaction-circuit-closed.ts +26 -0
- package/src/api/events/compaction-circuit-open.ts +28 -0
- package/src/api/events/confirmation-request.ts +114 -0
- package/src/api/events/contact-request.ts +33 -0
- package/src/api/events/conversation-error.ts +77 -0
- package/src/api/events/conversation-list-invalidated.ts +38 -0
- package/src/api/events/conversation-title-updated.ts +24 -0
- package/src/api/events/disk-pressure-status-changed.ts +61 -0
- package/src/api/events/document-comment-created.ts +44 -0
- package/src/api/events/document-comment-deleted.ts +22 -0
- package/src/api/events/document-comment-reopened.ts +23 -0
- package/src/api/events/document-comment-resolved.ts +25 -0
- package/src/api/events/document-editor-update.ts +27 -0
- package/src/api/events/error.ts +32 -0
- package/src/api/events/generation-cancelled.ts +22 -0
- package/src/api/events/generation-handoff.ts +39 -0
- package/src/api/events/home-feed-updated.ts +26 -0
- package/src/api/events/identity-changed.ts +32 -0
- package/src/api/events/interaction-resolved.ts +50 -0
- package/src/api/events/message-complete.ts +40 -0
- package/src/api/events/message-dequeued.ts +21 -0
- package/src/api/events/message-queued-deleted.ts +23 -0
- package/src/api/events/message-queued.ts +22 -0
- package/src/api/events/message-request-complete.ts +29 -0
- package/src/api/events/navigate-settings.ts +20 -0
- package/src/api/events/notification-intent.ts +33 -0
- package/src/api/events/open-url.ts +28 -0
- package/src/api/events/question-request.ts +67 -0
- package/src/{events → api/events}/relationship-state-updated.ts +6 -8
- package/src/api/events/secret-request.ts +42 -0
- package/src/api/events/subagent-event.ts +79 -0
- package/src/api/events/subagent-spawned.ts +40 -0
- package/src/api/events/subagent-status-changed.ts +65 -0
- package/src/api/events/sync-changed.ts +29 -0
- package/src/api/events/tool-result.ts +129 -0
- package/src/api/events/tool-use-start.ts +30 -0
- package/src/api/events/turn-profile-auto-routed.ts +28 -0
- package/src/api/events/ui-surface-complete.ts +30 -0
- package/src/api/events/ui-surface-dismiss.ts +22 -0
- package/src/api/events/ui-surface-show.ts +67 -0
- package/src/api/events/ui-surface-update.ts +26 -0
- package/src/api/events/usage-update.ts +34 -0
- package/src/api/events/user-message-echo.ts +35 -0
- package/src/api/index.ts +482 -3
- package/src/api/requests/dictation.ts +45 -0
- package/src/api/responses/disk-pressure-status.ts +26 -0
- package/src/api/responses/home.ts +217 -0
- package/src/api/responses/llm-context-response.ts +41 -0
- package/src/api/responses/llm-request-log-entry.ts +93 -0
- package/src/api/responses/memory-recall-log.ts +65 -0
- package/src/api/responses/memory-v2-activation-log.ts +78 -0
- package/src/api/responses/memory-v3-selection-log.ts +50 -0
- package/src/api/responses/subagent-detail.ts +48 -0
- package/src/approvals/guardian-decision-primitive.ts +7 -15
- package/src/approvals/guardian-request-resolvers.ts +6 -9
- package/src/avatar/__tests__/avatar-manifest.test.ts +236 -0
- package/src/avatar/__tests__/avatar-store.test.ts +193 -0
- package/src/avatar/avatar-manifest.ts +195 -0
- package/src/avatar/avatar-store.ts +113 -0
- package/src/avatar/traits-png-sync.ts +8 -2
- package/src/background-wake/background-wake-routes.test.ts +687 -52
- package/src/background-wake/next-wake.test.ts +31 -1
- package/src/background-wake/next-wake.ts +4 -1
- package/src/background-wake/platform-client.test.ts +308 -0
- package/src/background-wake/platform-client.ts +167 -0
- package/src/background-wake/publisher.ts +91 -0
- package/src/background-wake/runtime-registry.ts +2 -2
- package/src/background-wake/wake-intent-hooks.test.ts +282 -0
- package/src/calls/call-conversation-messages.ts +6 -4
- package/src/calls/guardian-action-sweep.ts +6 -4
- package/src/calls/guardian-dispatch.ts +1 -0
- package/src/calls/relay-server.ts +12 -8
- package/src/calls/voice-session-bridge.ts +17 -31
- package/src/cli/commands/__tests__/conversations-slack.test.ts +16 -0
- package/src/cli/commands/__tests__/memory-v3.test.ts +245 -0
- package/src/cli/commands/__tests__/notifications.test.ts +184 -40
- package/src/cli/commands/avatar.ts +17 -11
- package/src/cli/commands/channels/__tests__/channels.test.ts +143 -0
- package/src/cli/commands/channels/index.ts +229 -0
- package/src/cli/commands/conversations.ts +15 -1
- package/src/cli/commands/db/__tests__/repair.test.ts +540 -0
- package/src/cli/commands/db/__tests__/status.test.ts +253 -0
- package/src/cli/commands/db/format.ts +48 -0
- package/src/cli/commands/db/index.ts +29 -0
- package/src/cli/commands/db/repair-step-conversation-backfill.ts +345 -0
- package/src/cli/commands/db/repair-step-integrity.ts +146 -0
- package/src/cli/commands/db/repair-steps.ts +164 -0
- package/src/cli/commands/db/repair.ts +141 -0
- package/src/cli/commands/db/status.ts +366 -0
- package/src/cli/commands/memory-v3.ts +168 -203
- package/src/cli/commands/notifications.ts +365 -55
- package/src/cli/lib/cli-colors.ts +24 -6
- package/src/cli/lib/open-browser.ts +7 -2
- package/src/cli/program.ts +6 -5
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/assistant-feature-flags.ts +25 -44
- package/src/config/bundled-skills/app-builder/SKILL.md +14 -3
- package/src/config/bundled-skills/document-editor/SKILL.md +5 -1
- package/src/config/bundled-skills/media-processing/services/reduce.ts +6 -9
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +7 -2
- package/src/config/bundled-skills/schedule/SKILL.md +2 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +10 -2
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -0
- package/src/config/call-site-defaults.ts +3 -8
- package/src/config/feature-flag-cache.ts +86 -0
- package/src/config/feature-flag-registry.json +42 -26
- package/src/config/llm-context-resolution.ts +10 -1
- package/src/config/llm-resolver.ts +121 -15
- package/src/config/loader.ts +4 -5
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -211
- package/src/config/schemas/call-site-catalog.ts +8 -15
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +92 -4
- package/src/config/schemas/memory-lifecycle.ts +24 -0
- package/src/config/schemas/memory-v2.ts +0 -227
- package/src/config/schemas/memory-v3.ts +39 -0
- package/src/config/schemas/memory.ts +6 -1
- package/src/config/schemas/services.ts +6 -2
- package/src/config/schemas/timeouts.ts +3 -1
- package/src/config/seed-inference-profiles.ts +36 -16
- package/src/context/compactor.ts +54 -31
- package/src/context/token-estimator.ts +29 -5
- package/src/context/tool-result-truncation.ts +1 -43
- package/src/context/window-manager.ts +138 -20
- package/src/credential-execution/executable-discovery.ts +40 -0
- package/src/credential-execution/process-manager.ts +6 -2
- package/src/credential-health/credential-health-service.ts +125 -40
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -6
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +15 -17
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -2
- package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -0
- package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +1 -0
- package/src/daemon/__tests__/switch-inference-profile-tool.test.ts +107 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +11 -6
- package/src/daemon/approval-generators.ts +4 -4
- package/src/daemon/config-watcher.ts +7 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +613 -155
- package/src/daemon/conversation-agent-loop.ts +409 -605
- package/src/daemon/conversation-error.ts +40 -12
- package/src/daemon/conversation-history.ts +22 -6
- package/src/daemon/conversation-launch.ts +4 -8
- package/src/daemon/conversation-lifecycle.ts +10 -38
- package/src/daemon/conversation-messaging.ts +83 -44
- package/src/daemon/conversation-notifiers.ts +7 -5
- package/src/daemon/conversation-process.ts +174 -116
- package/src/daemon/conversation-runtime-assembly.ts +76 -30
- package/src/daemon/conversation-skill-tools.ts +14 -30
- package/src/daemon/conversation-store.ts +6 -5
- package/src/daemon/conversation-surfaces.ts +124 -103
- package/src/daemon/conversation-tool-setup.ts +36 -48
- package/src/daemon/conversation.ts +111 -166
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/daemon-skill-host.ts +7 -4
- package/src/daemon/disk-pressure-guard.ts +54 -50
- package/src/daemon/external-plugins-bootstrap.ts +46 -24
- package/src/daemon/first-greeting.ts +53 -13
- package/src/daemon/guardian-action-generators.ts +2 -2
- package/src/daemon/handlers/conversations.ts +6 -22
- package/src/daemon/handlers/shared.ts +10 -1
- package/src/daemon/handlers/skills.ts +15 -14
- package/src/daemon/host-app-control-proxy.ts +54 -1
- package/src/daemon/host-cu-proxy.ts +46 -22
- package/src/daemon/host-file-proxy.ts +25 -1
- package/src/daemon/host-proxy-preactivation.ts +25 -6
- package/src/daemon/lifecycle.ts +40 -67
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/meet-manifest-loader.ts +10 -17
- package/src/daemon/message-protocol.ts +2 -3
- package/src/daemon/message-provenance.ts +49 -0
- package/src/daemon/message-types/contacts.ts +3 -20
- package/src/daemon/message-types/conversations.ts +25 -125
- package/src/daemon/message-types/document-comments.ts +8 -44
- package/src/daemon/message-types/documents.ts +3 -9
- package/src/daemon/message-types/home.ts +5 -18
- package/src/daemon/message-types/integrations.ts +4 -13
- package/src/daemon/message-types/messages.ts +47 -377
- package/src/daemon/message-types/notifications.ts +2 -32
- package/src/daemon/message-types/settings.ts +3 -8
- package/src/daemon/message-types/skills.ts +2 -0
- package/src/daemon/message-types/subagents.ts +6 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/sync.ts +12 -25
- package/src/daemon/message-types/workspace.ts +3 -11
- package/src/daemon/process-message.ts +58 -55
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/server.ts +28 -0
- package/src/daemon/switch-inference-profile-tool.ts +13 -3
- package/src/daemon/tool-setup-types.ts +0 -6
- package/src/daemon/tool-side-effects.ts +10 -7
- package/src/daemon/trust-context.ts +13 -0
- package/src/daemon/wake-target-adapter.ts +21 -1
- package/src/documents/document-store.ts +38 -0
- package/src/export/__tests__/transcript-formatter.test.ts +1 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +31 -0
- package/src/heartbeat/heartbeat-run-store.ts +31 -0
- package/src/heartbeat/heartbeat-service.ts +79 -0
- package/src/home/__tests__/feed-writer.test.ts +161 -0
- package/src/home/__tests__/post-connect-feed.test.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +55 -59
- package/src/home/feature-gate.ts +22 -0
- package/src/home/feed-types.ts +36 -221
- package/src/home/feed-writer.ts +146 -7
- package/src/home/suggested-prompts.ts +27 -145
- package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
- package/src/ipc/__tests__/email-ipc.test.ts +0 -9
- package/src/ipc/gateway-client.test.ts +4 -1
- package/src/ipc/routes/__tests__/route-adapter.test.ts +244 -0
- package/src/ipc/routes/route-adapter.ts +45 -6
- package/src/ipc/skill-routes/__tests__/memory.test.ts +19 -9
- package/src/ipc/skill-routes/__tests__/providers.test.ts +10 -10
- package/src/ipc/skill-routes/__tests__/registries.test.ts +59 -20
- package/src/ipc/skill-routes/memory.ts +27 -13
- package/src/ipc/skill-routes/providers.ts +5 -6
- package/src/ipc/skill-routes/registries.ts +39 -88
- package/src/live-voice/__tests__/live-voice-archive.test.ts +24 -11
- package/src/memory/__tests__/conversation-queries.test.ts +192 -8
- package/src/memory/__tests__/db-maintenance.test.ts +128 -0
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +1 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +5 -4
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +26 -5
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +1 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +11 -6
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +1 -0
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +31 -0
- package/src/memory/__tests__/memory-v3-selections-migration.test.ts +103 -0
- package/src/memory/context-search/agent-runner.ts +2 -4
- package/src/memory/conversation-attention-store.ts +17 -3
- package/src/memory/conversation-crud.ts +386 -115
- package/src/memory/conversation-queries.ts +78 -22
- package/src/memory/db-connection.ts +29 -19
- package/src/memory/db-init.ts +12 -0
- package/src/memory/db-maintenance.ts +18 -2
- package/src/memory/db-singleton.ts +77 -0
- package/src/memory/delivery-channels.ts +82 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +2 -4
- package/src/memory/graph/consolidation.ts +8 -11
- package/src/memory/graph/conversation-graph-memory.ts +41 -8
- package/src/memory/graph/extraction.ts +6 -9
- package/src/memory/graph/narrative.ts +2 -2
- package/src/memory/graph/pattern-scan.ts +2 -2
- package/src/memory/graph/retriever.test.ts +3 -3
- package/src/memory/graph/retriever.ts +20 -26
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/job-handlers/conversation-starters.ts +32 -32
- package/src/memory/job-handlers/embedding.test.ts +3 -2
- package/src/memory/job-handlers/summarization.ts +1 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -2
- package/src/memory/jobs-store.ts +3 -1
- package/src/memory/jobs-worker.ts +63 -40
- package/src/memory/llm-request-log-source-clickhouse.ts +55 -1
- package/src/memory/llm-request-log-source-local.ts +13 -0
- package/src/memory/llm-request-log-source.ts +21 -6
- package/src/memory/llm-request-log-store.ts +147 -3
- package/src/memory/llm-usage-store.ts +10 -0
- package/src/memory/memory-marker.ts +17 -0
- package/src/memory/memory-retrospective-job.ts +6 -2
- package/src/memory/memory-v2-activation-log-store.ts +13 -1
- package/src/memory/migrations/265-drop-provider-connection-status.ts +26 -0
- package/src/memory/migrations/266-messages-client-message-id.ts +43 -0
- package/src/memory/migrations/267-llm-usage-events-add-assistant-version.ts +46 -0
- package/src/memory/migrations/268-add-memory-v3-selections.ts +28 -0
- package/src/memory/migrations/269-schedule-script-timeout.ts +11 -0
- package/src/memory/migrations/270-messages-role-created-at-index.ts +18 -0
- package/src/memory/migrations/__tests__/267-llm-usage-events-add-assistant-version.test.ts +117 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/schema/conversations.ts +9 -1
- package/src/memory/schema/inference.ts +0 -1
- package/src/memory/schema/infrastructure.ts +11 -0
- package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +124 -0
- package/src/memory/v2/__tests__/harness-metrics.test.ts +9 -0
- package/src/memory/v2/__tests__/harness-replay-input.test.ts +9 -4
- package/src/memory/v2/__tests__/harness-runner.test.ts +26 -0
- package/src/memory/v2/__tests__/migration.test.ts +11 -3
- package/src/memory/v2/__tests__/page-index.test.ts +37 -1
- package/src/memory/v2/__tests__/router.test.ts +14 -4
- package/src/memory/v2/__tests__/sweep-job.test.ts +9 -5
- package/src/memory/v2/backfill-jobs.ts +6 -0
- package/src/memory/v2/consolidation-job.ts +89 -9
- package/src/memory/v2/harness/metrics.ts +5 -1
- package/src/memory/v2/harness/replay-input.ts +19 -3
- package/src/memory/v2/harness/runner.ts +6 -0
- package/src/memory/v2/harness/trace.ts +6 -0
- package/src/memory/v2/migration.ts +5 -3
- package/src/memory/v2/page-index.ts +11 -0
- package/src/memory/v2/router.ts +8 -11
- package/src/memory/v2/sweep-job.ts +8 -11
- package/src/memory/v2/types.ts +1 -0
- package/src/memory/v3/__tests__/assign.test.ts +242 -0
- package/src/memory/v3/__tests__/capabilities.test.ts +118 -0
- package/src/memory/v3/__tests__/core.test.ts +39 -0
- package/src/memory/v3/__tests__/fixtures/eval-turns.json +36 -0
- package/src/memory/v3/__tests__/fixtures/live-turns.json +37 -0
- package/src/memory/v3/__tests__/health.test.ts +203 -0
- package/src/memory/v3/__tests__/live-integration.test.ts +330 -0
- package/src/memory/v3/__tests__/maintain-job.test.ts +288 -0
- package/src/memory/v3/__tests__/needle.test.ts +107 -0
- package/src/memory/v3/__tests__/orchestrate.test.ts +400 -0
- package/src/memory/v3/__tests__/reconcile.test.ts +274 -0
- package/src/memory/v3/__tests__/render-injection.test.ts +61 -0
- package/src/memory/v3/__tests__/router.test.ts +260 -0
- package/src/memory/v3/__tests__/selection-log-store.test.ts +179 -0
- package/src/memory/v3/__tests__/selector.test.ts +404 -0
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +414 -0
- package/src/memory/v3/__tests__/snapshot.test.ts +168 -0
- package/src/memory/v3/__tests__/tree.test.ts +192 -0
- package/src/memory/v3/__tests__/types.test.ts +54 -0
- package/src/memory/v3/__tests__/working-set-eviction.test.ts +106 -0
- package/src/memory/v3/__tests__/working-set-skeleton.test.ts +44 -0
- package/src/memory/v3/assign.ts +268 -0
- package/src/memory/v3/capabilities.ts +124 -0
- package/src/memory/v3/core.ts +26 -0
- package/src/memory/v3/data/README.md +84 -0
- package/src/memory/v3/data/assignments.json +5 -0
- package/src/memory/v3/data/core.json +1 -0
- package/src/memory/v3/data/leaves/domain-a/topic-x.md +9 -0
- package/src/memory/v3/data/leaves/domain-a/topic-y.md +9 -0
- package/src/memory/v3/data/leaves/domain-b/topic-z.md +9 -0
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/maintain-job.ts +314 -0
- package/src/memory/v3/needle.ts +115 -0
- package/src/memory/v3/orchestrate.ts +114 -0
- package/src/memory/v3/page-content.ts +34 -0
- package/src/memory/v3/provider-blocks.ts +16 -0
- package/src/memory/v3/reconcile.ts +523 -0
- package/src/memory/v3/render-injection.ts +32 -0
- package/src/memory/v3/router.ts +184 -0
- package/src/memory/v3/selection-log-store.ts +84 -0
- package/src/memory/v3/selector.ts +211 -0
- package/src/memory/v3/shadow-plugin.ts +379 -0
- package/src/memory/v3/snapshot.ts +209 -0
- package/src/memory/v3/tree.ts +174 -0
- package/src/memory/v3/types.ts +46 -60
- package/src/memory/v3/working-set.ts +88 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +1 -1
- package/src/messaging/providers/slack/render-transcript.ts +2 -2
- package/src/messaging/style-analyzer.ts +8 -11
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -0
- package/src/notifications/adapters/slack.ts +45 -11
- package/src/notifications/broadcaster.ts +114 -63
- package/src/notifications/conversation-pairing.ts +30 -8
- package/src/notifications/decision-engine.ts +10 -13
- package/src/notifications/decisions-store.ts +32 -1
- package/src/notifications/deliveries-store.ts +45 -0
- package/src/notifications/edit-notification.ts +201 -0
- package/src/notifications/emit-signal.ts +11 -1
- package/src/notifications/preference-extractor.ts +11 -14
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +37 -0
- package/src/oauth/byo-connection.test.ts +67 -3
- package/src/oauth/byo-connection.ts +32 -5
- package/src/oauth/connect-orchestrator.ts +9 -0
- package/src/oauth/connection-resolver.test.ts +76 -0
- package/src/oauth/connection-resolver.ts +49 -10
- package/src/oauth/manual-token-connection.ts +51 -3
- package/src/oauth/seed-providers.ts +3 -0
- package/src/permissions/approval-policy.test.ts +19 -5
- package/src/permissions/approval-policy.ts +14 -3
- package/src/permissions/checker.ts +21 -8
- package/src/permissions/prompter.ts +42 -36
- package/src/permissions/question-prompter.test.ts +35 -26
- package/src/permissions/question-prompter.ts +6 -10
- package/src/platform/client.test.ts +24 -1
- package/src/platform/client.ts +8 -0
- package/src/platform/feature-gate.ts +15 -0
- package/src/plugin-api/index.ts +2 -0
- package/src/plugin-api/types.ts +25 -3
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
- package/src/plugins/defaults/circuit-breaker/package.json +15 -0
- package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
- package/src/plugins/defaults/compaction/package.json +15 -0
- package/src/plugins/defaults/compaction/register.ts +35 -0
- package/src/plugins/defaults/compaction/terminal.ts +73 -0
- package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +22 -0
- package/src/plugins/defaults/empty-response/package.json +15 -0
- package/src/plugins/defaults/empty-response/register.ts +28 -0
- package/src/plugins/defaults/empty-response/terminal.ts +106 -0
- package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +35 -0
- package/src/plugins/defaults/history-repair/package.json +15 -0
- package/src/plugins/defaults/history-repair/register.ts +24 -0
- package/src/{daemon/history-repair.ts → plugins/defaults/history-repair/terminal.ts} +48 -35
- package/src/plugins/defaults/index.ts +29 -40
- package/src/plugins/defaults/injectors/package.json +15 -0
- package/src/plugins/defaults/{injectors.ts → injectors/register.ts} +16 -46
- package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +17 -0
- package/src/plugins/defaults/llm-call/package.json +15 -0
- package/src/plugins/defaults/{llm-call.ts → llm-call/register.ts} +6 -38
- package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +17 -0
- package/src/plugins/defaults/memory-retrieval/package.json +15 -0
- package/src/plugins/defaults/{memory-retrieval.ts → memory-retrieval/register.ts} +10 -48
- package/src/plugins/defaults/{overflow-reduce.ts → overflow-reduce/middlewares/overflowReduce.ts} +18 -77
- package/src/plugins/defaults/overflow-reduce/package.json +15 -0
- package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
- package/src/plugins/defaults/persistence/middlewares/persistence.ts +19 -0
- package/src/plugins/defaults/persistence/package.json +15 -0
- package/src/plugins/defaults/persistence/register.ts +38 -0
- package/src/plugins/defaults/persistence/terminal.ts +83 -0
- package/src/plugins/defaults/title-generate/package.json +15 -0
- package/src/plugins/defaults/title-generate/register.ts +35 -0
- package/src/plugins/defaults/title-generate/terminal.ts +31 -0
- package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +23 -0
- package/src/plugins/defaults/token-estimate/package.json +15 -0
- package/src/plugins/defaults/token-estimate/register.ts +34 -0
- package/src/plugins/defaults/token-estimate/terminal.ts +40 -0
- package/src/plugins/defaults/tool-error/middlewares/toolError.ts +21 -0
- package/src/plugins/defaults/tool-error/package.json +15 -0
- package/src/plugins/defaults/tool-error/register.ts +35 -0
- package/src/plugins/defaults/tool-error/terminal.ts +47 -0
- package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +23 -0
- package/src/plugins/defaults/tool-execute/package.json +15 -0
- package/src/plugins/defaults/{tool-execute.ts → tool-execute/register.ts} +8 -46
- package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +23 -0
- package/src/plugins/defaults/tool-result-truncate/package.json +15 -0
- package/src/plugins/defaults/tool-result-truncate/register.ts +35 -0
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +113 -0
- package/src/plugins/defaults/tool-result-truncate/types.ts +22 -0
- package/src/plugins/external-plugin-loader.ts +2 -2
- package/src/plugins/pipeline.ts +0 -12
- package/src/plugins/types.ts +107 -102
- package/src/plugins/user-loader.ts +4 -3
- package/src/proactive-artifact/aux-message-injector.ts +0 -1
- package/src/proactive-artifact/job.test.ts +21 -8
- package/src/proactive-artifact/job.ts +3 -1
- package/src/prompts/__tests__/system-prompt.test.ts +4 -4
- package/src/prompts/sections.ts +20 -7
- package/src/prompts/system-prompt.ts +38 -40
- package/src/prompts/template-detection.ts +10 -4
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +2 -2
- package/src/prompts/templates/BOOTSTRAP.md +10 -10
- package/src/prompts/templates/IDENTITY.md +0 -2
- package/src/prompts/templates/system-sections.ts +6 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -4
- package/src/providers/__tests__/registry-native-web-search.test.ts +122 -0
- package/src/providers/__tests__/retry-callsite.test.ts +25 -25
- package/src/providers/__tests__/satellite-connection-routing.test.ts +7 -21
- package/src/providers/anthropic/client.ts +24 -5
- package/src/providers/call-site-routing.ts +34 -18
- package/src/providers/connection-model-compat.ts +23 -0
- package/src/providers/connection-resolution.ts +39 -20
- package/src/providers/fireworks/client.ts +1 -0
- package/src/providers/gemini/client.ts +176 -37
- package/src/providers/gemini/inline-media.ts +74 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +0 -2
- package/src/providers/inference/__tests__/base-url-security.test.ts +2 -3
- package/src/providers/inference/__tests__/{connections-status-label.test.ts → connections-label.test.ts} +12 -111
- package/src/providers/inference/auth.ts +0 -8
- package/src/providers/inference/connections.ts +3 -66
- package/src/providers/inference/resolve-auth.ts +2 -3
- package/src/providers/model-catalog.ts +35 -1
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +157 -7
- package/src/providers/openai/chat-completions-provider.ts +111 -16
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/openai/responses-provider.ts +54 -57
- package/src/providers/openrouter/client.ts +14 -14
- package/src/providers/provider-send-message.ts +23 -14
- package/src/providers/ratelimit.ts +1 -9
- package/src/providers/registry.ts +48 -8
- package/src/providers/retry.ts +16 -9
- package/src/providers/search-provider-catalog.ts +17 -9
- package/src/providers/types.ts +20 -2
- package/src/providers/usage-tracking.ts +1 -9
- package/src/runtime/__tests__/agent-wake.test.ts +132 -26
- package/src/runtime/__tests__/background-job-runner.test.ts +2 -3
- package/src/runtime/access-request-helper.ts +1 -0
- package/src/runtime/agent-wake.ts +93 -18
- package/src/runtime/assistant-event-hub.ts +2 -2
- package/src/runtime/auth/__tests__/guard-tests.test.ts +75 -109
- package/src/runtime/auth/__tests__/route-policy.test.ts +153 -170
- package/src/runtime/auth/route-policy.ts +42 -1069
- package/src/runtime/background-job-runner.ts +1 -4
- package/src/runtime/btw-sidechain.ts +3 -1
- package/src/runtime/channel-approvals.ts +3 -14
- package/src/runtime/channel-invite-transport.ts +5 -6
- package/src/runtime/channel-readiness-service.ts +70 -5
- package/src/runtime/channel-reply-delivery.ts +23 -0
- package/src/runtime/channel-retry-sweep.ts +59 -30
- package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
- package/src/runtime/conversation-stream-state.ts +294 -0
- package/src/runtime/http-router.ts +19 -22
- package/src/runtime/http-types.ts +12 -6
- package/src/runtime/invite-instruction-generator.ts +3 -3
- package/src/runtime/migrations/vbundle-builder.ts +3 -2
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +565 -0
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +4 -4
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +436 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +237 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +209 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +26 -72
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +58 -5
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +6 -6
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +3 -2
- package/src/runtime/routes/__tests__/stt-routes.test.ts +3 -3
- package/src/runtime/routes/__tests__/suggest-trust-rule-routes.test.ts +5 -2
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +294 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
- package/src/runtime/routes/__tests__/tts-routes.test.ts +3 -3
- package/src/runtime/routes/acp-routes-list.test.ts +3 -0
- package/src/runtime/routes/acp-routes.test.ts +97 -75
- package/src/runtime/routes/acp-routes.ts +29 -6
- package/src/runtime/routes/app-management-routes.ts +208 -28
- package/src/runtime/routes/app-routes.ts +25 -5
- package/src/runtime/routes/approval-routes.ts +16 -4
- package/src/runtime/routes/attachment-routes.ts +25 -1
- package/src/runtime/routes/audio-routes.ts +1 -0
- package/src/runtime/routes/audit-routes.ts +5 -0
- package/src/runtime/routes/auth-routes.ts +5 -0
- package/src/runtime/routes/avatar-routes.ts +238 -59
- package/src/runtime/routes/background-tool-routes.ts +9 -0
- package/src/runtime/routes/background-wake-routes.ts +201 -23
- package/src/runtime/routes/backup-routes.ts +45 -0
- package/src/runtime/routes/bookmark-routes.ts +13 -0
- package/src/runtime/routes/brain-graph-routes.ts +9 -0
- package/src/runtime/routes/browser-routes.ts +5 -0
- package/src/runtime/routes/browser-tabs-routes.ts +5 -0
- package/src/runtime/routes/btw-routes.ts +9 -5
- package/src/runtime/routes/cache-routes.ts +13 -0
- package/src/runtime/routes/call-routes.ts +21 -10
- package/src/runtime/routes/channel-availability-routes.ts +5 -1
- package/src/runtime/routes/channel-readiness-routes.ts +37 -4
- package/src/runtime/routes/channel-route-definitions.ts +21 -0
- package/src/runtime/routes/channel-verification-routes.ts +21 -0
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +9 -2
- package/src/runtime/routes/client-routes.ts +9 -0
- package/src/runtime/routes/consolidation-routes.ts +13 -5
- package/src/runtime/routes/contact-prompt-routes.ts +9 -0
- package/src/runtime/routes/contact-routes.ts +90 -23
- package/src/runtime/routes/content-source-routes.ts +5 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +11 -1
- package/src/runtime/routes/conversation-attention-routes.ts +5 -0
- package/src/runtime/routes/conversation-cli-routes.ts +54 -7
- package/src/runtime/routes/conversation-compaction-routes.ts +292 -0
- package/src/runtime/routes/conversation-list-routes.ts +225 -9
- package/src/runtime/routes/conversation-management-routes.ts +96 -28
- package/src/runtime/routes/conversation-query-routes.ts +148 -51
- package/src/runtime/routes/conversation-routes.ts +259 -158
- package/src/runtime/routes/conversation-starter-routes.ts +22 -13
- package/src/runtime/routes/conversations-import-routes.ts +25 -7
- package/src/runtime/routes/credential-prompt-routes.ts +5 -0
- package/src/runtime/routes/credential-routes.ts +25 -6
- package/src/runtime/routes/debug-bash-routes.ts +5 -0
- package/src/runtime/routes/debug-routes.ts +11 -2
- package/src/runtime/routes/defer-routes.ts +13 -0
- package/src/runtime/routes/diagnostics-routes.ts +37 -46
- package/src/runtime/routes/disk-pressure-routes.ts +17 -31
- package/src/runtime/routes/document-comments-routes.ts +46 -27
- package/src/runtime/routes/documents-routes.ts +31 -11
- package/src/runtime/routes/domain-routes.ts +61 -28
- package/src/runtime/routes/email-routes.ts +33 -0
- package/src/runtime/routes/events-routes.ts +114 -9
- package/src/runtime/routes/filing-routes.ts +9 -4
- package/src/runtime/routes/gateway-log-routes.ts +5 -0
- package/src/runtime/routes/global-search-routes.ts +53 -50
- package/src/runtime/routes/group-routes.ts +32 -5
- package/src/runtime/routes/guardian-action-routes.ts +9 -0
- package/src/runtime/routes/guardian-approval-interception.ts +0 -31
- package/src/runtime/routes/heartbeat-routes.ts +25 -9
- package/src/runtime/routes/home-feed-routes.ts +149 -16
- package/src/runtime/routes/home-state-routes.ts +8 -40
- package/src/runtime/routes/host-app-control-routes.ts +5 -0
- package/src/runtime/routes/host-bash-routes.ts +5 -0
- package/src/runtime/routes/host-browser-routes.ts +13 -0
- package/src/runtime/routes/host-cu-routes.ts +5 -0
- package/src/runtime/routes/host-file-routes.ts +26 -6
- package/src/runtime/routes/host-transfer-routes.ts +13 -2
- package/src/runtime/routes/http-adapter.ts +1 -2
- package/src/runtime/routes/identity-intro-cache.ts +72 -16
- package/src/runtime/routes/identity-routes.ts +42 -11
- package/src/runtime/routes/image-generation-routes.ts +5 -0
- package/src/runtime/routes/inbound-message-handler.ts +15 -11
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +524 -12
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +72 -27
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profile-session-routes.ts +13 -3
- package/src/runtime/routes/inference-provider-connection-routes.ts +26 -31
- package/src/runtime/routes/inference-send-routes.ts +11 -11
- package/src/runtime/routes/integrations/a2a.ts +30 -7
- package/src/runtime/routes/integrations/slack/channel.ts +19 -3
- package/src/runtime/routes/integrations/slack/share.ts +9 -2
- package/src/runtime/routes/integrations/telegram.ts +28 -9
- package/src/runtime/routes/integrations/twilio.ts +35 -7
- package/src/runtime/routes/integrations/vercel.ts +18 -3
- package/src/runtime/routes/internal-oauth-routes.ts +5 -0
- package/src/runtime/routes/internal-twilio-routes.ts +13 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +39 -4
- package/src/runtime/routes/llm-context-normalization.ts +7 -2
- package/src/runtime/routes/log-export-routes.ts +28 -10
- package/src/runtime/routes/mcp-auth-routes.ts +25 -0
- package/src/runtime/routes/memory-item-routes.ts +21 -10
- package/src/runtime/routes/memory-v2-routes.ts +90 -36
- package/src/runtime/routes/memory-v3-routes.ts +283 -259
- package/src/runtime/routes/migration-rollback-routes.ts +5 -1
- package/src/runtime/routes/migration-routes.ts +49 -13
- package/src/runtime/routes/notification-routes.ts +80 -2
- package/src/runtime/routes/oauth-apps.ts +33 -11
- package/src/runtime/routes/oauth-commands-routes.ts +43 -15
- package/src/runtime/routes/oauth-connect-routes.ts +9 -0
- package/src/runtime/routes/oauth-lifecycle-routes.ts +5 -1
- package/src/runtime/routes/oauth-providers.ts +35 -10
- package/src/runtime/routes/platform-routes.ts +21 -0
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +3 -2
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +37 -16
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +7 -3
- package/src/runtime/routes/playground/__tests__/state.test.ts +10 -3
- package/src/runtime/routes/playground/force-compact.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +0 -1
- package/src/runtime/routes/playground/inject-failures.ts +13 -8
- package/src/runtime/routes/playground/reset-circuit.ts +14 -9
- package/src/runtime/routes/playground/seed-conversation.ts +1 -1
- package/src/runtime/routes/playground/seeded-conversations.ts +3 -3
- package/src/runtime/routes/playground/state.ts +4 -3
- package/src/runtime/routes/plugins-routes.ts +22 -19
- package/src/runtime/routes/profiler-routes.ts +17 -4
- package/src/runtime/routes/ps-routes.ts +5 -0
- package/src/runtime/routes/publish-routes.ts +13 -3
- package/src/runtime/routes/question-routes.ts +5 -0
- package/src/runtime/routes/recording-routes.ts +25 -12
- package/src/runtime/routes/rename-conversation-routes.ts +5 -0
- package/src/runtime/routes/sanity-routes.ts +9 -2
- package/src/runtime/routes/schedule-routes.ts +137 -47
- package/src/runtime/routes/secret-routes.ts +17 -4
- package/src/runtime/routes/sequence-routes.ts +33 -0
- package/src/runtime/routes/settings-routes.ts +65 -19
- package/src/runtime/routes/skills-routes.ts +133 -69
- package/src/runtime/routes/slack-channel-routes.ts +5 -0
- package/src/runtime/routes/stt-routes.ts +13 -6
- package/src/runtime/routes/subagents-routes.ts +24 -18
- package/src/runtime/routes/suggest-trust-rule-routes.ts +7 -2
- package/src/runtime/routes/surface-action-routes.ts +10 -38
- package/src/runtime/routes/surface-content-routes.ts +21 -6
- package/src/runtime/routes/surface-conversation-resolver.ts +65 -0
- package/src/runtime/routes/task-routes.ts +37 -0
- package/src/runtime/routes/telemetry-routes.ts +9 -0
- package/src/runtime/routes/trace-event-routes.ts +42 -1
- package/src/runtime/routes/trust-rules-routes.ts +5 -0
- package/src/runtime/routes/tts-routes.ts +13 -6
- package/src/runtime/routes/types.ts +17 -8
- package/src/runtime/routes/ui-request-routes.ts +5 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +5 -0
- package/src/runtime/routes/usage-routes.ts +71 -3
- package/src/runtime/routes/user-routes-cli.ts +9 -0
- package/src/runtime/routes/user-routes.ts +5 -1
- package/src/runtime/routes/wake-conversation-routes.ts +5 -0
- package/src/runtime/routes/watcher-routes.ts +21 -0
- package/src/runtime/routes/webhook-routes.ts +9 -0
- package/src/runtime/routes/wipe-conversation-routes.ts +8 -0
- package/src/runtime/routes/work-items-routes.ts +47 -19
- package/src/runtime/routes/workspace-commit-routes.ts +5 -0
- package/src/runtime/routes/workspace-routes.test.ts +42 -0
- package/src/runtime/routes/workspace-routes.ts +120 -9
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +4 -4
- package/src/runtime/services/analyze-conversation.ts +3 -6
- package/src/runtime/services/conversation-serializer.ts +24 -2
- package/src/runtime/slack-dm-text-delivery.ts +177 -0
- package/src/runtime/sync/resource-sync-events.ts +17 -3
- package/src/runtime/sync/sync-publisher.ts +2 -2
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/schedule/run-script.ts +28 -3
- package/src/schedule/schedule-store.ts +16 -1
- package/src/schedule/scheduler.ts +114 -16
- package/src/security/__tests__/provider-key-env-fallback.test.ts +3 -3
- package/src/security/encrypted-store.ts +7 -16
- package/src/security/store-path-override.ts +61 -0
- package/src/signals/user-message.ts +10 -16
- package/src/skills/catalog-files.ts +4 -1
- package/src/skills/clawhub-files.ts +2 -0
- package/src/skills/skillssh-files.ts +2 -0
- package/src/skills/validate-input.ts +177 -0
- package/src/subagent/manager.ts +16 -19
- package/src/subagent/types.ts +6 -0
- package/src/tasks/tool-sanitizer.ts +2 -2
- package/src/telemetry/types.ts +26 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +138 -1
- package/src/telemetry/usage-telemetry-reporter.ts +31 -0
- package/src/tools/acp/spawn.test.ts +88 -38
- package/src/tools/apps/definitions.ts +42 -24
- package/src/tools/ask-question/ask-question-tool.test.ts +120 -105
- package/src/tools/ask-question/ask-question-tool.ts +85 -90
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +2 -8
- package/src/tools/computer-use/definitions.ts +295 -289
- package/src/tools/credential-execution/make-authenticated-request.ts +56 -51
- package/src/tools/credential-execution/manage-secure-command-tool.ts +2 -2
- package/src/tools/credential-execution/run-authenticated-command.ts +82 -77
- package/src/tools/credentials/vault.ts +112 -111
- package/src/tools/document/document-tool.ts +131 -8
- package/src/tools/execution-target.ts +3 -6
- package/src/tools/execution-timeout.ts +3 -4
- package/src/tools/executor.ts +18 -55
- package/src/tools/filesystem/edit.ts +45 -42
- package/src/tools/filesystem/list.ts +33 -30
- package/src/tools/filesystem/read.ts +54 -35
- package/src/tools/filesystem/write.ts +34 -31
- package/src/tools/host-filesystem/edit.test.ts +1 -0
- package/src/tools/host-filesystem/edit.ts +44 -42
- package/src/tools/host-filesystem/read.test.ts +1 -0
- package/src/tools/host-filesystem/read.ts +49 -35
- package/src/tools/host-filesystem/transfer.test.ts +31 -6
- package/src/tools/host-filesystem/transfer.ts +121 -108
- package/src/tools/host-filesystem/write.test.ts +1 -0
- package/src/tools/host-filesystem/write.ts +33 -31
- package/src/tools/host-terminal/host-shell.ts +50 -48
- package/src/tools/mcp/mcp-tool-factory.ts +0 -2
- package/src/tools/memory/register.ts +23 -24
- package/src/tools/network/__tests__/managed-search-proxy.test.ts +282 -0
- package/src/tools/network/__tests__/web-search.test.ts +211 -3
- package/src/tools/network/managed-search-proxy.ts +183 -0
- package/src/tools/network/web-fetch.ts +49 -46
- package/src/tools/network/web-search.ts +215 -57
- package/src/tools/policy-context.ts +3 -1
- package/src/tools/registry.ts +184 -118
- package/src/tools/schedule/create.ts +12 -1
- package/src/tools/schedule/update.ts +16 -0
- package/src/tools/shared/filesystem/audio-read.ts +122 -0
- package/src/tools/shared/filesystem/image-read.ts +1 -1
- package/src/tools/skills/execute.ts +34 -31
- package/src/tools/skills/load.ts +29 -23
- package/src/tools/skills/skill-tool-factory.ts +17 -36
- package/src/tools/subagent/notify-parent.ts +35 -32
- package/src/tools/subagent/spawn.ts +3 -0
- package/src/tools/system/avatar-generator.ts +13 -22
- package/src/tools/system/request-permission.ts +30 -27
- package/src/tools/terminal/shell.ts +190 -61
- package/src/tools/tool-approval-handler.ts +10 -4
- package/src/tools/tool-defaults.ts +20 -9
- package/src/tools/tool-manifest.ts +4 -4
- package/src/tools/tool-name-aliases.ts +72 -14
- package/src/tools/types.ts +86 -33
- package/src/tools/ui-surface/definitions.ts +166 -94
- package/src/types/onboarding-context.ts +6 -0
- package/src/usage/attribution.ts +32 -1
- package/src/usage/types.ts +10 -0
- package/src/util/browser.ts +7 -2
- package/src/util/errors.ts +2 -2
- package/src/util/map-limit.ts +27 -0
- package/src/util/platform.ts +15 -12
- package/src/work-items/work-item-runner.ts +7 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +7 -20
- package/src/workspace/migrations/090-memory-router-cost-optimized-profile.ts +109 -0
- package/src/workspace/migrations/091-retighten-migration-onboarding-thread.ts +41 -0
- package/src/workspace/migrations/092-backfill-v3-leaves.ts +169 -0
- package/src/workspace/migrations/093-backfill-leaf-ids.ts +144 -0
- package/src/workspace/migrations/094-seed-avatar-manifest.ts +155 -0
- package/src/workspace/migrations/__tests__/094-seed-avatar-manifest.test.ts +136 -0
- package/src/workspace/migrations/__tests__/backfill-leaf-ids.test.ts +175 -0
- package/src/workspace/migrations/__tests__/backfill-v3-leaves.test.ts +124 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +15 -17
- package/tsconfig.json +4 -1
- package/src/__tests__/history-repair-pipeline.test.ts +0 -396
- package/src/cli/commands/__tests__/memory-v3-render.test.ts +0 -340
- package/src/cli/commands/memory-v3-render.ts +0 -344
- package/src/daemon/message-types/disk-pressure.ts +0 -9
- package/src/email/feature-gate.ts +0 -23
- package/src/memory/v3/__tests__/coactivation-store.test.ts +0 -422
- package/src/memory/v3/__tests__/consolidation-job.test.ts +0 -468
- package/src/memory/v3/__tests__/edge-learning-job.test.ts +0 -324
- package/src/memory/v3/__tests__/edges.test.ts +0 -563
- package/src/memory/v3/__tests__/filter.test.ts +0 -512
- package/src/memory/v3/__tests__/gate.test.ts +0 -574
- package/src/memory/v3/__tests__/index-composition.test.ts +0 -233
- package/src/memory/v3/__tests__/loop.test.ts +0 -530
- package/src/memory/v3/__tests__/retriever.test.ts +0 -226
- package/src/memory/v3/__tests__/scouts.test.ts +0 -440
- package/src/memory/v3/__tests__/shadow-middleware.test.ts +0 -312
- package/src/memory/v3/__tests__/system-prompts.test.ts +0 -154
- package/src/memory/v3/__tests__/traversal.test.ts +0 -469
- package/src/memory/v3/__tests__/tree-index.test.ts +0 -280
- package/src/memory/v3/__tests__/tree-store.test.ts +0 -529
- package/src/memory/v3/__tests__/tree-walk.test.ts +0 -707
- package/src/memory/v3/__tests__/validate.test.ts +0 -245
- package/src/memory/v3/auto-edges.ts +0 -223
- package/src/memory/v3/coactivation-store.ts +0 -124
- package/src/memory/v3/consolidation-job.ts +0 -323
- package/src/memory/v3/edge-learning-job.ts +0 -160
- package/src/memory/v3/edges.ts +0 -249
- package/src/memory/v3/filter.ts +0 -281
- package/src/memory/v3/gate.ts +0 -334
- package/src/memory/v3/index-composition.ts +0 -113
- package/src/memory/v3/llm-capture.ts +0 -46
- package/src/memory/v3/loop.ts +0 -382
- package/src/memory/v3/maintenance.ts +0 -144
- package/src/memory/v3/prompt-context.ts +0 -33
- package/src/memory/v3/prompts/consolidation.ts +0 -458
- package/src/memory/v3/prompts/system-prompts.ts +0 -196
- package/src/memory/v3/retriever.ts +0 -33
- package/src/memory/v3/scouts.ts +0 -420
- package/src/memory/v3/shadow-middleware.ts +0 -305
- package/src/memory/v3/traversal.ts +0 -206
- package/src/memory/v3/tree-index.ts +0 -237
- package/src/memory/v3/tree-store.ts +0 -394
- package/src/memory/v3/tree-walk.ts +0 -351
- package/src/memory/v3/validate.ts +0 -300
- package/src/plugins/defaults/circuit-breaker.ts +0 -141
- package/src/plugins/defaults/compaction.ts +0 -141
- package/src/plugins/defaults/empty-response.ts +0 -124
- package/src/plugins/defaults/history-repair.ts +0 -83
- package/src/plugins/defaults/persistence.ts +0 -127
- package/src/plugins/defaults/title-generate.ts +0 -90
- package/src/plugins/defaults/token-estimate.ts +0 -101
- package/src/plugins/defaults/tool-error.ts +0 -119
- package/src/plugins/defaults/tool-result-truncate.ts +0 -84
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
3
3
|
|
|
4
|
+
import { CompactionCircuit } from "../agent/compaction-circuit.js";
|
|
4
5
|
import type {
|
|
5
6
|
AgentEvent,
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
AgentLoopRunOptions,
|
|
8
|
+
AgentLoopRunResult,
|
|
9
|
+
MidLoopCompaction,
|
|
8
10
|
} from "../agent/loop.js";
|
|
11
|
+
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
9
12
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
13
|
+
import { defaultCompactionTerminal } from "../plugins/defaults/compaction/terminal.js";
|
|
10
14
|
import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
|
|
15
|
+
import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
|
|
16
|
+
import { getMiddlewaresFor } from "../plugins/registry.js";
|
|
17
|
+
import type {
|
|
18
|
+
CompactionArgs,
|
|
19
|
+
CompactionResult,
|
|
20
|
+
TurnContext,
|
|
21
|
+
} from "../plugins/types.js";
|
|
22
|
+
import { PluginTimeoutError } from "../plugins/types.js";
|
|
11
23
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
12
24
|
|
|
13
25
|
const conversationCrudRealSnapshot = {
|
|
@@ -176,6 +188,12 @@ let mockConversationRow: Record<string, unknown> = {
|
|
|
176
188
|
title: null,
|
|
177
189
|
};
|
|
178
190
|
let mockMessageById: Record<string, unknown> | null = null;
|
|
191
|
+
const deleteMessageByIdMock = mock(() => ({
|
|
192
|
+
segmentIds: [],
|
|
193
|
+
deletedSummaryIds: [],
|
|
194
|
+
}));
|
|
195
|
+
const reserveMessageMock = mock(async () => ({ id: "msg-reserve" }));
|
|
196
|
+
const updateMessageContentMock = mock(() => {});
|
|
179
197
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
180
198
|
setConversationOriginChannelIfUnset: () => {},
|
|
181
199
|
updateConversationUsage: () => {},
|
|
@@ -189,7 +207,7 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
189
207
|
}),
|
|
190
208
|
getConversationOriginInterface: () => null,
|
|
191
209
|
addMessage: () => ({ id: "mock-msg-id" }),
|
|
192
|
-
deleteMessageById:
|
|
210
|
+
deleteMessageById: deleteMessageByIdMock,
|
|
193
211
|
updateConversationContextWindow: () => {},
|
|
194
212
|
updateConversationSlackContextWatermark:
|
|
195
213
|
updateConversationSlackContextWatermarkMock,
|
|
@@ -197,6 +215,36 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
197
215
|
getConversationOriginChannel: () => null,
|
|
198
216
|
getMessageById: () => mockMessageById,
|
|
199
217
|
getLastUserTimestampBefore: () => 0,
|
|
218
|
+
reserveMessage: reserveMessageMock,
|
|
219
|
+
updateMessageContent: updateMessageContentMock,
|
|
220
|
+
// The real schema is a Zod object; tests don't exercise validation,
|
|
221
|
+
// so a passthrough is sufficient — the production code at
|
|
222
|
+
// `handleMessageComplete` only branches on `success` and reads two
|
|
223
|
+
// fields off `data`. `safeParse` of an empty object satisfies the
|
|
224
|
+
// schema (every field is optional).
|
|
225
|
+
messageMetadataSchema: {
|
|
226
|
+
safeParse: (input: unknown) => ({ success: true, data: input ?? {} }),
|
|
227
|
+
},
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
// The B3 indexing-restoration path imports `indexMessageNow` from
|
|
231
|
+
// `../memory/indexer.js` and `projectAssistantMessage` from
|
|
232
|
+
// `../memory/conversation-attention-store.js`; without these stubs the
|
|
233
|
+
// real modules would try to open a SQLite DB and read a real config.
|
|
234
|
+
const indexMessageNowMock = mock(async () => ({
|
|
235
|
+
indexedSegments: 0,
|
|
236
|
+
enqueuedJobs: 0,
|
|
237
|
+
}));
|
|
238
|
+
const projectAssistantMessageMock = mock(() => false);
|
|
239
|
+
const publishSyncInvalidationMock = mock(async () => {});
|
|
240
|
+
mock.module("../memory/indexer.js", () => ({
|
|
241
|
+
indexMessageNow: indexMessageNowMock,
|
|
242
|
+
}));
|
|
243
|
+
mock.module("../memory/conversation-attention-store.js", () => ({
|
|
244
|
+
projectAssistantMessage: projectAssistantMessageMock,
|
|
245
|
+
}));
|
|
246
|
+
mock.module("../runtime/sync/sync-publisher.js", () => ({
|
|
247
|
+
publishSyncInvalidation: publishSyncInvalidationMock,
|
|
200
248
|
}));
|
|
201
249
|
|
|
202
250
|
afterAll(() => {
|
|
@@ -371,7 +419,7 @@ mock.module("../daemon/date-context.js", () => ({
|
|
|
371
419
|
resolveTurnTimezoneContext: resolveTurnTimezoneContextMock,
|
|
372
420
|
}));
|
|
373
421
|
|
|
374
|
-
mock.module("../
|
|
422
|
+
mock.module("../plugins/defaults/history-repair/terminal.js", () => ({
|
|
375
423
|
repairHistory: (msgs: Message[]) => ({
|
|
376
424
|
messages: msgs,
|
|
377
425
|
stats: {
|
|
@@ -512,13 +560,149 @@ import {
|
|
|
512
560
|
type AgentLoopRun = (
|
|
513
561
|
messages: Message[],
|
|
514
562
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
515
|
-
|
|
516
|
-
requestId?: string,
|
|
517
|
-
onCheckpoint?: (
|
|
518
|
-
checkpoint: CheckpointInfo,
|
|
519
|
-
) => CheckpointDecision | Promise<CheckpointDecision>,
|
|
563
|
+
options?: AgentLoopRunOptions,
|
|
520
564
|
) => Promise<Message[]>;
|
|
521
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Faithful re-implementation of `AgentLoop.compact()` for the mock loop: run
|
|
568
|
+
* the compaction pipeline against the supplied turn context (which carries the
|
|
569
|
+
* test's `contextWindowManager`), invoke the orchestrator-supplied hooks, and
|
|
570
|
+
* return the continuation history — or `null` on timeout/exhaustion so the
|
|
571
|
+
* caller yields "budget".
|
|
572
|
+
*/
|
|
573
|
+
async function simulateInlineCompaction(
|
|
574
|
+
compaction: MidLoopCompaction,
|
|
575
|
+
history: Message[],
|
|
576
|
+
turnContext: TurnContext | undefined,
|
|
577
|
+
signal: AbortSignal | undefined,
|
|
578
|
+
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
579
|
+
compactionCircuit: CompactionCircuit,
|
|
580
|
+
): Promise<Message[] | null> {
|
|
581
|
+
await onEvent({ type: "context_compacting" });
|
|
582
|
+
const { rawHistory, options } = compaction.prepare(history);
|
|
583
|
+
let result: CompactionResult;
|
|
584
|
+
try {
|
|
585
|
+
result = await runPipeline<CompactionArgs, CompactionResult>(
|
|
586
|
+
"compaction",
|
|
587
|
+
getMiddlewaresFor("compaction"),
|
|
588
|
+
(args) => defaultCompactionTerminal(args, turnContext as TurnContext),
|
|
589
|
+
{ messages: rawHistory, signal, options },
|
|
590
|
+
turnContext as TurnContext,
|
|
591
|
+
DEFAULT_TIMEOUTS.compaction,
|
|
592
|
+
);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (error instanceof PluginTimeoutError) {
|
|
595
|
+
await compactionCircuit.recordOutcome(
|
|
596
|
+
{
|
|
597
|
+
currentRequestId: turnContext?.requestId,
|
|
598
|
+
currentTurnTrustContext: turnContext?.trust,
|
|
599
|
+
turnCount: turnContext?.turnIndex ?? 0,
|
|
600
|
+
},
|
|
601
|
+
true,
|
|
602
|
+
onEvent,
|
|
603
|
+
);
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
const compactResult = result as ContextWindowResult;
|
|
609
|
+
if (compactResult.summaryFailed !== undefined) {
|
|
610
|
+
await compactionCircuit.recordOutcome(
|
|
611
|
+
{
|
|
612
|
+
currentRequestId: turnContext?.requestId,
|
|
613
|
+
currentTurnTrustContext: turnContext?.trust,
|
|
614
|
+
turnCount: turnContext?.turnIndex ?? 0,
|
|
615
|
+
},
|
|
616
|
+
compactResult.summaryFailed,
|
|
617
|
+
onEvent,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (compactResult.compacted) {
|
|
621
|
+
await compaction.applyResult(compactResult, rawHistory);
|
|
622
|
+
}
|
|
623
|
+
if (compactResult.exhausted ?? false) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
return compaction.reinject();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Adapt a `Message[]`-returning mock loop body into `run()`'s real result
|
|
631
|
+
* shape. Mirrors the production loop: the pause-reason carried back is
|
|
632
|
+
* whatever the most recent `onCheckpoint` call yielded with (null when it
|
|
633
|
+
* never yielded), so the orchestrator derives its yield bookkeeping the same
|
|
634
|
+
* way it does against the real loop.
|
|
635
|
+
*/
|
|
636
|
+
const asAgentLoopRun = (
|
|
637
|
+
fn: AgentLoopRun,
|
|
638
|
+
compactionCircuit: CompactionCircuit,
|
|
639
|
+
): ((
|
|
640
|
+
messages: Message[],
|
|
641
|
+
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
642
|
+
options?: AgentLoopRunOptions,
|
|
643
|
+
) => Promise<AgentLoopRunResult>) => {
|
|
644
|
+
return async (messages, onEvent, options) => {
|
|
645
|
+
let exitReason: AgentLoopRunResult["exitReason"] = null;
|
|
646
|
+
let wrapped = options;
|
|
647
|
+
if (options?.onCheckpoint) {
|
|
648
|
+
const inner = options.onCheckpoint;
|
|
649
|
+
wrapped = {
|
|
650
|
+
...options,
|
|
651
|
+
onCheckpoint: async (info) => {
|
|
652
|
+
// Handoff is offered first, mirroring the loop's ordering.
|
|
653
|
+
const decision = await inner(info);
|
|
654
|
+
if (decision !== "continue") {
|
|
655
|
+
exitReason = decision;
|
|
656
|
+
return decision;
|
|
657
|
+
}
|
|
658
|
+
// The mid-loop budget gate and inline compaction both live inside
|
|
659
|
+
// `AgentLoop.run`. Replicate them here — same formula, stubbed
|
|
660
|
+
// estimator, and the loop's own `compact()` ceremony — so these
|
|
661
|
+
// orchestrator tests drive the real escalation path now that the
|
|
662
|
+
// orchestrator's `onCheckpoint` is handoff-only and compaction runs
|
|
663
|
+
// inline rather than via an orchestrator re-entry loop.
|
|
664
|
+
const contextWindow = options.resolveContextWindow?.();
|
|
665
|
+
if (contextWindow?.overflowRecovery.enabled) {
|
|
666
|
+
const { maxInputTokens, overflowRecovery } = contextWindow;
|
|
667
|
+
const safetyMargin =
|
|
668
|
+
info.history.length > 50
|
|
669
|
+
? Math.max(overflowRecovery.safetyMarginRatio, 0.15)
|
|
670
|
+
: overflowRecovery.safetyMarginRatio;
|
|
671
|
+
const preflightBudget = Math.floor(
|
|
672
|
+
maxInputTokens * (1 - safetyMargin),
|
|
673
|
+
);
|
|
674
|
+
if (mockEstimateTokens > preflightBudget * 0.85) {
|
|
675
|
+
// Mirror `AgentLoop.compact()`: when a compaction path is
|
|
676
|
+
// supplied, run it in place and continue; on timeout or
|
|
677
|
+
// exhaustion it returns null, so the loop yields "budget".
|
|
678
|
+
const compacted = options.compaction
|
|
679
|
+
? await simulateInlineCompaction(
|
|
680
|
+
options.compaction,
|
|
681
|
+
info.history,
|
|
682
|
+
options.turnContext,
|
|
683
|
+
options.signal,
|
|
684
|
+
onEvent,
|
|
685
|
+
compactionCircuit,
|
|
686
|
+
)
|
|
687
|
+
: null;
|
|
688
|
+
if (compacted) {
|
|
689
|
+
exitReason = null;
|
|
690
|
+
return "continue";
|
|
691
|
+
}
|
|
692
|
+
exitReason = "budget";
|
|
693
|
+
return "budget";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
exitReason = null;
|
|
697
|
+
return "continue";
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const history = await fn(messages, onEvent, wrapped);
|
|
702
|
+
return { history, exitReason };
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
|
|
522
706
|
function makeCtx(
|
|
523
707
|
overrides?: Partial<AgentLoopConversationContext> & {
|
|
524
708
|
agentLoopRun?: AgentLoopRun;
|
|
@@ -534,6 +718,8 @@ function makeCtx(
|
|
|
534
718
|
},
|
|
535
719
|
]);
|
|
536
720
|
|
|
721
|
+
const compactionCircuit = new CompactionCircuit("test-conv");
|
|
722
|
+
|
|
537
723
|
return {
|
|
538
724
|
conversationId: "test-conv",
|
|
539
725
|
messages: [
|
|
@@ -544,12 +730,13 @@ function makeCtx(
|
|
|
544
730
|
currentRequestId: "test-req",
|
|
545
731
|
|
|
546
732
|
agentLoop: {
|
|
547
|
-
run: agentLoopRun,
|
|
733
|
+
run: asAgentLoopRun(agentLoopRun, compactionCircuit),
|
|
548
734
|
getToolTokenBudget: () => 0,
|
|
549
735
|
getResolvedTools: () => [],
|
|
550
736
|
// Tests here don't exercise calibration; returning undefined makes
|
|
551
737
|
// the estimator use the per-provider aggregate key.
|
|
552
738
|
getActiveModel: () => undefined,
|
|
739
|
+
compactionCircuit,
|
|
553
740
|
} as unknown as AgentLoopConversationContext["agentLoop"],
|
|
554
741
|
provider: {
|
|
555
742
|
name: "mock-provider",
|
|
@@ -692,6 +879,13 @@ beforeEach(() => {
|
|
|
692
879
|
mockSlackChronologicalContext = null;
|
|
693
880
|
loadSlackChronologicalContextMock.mockClear();
|
|
694
881
|
getSlackCompactionWatermarkForPrefixMock.mockClear();
|
|
882
|
+
deleteMessageByIdMock.mockClear();
|
|
883
|
+
reserveMessageMock.mockClear();
|
|
884
|
+
updateMessageContentMock.mockClear();
|
|
885
|
+
indexMessageNowMock.mockClear();
|
|
886
|
+
projectAssistantMessageMock.mockClear();
|
|
887
|
+
publishSyncInvalidationMock.mockClear();
|
|
888
|
+
mockMessageById = null;
|
|
695
889
|
// Orchestrator pipelines (overflowReduce, persistence, …) run through the
|
|
696
890
|
// plugin registry; reset and re-register every default so the pipelines
|
|
697
891
|
// dispatch to middleware backed by the mocked collaborators these tests
|
|
@@ -762,7 +956,7 @@ describe("session-agent-loop", () => {
|
|
|
762
956
|
});
|
|
763
957
|
|
|
764
958
|
describe("proactive artifact trigger", () => {
|
|
765
|
-
test("
|
|
959
|
+
test("does not start proactive artifact jobs after foreground user turns", async () => {
|
|
766
960
|
mockConversationRow = {
|
|
767
961
|
...mockConversationRow,
|
|
768
962
|
id: "test-conv",
|
|
@@ -776,13 +970,10 @@ describe("session-agent-loop", () => {
|
|
|
776
970
|
mockHasProactiveArtifactCompleted = false;
|
|
777
971
|
mockTryClaimProactiveArtifactTrigger = true;
|
|
778
972
|
|
|
779
|
-
const agentLoopRun: AgentLoopRun = async (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
_requestId,
|
|
784
|
-
onCheckpoint,
|
|
785
|
-
) => {
|
|
973
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent, options) => {
|
|
974
|
+
// Prime the assistant row anchor for LLM call 1 — production code
|
|
975
|
+
// emits this from `AgentLoop.run` just before `provider.sendMessage`.
|
|
976
|
+
await onEvent({ type: "llm_call_started" });
|
|
786
977
|
await onEvent({
|
|
787
978
|
type: "message_complete",
|
|
788
979
|
message: {
|
|
@@ -802,12 +993,15 @@ describe("session-agent-loop", () => {
|
|
|
802
993
|
content: "{}",
|
|
803
994
|
isError: false,
|
|
804
995
|
});
|
|
805
|
-
await onCheckpoint?.({
|
|
996
|
+
await options?.onCheckpoint?.({
|
|
806
997
|
turnIndex: 0,
|
|
807
998
|
toolCount: 1,
|
|
808
999
|
hasToolUse: true,
|
|
809
1000
|
history: messages,
|
|
810
1001
|
});
|
|
1002
|
+
// Prime the anchor again for LLM call 2 — multi-call agent turns
|
|
1003
|
+
// reserve a fresh assistant row per LLM call.
|
|
1004
|
+
await onEvent({ type: "llm_call_started" });
|
|
811
1005
|
await onEvent({
|
|
812
1006
|
type: "message_complete",
|
|
813
1007
|
message: {
|
|
@@ -839,11 +1033,7 @@ describe("session-agent-loop", () => {
|
|
|
839
1033
|
);
|
|
840
1034
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
841
1035
|
|
|
842
|
-
expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(
|
|
843
|
-
expect(runProactiveArtifactJobMock.mock.calls[0]?.[0]).toMatchObject({
|
|
844
|
-
conversationId: "test-conv",
|
|
845
|
-
suppressAppBuild: true,
|
|
846
|
-
});
|
|
1036
|
+
expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
|
|
847
1037
|
});
|
|
848
1038
|
});
|
|
849
1039
|
|
|
@@ -981,7 +1171,10 @@ describe("session-agent-loop", () => {
|
|
|
981
1171
|
},
|
|
982
1172
|
} as unknown as AgentLoopConversationContext["traceEmitter"],
|
|
983
1173
|
});
|
|
984
|
-
ctx.agentLoop.run =
|
|
1174
|
+
ctx.agentLoop.run = asAgentLoopRun(
|
|
1175
|
+
agentLoopRun,
|
|
1176
|
+
ctx.agentLoop.compactionCircuit,
|
|
1177
|
+
);
|
|
985
1178
|
|
|
986
1179
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
987
1180
|
|
|
@@ -990,8 +1183,7 @@ describe("session-agent-loop", () => {
|
|
|
990
1183
|
expect(activityStates).toContainEqual([
|
|
991
1184
|
"idle",
|
|
992
1185
|
"error_terminal",
|
|
993
|
-
"global",
|
|
994
|
-
"test-req",
|
|
1186
|
+
{ anchor: "global", requestId: "test-req" },
|
|
995
1187
|
]);
|
|
996
1188
|
expect(traceEvents[0]).toEqual([
|
|
997
1189
|
"request_error",
|
|
@@ -1053,8 +1245,7 @@ describe("session-agent-loop", () => {
|
|
|
1053
1245
|
expect(activityStates).toContainEqual([
|
|
1054
1246
|
"idle",
|
|
1055
1247
|
"error_terminal",
|
|
1056
|
-
"global",
|
|
1057
|
-
"test-req",
|
|
1248
|
+
{ anchor: "global", requestId: "test-req" },
|
|
1058
1249
|
]);
|
|
1059
1250
|
});
|
|
1060
1251
|
});
|
|
@@ -1064,6 +1255,9 @@ describe("session-agent-loop", () => {
|
|
|
1064
1255
|
const events: ServerMessage[] = [];
|
|
1065
1256
|
|
|
1066
1257
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1258
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1259
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1260
|
+
await onEvent({ type: "llm_call_started" });
|
|
1067
1261
|
// Simulate tool_use + error during execution
|
|
1068
1262
|
onEvent({
|
|
1069
1263
|
type: "tool_use",
|
|
@@ -1113,6 +1307,9 @@ describe("session-agent-loop", () => {
|
|
|
1113
1307
|
const events: ServerMessage[] = [];
|
|
1114
1308
|
|
|
1115
1309
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1310
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1311
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1312
|
+
await onEvent({ type: "llm_call_started" });
|
|
1116
1313
|
onEvent({
|
|
1117
1314
|
type: "message_complete",
|
|
1118
1315
|
message: {
|
|
@@ -1173,6 +1370,9 @@ describe("session-agent-loop", () => {
|
|
|
1173
1370
|
};
|
|
1174
1371
|
|
|
1175
1372
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1373
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1374
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1375
|
+
await onEvent({ type: "llm_call_started" });
|
|
1176
1376
|
onEvent({
|
|
1177
1377
|
type: "message_complete",
|
|
1178
1378
|
message: {
|
|
@@ -1238,6 +1438,9 @@ describe("session-agent-loop", () => {
|
|
|
1238
1438
|
};
|
|
1239
1439
|
|
|
1240
1440
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1441
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1442
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1443
|
+
await onEvent({ type: "llm_call_started" });
|
|
1241
1444
|
onEvent({
|
|
1242
1445
|
type: "message_complete",
|
|
1243
1446
|
message: {
|
|
@@ -1320,6 +1523,9 @@ describe("session-agent-loop", () => {
|
|
|
1320
1523
|
};
|
|
1321
1524
|
|
|
1322
1525
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1526
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1527
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1528
|
+
await onEvent({ type: "llm_call_started" });
|
|
1323
1529
|
onEvent({
|
|
1324
1530
|
type: "message_complete",
|
|
1325
1531
|
message: {
|
|
@@ -1388,6 +1594,9 @@ describe("session-agent-loop", () => {
|
|
|
1388
1594
|
}> = [];
|
|
1389
1595
|
|
|
1390
1596
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1597
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1598
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1599
|
+
await onEvent({ type: "llm_call_started" });
|
|
1391
1600
|
onEvent({ type: "text_delta", text: "Hi." });
|
|
1392
1601
|
onEvent({
|
|
1393
1602
|
type: "message_complete",
|
|
@@ -1463,6 +1672,9 @@ describe("session-agent-loop", () => {
|
|
|
1463
1672
|
}> = [];
|
|
1464
1673
|
|
|
1465
1674
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1675
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1676
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1677
|
+
await onEvent({ type: "llm_call_started" });
|
|
1466
1678
|
// No text_delta — pure tool-call response
|
|
1467
1679
|
onEvent({
|
|
1468
1680
|
type: "message_complete",
|
|
@@ -1526,6 +1738,9 @@ describe("session-agent-loop", () => {
|
|
|
1526
1738
|
const events: ServerMessage[] = [];
|
|
1527
1739
|
|
|
1528
1740
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1741
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1742
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1743
|
+
await onEvent({ type: "llm_call_started" });
|
|
1529
1744
|
onEvent({
|
|
1530
1745
|
type: "message_complete",
|
|
1531
1746
|
message: {
|
|
@@ -1638,6 +1853,9 @@ describe("session-agent-loop", () => {
|
|
|
1638
1853
|
});
|
|
1639
1854
|
|
|
1640
1855
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1856
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1857
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
1858
|
+
await onEvent({ type: "llm_call_started" });
|
|
1641
1859
|
onEvent({
|
|
1642
1860
|
type: "message_complete",
|
|
1643
1861
|
message: {
|
|
@@ -1728,6 +1946,11 @@ describe("session-agent-loop", () => {
|
|
|
1728
1946
|
};
|
|
1729
1947
|
|
|
1730
1948
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
1949
|
+
// Prime the assistant row anchor — production code emits this from
|
|
1950
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Retry branches
|
|
1951
|
+
// need this on every invocation: each agent-loop iteration reserves
|
|
1952
|
+
// its own row.
|
|
1953
|
+
await onEvent({ type: "llm_call_started" });
|
|
1731
1954
|
callCount++;
|
|
1732
1955
|
if (callCount === 1) {
|
|
1733
1956
|
onEvent({
|
|
@@ -1855,6 +2078,11 @@ describe("session-agent-loop", () => {
|
|
|
1855
2078
|
};
|
|
1856
2079
|
|
|
1857
2080
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
2081
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2082
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Retry branches
|
|
2083
|
+
// need this on every invocation: each agent-loop iteration reserves
|
|
2084
|
+
// its own row.
|
|
2085
|
+
await onEvent({ type: "llm_call_started" });
|
|
1858
2086
|
callCount++;
|
|
1859
2087
|
if (callCount === 1) {
|
|
1860
2088
|
onEvent({
|
|
@@ -1940,6 +2168,11 @@ describe("session-agent-loop", () => {
|
|
|
1940
2168
|
mockOverflowAction = "auto_compress_latest_turn";
|
|
1941
2169
|
|
|
1942
2170
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
2171
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2172
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Retry branches
|
|
2173
|
+
// need this on every invocation: each agent-loop iteration reserves
|
|
2174
|
+
// its own row.
|
|
2175
|
+
await onEvent({ type: "llm_call_started" });
|
|
1943
2176
|
callCount++;
|
|
1944
2177
|
if (callCount <= 2) {
|
|
1945
2178
|
onEvent({
|
|
@@ -2059,13 +2292,7 @@ describe("session-agent-loop", () => {
|
|
|
2059
2292
|
// call). 90k satisfies both so the path reaches call 3.
|
|
2060
2293
|
mockEstimateTokens = 90_000;
|
|
2061
2294
|
|
|
2062
|
-
const agentLoopRun: AgentLoopRun = async (
|
|
2063
|
-
messages,
|
|
2064
|
-
onEvent,
|
|
2065
|
-
_signal,
|
|
2066
|
-
_reqId,
|
|
2067
|
-
onCheckpoint,
|
|
2068
|
-
) => {
|
|
2295
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent, options) => {
|
|
2069
2296
|
callCount++;
|
|
2070
2297
|
if (callCount <= 2) {
|
|
2071
2298
|
// Calls 1 (initial) and 2 (convergence rerun): error so
|
|
@@ -2089,8 +2316,8 @@ describe("session-agent-loop", () => {
|
|
|
2089
2316
|
// flips `yieldedForBudget` to true, then return without
|
|
2090
2317
|
// finishing — mirroring what AgentLoop.run does when its
|
|
2091
2318
|
// checkpoint returns "yield".
|
|
2092
|
-
if (onCheckpoint) {
|
|
2093
|
-
await onCheckpoint({
|
|
2319
|
+
if (options?.onCheckpoint) {
|
|
2320
|
+
await options.onCheckpoint({
|
|
2094
2321
|
turnIndex: 0,
|
|
2095
2322
|
toolCount: 1,
|
|
2096
2323
|
hasToolUse: true,
|
|
@@ -2237,6 +2464,9 @@ describe("session-agent-loop", () => {
|
|
|
2237
2464
|
|
|
2238
2465
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
2239
2466
|
agentLoopCalls++;
|
|
2467
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2468
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
2469
|
+
await onEvent({ type: "llm_call_started" });
|
|
2240
2470
|
onEvent({
|
|
2241
2471
|
type: "message_complete",
|
|
2242
2472
|
message: {
|
|
@@ -2285,6 +2515,11 @@ describe("session-agent-loop", () => {
|
|
|
2285
2515
|
let callCount = 0;
|
|
2286
2516
|
|
|
2287
2517
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
2518
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2519
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Retry branches
|
|
2520
|
+
// need this on every invocation: each agent-loop iteration reserves
|
|
2521
|
+
// its own row.
|
|
2522
|
+
await onEvent({ type: "llm_call_started" });
|
|
2288
2523
|
callCount++;
|
|
2289
2524
|
if (callCount === 1) {
|
|
2290
2525
|
onEvent({
|
|
@@ -2362,13 +2597,12 @@ describe("session-agent-loop", () => {
|
|
|
2362
2597
|
test("yields at checkpoint when canHandoffAtCheckpoint returns true", async () => {
|
|
2363
2598
|
const events: ServerMessage[] = [];
|
|
2364
2599
|
|
|
2365
|
-
const agentLoopRun: AgentLoopRun = async (
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
) => {
|
|
2600
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent, options) => {
|
|
2601
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2602
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Retry branches
|
|
2603
|
+
// need this on every invocation: each agent-loop iteration reserves
|
|
2604
|
+
// its own row.
|
|
2605
|
+
await onEvent({ type: "llm_call_started" });
|
|
2372
2606
|
// Simulate tool use followed by checkpoint
|
|
2373
2607
|
onEvent({ type: "tool_use", id: "tu-1", name: "file_read", input: {} });
|
|
2374
2608
|
onEvent({
|
|
@@ -2391,14 +2625,14 @@ describe("session-agent-loop", () => {
|
|
|
2391
2625
|
model: "test-model",
|
|
2392
2626
|
providerDurationMs: 100,
|
|
2393
2627
|
});
|
|
2394
|
-
if (onCheckpoint) {
|
|
2395
|
-
const decision = await onCheckpoint({
|
|
2628
|
+
if (options?.onCheckpoint) {
|
|
2629
|
+
const decision = await options.onCheckpoint({
|
|
2396
2630
|
turnIndex: 0,
|
|
2397
2631
|
toolCount: 1,
|
|
2398
2632
|
hasToolUse: true,
|
|
2399
2633
|
history: messages,
|
|
2400
2634
|
});
|
|
2401
|
-
if (decision
|
|
2635
|
+
if (decision !== "continue") {
|
|
2402
2636
|
return [
|
|
2403
2637
|
...messages,
|
|
2404
2638
|
{
|
|
@@ -2435,13 +2669,12 @@ describe("session-agent-loop", () => {
|
|
|
2435
2669
|
test("continues when canHandoffAtCheckpoint returns false", async () => {
|
|
2436
2670
|
const events: ServerMessage[] = [];
|
|
2437
2671
|
|
|
2438
|
-
const agentLoopRun: AgentLoopRun = async (
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
) => {
|
|
2672
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent, options) => {
|
|
2673
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2674
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Retry branches
|
|
2675
|
+
// need this on every invocation: each agent-loop iteration reserves
|
|
2676
|
+
// its own row.
|
|
2677
|
+
await onEvent({ type: "llm_call_started" });
|
|
2445
2678
|
onEvent({ type: "tool_use", id: "tu-1", name: "file_read", input: {} });
|
|
2446
2679
|
onEvent({
|
|
2447
2680
|
type: "tool_result",
|
|
@@ -2463,8 +2696,8 @@ describe("session-agent-loop", () => {
|
|
|
2463
2696
|
model: "test-model",
|
|
2464
2697
|
providerDurationMs: 100,
|
|
2465
2698
|
});
|
|
2466
|
-
if (onCheckpoint) {
|
|
2467
|
-
await onCheckpoint({
|
|
2699
|
+
if (options?.onCheckpoint) {
|
|
2700
|
+
await options.onCheckpoint({
|
|
2468
2701
|
turnIndex: 0,
|
|
2469
2702
|
toolCount: 1,
|
|
2470
2703
|
hasToolUse: true,
|
|
@@ -2504,6 +2737,9 @@ describe("session-agent-loop", () => {
|
|
|
2504
2737
|
const abortController = new AbortController();
|
|
2505
2738
|
|
|
2506
2739
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
2740
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2741
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
2742
|
+
await onEvent({ type: "llm_call_started" });
|
|
2507
2743
|
onEvent({
|
|
2508
2744
|
type: "message_complete",
|
|
2509
2745
|
message: {
|
|
@@ -2564,6 +2800,9 @@ describe("session-agent-loop", () => {
|
|
|
2564
2800
|
resolveAssistantAttachmentsMock.mockClear();
|
|
2565
2801
|
|
|
2566
2802
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
2803
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2804
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
2805
|
+
await onEvent({ type: "llm_call_started" });
|
|
2567
2806
|
onEvent({
|
|
2568
2807
|
type: "message_complete",
|
|
2569
2808
|
message: {
|
|
@@ -2603,6 +2842,9 @@ describe("session-agent-loop", () => {
|
|
|
2603
2842
|
test("increments turnCount after successful run", async () => {
|
|
2604
2843
|
const ctx = makeCtx({
|
|
2605
2844
|
agentLoopRun: async (messages, onEvent) => {
|
|
2845
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2846
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
2847
|
+
await onEvent({ type: "llm_call_started" });
|
|
2606
2848
|
onEvent({
|
|
2607
2849
|
type: "message_complete",
|
|
2608
2850
|
message: {
|
|
@@ -2636,6 +2878,9 @@ describe("session-agent-loop", () => {
|
|
|
2636
2878
|
test("clears processing state and abort controller", async () => {
|
|
2637
2879
|
const ctx = makeCtx({
|
|
2638
2880
|
agentLoopRun: async (messages, onEvent) => {
|
|
2881
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2882
|
+
// `AgentLoop.run` just before `provider.sendMessage`.
|
|
2883
|
+
await onEvent({ type: "llm_call_started" });
|
|
2639
2884
|
onEvent({
|
|
2640
2885
|
type: "message_complete",
|
|
2641
2886
|
message: {
|
|
@@ -2699,8 +2944,13 @@ describe("session-agent-loop", () => {
|
|
|
2699
2944
|
const ctx = makeCtx({
|
|
2700
2945
|
agentLoopRun: async (
|
|
2701
2946
|
messages: Message[],
|
|
2702
|
-
onEvent: (event: AgentEvent) => void
|
|
2947
|
+
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
2703
2948
|
) => {
|
|
2949
|
+
// Prime the assistant row anchor — production code emits this from
|
|
2950
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Must be
|
|
2951
|
+
// awaited so the assistant row is reserved before message_complete
|
|
2952
|
+
// tries to write into it.
|
|
2953
|
+
await onEvent({ type: "llm_call_started" });
|
|
2704
2954
|
onEvent({
|
|
2705
2955
|
type: "message_complete",
|
|
2706
2956
|
message: {
|
|
@@ -3044,6 +3294,674 @@ describe("session-agent-loop", () => {
|
|
|
3044
3294
|
});
|
|
3045
3295
|
});
|
|
3046
3296
|
|
|
3297
|
+
describe("B3 pre-allocation: indexing + cleanup", () => {
|
|
3298
|
+
test("handleMessageComplete indexes and projects the finalized assistant row", async () => {
|
|
3299
|
+
// The pre-B3 path inserted assistant rows via `addMessage`, which ran
|
|
3300
|
+
// the memory indexer and the conversation-attention projector as
|
|
3301
|
+
// side-effects of the insert. B3 splits the write into
|
|
3302
|
+
// `reserveMessage` + `updateMessageContent`, both of which are CRUD-only,
|
|
3303
|
+
// so the indexing + projection calls had to be re-driven explicitly
|
|
3304
|
+
// after `updateContent` succeeds. Codex P1 caught a regression where
|
|
3305
|
+
// this path was missing entirely; this test pins it down.
|
|
3306
|
+
mockMessageById = {
|
|
3307
|
+
id: "msg-reserve",
|
|
3308
|
+
conversationId: "test-conv",
|
|
3309
|
+
createdAt: 1234567,
|
|
3310
|
+
role: "assistant",
|
|
3311
|
+
content: "[]",
|
|
3312
|
+
metadata: null,
|
|
3313
|
+
};
|
|
3314
|
+
// Force attention projection to report a state change so we also
|
|
3315
|
+
// observe the sync-invalidation publish path on the same turn.
|
|
3316
|
+
projectAssistantMessageMock.mockImplementationOnce(() => true);
|
|
3317
|
+
|
|
3318
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3319
|
+
await onEvent({ type: "llm_call_started" });
|
|
3320
|
+
// `message_complete` is awaited so `handleMessageComplete` (and its
|
|
3321
|
+
// async indexer + projector chain) completes before the next event
|
|
3322
|
+
// or before the loop returns. Without the await the projector's
|
|
3323
|
+
// synchronous call still races against the test's assertion phase
|
|
3324
|
+
// because the indexer's `await` yields microtasks.
|
|
3325
|
+
await onEvent({
|
|
3326
|
+
type: "message_complete",
|
|
3327
|
+
message: {
|
|
3328
|
+
role: "assistant",
|
|
3329
|
+
content: [{ type: "text", text: "indexed reply" }],
|
|
3330
|
+
},
|
|
3331
|
+
});
|
|
3332
|
+
onEvent({
|
|
3333
|
+
type: "usage",
|
|
3334
|
+
inputTokens: 10,
|
|
3335
|
+
outputTokens: 5,
|
|
3336
|
+
model: "test",
|
|
3337
|
+
providerDurationMs: 50,
|
|
3338
|
+
});
|
|
3339
|
+
return [
|
|
3340
|
+
...messages,
|
|
3341
|
+
{
|
|
3342
|
+
role: "assistant" as const,
|
|
3343
|
+
content: [
|
|
3344
|
+
{ type: "text", text: "indexed reply" },
|
|
3345
|
+
] as ContentBlock[],
|
|
3346
|
+
},
|
|
3347
|
+
];
|
|
3348
|
+
};
|
|
3349
|
+
|
|
3350
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3351
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3352
|
+
|
|
3353
|
+
// Indexer fired with the reserved row's id + the finalized content.
|
|
3354
|
+
expect(indexMessageNowMock).toHaveBeenCalledTimes(1);
|
|
3355
|
+
const indexCallArgs = indexMessageNowMock.mock.calls[0] as unknown as [
|
|
3356
|
+
{
|
|
3357
|
+
messageId: string;
|
|
3358
|
+
conversationId: string;
|
|
3359
|
+
role: string;
|
|
3360
|
+
content: string;
|
|
3361
|
+
createdAt: number;
|
|
3362
|
+
scopeId: string;
|
|
3363
|
+
},
|
|
3364
|
+
unknown,
|
|
3365
|
+
];
|
|
3366
|
+
const indexCall = indexCallArgs[0];
|
|
3367
|
+
expect(indexCall).toMatchObject({
|
|
3368
|
+
messageId: "msg-reserve",
|
|
3369
|
+
conversationId: "test-conv",
|
|
3370
|
+
role: "assistant",
|
|
3371
|
+
createdAt: 1234567,
|
|
3372
|
+
scopeId: "default",
|
|
3373
|
+
});
|
|
3374
|
+
expect(indexCall.content).toContain("indexed reply");
|
|
3375
|
+
|
|
3376
|
+
// Attention projector fired with the same row coordinates.
|
|
3377
|
+
expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
|
|
3378
|
+
const projectCall = projectAssistantMessageMock.mock
|
|
3379
|
+
.calls[0] as unknown as [
|
|
3380
|
+
{ conversationId: string; messageId: string; messageAt: number },
|
|
3381
|
+
];
|
|
3382
|
+
expect(projectCall[0]).toEqual({
|
|
3383
|
+
conversationId: "test-conv",
|
|
3384
|
+
messageId: "msg-reserve",
|
|
3385
|
+
messageAt: 1234567,
|
|
3386
|
+
});
|
|
3387
|
+
|
|
3388
|
+
// Projection reported a state change → sync invalidation fires with
|
|
3389
|
+
// the conversation `:metadata` tag. The mock also receives a
|
|
3390
|
+
// `:messages` invalidation from the orchestrator's
|
|
3391
|
+
// `publishLoopMessagesChanged` post-loop emit, so we filter by tag
|
|
3392
|
+
// rather than asserting a total call count.
|
|
3393
|
+
const metadataPublishes = (
|
|
3394
|
+
publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
|
|
3395
|
+
).filter((args) => args[0]?.includes("conversation:test-conv:metadata"));
|
|
3396
|
+
expect(metadataPublishes).toHaveLength(1);
|
|
3397
|
+
});
|
|
3398
|
+
|
|
3399
|
+
test("handleMessageComplete skips sync invalidation when attention state unchanged", async () => {
|
|
3400
|
+
// Mirror of the previous test but with the default projector return
|
|
3401
|
+
// (`false`). The projection still runs every turn, but the sync
|
|
3402
|
+
// invalidation publish must be gated on attention-state movement to
|
|
3403
|
+
// avoid flooding clients with no-op metadata refreshes.
|
|
3404
|
+
mockMessageById = {
|
|
3405
|
+
id: "msg-reserve",
|
|
3406
|
+
conversationId: "test-conv",
|
|
3407
|
+
createdAt: 999,
|
|
3408
|
+
role: "assistant",
|
|
3409
|
+
content: "[]",
|
|
3410
|
+
metadata: null,
|
|
3411
|
+
};
|
|
3412
|
+
|
|
3413
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3414
|
+
await onEvent({ type: "llm_call_started" });
|
|
3415
|
+
// See sibling test — `message_complete` must be awaited so the
|
|
3416
|
+
// projector call lands before the assertion phase.
|
|
3417
|
+
await onEvent({
|
|
3418
|
+
type: "message_complete",
|
|
3419
|
+
message: {
|
|
3420
|
+
role: "assistant",
|
|
3421
|
+
content: [{ type: "text", text: "quiet" }],
|
|
3422
|
+
},
|
|
3423
|
+
});
|
|
3424
|
+
onEvent({
|
|
3425
|
+
type: "usage",
|
|
3426
|
+
inputTokens: 1,
|
|
3427
|
+
outputTokens: 1,
|
|
3428
|
+
model: "test",
|
|
3429
|
+
providerDurationMs: 1,
|
|
3430
|
+
});
|
|
3431
|
+
return [
|
|
3432
|
+
...messages,
|
|
3433
|
+
{
|
|
3434
|
+
role: "assistant" as const,
|
|
3435
|
+
content: [{ type: "text", text: "quiet" }] as ContentBlock[],
|
|
3436
|
+
},
|
|
3437
|
+
];
|
|
3438
|
+
};
|
|
3439
|
+
|
|
3440
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3441
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3442
|
+
|
|
3443
|
+
expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
|
|
3444
|
+
// The mock will still receive a `:messages` invalidation from the
|
|
3445
|
+
// orchestrator's `publishLoopMessagesChanged` — filter to the
|
|
3446
|
+
// `:metadata` tag and assert it never landed.
|
|
3447
|
+
const metadataPublishes = (
|
|
3448
|
+
publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
|
|
3449
|
+
).filter((args) => args[0]?.includes("conversation:test-conv:metadata"));
|
|
3450
|
+
expect(metadataPublishes).toHaveLength(0);
|
|
3451
|
+
});
|
|
3452
|
+
|
|
3453
|
+
test("handleLlmCallStarted deletes a stranded reservation before reserving a new row", async () => {
|
|
3454
|
+
// Simulates a retry path: the first LLM call reserves an assistant row
|
|
3455
|
+
// but exits without `message_complete` (e.g. context-overflow rescue,
|
|
3456
|
+
// ordering-error rescue, image-overflow rescue). The next
|
|
3457
|
+
// `llm_call_started` must delete the stranded row so the transcript
|
|
3458
|
+
// does not accumulate empty assistant bubbles.
|
|
3459
|
+
reserveMessageMock
|
|
3460
|
+
.mockImplementationOnce(async () => ({ id: "msg-strand-A" }))
|
|
3461
|
+
.mockImplementationOnce(async () => ({ id: "msg-strand-B" }));
|
|
3462
|
+
// Indexer/projector mocks default to no-op; no finalized row in this
|
|
3463
|
+
// test, so `mockMessageById` stays null.
|
|
3464
|
+
|
|
3465
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3466
|
+
// First LLM call: reserve msg-strand-A, never finalize.
|
|
3467
|
+
await onEvent({ type: "llm_call_started" });
|
|
3468
|
+
// Second LLM call: should delete msg-strand-A before reserving
|
|
3469
|
+
// msg-strand-B.
|
|
3470
|
+
await onEvent({ type: "llm_call_started" });
|
|
3471
|
+
// Finalize the second one so the loop has a valid assistant message
|
|
3472
|
+
// and exits cleanly.
|
|
3473
|
+
onEvent({
|
|
3474
|
+
type: "message_complete",
|
|
3475
|
+
message: {
|
|
3476
|
+
role: "assistant",
|
|
3477
|
+
content: [{ type: "text", text: "retry succeeded" }],
|
|
3478
|
+
},
|
|
3479
|
+
});
|
|
3480
|
+
onEvent({
|
|
3481
|
+
type: "usage",
|
|
3482
|
+
inputTokens: 5,
|
|
3483
|
+
outputTokens: 3,
|
|
3484
|
+
model: "test",
|
|
3485
|
+
providerDurationMs: 25,
|
|
3486
|
+
});
|
|
3487
|
+
return [
|
|
3488
|
+
...messages,
|
|
3489
|
+
{
|
|
3490
|
+
role: "assistant" as const,
|
|
3491
|
+
content: [
|
|
3492
|
+
{ type: "text", text: "retry succeeded" },
|
|
3493
|
+
] as ContentBlock[],
|
|
3494
|
+
},
|
|
3495
|
+
];
|
|
3496
|
+
};
|
|
3497
|
+
|
|
3498
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3499
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3500
|
+
|
|
3501
|
+
// Exactly one delete fires — for msg-strand-A, before the second
|
|
3502
|
+
// reserve. The second reservation is committed via `updateContent`
|
|
3503
|
+
// (not deleted), and after the run completes
|
|
3504
|
+
// `assistantRowAwaitingFinalization` is false, so no further delete
|
|
3505
|
+
// is attempted on shutdown.
|
|
3506
|
+
expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
|
|
3507
|
+
const strandDeleteCall = deleteMessageByIdMock.mock
|
|
3508
|
+
.calls[0] as unknown as [string];
|
|
3509
|
+
expect(strandDeleteCall[0]).toBe("msg-strand-A");
|
|
3510
|
+
expect(reserveMessageMock).toHaveBeenCalledTimes(2);
|
|
3511
|
+
});
|
|
3512
|
+
|
|
3513
|
+
test("provider-error branch deletes the orphaned reservation and repoints lastAssistantMessageId", async () => {
|
|
3514
|
+
// Codex P2 regression: B3 reserves an empty assistant row at
|
|
3515
|
+
// `llm_call_started`. When the call exits via the provider-error
|
|
3516
|
+
// branch (no `message_complete`), the synthetic error message is
|
|
3517
|
+
// inserted separately. Without cleanup the transcript would carry
|
|
3518
|
+
// both the empty reserved row AND the error message, and
|
|
3519
|
+
// `syncLastAssistantMessageToDisk` (which reads
|
|
3520
|
+
// `state.lastAssistantMessageId`) would mis-target the deleted
|
|
3521
|
+
// reservation id.
|
|
3522
|
+
reserveMessageMock.mockImplementationOnce(async () => ({
|
|
3523
|
+
id: "msg-orphaned-reservation",
|
|
3524
|
+
}));
|
|
3525
|
+
|
|
3526
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3527
|
+
// Reserve the orphan.
|
|
3528
|
+
await onEvent({ type: "llm_call_started" });
|
|
3529
|
+
// Provider rejects — writes the llm_request_log row and arms
|
|
3530
|
+
// `state.providerErrorUserMessage` via `handleError`.
|
|
3531
|
+
onEvent({
|
|
3532
|
+
type: "provider_error",
|
|
3533
|
+
error: new Error("upstream 500"),
|
|
3534
|
+
rawRequest: { model: "gpt-4.1", messages: [] },
|
|
3535
|
+
actualProvider: "openai",
|
|
3536
|
+
});
|
|
3537
|
+
onEvent({
|
|
3538
|
+
type: "error",
|
|
3539
|
+
error: new Error("upstream 500"),
|
|
3540
|
+
});
|
|
3541
|
+
// No assistant message in the result — the synthetic-error branch
|
|
3542
|
+
// below the agent loop fires.
|
|
3543
|
+
return messages;
|
|
3544
|
+
};
|
|
3545
|
+
|
|
3546
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3547
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3548
|
+
|
|
3549
|
+
// The orphan was deleted exactly once, before the synthetic error
|
|
3550
|
+
// message landed.
|
|
3551
|
+
expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
|
|
3552
|
+
const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
|
|
3553
|
+
string,
|
|
3554
|
+
];
|
|
3555
|
+
expect(deleteCall[0]).toBe("msg-orphaned-reservation");
|
|
3556
|
+
|
|
3557
|
+
// Post-loop `syncLastAssistantMessageToDisk` targets the synthetic
|
|
3558
|
+
// error row's id (`mock-msg-id` from the mocked `addMessage`), NOT
|
|
3559
|
+
// the deleted reservation id. This is the externally-observable
|
|
3560
|
+
// proof that `state.lastAssistantMessageId` was repointed.
|
|
3561
|
+
expect(syncMessageToDiskMock).toHaveBeenCalled();
|
|
3562
|
+
const syncCalls = syncMessageToDiskMock.mock.calls as unknown as Array<
|
|
3563
|
+
[string, string, number]
|
|
3564
|
+
>;
|
|
3565
|
+
const lastSync = syncCalls[syncCalls.length - 1];
|
|
3566
|
+
expect(lastSync?.[1]).toBe("mock-msg-id");
|
|
3567
|
+
expect(lastSync?.[1]).not.toBe("msg-orphaned-reservation");
|
|
3568
|
+
});
|
|
3569
|
+
});
|
|
3570
|
+
|
|
3571
|
+
describe("partial persistence", () => {
|
|
3572
|
+
// The legacy flow reserves an empty assistant row at `llm_call_started`
|
|
3573
|
+
// (`content: "[]"`) and never touches it again until
|
|
3574
|
+
// `handleMessageComplete` fires the single authoritative
|
|
3575
|
+
// `updateContent`. Between those events the row is empty for the full
|
|
3576
|
+
// duration of a turn — a browser refresh mid-turn sees nothing where
|
|
3577
|
+
// the in-progress assistant reply should be.
|
|
3578
|
+
//
|
|
3579
|
+
// Partial persistence closes that durability gap with a debounced
|
|
3580
|
+
// flush from `handleTextDelta` (250ms timer). `handleToolUse`
|
|
3581
|
+
// intentionally does NOT flush — `AgentLoop.run` emits `tool_use`
|
|
3582
|
+
// strictly AFTER `message_complete`, so any flush from that handler
|
|
3583
|
+
// would land after the authoritative finalize and overwrite the
|
|
3584
|
+
// finalized row. The indexer + projector still fire ONLY at
|
|
3585
|
+
// `message_complete` — partial rows are never indexed.
|
|
3586
|
+
//
|
|
3587
|
+
// These tests pin down the wire-level contract by counting
|
|
3588
|
+
// `updateMessageContent` calls and inspecting the JSON payload of the
|
|
3589
|
+
// partial-flush writes. The indexing / sync-invalidation paths are
|
|
3590
|
+
// covered by the pre-allocation block above.
|
|
3591
|
+
|
|
3592
|
+
test("debounced time gate flushes one partial write after PARTIAL_PERSIST_DEBOUNCE_MS", async () => {
|
|
3593
|
+
mockMessageById = {
|
|
3594
|
+
id: "msg-reserve",
|
|
3595
|
+
conversationId: "test-conv",
|
|
3596
|
+
createdAt: 1234567,
|
|
3597
|
+
role: "assistant",
|
|
3598
|
+
content: "[]",
|
|
3599
|
+
metadata: null,
|
|
3600
|
+
};
|
|
3601
|
+
|
|
3602
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3603
|
+
await onEvent({ type: "llm_call_started" });
|
|
3604
|
+
// Two small deltas — well under the 1024-char size gate — should
|
|
3605
|
+
// schedule a single debounced flush.
|
|
3606
|
+
onEvent({ type: "text_delta", text: "Hello, " });
|
|
3607
|
+
onEvent({ type: "text_delta", text: "world." });
|
|
3608
|
+
// Wait long enough for the 250ms debounce to fire.
|
|
3609
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
3610
|
+
await onEvent({
|
|
3611
|
+
type: "message_complete",
|
|
3612
|
+
message: {
|
|
3613
|
+
role: "assistant",
|
|
3614
|
+
content: [{ type: "text", text: "Hello, world." }],
|
|
3615
|
+
},
|
|
3616
|
+
});
|
|
3617
|
+
onEvent({
|
|
3618
|
+
type: "usage",
|
|
3619
|
+
inputTokens: 10,
|
|
3620
|
+
outputTokens: 5,
|
|
3621
|
+
model: "test",
|
|
3622
|
+
providerDurationMs: 50,
|
|
3623
|
+
});
|
|
3624
|
+
return [
|
|
3625
|
+
...messages,
|
|
3626
|
+
{
|
|
3627
|
+
role: "assistant" as const,
|
|
3628
|
+
content: [
|
|
3629
|
+
{ type: "text", text: "Hello, world." },
|
|
3630
|
+
] as ContentBlock[],
|
|
3631
|
+
},
|
|
3632
|
+
];
|
|
3633
|
+
};
|
|
3634
|
+
|
|
3635
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3636
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3637
|
+
|
|
3638
|
+
// Exactly two `updateContent` calls land:
|
|
3639
|
+
// 1. the debounced partial flush after both deltas accumulated, and
|
|
3640
|
+
// 2. the final authoritative flush in `handleMessageComplete`.
|
|
3641
|
+
// Without the debounce gate this would be one-per-delta + one final
|
|
3642
|
+
// (3). Without the partial flush at all it would be just 1.
|
|
3643
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(2);
|
|
3644
|
+
const calls = updateMessageContentMock.mock.calls as unknown as Array<
|
|
3645
|
+
[string, string]
|
|
3646
|
+
>;
|
|
3647
|
+
const partialFlush = calls[0];
|
|
3648
|
+
expect(partialFlush?.[0]).toBe("msg-reserve");
|
|
3649
|
+
const partialBlocks = JSON.parse(partialFlush?.[1] ?? "[]") as Array<{
|
|
3650
|
+
type: string;
|
|
3651
|
+
text?: string;
|
|
3652
|
+
}>;
|
|
3653
|
+
expect(partialBlocks).toEqual([{ type: "text", text: "Hello, world." }]);
|
|
3654
|
+
});
|
|
3655
|
+
|
|
3656
|
+
test("handleToolUse does NOT trigger a partial flush of its own", async () => {
|
|
3657
|
+
// `AgentLoop.run` emits `tool_use` strictly AFTER `message_complete`,
|
|
3658
|
+
// so a flush from the tool_use handler would land after the
|
|
3659
|
+
// authoritative final `updateContent` and overwrite the finalized
|
|
3660
|
+
// row (Codex P1 / Vargas review feedback). The handler must be a
|
|
3661
|
+
// no-op for the partial-persist accumulator.
|
|
3662
|
+
mockMessageById = {
|
|
3663
|
+
id: "msg-reserve",
|
|
3664
|
+
conversationId: "test-conv",
|
|
3665
|
+
createdAt: 1234567,
|
|
3666
|
+
role: "assistant",
|
|
3667
|
+
content: "[]",
|
|
3668
|
+
metadata: null,
|
|
3669
|
+
};
|
|
3670
|
+
|
|
3671
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3672
|
+
await onEvent({ type: "llm_call_started" });
|
|
3673
|
+
// No text delta — only a tool_use. If `handleToolUse` were
|
|
3674
|
+
// flushing, this would land a partial write before
|
|
3675
|
+
// `message_complete`.
|
|
3676
|
+
onEvent({
|
|
3677
|
+
type: "tool_use",
|
|
3678
|
+
id: "tu-no-flush",
|
|
3679
|
+
name: "file_read",
|
|
3680
|
+
input: { path: "/foo" },
|
|
3681
|
+
});
|
|
3682
|
+
// Yield a microtask so any (incorrectly) fire-and-forget
|
|
3683
|
+
// pipeline call has a chance to land before message_complete.
|
|
3684
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
3685
|
+
onEvent({
|
|
3686
|
+
type: "tool_result",
|
|
3687
|
+
toolUseId: "tu-no-flush",
|
|
3688
|
+
content: "ok",
|
|
3689
|
+
isError: false,
|
|
3690
|
+
});
|
|
3691
|
+
await onEvent({
|
|
3692
|
+
type: "message_complete",
|
|
3693
|
+
message: {
|
|
3694
|
+
role: "assistant",
|
|
3695
|
+
content: [
|
|
3696
|
+
{
|
|
3697
|
+
type: "tool_use",
|
|
3698
|
+
id: "tu-no-flush",
|
|
3699
|
+
name: "file_read",
|
|
3700
|
+
input: { path: "/foo" },
|
|
3701
|
+
},
|
|
3702
|
+
],
|
|
3703
|
+
},
|
|
3704
|
+
});
|
|
3705
|
+
onEvent({
|
|
3706
|
+
type: "usage",
|
|
3707
|
+
inputTokens: 10,
|
|
3708
|
+
outputTokens: 5,
|
|
3709
|
+
model: "test",
|
|
3710
|
+
providerDurationMs: 50,
|
|
3711
|
+
});
|
|
3712
|
+
return [
|
|
3713
|
+
...messages,
|
|
3714
|
+
{
|
|
3715
|
+
role: "assistant" as const,
|
|
3716
|
+
content: [
|
|
3717
|
+
{
|
|
3718
|
+
type: "tool_use",
|
|
3719
|
+
id: "tu-no-flush",
|
|
3720
|
+
name: "file_read",
|
|
3721
|
+
input: { path: "/foo" },
|
|
3722
|
+
},
|
|
3723
|
+
] as ContentBlock[],
|
|
3724
|
+
},
|
|
3725
|
+
];
|
|
3726
|
+
};
|
|
3727
|
+
|
|
3728
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3729
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3730
|
+
|
|
3731
|
+
// Only the authoritative final flush from `handleMessageComplete`
|
|
3732
|
+
// lands. A partial flush from `handleToolUse` would have made this
|
|
3733
|
+
// 2; that's the regression this test guards against.
|
|
3734
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(1);
|
|
3735
|
+
});
|
|
3736
|
+
|
|
3737
|
+
test("handleMessageComplete clears any pending debounce timer before the final flush", async () => {
|
|
3738
|
+
mockMessageById = {
|
|
3739
|
+
id: "msg-reserve",
|
|
3740
|
+
conversationId: "test-conv",
|
|
3741
|
+
createdAt: 1234567,
|
|
3742
|
+
role: "assistant",
|
|
3743
|
+
content: "[]",
|
|
3744
|
+
metadata: null,
|
|
3745
|
+
};
|
|
3746
|
+
|
|
3747
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3748
|
+
await onEvent({ type: "llm_call_started" });
|
|
3749
|
+
// Short delta — schedules a debounce timer but does NOT trip the
|
|
3750
|
+
// size gate. message_complete then arrives immediately after,
|
|
3751
|
+
// before the 250ms timer can fire.
|
|
3752
|
+
onEvent({ type: "text_delta", text: "Quick reply." });
|
|
3753
|
+
await onEvent({
|
|
3754
|
+
type: "message_complete",
|
|
3755
|
+
message: {
|
|
3756
|
+
role: "assistant",
|
|
3757
|
+
content: [{ type: "text", text: "Quick reply." }],
|
|
3758
|
+
},
|
|
3759
|
+
});
|
|
3760
|
+
onEvent({
|
|
3761
|
+
type: "usage",
|
|
3762
|
+
inputTokens: 10,
|
|
3763
|
+
outputTokens: 5,
|
|
3764
|
+
model: "test",
|
|
3765
|
+
providerDurationMs: 50,
|
|
3766
|
+
});
|
|
3767
|
+
// Wait past the original debounce window to prove a late timer
|
|
3768
|
+
// does NOT fire a stray partial flush.
|
|
3769
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
3770
|
+
return [
|
|
3771
|
+
...messages,
|
|
3772
|
+
{
|
|
3773
|
+
role: "assistant" as const,
|
|
3774
|
+
content: [{ type: "text", text: "Quick reply." }] as ContentBlock[],
|
|
3775
|
+
},
|
|
3776
|
+
];
|
|
3777
|
+
};
|
|
3778
|
+
|
|
3779
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3780
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3781
|
+
|
|
3782
|
+
// Only the final flush from `handleMessageComplete` lands. The
|
|
3783
|
+
// debounced partial would have fired around T+250ms; the timer-clear
|
|
3784
|
+
// at the top of `handleMessageComplete` cancels it.
|
|
3785
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(1);
|
|
3786
|
+
});
|
|
3787
|
+
|
|
3788
|
+
test("partial flushes never trigger the indexer or attention projector", async () => {
|
|
3789
|
+
mockMessageById = {
|
|
3790
|
+
id: "msg-reserve",
|
|
3791
|
+
conversationId: "test-conv",
|
|
3792
|
+
createdAt: 1234567,
|
|
3793
|
+
role: "assistant",
|
|
3794
|
+
content: "[]",
|
|
3795
|
+
metadata: null,
|
|
3796
|
+
};
|
|
3797
|
+
|
|
3798
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3799
|
+
await onEvent({ type: "llm_call_started" });
|
|
3800
|
+
onEvent({ type: "text_delta", text: "hello world" });
|
|
3801
|
+
// Wait past the 250ms debounce so the partial flush definitely
|
|
3802
|
+
// lands BEFORE message_complete fires.
|
|
3803
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
3804
|
+
// Snapshot the indexer/projector call counts AFTER the partial
|
|
3805
|
+
// flush has run but BEFORE message_complete. They must be zero.
|
|
3806
|
+
const indexerCallsBeforeComplete =
|
|
3807
|
+
indexMessageNowMock.mock.calls.length;
|
|
3808
|
+
const projectorCallsBeforeComplete =
|
|
3809
|
+
projectAssistantMessageMock.mock.calls.length;
|
|
3810
|
+
// Stash on a side channel the assertion phase can read.
|
|
3811
|
+
(
|
|
3812
|
+
ctx as unknown as { __partialSnapshot?: [number, number] }
|
|
3813
|
+
).__partialSnapshot = [
|
|
3814
|
+
indexerCallsBeforeComplete,
|
|
3815
|
+
projectorCallsBeforeComplete,
|
|
3816
|
+
];
|
|
3817
|
+
await onEvent({
|
|
3818
|
+
type: "message_complete",
|
|
3819
|
+
message: {
|
|
3820
|
+
role: "assistant",
|
|
3821
|
+
content: [{ type: "text", text: "hello world" }],
|
|
3822
|
+
},
|
|
3823
|
+
});
|
|
3824
|
+
onEvent({
|
|
3825
|
+
type: "usage",
|
|
3826
|
+
inputTokens: 10,
|
|
3827
|
+
outputTokens: 5,
|
|
3828
|
+
model: "test",
|
|
3829
|
+
providerDurationMs: 50,
|
|
3830
|
+
});
|
|
3831
|
+
return [
|
|
3832
|
+
...messages,
|
|
3833
|
+
{
|
|
3834
|
+
role: "assistant" as const,
|
|
3835
|
+
content: [{ type: "text", text: "hello world" }] as ContentBlock[],
|
|
3836
|
+
},
|
|
3837
|
+
];
|
|
3838
|
+
};
|
|
3839
|
+
|
|
3840
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3841
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3842
|
+
|
|
3843
|
+
const snapshot = (
|
|
3844
|
+
ctx as unknown as { __partialSnapshot?: [number, number] }
|
|
3845
|
+
).__partialSnapshot;
|
|
3846
|
+
expect(snapshot).toBeDefined();
|
|
3847
|
+
// Indexer + projector were both ZERO during the mid-turn partial
|
|
3848
|
+
// flush — they only fire from `handleMessageComplete` after the
|
|
3849
|
+
// authoritative `updateContent`.
|
|
3850
|
+
expect(snapshot![0]).toBe(0);
|
|
3851
|
+
expect(snapshot![1]).toBe(0);
|
|
3852
|
+
// After the loop completes the indexer + projector each ran exactly
|
|
3853
|
+
// once (the pre-allocation finalize path).
|
|
3854
|
+
expect(indexMessageNowMock).toHaveBeenCalledTimes(1);
|
|
3855
|
+
expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
|
|
3856
|
+
});
|
|
3857
|
+
|
|
3858
|
+
test("partial flushes redact secrets from text blocks before writing", async () => {
|
|
3859
|
+
mockMessageById = {
|
|
3860
|
+
id: "msg-reserve",
|
|
3861
|
+
conversationId: "test-conv",
|
|
3862
|
+
createdAt: 1234567,
|
|
3863
|
+
role: "assistant",
|
|
3864
|
+
content: "[]",
|
|
3865
|
+
metadata: null,
|
|
3866
|
+
};
|
|
3867
|
+
// A GitHub PAT-shaped token mid-stream — the redaction discipline
|
|
3868
|
+
// mirrors `handleMessageComplete`'s final flush so a refresh mid-turn
|
|
3869
|
+
// never sees plaintext credentials in the persisted row.
|
|
3870
|
+
const ghToken = "ghp_" + "a".repeat(36);
|
|
3871
|
+
const payload = "Here's the key: " + ghToken + " enjoy.";
|
|
3872
|
+
|
|
3873
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3874
|
+
await onEvent({ type: "llm_call_started" });
|
|
3875
|
+
onEvent({ type: "text_delta", text: payload });
|
|
3876
|
+
// Wait past the 250ms debounce so the partial flush lands.
|
|
3877
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
3878
|
+
await onEvent({
|
|
3879
|
+
type: "message_complete",
|
|
3880
|
+
message: {
|
|
3881
|
+
role: "assistant",
|
|
3882
|
+
content: [{ type: "text", text: payload }],
|
|
3883
|
+
},
|
|
3884
|
+
});
|
|
3885
|
+
onEvent({
|
|
3886
|
+
type: "usage",
|
|
3887
|
+
inputTokens: 10,
|
|
3888
|
+
outputTokens: 5,
|
|
3889
|
+
model: "test",
|
|
3890
|
+
providerDurationMs: 50,
|
|
3891
|
+
});
|
|
3892
|
+
return [
|
|
3893
|
+
...messages,
|
|
3894
|
+
{
|
|
3895
|
+
role: "assistant" as const,
|
|
3896
|
+
content: [{ type: "text", text: payload }] as ContentBlock[],
|
|
3897
|
+
},
|
|
3898
|
+
];
|
|
3899
|
+
};
|
|
3900
|
+
|
|
3901
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3902
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3903
|
+
|
|
3904
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(2);
|
|
3905
|
+
const partialPayload = (
|
|
3906
|
+
updateMessageContentMock.mock.calls[0] as unknown as [string, string]
|
|
3907
|
+
)[1];
|
|
3908
|
+
// The raw PAT must never appear in the persisted snapshot. The
|
|
3909
|
+
// redaction substitute is implementation-defined; the contract here
|
|
3910
|
+
// is "the literal token string is gone".
|
|
3911
|
+
expect(partialPayload).not.toContain(ghToken);
|
|
3912
|
+
});
|
|
3913
|
+
|
|
3914
|
+
test("provider-error cleanup deletes a row that has accumulated partial content", async () => {
|
|
3915
|
+
// Regression check: the pre-allocation orphan-cleanup branch
|
|
3916
|
+
// already deletes the reserved row when the LLM call exits via
|
|
3917
|
+
// `provider_error`. Partial-persist writes content to that row
|
|
3918
|
+
// mid-turn; the cleanup must still fire and the row (along with
|
|
3919
|
+
// its partial content) must still be deleted before the synthetic
|
|
3920
|
+
// error message lands.
|
|
3921
|
+
reserveMessageMock.mockImplementationOnce(async () => ({
|
|
3922
|
+
id: "msg-orphan-with-partial",
|
|
3923
|
+
}));
|
|
3924
|
+
|
|
3925
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3926
|
+
await onEvent({ type: "llm_call_started" });
|
|
3927
|
+
// A debounced delta lands a partial flush BEFORE the provider
|
|
3928
|
+
// error fires.
|
|
3929
|
+
onEvent({ type: "text_delta", text: "hello world" });
|
|
3930
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
3931
|
+
onEvent({
|
|
3932
|
+
type: "provider_error",
|
|
3933
|
+
error: new Error("upstream 500"),
|
|
3934
|
+
rawRequest: { model: "gpt-4.1", messages: [] },
|
|
3935
|
+
actualProvider: "openai",
|
|
3936
|
+
});
|
|
3937
|
+
onEvent({
|
|
3938
|
+
type: "error",
|
|
3939
|
+
error: new Error("upstream 500"),
|
|
3940
|
+
});
|
|
3941
|
+
return messages;
|
|
3942
|
+
};
|
|
3943
|
+
|
|
3944
|
+
const ctx = makeCtx({ agentLoopRun });
|
|
3945
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3946
|
+
|
|
3947
|
+
// Partial flush fired exactly once (before the provider error).
|
|
3948
|
+
// The orphan row was then deleted; the synthetic error message is
|
|
3949
|
+
// inserted separately via `addMessage` (`mock-msg-id`) and never
|
|
3950
|
+
// touched by `updateContent`.
|
|
3951
|
+
const partialFlushes = (
|
|
3952
|
+
updateMessageContentMock.mock.calls as unknown as Array<
|
|
3953
|
+
[string, string]
|
|
3954
|
+
>
|
|
3955
|
+
).filter(([id]) => id === "msg-orphan-with-partial");
|
|
3956
|
+
expect(partialFlushes).toHaveLength(1);
|
|
3957
|
+
expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
|
|
3958
|
+
const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
|
|
3959
|
+
string,
|
|
3960
|
+
];
|
|
3961
|
+
expect(deleteCall[0]).toBe("msg-orphan-with-partial");
|
|
3962
|
+
});
|
|
3963
|
+
});
|
|
3964
|
+
|
|
3047
3965
|
describe("pkbSystemReminderBlock metadata persistence", () => {
|
|
3048
3966
|
test("persists pkbSystemReminderBlock in full mode with PKB active", async () => {
|
|
3049
3967
|
const reminder = "<system_reminder>\npkb content\n</system_reminder>";
|
|
@@ -3539,21 +4457,22 @@ describe("session-agent-loop", () => {
|
|
|
3539
4457
|
const agentLoopRun: AgentLoopRun = async (
|
|
3540
4458
|
messages,
|
|
3541
4459
|
_onEvent,
|
|
3542
|
-
|
|
3543
|
-
_reqId,
|
|
3544
|
-
onCheckpoint,
|
|
4460
|
+
options,
|
|
3545
4461
|
) => {
|
|
3546
4462
|
runCount++;
|
|
3547
4463
|
if (runCount === 1) {
|
|
4464
|
+
// The loop reaches its mid-loop budget checkpoint with the raw
|
|
4465
|
+
// persistent basis as its in-loop history; the wrapped onCheckpoint
|
|
4466
|
+
// trips the gate and runs inline compaction over that basis.
|
|
3548
4467
|
mockEstimateTokens = 90_000;
|
|
3549
|
-
const decision = await onCheckpoint?.({
|
|
4468
|
+
const decision = await options?.onCheckpoint?.({
|
|
3550
4469
|
turnIndex: 0,
|
|
3551
4470
|
toolCount: 1,
|
|
3552
4471
|
hasToolUse: true,
|
|
3553
|
-
history:
|
|
4472
|
+
history: rawMidLoopBasis,
|
|
3554
4473
|
});
|
|
3555
4474
|
mockEstimateTokens = 1000;
|
|
3556
|
-
if (decision
|
|
4475
|
+
if (decision !== "continue") {
|
|
3557
4476
|
return rawMidLoopBasis;
|
|
3558
4477
|
}
|
|
3559
4478
|
}
|
|
@@ -3908,6 +4827,11 @@ describe("session-agent-loop", () => {
|
|
|
3908
4827
|
let callCount = 0;
|
|
3909
4828
|
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
3910
4829
|
callCount++;
|
|
4830
|
+
// Prime the assistant row anchor — production code emits this from
|
|
4831
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Retry branches
|
|
4832
|
+
// need this on every invocation: each agent-loop iteration reserves
|
|
4833
|
+
// its own row.
|
|
4834
|
+
await onEvent({ type: "llm_call_started" });
|
|
3911
4835
|
if (callCount === 1) {
|
|
3912
4836
|
// Trigger convergence path: error + appended assistant message so
|
|
3913
4837
|
// updatedHistory.length > preRunHistoryLength at the strip site.
|
|
@@ -3968,5 +4892,86 @@ describe("session-agent-loop", () => {
|
|
|
3968
4892
|
);
|
|
3969
4893
|
expect(stripCalls.length).toBeGreaterThanOrEqual(1);
|
|
3970
4894
|
});
|
|
4895
|
+
|
|
4896
|
+
test("strip-site marker write is non-fatal when the helper throws", async () => {
|
|
4897
|
+
setConversationHistoryStrippedAtMock.mockImplementation(() => {
|
|
4898
|
+
throw new Error("db write failed");
|
|
4899
|
+
});
|
|
4900
|
+
|
|
4901
|
+
mockReducerStepFn = (msgs: Message[]) => ({
|
|
4902
|
+
messages: msgs,
|
|
4903
|
+
tier: "forced_compaction",
|
|
4904
|
+
state: {
|
|
4905
|
+
appliedTiers: ["forced_compaction"],
|
|
4906
|
+
injectionMode: "full",
|
|
4907
|
+
exhausted: false,
|
|
4908
|
+
},
|
|
4909
|
+
estimatedTokens: 5000,
|
|
4910
|
+
});
|
|
4911
|
+
|
|
4912
|
+
let callCount = 0;
|
|
4913
|
+
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
4914
|
+
callCount++;
|
|
4915
|
+
// Prime the assistant row anchor — production code emits this from
|
|
4916
|
+
// `AgentLoop.run` just before `provider.sendMessage`. Retry branches
|
|
4917
|
+
// need this on every invocation: each agent-loop iteration reserves
|
|
4918
|
+
// its own row.
|
|
4919
|
+
await onEvent({ type: "llm_call_started" });
|
|
4920
|
+
if (callCount === 1) {
|
|
4921
|
+
onEvent({
|
|
4922
|
+
type: "error",
|
|
4923
|
+
error: new Error("context_length_exceeded"),
|
|
4924
|
+
});
|
|
4925
|
+
onEvent({
|
|
4926
|
+
type: "usage",
|
|
4927
|
+
inputTokens: 100,
|
|
4928
|
+
outputTokens: 0,
|
|
4929
|
+
model: "test-model",
|
|
4930
|
+
providerDurationMs: 50,
|
|
4931
|
+
});
|
|
4932
|
+
return [
|
|
4933
|
+
...messages,
|
|
4934
|
+
{
|
|
4935
|
+
role: "assistant" as const,
|
|
4936
|
+
content: [{ type: "text", text: "partial" }] as ContentBlock[],
|
|
4937
|
+
},
|
|
4938
|
+
];
|
|
4939
|
+
}
|
|
4940
|
+
onEvent({
|
|
4941
|
+
type: "message_complete",
|
|
4942
|
+
message: {
|
|
4943
|
+
role: "assistant",
|
|
4944
|
+
content: [{ type: "text", text: "recovered" }],
|
|
4945
|
+
},
|
|
4946
|
+
});
|
|
4947
|
+
onEvent({
|
|
4948
|
+
type: "usage",
|
|
4949
|
+
inputTokens: 50,
|
|
4950
|
+
outputTokens: 25,
|
|
4951
|
+
model: "test-model",
|
|
4952
|
+
providerDurationMs: 100,
|
|
4953
|
+
});
|
|
4954
|
+
return [
|
|
4955
|
+
...messages,
|
|
4956
|
+
{
|
|
4957
|
+
role: "assistant" as const,
|
|
4958
|
+
content: [{ type: "text", text: "recovered" }] as ContentBlock[],
|
|
4959
|
+
},
|
|
4960
|
+
];
|
|
4961
|
+
};
|
|
4962
|
+
|
|
4963
|
+
const ctx = makeCtx({
|
|
4964
|
+
agentLoopRun,
|
|
4965
|
+
contextWindowManager: {
|
|
4966
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
4967
|
+
maybeCompact: async () => ({ compacted: false }),
|
|
4968
|
+
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
4969
|
+
});
|
|
4970
|
+
|
|
4971
|
+
// Must not throw — the strip-site marker write is wrapped in try/catch.
|
|
4972
|
+
await expect(
|
|
4973
|
+
runAgentLoopImpl(ctx, "hello", "msg-1", () => {}),
|
|
4974
|
+
).resolves.toBeUndefined();
|
|
4975
|
+
});
|
|
3971
4976
|
});
|
|
3972
4977
|
});
|