@vellumai/assistant 0.8.5 → 0.8.7-dev.202606052118.34cd356
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 +21 -4
- package/bun.lock +13 -4
- package/bunfig.toml +6 -1
- package/docker-entrypoint.sh +12 -8
- package/docker-init-apt-root.sh +3 -1
- package/docker-kata-apt-env.sh +3 -1
- package/docker-kata-runtime-family.sh +12 -0
- package/docs/architecture/memory.md +1 -1
- package/docs/credential-execution-service.md +6 -6
- package/docs/plugins.md +114 -86
- package/examples/plugins/echo/README.md +13 -12
- package/examples/plugins/echo/register.ts +0 -54
- 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/server-message.ts +3 -3
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +15 -7
- package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
- package/openapi.yaml +8748 -589
- package/package.json +8 -4
- package/scripts/generate-openapi.ts +88 -54
- 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 +284 -48
- 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 +7 -5
- package/src/__tests__/agent-loop-thinking.test.ts +17 -12
- package/src/__tests__/agent-loop.test.ts +238 -422
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +7 -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-activity-metadata.test.ts +262 -0
- package/src/__tests__/annotate-risk-options.test.ts +3 -3
- package/src/__tests__/anthropic-provider.test.ts +296 -57
- package/src/__tests__/app-builder-skill-instructions.test.ts +22 -0
- package/src/__tests__/app-control-flow.test.ts +6 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/approval-cascade.test.ts +5 -11
- package/src/__tests__/approval-routes-http.test.ts +17 -16
- package/src/__tests__/assert-not-live-db.ts +79 -0
- package/src/__tests__/assistant-event-hub.test.ts +25 -0
- package/src/__tests__/assistant-event.test.ts +15 -0
- package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -27
- package/src/__tests__/assistant-stream-state.test.ts +645 -0
- package/src/__tests__/audit-log-rotation.test.ts +2 -2
- package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
- 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 +11 -8
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/btw-routes.test.ts +72 -17
- package/src/__tests__/build-persisted-content.test.ts +184 -0
- 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 +52 -23
- package/src/__tests__/channel-approvals.test.ts +4 -2
- 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__/clawhub-files.test.ts +1 -0
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +6 -17
- package/src/__tests__/compaction-trail-store.test.ts +186 -0
- package/src/__tests__/compaction.benchmark.test.ts +0 -30
- 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 +73 -23
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +10 -7
- package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +23 -34
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +477 -930
- package/src/__tests__/conversation-agent-loop.test.ts +1056 -1068
- package/src/__tests__/conversation-analysis-routes.test.ts +8 -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-clean-command.test.ts +5 -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 +12 -1
- 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 +60 -4
- package/src/__tests__/conversation-pre-run-repair.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +36 -7
- package/src/__tests__/conversation-process-callsite.test.ts +28 -30
- package/src/__tests__/conversation-provider-retry-repair.test.ts +85 -51
- package/src/__tests__/conversation-queue.test.ts +605 -455
- package/src/__tests__/conversation-routes-disk-view.test.ts +9 -20
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +35 -10
- package/src/__tests__/conversation-routes-slash-commands.test.ts +40 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +395 -313
- package/src/__tests__/conversation-runtime-workspace.test.ts +114 -36
- package/src/__tests__/conversation-skill-tools.test.ts +38 -142
- package/src/__tests__/conversation-slash-commands.test.ts +8 -42
- package/src/__tests__/conversation-slash-queue.test.ts +125 -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-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +136 -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 +140 -12
- package/src/__tests__/conversation-title-service.test.ts +136 -2
- 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 +21 -17
- package/src/__tests__/conversation-workspace-injection.test.ts +119 -23
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +39 -13
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- 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 +275 -4
- package/src/__tests__/cu-unified-flow.test.ts +26 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- 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 +16 -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 +38 -43
- 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 +99 -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-hook.test.ts +304 -0
- 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__/file-write-tool.test.ts +63 -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-image-service.test.ts +13 -0
- 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-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-outbound-http.test.ts +3 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- 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 +11 -8
- package/src/__tests__/heartbeat-disk-pressure.test.ts +3 -0
- package/src/__tests__/heartbeat-service.test.ts +4 -1
- package/src/__tests__/helpers/mock-logger.ts +26 -0
- package/src/__tests__/helpers/mock-provider.ts +110 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
- package/src/__tests__/history-repair-hook.test.ts +162 -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-app-control-routes.test.ts +1 -1
- 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 +4 -3
- 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 +140 -96
- package/src/__tests__/identity-routes.test.ts +248 -7
- package/src/__tests__/inbound-slack-persistence.test.ts +56 -75
- 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 +3 -9
- package/src/__tests__/injector-chain.test.ts +139 -275
- package/src/__tests__/injector-disk-pressure.test.ts +78 -58
- package/src/__tests__/injector-document-comments.test.ts +3 -3
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
- package/src/__tests__/injector-v3-suppression.test.ts +214 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
- package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
- package/src/__tests__/list-messages-attachments.test.ts +7 -8
- package/src/__tests__/list-messages-hidden-metadata.test.ts +131 -11
- package/src/__tests__/list-messages-page-latest.test.ts +60 -1
- package/src/__tests__/list-messages-tool-merge.test.ts +56 -6
- 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 +268 -1
- 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-hook.test.ts +297 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
- 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 +235 -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 +15 -2
- package/src/__tests__/openai-image-service.test.ts +17 -0
- package/src/__tests__/openai-provider.test.ts +105 -80
- 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 → overflow-reduction-loop.test.ts} +64 -286
- package/src/__tests__/parallel-tool.benchmark.test.ts +24 -36
- package/src/__tests__/persist-unsendable-image.test.ts +215 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +86 -13
- package/src/__tests__/pipeline-runner.test.ts +31 -43
- package/src/__tests__/pkb-autoinject.test.ts +2 -5
- package/src/__tests__/plugin-bootstrap.test.ts +72 -63
- package/src/__tests__/plugin-registry.test.ts +0 -27
- 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 +8 -173
- 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 +55 -66
- 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__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +6 -1
- package/src/__tests__/registry.test.ts +82 -76
- package/src/__tests__/relay-server.test.ts +30 -23
- package/src/__tests__/resolve-trust-class.test.ts +4 -4
- 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 +390 -0
- package/src/__tests__/schedule-routes.test.ts +683 -12
- package/src/__tests__/schedule-store.test.ts +124 -1
- package/src/__tests__/schedule-tools.test.ts +160 -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 +87 -44
- package/src/__tests__/server-history-render.test.ts +318 -2
- package/src/__tests__/shell-observability.test.ts +249 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +52 -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 +14 -6
- package/src/__tests__/subagent-disposal.test.ts +27 -8
- package/src/__tests__/subagent-fork-notifications.test.ts +25 -12
- package/src/__tests__/subagent-fork-spawn.test.ts +14 -5
- package/src/__tests__/subagent-manager-notify.test.ts +21 -11
- package/src/__tests__/subagent-notify-parent.test.ts +6 -7
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +59 -1
- 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 +112 -0
- package/src/__tests__/task-scheduler.test.ts +162 -1
- package/src/__tests__/terminal-tools.test.ts +9 -25
- 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-hook.test.ts +319 -0
- package/src/__tests__/tool-error-hook.test.ts +278 -0
- 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 +481 -15
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
- package/src/__tests__/tool-result-truncation.test.ts +1 -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__/ui-choice-copy-surfaces.test.ts +254 -0
- package/src/__tests__/ui-work-result-surface.test.ts +159 -0
- package/src/__tests__/usage-routes.test.ts +285 -1
- package/src/__tests__/user-plugin-loader.test.ts +2 -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 +56 -38
- package/src/__tests__/web-search-backend-failure.test.ts +166 -0
- 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__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +143 -31
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +695 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +125 -0
- package/src/acp/auto-install.ts +174 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +52 -11
- package/src/acp/resolve-agent.test.ts +147 -6
- package/src/acp/resolve-agent.ts +81 -7
- package/src/acp/resume-hint.ts +22 -0
- package/src/acp/session-manager.ts +492 -77
- package/src/agent/compaction-circuit.ts +98 -0
- package/src/agent/loop.ts +730 -449
- 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-thinking-delta.ts +33 -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-output-chunk.ts +45 -0
- package/src/api/events/tool-result.ts +129 -0
- package/src/api/events/tool-use-preview-start.ts +32 -0
- package/src/api/events/tool-use-start.ts +30 -0
- package/src/api/events/trace-event.ts +69 -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 +517 -3
- package/src/api/requests/dictation.ts +45 -0
- package/src/api/responses/conversation-message.ts +374 -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 +7 -10
- package/src/avatar/__tests__/avatar-manifest.test.ts +236 -0
- package/src/avatar/__tests__/avatar-store.test.ts +198 -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 +5 -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 +242 -54
- 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 +427 -65
- 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 +4 -4
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/assistant-feature-flags.ts +47 -55
- package/src/config/bundled-skills/acp/SKILL.md +64 -30
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -387
- package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
- package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
- package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
- package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +33 -24
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-skills/media-processing/services/reduce.ts +6 -9
- package/src/config/bundled-skills/messaging/SKILL.md +0 -7
- 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/bundled-tool-registry.ts +2 -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 +81 -25
- 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 +2 -211
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
- package/src/config/schemas/call-site-catalog.ts +8 -15
- package/src/config/schemas/heartbeat.ts +10 -1
- package/src/config/schemas/llm.ts +93 -4
- package/src/config/schemas/memory-lifecycle.ts +24 -0
- package/src/config/schemas/memory-v2.ts +8 -227
- package/src/config/schemas/memory-v3.ts +47 -0
- package/src/config/schemas/memory.ts +6 -1
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/schemas/services.ts +6 -2
- package/src/config/schemas/timeouts.ts +3 -1
- package/src/config/seed-inference-profiles.ts +38 -18
- package/src/config/skills.ts +13 -0
- package/src/context/compactor.ts +55 -32
- package/src/context/strip-injections.ts +128 -0
- package/src/context/token-estimator.ts +52 -5
- package/src/context/tool-result-truncation.ts +1 -66
- package/src/context/window-manager.ts +141 -26
- package/src/credential-execution/executable-discovery.ts +56 -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 +9 -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__/inference-profile-notification.test.ts +153 -0
- package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +11 -8
- 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/assistant-attachments.ts +1 -1
- package/src/daemon/config-watcher.ts +7 -1
- package/src/daemon/context-overflow-reducer.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +1146 -247
- package/src/daemon/conversation-agent-loop.ts +570 -1457
- package/src/daemon/conversation-error.ts +40 -12
- package/src/daemon/conversation-history.ts +27 -10
- package/src/daemon/conversation-launch.ts +4 -8
- package/src/daemon/conversation-lifecycle.ts +13 -42
- package/src/daemon/conversation-messaging.ts +89 -49
- package/src/daemon/conversation-notifiers.ts +7 -5
- package/src/daemon/conversation-process.ts +183 -130
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +209 -362
- package/src/daemon/conversation-skill-tools.ts +14 -30
- package/src/daemon/conversation-slash.ts +6 -25
- package/src/daemon/conversation-store.ts +15 -95
- package/src/daemon/conversation-surfaces.ts +346 -107
- package/src/daemon/conversation-tool-setup.ts +38 -77
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +143 -186
- 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 +53 -32
- package/src/daemon/first-greeting.ts +53 -13
- package/src/daemon/guardian-action-generators.ts +2 -2
- package/src/daemon/handlers/config-a2a.ts +51 -36
- package/src/daemon/handlers/config-slack-channel.ts +20 -14
- package/src/daemon/handlers/config-telegram.ts +16 -2
- package/src/daemon/handlers/conversations.ts +9 -23
- package/src/daemon/handlers/shared.ts +158 -77
- package/src/daemon/handlers/skills.ts +53 -20
- 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 +65 -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/apps.ts +1 -29
- 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 +50 -428
- 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 +4 -0
- package/src/daemon/message-types/subagents.ts +6 -0
- package/src/daemon/message-types/surfaces.ts +138 -3
- package/src/daemon/message-types/sync.ts +12 -25
- package/src/daemon/message-types/workspace.ts +3 -11
- package/src/daemon/now-scratchpad.ts +21 -0
- package/src/daemon/orphan-reaper.test.ts +210 -0
- package/src/daemon/orphan-reaper.ts +240 -0
- package/src/daemon/overflow-reduction-loop.ts +230 -0
- package/src/daemon/persist-unsendable-image.ts +117 -0
- package/src/daemon/process-message.ts +59 -58
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/server.ts +30 -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/trace-emitter.ts +6 -4
- package/src/daemon/trust-context.ts +32 -0
- package/src/daemon/wake-target-adapter.ts +24 -2
- 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 +34 -0
- package/src/heartbeat/heartbeat-run-store.ts +54 -1
- package/src/heartbeat/heartbeat-service.ts +105 -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/feed-types.ts +36 -221
- package/src/home/feed-writer.ts +146 -7
- package/src/home/home-greeting-cache.ts +24 -1
- package/src/home/suggested-prompts.ts +27 -145
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
- package/src/ipc/__tests__/email-ipc.test.ts +0 -9
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/gateway-client.test.ts +6 -3
- package/src/ipc/gateway-client.ts +3 -3
- 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 +34 -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 +29 -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/media/gemini-image-service.ts +15 -0
- package/src/media/openai-image-service.ts +14 -0
- package/src/media/types.ts +34 -0
- 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 +82 -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/auth-fallback-events-store.ts +94 -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/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/conversation-title-service.ts +65 -41
- package/src/memory/db-connection.ts +29 -19
- package/src/memory/db-init.ts +18 -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-registry.test.ts +119 -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 +106 -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 +45 -34
- 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 +36 -1
- package/src/memory/jobs-worker.ts +93 -43
- 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 +234 -50
- 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/222-strip-placeholder-sentinels-from-messages.ts +6 -5
- 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/270-schedule-source-conversation.ts +13 -0
- package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/__tests__/267-llm-usage-events-add-assistant-version.test.ts +117 -0
- package/src/memory/migrations/index.ts +9 -0
- package/src/memory/pkb/autoinject.ts +61 -0
- package/src/memory/pkb/context.ts +50 -0
- package/src/memory/pkb/types.ts +14 -0
- package/src/memory/schedule-attribution-sql.ts +104 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/schema/conversations.ts +9 -1
- package/src/memory/schema/inference.ts +0 -1
- package/src/memory/schema/infrastructure.ts +27 -0
- package/src/memory/usage-grouped-buckets.ts +6 -1
- package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +125 -1
- 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 +99 -10
- 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/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 +23 -8
- package/src/notifications/decision-engine.ts +16 -16
- 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/home-feed-side-effect.ts +12 -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 +46 -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/constants.ts +4 -0
- package/src/plugin-api/index.ts +10 -1
- package/src/plugin-api/types.ts +176 -4
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/plugins/defaults/compaction/package.json +15 -0
- package/src/plugins/defaults/compaction/register.ts +24 -0
- package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
- package/src/plugins/defaults/empty-response/package.json +15 -0
- package/src/plugins/defaults/empty-response/register.ts +23 -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 +22 -49
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
- package/src/plugins/defaults/{injectors.ts → memory-retrieval/injectors.ts} +297 -120
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +242 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +118 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +39 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +36 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +37 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +219 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +330 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +288 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +107 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +436 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +274 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +61 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +332 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +179 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +470 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +432 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +168 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +192 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +54 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +106 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +44 -0
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +268 -0
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +124 -0
- package/src/plugins/defaults/memory-v3-shadow/core.ts +26 -0
- package/src/plugins/defaults/memory-v3-shadow/data/README.md +84 -0
- package/src/plugins/defaults/memory-v3-shadow/data/assignments.json +5 -0
- package/src/plugins/defaults/memory-v3-shadow/data/core.json +1 -0
- package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-a/topic-x.md +9 -0
- package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-a/topic-y.md +9 -0
- package/src/plugins/defaults/memory-v3-shadow/data/leaves/domain-b/topic-z.md +9 -0
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +314 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +126 -0
- package/src/plugins/defaults/memory-v3-shadow/package.json +15 -0
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +34 -0
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +523 -0
- package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +32 -0
- package/src/plugins/defaults/memory-v3-shadow/router.ts +190 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +84 -0
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +226 -0
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +349 -0
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +209 -0
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +174 -0
- package/src/plugins/defaults/memory-v3-shadow/types.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +88 -0
- package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -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/tool-error/hooks/post-tool-use.ts +118 -0
- package/src/plugins/defaults/tool-error/package.json +15 -0
- package/src/plugins/defaults/tool-error/register.ts +23 -0
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
- package/src/plugins/defaults/tool-result-truncate/package.json +15 -0
- package/src/plugins/defaults/tool-result-truncate/register.ts +24 -0
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +132 -0
- package/src/plugins/external-plugin-loader.ts +2 -2
- package/src/plugins/pipeline.ts +8 -35
- package/src/plugins/registry.ts +8 -25
- package/src/plugins/types.ts +62 -677
- package/src/plugins/user-loader.ts +4 -3
- package/src/proactive-artifact/aux-message-injector.ts +4 -5
- package/src/proactive-artifact/job.test.ts +29 -21
- package/src/proactive-artifact/job.ts +3 -1
- package/src/prompts/__tests__/system-prompt.test.ts +46 -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-ACTIVATION-RAIL.md +64 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +2 -2
- package/src/prompts/templates/BOOTSTRAP.md +12 -12
- package/src/prompts/templates/IDENTITY.md +0 -2
- package/src/prompts/templates/system-sections.ts +21 -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 +61 -34
- 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 +269 -7
- package/src/providers/openai/chat-completions-provider.ts +155 -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 +15 -14
- package/src/providers/placeholder-sentinels.ts +35 -0
- 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 +142 -32
- package/src/runtime/__tests__/background-job-runner.test.ts +2 -3
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/access-request-helper.ts +1 -0
- package/src/runtime/agent-wake.ts +95 -23
- package/src/runtime/assistant-event-hub.ts +38 -8
- package/src/runtime/assistant-stream-state.ts +368 -0
- 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 +4 -15
- 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/http-router.ts +35 -43
- package/src/runtime/http-types.ts +23 -71
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/invite-instruction-generator.ts +3 -3
- package/src/runtime/migrations/vbundle-builder.ts +3 -2
- package/src/runtime/pending-interactions.ts +3 -2
- package/src/runtime/routes/__tests__/acp-routes.test.ts +253 -55
- 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__/consolidation-routes.test.ts +265 -2
- 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 +129 -1
- 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-routes.test.ts +6 -2
- 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-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +297 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
- package/src/runtime/routes/__tests__/tts-routes.test.ts +9 -5
- package/src/runtime/routes/acp-routes-list.test.ts +3 -0
- package/src/runtime/routes/acp-routes.test.ts +186 -100
- package/src/runtime/routes/acp-routes.ts +110 -35
- package/src/runtime/routes/app-management-routes.ts +175 -106
- package/src/runtime/routes/app-routes.ts +38 -20
- package/src/runtime/routes/approval-routes.ts +17 -5
- package/src/runtime/routes/attachment-routes.ts +51 -16
- 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 +264 -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 +6 -1
- package/src/runtime/routes/browser-tabs-routes.ts +11 -10
- package/src/runtime/routes/btw-routes.ts +37 -27
- 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 +133 -25
- 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 +150 -51
- package/src/runtime/routes/conversation-routes.ts +615 -327
- package/src/runtime/routes/conversation-starter-routes.ts +35 -20
- package/src/runtime/routes/conversations-import-routes.ts +48 -13
- 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 +35 -11
- package/src/runtime/routes/domain-routes.ts +98 -51
- package/src/runtime/routes/email-routes.ts +33 -0
- package/src/runtime/routes/epoch-millis-range.ts +34 -0
- package/src/runtime/routes/events-routes.ts +107 -8
- package/src/runtime/routes/filing-routes.ts +9 -4
- package/src/runtime/routes/gateway-log-routes.ts +31 -4
- 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 +57 -21
- 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 +6 -1
- 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 +6 -1
- 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 -39
- package/src/runtime/routes/identity-routes.ts +257 -20
- package/src/runtime/routes/image-generation-routes.ts +45 -2
- package/src/runtime/routes/inbound-message-handler.ts +16 -12
- 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 +4 -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 +32 -7
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
- package/src/runtime/routes/integrations/slack/channel.ts +23 -3
- package/src/runtime/routes/integrations/slack/share.ts +36 -8
- package/src/runtime/routes/integrations/telegram.ts +34 -9
- package/src/runtime/routes/integrations/twilio.ts +77 -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-telemetry-routes.ts +88 -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 +36 -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 +105 -44
- package/src/runtime/routes/memory-v3-routes.ts +313 -257
- 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 +99 -23
- 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 +79 -15
- package/src/runtime/routes/platform-routes.ts +102 -5
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +9 -6
- 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 +2 -2
- package/src/runtime/routes/playground/helpers.ts +1 -2
- 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 +10 -0
- package/src/runtime/routes/sanity-routes.ts +9 -2
- package/src/runtime/routes/schedule-routes.ts +288 -88
- package/src/runtime/routes/secret-routes.ts +31 -6
- 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 +166 -73
- 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 +66 -0
- package/src/runtime/routes/task-routes.ts +37 -0
- package/src/runtime/routes/telemetry-routes.ts +9 -0
- package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
- package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
- package/src/runtime/routes/trace-event-routes.ts +42 -1
- package/src/runtime/routes/trust-rules-routes.ts +31 -2
- package/src/runtime/routes/tts-routes.ts +48 -6
- package/src/runtime/routes/types.ts +83 -16
- 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 +118 -42
- 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 +50 -2
- package/src/runtime/routes/wipe-conversation-routes.ts +8 -0
- package/src/runtime/routes/work-items-routes.ts +49 -23
- 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 +124 -9
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +10 -4
- package/src/runtime/services/analyze-conversation.ts +5 -8
- 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 +36 -2
- package/src/schedule/schedule-usage-store.ts +83 -0
- package/src/schedule/scheduler.ts +126 -21
- 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/cancel.ts +2 -4
- package/src/signals/user-message.ts +10 -16
- package/src/skills/catalog-files.ts +4 -1
- package/src/skills/catalog-install.ts +3 -0
- package/src/skills/categories-cache.ts +118 -0
- package/src/skills/clawhub-files.ts +1 -0
- package/src/skills/skillssh-files.ts +1 -0
- package/src/skills/validate-input.ts +177 -0
- package/src/subagent/manager.ts +33 -24
- package/src/subagent/types.ts +6 -0
- package/src/tasks/tool-sanitizer.ts +2 -2
- package/src/telemetry/types.ts +55 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +250 -4
- package/src/telemetry/usage-telemetry-reporter.ts +88 -2
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +7 -1
- package/src/tools/acp/spawn.test.ts +198 -93
- package/src/tools/acp/spawn.ts +32 -70
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/definitions.ts +42 -24
- package/src/tools/apps/executors.ts +13 -8
- 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 -107
- 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 +69 -32
- 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-metadata.test.ts +7 -1
- package/src/tools/network/__tests__/web-search.test.ts +222 -6
- 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-error.test.ts +248 -0
- package/src/tools/network/web-search-error.ts +267 -0
- package/src/tools/network/web-search.ts +414 -97
- package/src/tools/policy-context.ts +3 -1
- package/src/tools/registry.ts +184 -118
- package/src/tools/schedule/create.ts +14 -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 +2 -1
- package/src/tools/system/avatar-generator.ts +13 -22
- package/src/tools/system/request-permission.ts +30 -27
- package/src/tools/terminal/safe-env.ts +10 -1
- 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 +196 -95
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
- package/src/tts/provider-catalog.ts +76 -1
- 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/mutex.ts +47 -0
- package/src/util/platform.ts +15 -12
- package/src/work-items/work-item-runner.ts +7 -2
- package/src/workspace/git-service.ts +1 -42
- 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/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
- package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -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 +16 -0
- package/src/workspace/provider-commit-message-generator.ts +15 -17
- package/tsconfig.json +4 -1
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -262
- package/src/__tests__/empty-response-pipeline.test.ts +0 -301
- package/src/__tests__/history-repair-pipeline.test.ts +0 -396
- package/src/__tests__/llm-call-pipeline.test.ts +0 -281
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
- package/src/__tests__/persistence-pipeline.test.ts +0 -376
- package/src/__tests__/title-generate-pipeline.test.ts +0 -211
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -481
- package/src/__tests__/tool-error-pipeline.test.ts +0 -241
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -344
- 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/bootstrap-turn-cleanup.ts +0 -45
- package/src/daemon/message-types/disk-pressure.ts +0 -9
- package/src/email/feature-gate.ts +0 -23
- package/src/gallery/default-gallery.ts +0 -1359
- package/src/gallery/gallery-manifest.ts +0 -28
- 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/types.ts +0 -65
- 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/llm-call.ts +0 -77
- package/src/plugins/defaults/memory-retrieval.ts +0 -219
- package/src/plugins/defaults/overflow-reduce.ts +0 -185
- 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-execute.ts +0 -87
- package/src/plugins/defaults/tool-result-truncate.ts +0 -84
- package/src/skills/category-inference.ts +0 -111
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import {
|
|
3
|
+
afterAll,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
mock,
|
|
8
|
+
spyOn,
|
|
9
|
+
test,
|
|
10
|
+
} from "bun:test";
|
|
11
|
+
|
|
12
|
+
import type { LoopToolExecutor } from "../agent/loop.js";
|
|
9
13
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
10
14
|
import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
|
|
11
|
-
import type {
|
|
15
|
+
import type { Message, Provider, ToolDefinition } from "../providers/types.js";
|
|
12
16
|
|
|
13
17
|
const conversationCrudRealSnapshot = {
|
|
14
18
|
...(createRequire(import.meta.url)(
|
|
@@ -64,6 +68,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
64
68
|
memory: { retrieval: { scratchpadInjection: { enabled: true } } },
|
|
65
69
|
ui: mockUiConfig,
|
|
66
70
|
compaction: { enabled: true, autoThreshold: 0.7 },
|
|
71
|
+
conversations: { skipAutoRetitling: true },
|
|
67
72
|
}),
|
|
68
73
|
loadRawConfig: () => ({}),
|
|
69
74
|
saveRawConfig: () => {},
|
|
@@ -74,17 +79,20 @@ mock.module("../config/loader.js", () => ({
|
|
|
74
79
|
|
|
75
80
|
// Token estimator returns a small value by default (well within budget)
|
|
76
81
|
// so preflight does not trigger unless the test overrides it. Both the
|
|
77
|
-
// calibrated entry point (`estimatePromptTokens`,
|
|
78
|
-
// path) and the raw entry point
|
|
79
|
-
//
|
|
82
|
+
// calibrated entry point (`estimatePromptTokens`, which backs the preflight
|
|
83
|
+
// overflow gate and the convergence path) and the raw entry point
|
|
84
|
+
// (`estimatePromptTokensRaw`, used by the pre-send calibration capture) are
|
|
80
85
|
// stubbed so either call site can drive the test.
|
|
81
86
|
let mockEstimateTokens = 1000;
|
|
82
87
|
mock.module("../context/token-estimator.js", () => ({
|
|
83
88
|
estimatePromptTokens: () => mockEstimateTokens,
|
|
84
89
|
estimatePromptTokensRaw: () => mockEstimateTokens,
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
90
|
+
// The preflight overflow gate calls this calibrated wrapper directly, so it
|
|
91
|
+
// must honor `mockEstimateTokens` too rather than fall through to the real
|
|
92
|
+
// implementation.
|
|
93
|
+
estimatePromptTokensWithTools: () => mockEstimateTokens,
|
|
94
|
+
// Pass-through: `estimatePromptTokensWithTools` computes `toolTokenBudget`
|
|
95
|
+
// via this helper. Return 0 so the mocked estimate is not perturbed.
|
|
88
96
|
estimateToolsTokens: () => 0,
|
|
89
97
|
}));
|
|
90
98
|
|
|
@@ -176,6 +184,12 @@ let mockConversationRow: Record<string, unknown> = {
|
|
|
176
184
|
title: null,
|
|
177
185
|
};
|
|
178
186
|
let mockMessageById: Record<string, unknown> | null = null;
|
|
187
|
+
const deleteMessageByIdMock = mock(() => ({
|
|
188
|
+
segmentIds: [],
|
|
189
|
+
deletedSummaryIds: [],
|
|
190
|
+
}));
|
|
191
|
+
const reserveMessageMock = mock(async () => ({ id: "msg-reserve" }));
|
|
192
|
+
const updateMessageContentMock = mock(() => {});
|
|
179
193
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
180
194
|
setConversationOriginChannelIfUnset: () => {},
|
|
181
195
|
updateConversationUsage: () => {},
|
|
@@ -189,7 +203,7 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
189
203
|
}),
|
|
190
204
|
getConversationOriginInterface: () => null,
|
|
191
205
|
addMessage: () => ({ id: "mock-msg-id" }),
|
|
192
|
-
deleteMessageById:
|
|
206
|
+
deleteMessageById: deleteMessageByIdMock,
|
|
193
207
|
updateConversationContextWindow: () => {},
|
|
194
208
|
updateConversationSlackContextWatermark:
|
|
195
209
|
updateConversationSlackContextWatermarkMock,
|
|
@@ -197,6 +211,36 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
197
211
|
getConversationOriginChannel: () => null,
|
|
198
212
|
getMessageById: () => mockMessageById,
|
|
199
213
|
getLastUserTimestampBefore: () => 0,
|
|
214
|
+
reserveMessage: reserveMessageMock,
|
|
215
|
+
updateMessageContent: updateMessageContentMock,
|
|
216
|
+
// The real schema is a Zod object; tests don't exercise validation,
|
|
217
|
+
// so a passthrough is sufficient — the production code at
|
|
218
|
+
// `handleMessageComplete` only branches on `success` and reads two
|
|
219
|
+
// fields off `data`. `safeParse` of an empty object satisfies the
|
|
220
|
+
// schema (every field is optional).
|
|
221
|
+
messageMetadataSchema: {
|
|
222
|
+
safeParse: (input: unknown) => ({ success: true, data: input ?? {} }),
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
// The B3 indexing-restoration path imports `indexMessageNow` from
|
|
227
|
+
// `../memory/indexer.js` and `projectAssistantMessage` from
|
|
228
|
+
// `../memory/conversation-attention-store.js`; without these stubs the
|
|
229
|
+
// real modules would try to open a SQLite DB and read a real config.
|
|
230
|
+
const indexMessageNowMock = mock(async () => ({
|
|
231
|
+
indexedSegments: 0,
|
|
232
|
+
enqueuedJobs: 0,
|
|
233
|
+
}));
|
|
234
|
+
const projectAssistantMessageMock = mock(() => false);
|
|
235
|
+
const publishSyncInvalidationMock = mock(async () => {});
|
|
236
|
+
mock.module("../memory/indexer.js", () => ({
|
|
237
|
+
indexMessageNow: indexMessageNowMock,
|
|
238
|
+
}));
|
|
239
|
+
mock.module("../memory/conversation-attention-store.js", () => ({
|
|
240
|
+
projectAssistantMessage: projectAssistantMessageMock,
|
|
241
|
+
}));
|
|
242
|
+
mock.module("../runtime/sync/sync-publisher.js", () => ({
|
|
243
|
+
publishSyncInvalidation: publishSyncInvalidationMock,
|
|
200
244
|
}));
|
|
201
245
|
|
|
202
246
|
afterAll(() => {
|
|
@@ -272,12 +316,14 @@ const buildUnifiedTurnContextBlockMock = mock(
|
|
|
272
316
|
(options: Record<string, unknown>) =>
|
|
273
317
|
`<turn_context>\ncurrent_time: ${String(options.timestamp)}\n</turn_context>`,
|
|
274
318
|
);
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
319
|
+
const defaultApplyRuntimeInjectionsImpl = async (
|
|
320
|
+
msgs: Message[],
|
|
321
|
+
_options?: unknown,
|
|
322
|
+
) => ({
|
|
323
|
+
messages: msgs,
|
|
324
|
+
blocks: { ...mockInjectionBlocks },
|
|
325
|
+
});
|
|
326
|
+
const applyRuntimeInjectionsMock = mock(defaultApplyRuntimeInjectionsImpl);
|
|
281
327
|
let mockSlackChronologicalContext: {
|
|
282
328
|
renderedMessages: Array<{
|
|
283
329
|
message: Message;
|
|
@@ -316,15 +362,6 @@ mock.module("../daemon/conversation-runtime-assembly.js", () => ({
|
|
|
316
362
|
applyRuntimeInjections: applyRuntimeInjectionsMock,
|
|
317
363
|
buildUnifiedTurnContextBlock: buildUnifiedTurnContextBlockMock,
|
|
318
364
|
stripInjectionsForCompaction: (msgs: Message[]) => msgs,
|
|
319
|
-
findLastInjectedNowContent: () => null,
|
|
320
|
-
readNowScratchpad: () => null,
|
|
321
|
-
readPkbContext: () => null,
|
|
322
|
-
getPkbAutoInjectList: () => [
|
|
323
|
-
"INDEX.md",
|
|
324
|
-
"essentials.md",
|
|
325
|
-
"threads.md",
|
|
326
|
-
"buffer.md",
|
|
327
|
-
],
|
|
328
365
|
isSlackChannelConversation: () => false,
|
|
329
366
|
getSlackCompactionWatermarkForPrefix:
|
|
330
367
|
getSlackCompactionWatermarkForPrefixMock,
|
|
@@ -371,7 +408,7 @@ mock.module("../daemon/date-context.js", () => ({
|
|
|
371
408
|
resolveTurnTimezoneContext: resolveTurnTimezoneContextMock,
|
|
372
409
|
}));
|
|
373
410
|
|
|
374
|
-
mock.module("../
|
|
411
|
+
mock.module("../plugins/defaults/history-repair/terminal.js", () => ({
|
|
375
412
|
repairHistory: (msgs: Message[]) => ({
|
|
376
413
|
messages: msgs,
|
|
377
414
|
stats: {
|
|
@@ -501,56 +538,78 @@ mock.module("../proactive-artifact/index.js", () => ({
|
|
|
501
538
|
|
|
502
539
|
// ── Imports (after mocks) ────────────────────────────────────────────
|
|
503
540
|
|
|
541
|
+
import { AgentLoop } from "../agent/loop.js";
|
|
504
542
|
import {
|
|
505
543
|
type AgentLoopConversationContext,
|
|
506
544
|
applyCompactionResult,
|
|
507
545
|
runAgentLoopImpl,
|
|
508
546
|
} from "../daemon/conversation-agent-loop.js";
|
|
547
|
+
import {
|
|
548
|
+
createMockProvider,
|
|
549
|
+
type ScriptedResponse,
|
|
550
|
+
textResponse,
|
|
551
|
+
toolUseResponse,
|
|
552
|
+
} from "./helpers/mock-provider.js";
|
|
509
553
|
|
|
510
554
|
// ── Test helpers ─────────────────────────────────────────────────────
|
|
511
555
|
|
|
512
|
-
type AgentLoopRun = (
|
|
513
|
-
messages: Message[],
|
|
514
|
-
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
515
|
-
signal?: AbortSignal,
|
|
516
|
-
requestId?: string,
|
|
517
|
-
onCheckpoint?: (
|
|
518
|
-
checkpoint: CheckpointInfo,
|
|
519
|
-
) => CheckpointDecision | Promise<CheckpointDecision>,
|
|
520
|
-
) => Promise<Message[]>;
|
|
521
|
-
|
|
522
556
|
function makeCtx(
|
|
523
557
|
overrides?: Partial<AgentLoopConversationContext> & {
|
|
524
|
-
|
|
558
|
+
providerResponses?: ScriptedResponse[];
|
|
559
|
+
loopProvider?: Provider;
|
|
560
|
+
loopTools?: ToolDefinition[];
|
|
561
|
+
toolExecutor?: LoopToolExecutor;
|
|
525
562
|
},
|
|
526
563
|
): AgentLoopConversationContext {
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
564
|
+
const {
|
|
565
|
+
providerResponses,
|
|
566
|
+
loopProvider,
|
|
567
|
+
loopTools,
|
|
568
|
+
toolExecutor,
|
|
569
|
+
...ctxOverrides
|
|
570
|
+
} = overrides ?? {};
|
|
571
|
+
const conversationId = ctxOverrides.conversationId ?? "test-conv";
|
|
572
|
+
let processing = true;
|
|
573
|
+
|
|
574
|
+
// Drive the real `AgentLoop` against a scripted provider, mocking only the
|
|
575
|
+
// provider HTTP boundary. The loop owns its mid-loop budget gate, inline
|
|
576
|
+
// compaction, and event emission, so these orchestrator tests exercise the
|
|
577
|
+
// real escalation/persistence path.
|
|
578
|
+
//
|
|
579
|
+
// Name the loop's provider after `ctx.provider` so the two stay in sync,
|
|
580
|
+
// mirroring production where the orchestrator hands the same provider to
|
|
581
|
+
// the loop. The loop stamps this name onto `usage.actualProvider` whenever
|
|
582
|
+
// a response omits its own, which is what the request-log fallback reads.
|
|
583
|
+
// Tests that need to introspect provider calls (or sequence a rejection)
|
|
584
|
+
// build their own `loopProvider` via `createMockProvider`.
|
|
585
|
+
const loopProviderName =
|
|
586
|
+
(ctxOverrides.provider as { name?: string } | undefined)?.name ??
|
|
587
|
+
"mock-provider";
|
|
588
|
+
const provider =
|
|
589
|
+
loopProvider ??
|
|
590
|
+
createMockProvider(
|
|
591
|
+
providerResponses ?? [textResponse("response")],
|
|
592
|
+
loopProviderName,
|
|
593
|
+
).provider;
|
|
594
|
+
const agentLoop = new AgentLoop(provider, "system prompt", {
|
|
595
|
+
conversationId,
|
|
596
|
+
tools: loopTools ?? [],
|
|
597
|
+
toolExecutor,
|
|
598
|
+
});
|
|
536
599
|
|
|
537
600
|
return {
|
|
538
601
|
conversationId: "test-conv",
|
|
539
602
|
messages: [
|
|
540
603
|
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
541
604
|
] as Message[],
|
|
542
|
-
|
|
605
|
+
isProcessing: () => processing,
|
|
606
|
+
setProcessing: (value: boolean) => {
|
|
607
|
+
processing = value;
|
|
608
|
+
},
|
|
543
609
|
abortController: new AbortController(),
|
|
544
610
|
currentRequestId: "test-req",
|
|
545
611
|
|
|
546
|
-
agentLoop
|
|
547
|
-
run: agentLoopRun,
|
|
548
|
-
getToolTokenBudget: () => 0,
|
|
549
|
-
getResolvedTools: () => [],
|
|
550
|
-
// Tests here don't exercise calibration; returning undefined makes
|
|
551
|
-
// the estimator use the per-provider aggregate key.
|
|
552
|
-
getActiveModel: () => undefined,
|
|
553
|
-
} as unknown as AgentLoopConversationContext["agentLoop"],
|
|
612
|
+
agentLoop,
|
|
554
613
|
provider: {
|
|
555
614
|
name: "mock-provider",
|
|
556
615
|
sendMessage: async () => ({
|
|
@@ -579,8 +638,6 @@ function makeCtx(
|
|
|
579
638
|
currentTurnSurfaces: [],
|
|
580
639
|
|
|
581
640
|
workingDir: "/tmp",
|
|
582
|
-
workspaceTopLevelContext: null,
|
|
583
|
-
workspaceTopLevelDirty: false,
|
|
584
641
|
channelCapabilities: undefined,
|
|
585
642
|
commandIntent: undefined,
|
|
586
643
|
trustContext: undefined,
|
|
@@ -617,7 +674,6 @@ function makeCtx(
|
|
|
617
674
|
getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
|
|
618
675
|
commitTurnChanges: async () => {},
|
|
619
676
|
|
|
620
|
-
refreshWorkspaceTopLevelContextIfNeeded: () => {},
|
|
621
677
|
markWorkspaceTopLevelDirty: () => {},
|
|
622
678
|
emitActivityState: () => {},
|
|
623
679
|
getQueueDepth: () => 0,
|
|
@@ -643,9 +699,10 @@ function makeCtx(
|
|
|
643
699
|
injectedTokens: 0,
|
|
644
700
|
}),
|
|
645
701
|
retrackCachedNodes: () => {},
|
|
702
|
+
recordPkbQueryVectors: () => {},
|
|
646
703
|
} as unknown as AgentLoopConversationContext["graphMemory"],
|
|
647
704
|
|
|
648
|
-
...
|
|
705
|
+
...ctxOverrides,
|
|
649
706
|
} as AgentLoopConversationContext;
|
|
650
707
|
}
|
|
651
708
|
|
|
@@ -686,17 +743,26 @@ beforeEach(() => {
|
|
|
686
743
|
setConversationHistoryStrippedAtMock.mockClear();
|
|
687
744
|
setConversationHistoryStrippedAtMock.mockImplementation(() => {});
|
|
688
745
|
applyRuntimeInjectionsMock.mockClear();
|
|
746
|
+
applyRuntimeInjectionsMock.mockImplementation(
|
|
747
|
+
defaultApplyRuntimeInjectionsImpl,
|
|
748
|
+
);
|
|
689
749
|
buildUnifiedTurnContextBlockMock.mockClear();
|
|
690
750
|
resolveTurnTimezoneContextMock.mockClear();
|
|
691
751
|
formatTurnTimestampMock.mockClear();
|
|
692
752
|
mockSlackChronologicalContext = null;
|
|
693
753
|
loadSlackChronologicalContextMock.mockClear();
|
|
694
754
|
getSlackCompactionWatermarkForPrefixMock.mockClear();
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
755
|
+
deleteMessageByIdMock.mockClear();
|
|
756
|
+
reserveMessageMock.mockClear();
|
|
757
|
+
updateMessageContentMock.mockClear();
|
|
758
|
+
indexMessageNowMock.mockClear();
|
|
759
|
+
projectAssistantMessageMock.mockClear();
|
|
760
|
+
publishSyncInvalidationMock.mockClear();
|
|
761
|
+
mockMessageById = null;
|
|
762
|
+
// The compaction pipeline runs through the plugin registry; reset and
|
|
763
|
+
// re-register every default so it dispatches to middleware backed by the
|
|
764
|
+
// mocked collaborators these tests install (`syncMessageToDisk`, etc.)
|
|
765
|
+
// instead of hitting the bare terminal.
|
|
700
766
|
resetPluginRegistryAndRegisterDefaults();
|
|
701
767
|
});
|
|
702
768
|
|
|
@@ -762,7 +828,7 @@ describe("session-agent-loop", () => {
|
|
|
762
828
|
});
|
|
763
829
|
|
|
764
830
|
describe("proactive artifact trigger", () => {
|
|
765
|
-
test("
|
|
831
|
+
test("does not start proactive artifact jobs after foreground user turns", async () => {
|
|
766
832
|
mockConversationRow = {
|
|
767
833
|
...mockConversationRow,
|
|
768
834
|
id: "test-conv",
|
|
@@ -776,57 +842,28 @@ describe("session-agent-loop", () => {
|
|
|
776
842
|
mockHasProactiveArtifactCompleted = false;
|
|
777
843
|
mockTryClaimProactiveArtifactTrigger = true;
|
|
778
844
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
onEvent,
|
|
782
|
-
_signal,
|
|
783
|
-
_requestId,
|
|
784
|
-
onCheckpoint,
|
|
785
|
-
) => {
|
|
786
|
-
await onEvent({
|
|
787
|
-
type: "message_complete",
|
|
788
|
-
message: {
|
|
789
|
-
role: "assistant",
|
|
790
|
-
content: [{ type: "text", text: "I'll build that app." }],
|
|
791
|
-
},
|
|
792
|
-
});
|
|
793
|
-
await onEvent({
|
|
794
|
-
type: "tool_use",
|
|
795
|
-
id: "tool-1",
|
|
796
|
-
name: "app_create",
|
|
797
|
-
input: { name: "Flow" },
|
|
798
|
-
});
|
|
799
|
-
await onEvent({
|
|
800
|
-
type: "tool_result",
|
|
801
|
-
toolUseId: "tool-1",
|
|
802
|
-
content: "{}",
|
|
803
|
-
isError: false,
|
|
804
|
-
});
|
|
805
|
-
await onCheckpoint?.({
|
|
806
|
-
turnIndex: 0,
|
|
807
|
-
toolCount: 1,
|
|
808
|
-
hasToolUse: true,
|
|
809
|
-
history: messages,
|
|
810
|
-
});
|
|
811
|
-
await onEvent({
|
|
812
|
-
type: "message_complete",
|
|
813
|
-
message: {
|
|
814
|
-
role: "assistant",
|
|
815
|
-
content: [{ type: "text", text: "Done." }],
|
|
816
|
-
},
|
|
817
|
-
});
|
|
818
|
-
return [
|
|
819
|
-
...messages,
|
|
820
|
-
{
|
|
821
|
-
role: "assistant" as const,
|
|
822
|
-
content: [{ type: "text" as const, text: "Done." }],
|
|
823
|
-
},
|
|
824
|
-
];
|
|
825
|
-
};
|
|
826
|
-
|
|
845
|
+
// A two-call agent turn: the model invokes `app_create`, then wraps up
|
|
846
|
+
// with a final text reply.
|
|
827
847
|
const ctx = makeCtx({
|
|
828
848
|
conversationId: "test-conv",
|
|
829
|
-
|
|
849
|
+
providerResponses: [
|
|
850
|
+
{
|
|
851
|
+
content: [
|
|
852
|
+
{ type: "text", text: "I'll build that app." },
|
|
853
|
+
{
|
|
854
|
+
type: "tool_use",
|
|
855
|
+
id: "tool-1",
|
|
856
|
+
name: "app_create",
|
|
857
|
+
input: { name: "Flow" },
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
model: "mock-model",
|
|
861
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
862
|
+
stopReason: "tool_use",
|
|
863
|
+
},
|
|
864
|
+
textResponse("Done."),
|
|
865
|
+
],
|
|
866
|
+
toolExecutor: async () => ({ content: "{}", isError: false }),
|
|
830
867
|
});
|
|
831
868
|
await runAgentLoopImpl(
|
|
832
869
|
ctx,
|
|
@@ -839,16 +876,28 @@ describe("session-agent-loop", () => {
|
|
|
839
876
|
);
|
|
840
877
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
841
878
|
|
|
842
|
-
expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(
|
|
843
|
-
expect(runProactiveArtifactJobMock.mock.calls[0]?.[0]).toMatchObject({
|
|
844
|
-
conversationId: "test-conv",
|
|
845
|
-
suppressAppBuild: true,
|
|
846
|
-
});
|
|
879
|
+
expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
|
|
847
880
|
});
|
|
848
881
|
});
|
|
849
882
|
|
|
850
883
|
describe("disk pressure injection context", () => {
|
|
851
|
-
|
|
884
|
+
// The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
|
|
885
|
+
// turn (the disk-pressure-warning injector reads it via the per-conversation
|
|
886
|
+
// registry) and resets it in the turn-end cleanup path. Snapshot the flag at
|
|
887
|
+
// each `applyRuntimeInjections` call so assertions observe its value while
|
|
888
|
+
// injection runs, not the post-turn reset.
|
|
889
|
+
function captureCleanupFlagDuringInjection(ctx: {
|
|
890
|
+
diskPressureCleanupModeActive?: boolean;
|
|
891
|
+
}): () => Array<boolean | undefined> {
|
|
892
|
+
const observed: Array<boolean | undefined> = [];
|
|
893
|
+
applyRuntimeInjectionsMock.mockImplementation(async (msgs: Message[]) => {
|
|
894
|
+
observed.push(ctx.diskPressureCleanupModeActive);
|
|
895
|
+
return { messages: msgs, blocks: { ...mockInjectionBlocks } };
|
|
896
|
+
});
|
|
897
|
+
return () => observed;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
test("sets the cleanup-mode flag on the conversation for cleanup-mode turns", async () => {
|
|
852
901
|
mockDiskPressureDecision = {
|
|
853
902
|
action: "allow-cleanup-mode",
|
|
854
903
|
reason: "guardian",
|
|
@@ -871,6 +920,7 @@ describe("session-agent-loop", () => {
|
|
|
871
920
|
trustClass: "guardian",
|
|
872
921
|
} as AgentLoopConversationContext["trustContext"],
|
|
873
922
|
});
|
|
923
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
874
924
|
|
|
875
925
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
876
926
|
|
|
@@ -889,21 +939,16 @@ describe("session-agent-loop", () => {
|
|
|
889
939
|
},
|
|
890
940
|
}),
|
|
891
941
|
);
|
|
892
|
-
|
|
893
|
-
.calls[0]![1] as {
|
|
894
|
-
diskPressureContext?: { cleanupModeActive: boolean } | null;
|
|
895
|
-
};
|
|
896
|
-
expect(firstInjectionOptions.diskPressureContext).toEqual({
|
|
897
|
-
cleanupModeActive: true,
|
|
898
|
-
});
|
|
942
|
+
expect(cleanupFlagDuringInjection()).toEqual([true]);
|
|
899
943
|
});
|
|
900
944
|
|
|
901
|
-
test("
|
|
945
|
+
test("sets the cleanup-mode flag on the conversation for local-owner turns", async () => {
|
|
902
946
|
mockDiskPressureDecision = {
|
|
903
947
|
action: "allow-cleanup-mode",
|
|
904
948
|
reason: "local-owner",
|
|
905
949
|
};
|
|
906
950
|
const ctx = makeCtx();
|
|
951
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
907
952
|
|
|
908
953
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
909
954
|
|
|
@@ -915,16 +960,10 @@ describe("session-agent-loop", () => {
|
|
|
915
960
|
trustContext: null,
|
|
916
961
|
}),
|
|
917
962
|
);
|
|
918
|
-
|
|
919
|
-
.calls[0]![1] as {
|
|
920
|
-
diskPressureContext?: { cleanupModeActive: boolean } | null;
|
|
921
|
-
};
|
|
922
|
-
expect(firstInjectionOptions.diskPressureContext).toEqual({
|
|
923
|
-
cleanupModeActive: true,
|
|
924
|
-
});
|
|
963
|
+
expect(cleanupFlagDuringInjection()).toEqual([true]);
|
|
925
964
|
});
|
|
926
965
|
|
|
927
|
-
test("keeps cleanup
|
|
966
|
+
test("keeps the cleanup-mode flag set across overflow recovery reinjection", async () => {
|
|
928
967
|
mockDiskPressureDecision = {
|
|
929
968
|
action: "allow-cleanup-mode",
|
|
930
969
|
reason: "guardian",
|
|
@@ -946,18 +985,14 @@ describe("session-agent-loop", () => {
|
|
|
946
985
|
trustClass: "guardian",
|
|
947
986
|
} as AgentLoopConversationContext["trustContext"],
|
|
948
987
|
});
|
|
988
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
949
989
|
|
|
950
990
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
951
991
|
|
|
952
992
|
expect(applyRuntimeInjectionsMock.mock.calls.length).toBeGreaterThan(1);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
};
|
|
957
|
-
expect(options.diskPressureContext).toEqual({
|
|
958
|
-
cleanupModeActive: true,
|
|
959
|
-
});
|
|
960
|
-
}
|
|
993
|
+
const flags = cleanupFlagDuringInjection();
|
|
994
|
+
expect(flags.length).toBeGreaterThan(1);
|
|
995
|
+
expect(flags.every((flag) => flag === true)).toBe(true);
|
|
961
996
|
});
|
|
962
997
|
|
|
963
998
|
test("blocks policy-denied turns before runtime injection or model execution", async () => {
|
|
@@ -966,9 +1001,6 @@ describe("session-agent-loop", () => {
|
|
|
966
1001
|
reason: "trusted-contact",
|
|
967
1002
|
};
|
|
968
1003
|
const events: ServerMessage[] = [];
|
|
969
|
-
const agentLoopRun = mock(async (_messages: Message[]) => {
|
|
970
|
-
throw new Error("agent loop should not run");
|
|
971
|
-
});
|
|
972
1004
|
const activityStates: unknown[][] = [];
|
|
973
1005
|
const traceEvents: unknown[][] = [];
|
|
974
1006
|
const ctx = makeCtx({
|
|
@@ -981,17 +1013,16 @@ describe("session-agent-loop", () => {
|
|
|
981
1013
|
},
|
|
982
1014
|
} as unknown as AgentLoopConversationContext["traceEmitter"],
|
|
983
1015
|
});
|
|
984
|
-
ctx.agentLoop
|
|
1016
|
+
const runSpy = spyOn(ctx.agentLoop, "run");
|
|
985
1017
|
|
|
986
1018
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
987
1019
|
|
|
988
|
-
expect(
|
|
1020
|
+
expect(runSpy).not.toHaveBeenCalled();
|
|
989
1021
|
expect(applyRuntimeInjectionsMock).not.toHaveBeenCalled();
|
|
990
1022
|
expect(activityStates).toContainEqual([
|
|
991
1023
|
"idle",
|
|
992
1024
|
"error_terminal",
|
|
993
|
-
"global",
|
|
994
|
-
"test-req",
|
|
1025
|
+
{ anchor: "global", requestId: "test-req" },
|
|
995
1026
|
]);
|
|
996
1027
|
expect(traceEvents[0]).toEqual([
|
|
997
1028
|
"request_error",
|
|
@@ -1046,15 +1077,14 @@ describe("session-agent-loop", () => {
|
|
|
1046
1077
|
});
|
|
1047
1078
|
|
|
1048
1079
|
expect(applyRuntimeInjectionsMock).not.toHaveBeenCalled();
|
|
1049
|
-
expect(ctx.
|
|
1080
|
+
expect(ctx.isProcessing()).toBe(false);
|
|
1050
1081
|
expect(ctx.abortController).toBeNull();
|
|
1051
1082
|
expect(ctx.currentRequestId).toBeUndefined();
|
|
1052
1083
|
expect(drainQueue).toHaveBeenCalledWith("loop_complete");
|
|
1053
1084
|
expect(activityStates).toContainEqual([
|
|
1054
1085
|
"idle",
|
|
1055
1086
|
"error_terminal",
|
|
1056
|
-
"global",
|
|
1057
|
-
"test-req",
|
|
1087
|
+
{ anchor: "global", requestId: "test-req" },
|
|
1058
1088
|
]);
|
|
1059
1089
|
});
|
|
1060
1090
|
});
|
|
@@ -1063,44 +1093,14 @@ describe("session-agent-loop", () => {
|
|
|
1063
1093
|
test("error events from agent loop are classified and emitted", async () => {
|
|
1064
1094
|
const events: ServerMessage[] = [];
|
|
1065
1095
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
onEvent({
|
|
1075
|
-
type: "error",
|
|
1076
|
-
error: new Error("Tool execution failed: permission denied"),
|
|
1077
|
-
});
|
|
1078
|
-
onEvent({
|
|
1079
|
-
type: "message_complete",
|
|
1080
|
-
message: {
|
|
1081
|
-
role: "assistant",
|
|
1082
|
-
content: [{ type: "text", text: "I encountered an error" }],
|
|
1083
|
-
},
|
|
1084
|
-
});
|
|
1085
|
-
onEvent({
|
|
1086
|
-
type: "usage",
|
|
1087
|
-
inputTokens: 100,
|
|
1088
|
-
outputTokens: 50,
|
|
1089
|
-
model: "test-model",
|
|
1090
|
-
providerDurationMs: 200,
|
|
1091
|
-
});
|
|
1092
|
-
return [
|
|
1093
|
-
...messages,
|
|
1094
|
-
{
|
|
1095
|
-
role: "assistant" as const,
|
|
1096
|
-
content: [
|
|
1097
|
-
{ type: "text", text: "I encountered an error" },
|
|
1098
|
-
] as ContentBlock[],
|
|
1099
|
-
},
|
|
1100
|
-
];
|
|
1101
|
-
};
|
|
1102
|
-
|
|
1103
|
-
const ctx = makeCtx({ agentLoopRun });
|
|
1096
|
+
// The model calls a tool whose executor throws, surfacing an `error`
|
|
1097
|
+
// event from the loop's catch handler.
|
|
1098
|
+
const ctx = makeCtx({
|
|
1099
|
+
providerResponses: [toolUseResponse("tu-1", "bash", { cmd: "ls" })],
|
|
1100
|
+
toolExecutor: async () => {
|
|
1101
|
+
throw new Error("Tool execution failed: permission denied");
|
|
1102
|
+
},
|
|
1103
|
+
});
|
|
1104
1104
|
await runAgentLoopImpl(ctx, "run ls", "msg-1", (msg) => events.push(msg));
|
|
1105
1105
|
|
|
1106
1106
|
const conversationError = events.find(
|
|
@@ -1112,31 +1112,9 @@ describe("session-agent-loop", () => {
|
|
|
1112
1112
|
test("non-error agent loop completion does not emit conversation_error", async () => {
|
|
1113
1113
|
const events: ServerMessage[] = [];
|
|
1114
1114
|
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
message: {
|
|
1119
|
-
role: "assistant",
|
|
1120
|
-
content: [{ type: "text", text: "All good" }],
|
|
1121
|
-
},
|
|
1122
|
-
});
|
|
1123
|
-
onEvent({
|
|
1124
|
-
type: "usage",
|
|
1125
|
-
inputTokens: 50,
|
|
1126
|
-
outputTokens: 25,
|
|
1127
|
-
model: "test-model",
|
|
1128
|
-
providerDurationMs: 100,
|
|
1129
|
-
});
|
|
1130
|
-
return [
|
|
1131
|
-
...messages,
|
|
1132
|
-
{
|
|
1133
|
-
role: "assistant" as const,
|
|
1134
|
-
content: [{ type: "text", text: "All good" }] as ContentBlock[],
|
|
1135
|
-
},
|
|
1136
|
-
];
|
|
1137
|
-
};
|
|
1138
|
-
|
|
1139
|
-
const ctx = makeCtx({ agentLoopRun });
|
|
1115
|
+
const ctx = makeCtx({
|
|
1116
|
+
providerResponses: [textResponse("All good")],
|
|
1117
|
+
});
|
|
1140
1118
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1141
1119
|
|
|
1142
1120
|
const conversationError = events.find(
|
|
@@ -1172,35 +1150,20 @@ describe("session-agent-loop", () => {
|
|
|
1172
1150
|
},
|
|
1173
1151
|
};
|
|
1174
1152
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
role: "assistant",
|
|
1180
|
-
content: [{ type: "text", text: "Hi there." }],
|
|
1181
|
-
},
|
|
1182
|
-
});
|
|
1183
|
-
onEvent({
|
|
1184
|
-
type: "usage",
|
|
1185
|
-
inputTokens: 12,
|
|
1186
|
-
outputTokens: 3,
|
|
1187
|
-
model: "gpt-4.1-2026-03-01",
|
|
1188
|
-
actualProvider: "fireworks",
|
|
1189
|
-
providerDurationMs: 45,
|
|
1190
|
-
rawRequest,
|
|
1191
|
-
rawResponse,
|
|
1192
|
-
});
|
|
1193
|
-
return [
|
|
1194
|
-
...messages,
|
|
1153
|
+
// The provider response carries its own `actualProvider`, so the logged
|
|
1154
|
+
// row should record that name rather than the runtime provider.
|
|
1155
|
+
const ctx = makeCtx({
|
|
1156
|
+
providerResponses: [
|
|
1195
1157
|
{
|
|
1196
|
-
|
|
1197
|
-
|
|
1158
|
+
content: [{ type: "text", text: "Hi there." }],
|
|
1159
|
+
model: "gpt-4.1-2026-03-01",
|
|
1160
|
+
usage: { inputTokens: 12, outputTokens: 3 },
|
|
1161
|
+
stopReason: "end_turn",
|
|
1162
|
+
actualProvider: "fireworks",
|
|
1163
|
+
rawRequest,
|
|
1164
|
+
rawResponse,
|
|
1198
1165
|
},
|
|
1199
|
-
]
|
|
1200
|
-
};
|
|
1201
|
-
|
|
1202
|
-
const ctx = makeCtx({
|
|
1203
|
-
agentLoopRun,
|
|
1166
|
+
],
|
|
1204
1167
|
provider: {
|
|
1205
1168
|
name: "openrouter",
|
|
1206
1169
|
sendMessage: async () => ({
|
|
@@ -1237,34 +1200,19 @@ describe("session-agent-loop", () => {
|
|
|
1237
1200
|
],
|
|
1238
1201
|
};
|
|
1239
1202
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
role: "assistant",
|
|
1245
|
-
content: [{ type: "text", text: "Hi there." }],
|
|
1246
|
-
},
|
|
1247
|
-
});
|
|
1248
|
-
onEvent({
|
|
1249
|
-
type: "usage",
|
|
1250
|
-
inputTokens: 12,
|
|
1251
|
-
outputTokens: 3,
|
|
1252
|
-
model: "gpt-4.1-2026-03-01",
|
|
1253
|
-
providerDurationMs: 45,
|
|
1254
|
-
rawRequest,
|
|
1255
|
-
rawResponse,
|
|
1256
|
-
});
|
|
1257
|
-
return [
|
|
1258
|
-
...messages,
|
|
1203
|
+
// The provider response omits `actualProvider`, so the loop stamps the
|
|
1204
|
+
// runtime provider name onto the usage event and the row records it.
|
|
1205
|
+
const ctx = makeCtx({
|
|
1206
|
+
providerResponses: [
|
|
1259
1207
|
{
|
|
1260
|
-
|
|
1261
|
-
|
|
1208
|
+
content: [{ type: "text", text: "Hi there." }],
|
|
1209
|
+
model: "gpt-4.1-2026-03-01",
|
|
1210
|
+
usage: { inputTokens: 12, outputTokens: 3 },
|
|
1211
|
+
stopReason: "end_turn",
|
|
1212
|
+
rawRequest,
|
|
1213
|
+
rawResponse,
|
|
1262
1214
|
},
|
|
1263
|
-
]
|
|
1264
|
-
};
|
|
1265
|
-
|
|
1266
|
-
const ctx = makeCtx({
|
|
1267
|
-
agentLoopRun,
|
|
1215
|
+
],
|
|
1268
1216
|
provider: {
|
|
1269
1217
|
name: "openrouter",
|
|
1270
1218
|
sendMessage: async () => ({
|
|
@@ -1319,35 +1267,18 @@ describe("session-agent-loop", () => {
|
|
|
1319
1267
|
status: "completed",
|
|
1320
1268
|
};
|
|
1321
1269
|
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
type: "message_complete",
|
|
1325
|
-
message: {
|
|
1326
|
-
role: "assistant",
|
|
1327
|
-
content: [{ type: "text", text: "Hi there." }],
|
|
1328
|
-
},
|
|
1329
|
-
});
|
|
1330
|
-
onEvent({
|
|
1331
|
-
type: "usage",
|
|
1332
|
-
inputTokens: 12,
|
|
1333
|
-
outputTokens: 3,
|
|
1334
|
-
model: "gpt-5.4",
|
|
1335
|
-
actualProvider: "openai",
|
|
1336
|
-
providerDurationMs: 45,
|
|
1337
|
-
rawRequest,
|
|
1338
|
-
rawResponse,
|
|
1339
|
-
});
|
|
1340
|
-
return [
|
|
1341
|
-
...messages,
|
|
1270
|
+
const ctx = makeCtx({
|
|
1271
|
+
providerResponses: [
|
|
1342
1272
|
{
|
|
1343
|
-
|
|
1344
|
-
|
|
1273
|
+
content: [{ type: "text", text: "Hi there." }],
|
|
1274
|
+
model: "gpt-5.4",
|
|
1275
|
+
usage: { inputTokens: 12, outputTokens: 3 },
|
|
1276
|
+
stopReason: "end_turn",
|
|
1277
|
+
actualProvider: "openai",
|
|
1278
|
+
rawRequest,
|
|
1279
|
+
rawResponse,
|
|
1345
1280
|
},
|
|
1346
|
-
]
|
|
1347
|
-
};
|
|
1348
|
-
|
|
1349
|
-
const ctx = makeCtx({
|
|
1350
|
-
agentLoopRun,
|
|
1281
|
+
],
|
|
1351
1282
|
provider: {
|
|
1352
1283
|
name: "openai",
|
|
1353
1284
|
sendMessage: async () => ({
|
|
@@ -1387,34 +1318,17 @@ describe("session-agent-loop", () => {
|
|
|
1387
1318
|
attrs: Record<string, unknown>;
|
|
1388
1319
|
}> = [];
|
|
1389
1320
|
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
type: "message_complete",
|
|
1394
|
-
message: {
|
|
1395
|
-
role: "assistant",
|
|
1396
|
-
content: [{ type: "text", text: "Hi." }],
|
|
1397
|
-
},
|
|
1398
|
-
});
|
|
1399
|
-
onEvent({
|
|
1400
|
-
type: "usage",
|
|
1401
|
-
inputTokens: 10,
|
|
1402
|
-
outputTokens: 2,
|
|
1403
|
-
model: "gpt-5.5-2026-04-23",
|
|
1404
|
-
actualProvider: "openai",
|
|
1405
|
-
providerDurationMs: 100,
|
|
1406
|
-
});
|
|
1407
|
-
return [
|
|
1408
|
-
...messages,
|
|
1321
|
+
const ctx = makeCtx({
|
|
1322
|
+
// The loop replays the text block as a `text_delta` before `usage`.
|
|
1323
|
+
providerResponses: [
|
|
1409
1324
|
{
|
|
1410
|
-
|
|
1411
|
-
|
|
1325
|
+
content: [{ type: "text", text: "Hi." }],
|
|
1326
|
+
model: "gpt-5.5-2026-04-23",
|
|
1327
|
+
usage: { inputTokens: 10, outputTokens: 2 },
|
|
1328
|
+
stopReason: "end_turn",
|
|
1329
|
+
actualProvider: "openai",
|
|
1412
1330
|
},
|
|
1413
|
-
]
|
|
1414
|
-
};
|
|
1415
|
-
|
|
1416
|
-
const ctx = makeCtx({
|
|
1417
|
-
agentLoopRun,
|
|
1331
|
+
],
|
|
1418
1332
|
// Provider name matches actualProvider so both paths agree.
|
|
1419
1333
|
provider: {
|
|
1420
1334
|
name: "openai",
|
|
@@ -1462,28 +1376,18 @@ describe("session-agent-loop", () => {
|
|
|
1462
1376
|
attrs: Record<string, unknown>;
|
|
1463
1377
|
}> = [];
|
|
1464
1378
|
|
|
1465
|
-
const
|
|
1466
|
-
//
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
role: "assistant",
|
|
1379
|
+
const ctx = makeCtx({
|
|
1380
|
+
// An empty-content response: no text block fires `text_delta`, so the
|
|
1381
|
+
// started event falls back to the resolved usage provider name.
|
|
1382
|
+
providerResponses: [
|
|
1383
|
+
{
|
|
1471
1384
|
content: [],
|
|
1385
|
+
model: "gpt-5.5-2026-04-23",
|
|
1386
|
+
usage: { inputTokens: 10, outputTokens: 2 },
|
|
1387
|
+
stopReason: "end_turn",
|
|
1388
|
+
actualProvider: "openai",
|
|
1472
1389
|
},
|
|
1473
|
-
|
|
1474
|
-
onEvent({
|
|
1475
|
-
type: "usage",
|
|
1476
|
-
inputTokens: 10,
|
|
1477
|
-
outputTokens: 2,
|
|
1478
|
-
model: "gpt-5.5-2026-04-23",
|
|
1479
|
-
actualProvider: "openai",
|
|
1480
|
-
providerDurationMs: 100,
|
|
1481
|
-
});
|
|
1482
|
-
return messages;
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
const ctx = makeCtx({
|
|
1486
|
-
agentLoopRun,
|
|
1390
|
+
],
|
|
1487
1391
|
provider: {
|
|
1488
1392
|
name: "anthropic",
|
|
1489
1393
|
sendMessage: async () => ({
|
|
@@ -1525,49 +1429,32 @@ describe("session-agent-loop", () => {
|
|
|
1525
1429
|
test("records the actual provider for usage accounting", async () => {
|
|
1526
1430
|
const events: ServerMessage[] = [];
|
|
1527
1431
|
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
message: {
|
|
1532
|
-
role: "assistant",
|
|
1432
|
+
const ctx = makeCtx({
|
|
1433
|
+
providerResponses: [
|
|
1434
|
+
{
|
|
1533
1435
|
content: [{ type: "text", text: "Hi there." }],
|
|
1534
|
-
},
|
|
1535
|
-
});
|
|
1536
|
-
onEvent({
|
|
1537
|
-
type: "usage",
|
|
1538
|
-
inputTokens: 12,
|
|
1539
|
-
outputTokens: 3,
|
|
1540
|
-
model: "gpt-4.1-2026-03-01",
|
|
1541
|
-
actualProvider: "fireworks",
|
|
1542
|
-
providerDurationMs: 45,
|
|
1543
|
-
rawRequest: {
|
|
1544
|
-
model: "gpt-4.1",
|
|
1545
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
1546
|
-
},
|
|
1547
|
-
rawResponse: {
|
|
1548
1436
|
model: "gpt-4.1-2026-03-01",
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1437
|
+
usage: { inputTokens: 12, outputTokens: 3 },
|
|
1438
|
+
stopReason: "end_turn",
|
|
1439
|
+
actualProvider: "fireworks",
|
|
1440
|
+
rawRequest: {
|
|
1441
|
+
model: "gpt-4.1",
|
|
1442
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
1443
|
+
},
|
|
1444
|
+
rawResponse: {
|
|
1445
|
+
model: "gpt-4.1-2026-03-01",
|
|
1446
|
+
choices: [
|
|
1447
|
+
{
|
|
1448
|
+
finish_reason: "stop",
|
|
1449
|
+
message: {
|
|
1450
|
+
role: "assistant",
|
|
1451
|
+
content: "Hi there.",
|
|
1452
|
+
},
|
|
1555
1453
|
},
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
},
|
|
1559
|
-
});
|
|
1560
|
-
return [
|
|
1561
|
-
...messages,
|
|
1562
|
-
{
|
|
1563
|
-
role: "assistant" as const,
|
|
1564
|
-
content: [{ type: "text", text: "Hi there." }] as ContentBlock[],
|
|
1454
|
+
],
|
|
1455
|
+
},
|
|
1565
1456
|
},
|
|
1566
|
-
]
|
|
1567
|
-
};
|
|
1568
|
-
|
|
1569
|
-
const ctx = makeCtx({
|
|
1570
|
-
agentLoopRun,
|
|
1457
|
+
],
|
|
1571
1458
|
provider: {
|
|
1572
1459
|
name: "openrouter",
|
|
1573
1460
|
sendMessage: async () => ({
|
|
@@ -1637,24 +1524,9 @@ describe("session-agent-loop", () => {
|
|
|
1637
1524
|
},
|
|
1638
1525
|
});
|
|
1639
1526
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
message: {
|
|
1644
|
-
role: "assistant",
|
|
1645
|
-
content: [{ type: "text", text: "recovered" }],
|
|
1646
|
-
},
|
|
1647
|
-
});
|
|
1648
|
-
return [
|
|
1649
|
-
...messages,
|
|
1650
|
-
{
|
|
1651
|
-
role: "assistant" as const,
|
|
1652
|
-
content: [{ type: "text", text: "recovered" }] as ContentBlock[],
|
|
1653
|
-
},
|
|
1654
|
-
];
|
|
1655
|
-
};
|
|
1656
|
-
|
|
1657
|
-
const ctx = makeCtx({ agentLoopRun });
|
|
1527
|
+
// After the orchestrator's preflight compaction runs, the loop completes
|
|
1528
|
+
// the turn normally.
|
|
1529
|
+
const ctx = makeCtx({ providerResponses: [textResponse("recovered")] });
|
|
1658
1530
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1659
1531
|
|
|
1660
1532
|
const compactorCall = recordUsageMock.mock.calls.find(
|
|
@@ -1693,7 +1565,6 @@ describe("session-agent-loop", () => {
|
|
|
1693
1565
|
|
|
1694
1566
|
test("convergence loop applies reducer and retries when context-too-large is detected", async () => {
|
|
1695
1567
|
const events: ServerMessage[] = [];
|
|
1696
|
-
let callCount = 0;
|
|
1697
1568
|
let reducerCalled = false;
|
|
1698
1569
|
|
|
1699
1570
|
// Configure reducer to succeed on first call — return reduced messages
|
|
@@ -1727,48 +1598,15 @@ describe("session-agent-loop", () => {
|
|
|
1727
1598
|
};
|
|
1728
1599
|
};
|
|
1729
1600
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
});
|
|
1737
|
-
onEvent({
|
|
1738
|
-
type: "usage",
|
|
1739
|
-
inputTokens: 100,
|
|
1740
|
-
outputTokens: 0,
|
|
1741
|
-
model: "test-model",
|
|
1742
|
-
providerDurationMs: 50,
|
|
1743
|
-
});
|
|
1744
|
-
return messages;
|
|
1745
|
-
}
|
|
1746
|
-
// Second call (after reducer): succeed
|
|
1747
|
-
onEvent({
|
|
1748
|
-
type: "message_complete",
|
|
1749
|
-
message: {
|
|
1750
|
-
role: "assistant",
|
|
1751
|
-
content: [{ type: "text", text: "recovered" }],
|
|
1752
|
-
},
|
|
1753
|
-
});
|
|
1754
|
-
onEvent({
|
|
1755
|
-
type: "usage",
|
|
1756
|
-
inputTokens: 50,
|
|
1757
|
-
outputTokens: 25,
|
|
1758
|
-
model: "test-model",
|
|
1759
|
-
providerDurationMs: 100,
|
|
1760
|
-
});
|
|
1761
|
-
return [
|
|
1762
|
-
...messages,
|
|
1763
|
-
{
|
|
1764
|
-
role: "assistant" as const,
|
|
1765
|
-
content: [{ type: "text", text: "recovered" }] as ContentBlock[],
|
|
1766
|
-
},
|
|
1767
|
-
];
|
|
1768
|
-
};
|
|
1601
|
+
// The provider rejects the first call with a context-too-large error,
|
|
1602
|
+
// then succeeds once the orchestrator has reduced the context.
|
|
1603
|
+
const { provider, calls } = createMockProvider([
|
|
1604
|
+
new Error("context_length_exceeded"),
|
|
1605
|
+
textResponse("recovered"),
|
|
1606
|
+
]);
|
|
1769
1607
|
|
|
1770
1608
|
const ctx = makeCtx({
|
|
1771
|
-
|
|
1609
|
+
loopProvider: provider,
|
|
1772
1610
|
contextWindowManager: {
|
|
1773
1611
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1774
1612
|
maybeCompact: async () => ({ compacted: false }),
|
|
@@ -1778,7 +1616,7 @@ describe("session-agent-loop", () => {
|
|
|
1778
1616
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1779
1617
|
|
|
1780
1618
|
expect(reducerCalled).toBe(true);
|
|
1781
|
-
expect(
|
|
1619
|
+
expect(calls.length).toBe(2);
|
|
1782
1620
|
const compactEvent = events.find((e) => e.type === "context_compacted");
|
|
1783
1621
|
expect(compactEvent).toBeDefined();
|
|
1784
1622
|
});
|
|
@@ -1786,23 +1624,10 @@ describe("session-agent-loop", () => {
|
|
|
1786
1624
|
test("emits conversation_error when context stays too large after all recovery attempts", async () => {
|
|
1787
1625
|
const events: ServerMessage[] = [];
|
|
1788
1626
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
type: "error",
|
|
1792
|
-
error: new Error("context_length_exceeded"),
|
|
1793
|
-
});
|
|
1794
|
-
onEvent({
|
|
1795
|
-
type: "usage",
|
|
1796
|
-
inputTokens: 100,
|
|
1797
|
-
outputTokens: 0,
|
|
1798
|
-
model: "test-model",
|
|
1799
|
-
providerDurationMs: 50,
|
|
1800
|
-
});
|
|
1801
|
-
return messages;
|
|
1802
|
-
};
|
|
1803
|
-
|
|
1627
|
+
// The provider rejects every call with a context-too-large error, so the
|
|
1628
|
+
// orchestrator exhausts its recovery attempts.
|
|
1804
1629
|
const ctx = makeCtx({
|
|
1805
|
-
|
|
1630
|
+
providerResponses: [new Error("context_length_exceeded")],
|
|
1806
1631
|
contextWindowManager: {
|
|
1807
1632
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1808
1633
|
// Compaction succeeds but context is still too large
|
|
@@ -1836,7 +1661,6 @@ describe("session-agent-loop", () => {
|
|
|
1836
1661
|
|
|
1837
1662
|
test("bounded convergence loop applies reducer tiers and recovers", async () => {
|
|
1838
1663
|
const events: ServerMessage[] = [];
|
|
1839
|
-
let callCount = 0;
|
|
1840
1664
|
let reducerCalls = 0;
|
|
1841
1665
|
|
|
1842
1666
|
// Reducer: succeed on first call, returning reduced messages
|
|
@@ -1854,50 +1678,15 @@ describe("session-agent-loop", () => {
|
|
|
1854
1678
|
};
|
|
1855
1679
|
};
|
|
1856
1680
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
});
|
|
1864
|
-
onEvent({
|
|
1865
|
-
type: "usage",
|
|
1866
|
-
inputTokens: 100,
|
|
1867
|
-
outputTokens: 0,
|
|
1868
|
-
model: "test-model",
|
|
1869
|
-
providerDurationMs: 50,
|
|
1870
|
-
});
|
|
1871
|
-
return messages;
|
|
1872
|
-
}
|
|
1873
|
-
// After reducer runs, succeed
|
|
1874
|
-
onEvent({
|
|
1875
|
-
type: "message_complete",
|
|
1876
|
-
message: {
|
|
1877
|
-
role: "assistant",
|
|
1878
|
-
content: [{ type: "text", text: "recovered via convergence" }],
|
|
1879
|
-
},
|
|
1880
|
-
});
|
|
1881
|
-
onEvent({
|
|
1882
|
-
type: "usage",
|
|
1883
|
-
inputTokens: 50,
|
|
1884
|
-
outputTokens: 25,
|
|
1885
|
-
model: "test-model",
|
|
1886
|
-
providerDurationMs: 100,
|
|
1887
|
-
});
|
|
1888
|
-
return [
|
|
1889
|
-
...messages,
|
|
1890
|
-
{
|
|
1891
|
-
role: "assistant" as const,
|
|
1892
|
-
content: [
|
|
1893
|
-
{ type: "text", text: "recovered via convergence" },
|
|
1894
|
-
] as ContentBlock[],
|
|
1895
|
-
},
|
|
1896
|
-
];
|
|
1897
|
-
};
|
|
1681
|
+
// The provider rejects the first call with a context-too-large error,
|
|
1682
|
+
// then succeeds once the orchestrator has reduced the context.
|
|
1683
|
+
const { provider, calls } = createMockProvider([
|
|
1684
|
+
new Error("context_length_exceeded"),
|
|
1685
|
+
textResponse("recovered via convergence"),
|
|
1686
|
+
]);
|
|
1898
1687
|
|
|
1899
1688
|
const ctx = makeCtx({
|
|
1900
|
-
|
|
1689
|
+
loopProvider: provider,
|
|
1901
1690
|
contextWindowManager: {
|
|
1902
1691
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1903
1692
|
maybeCompact: async () => ({ compacted: false }),
|
|
@@ -1907,7 +1696,7 @@ describe("session-agent-loop", () => {
|
|
|
1907
1696
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1908
1697
|
|
|
1909
1698
|
expect(reducerCalls).toBeGreaterThanOrEqual(1);
|
|
1910
|
-
expect(
|
|
1699
|
+
expect(calls.length).toBe(2);
|
|
1911
1700
|
const conversationError = events.find(
|
|
1912
1701
|
(e) => e.type === "conversation_error",
|
|
1913
1702
|
);
|
|
@@ -1918,7 +1707,6 @@ describe("session-agent-loop", () => {
|
|
|
1918
1707
|
|
|
1919
1708
|
test("non-interactive auto-compress continues without approval prompt", async () => {
|
|
1920
1709
|
const events: ServerMessage[] = [];
|
|
1921
|
-
let callCount = 0;
|
|
1922
1710
|
|
|
1923
1711
|
// Reducer exhausts all tiers
|
|
1924
1712
|
mockReducerStepFn = (msgs: Message[]) => ({
|
|
@@ -1939,49 +1727,14 @@ describe("session-agent-loop", () => {
|
|
|
1939
1727
|
|
|
1940
1728
|
mockOverflowAction = "auto_compress_latest_turn";
|
|
1941
1729
|
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
if (callCount <= 2) {
|
|
1945
|
-
onEvent({
|
|
1946
|
-
type: "error",
|
|
1947
|
-
error: new Error("context_length_exceeded"),
|
|
1948
|
-
});
|
|
1949
|
-
onEvent({
|
|
1950
|
-
type: "usage",
|
|
1951
|
-
inputTokens: 100,
|
|
1952
|
-
outputTokens: 0,
|
|
1953
|
-
model: "test-model",
|
|
1954
|
-
providerDurationMs: 50,
|
|
1955
|
-
});
|
|
1956
|
-
return messages;
|
|
1957
|
-
}
|
|
1958
|
-
onEvent({
|
|
1959
|
-
type: "message_complete",
|
|
1960
|
-
message: {
|
|
1961
|
-
role: "assistant",
|
|
1962
|
-
content: [{ type: "text", text: "auto-recovered" }],
|
|
1963
|
-
},
|
|
1964
|
-
});
|
|
1965
|
-
onEvent({
|
|
1966
|
-
type: "usage",
|
|
1967
|
-
inputTokens: 50,
|
|
1968
|
-
outputTokens: 25,
|
|
1969
|
-
model: "test-model",
|
|
1970
|
-
providerDurationMs: 100,
|
|
1971
|
-
});
|
|
1972
|
-
return [
|
|
1973
|
-
...messages,
|
|
1974
|
-
{
|
|
1975
|
-
role: "assistant" as const,
|
|
1976
|
-
content: [
|
|
1977
|
-
{ type: "text", text: "auto-recovered" },
|
|
1978
|
-
] as ContentBlock[],
|
|
1979
|
-
},
|
|
1980
|
-
];
|
|
1981
|
-
};
|
|
1982
|
-
|
|
1730
|
+
// The provider rejects the first two calls with context-too-large errors,
|
|
1731
|
+
// then succeeds after the emergency auto-compress runs.
|
|
1983
1732
|
const ctx = makeCtx({
|
|
1984
|
-
|
|
1733
|
+
providerResponses: [
|
|
1734
|
+
new Error("context_length_exceeded"),
|
|
1735
|
+
new Error("context_length_exceeded"),
|
|
1736
|
+
textResponse("auto-recovered"),
|
|
1737
|
+
],
|
|
1985
1738
|
hasNoClient: true,
|
|
1986
1739
|
contextWindowManager: {
|
|
1987
1740
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
@@ -2028,7 +1781,6 @@ describe("session-agent-loop", () => {
|
|
|
2028
1781
|
// `budget_yield_unrecovered` so the inspector and dashboards can
|
|
2029
1782
|
// attribute the silent stall.
|
|
2030
1783
|
const events: ServerMessage[] = [];
|
|
2031
|
-
let callCount = 0;
|
|
2032
1784
|
|
|
2033
1785
|
// Reducer exhausts all 4 tiers on first call so the convergence
|
|
2034
1786
|
// loop runs exactly one iteration before falling through to
|
|
@@ -2059,49 +1811,30 @@ describe("session-agent-loop", () => {
|
|
|
2059
1811
|
// call). 90k satisfies both so the path reaches call 3.
|
|
2060
1812
|
mockEstimateTokens = 90_000;
|
|
2061
1813
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
callCount++;
|
|
2070
|
-
if (callCount <= 2) {
|
|
2071
|
-
// Calls 1 (initial) and 2 (convergence rerun): error so
|
|
2072
|
-
// `state.contextTooLargeDetected` stays true through
|
|
2073
|
-
// convergence exit and we enter the auto_compress branch.
|
|
2074
|
-
onEvent({
|
|
2075
|
-
type: "error",
|
|
2076
|
-
error: new Error("context_length_exceeded"),
|
|
2077
|
-
});
|
|
2078
|
-
onEvent({
|
|
2079
|
-
type: "usage",
|
|
2080
|
-
inputTokens: 100,
|
|
2081
|
-
outputTokens: 0,
|
|
2082
|
-
model: "test-model",
|
|
2083
|
-
providerDurationMs: 50,
|
|
2084
|
-
});
|
|
2085
|
-
return messages;
|
|
2086
|
-
}
|
|
2087
|
-
// Call 3: the auto_compress_latest_turn rerun. Invoke
|
|
2088
|
-
// onCheckpoint so the orchestrator's mid-loop budget check
|
|
2089
|
-
// flips `yieldedForBudget` to true, then return without
|
|
2090
|
-
// finishing — mirroring what AgentLoop.run does when its
|
|
2091
|
-
// checkpoint returns "yield".
|
|
2092
|
-
if (onCheckpoint) {
|
|
2093
|
-
await onCheckpoint({
|
|
2094
|
-
turnIndex: 0,
|
|
2095
|
-
toolCount: 1,
|
|
2096
|
-
hasToolUse: true,
|
|
2097
|
-
history: messages,
|
|
2098
|
-
});
|
|
2099
|
-
}
|
|
2100
|
-
return messages;
|
|
2101
|
-
};
|
|
2102
|
-
|
|
1814
|
+
// Calls 1 (initial) and 2 (convergence rerun) reject with
|
|
1815
|
+
// context-too-large so `contextTooLargeDetected` stays true through the
|
|
1816
|
+
// convergence exit and the orchestrator enters the auto_compress branch.
|
|
1817
|
+
// Call 3 (the auto_compress rerun) is a tool turn: the loop runs it
|
|
1818
|
+
// without a compaction hook, so when its mid-loop budget gate trips on
|
|
1819
|
+
// the still-oversized estimate it yields `exitReason = "budget"` rather
|
|
1820
|
+
// than recovering — the silent-stall path under test.
|
|
2103
1821
|
const ctx = makeCtx({
|
|
2104
|
-
|
|
1822
|
+
providerResponses: [
|
|
1823
|
+
new Error("context_length_exceeded"),
|
|
1824
|
+
new Error("context_length_exceeded"),
|
|
1825
|
+
toolUseResponse("t1", "read_file", { path: "/a.txt" }),
|
|
1826
|
+
],
|
|
1827
|
+
loopTools: [
|
|
1828
|
+
{
|
|
1829
|
+
name: "read_file",
|
|
1830
|
+
description: "Read a file",
|
|
1831
|
+
input_schema: {
|
|
1832
|
+
type: "object",
|
|
1833
|
+
properties: { path: { type: "string" } },
|
|
1834
|
+
},
|
|
1835
|
+
},
|
|
1836
|
+
],
|
|
1837
|
+
toolExecutor: async () => ({ content: "data", isError: false }),
|
|
2105
1838
|
hasNoClient: true,
|
|
2106
1839
|
contextWindowManager: {
|
|
2107
1840
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
@@ -2184,23 +1917,10 @@ describe("session-agent-loop", () => {
|
|
|
2184
1917
|
};
|
|
2185
1918
|
};
|
|
2186
1919
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
type: "error",
|
|
2190
|
-
error: new Error("context_length_exceeded"),
|
|
2191
|
-
});
|
|
2192
|
-
onEvent({
|
|
2193
|
-
type: "usage",
|
|
2194
|
-
inputTokens: 100,
|
|
2195
|
-
outputTokens: 0,
|
|
2196
|
-
model: "test-model",
|
|
2197
|
-
providerDurationMs: 50,
|
|
2198
|
-
});
|
|
2199
|
-
return messages;
|
|
2200
|
-
};
|
|
2201
|
-
|
|
1920
|
+
// The provider rejects every call with a context-too-large error, so the
|
|
1921
|
+
// orchestrator keeps retrying until it hits the attempt ceiling.
|
|
2202
1922
|
const ctx = makeCtx({
|
|
2203
|
-
|
|
1923
|
+
providerResponses: [new Error("context_length_exceeded")],
|
|
2204
1924
|
contextWindowManager: {
|
|
2205
1925
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
2206
1926
|
maybeCompact: async () => ({ compacted: false }),
|
|
@@ -2216,7 +1936,6 @@ describe("session-agent-loop", () => {
|
|
|
2216
1936
|
test("preflight budget evaluation invokes reducer before provider call", async () => {
|
|
2217
1937
|
const events: ServerMessage[] = [];
|
|
2218
1938
|
let reducerCalls = 0;
|
|
2219
|
-
let agentLoopCalls = 0;
|
|
2220
1939
|
|
|
2221
1940
|
// Set token estimate above budget (100000 * 0.95 = 95000)
|
|
2222
1941
|
mockEstimateTokens = 96000;
|
|
@@ -2235,33 +1954,11 @@ describe("session-agent-loop", () => {
|
|
|
2235
1954
|
};
|
|
2236
1955
|
};
|
|
2237
1956
|
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
type: "message_complete",
|
|
2242
|
-
message: {
|
|
2243
|
-
role: "assistant",
|
|
2244
|
-
content: [{ type: "text", text: "ok" }],
|
|
2245
|
-
},
|
|
2246
|
-
});
|
|
2247
|
-
onEvent({
|
|
2248
|
-
type: "usage",
|
|
2249
|
-
inputTokens: 50,
|
|
2250
|
-
outputTokens: 25,
|
|
2251
|
-
model: "test-model",
|
|
2252
|
-
providerDurationMs: 100,
|
|
2253
|
-
});
|
|
2254
|
-
return [
|
|
2255
|
-
...messages,
|
|
2256
|
-
{
|
|
2257
|
-
role: "assistant" as const,
|
|
2258
|
-
content: [{ type: "text", text: "ok" }] as ContentBlock[],
|
|
2259
|
-
},
|
|
2260
|
-
];
|
|
2261
|
-
};
|
|
2262
|
-
|
|
1957
|
+
// After the preflight reducer brings the estimate under budget, the loop
|
|
1958
|
+
// completes the turn in a single provider call.
|
|
1959
|
+
const { provider, calls } = createMockProvider([textResponse("ok")]);
|
|
2263
1960
|
const ctx = makeCtx({
|
|
2264
|
-
|
|
1961
|
+
loopProvider: provider,
|
|
2265
1962
|
contextWindowManager: {
|
|
2266
1963
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
2267
1964
|
maybeCompact: async () => ({ compacted: false }),
|
|
@@ -2272,8 +1969,8 @@ describe("session-agent-loop", () => {
|
|
|
2272
1969
|
|
|
2273
1970
|
// Reducer should have been called during preflight
|
|
2274
1971
|
expect(reducerCalls).toBeGreaterThanOrEqual(1);
|
|
2275
|
-
// Agent loop should still succeed
|
|
2276
|
-
expect(
|
|
1972
|
+
// Agent loop should still succeed in a single provider call
|
|
1973
|
+
expect(calls.length).toBe(1);
|
|
2277
1974
|
const complete = events.find((e) => e.type === "message_complete");
|
|
2278
1975
|
expect(complete).toBeDefined();
|
|
2279
1976
|
});
|
|
@@ -2282,73 +1979,28 @@ describe("session-agent-loop", () => {
|
|
|
2282
1979
|
describe("provider ordering error retry", () => {
|
|
2283
1980
|
test("retries with deep repair when ordering error is detected", async () => {
|
|
2284
1981
|
const events: ServerMessage[] = [];
|
|
2285
|
-
let callCount = 0;
|
|
2286
|
-
|
|
2287
|
-
const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
|
|
2288
|
-
callCount++;
|
|
2289
|
-
if (callCount === 1) {
|
|
2290
|
-
onEvent({
|
|
2291
|
-
type: "error",
|
|
2292
|
-
error: new Error("messages ordering error"),
|
|
2293
|
-
});
|
|
2294
|
-
onEvent({
|
|
2295
|
-
type: "usage",
|
|
2296
|
-
inputTokens: 100,
|
|
2297
|
-
outputTokens: 0,
|
|
2298
|
-
model: "test-model",
|
|
2299
|
-
providerDurationMs: 50,
|
|
2300
|
-
});
|
|
2301
|
-
return messages;
|
|
2302
|
-
}
|
|
2303
|
-
// Retry succeeds
|
|
2304
|
-
onEvent({
|
|
2305
|
-
type: "message_complete",
|
|
2306
|
-
message: {
|
|
2307
|
-
role: "assistant",
|
|
2308
|
-
content: [{ type: "text", text: "fixed" }],
|
|
2309
|
-
},
|
|
2310
|
-
});
|
|
2311
|
-
onEvent({
|
|
2312
|
-
type: "usage",
|
|
2313
|
-
inputTokens: 50,
|
|
2314
|
-
outputTokens: 25,
|
|
2315
|
-
model: "test-model",
|
|
2316
|
-
providerDurationMs: 100,
|
|
2317
|
-
});
|
|
2318
|
-
return [
|
|
2319
|
-
...messages,
|
|
2320
|
-
{
|
|
2321
|
-
role: "assistant" as const,
|
|
2322
|
-
content: [{ type: "text", text: "fixed" }] as ContentBlock[],
|
|
2323
|
-
},
|
|
2324
|
-
];
|
|
2325
|
-
};
|
|
2326
1982
|
|
|
2327
|
-
|
|
1983
|
+
// The provider rejects the first call with an ordering error, then
|
|
1984
|
+
// succeeds once the orchestrator's deep repair re-sends the turn.
|
|
1985
|
+
const { provider, calls } = createMockProvider([
|
|
1986
|
+
new Error("messages ordering error"),
|
|
1987
|
+
textResponse("fixed"),
|
|
1988
|
+
]);
|
|
1989
|
+
|
|
1990
|
+
const ctx = makeCtx({ loopProvider: provider });
|
|
2328
1991
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2329
1992
|
|
|
2330
|
-
expect(
|
|
1993
|
+
expect(calls.length).toBe(2);
|
|
2331
1994
|
});
|
|
2332
1995
|
|
|
2333
1996
|
test("emits deferred ordering error when retry also fails", async () => {
|
|
2334
1997
|
const events: ServerMessage[] = [];
|
|
2335
1998
|
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
onEvent({
|
|
2342
|
-
type: "usage",
|
|
2343
|
-
inputTokens: 100,
|
|
2344
|
-
outputTokens: 0,
|
|
2345
|
-
model: "test-model",
|
|
2346
|
-
providerDurationMs: 50,
|
|
2347
|
-
});
|
|
2348
|
-
return messages;
|
|
2349
|
-
};
|
|
2350
|
-
|
|
2351
|
-
const ctx = makeCtx({ agentLoopRun });
|
|
1999
|
+
// The provider rejects every call with an ordering error, so even the
|
|
2000
|
+
// deep-repair retry fails and the orchestrator surfaces the error.
|
|
2001
|
+
const ctx = makeCtx({
|
|
2002
|
+
providerResponses: [new Error("messages ordering error")],
|
|
2003
|
+
});
|
|
2352
2004
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2353
2005
|
|
|
2354
2006
|
const conversationError = events.find(
|
|
@@ -2362,63 +2014,18 @@ describe("session-agent-loop", () => {
|
|
|
2362
2014
|
test("yields at checkpoint when canHandoffAtCheckpoint returns true", async () => {
|
|
2363
2015
|
const events: ServerMessage[] = [];
|
|
2364
2016
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
onCheckpoint,
|
|
2371
|
-
) => {
|
|
2372
|
-
// Simulate tool use followed by checkpoint
|
|
2373
|
-
onEvent({ type: "tool_use", id: "tu-1", name: "file_read", input: {} });
|
|
2374
|
-
onEvent({
|
|
2375
|
-
type: "tool_result",
|
|
2376
|
-
toolUseId: "tu-1",
|
|
2377
|
-
content: "file content",
|
|
2378
|
-
isError: false,
|
|
2379
|
-
});
|
|
2380
|
-
onEvent({
|
|
2381
|
-
type: "message_complete",
|
|
2382
|
-
message: {
|
|
2383
|
-
role: "assistant",
|
|
2384
|
-
content: [{ type: "text", text: "partial" }],
|
|
2385
|
-
},
|
|
2386
|
-
});
|
|
2387
|
-
onEvent({
|
|
2388
|
-
type: "usage",
|
|
2389
|
-
inputTokens: 100,
|
|
2390
|
-
outputTokens: 50,
|
|
2391
|
-
model: "test-model",
|
|
2392
|
-
providerDurationMs: 100,
|
|
2393
|
-
});
|
|
2394
|
-
if (onCheckpoint) {
|
|
2395
|
-
const decision = await onCheckpoint({
|
|
2396
|
-
turnIndex: 0,
|
|
2397
|
-
toolCount: 1,
|
|
2398
|
-
hasToolUse: true,
|
|
2399
|
-
history: messages,
|
|
2400
|
-
});
|
|
2401
|
-
if (decision === "yield") {
|
|
2402
|
-
return [
|
|
2403
|
-
...messages,
|
|
2404
|
-
{
|
|
2405
|
-
role: "assistant" as const,
|
|
2406
|
-
content: [{ type: "text", text: "partial" }] as ContentBlock[],
|
|
2407
|
-
},
|
|
2408
|
-
];
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
return [
|
|
2412
|
-
...messages,
|
|
2017
|
+
// A tool turn drives the loop to its first mid-loop checkpoint, where the
|
|
2018
|
+
// orchestrator yields for a queued handoff.
|
|
2019
|
+
const ctx = makeCtx({
|
|
2020
|
+
providerResponses: [toolUseResponse("tu-1", "file_read", {})],
|
|
2021
|
+
loopTools: [
|
|
2413
2022
|
{
|
|
2414
|
-
|
|
2415
|
-
|
|
2023
|
+
name: "file_read",
|
|
2024
|
+
description: "Read a file",
|
|
2025
|
+
input_schema: { type: "object", properties: {} },
|
|
2416
2026
|
},
|
|
2417
|
-
]
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
const ctx = makeCtx({
|
|
2421
|
-
agentLoopRun,
|
|
2027
|
+
],
|
|
2028
|
+
toolExecutor: async () => ({ content: "file content", isError: false }),
|
|
2422
2029
|
canHandoffAtCheckpoint: () => true,
|
|
2423
2030
|
} as unknown as Partial<AgentLoopConversationContext>);
|
|
2424
2031
|
|
|
@@ -2435,53 +2042,21 @@ describe("session-agent-loop", () => {
|
|
|
2435
2042
|
test("continues when canHandoffAtCheckpoint returns false", async () => {
|
|
2436
2043
|
const events: ServerMessage[] = [];
|
|
2437
2044
|
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
onEvent({
|
|
2447
|
-
type: "tool_result",
|
|
2448
|
-
toolUseId: "tu-1",
|
|
2449
|
-
content: "content",
|
|
2450
|
-
isError: false,
|
|
2451
|
-
});
|
|
2452
|
-
onEvent({
|
|
2453
|
-
type: "message_complete",
|
|
2454
|
-
message: {
|
|
2455
|
-
role: "assistant",
|
|
2456
|
-
content: [{ type: "text", text: "done" }],
|
|
2457
|
-
},
|
|
2458
|
-
});
|
|
2459
|
-
onEvent({
|
|
2460
|
-
type: "usage",
|
|
2461
|
-
inputTokens: 100,
|
|
2462
|
-
outputTokens: 50,
|
|
2463
|
-
model: "test-model",
|
|
2464
|
-
providerDurationMs: 100,
|
|
2465
|
-
});
|
|
2466
|
-
if (onCheckpoint) {
|
|
2467
|
-
await onCheckpoint({
|
|
2468
|
-
turnIndex: 0,
|
|
2469
|
-
toolCount: 1,
|
|
2470
|
-
hasToolUse: true,
|
|
2471
|
-
history: messages,
|
|
2472
|
-
});
|
|
2473
|
-
}
|
|
2474
|
-
return [
|
|
2475
|
-
...messages,
|
|
2045
|
+
// The tool turn reaches a checkpoint, but with handoff disabled the loop
|
|
2046
|
+
// continues to the next turn and completes normally.
|
|
2047
|
+
const ctx = makeCtx({
|
|
2048
|
+
providerResponses: [
|
|
2049
|
+
toolUseResponse("tu-1", "file_read", {}),
|
|
2050
|
+
textResponse("done"),
|
|
2051
|
+
],
|
|
2052
|
+
loopTools: [
|
|
2476
2053
|
{
|
|
2477
|
-
|
|
2478
|
-
|
|
2054
|
+
name: "file_read",
|
|
2055
|
+
description: "Read a file",
|
|
2056
|
+
input_schema: { type: "object", properties: {} },
|
|
2479
2057
|
},
|
|
2480
|
-
]
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
const ctx = makeCtx({
|
|
2484
|
-
agentLoopRun,
|
|
2058
|
+
],
|
|
2059
|
+
toolExecutor: async () => ({ content: "content", isError: false }),
|
|
2485
2060
|
canHandoffAtCheckpoint: () => false,
|
|
2486
2061
|
} as unknown as Partial<AgentLoopConversationContext>);
|
|
2487
2062
|
|
|
@@ -2503,33 +2078,18 @@ describe("session-agent-loop", () => {
|
|
|
2503
2078
|
const events: ServerMessage[] = [];
|
|
2504
2079
|
const abortController = new AbortController();
|
|
2505
2080
|
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
type: "usage",
|
|
2516
|
-
inputTokens: 100,
|
|
2517
|
-
outputTokens: 50,
|
|
2518
|
-
model: "test-model",
|
|
2519
|
-
providerDurationMs: 100,
|
|
2520
|
-
});
|
|
2521
|
-
// Simulate abort after processing
|
|
2522
|
-
abortController.abort();
|
|
2523
|
-
return [
|
|
2524
|
-
...messages,
|
|
2525
|
-
{
|
|
2526
|
-
role: "assistant" as const,
|
|
2527
|
-
content: [{ type: "text", text: "partial" }] as ContentBlock[],
|
|
2528
|
-
},
|
|
2529
|
-
];
|
|
2081
|
+
// The provider completes its response but the user cancels mid-turn, so
|
|
2082
|
+
// the orchestrator observes the aborted signal once the loop returns.
|
|
2083
|
+
const provider: Provider = {
|
|
2084
|
+
name: "mock",
|
|
2085
|
+
async sendMessage(_messages, options) {
|
|
2086
|
+
options?.onEvent?.({ type: "text_delta", text: "partial" });
|
|
2087
|
+
abortController.abort();
|
|
2088
|
+
return textResponse("partial");
|
|
2089
|
+
},
|
|
2530
2090
|
};
|
|
2531
2091
|
|
|
2532
|
-
const ctx = makeCtx({
|
|
2092
|
+
const ctx = makeCtx({ loopProvider: provider, abortController });
|
|
2533
2093
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2534
2094
|
|
|
2535
2095
|
const cancelled = events.find((e) => e.type === "generation_cancelled");
|
|
@@ -2540,13 +2100,16 @@ describe("session-agent-loop", () => {
|
|
|
2540
2100
|
const events: ServerMessage[] = [];
|
|
2541
2101
|
const abortController = new AbortController();
|
|
2542
2102
|
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2103
|
+
// The provider rejects with an AbortError after the user cancels.
|
|
2104
|
+
const provider: Provider = {
|
|
2105
|
+
name: "mock",
|
|
2106
|
+
async sendMessage() {
|
|
2107
|
+
abortController.abort();
|
|
2108
|
+
throw new DOMException("The operation was aborted", "AbortError");
|
|
2109
|
+
},
|
|
2547
2110
|
};
|
|
2548
2111
|
|
|
2549
|
-
const ctx = makeCtx({
|
|
2112
|
+
const ctx = makeCtx({ loopProvider: provider, abortController });
|
|
2550
2113
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2551
2114
|
|
|
2552
2115
|
const cancelled = events.find((e) => e.type === "generation_cancelled");
|
|
@@ -2563,33 +2126,17 @@ describe("session-agent-loop", () => {
|
|
|
2563
2126
|
const abortController = new AbortController();
|
|
2564
2127
|
resolveAssistantAttachmentsMock.mockClear();
|
|
2565
2128
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
}
|
|
2574
|
-
onEvent({
|
|
2575
|
-
type: "usage",
|
|
2576
|
-
inputTokens: 100,
|
|
2577
|
-
outputTokens: 50,
|
|
2578
|
-
model: "test-model",
|
|
2579
|
-
providerDurationMs: 100,
|
|
2580
|
-
});
|
|
2581
|
-
// Simulate abort after processing
|
|
2582
|
-
abortController.abort();
|
|
2583
|
-
return [
|
|
2584
|
-
...messages,
|
|
2585
|
-
{
|
|
2586
|
-
role: "assistant" as const,
|
|
2587
|
-
content: [{ type: "text", text: "partial" }] as ContentBlock[],
|
|
2588
|
-
},
|
|
2589
|
-
];
|
|
2129
|
+
// The provider completes its response but the user cancels mid-turn.
|
|
2130
|
+
const provider: Provider = {
|
|
2131
|
+
name: "mock",
|
|
2132
|
+
async sendMessage(_messages, options) {
|
|
2133
|
+
options?.onEvent?.({ type: "text_delta", text: "partial" });
|
|
2134
|
+
abortController.abort();
|
|
2135
|
+
return textResponse("partial");
|
|
2136
|
+
},
|
|
2590
2137
|
};
|
|
2591
2138
|
|
|
2592
|
-
const ctx = makeCtx({
|
|
2139
|
+
const ctx = makeCtx({ loopProvider: provider, abortController });
|
|
2593
2140
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2594
2141
|
|
|
2595
2142
|
const cancelled = events.find((e) => e.type === "generation_cancelled");
|
|
@@ -2601,90 +2148,50 @@ describe("session-agent-loop", () => {
|
|
|
2601
2148
|
|
|
2602
2149
|
describe("finally block cleanup", () => {
|
|
2603
2150
|
test("increments turnCount after successful run", async () => {
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
onEvent({
|
|
2607
|
-
type: "message_complete",
|
|
2608
|
-
message: {
|
|
2609
|
-
role: "assistant",
|
|
2610
|
-
content: [{ type: "text", text: "hi" }],
|
|
2611
|
-
},
|
|
2612
|
-
});
|
|
2613
|
-
onEvent({
|
|
2614
|
-
type: "usage",
|
|
2615
|
-
inputTokens: 10,
|
|
2616
|
-
outputTokens: 5,
|
|
2617
|
-
model: "test",
|
|
2618
|
-
providerDurationMs: 50,
|
|
2619
|
-
});
|
|
2620
|
-
return [
|
|
2621
|
-
...messages,
|
|
2622
|
-
{
|
|
2623
|
-
role: "assistant" as const,
|
|
2624
|
-
content: [{ type: "text", text: "hi" }] as ContentBlock[],
|
|
2625
|
-
},
|
|
2626
|
-
];
|
|
2627
|
-
},
|
|
2628
|
-
});
|
|
2151
|
+
// GIVEN a real loop that answers in a single text turn
|
|
2152
|
+
const ctx = makeCtx({ providerResponses: [textResponse("hi")] });
|
|
2629
2153
|
expect(ctx.turnCount).toBe(0);
|
|
2630
2154
|
|
|
2155
|
+
// WHEN the orchestrator runs the turn to completion
|
|
2631
2156
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2632
2157
|
|
|
2158
|
+
// THEN the finally block increments the turn count
|
|
2633
2159
|
expect(ctx.turnCount).toBe(1);
|
|
2634
2160
|
});
|
|
2635
2161
|
|
|
2636
2162
|
test("clears processing state and abort controller", async () => {
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
onEvent({
|
|
2640
|
-
type: "message_complete",
|
|
2641
|
-
message: {
|
|
2642
|
-
role: "assistant",
|
|
2643
|
-
content: [{ type: "text", text: "hi" }],
|
|
2644
|
-
},
|
|
2645
|
-
});
|
|
2646
|
-
onEvent({
|
|
2647
|
-
type: "usage",
|
|
2648
|
-
inputTokens: 10,
|
|
2649
|
-
outputTokens: 5,
|
|
2650
|
-
model: "test",
|
|
2651
|
-
providerDurationMs: 50,
|
|
2652
|
-
});
|
|
2653
|
-
return [
|
|
2654
|
-
...messages,
|
|
2655
|
-
{
|
|
2656
|
-
role: "assistant" as const,
|
|
2657
|
-
content: [{ type: "text", text: "hi" }] as ContentBlock[],
|
|
2658
|
-
},
|
|
2659
|
-
];
|
|
2660
|
-
},
|
|
2661
|
-
});
|
|
2163
|
+
// GIVEN a real loop that answers in a single text turn
|
|
2164
|
+
const ctx = makeCtx({ providerResponses: [textResponse("hi")] });
|
|
2662
2165
|
|
|
2166
|
+
// WHEN the orchestrator runs the turn to completion
|
|
2663
2167
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2664
2168
|
|
|
2665
|
-
|
|
2169
|
+
// THEN the finally block clears all per-turn processing state
|
|
2170
|
+
expect(ctx.isProcessing()).toBe(false);
|
|
2666
2171
|
expect(ctx.abortController).toBeNull();
|
|
2667
2172
|
expect(ctx.currentRequestId).toBeUndefined();
|
|
2668
2173
|
expect(ctx.commandIntent).toBeUndefined();
|
|
2669
2174
|
});
|
|
2670
2175
|
|
|
2671
|
-
test("clears state
|
|
2176
|
+
test("clears state and surfaces a processing error when the provider call fails", async () => {
|
|
2177
|
+
// GIVEN a real loop whose provider rejects with an unexpected error
|
|
2672
2178
|
const events: ServerMessage[] = [];
|
|
2673
2179
|
const ctx = makeCtx({
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2180
|
+
loopProvider: {
|
|
2181
|
+
name: "mock-provider",
|
|
2182
|
+
async sendMessage() {
|
|
2183
|
+
throw new Error("unexpected crash");
|
|
2184
|
+
},
|
|
2185
|
+
} as unknown as Provider,
|
|
2677
2186
|
});
|
|
2678
2187
|
|
|
2188
|
+
// WHEN the orchestrator runs the turn
|
|
2679
2189
|
await runAgentLoopImpl(ctx, "hi", "msg-1", (msg) => events.push(msg));
|
|
2680
2190
|
|
|
2681
|
-
|
|
2191
|
+
// THEN the finally block clears per-turn state and the failure is
|
|
2192
|
+
// surfaced as a processing-failed conversation error
|
|
2193
|
+
expect(ctx.isProcessing()).toBe(false);
|
|
2682
2194
|
expect(ctx.abortController).toBeNull();
|
|
2683
|
-
expect(events.find((event) => event.type === "error")).toMatchObject({
|
|
2684
|
-
type: "error",
|
|
2685
|
-
code: "CONVERSATION_PROCESSING_FAILED",
|
|
2686
|
-
errorCategory: "processing_failed",
|
|
2687
|
-
});
|
|
2688
2195
|
expect(
|
|
2689
2196
|
events.find((event) => event.type === "conversation_error"),
|
|
2690
2197
|
).toMatchObject({
|
|
@@ -2695,41 +2202,19 @@ describe("session-agent-loop", () => {
|
|
|
2695
2202
|
});
|
|
2696
2203
|
|
|
2697
2204
|
test("drains queue after completion", async () => {
|
|
2205
|
+
// GIVEN a real loop that answers in a single text turn
|
|
2698
2206
|
let drainReason: string | undefined;
|
|
2699
2207
|
const ctx = makeCtx({
|
|
2700
|
-
|
|
2701
|
-
messages: Message[],
|
|
2702
|
-
onEvent: (event: AgentEvent) => void,
|
|
2703
|
-
) => {
|
|
2704
|
-
onEvent({
|
|
2705
|
-
type: "message_complete",
|
|
2706
|
-
message: {
|
|
2707
|
-
role: "assistant",
|
|
2708
|
-
content: [{ type: "text", text: "ok" }],
|
|
2709
|
-
},
|
|
2710
|
-
});
|
|
2711
|
-
onEvent({
|
|
2712
|
-
type: "usage",
|
|
2713
|
-
inputTokens: 10,
|
|
2714
|
-
outputTokens: 5,
|
|
2715
|
-
model: "test",
|
|
2716
|
-
providerDurationMs: 50,
|
|
2717
|
-
});
|
|
2718
|
-
return [
|
|
2719
|
-
...messages,
|
|
2720
|
-
{
|
|
2721
|
-
role: "assistant" as const,
|
|
2722
|
-
content: [{ type: "text", text: "ok" }] as ContentBlock[],
|
|
2723
|
-
},
|
|
2724
|
-
];
|
|
2725
|
-
},
|
|
2208
|
+
providerResponses: [textResponse("ok")],
|
|
2726
2209
|
drainQueue: (reason: string) => {
|
|
2727
2210
|
drainReason = reason;
|
|
2728
2211
|
},
|
|
2729
2212
|
} as unknown as Partial<AgentLoopConversationContext>);
|
|
2730
2213
|
|
|
2214
|
+
// WHEN the orchestrator runs the turn to completion
|
|
2731
2215
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2732
2216
|
|
|
2217
|
+
// THEN the queue is drained with the loop-complete reason
|
|
2733
2218
|
expect(drainReason).toBe("loop_complete");
|
|
2734
2219
|
});
|
|
2735
2220
|
});
|
|
@@ -2848,7 +2333,7 @@ describe("session-agent-loop", () => {
|
|
|
2848
2333
|
isUserMessage: true,
|
|
2849
2334
|
});
|
|
2850
2335
|
|
|
2851
|
-
expect(ctx.
|
|
2336
|
+
expect(ctx.isProcessing()).toBe(false);
|
|
2852
2337
|
expect(ctx.abortController).toBeNull();
|
|
2853
2338
|
expect(ctx.currentRequestId).toBeUndefined();
|
|
2854
2339
|
});
|
|
@@ -2958,24 +2443,17 @@ describe("session-agent-loop", () => {
|
|
|
2958
2443
|
test("synthesizes error assistant message when provider returns no response", async () => {
|
|
2959
2444
|
const events: ServerMessage[] = [];
|
|
2960
2445
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
providerDurationMs: 50,
|
|
2973
|
-
});
|
|
2974
|
-
// Return same messages (no assistant message appended)
|
|
2975
|
-
return messages;
|
|
2976
|
-
};
|
|
2977
|
-
|
|
2978
|
-
const ctx = makeCtx({ agentLoopRun });
|
|
2446
|
+
// GIVEN a real loop whose provider rejects with a generic error
|
|
2447
|
+
// (non-ordering, non-context-too-large) so the loop emits `error` and
|
|
2448
|
+
// the orchestrator sets `providerErrorUserMessage`.
|
|
2449
|
+
const ctx = makeCtx({
|
|
2450
|
+
loopProvider: {
|
|
2451
|
+
name: "mock-provider",
|
|
2452
|
+
async sendMessage() {
|
|
2453
|
+
throw new Error("Internal processing failure");
|
|
2454
|
+
},
|
|
2455
|
+
} as unknown as Provider,
|
|
2456
|
+
});
|
|
2979
2457
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2980
2458
|
|
|
2981
2459
|
// The error should be sent as a conversation_error (not as an
|
|
@@ -2999,26 +2477,19 @@ describe("session-agent-loop", () => {
|
|
|
2999
2477
|
// sweep would wrong-attach this row to the wrong assistant message.
|
|
3000
2478
|
const events: ServerMessage[] = [];
|
|
3001
2479
|
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
error: new Error("upstream 500"),
|
|
3016
|
-
});
|
|
3017
|
-
// Provider returned no assistant content — same messages back.
|
|
3018
|
-
return messages;
|
|
3019
|
-
};
|
|
3020
|
-
|
|
3021
|
-
const ctx = makeCtx({ agentLoopRun });
|
|
2480
|
+
// GIVEN a real loop whose provider rejects: the loop emits
|
|
2481
|
+
// `provider_error` (writing an `llm_request_logs` row with
|
|
2482
|
+
// messageId=null — the orphan we link) then `error` (which sets
|
|
2483
|
+
// `state.providerErrorUserMessage`, activating the synthetic-message
|
|
2484
|
+
// branch below the loop).
|
|
2485
|
+
const ctx = makeCtx({
|
|
2486
|
+
loopProvider: {
|
|
2487
|
+
name: "mock-provider",
|
|
2488
|
+
async sendMessage() {
|
|
2489
|
+
throw new Error("upstream 500");
|
|
2490
|
+
},
|
|
2491
|
+
} as unknown as Provider,
|
|
2492
|
+
});
|
|
3022
2493
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
3023
2494
|
|
|
3024
2495
|
// The orphan was written with messageId=undefined.
|
|
@@ -3044,6 +2515,520 @@ describe("session-agent-loop", () => {
|
|
|
3044
2515
|
});
|
|
3045
2516
|
});
|
|
3046
2517
|
|
|
2518
|
+
describe("B3 pre-allocation: indexing + cleanup", () => {
|
|
2519
|
+
test("handleMessageComplete indexes and projects the finalized assistant row", async () => {
|
|
2520
|
+
// The pre-B3 path inserted assistant rows via `addMessage`, which ran
|
|
2521
|
+
// the memory indexer and the conversation-attention projector as
|
|
2522
|
+
// side-effects of the insert. B3 splits the write into
|
|
2523
|
+
// `reserveMessage` + `updateMessageContent`, both of which are CRUD-only,
|
|
2524
|
+
// so the indexing + projection calls had to be re-driven explicitly
|
|
2525
|
+
// after `updateContent` succeeds. Codex P1 caught a regression where
|
|
2526
|
+
// this path was missing entirely; this test pins it down.
|
|
2527
|
+
mockMessageById = {
|
|
2528
|
+
id: "msg-reserve",
|
|
2529
|
+
conversationId: "test-conv",
|
|
2530
|
+
createdAt: 1234567,
|
|
2531
|
+
role: "assistant",
|
|
2532
|
+
content: "[]",
|
|
2533
|
+
metadata: null,
|
|
2534
|
+
};
|
|
2535
|
+
// Force attention projection to report a state change so we also
|
|
2536
|
+
// observe the sync-invalidation publish path on the same turn.
|
|
2537
|
+
projectAssistantMessageMock.mockImplementationOnce(() => true);
|
|
2538
|
+
|
|
2539
|
+
// GIVEN a real loop that answers with a single finalized assistant turn
|
|
2540
|
+
const ctx = makeCtx({
|
|
2541
|
+
providerResponses: [textResponse("indexed reply")],
|
|
2542
|
+
});
|
|
2543
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2544
|
+
|
|
2545
|
+
// Indexer fired with the reserved row's id + the finalized content.
|
|
2546
|
+
expect(indexMessageNowMock).toHaveBeenCalledTimes(1);
|
|
2547
|
+
const indexCallArgs = indexMessageNowMock.mock.calls[0] as unknown as [
|
|
2548
|
+
{
|
|
2549
|
+
messageId: string;
|
|
2550
|
+
conversationId: string;
|
|
2551
|
+
role: string;
|
|
2552
|
+
content: string;
|
|
2553
|
+
createdAt: number;
|
|
2554
|
+
scopeId: string;
|
|
2555
|
+
},
|
|
2556
|
+
unknown,
|
|
2557
|
+
];
|
|
2558
|
+
const indexCall = indexCallArgs[0];
|
|
2559
|
+
expect(indexCall).toMatchObject({
|
|
2560
|
+
messageId: "msg-reserve",
|
|
2561
|
+
conversationId: "test-conv",
|
|
2562
|
+
role: "assistant",
|
|
2563
|
+
createdAt: 1234567,
|
|
2564
|
+
scopeId: "default",
|
|
2565
|
+
});
|
|
2566
|
+
expect(indexCall.content).toContain("indexed reply");
|
|
2567
|
+
|
|
2568
|
+
// Attention projector fired with the same row coordinates.
|
|
2569
|
+
expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
|
|
2570
|
+
const projectCall = projectAssistantMessageMock.mock
|
|
2571
|
+
.calls[0] as unknown as [
|
|
2572
|
+
{ conversationId: string; messageId: string; messageAt: number },
|
|
2573
|
+
];
|
|
2574
|
+
expect(projectCall[0]).toEqual({
|
|
2575
|
+
conversationId: "test-conv",
|
|
2576
|
+
messageId: "msg-reserve",
|
|
2577
|
+
messageAt: 1234567,
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2580
|
+
// Projection reported a state change → sync invalidation fires with
|
|
2581
|
+
// the conversation `:metadata` tag. The mock also receives a
|
|
2582
|
+
// `:messages` invalidation from the orchestrator's
|
|
2583
|
+
// `publishLoopMessagesChanged` post-loop emit, so we filter by tag
|
|
2584
|
+
// rather than asserting a total call count.
|
|
2585
|
+
const metadataPublishes = (
|
|
2586
|
+
publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
|
|
2587
|
+
).filter((args) => args[0]?.includes("conversation:test-conv:metadata"));
|
|
2588
|
+
expect(metadataPublishes).toHaveLength(1);
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
test("handleMessageComplete skips sync invalidation when attention state unchanged", async () => {
|
|
2592
|
+
// Mirror of the previous test but with the default projector return
|
|
2593
|
+
// (`false`). The projection still runs every turn, but the sync
|
|
2594
|
+
// invalidation publish must be gated on attention-state movement to
|
|
2595
|
+
// avoid flooding clients with no-op metadata refreshes.
|
|
2596
|
+
mockMessageById = {
|
|
2597
|
+
id: "msg-reserve",
|
|
2598
|
+
conversationId: "test-conv",
|
|
2599
|
+
createdAt: 999,
|
|
2600
|
+
role: "assistant",
|
|
2601
|
+
content: "[]",
|
|
2602
|
+
metadata: null,
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
// GIVEN a real loop that answers with a single finalized assistant turn
|
|
2606
|
+
const ctx = makeCtx({ providerResponses: [textResponse("quiet")] });
|
|
2607
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2608
|
+
|
|
2609
|
+
expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
|
|
2610
|
+
// The mock will still receive a `:messages` invalidation from the
|
|
2611
|
+
// orchestrator's `publishLoopMessagesChanged` — filter to the
|
|
2612
|
+
// `:metadata` tag and assert it never landed.
|
|
2613
|
+
const metadataPublishes = (
|
|
2614
|
+
publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
|
|
2615
|
+
).filter((args) => args[0]?.includes("conversation:test-conv:metadata"));
|
|
2616
|
+
expect(metadataPublishes).toHaveLength(0);
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
test("handleLlmCallStarted deletes a stranded reservation before reserving a new row", async () => {
|
|
2620
|
+
// Simulates a retry path: the first LLM call reserves an assistant row
|
|
2621
|
+
// but exits without `message_complete` (e.g. context-overflow rescue,
|
|
2622
|
+
// ordering-error rescue, image-overflow rescue). The next
|
|
2623
|
+
// `llm_call_started` must delete the stranded row so the transcript
|
|
2624
|
+
// does not accumulate empty assistant bubbles.
|
|
2625
|
+
reserveMessageMock
|
|
2626
|
+
.mockImplementationOnce(async () => ({ id: "msg-strand-A" }))
|
|
2627
|
+
.mockImplementationOnce(async () => ({ id: "msg-strand-B" }));
|
|
2628
|
+
// Indexer/projector mocks default to no-op; no finalized row in this
|
|
2629
|
+
// test, so `mockMessageById` stays null.
|
|
2630
|
+
|
|
2631
|
+
// A single reducer tier converges the oversized context so the
|
|
2632
|
+
// orchestrator re-enters the loop after the first call fails.
|
|
2633
|
+
mockReducerStepFn = (msgs: Message[]) => ({
|
|
2634
|
+
messages: msgs,
|
|
2635
|
+
tier: "forced_compaction",
|
|
2636
|
+
state: {
|
|
2637
|
+
appliedTiers: ["forced_compaction"],
|
|
2638
|
+
injectionMode: "full",
|
|
2639
|
+
exhausted: false,
|
|
2640
|
+
},
|
|
2641
|
+
estimatedTokens: 5000,
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
// GIVEN a real loop whose first call rejects with context-too-large
|
|
2645
|
+
// (reserving msg-strand-A but never finalizing it), then recovers via
|
|
2646
|
+
// convergence on re-entry. The re-entry's `llm_call_started` must
|
|
2647
|
+
// delete the stranded msg-strand-A before reserving msg-strand-B.
|
|
2648
|
+
const ctx = makeCtx({
|
|
2649
|
+
providerResponses: [
|
|
2650
|
+
new Error("context_length_exceeded"),
|
|
2651
|
+
textResponse("retry succeeded"),
|
|
2652
|
+
],
|
|
2653
|
+
contextWindowManager: {
|
|
2654
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
2655
|
+
maybeCompact: async () => ({ compacted: false }),
|
|
2656
|
+
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
2657
|
+
});
|
|
2658
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2659
|
+
|
|
2660
|
+
// Exactly one delete fires — for msg-strand-A, before the second
|
|
2661
|
+
// reserve. The second reservation is committed via `updateContent`
|
|
2662
|
+
// (not deleted), and after the run completes
|
|
2663
|
+
// `assistantRowAwaitingFinalization` is false, so no further delete
|
|
2664
|
+
// is attempted on shutdown.
|
|
2665
|
+
expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
|
|
2666
|
+
const strandDeleteCall = deleteMessageByIdMock.mock
|
|
2667
|
+
.calls[0] as unknown as [string];
|
|
2668
|
+
expect(strandDeleteCall[0]).toBe("msg-strand-A");
|
|
2669
|
+
expect(reserveMessageMock).toHaveBeenCalledTimes(2);
|
|
2670
|
+
});
|
|
2671
|
+
|
|
2672
|
+
test("provider-error branch deletes the orphaned reservation and repoints lastAssistantMessageId", async () => {
|
|
2673
|
+
// Codex P2 regression: B3 reserves an empty assistant row at
|
|
2674
|
+
// `llm_call_started`. When the call exits via the provider-error
|
|
2675
|
+
// branch (no `message_complete`), the synthetic error message is
|
|
2676
|
+
// inserted separately. Without cleanup the transcript would carry
|
|
2677
|
+
// both the empty reserved row AND the error message, and
|
|
2678
|
+
// `syncLastAssistantMessageToDisk` (which reads
|
|
2679
|
+
// `state.lastAssistantMessageId`) would mis-target the deleted
|
|
2680
|
+
// reservation id.
|
|
2681
|
+
reserveMessageMock.mockImplementationOnce(async () => ({
|
|
2682
|
+
id: "msg-orphaned-reservation",
|
|
2683
|
+
}));
|
|
2684
|
+
|
|
2685
|
+
// GIVEN a real loop that reserves an assistant row at
|
|
2686
|
+
// `llm_call_started`, then whose provider rejects: the loop emits
|
|
2687
|
+
// `provider_error` (writing the llm_request_log row) and `error`
|
|
2688
|
+
// (arming `state.providerErrorUserMessage`), exiting with no
|
|
2689
|
+
// `message_complete` so the synthetic-error branch below the loop
|
|
2690
|
+
// fires.
|
|
2691
|
+
const ctx = makeCtx({
|
|
2692
|
+
loopProvider: {
|
|
2693
|
+
name: "mock-provider",
|
|
2694
|
+
async sendMessage() {
|
|
2695
|
+
throw new Error("upstream 500");
|
|
2696
|
+
},
|
|
2697
|
+
} as unknown as Provider,
|
|
2698
|
+
});
|
|
2699
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2700
|
+
|
|
2701
|
+
// The orphan was deleted exactly once, before the synthetic error
|
|
2702
|
+
// message landed.
|
|
2703
|
+
expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
|
|
2704
|
+
const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
|
|
2705
|
+
string,
|
|
2706
|
+
];
|
|
2707
|
+
expect(deleteCall[0]).toBe("msg-orphaned-reservation");
|
|
2708
|
+
|
|
2709
|
+
// Post-loop `syncLastAssistantMessageToDisk` targets the synthetic
|
|
2710
|
+
// error row's id (`mock-msg-id` from the mocked `addMessage`), NOT
|
|
2711
|
+
// the deleted reservation id. This is the externally-observable
|
|
2712
|
+
// proof that `state.lastAssistantMessageId` was repointed.
|
|
2713
|
+
expect(syncMessageToDiskMock).toHaveBeenCalled();
|
|
2714
|
+
const syncCalls = syncMessageToDiskMock.mock.calls as unknown as Array<
|
|
2715
|
+
[string, string, number]
|
|
2716
|
+
>;
|
|
2717
|
+
const lastSync = syncCalls[syncCalls.length - 1];
|
|
2718
|
+
expect(lastSync?.[1]).toBe("mock-msg-id");
|
|
2719
|
+
expect(lastSync?.[1]).not.toBe("msg-orphaned-reservation");
|
|
2720
|
+
});
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
describe("partial persistence", () => {
|
|
2724
|
+
// The legacy flow reserves an empty assistant row at `llm_call_started`
|
|
2725
|
+
// (`content: "[]"`) and never touches it again until
|
|
2726
|
+
// `handleMessageComplete` fires the single authoritative
|
|
2727
|
+
// `updateContent`. Between those events the row is empty for the full
|
|
2728
|
+
// duration of a turn — a browser refresh mid-turn sees nothing where
|
|
2729
|
+
// the in-progress assistant reply should be.
|
|
2730
|
+
//
|
|
2731
|
+
// Partial persistence closes that durability gap with a debounced
|
|
2732
|
+
// flush from `handleTextDelta` (250ms timer). `handleToolUse`
|
|
2733
|
+
// intentionally does NOT flush — `AgentLoop.run` emits `tool_use`
|
|
2734
|
+
// strictly AFTER `message_complete`, so any flush from that handler
|
|
2735
|
+
// would land after the authoritative finalize and overwrite the
|
|
2736
|
+
// finalized row. The indexer + projector still fire ONLY at
|
|
2737
|
+
// `message_complete` — partial rows are never indexed.
|
|
2738
|
+
//
|
|
2739
|
+
// These tests pin down the wire-level contract by counting
|
|
2740
|
+
// `updateMessageContent` calls and inspecting the JSON payload of the
|
|
2741
|
+
// partial-flush writes. The indexing / sync-invalidation paths are
|
|
2742
|
+
// covered by the pre-allocation block above.
|
|
2743
|
+
|
|
2744
|
+
test("debounced time gate flushes one partial write after PARTIAL_PERSIST_DEBOUNCE_MS", async () => {
|
|
2745
|
+
mockMessageById = {
|
|
2746
|
+
id: "msg-reserve",
|
|
2747
|
+
conversationId: "test-conv",
|
|
2748
|
+
createdAt: 1234567,
|
|
2749
|
+
role: "assistant",
|
|
2750
|
+
content: "[]",
|
|
2751
|
+
metadata: null,
|
|
2752
|
+
};
|
|
2753
|
+
|
|
2754
|
+
// GIVEN a real loop whose provider streams two small deltas (each under
|
|
2755
|
+
// the 1024-char size gate) then holds the turn open past the 250ms
|
|
2756
|
+
// debounce window before completing, so a single debounced partial
|
|
2757
|
+
// flush lands before `message_complete`.
|
|
2758
|
+
const ctx = makeCtx({
|
|
2759
|
+
loopProvider: {
|
|
2760
|
+
name: "mock-provider",
|
|
2761
|
+
async sendMessage(_messages, options) {
|
|
2762
|
+
options?.onEvent?.({ type: "text_delta", text: "Hello, " });
|
|
2763
|
+
options?.onEvent?.({ type: "text_delta", text: "world." });
|
|
2764
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
2765
|
+
return textResponse("Hello, world.");
|
|
2766
|
+
},
|
|
2767
|
+
},
|
|
2768
|
+
});
|
|
2769
|
+
|
|
2770
|
+
// WHEN the orchestrator runs the turn to completion
|
|
2771
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2772
|
+
|
|
2773
|
+
// Exactly two `updateContent` calls land:
|
|
2774
|
+
// 1. the debounced partial flush after both deltas accumulated, and
|
|
2775
|
+
// 2. the final authoritative flush in `handleMessageComplete`.
|
|
2776
|
+
// Without the debounce gate this would be one-per-delta + one final
|
|
2777
|
+
// (3). Without the partial flush at all it would be just 1.
|
|
2778
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(2);
|
|
2779
|
+
const calls = updateMessageContentMock.mock.calls as unknown as Array<
|
|
2780
|
+
[string, string]
|
|
2781
|
+
>;
|
|
2782
|
+
const partialFlush = calls[0];
|
|
2783
|
+
expect(partialFlush?.[0]).toBe("msg-reserve");
|
|
2784
|
+
const partialBlocks = JSON.parse(partialFlush?.[1] ?? "[]") as Array<{
|
|
2785
|
+
type: string;
|
|
2786
|
+
text?: string;
|
|
2787
|
+
}>;
|
|
2788
|
+
expect(partialBlocks).toEqual([{ type: "text", text: "Hello, world." }]);
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
test("handleToolUse does NOT trigger a partial flush of its own", async () => {
|
|
2792
|
+
// `AgentLoop.run` emits `tool_use` strictly AFTER `message_complete`,
|
|
2793
|
+
// so a flush from the tool_use handler would land after the
|
|
2794
|
+
// authoritative final `updateContent` and overwrite the finalized
|
|
2795
|
+
// row (Codex P1 / Vargas review feedback). The handler must be a
|
|
2796
|
+
// no-op for the partial-persist accumulator.
|
|
2797
|
+
mockMessageById = {
|
|
2798
|
+
id: "msg-reserve",
|
|
2799
|
+
conversationId: "test-conv",
|
|
2800
|
+
createdAt: 1234567,
|
|
2801
|
+
role: "assistant",
|
|
2802
|
+
content: "[]",
|
|
2803
|
+
metadata: null,
|
|
2804
|
+
};
|
|
2805
|
+
|
|
2806
|
+
// GIVEN a real loop that runs one tool turn — the loop emits `tool_use`
|
|
2807
|
+
// strictly AFTER `message_complete` — and then answers with a final
|
|
2808
|
+
// text turn. The tool executor returns immediately.
|
|
2809
|
+
const ctx = makeCtx({
|
|
2810
|
+
providerResponses: [
|
|
2811
|
+
toolUseResponse("tu-no-flush", "file_read", { path: "/foo" }),
|
|
2812
|
+
textResponse("done"),
|
|
2813
|
+
],
|
|
2814
|
+
loopTools: [
|
|
2815
|
+
{
|
|
2816
|
+
name: "file_read",
|
|
2817
|
+
description: "Read a file",
|
|
2818
|
+
input_schema: {
|
|
2819
|
+
type: "object",
|
|
2820
|
+
properties: { path: { type: "string" } },
|
|
2821
|
+
},
|
|
2822
|
+
},
|
|
2823
|
+
],
|
|
2824
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
// WHEN the orchestrator runs the turn to completion
|
|
2828
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2829
|
+
|
|
2830
|
+
// Four authoritative writes land and no stray partial flush:
|
|
2831
|
+
// - one final flush per `message_complete` (the tool turn and the final
|
|
2832
|
+
// text turn), plus
|
|
2833
|
+
// - two grouped tool-result user-row writes (persist-on-arrival and the
|
|
2834
|
+
// turn-boundary finalize).
|
|
2835
|
+
// `handleToolUse` contributes no partial flush of its own; one would make
|
|
2836
|
+
// this 5. That stray flush is the regression this test guards against.
|
|
2837
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(4);
|
|
2838
|
+
});
|
|
2839
|
+
|
|
2840
|
+
test("handleMessageComplete clears any pending debounce timer before the final flush", async () => {
|
|
2841
|
+
mockMessageById = {
|
|
2842
|
+
id: "msg-reserve",
|
|
2843
|
+
conversationId: "test-conv",
|
|
2844
|
+
createdAt: 1234567,
|
|
2845
|
+
role: "assistant",
|
|
2846
|
+
content: "[]",
|
|
2847
|
+
metadata: null,
|
|
2848
|
+
};
|
|
2849
|
+
|
|
2850
|
+
// GIVEN a real loop whose first turn streams a short delta (scheduling a
|
|
2851
|
+
// debounce timer) and completes as a tool turn — so `message_complete`
|
|
2852
|
+
// arrives before the 250ms timer and clears it. The tool executor then
|
|
2853
|
+
// holds the loop open well past the original debounce window, proving a
|
|
2854
|
+
// late timer does NOT fire a stray partial flush, before a final text
|
|
2855
|
+
// turn ends the run.
|
|
2856
|
+
const ctx = makeCtx({
|
|
2857
|
+
providerResponses: [
|
|
2858
|
+
{
|
|
2859
|
+
content: [
|
|
2860
|
+
{ type: "text", text: "Quick reply." },
|
|
2861
|
+
{
|
|
2862
|
+
type: "tool_use",
|
|
2863
|
+
id: "tu-keep-alive",
|
|
2864
|
+
name: "file_read",
|
|
2865
|
+
input: {},
|
|
2866
|
+
},
|
|
2867
|
+
],
|
|
2868
|
+
model: "mock-model",
|
|
2869
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
2870
|
+
stopReason: "tool_use",
|
|
2871
|
+
},
|
|
2872
|
+
textResponse("done"),
|
|
2873
|
+
],
|
|
2874
|
+
loopTools: [
|
|
2875
|
+
{
|
|
2876
|
+
name: "file_read",
|
|
2877
|
+
description: "Read a file",
|
|
2878
|
+
input_schema: { type: "object", properties: {} },
|
|
2879
|
+
},
|
|
2880
|
+
],
|
|
2881
|
+
toolExecutor: async () => {
|
|
2882
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
2883
|
+
return { content: "ok", isError: false };
|
|
2884
|
+
},
|
|
2885
|
+
});
|
|
2886
|
+
|
|
2887
|
+
// WHEN the orchestrator runs the turn to completion
|
|
2888
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2889
|
+
|
|
2890
|
+
// Four authoritative writes land: one final flush per `message_complete`
|
|
2891
|
+
// (the tool turn and the final text turn) plus two grouped tool-result
|
|
2892
|
+
// user-row writes (persist-on-arrival and the turn-boundary finalize).
|
|
2893
|
+
// The debounced partial would have fired around T+250ms — during the tool
|
|
2894
|
+
// executor's hold — but the timer-clear at the top of
|
|
2895
|
+
// `handleMessageComplete` cancels it, so no stray fifth flush appears.
|
|
2896
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(4);
|
|
2897
|
+
});
|
|
2898
|
+
|
|
2899
|
+
test("partial flushes never trigger the indexer or attention projector", async () => {
|
|
2900
|
+
mockMessageById = {
|
|
2901
|
+
id: "msg-reserve",
|
|
2902
|
+
conversationId: "test-conv",
|
|
2903
|
+
createdAt: 1234567,
|
|
2904
|
+
role: "assistant",
|
|
2905
|
+
content: "[]",
|
|
2906
|
+
metadata: null,
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
// GIVEN a real loop whose provider streams a delta then holds the turn
|
|
2910
|
+
// open past the 250ms debounce window so the partial flush lands BEFORE
|
|
2911
|
+
// `message_complete`. The indexer/projector counts are snapshotted at
|
|
2912
|
+
// that mid-turn point (after the partial flush, before completion).
|
|
2913
|
+
let snapshot: [number, number] | undefined;
|
|
2914
|
+
const ctx = makeCtx({
|
|
2915
|
+
loopProvider: {
|
|
2916
|
+
name: "mock-provider",
|
|
2917
|
+
async sendMessage(_messages, options) {
|
|
2918
|
+
options?.onEvent?.({ type: "text_delta", text: "hello world" });
|
|
2919
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
2920
|
+
snapshot = [
|
|
2921
|
+
indexMessageNowMock.mock.calls.length,
|
|
2922
|
+
projectAssistantMessageMock.mock.calls.length,
|
|
2923
|
+
];
|
|
2924
|
+
return textResponse("hello world");
|
|
2925
|
+
},
|
|
2926
|
+
},
|
|
2927
|
+
});
|
|
2928
|
+
|
|
2929
|
+
// WHEN the orchestrator runs the turn to completion
|
|
2930
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2931
|
+
|
|
2932
|
+
expect(snapshot).toBeDefined();
|
|
2933
|
+
// Indexer + projector were both ZERO during the mid-turn partial
|
|
2934
|
+
// flush — they only fire from `handleMessageComplete` after the
|
|
2935
|
+
// authoritative `updateContent`.
|
|
2936
|
+
expect(snapshot![0]).toBe(0);
|
|
2937
|
+
expect(snapshot![1]).toBe(0);
|
|
2938
|
+
// After the loop completes the indexer + projector each ran exactly
|
|
2939
|
+
// once (the pre-allocation finalize path).
|
|
2940
|
+
expect(indexMessageNowMock).toHaveBeenCalledTimes(1);
|
|
2941
|
+
expect(projectAssistantMessageMock).toHaveBeenCalledTimes(1);
|
|
2942
|
+
});
|
|
2943
|
+
|
|
2944
|
+
test("partial flushes redact secrets from text blocks before writing", async () => {
|
|
2945
|
+
mockMessageById = {
|
|
2946
|
+
id: "msg-reserve",
|
|
2947
|
+
conversationId: "test-conv",
|
|
2948
|
+
createdAt: 1234567,
|
|
2949
|
+
role: "assistant",
|
|
2950
|
+
content: "[]",
|
|
2951
|
+
metadata: null,
|
|
2952
|
+
};
|
|
2953
|
+
// A GitHub PAT-shaped token mid-stream — the redaction discipline
|
|
2954
|
+
// mirrors `handleMessageComplete`'s final flush so a refresh mid-turn
|
|
2955
|
+
// never sees plaintext credentials in the persisted row.
|
|
2956
|
+
const ghToken = "ghp_" + "a".repeat(36);
|
|
2957
|
+
const payload = "Here's the key: " + ghToken + " enjoy.";
|
|
2958
|
+
|
|
2959
|
+
// GIVEN a real loop whose provider streams the PAT-bearing payload as a
|
|
2960
|
+
// delta then holds the turn open past the 250ms debounce window so the
|
|
2961
|
+
// partial flush lands before `message_complete`.
|
|
2962
|
+
const ctx = makeCtx({
|
|
2963
|
+
loopProvider: {
|
|
2964
|
+
name: "mock-provider",
|
|
2965
|
+
async sendMessage(_messages, options) {
|
|
2966
|
+
options?.onEvent?.({ type: "text_delta", text: payload });
|
|
2967
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
2968
|
+
return textResponse(payload);
|
|
2969
|
+
},
|
|
2970
|
+
},
|
|
2971
|
+
});
|
|
2972
|
+
|
|
2973
|
+
// WHEN the orchestrator runs the turn to completion
|
|
2974
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2975
|
+
|
|
2976
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(2);
|
|
2977
|
+
const partialPayload = (
|
|
2978
|
+
updateMessageContentMock.mock.calls[0] as unknown as [string, string]
|
|
2979
|
+
)[1];
|
|
2980
|
+
// The raw PAT must never appear in the persisted snapshot. The
|
|
2981
|
+
// redaction substitute is implementation-defined; the contract here
|
|
2982
|
+
// is "the literal token string is gone".
|
|
2983
|
+
expect(partialPayload).not.toContain(ghToken);
|
|
2984
|
+
});
|
|
2985
|
+
|
|
2986
|
+
test("provider-error cleanup deletes a row that has accumulated partial content", async () => {
|
|
2987
|
+
// Regression check: the pre-allocation orphan-cleanup branch
|
|
2988
|
+
// already deletes the reserved row when the LLM call exits via
|
|
2989
|
+
// `provider_error`. Partial-persist writes content to that row
|
|
2990
|
+
// mid-turn; the cleanup must still fire and the row (along with
|
|
2991
|
+
// its partial content) must still be deleted before the synthetic
|
|
2992
|
+
// error message lands.
|
|
2993
|
+
reserveMessageMock.mockImplementationOnce(async () => ({
|
|
2994
|
+
id: "msg-orphan-with-partial",
|
|
2995
|
+
}));
|
|
2996
|
+
|
|
2997
|
+
// GIVEN a real loop whose provider streams a delta — landing a debounced
|
|
2998
|
+
// partial flush on the reserved row — then rejects, so the loop emits
|
|
2999
|
+
// `provider_error` and `error` and exits with no `message_complete`.
|
|
3000
|
+
const ctx = makeCtx({
|
|
3001
|
+
loopProvider: {
|
|
3002
|
+
name: "mock-provider",
|
|
3003
|
+
async sendMessage(_messages, options) {
|
|
3004
|
+
options?.onEvent?.({ type: "text_delta", text: "hello world" });
|
|
3005
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
3006
|
+
throw new Error("upstream 500");
|
|
3007
|
+
},
|
|
3008
|
+
},
|
|
3009
|
+
});
|
|
3010
|
+
|
|
3011
|
+
// WHEN the orchestrator runs the turn
|
|
3012
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
3013
|
+
|
|
3014
|
+
// Partial flush fired exactly once (before the provider error).
|
|
3015
|
+
// The orphan row was then deleted; the synthetic error message is
|
|
3016
|
+
// inserted separately via `addMessage` (`mock-msg-id`) and never
|
|
3017
|
+
// touched by `updateContent`.
|
|
3018
|
+
const partialFlushes = (
|
|
3019
|
+
updateMessageContentMock.mock.calls as unknown as Array<
|
|
3020
|
+
[string, string]
|
|
3021
|
+
>
|
|
3022
|
+
).filter(([id]) => id === "msg-orphan-with-partial");
|
|
3023
|
+
expect(partialFlushes).toHaveLength(1);
|
|
3024
|
+
expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
|
|
3025
|
+
const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
|
|
3026
|
+
string,
|
|
3027
|
+
];
|
|
3028
|
+
expect(deleteCall[0]).toBe("msg-orphan-with-partial");
|
|
3029
|
+
});
|
|
3030
|
+
});
|
|
3031
|
+
|
|
3047
3032
|
describe("pkbSystemReminderBlock metadata persistence", () => {
|
|
3048
3033
|
test("persists pkbSystemReminderBlock in full mode with PKB active", async () => {
|
|
3049
3034
|
const reminder = "<system_reminder>\npkb content\n</system_reminder>";
|
|
@@ -3524,50 +3509,32 @@ describe("session-agent-loop", () => {
|
|
|
3524
3509
|
compactableStartIndex: 0,
|
|
3525
3510
|
};
|
|
3526
3511
|
|
|
3527
|
-
const rawMidLoopBasis: Message[] = [
|
|
3528
|
-
{
|
|
3529
|
-
role: "user",
|
|
3530
|
-
content: [{ type: "text", text: "fresh DB basis user row" }],
|
|
3531
|
-
},
|
|
3532
|
-
{
|
|
3533
|
-
role: "assistant",
|
|
3534
|
-
content: [{ type: "text", text: "partial assistant response" }],
|
|
3535
|
-
},
|
|
3536
|
-
];
|
|
3537
3512
|
const maybeCompactInputs: Message[][] = [];
|
|
3538
|
-
let runCount = 0;
|
|
3539
|
-
const agentLoopRun: AgentLoopRun = async (
|
|
3540
|
-
messages,
|
|
3541
|
-
_onEvent,
|
|
3542
|
-
_signal,
|
|
3543
|
-
_reqId,
|
|
3544
|
-
onCheckpoint,
|
|
3545
|
-
) => {
|
|
3546
|
-
runCount++;
|
|
3547
|
-
if (runCount === 1) {
|
|
3548
|
-
mockEstimateTokens = 90_000;
|
|
3549
|
-
const decision = await onCheckpoint?.({
|
|
3550
|
-
turnIndex: 0,
|
|
3551
|
-
toolCount: 1,
|
|
3552
|
-
hasToolUse: true,
|
|
3553
|
-
history: messages,
|
|
3554
|
-
});
|
|
3555
|
-
mockEstimateTokens = 1000;
|
|
3556
|
-
if (decision === "yield") {
|
|
3557
|
-
return rawMidLoopBasis;
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
return [
|
|
3561
|
-
...messages,
|
|
3562
|
-
{
|
|
3563
|
-
role: "assistant" as const,
|
|
3564
|
-
content: [{ type: "text" as const, text: "final response" }],
|
|
3565
|
-
},
|
|
3566
|
-
];
|
|
3567
|
-
};
|
|
3568
3513
|
|
|
3514
|
+
// AND a real loop that runs one tool turn and then a final text turn.
|
|
3515
|
+
// The tool executor raises the token estimate above the mid-loop budget
|
|
3516
|
+
// threshold so the loop compacts in place at the post-tool checkpoint —
|
|
3517
|
+
// over its own in-loop history, which does not match the loaded Slack
|
|
3518
|
+
// rows.
|
|
3569
3519
|
const ctx = makeCtx({
|
|
3570
|
-
|
|
3520
|
+
providerResponses: [
|
|
3521
|
+
toolUseResponse("tu-mid-loop", "file_read", { path: "/foo" }),
|
|
3522
|
+
textResponse("final response"),
|
|
3523
|
+
],
|
|
3524
|
+
loopTools: [
|
|
3525
|
+
{
|
|
3526
|
+
name: "file_read",
|
|
3527
|
+
description: "Read a file",
|
|
3528
|
+
input_schema: {
|
|
3529
|
+
type: "object",
|
|
3530
|
+
properties: { path: { type: "string" } },
|
|
3531
|
+
},
|
|
3532
|
+
},
|
|
3533
|
+
],
|
|
3534
|
+
toolExecutor: async () => {
|
|
3535
|
+
mockEstimateTokens = 90_000;
|
|
3536
|
+
return { content: "ok", isError: false };
|
|
3537
|
+
},
|
|
3571
3538
|
channelCapabilities: {
|
|
3572
3539
|
channel: "slack",
|
|
3573
3540
|
dashboardCapable: false,
|
|
@@ -3604,6 +3571,9 @@ describe("session-agent-loop", () => {
|
|
|
3604
3571
|
summaryText: "",
|
|
3605
3572
|
};
|
|
3606
3573
|
}
|
|
3574
|
+
// The mid-loop gate compacted its in-loop basis; drop the estimate
|
|
3575
|
+
// back under budget so the post-compaction provider call proceeds.
|
|
3576
|
+
mockEstimateTokens = 1000;
|
|
3607
3577
|
return {
|
|
3608
3578
|
compacted: true,
|
|
3609
3579
|
messages: [
|
|
@@ -3632,7 +3602,9 @@ describe("session-agent-loop", () => {
|
|
|
3632
3602
|
await runAgentLoopImpl(ctx, "next reply", "user-msg-mid-loop", () => {});
|
|
3633
3603
|
|
|
3634
3604
|
expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
|
|
3635
|
-
|
|
3605
|
+
// The mid-loop gate compacts the loop's own in-loop history, never the
|
|
3606
|
+
// loaded Slack rows — the mismatch this test guards against.
|
|
3607
|
+
expect(maybeCompactInputs[1]).not.toBe(renderedSlackMessages);
|
|
3636
3608
|
expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
|
|
3637
3609
|
null,
|
|
3638
3610
|
2,
|
|
@@ -3905,62 +3877,32 @@ describe("session-agent-loop", () => {
|
|
|
3905
3877
|
estimatedTokens: 5000,
|
|
3906
3878
|
});
|
|
3907
3879
|
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
inputTokens: 100,
|
|
3921
|
-
outputTokens: 0,
|
|
3922
|
-
model: "test-model",
|
|
3923
|
-
providerDurationMs: 50,
|
|
3924
|
-
});
|
|
3925
|
-
return [
|
|
3926
|
-
...messages,
|
|
3927
|
-
{
|
|
3928
|
-
role: "assistant" as const,
|
|
3929
|
-
content: [{ type: "text", text: "partial" }] as ContentBlock[],
|
|
3930
|
-
},
|
|
3931
|
-
];
|
|
3932
|
-
}
|
|
3933
|
-
onEvent({
|
|
3934
|
-
type: "message_complete",
|
|
3935
|
-
message: {
|
|
3936
|
-
role: "assistant",
|
|
3937
|
-
content: [{ type: "text", text: "recovered" }],
|
|
3938
|
-
},
|
|
3939
|
-
});
|
|
3940
|
-
onEvent({
|
|
3941
|
-
type: "usage",
|
|
3942
|
-
inputTokens: 50,
|
|
3943
|
-
outputTokens: 25,
|
|
3944
|
-
model: "test-model",
|
|
3945
|
-
providerDurationMs: 100,
|
|
3946
|
-
});
|
|
3947
|
-
return [
|
|
3948
|
-
...messages,
|
|
3880
|
+
// GIVEN a real loop that appends a tool turn (so the run reports
|
|
3881
|
+
// `appendedNewMessages`) and then rejects with a context-too-large
|
|
3882
|
+
// error on the following call — the orchestrator strips that appended
|
|
3883
|
+
// history during its bounded convergence path before a final call
|
|
3884
|
+
// recovers.
|
|
3885
|
+
const ctx = makeCtx({
|
|
3886
|
+
providerResponses: [
|
|
3887
|
+
toolUseResponse("t1", "file_read", {}),
|
|
3888
|
+
new Error("context_length_exceeded"),
|
|
3889
|
+
textResponse("recovered"),
|
|
3890
|
+
],
|
|
3891
|
+
loopTools: [
|
|
3949
3892
|
{
|
|
3950
|
-
|
|
3951
|
-
|
|
3893
|
+
name: "file_read",
|
|
3894
|
+
description: "Read a file",
|
|
3895
|
+
input_schema: { type: "object", properties: {} },
|
|
3952
3896
|
},
|
|
3953
|
-
]
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
const ctx = makeCtx({
|
|
3957
|
-
agentLoopRun,
|
|
3897
|
+
],
|
|
3898
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
3958
3899
|
contextWindowManager: {
|
|
3959
3900
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3960
3901
|
maybeCompact: async () => ({ compacted: false }),
|
|
3961
3902
|
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
3962
3903
|
});
|
|
3963
3904
|
|
|
3905
|
+
// WHEN the orchestrator runs the turn to completion
|
|
3964
3906
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
3965
3907
|
|
|
3966
3908
|
const stripCalls = setConversationHistoryStrippedAtMock.mock.calls.filter(
|
|
@@ -3968,5 +3910,51 @@ describe("session-agent-loop", () => {
|
|
|
3968
3910
|
);
|
|
3969
3911
|
expect(stripCalls.length).toBeGreaterThanOrEqual(1);
|
|
3970
3912
|
});
|
|
3913
|
+
|
|
3914
|
+
test("strip-site marker write is non-fatal when the helper throws", async () => {
|
|
3915
|
+
setConversationHistoryStrippedAtMock.mockImplementation(() => {
|
|
3916
|
+
throw new Error("db write failed");
|
|
3917
|
+
});
|
|
3918
|
+
|
|
3919
|
+
mockReducerStepFn = (msgs: Message[]) => ({
|
|
3920
|
+
messages: msgs,
|
|
3921
|
+
tier: "forced_compaction",
|
|
3922
|
+
state: {
|
|
3923
|
+
appliedTiers: ["forced_compaction"],
|
|
3924
|
+
injectionMode: "full",
|
|
3925
|
+
exhausted: false,
|
|
3926
|
+
},
|
|
3927
|
+
estimatedTokens: 5000,
|
|
3928
|
+
});
|
|
3929
|
+
|
|
3930
|
+
// GIVEN a real loop that appends a tool turn and then rejects with a
|
|
3931
|
+
// context-too-large error on the following call, driving the
|
|
3932
|
+
// convergence strip whose marker-write helper is stubbed to throw,
|
|
3933
|
+
// before a final call recovers.
|
|
3934
|
+
const ctx = makeCtx({
|
|
3935
|
+
providerResponses: [
|
|
3936
|
+
toolUseResponse("t1", "file_read", {}),
|
|
3937
|
+
new Error("context_length_exceeded"),
|
|
3938
|
+
textResponse("recovered"),
|
|
3939
|
+
],
|
|
3940
|
+
loopTools: [
|
|
3941
|
+
{
|
|
3942
|
+
name: "file_read",
|
|
3943
|
+
description: "Read a file",
|
|
3944
|
+
input_schema: { type: "object", properties: {} },
|
|
3945
|
+
},
|
|
3946
|
+
],
|
|
3947
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
3948
|
+
contextWindowManager: {
|
|
3949
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3950
|
+
maybeCompact: async () => ({ compacted: false }),
|
|
3951
|
+
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
3952
|
+
});
|
|
3953
|
+
|
|
3954
|
+
// Must not throw — the strip-site marker write is wrapped in try/catch.
|
|
3955
|
+
await expect(
|
|
3956
|
+
runAgentLoopImpl(ctx, "hello", "msg-1", () => {}),
|
|
3957
|
+
).resolves.toBeUndefined();
|
|
3958
|
+
});
|
|
3971
3959
|
});
|
|
3972
3960
|
});
|