@vellumai/assistant 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +27 -0
- package/.env.example +22 -0
- package/Dockerfile +99 -0
- package/Dockerfile.sandbox +5 -0
- package/README.md +248 -0
- package/bun.lock +1723 -0
- package/bunfig.toml +2 -0
- package/docs/skills.md +158 -0
- package/drizzle/0000_dizzy_maggott.sql +301 -0
- package/drizzle/meta/0000_snapshot.json +1999 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/eslint.config.mjs +17 -0
- package/hook-templates/debug-prompt-logger/hook.json +7 -0
- package/hook-templates/debug-prompt-logger/run.sh +68 -0
- package/knip.json +9 -0
- package/package.json +70 -0
- package/scripts/capture-x-graphql.ts +545 -0
- package/scripts/ipc/check-contract-inventory.ts +104 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +166 -0
- package/scripts/ipc/generate-swift.ts +492 -0
- package/scripts/test-filesystem-tools.sh +48 -0
- package/scripts/test.sh +127 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2485 -0
- package/src/__tests__/account-registry.test.ts +245 -0
- package/src/__tests__/active-skill-tools.test.ts +378 -0
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/agent-loop-thinking.test.ts +81 -0
- package/src/__tests__/agent-loop.test.ts +1135 -0
- package/src/__tests__/anthropic-provider.test.ts +778 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +290 -0
- package/src/__tests__/app-bundler.test.ts +292 -0
- package/src/__tests__/app-executors.test.ts +613 -0
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/app-open-proxy.test.ts +62 -0
- package/src/__tests__/asset-materialize-tool.test.ts +452 -0
- package/src/__tests__/asset-search-tool.test.ts +477 -0
- package/src/__tests__/assistant-attachment-directive.test.ts +401 -0
- package/src/__tests__/assistant-attachments.test.ts +437 -0
- package/src/__tests__/assistant-event-hub.test.ts +226 -0
- package/src/__tests__/assistant-event.test.ts +123 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/attachments-store.test.ts +476 -0
- package/src/__tests__/attachments.test.ts +134 -0
- package/src/__tests__/audit-log-rotation.test.ts +154 -0
- package/src/__tests__/browser-fill-credential.test.ts +309 -0
- package/src/__tests__/browser-manager.test.ts +203 -0
- package/src/__tests__/browser-runtime-check.test.ts +55 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +68 -0
- package/src/__tests__/browser-skill-endstate.test.ts +195 -0
- package/src/__tests__/bundle-scanner.test.ts +313 -0
- package/src/__tests__/call-bridge.test.ts +517 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +625 -0
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +699 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-state.test.ts +174 -0
- package/src/__tests__/call-store.test.ts +691 -0
- package/src/__tests__/channel-approval-routes.test.ts +2356 -0
- package/src/__tests__/channel-approval.test.ts +299 -0
- package/src/__tests__/channel-approvals.test.ts +521 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/channel-guardian.test.ts +1005 -0
- package/src/__tests__/checker.test.ts +3519 -0
- package/src/__tests__/clarification-resolver.test.ts +159 -0
- package/src/__tests__/classifier.test.ts +67 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +127 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +88 -0
- package/src/__tests__/cli-discover.test.ts +85 -0
- package/src/__tests__/cli.test.ts +26 -0
- package/src/__tests__/clipboard.test.ts +80 -0
- package/src/__tests__/commit-guarantee.test.ts +335 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-session-compaction.test.ts +132 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +293 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +117 -0
- package/src/__tests__/computer-use-skill-baseline.test.ts +74 -0
- package/src/__tests__/computer-use-skill-endstate.test.ts +89 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +217 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +107 -0
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +54 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +1462 -0
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +332 -0
- package/src/__tests__/connection-policy.test.ts +102 -0
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/context-memory-e2e.test.ts +434 -0
- package/src/__tests__/context-token-estimator.test.ts +135 -0
- package/src/__tests__/context-window-manager.test.ts +376 -0
- package/src/__tests__/contradiction-checker.test.ts +314 -0
- package/src/__tests__/conversation-store.test.ts +612 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +517 -0
- package/src/__tests__/credential-broker-server-use.test.ts +554 -0
- package/src/__tests__/credential-broker.test.ts +167 -0
- package/src/__tests__/credential-host-pattern-match.test.ts +104 -0
- package/src/__tests__/credential-metadata-store.test.ts +779 -0
- package/src/__tests__/credential-policy-validate.test.ts +121 -0
- package/src/__tests__/credential-resolve.test.ts +328 -0
- package/src/__tests__/credential-security-e2e.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +583 -0
- package/src/__tests__/credential-selection.test.ts +354 -0
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/credential-vault.test.ts +852 -0
- package/src/__tests__/daemon-assistant-events.test.ts +164 -0
- package/src/__tests__/daemon-server-session-init.test.ts +522 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +97 -0
- package/src/__tests__/diff.test.ts +121 -0
- package/src/__tests__/domain-normalize.test.ts +112 -0
- package/src/__tests__/domain-policy.test.ts +124 -0
- package/src/__tests__/doordash-client.test.ts +186 -0
- package/src/__tests__/doordash-session.test.ts +152 -0
- package/src/__tests__/dynamic-page-surface.test.ts +91 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +132 -0
- package/src/__tests__/edit-engine.test.ts +180 -0
- package/src/__tests__/elevenlabs-client.test.ts +271 -0
- package/src/__tests__/email-cli.test.ts +283 -0
- package/src/__tests__/encrypted-store.test.ts +332 -0
- package/src/__tests__/entity-extractor.test.ts +190 -0
- package/src/__tests__/ephemeral-permissions.test.ts +362 -0
- package/src/__tests__/evaluate-typescript-tool.test.ts +286 -0
- package/src/__tests__/event-bus.test.ts +222 -0
- package/src/__tests__/file-edit-tool.test.ts +122 -0
- package/src/__tests__/file-ops-service.test.ts +330 -0
- package/src/__tests__/file-read-tool.test.ts +75 -0
- package/src/__tests__/file-write-tool.test.ts +113 -0
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/fixtures/credential-security-fixtures.ts +181 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +126 -0
- package/src/__tests__/fixtures/mock-signup-server.ts +387 -0
- package/src/__tests__/fixtures/proxy-fixtures.ts +147 -0
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +71 -0
- package/src/__tests__/fuzzy-match-property.test.ts +216 -0
- package/src/__tests__/fuzzy-match.test.ts +138 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +631 -0
- package/src/__tests__/gemini-image-service.test.ts +261 -0
- package/src/__tests__/gemini-provider.test.ts +651 -0
- package/src/__tests__/get-weather.test.ts +318 -0
- package/src/__tests__/gmail-integration.test.ts +73 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +352 -0
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +191 -0
- package/src/__tests__/handlers-slack-config.test.ts +200 -0
- package/src/__tests__/handlers-task-submit-slash.test.ts +38 -0
- package/src/__tests__/handlers-telegram-config.test.ts +968 -0
- package/src/__tests__/handlers-twilio-config.test.ts +659 -0
- package/src/__tests__/handlers-twitter-config.test.ts +858 -0
- package/src/__tests__/headless-browser-interactions.test.ts +536 -0
- package/src/__tests__/headless-browser-navigate.test.ts +211 -0
- package/src/__tests__/headless-browser-read-tools.test.ts +261 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +185 -0
- package/src/__tests__/history-repair-observability.test.ts +56 -0
- package/src/__tests__/history-repair.test.ts +510 -0
- package/src/__tests__/home-base-bootstrap.test.ts +82 -0
- package/src/__tests__/hooks-blocking.test.ts +128 -0
- package/src/__tests__/hooks-cli.test.ts +144 -0
- package/src/__tests__/hooks-config.test.ts +93 -0
- package/src/__tests__/hooks-discovery.test.ts +199 -0
- package/src/__tests__/hooks-integration.test.ts +189 -0
- package/src/__tests__/hooks-manager.test.ts +187 -0
- package/src/__tests__/hooks-runner.test.ts +182 -0
- package/src/__tests__/hooks-settings.test.ts +154 -0
- package/src/__tests__/hooks-templates.test.ts +137 -0
- package/src/__tests__/hooks-ts-runner.test.ts +125 -0
- package/src/__tests__/hooks-watch.test.ts +100 -0
- package/src/__tests__/host-file-edit-tool.test.ts +228 -0
- package/src/__tests__/host-file-read-tool.test.ts +123 -0
- package/src/__tests__/host-file-write-tool.test.ts +136 -0
- package/src/__tests__/host-shell-tool.test.ts +562 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ingress-url-consistency.test.ts +214 -0
- package/src/__tests__/intent-routing.test.ts +259 -0
- package/src/__tests__/ipc-blob-store.test.ts +315 -0
- package/src/__tests__/ipc-contract-inventory.test.ts +54 -0
- package/src/__tests__/ipc-contract.test.ts +74 -0
- package/src/__tests__/ipc-protocol.test.ts +113 -0
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +1769 -0
- package/src/__tests__/ipc-validate.test.ts +407 -0
- package/src/__tests__/key-migration.test.ts +206 -0
- package/src/__tests__/keychain.test.ts +258 -0
- package/src/__tests__/llm-usage-store.test.ts +221 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +257 -0
- package/src/__tests__/managed-store.test.ts +608 -0
- package/src/__tests__/media-generate-image.test.ts +238 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +676 -0
- package/src/__tests__/media-visibility-policy.test.ts +141 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +235 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +481 -0
- package/src/__tests__/memory-query-builder.test.ts +59 -0
- package/src/__tests__/memory-recall-quality.test.ts +846 -0
- package/src/__tests__/memory-regressions.experimental.test.ts +538 -0
- package/src/__tests__/memory-regressions.test.ts +4435 -0
- package/src/__tests__/memory-retrieval-budget.test.ts +49 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/migration-cli-flows.test.ts +169 -0
- package/src/__tests__/migration-ordering.test.ts +249 -0
- package/src/__tests__/mock-signup-server.test.ts +528 -0
- package/src/__tests__/oauth-callback-registry.test.ts +92 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +285 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +176 -0
- package/src/__tests__/onboarding-template-contract.test.ts +58 -0
- package/src/__tests__/openai-provider.test.ts +753 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/parser.test.ts +472 -0
- package/src/__tests__/path-classifier.test.ts +73 -0
- package/src/__tests__/path-policy.test.ts +435 -0
- package/src/__tests__/platform-move-helper.test.ts +99 -0
- package/src/__tests__/platform-socket-path.test.ts +52 -0
- package/src/__tests__/platform-workspace-migration.test.ts +1000 -0
- package/src/__tests__/platform.test.ts +131 -0
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +340 -0
- package/src/__tests__/prebuilt-home-base-seed.test.ts +75 -0
- package/src/__tests__/pricing.test.ts +256 -0
- package/src/__tests__/profile-compiler.test.ts +374 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +342 -0
- package/src/__tests__/provider-registry-ollama.test.ts +16 -0
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/proxy-approval-callback.test.ts +601 -0
- package/src/__tests__/public-ingress-urls.test.ts +256 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/ratelimit.test.ts +297 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +175 -0
- package/src/__tests__/recurrence-engine.test.ts +78 -0
- package/src/__tests__/recurrence-types.test.ts +79 -0
- package/src/__tests__/registry.test.ts +494 -0
- package/src/__tests__/relay-server.test.ts +688 -0
- package/src/__tests__/reminder-store.test.ts +223 -0
- package/src/__tests__/reminder.test.ts +229 -0
- package/src/__tests__/request-file-tool.test.ts +158 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +227 -0
- package/src/__tests__/run-orchestrator.test.ts +425 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +189 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +343 -0
- package/src/__tests__/runtime-events-sse.test.ts +162 -0
- package/src/__tests__/runtime-runs-http.test.ts +438 -0
- package/src/__tests__/runtime-runs.test.ts +260 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +408 -0
- package/src/__tests__/sandbox-host-parity.test.ts +950 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +253 -0
- package/src/__tests__/schedule-store.test.ts +484 -0
- package/src/__tests__/schedule-tools.test.ts +783 -0
- package/src/__tests__/scheduler-recurrence.test.ts +430 -0
- package/src/__tests__/script-proxy-certs.test.ts +90 -0
- package/src/__tests__/script-proxy-connect-tunnel.test.ts +177 -0
- package/src/__tests__/script-proxy-decision-trace.test.ts +156 -0
- package/src/__tests__/script-proxy-http-forwarder.test.ts +281 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +401 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +407 -0
- package/src/__tests__/script-proxy-policy-runtime.test.ts +287 -0
- package/src/__tests__/script-proxy-policy.test.ts +310 -0
- package/src/__tests__/script-proxy-rewrite-specificity.test.ts +135 -0
- package/src/__tests__/script-proxy-router.test.ts +180 -0
- package/src/__tests__/script-proxy-session-manager.test.ts +382 -0
- package/src/__tests__/script-proxy-session-runtime.test.ts +113 -0
- package/src/__tests__/secret-allowlist.test.ts +230 -0
- package/src/__tests__/secret-ingress-handler.test.ts +110 -0
- package/src/__tests__/secret-onetime-send.test.ts +130 -0
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +106 -0
- package/src/__tests__/secret-response-routing.test.ts +93 -0
- package/src/__tests__/secret-scanner-executor.test.ts +348 -0
- package/src/__tests__/secret-scanner.test.ts +900 -0
- package/src/__tests__/secure-keys.test.ts +323 -0
- package/src/__tests__/server-history-render.test.ts +431 -0
- package/src/__tests__/session-abort-tool-results.test.ts +240 -0
- package/src/__tests__/session-conflict-gate.test.ts +1136 -0
- package/src/__tests__/session-error.test.ts +369 -0
- package/src/__tests__/session-evictor.test.ts +188 -0
- package/src/__tests__/session-init.benchmark.test.ts +465 -0
- package/src/__tests__/session-load-history-repair.test.ts +222 -0
- package/src/__tests__/session-pre-run-repair.test.ts +213 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-profile-injection.test.ts +444 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +306 -0
- package/src/__tests__/session-queue.test.ts +1535 -0
- package/src/__tests__/session-runtime-assembly.test.ts +476 -0
- package/src/__tests__/session-runtime-workspace.test.ts +183 -0
- package/src/__tests__/session-skill-tools.test.ts +2431 -0
- package/src/__tests__/session-slash-known.test.ts +368 -0
- package/src/__tests__/session-slash-queue.test.ts +288 -0
- package/src/__tests__/session-slash-unknown.test.ts +271 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +473 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +140 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +140 -0
- package/src/__tests__/session-undo.test.ts +75 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +246 -0
- package/src/__tests__/session-workspace-injection.test.ts +327 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +240 -0
- package/src/__tests__/shared-filesystem-errors.test.ts +78 -0
- package/src/__tests__/shell-credential-ref.test.ts +187 -0
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/shell-parser-fuzz.test.ts +544 -0
- package/src/__tests__/shell-parser-property.test.ts +433 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +272 -0
- package/src/__tests__/signup-e2e.test.ts +353 -0
- package/src/__tests__/size-guard.test.ts +117 -0
- package/src/__tests__/skill-include-graph.test.ts +303 -0
- package/src/__tests__/skill-load-tool.test.ts +409 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +338 -0
- package/src/__tests__/skill-script-runner-host.test.ts +489 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +349 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/skill-tool-factory.test.ts +252 -0
- package/src/__tests__/skill-tool-manifest.test.ts +658 -0
- package/src/__tests__/skill-version-hash.test.ts +182 -0
- package/src/__tests__/skills.test.ts +680 -0
- package/src/__tests__/slash-commands-catalog.test.ts +86 -0
- package/src/__tests__/slash-commands-parser.test.ts +119 -0
- package/src/__tests__/slash-commands-resolver.test.ts +193 -0
- package/src/__tests__/slash-commands-rewrite.test.ts +39 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/starter-bundle.test.ts +136 -0
- package/src/__tests__/starter-task-flow.test.ts +143 -0
- package/src/__tests__/subagent-manager-notify.test.ts +404 -0
- package/src/__tests__/subagent-tools.test.ts +801 -0
- package/src/__tests__/subagent-types.test.ts +78 -0
- package/src/__tests__/swarm-orchestrator.test.ts +428 -0
- package/src/__tests__/swarm-plan-validator.test.ts +330 -0
- package/src/__tests__/swarm-recursion.test.ts +165 -0
- package/src/__tests__/swarm-router-planner.test.ts +208 -0
- package/src/__tests__/swarm-session-integration.test.ts +274 -0
- package/src/__tests__/swarm-tool.test.ts +145 -0
- package/src/__tests__/swarm-worker-backend.test.ts +129 -0
- package/src/__tests__/swarm-worker-runner.test.ts +272 -0
- package/src/__tests__/system-prompt.test.ts +439 -0
- package/src/__tests__/task-compiler.test.ts +284 -0
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +216 -0
- package/src/__tests__/task-scheduler.test.ts +217 -0
- package/src/__tests__/task-tools.test.ts +595 -0
- package/src/__tests__/terminal-sandbox-docker.test.ts +1064 -0
- package/src/__tests__/terminal-sandbox.integration.test.ts +178 -0
- package/src/__tests__/terminal-sandbox.test.ts +202 -0
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +90 -0
- package/src/__tests__/test-support/computer-use-skill-harness.ts +45 -0
- package/src/__tests__/tool-audit-listener.test.ts +113 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +253 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +516 -0
- package/src/__tests__/tool-executor-redaction.test.ts +289 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +1989 -0
- package/src/__tests__/tool-metrics-listener.test.ts +225 -0
- package/src/__tests__/tool-notification-listener.test.ts +49 -0
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/tool-policy.test.ts +54 -0
- package/src/__tests__/tool-profiling-listener.test.ts +268 -0
- package/src/__tests__/tool-result-truncation.test.ts +217 -0
- package/src/__tests__/tool-trace-listener.test.ts +226 -0
- package/src/__tests__/top-level-renderer.test.ts +121 -0
- package/src/__tests__/top-level-scanner.test.ts +141 -0
- package/src/__tests__/trace-emitter.test.ts +173 -0
- package/src/__tests__/trust-store.test.ts +1605 -0
- package/src/__tests__/turn-commit.test.ts +554 -0
- package/src/__tests__/twilio-provider.test.ts +329 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +577 -0
- package/src/__tests__/twitter-auth-handler.test.ts +667 -0
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/url-safety.test.ts +418 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/weather-skill-regression.test.ts +225 -0
- package/src/__tests__/web-fetch.test.ts +869 -0
- package/src/__tests__/web-search.test.ts +584 -0
- package/src/__tests__/workspace-git-service.test.ts +1153 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +486 -0
- package/src/__tests__/workspace-lifecycle.test.ts +292 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/agent/attachments.ts +35 -0
- package/src/agent/loop.ts +500 -0
- package/src/agent/message-types.ts +17 -0
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/autonomy/autonomy-resolver.ts +60 -0
- package/src/autonomy/autonomy-store.ts +122 -0
- package/src/autonomy/disposition-mapper.ts +31 -0
- package/src/autonomy/index.ts +11 -0
- package/src/autonomy/types.ts +39 -0
- package/src/bundler/app-bundler.ts +295 -0
- package/src/bundler/bundle-scanner.ts +535 -0
- package/src/bundler/bundle-signer.ts +124 -0
- package/src/bundler/manifest.ts +21 -0
- package/src/bundler/signature-verifier.ts +184 -0
- package/src/calls/call-bridge.ts +168 -0
- package/src/calls/call-constants.ts +48 -0
- package/src/calls/call-domain.ts +430 -0
- package/src/calls/call-orchestrator.ts +498 -0
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-state.ts +87 -0
- package/src/calls/call-store.ts +422 -0
- package/src/calls/elevenlabs-client.ts +97 -0
- package/src/calls/elevenlabs-config.ts +31 -0
- package/src/calls/relay-server.ts +390 -0
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +45 -0
- package/src/calls/twilio-provider.ts +263 -0
- package/src/calls/twilio-rest.ts +156 -0
- package/src/calls/twilio-routes.ts +311 -0
- package/src/calls/types.ts +39 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/calls/voice-quality.ts +114 -0
- package/src/cli/autonomy.ts +188 -0
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/contacts.ts +149 -0
- package/src/cli/core-commands.ts +784 -0
- package/src/cli/doordash.ts +1055 -0
- package/src/cli/email-guardrails.ts +200 -0
- package/src/cli/email.ts +405 -0
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/main-screen.tsx +53 -0
- package/src/cli/map.ts +270 -0
- package/src/cli/twitter.ts +754 -0
- package/src/cli.ts +918 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/.gitkeep +0 -0
- package/src/config/bundled-skills/agentmail/SKILL.md +128 -0
- package/src/config/bundled-skills/agentmail/icon.svg +21 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1404 -0
- package/src/config/bundled-skills/app-builder/TOOLS.json +279 -0
- package/src/config/bundled-skills/app-builder/icon.svg +9 -0
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +15 -0
- package/src/config/bundled-skills/app-builder/tools/app-delete.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +11 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +18 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +11 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-query.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +20 -0
- package/src/config/bundled-skills/browser/SKILL.md +28 -0
- package/src/config/bundled-skills/browser/TOOLS.json +234 -0
- package/src/config/bundled-skills/browser/tools/browser-click.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-close.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-type.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +9 -0
- package/src/config/bundled-skills/claude-code/SKILL.md +50 -0
- package/src/config/bundled-skills/claude-code/TOOLS.json +40 -0
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +9 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +17 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +326 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-done.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-double-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-drag.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-key.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-open-app.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-respond.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-right-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-run-applescript.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-scroll.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-type-text.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-wait.ts +9 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +57 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +60 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +66 -0
- package/src/config/bundled-skills/document/SKILL.md +26 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +163 -0
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/icon.svg +24 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +51 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +108 -0
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +165 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +21 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +42 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +13 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +30 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +41 -0
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +18 -0
- package/src/config/bundled-skills/google-calendar/types.ts +97 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +32 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +42 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +115 -0
- package/src/config/bundled-skills/macos-automation/SKILL.md +66 -0
- package/src/config/bundled-skills/messaging/SKILL.md +153 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +357 -0
- package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +26 -0
- package/src/config/bundled-skills/messaging/tools/gmail-label.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/gmail-trash.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +84 -0
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +18 -0
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +125 -0
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +16 -0
- package/src/config/bundled-skills/messaging/tools/messaging-draft.ts +49 -0
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +21 -0
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +28 -0
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +32 -0
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +22 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +31 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +76 -0
- package/src/config/bundled-skills/messaging/tools/slack-add-reaction.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/slack-leave-channel.ts +23 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +533 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +98 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +54 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +76 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +113 -0
- package/src/config/bundled-skills/public-ingress/SKILL.md +200 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/self-upgrade/SKILL.md +68 -0
- package/src/config/bundled-skills/start-the-day/SKILL.md +70 -0
- package/src/config/bundled-skills/start-the-day/icon.svg +13 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +281 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +25 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +32 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +370 -0
- package/src/config/bundled-skills/twitter/SKILL.md +220 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/bundled-skills/weather/SKILL.md +37 -0
- package/src/config/bundled-skills/weather/TOOLS.json +32 -0
- package/src/config/bundled-skills/weather/icon.svg +24 -0
- package/src/config/bundled-skills/weather/tools/get-weather.ts +9 -0
- package/src/config/computer-use-prompt.ts +97 -0
- package/src/config/defaults.ts +263 -0
- package/src/config/loader.ts +339 -0
- package/src/config/schema.ts +1436 -0
- package/src/config/skill-state.ts +95 -0
- package/src/config/skills.ts +972 -0
- package/src/config/system-prompt.ts +675 -0
- package/src/config/templates/BOOTSTRAP.md +70 -0
- package/src/config/templates/IDENTITY.md +25 -0
- package/src/config/templates/LOOKS.md +25 -0
- package/src/config/templates/SOUL.md +37 -0
- package/src/config/templates/USER.md +19 -0
- package/src/config/types.ts +42 -0
- package/src/config/vellum-skills/chatgpt-import/SKILL.md +24 -0
- package/src/config/vellum-skills/chatgpt-import/TOOLS.json +23 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +284 -0
- package/src/config/vellum-skills/deploy-fullstack-vercel/SKILL.md +179 -0
- package/src/config/vellum-skills/document-writer/SKILL.md +195 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +199 -0
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +153 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +143 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +213 -0
- package/src/contacts/contact-store.ts +410 -0
- package/src/contacts/index.ts +11 -0
- package/src/contacts/types.ts +28 -0
- package/src/context/token-estimator.ts +108 -0
- package/src/context/tool-result-truncation.ts +128 -0
- package/src/context/window-manager.ts +531 -0
- package/src/daemon/assistant-attachments.ts +691 -0
- package/src/daemon/classifier.ts +110 -0
- package/src/daemon/computer-use-session.ts +903 -0
- package/src/daemon/connection-policy.ts +41 -0
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +530 -0
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +187 -0
- package/src/daemon/handlers/config.ts +1517 -0
- package/src/daemon/handlers/diagnostics.ts +338 -0
- package/src/daemon/handlers/documents.ts +173 -0
- package/src/daemon/handlers/home-base.ts +78 -0
- package/src/daemon/handlers/identity.ts +127 -0
- package/src/daemon/handlers/index.ts +129 -0
- package/src/daemon/handlers/misc.ts +331 -0
- package/src/daemon/handlers/open-bundle-handler.ts +80 -0
- package/src/daemon/handlers/publish.ts +187 -0
- package/src/daemon/handlers/sessions.ts +555 -0
- package/src/daemon/handlers/shared.ts +570 -0
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +486 -0
- package/src/daemon/handlers/subagents.ts +210 -0
- package/src/daemon/handlers/twitter-auth.ts +198 -0
- package/src/daemon/handlers/work-items.ts +632 -0
- package/src/daemon/handlers/workspace-files.ts +75 -0
- package/src/daemon/handlers.ts +17 -0
- package/src/daemon/history-repair.ts +214 -0
- package/src/daemon/ipc-blob-store.ts +231 -0
- package/src/daemon/ipc-contract-inventory.json +495 -0
- package/src/daemon/ipc-contract-inventory.ts +126 -0
- package/src/daemon/ipc-contract.ts +2551 -0
- package/src/daemon/ipc-protocol.ts +75 -0
- package/src/daemon/ipc-validate.ts +188 -0
- package/src/daemon/lifecycle.ts +582 -0
- package/src/daemon/main.ts +21 -0
- package/src/daemon/media-visibility-policy.ts +57 -0
- package/src/daemon/ride-shotgun-handler.ts +309 -0
- package/src/daemon/server.ts +1215 -0
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +196 -0
- package/src/daemon/session-conflict-gate.ts +184 -0
- package/src/daemon/session-dynamic-profile.ts +63 -0
- package/src/daemon/session-error.ts +290 -0
- package/src/daemon/session-evictor.ts +196 -0
- package/src/daemon/session-history.ts +437 -0
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-memory.ts +212 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +193 -0
- package/src/daemon/session-process.ts +323 -0
- package/src/daemon/session-queue-manager.ts +82 -0
- package/src/daemon/session-runtime-assembly.ts +447 -0
- package/src/daemon/session-skill-tools.ts +356 -0
- package/src/daemon/session-slash.ts +305 -0
- package/src/daemon/session-surfaces.ts +702 -0
- package/src/daemon/session-tool-setup.ts +523 -0
- package/src/daemon/session-usage.ts +72 -0
- package/src/daemon/session-workspace.ts +19 -0
- package/src/daemon/session.ts +400 -0
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/trace-emitter.ts +82 -0
- package/src/daemon/video-thumbnail.ts +62 -0
- package/src/daemon/watch-handler.ts +274 -0
- package/src/doordash/client.ts +999 -0
- package/src/doordash/queries.ts +1311 -0
- package/src/doordash/query-extractor.ts +93 -0
- package/src/doordash/session.ts +82 -0
- package/src/email/provider.ts +117 -0
- package/src/email/providers/agentmail.ts +317 -0
- package/src/email/providers/index.ts +58 -0
- package/src/email/service.ts +303 -0
- package/src/email/types.ts +126 -0
- package/src/events/bus.ts +157 -0
- package/src/events/domain-events.ts +83 -0
- package/src/events/index.ts +18 -0
- package/src/events/tool-audit-listener.ts +80 -0
- package/src/events/tool-domain-event-publisher.ts +111 -0
- package/src/events/tool-metrics-listener.ts +159 -0
- package/src/events/tool-notification-listener.ts +17 -0
- package/src/events/tool-profiling-listener.ts +158 -0
- package/src/events/tool-trace-listener.ts +75 -0
- package/src/export/formatter.ts +98 -0
- package/src/followups/followup-store.ts +168 -0
- package/src/followups/index.ts +10 -0
- package/src/followups/types.ts +29 -0
- package/src/gallery/default-gallery.ts +795 -0
- package/src/gallery/gallery-manifest.ts +24 -0
- package/src/home-base/app-link-store.ts +82 -0
- package/src/home-base/bootstrap.ts +68 -0
- package/src/home-base/prebuilt/index.html +662 -0
- package/src/home-base/prebuilt/seed-metadata.json +21 -0
- package/src/home-base/prebuilt/seed.ts +112 -0
- package/src/home-base/prebuilt-home-base-updater.ts +30 -0
- package/src/hooks/cli.ts +163 -0
- package/src/hooks/config.ts +88 -0
- package/src/hooks/discovery.ts +110 -0
- package/src/hooks/manager.ts +124 -0
- package/src/hooks/runner.ts +123 -0
- package/src/hooks/templates.ts +52 -0
- package/src/hooks/types.ts +72 -0
- package/src/inbound/public-ingress-urls.ts +123 -0
- package/src/index.ts +81 -0
- package/src/instrument.ts +60 -0
- package/src/logfire.ts +99 -0
- package/src/media/gemini-image-service.ts +136 -0
- package/src/memory/account-store.ts +108 -0
- package/src/memory/admin.ts +211 -0
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +577 -0
- package/src/memory/attachments-store.ts +397 -0
- package/src/memory/channel-delivery-store.ts +353 -0
- package/src/memory/channel-guardian-store.ts +669 -0
- package/src/memory/checkpoints.ts +52 -0
- package/src/memory/clarification-resolver.ts +298 -0
- package/src/memory/conflict-intent.ts +157 -0
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +350 -0
- package/src/memory/contradiction-checker.ts +358 -0
- package/src/memory/conversation-key-store.ts +122 -0
- package/src/memory/conversation-store.ts +470 -0
- package/src/memory/db.ts +1991 -0
- package/src/memory/embedding-backend.ts +229 -0
- package/src/memory/embedding-gemini.ts +52 -0
- package/src/memory/embedding-local.ts +65 -0
- package/src/memory/embedding-ollama.ts +55 -0
- package/src/memory/embedding-openai.ts +25 -0
- package/src/memory/entity-extractor.ts +474 -0
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/fingerprint.ts +20 -0
- package/src/memory/indexer.ts +156 -0
- package/src/memory/items-extractor.ts +461 -0
- package/src/memory/job-handlers/backfill.ts +139 -0
- package/src/memory/job-handlers/cleanup.ts +58 -0
- package/src/memory/job-handlers/conflict.ts +141 -0
- package/src/memory/job-handlers/embedding.ts +61 -0
- package/src/memory/job-handlers/extraction.ts +123 -0
- package/src/memory/job-handlers/index-maintenance.ts +54 -0
- package/src/memory/job-handlers/summarization.ts +286 -0
- package/src/memory/job-utils.ts +170 -0
- package/src/memory/jobs-store.ts +401 -0
- package/src/memory/jobs-worker.ts +313 -0
- package/src/memory/llm-request-log-store.ts +45 -0
- package/src/memory/llm-usage-store.ts +60 -0
- package/src/memory/message-content.ts +54 -0
- package/src/memory/profile-compiler.ts +160 -0
- package/src/memory/published-pages-store.ts +137 -0
- package/src/memory/qdrant-client.ts +366 -0
- package/src/memory/qdrant-manager.ts +242 -0
- package/src/memory/query-builder.ts +45 -0
- package/src/memory/retrieval-budget.ts +30 -0
- package/src/memory/retriever.ts +653 -0
- package/src/memory/runs-store.ts +305 -0
- package/src/memory/schema.ts +677 -0
- package/src/memory/search/entity.ts +298 -0
- package/src/memory/search/formatting.ts +207 -0
- package/src/memory/search/lexical.ts +227 -0
- package/src/memory/search/ranking.ts +401 -0
- package/src/memory/search/semantic.ts +121 -0
- package/src/memory/search/types.ts +137 -0
- package/src/memory/segmenter.ts +68 -0
- package/src/memory/shared-app-links-store.ts +138 -0
- package/src/memory/tool-usage-store.ts +62 -0
- package/src/messaging/activity-analyzer.ts +76 -0
- package/src/messaging/draft-store.ts +88 -0
- package/src/messaging/index.ts +3 -0
- package/src/messaging/provider-types.ts +80 -0
- package/src/messaging/provider.ts +52 -0
- package/src/messaging/providers/gmail/adapter.ts +193 -0
- package/src/messaging/providers/gmail/client.ts +204 -0
- package/src/messaging/providers/gmail/types.ts +90 -0
- package/src/messaging/providers/slack/adapter.ts +202 -0
- package/src/messaging/providers/slack/client.ts +198 -0
- package/src/messaging/providers/slack/types.ts +119 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +35 -0
- package/src/messaging/style-analyzer.ts +159 -0
- package/src/messaging/thread-summarizer.ts +306 -0
- package/src/messaging/triage-engine.ts +323 -0
- package/src/messaging/types.ts +55 -0
- package/src/permissions/checker.ts +640 -0
- package/src/permissions/defaults.ts +254 -0
- package/src/permissions/prompter.ts +98 -0
- package/src/permissions/secret-prompter.ts +114 -0
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +607 -0
- package/src/permissions/types.ts +43 -0
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/playbooks/index.ts +2 -0
- package/src/playbooks/playbook-compiler.ts +90 -0
- package/src/playbooks/types.ts +55 -0
- package/src/providers/anthropic/client.ts +751 -0
- package/src/providers/failover.ts +129 -0
- package/src/providers/fireworks/client.ts +20 -0
- package/src/providers/gemini/client.ts +285 -0
- package/src/providers/ollama/client.ts +30 -0
- package/src/providers/openai/client.ts +337 -0
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/ratelimit.ts +93 -0
- package/src/providers/registry.ts +146 -0
- package/src/providers/retry.ts +81 -0
- package/src/providers/stream-timeout.ts +38 -0
- package/src/providers/types.ts +109 -0
- package/src/runtime/assistant-event-hub.ts +157 -0
- package/src/runtime/assistant-event.ts +82 -0
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +73 -0
- package/src/runtime/channel-approvals.ts +206 -0
- package/src/runtime/channel-guardian-service.ts +212 -0
- package/src/runtime/gateway-client.ts +58 -0
- package/src/runtime/http-server.ts +1076 -0
- package/src/runtime/http-types.ts +66 -0
- package/src/runtime/routes/app-routes.ts +174 -0
- package/src/runtime/routes/attachment-routes.ts +133 -0
- package/src/runtime/routes/call-routes.ts +190 -0
- package/src/runtime/routes/channel-routes.ts +1404 -0
- package/src/runtime/routes/conversation-routes.ts +352 -0
- package/src/runtime/routes/events-routes.ts +148 -0
- package/src/runtime/routes/run-routes.ts +257 -0
- package/src/runtime/routes/secret-routes.ts +76 -0
- package/src/runtime/run-orchestrator.ts +330 -0
- package/src/schedule/recurrence-engine.ts +162 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +506 -0
- package/src/schedule/scheduler.ts +171 -0
- package/src/security/encrypted-store.ts +238 -0
- package/src/security/keychain.ts +252 -0
- package/src/security/oauth-callback-registry.ts +66 -0
- package/src/security/oauth2.ts +274 -0
- package/src/security/redaction.ts +89 -0
- package/src/security/secret-allowlist.ts +164 -0
- package/src/security/secret-ingress.ts +57 -0
- package/src/security/secret-scanner.ts +550 -0
- package/src/security/secure-keys.ts +180 -0
- package/src/security/token-manager.ts +141 -0
- package/src/services/published-app-updater.ts +69 -0
- package/src/services/vercel-deploy.ts +73 -0
- package/src/skills/active-skill-tools.ts +81 -0
- package/src/skills/clawhub.ts +414 -0
- package/src/skills/include-graph.ts +146 -0
- package/src/skills/managed-store.ts +233 -0
- package/src/skills/path-classifier.ts +128 -0
- package/src/skills/slash-commands.ts +174 -0
- package/src/skills/tool-manifest.ts +165 -0
- package/src/skills/version-hash.ts +110 -0
- package/src/slack/slack-webhook.ts +61 -0
- package/src/subagent/index.ts +19 -0
- package/src/subagent/manager.ts +511 -0
- package/src/subagent/types.ts +69 -0
- package/src/swarm/backend-claude-code.ts +145 -0
- package/src/swarm/index.ts +44 -0
- package/src/swarm/limits.ts +37 -0
- package/src/swarm/orchestrator.ts +279 -0
- package/src/swarm/plan-validator.ts +151 -0
- package/src/swarm/router-planner.ts +100 -0
- package/src/swarm/router-prompts.ts +36 -0
- package/src/swarm/synthesizer.ts +62 -0
- package/src/swarm/types.ts +62 -0
- package/src/swarm/worker-backend.ts +121 -0
- package/src/swarm/worker-prompts.ts +79 -0
- package/src/swarm/worker-runner.ts +164 -0
- package/src/tasks/SPEC.md +139 -0
- package/src/tasks/candidate-store.ts +86 -0
- package/src/tasks/ephemeral-permissions.ts +48 -0
- package/src/tasks/task-compiler.ts +199 -0
- package/src/tasks/task-runner.ts +90 -0
- package/src/tasks/task-scheduler.ts +21 -0
- package/src/tasks/task-store.ts +127 -0
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/apps/definitions.ts +59 -0
- package/src/tools/apps/executors.ts +313 -0
- package/src/tools/apps/open-proxy.ts +43 -0
- package/src/tools/apps/registry.ts +16 -0
- package/src/tools/assets/materialize.ts +218 -0
- package/src/tools/assets/search.ts +361 -0
- package/src/tools/browser/__tests__/auth-cache.test.ts +219 -0
- package/src/tools/browser/__tests__/auth-detector.test.ts +362 -0
- package/src/tools/browser/__tests__/jit-auth.test.ts +189 -0
- package/src/tools/browser/api-map.ts +293 -0
- package/src/tools/browser/auth-cache.ts +149 -0
- package/src/tools/browser/auth-detector.ts +347 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +980 -0
- package/src/tools/browser/browser-handoff.ts +79 -0
- package/src/tools/browser/browser-manager.ts +715 -0
- package/src/tools/browser/browser-screencast.ts +217 -0
- package/src/tools/browser/headless-browser.ts +450 -0
- package/src/tools/browser/jit-auth.ts +51 -0
- package/src/tools/browser/network-recorder.ts +349 -0
- package/src/tools/browser/network-recording-types.ts +49 -0
- package/src/tools/browser/recording-store.ts +49 -0
- package/src/tools/browser/runtime-check.ts +43 -0
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +67 -0
- package/src/tools/calls/call-start.ts +81 -0
- package/src/tools/calls/call-status.ts +81 -0
- package/src/tools/claude-code/claude-code.ts +428 -0
- package/src/tools/computer-use/definitions.ts +443 -0
- package/src/tools/computer-use/registry.ts +22 -0
- package/src/tools/computer-use/request-computer-control.ts +53 -0
- package/src/tools/computer-use/skill-proxy-bridge.ts +28 -0
- package/src/tools/credentials/account-registry.ts +127 -0
- package/src/tools/credentials/broker-types.ts +107 -0
- package/src/tools/credentials/broker.ts +372 -0
- package/src/tools/credentials/domain-policy.ts +51 -0
- package/src/tools/credentials/host-pattern-match.ts +60 -0
- package/src/tools/credentials/metadata-store.ts +335 -0
- package/src/tools/credentials/policy-types.ts +52 -0
- package/src/tools/credentials/policy-validate.ts +80 -0
- package/src/tools/credentials/resolve.ts +122 -0
- package/src/tools/credentials/selection.ts +159 -0
- package/src/tools/credentials/tool-policy.ts +25 -0
- package/src/tools/credentials/vault.ts +657 -0
- package/src/tools/document/document-tool.ts +92 -0
- package/src/tools/document/editor-template.ts +237 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +815 -0
- package/src/tools/filesystem/edit.ts +127 -0
- package/src/tools/filesystem/fuzzy-match.ts +202 -0
- package/src/tools/filesystem/read.ts +71 -0
- package/src/tools/filesystem/view-image.ts +199 -0
- package/src/tools/filesystem/write.ts +79 -0
- package/src/tools/followups/followup_create.ts +76 -0
- package/src/tools/followups/followup_list.ts +60 -0
- package/src/tools/followups/followup_resolve.ts +56 -0
- package/src/tools/host-filesystem/edit.ts +125 -0
- package/src/tools/host-filesystem/read.ts +80 -0
- package/src/tools/host-filesystem/write.ts +76 -0
- package/src/tools/host-terminal/cli-discover.ts +180 -0
- package/src/tools/host-terminal/host-shell.ts +191 -0
- package/src/tools/memory/definitions.ts +69 -0
- package/src/tools/memory/handlers.ts +246 -0
- package/src/tools/memory/register.ts +66 -0
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/domain-normalize.ts +85 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/script-proxy/certs.ts +237 -0
- package/src/tools/network/script-proxy/connect-tunnel.ts +82 -0
- package/src/tools/network/script-proxy/http-forwarder.ts +151 -0
- package/src/tools/network/script-proxy/index.ts +28 -0
- package/src/tools/network/script-proxy/logging.ts +196 -0
- package/src/tools/network/script-proxy/mitm-handler.ts +269 -0
- package/src/tools/network/script-proxy/policy.ts +152 -0
- package/src/tools/network/script-proxy/router.ts +60 -0
- package/src/tools/network/script-proxy/server.ts +136 -0
- package/src/tools/network/script-proxy/session-manager.ts +534 -0
- package/src/tools/network/script-proxy/types.ts +125 -0
- package/src/tools/network/url-safety.ts +227 -0
- package/src/tools/network/web-fetch.ts +713 -0
- package/src/tools/network/web-search.ts +296 -0
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/registry.ts +295 -0
- package/src/tools/reminder/reminder-store.ts +148 -0
- package/src/tools/reminder/reminder.ts +80 -0
- package/src/tools/schedule/create.ts +81 -0
- package/src/tools/schedule/delete.ts +28 -0
- package/src/tools/schedule/list.ts +69 -0
- package/src/tools/schedule/update.ts +97 -0
- package/src/tools/shared/filesystem/edit-engine.ts +56 -0
- package/src/tools/shared/filesystem/errors.ts +85 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +215 -0
- package/src/tools/shared/filesystem/format-diff.ts +35 -0
- package/src/tools/shared/filesystem/path-policy.ts +125 -0
- package/src/tools/shared/filesystem/size-guard.ts +41 -0
- package/src/tools/shared/filesystem/types.ts +80 -0
- package/src/tools/shared/shell-output.ts +52 -0
- package/src/tools/skills/delete-managed.ts +60 -0
- package/src/tools/skills/load.ts +139 -0
- package/src/tools/skills/sandbox-runner.ts +279 -0
- package/src/tools/skills/scaffold-managed.ts +150 -0
- package/src/tools/skills/script-contract.ts +6 -0
- package/src/tools/skills/skill-script-runner.ts +86 -0
- package/src/tools/skills/skill-tool-factory.ts +64 -0
- package/src/tools/skills/vellum-catalog.ts +217 -0
- package/src/tools/subagent/abort.ts +33 -0
- package/src/tools/subagent/message.ts +39 -0
- package/src/tools/subagent/read.ts +67 -0
- package/src/tools/subagent/spawn.ts +46 -0
- package/src/tools/subagent/status.ts +45 -0
- package/src/tools/swarm/delegate.ts +183 -0
- package/src/tools/system/request-permission.ts +98 -0
- package/src/tools/system/version.ts +43 -0
- package/src/tools/tasks/index.ts +27 -0
- package/src/tools/tasks/task-delete.ts +82 -0
- package/src/tools/tasks/task-list.ts +44 -0
- package/src/tools/tasks/task-run.ts +97 -0
- package/src/tools/tasks/task-save.ts +47 -0
- package/src/tools/tasks/work-item-enqueue.ts +234 -0
- package/src/tools/tasks/work-item-list.ts +55 -0
- package/src/tools/tasks/work-item-remove.ts +60 -0
- package/src/tools/tasks/work-item-run.ts +78 -0
- package/src/tools/tasks/work-item-update.ts +114 -0
- package/src/tools/terminal/backends/docker.ts +372 -0
- package/src/tools/terminal/backends/native.ts +190 -0
- package/src/tools/terminal/backends/types.ts +26 -0
- package/src/tools/terminal/evaluate-typescript.ts +275 -0
- package/src/tools/terminal/parser.ts +413 -0
- package/src/tools/terminal/safe-env.ts +37 -0
- package/src/tools/terminal/sandbox-diagnostics.ts +149 -0
- package/src/tools/terminal/sandbox.ts +44 -0
- package/src/tools/terminal/shell.ts +257 -0
- package/src/tools/tool-manifest.ts +198 -0
- package/src/tools/types.ts +176 -0
- package/src/tools/ui-surface/definitions.ts +244 -0
- package/src/tools/ui-surface/registry.ts +14 -0
- package/src/tools/watch/screen-watch.ts +130 -0
- package/src/tools/watch/watch-state.ts +119 -0
- package/src/tools/watcher/create.ts +64 -0
- package/src/tools/watcher/delete.ts +27 -0
- package/src/tools/watcher/digest.ts +50 -0
- package/src/tools/watcher/list.ts +60 -0
- package/src/tools/watcher/update.ts +56 -0
- package/src/tools/weather/service.ts +551 -0
- package/src/twitter/client.ts +690 -0
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/actors.ts +24 -0
- package/src/usage/types.ts +37 -0
- package/src/util/clipboard.ts +33 -0
- package/src/util/content-id.ts +16 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/diff.ts +181 -0
- package/src/util/errors.ts +129 -0
- package/src/util/logger.ts +243 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +632 -0
- package/src/util/pricing.ts +150 -0
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/spinner.ts +51 -0
- package/src/util/time.ts +16 -0
- package/src/util/truncate.ts +6 -0
- package/src/util/xml.ts +4 -0
- package/src/version.ts +3 -0
- package/src/watcher/constants.ts +11 -0
- package/src/watcher/engine.ts +199 -0
- package/src/watcher/provider-registry.ts +15 -0
- package/src/watcher/provider-types.ts +48 -0
- package/src/watcher/providers/gmail.ts +198 -0
- package/src/watcher/providers/google-calendar.ts +228 -0
- package/src/watcher/providers/slack.ts +129 -0
- package/src/watcher/watcher-store.ts +419 -0
- package/src/work-items/work-item-runner.ts +171 -0
- package/src/work-items/work-item-store.ts +325 -0
- package/src/workspace/commit-message-enrichment-service.ts +284 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +857 -0
- package/src/workspace/heartbeat-service.ts +345 -0
- package/src/workspace/provider-commit-message-generator.ts +285 -0
- package/src/workspace/top-level-renderer.ts +19 -0
- package/src/workspace/top-level-scanner.ts +41 -0
- package/src/workspace/turn-commit.ts +175 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,1535 @@
|
|
|
1
|
+
import { describe, expect, mock, test, beforeEach, afterAll } from 'bun:test';
|
|
2
|
+
import { rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import type { Message, ProviderResponse } from '../providers/types.js';
|
|
4
|
+
import type { AgentEvent, CheckpointInfo, CheckpointDecision } from '../agent/loop.js';
|
|
5
|
+
import type { ServerMessage } from '../daemon/ipc-protocol.js';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Mocks — must precede the Session import so Bun applies them at load time.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function makeLoggerStub(): Record<string, unknown> {
|
|
12
|
+
const stub: Record<string, unknown> = {};
|
|
13
|
+
for (const m of ['info', 'warn', 'error', 'debug', 'trace', 'fatal', 'silent', 'child']) {
|
|
14
|
+
stub[m] = m === 'child' ? () => makeLoggerStub() : () => {};
|
|
15
|
+
}
|
|
16
|
+
return stub;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
mock.module('../util/logger.js', () => ({
|
|
20
|
+
getLogger: () => makeLoggerStub(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module('../util/platform.js', () => ({
|
|
24
|
+
getSocketPath: () => '/tmp/test.sock',
|
|
25
|
+
getDataDir: () => '/tmp',
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock.module('../providers/registry.js', () => ({
|
|
29
|
+
getProvider: () => ({ name: 'mock-provider' }),
|
|
30
|
+
initializeProviders: () => {},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module('../config/loader.js', () => ({
|
|
34
|
+
getConfig: () => ({
|
|
35
|
+
provider: 'mock-provider',
|
|
36
|
+
maxTokens: 4096,
|
|
37
|
+
thinking: false,
|
|
38
|
+
contextWindow: {
|
|
39
|
+
maxInputTokens: 100000,
|
|
40
|
+
thresholdTokens: 80000,
|
|
41
|
+
preserveRecentMessages: 6,
|
|
42
|
+
summaryModel: 'mock-model',
|
|
43
|
+
maxSummaryTokens: 512,
|
|
44
|
+
},
|
|
45
|
+
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
46
|
+
timeouts: { permissionTimeoutSec: 1 },
|
|
47
|
+
apiKeys: {},
|
|
48
|
+
skills: { entries: {}, allowBundled: true },
|
|
49
|
+
memory: { retrieval: { injectionStrategy: 'inline' } },
|
|
50
|
+
permissions: { mode: 'legacy' },
|
|
51
|
+
}),
|
|
52
|
+
loadRawConfig: () => ({}),
|
|
53
|
+
saveRawConfig: () => {},
|
|
54
|
+
invalidateConfigCache: () => {},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
mock.module('../config/system-prompt.js', () => ({
|
|
58
|
+
buildSystemPrompt: () => 'system prompt',
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
mock.module('../config/skills.js', () => ({
|
|
62
|
+
loadSkillCatalog: () => [],
|
|
63
|
+
loadSkillBySelector: () => ({ skill: null }),
|
|
64
|
+
ensureSkillIcon: async () => null,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
mock.module('../config/skill-state.js', () => ({
|
|
68
|
+
resolveSkillStates: () => [],
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
mock.module('../skills/slash-commands.js', () => ({
|
|
72
|
+
buildInvocableSlashCatalog: () => new Map(),
|
|
73
|
+
resolveSlashSkillCommand: () => ({ kind: 'not_slash' }),
|
|
74
|
+
rewriteKnownSlashCommandPrompt: () => '',
|
|
75
|
+
parseSlashCandidate: () => ({ kind: 'not_slash' }),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
mock.module('../permissions/trust-store.js', () => ({
|
|
79
|
+
addRule: () => {},
|
|
80
|
+
findHighestPriorityRule: () => null,
|
|
81
|
+
clearCache: () => {},
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
mock.module('../security/secret-allowlist.js', () => ({
|
|
85
|
+
resetAllowlist: () => {},
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
mock.module('../memory/admin.js', () => ({
|
|
89
|
+
getMemoryConflictAndCleanupStats: () => ({
|
|
90
|
+
conflicts: { pending: 0, resolved: 0, oldestPendingAgeMs: null },
|
|
91
|
+
cleanup: { resolvedBacklog: 0, supersededBacklog: 0, resolvedCompleted24h: 0, supersededCompleted24h: 0 },
|
|
92
|
+
}),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
mock.module('../memory/conversation-store.js', () => ({
|
|
96
|
+
getMessages: () => [],
|
|
97
|
+
getConversation: () => ({
|
|
98
|
+
id: 'conv-1',
|
|
99
|
+
contextSummary: null,
|
|
100
|
+
contextCompactedMessageCount: 0,
|
|
101
|
+
totalInputTokens: 0,
|
|
102
|
+
totalOutputTokens: 0,
|
|
103
|
+
totalEstimatedCost: 0,
|
|
104
|
+
}),
|
|
105
|
+
createConversation: () => ({ id: 'conv-1' }),
|
|
106
|
+
listConversations: () => [],
|
|
107
|
+
addMessage: (_convId: string, _role: string, _content: string) => {
|
|
108
|
+
return { id: `msg-${Date.now()}` };
|
|
109
|
+
},
|
|
110
|
+
updateConversationUsage: () => {},
|
|
111
|
+
updateConversationTitle: () => {},
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
let linkAttachmentShouldThrow = false;
|
|
115
|
+
|
|
116
|
+
mock.module('../memory/attachments-store.js', () => ({
|
|
117
|
+
uploadAttachment: () => ({ id: `att-${Date.now()}` }),
|
|
118
|
+
linkAttachmentToMessage: () => {
|
|
119
|
+
if (linkAttachmentShouldThrow) {
|
|
120
|
+
throw new Error('Simulated linkAttachmentToMessage failure');
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
mock.module('../memory/retriever.js', () => ({
|
|
126
|
+
buildMemoryRecall: async () => ({
|
|
127
|
+
enabled: false,
|
|
128
|
+
degraded: false,
|
|
129
|
+
injectedText: '',
|
|
130
|
+
lexicalHits: 0,
|
|
131
|
+
semanticHits: 0,
|
|
132
|
+
recencyHits: 0,
|
|
133
|
+
injectedTokens: 0,
|
|
134
|
+
latencyMs: 0,
|
|
135
|
+
}),
|
|
136
|
+
injectMemoryRecallIntoUserMessage: (msg: Message) => msg,
|
|
137
|
+
stripMemoryRecallMessages: (msgs: Message[]) => msgs,
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
mock.module('../context/window-manager.js', () => ({
|
|
141
|
+
ContextWindowManager: class {
|
|
142
|
+
constructor() {}
|
|
143
|
+
async maybeCompact() { return { compacted: false }; }
|
|
144
|
+
},
|
|
145
|
+
createContextSummaryMessage: () => ({ role: 'user', content: [{ type: 'text', text: 'summary' }] }),
|
|
146
|
+
getSummaryFromContextMessage: () => null,
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Workspace/git turn-commit test hooks.
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
const turnCommitCalls: Array<{ workspaceDir: string; sessionId: string; turnNumber: number }> = [];
|
|
154
|
+
let turnCommitHangForever = false;
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Usage event capture for request-ID correlation tests.
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
interface CapturedUsageEvent {
|
|
161
|
+
requestId: string | null;
|
|
162
|
+
actor: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let capturedUsageEvents: CapturedUsageEvent[] = [];
|
|
166
|
+
|
|
167
|
+
mock.module('../memory/llm-usage-store.js', () => ({
|
|
168
|
+
recordUsageEvent: (input: { requestId: string | null; actor: string }) => {
|
|
169
|
+
capturedUsageEvents.push({ requestId: input.requestId, actor: input.actor });
|
|
170
|
+
return { id: 'mock-id', createdAt: Date.now(), ...input };
|
|
171
|
+
},
|
|
172
|
+
listUsageEvents: () => [],
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Controllable AgentLoop mock.
|
|
177
|
+
//
|
|
178
|
+
// Each `run()` call returns a promise that does NOT resolve until the test
|
|
179
|
+
// explicitly calls the stored `resolve` callback. This lets us simulate a
|
|
180
|
+
// long-running agent loop so we can enqueue messages while the first one is
|
|
181
|
+
// still "processing".
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
interface PendingRun {
|
|
185
|
+
resolve: (history: Message[]) => void;
|
|
186
|
+
reject: (err: Error) => void;
|
|
187
|
+
messages: Message[];
|
|
188
|
+
onEvent: (event: AgentEvent) => void;
|
|
189
|
+
onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let pendingRuns: PendingRun[] = [];
|
|
193
|
+
|
|
194
|
+
mock.module('../agent/loop.js', () => ({
|
|
195
|
+
AgentLoop: class {
|
|
196
|
+
constructor() {}
|
|
197
|
+
async run(
|
|
198
|
+
messages: Message[],
|
|
199
|
+
onEvent: (event: AgentEvent) => void,
|
|
200
|
+
_signal?: AbortSignal,
|
|
201
|
+
_requestId?: string,
|
|
202
|
+
onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision,
|
|
203
|
+
): Promise<Message[]> {
|
|
204
|
+
return new Promise<Message[]>((resolve, reject) => {
|
|
205
|
+
pendingRuns.push({ resolve, reject, messages, onEvent, onCheckpoint });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Import Session AFTER mocks are registered.
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
import { Session, MAX_QUEUE_DEPTH } from '../daemon/session.js';
|
|
216
|
+
import type { QueueDrainReason, QueuePolicy } from '../daemon/session.js';
|
|
217
|
+
|
|
218
|
+
type SessionWithWorkspaceDeps = Session & {
|
|
219
|
+
getWorkspaceGitService?: (_workspaceDir: string) => { ensureInitialized: () => Promise<void> };
|
|
220
|
+
commitTurnChanges?: (
|
|
221
|
+
workspaceDir: string,
|
|
222
|
+
sessionId: string,
|
|
223
|
+
turnNumber: number,
|
|
224
|
+
provider?: unknown,
|
|
225
|
+
deadlineMs?: number,
|
|
226
|
+
) => Promise<void>;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function makeSession(sendToClient?: (msg: ServerMessage) => void): Session {
|
|
230
|
+
const provider = {
|
|
231
|
+
name: 'mock',
|
|
232
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
233
|
+
return {
|
|
234
|
+
content: [],
|
|
235
|
+
model: 'mock',
|
|
236
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
237
|
+
stopReason: 'end_turn',
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
const session = new Session('conv-1', provider, 'system prompt', 4096, sendToClient ?? (() => {}), '/tmp');
|
|
242
|
+
const sessionWithWorkspaceDeps = session as SessionWithWorkspaceDeps;
|
|
243
|
+
sessionWithWorkspaceDeps.getWorkspaceGitService = () => ({
|
|
244
|
+
ensureInitialized: async () => {},
|
|
245
|
+
});
|
|
246
|
+
sessionWithWorkspaceDeps.commitTurnChanges = async (workspaceDir: string, sessionId: string, turnNumber: number) => {
|
|
247
|
+
turnCommitCalls.push({ workspaceDir, sessionId, turnNumber });
|
|
248
|
+
if (turnCommitHangForever) {
|
|
249
|
+
// Simulate a commit that never resolves within the timeout budget
|
|
250
|
+
await new Promise<void>(() => {});
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
return session;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Wait until the pending runs array has at least `count` entries.
|
|
258
|
+
* This is needed because `processMessage` is async and goes through
|
|
259
|
+
* several awaited steps (context compaction, memory recall) before
|
|
260
|
+
* reaching `agentLoop.run()`.
|
|
261
|
+
*/
|
|
262
|
+
async function waitForPendingRun(count: number, timeoutMs = 2000): Promise<void> {
|
|
263
|
+
const start = Date.now();
|
|
264
|
+
while (pendingRuns.length < count) {
|
|
265
|
+
if (Date.now() - start > timeoutMs) {
|
|
266
|
+
throw new Error(`Timed out waiting for ${count} pending runs (have ${pendingRuns.length})`);
|
|
267
|
+
}
|
|
268
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function waitForCondition(predicate: () => boolean, timeoutMs = 2000): Promise<void> {
|
|
273
|
+
const start = Date.now();
|
|
274
|
+
while (!predicate()) {
|
|
275
|
+
if (Date.now() - start > timeoutMs) {
|
|
276
|
+
throw new Error('Timed out waiting for condition');
|
|
277
|
+
}
|
|
278
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Resolve the Nth pending AgentLoop.run() call. Fires the minimal events
|
|
284
|
+
* that `runAgentLoop` expects (usage + message_complete) so the session
|
|
285
|
+
* cleanly transitions out of its processing state.
|
|
286
|
+
*/
|
|
287
|
+
function resolveRun(index: number) {
|
|
288
|
+
const run = pendingRuns[index];
|
|
289
|
+
if (!run) throw new Error(`No pending run at index ${index}`);
|
|
290
|
+
// Emit the events runAgentLoop expects
|
|
291
|
+
const assistantMsg: Message = {
|
|
292
|
+
role: 'assistant',
|
|
293
|
+
content: [{ type: 'text', text: `reply-${index}` }],
|
|
294
|
+
};
|
|
295
|
+
run.onEvent({ type: 'usage', inputTokens: 10, outputTokens: 5, model: 'mock', providerDurationMs: 100 });
|
|
296
|
+
run.onEvent({ type: 'message_complete', message: assistantMsg });
|
|
297
|
+
// Return updated history with the assistant message appended
|
|
298
|
+
run.resolve([...run.messages, assistantMsg]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
beforeEach(() => {
|
|
302
|
+
turnCommitCalls.length = 0;
|
|
303
|
+
turnCommitHangForever = false;
|
|
304
|
+
linkAttachmentShouldThrow = false;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
afterAll(() => {
|
|
308
|
+
mock.restore();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Tests
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
describe('Session message queue', () => {
|
|
316
|
+
beforeEach(() => {
|
|
317
|
+
pendingRuns = [];
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('second message is queued when session is busy (does not throw)', async () => {
|
|
321
|
+
const session = makeSession();
|
|
322
|
+
await session.loadFromDb();
|
|
323
|
+
|
|
324
|
+
const events1: ServerMessage[] = [];
|
|
325
|
+
const events2: ServerMessage[] = [];
|
|
326
|
+
|
|
327
|
+
// Start first message — this will block on AgentLoop.run
|
|
328
|
+
const p1 = session.processMessage('msg-1', [], (e) => events1.push(e), 'req-1');
|
|
329
|
+
|
|
330
|
+
// Wait for the first AgentLoop.run to be registered
|
|
331
|
+
await waitForPendingRun(1);
|
|
332
|
+
|
|
333
|
+
// Session should now be processing
|
|
334
|
+
expect(session.isProcessing()).toBe(true);
|
|
335
|
+
|
|
336
|
+
// Enqueue second message — should NOT throw
|
|
337
|
+
const result = session.enqueueMessage('msg-2', [], (e) => events2.push(e), 'req-2');
|
|
338
|
+
expect(result.queued).toBe(true);
|
|
339
|
+
expect(result.requestId).toBe('req-2');
|
|
340
|
+
expect(session.getQueueDepth()).toBe(1);
|
|
341
|
+
|
|
342
|
+
// Complete the first message
|
|
343
|
+
resolveRun(0);
|
|
344
|
+
await p1;
|
|
345
|
+
|
|
346
|
+
// After the first run resolves, the queue drains and triggers a second run.
|
|
347
|
+
await waitForPendingRun(2);
|
|
348
|
+
|
|
349
|
+
// The dequeued event should have been sent to events2
|
|
350
|
+
expect(events2.some((e) => e.type === 'message_dequeued')).toBe(true);
|
|
351
|
+
|
|
352
|
+
// A second AgentLoop.run should now be pending
|
|
353
|
+
expect(pendingRuns.length).toBe(2);
|
|
354
|
+
|
|
355
|
+
// Complete the second run
|
|
356
|
+
resolveRun(1);
|
|
357
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('[experimental] queued messages are processed in FIFO order', async () => {
|
|
361
|
+
const session = makeSession();
|
|
362
|
+
await session.loadFromDb();
|
|
363
|
+
|
|
364
|
+
const processedOrder: string[] = [];
|
|
365
|
+
|
|
366
|
+
const makeHandler = (label: string) => (e: ServerMessage) => {
|
|
367
|
+
if (e.type === 'message_complete') processedOrder.push(label);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// Start first message
|
|
371
|
+
const p1 = session.processMessage('msg-1', [], makeHandler('msg-1'), 'req-1');
|
|
372
|
+
await waitForPendingRun(1);
|
|
373
|
+
|
|
374
|
+
// Enqueue two more
|
|
375
|
+
session.enqueueMessage('msg-2', [], makeHandler('msg-2'), 'req-2');
|
|
376
|
+
session.enqueueMessage('msg-3', [], makeHandler('msg-3'), 'req-3');
|
|
377
|
+
expect(session.getQueueDepth()).toBe(2);
|
|
378
|
+
|
|
379
|
+
// Complete first → triggers second
|
|
380
|
+
resolveRun(0);
|
|
381
|
+
await p1;
|
|
382
|
+
await waitForPendingRun(2);
|
|
383
|
+
|
|
384
|
+
// Complete second → triggers third
|
|
385
|
+
resolveRun(1);
|
|
386
|
+
await waitForPendingRun(3);
|
|
387
|
+
|
|
388
|
+
// Complete third
|
|
389
|
+
resolveRun(2);
|
|
390
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
391
|
+
|
|
392
|
+
expect(processedOrder).toEqual(['msg-1', 'msg-2', 'msg-3']);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('message_queued and message_dequeued events are emitted', async () => {
|
|
396
|
+
const session = makeSession();
|
|
397
|
+
await session.loadFromDb();
|
|
398
|
+
|
|
399
|
+
const events2: ServerMessage[] = [];
|
|
400
|
+
|
|
401
|
+
// Start first message
|
|
402
|
+
const p1 = session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
403
|
+
await waitForPendingRun(1);
|
|
404
|
+
|
|
405
|
+
// Enqueue second — simulating what handleUserMessage does
|
|
406
|
+
const result = session.enqueueMessage('msg-2', [], (e) => events2.push(e), 'req-2');
|
|
407
|
+
expect(result.queued).toBe(true);
|
|
408
|
+
|
|
409
|
+
// Complete first
|
|
410
|
+
resolveRun(0);
|
|
411
|
+
await p1;
|
|
412
|
+
await waitForPendingRun(2);
|
|
413
|
+
|
|
414
|
+
// Check for message_dequeued with correct fields
|
|
415
|
+
const dequeued = events2.find((e) => e.type === 'message_dequeued');
|
|
416
|
+
expect(dequeued).toBeDefined();
|
|
417
|
+
expect(dequeued).toEqual({
|
|
418
|
+
type: 'message_dequeued',
|
|
419
|
+
sessionId: 'conv-1',
|
|
420
|
+
requestId: 'req-2',
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Complete second run so the session finishes cleanly
|
|
424
|
+
resolveRun(1);
|
|
425
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('abort() clears the queue and sends generation_cancelled for each queued message', async () => {
|
|
429
|
+
const session = makeSession();
|
|
430
|
+
await session.loadFromDb();
|
|
431
|
+
|
|
432
|
+
const events2: ServerMessage[] = [];
|
|
433
|
+
const events3: ServerMessage[] = [];
|
|
434
|
+
|
|
435
|
+
// Start first message
|
|
436
|
+
session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
437
|
+
await waitForPendingRun(1);
|
|
438
|
+
|
|
439
|
+
// Enqueue two more
|
|
440
|
+
session.enqueueMessage('msg-2', [], (e) => events2.push(e), 'req-2');
|
|
441
|
+
session.enqueueMessage('msg-3', [], (e) => events3.push(e), 'req-3');
|
|
442
|
+
expect(session.getQueueDepth()).toBe(2);
|
|
443
|
+
|
|
444
|
+
// Abort
|
|
445
|
+
session.abort();
|
|
446
|
+
|
|
447
|
+
// Queue should be empty
|
|
448
|
+
expect(session.getQueueDepth()).toBe(0);
|
|
449
|
+
|
|
450
|
+
// Both queued messages should receive session-scoped cancellation events.
|
|
451
|
+
const cancel2 = events2.find((e) => e.type === 'generation_cancelled');
|
|
452
|
+
expect(cancel2).toEqual({ type: 'generation_cancelled', sessionId: 'conv-1' });
|
|
453
|
+
|
|
454
|
+
const cancel3 = events3.find((e) => e.type === 'generation_cancelled');
|
|
455
|
+
expect(cancel3).toEqual({ type: 'generation_cancelled', sessionId: 'conv-1' });
|
|
456
|
+
|
|
457
|
+
// abort() must NOT emit session_error or generic error for queued discards.
|
|
458
|
+
const err2 = events2.find((e) => e.type === 'error');
|
|
459
|
+
expect(err2).toBeUndefined();
|
|
460
|
+
const err3 = events3.find((e) => e.type === 'error');
|
|
461
|
+
expect(err3).toBeUndefined();
|
|
462
|
+
|
|
463
|
+
const sessionErr2 = events2.find((e) => e.type === 'session_error');
|
|
464
|
+
expect(sessionErr2).toBeUndefined();
|
|
465
|
+
|
|
466
|
+
const sessionErr3 = events3.find((e) => e.type === 'session_error');
|
|
467
|
+
expect(sessionErr3).toBeUndefined();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('session-scoped errors emit both session_error and generic error', async () => {
|
|
471
|
+
const session = makeSession();
|
|
472
|
+
await session.loadFromDb();
|
|
473
|
+
|
|
474
|
+
const events: ServerMessage[] = [];
|
|
475
|
+
|
|
476
|
+
// Start a message — blocks on AgentLoop.run
|
|
477
|
+
const p1 = session.processMessage('msg-1', [], (e) => events.push(e), 'req-1');
|
|
478
|
+
await waitForPendingRun(1);
|
|
479
|
+
|
|
480
|
+
// Reject the AgentLoop.run() with a provider error to trigger the
|
|
481
|
+
// runAgentLoop catch block
|
|
482
|
+
pendingRuns[0].reject(new Error('Provider returned 500'));
|
|
483
|
+
await p1;
|
|
484
|
+
|
|
485
|
+
// Should emit session_error (typed, structured)
|
|
486
|
+
const sessionErr = events.find((e) => e.type === 'session_error');
|
|
487
|
+
expect(sessionErr).toBeDefined();
|
|
488
|
+
|
|
489
|
+
// Should also emit generic error for backward compatibility
|
|
490
|
+
// (RunOrchestrator relies on error events to detect failures)
|
|
491
|
+
const genericErr = events.find((e) => e.type === 'error');
|
|
492
|
+
expect(genericErr).toBeDefined();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test('queue depth is reported correctly as messages are added and drained', async () => {
|
|
496
|
+
const session = makeSession();
|
|
497
|
+
await session.loadFromDb();
|
|
498
|
+
|
|
499
|
+
// Start first message
|
|
500
|
+
const p1 = session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
501
|
+
await waitForPendingRun(1);
|
|
502
|
+
|
|
503
|
+
expect(session.getQueueDepth()).toBe(0);
|
|
504
|
+
|
|
505
|
+
session.enqueueMessage('msg-2', [], () => {}, 'req-2');
|
|
506
|
+
expect(session.getQueueDepth()).toBe(1);
|
|
507
|
+
|
|
508
|
+
session.enqueueMessage('msg-3', [], () => {}, 'req-3');
|
|
509
|
+
expect(session.getQueueDepth()).toBe(2);
|
|
510
|
+
|
|
511
|
+
session.enqueueMessage('msg-4', [], () => {}, 'req-4');
|
|
512
|
+
expect(session.getQueueDepth()).toBe(3);
|
|
513
|
+
|
|
514
|
+
// Complete first → drains one from queue
|
|
515
|
+
resolveRun(0);
|
|
516
|
+
await p1;
|
|
517
|
+
await waitForPendingRun(2);
|
|
518
|
+
|
|
519
|
+
expect(session.getQueueDepth()).toBe(2);
|
|
520
|
+
|
|
521
|
+
// Complete second → drains another
|
|
522
|
+
resolveRun(1);
|
|
523
|
+
await waitForPendingRun(3);
|
|
524
|
+
|
|
525
|
+
expect(session.getQueueDepth()).toBe(1);
|
|
526
|
+
|
|
527
|
+
// Complete third → drains last
|
|
528
|
+
resolveRun(2);
|
|
529
|
+
await waitForPendingRun(4);
|
|
530
|
+
|
|
531
|
+
expect(session.getQueueDepth()).toBe(0);
|
|
532
|
+
|
|
533
|
+
// Complete fourth (final queued message)
|
|
534
|
+
resolveRun(3);
|
|
535
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('[experimental] drain continues after a queued message fails to persist', async () => {
|
|
539
|
+
const session = makeSession();
|
|
540
|
+
await session.loadFromDb();
|
|
541
|
+
|
|
542
|
+
const events1: ServerMessage[] = [];
|
|
543
|
+
const events2: ServerMessage[] = [];
|
|
544
|
+
const events3: ServerMessage[] = [];
|
|
545
|
+
|
|
546
|
+
// Start first message — blocks on AgentLoop.run
|
|
547
|
+
const p1 = session.processMessage('msg-1', [], (e) => events1.push(e), 'req-1');
|
|
548
|
+
await waitForPendingRun(1);
|
|
549
|
+
|
|
550
|
+
// Enqueue a message with empty content (will fail persistUserMessage)
|
|
551
|
+
session.enqueueMessage('', [], (e) => events2.push(e), 'req-2');
|
|
552
|
+
// Enqueue a valid message after the bad one
|
|
553
|
+
session.enqueueMessage('msg-3', [], (e) => events3.push(e), 'req-3');
|
|
554
|
+
expect(session.getQueueDepth()).toBe(2);
|
|
555
|
+
|
|
556
|
+
// Complete first message — triggers drain. The empty message should fail
|
|
557
|
+
// to persist, but the drain should continue to msg-3.
|
|
558
|
+
resolveRun(0);
|
|
559
|
+
await p1;
|
|
560
|
+
|
|
561
|
+
// msg-3 should have been dequeued and started a new AgentLoop.run
|
|
562
|
+
await waitForPendingRun(2);
|
|
563
|
+
|
|
564
|
+
// The empty message should have received an error event
|
|
565
|
+
const err2 = events2.find((e) => e.type === 'error');
|
|
566
|
+
expect(err2).toBeDefined();
|
|
567
|
+
if (err2 && err2.type === 'error') {
|
|
568
|
+
expect(err2.message).toContain('required');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// msg-3 should have received a dequeued event
|
|
572
|
+
expect(events3.some((e) => e.type === 'message_dequeued')).toBe(true);
|
|
573
|
+
|
|
574
|
+
// Complete the third message's run
|
|
575
|
+
resolveRun(1);
|
|
576
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
577
|
+
|
|
578
|
+
// msg-3 should have completed successfully
|
|
579
|
+
expect(events3.some((e) => e.type === 'message_complete')).toBe(true);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('queue rejects when at max depth', async () => {
|
|
583
|
+
const session = makeSession();
|
|
584
|
+
await session.loadFromDb();
|
|
585
|
+
|
|
586
|
+
// Start first message to make session busy
|
|
587
|
+
session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
588
|
+
await waitForPendingRun(1);
|
|
589
|
+
|
|
590
|
+
// Fill the queue to MAX_QUEUE_DEPTH
|
|
591
|
+
for (let i = 0; i < MAX_QUEUE_DEPTH; i++) {
|
|
592
|
+
const result = session.enqueueMessage(`msg-${i + 2}`, [], () => {}, `req-${i + 2}`);
|
|
593
|
+
expect(result.queued).toBe(true);
|
|
594
|
+
expect(result.rejected).toBeUndefined();
|
|
595
|
+
}
|
|
596
|
+
expect(session.getQueueDepth()).toBe(MAX_QUEUE_DEPTH);
|
|
597
|
+
|
|
598
|
+
// Next enqueue should be rejected
|
|
599
|
+
const rejected = session.enqueueMessage('overflow', [], () => {}, 'req-overflow');
|
|
600
|
+
expect(rejected.queued).toBe(false);
|
|
601
|
+
expect(rejected.rejected).toBe(true);
|
|
602
|
+
|
|
603
|
+
// Queue depth should not have increased
|
|
604
|
+
expect(session.getQueueDepth()).toBe(MAX_QUEUE_DEPTH);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
// Queue policy primitives
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
|
|
612
|
+
describe('Session queue policy helpers', () => {
|
|
613
|
+
beforeEach(() => {
|
|
614
|
+
pendingRuns = [];
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test('hasQueuedMessages() returns false on a fresh session', async () => {
|
|
618
|
+
const session = makeSession();
|
|
619
|
+
await session.loadFromDb();
|
|
620
|
+
expect(session.hasQueuedMessages()).toBe(false);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('hasQueuedMessages() returns true after enqueuing while processing', async () => {
|
|
624
|
+
const session = makeSession();
|
|
625
|
+
await session.loadFromDb();
|
|
626
|
+
|
|
627
|
+
// Start processing to make the session busy
|
|
628
|
+
session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
629
|
+
await waitForPendingRun(1);
|
|
630
|
+
|
|
631
|
+
// Enqueue a message while processing
|
|
632
|
+
session.enqueueMessage('msg-2', [], () => {}, 'req-2');
|
|
633
|
+
expect(session.hasQueuedMessages()).toBe(true);
|
|
634
|
+
|
|
635
|
+
// Cleanup: resolve the pending run
|
|
636
|
+
resolveRun(0);
|
|
637
|
+
await waitForPendingRun(2);
|
|
638
|
+
resolveRun(1);
|
|
639
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test('canHandoffAtCheckpoint() returns false when not processing', async () => {
|
|
643
|
+
const session = makeSession();
|
|
644
|
+
await session.loadFromDb();
|
|
645
|
+
|
|
646
|
+
// Not processing, no queued messages
|
|
647
|
+
expect(session.canHandoffAtCheckpoint()).toBe(false);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('canHandoffAtCheckpoint() returns false when processing but no queued messages', async () => {
|
|
651
|
+
const session = makeSession();
|
|
652
|
+
await session.loadFromDb();
|
|
653
|
+
|
|
654
|
+
// Start processing — but don't enqueue anything
|
|
655
|
+
session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
656
|
+
await waitForPendingRun(1);
|
|
657
|
+
|
|
658
|
+
expect(session.isProcessing()).toBe(true);
|
|
659
|
+
expect(session.hasQueuedMessages()).toBe(false);
|
|
660
|
+
expect(session.canHandoffAtCheckpoint()).toBe(false);
|
|
661
|
+
|
|
662
|
+
// Cleanup
|
|
663
|
+
resolveRun(0);
|
|
664
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('canHandoffAtCheckpoint() returns true when processing and queue has messages', async () => {
|
|
668
|
+
const session = makeSession();
|
|
669
|
+
await session.loadFromDb();
|
|
670
|
+
|
|
671
|
+
// Start processing
|
|
672
|
+
session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
673
|
+
await waitForPendingRun(1);
|
|
674
|
+
|
|
675
|
+
// Enqueue a message
|
|
676
|
+
session.enqueueMessage('msg-2', [], () => {}, 'req-2');
|
|
677
|
+
|
|
678
|
+
expect(session.isProcessing()).toBe(true);
|
|
679
|
+
expect(session.hasQueuedMessages()).toBe(true);
|
|
680
|
+
expect(session.canHandoffAtCheckpoint()).toBe(true);
|
|
681
|
+
|
|
682
|
+
// Cleanup
|
|
683
|
+
resolveRun(0);
|
|
684
|
+
await waitForPendingRun(2);
|
|
685
|
+
resolveRun(1);
|
|
686
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test('QueueDrainReason type accepts expected values', () => {
|
|
690
|
+
// Compile-time verification that these are valid QueueDrainReason values
|
|
691
|
+
const reason1: QueueDrainReason = 'loop_complete';
|
|
692
|
+
const reason2: QueueDrainReason = 'checkpoint_handoff';
|
|
693
|
+
expect(reason1).toBe('loop_complete');
|
|
694
|
+
expect(reason2).toBe('checkpoint_handoff');
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test('QueuePolicy type accepts expected shape', () => {
|
|
698
|
+
// Compile-time verification that the QueuePolicy interface works
|
|
699
|
+
const policy: QueuePolicy = { checkpointHandoffEnabled: true };
|
|
700
|
+
expect(policy.checkpointHandoffEnabled).toBe(true);
|
|
701
|
+
|
|
702
|
+
const disabledPolicy: QueuePolicy = { checkpointHandoffEnabled: false };
|
|
703
|
+
expect(disabledPolicy.checkpointHandoffEnabled).toBe(false);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
// Checkpoint handoff tests
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
|
|
711
|
+
describe('Session checkpoint handoff', () => {
|
|
712
|
+
beforeEach(() => {
|
|
713
|
+
pendingRuns = [];
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test('[experimental] onCheckpoint yields when there is a queued message', async () => {
|
|
717
|
+
const session = makeSession();
|
|
718
|
+
await session.loadFromDb();
|
|
719
|
+
|
|
720
|
+
const events1: ServerMessage[] = [];
|
|
721
|
+
|
|
722
|
+
// Start processing first message
|
|
723
|
+
const p1 = session.processMessage('msg-1', [], (e) => events1.push(e), 'req-1');
|
|
724
|
+
await waitForPendingRun(1);
|
|
725
|
+
|
|
726
|
+
// Enqueue a second message while the first is processing
|
|
727
|
+
session.enqueueMessage('msg-2', [], () => {}, 'req-2');
|
|
728
|
+
expect(session.hasQueuedMessages()).toBe(true);
|
|
729
|
+
|
|
730
|
+
// The pending run should have received an onCheckpoint callback.
|
|
731
|
+
// Simulate the agent loop calling it at a turn boundary.
|
|
732
|
+
const run = pendingRuns[0];
|
|
733
|
+
expect(run.onCheckpoint).toBeDefined();
|
|
734
|
+
const decision = run.onCheckpoint!({
|
|
735
|
+
turnIndex: 0,
|
|
736
|
+
toolCount: 1,
|
|
737
|
+
hasToolUse: true,
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Because there is a queued message, the callback should return 'yield'
|
|
741
|
+
expect(decision).toBe('yield');
|
|
742
|
+
|
|
743
|
+
// Complete the run so the session finishes cleanly
|
|
744
|
+
resolveRun(0);
|
|
745
|
+
await p1;
|
|
746
|
+
|
|
747
|
+
// After yield, the first message should emit generation_handoff
|
|
748
|
+
const handoff = events1.find((e) => e.type === 'generation_handoff');
|
|
749
|
+
expect(handoff).toBeDefined();
|
|
750
|
+
expect(handoff).toMatchObject({
|
|
751
|
+
type: 'generation_handoff',
|
|
752
|
+
sessionId: 'conv-1',
|
|
753
|
+
requestId: 'req-1',
|
|
754
|
+
queuedCount: 1,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// The queued message should now be draining (second run started)
|
|
758
|
+
await waitForPendingRun(2);
|
|
759
|
+
resolveRun(1);
|
|
760
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test('onCheckpoint returns continue when queue is empty', async () => {
|
|
764
|
+
const session = makeSession();
|
|
765
|
+
await session.loadFromDb();
|
|
766
|
+
|
|
767
|
+
// Start processing — no enqueued messages
|
|
768
|
+
const p1 = session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
769
|
+
await waitForPendingRun(1);
|
|
770
|
+
|
|
771
|
+
expect(session.hasQueuedMessages()).toBe(false);
|
|
772
|
+
|
|
773
|
+
// The pending run should have an onCheckpoint callback
|
|
774
|
+
const run = pendingRuns[0];
|
|
775
|
+
expect(run.onCheckpoint).toBeDefined();
|
|
776
|
+
const decision = run.onCheckpoint!({
|
|
777
|
+
turnIndex: 0,
|
|
778
|
+
toolCount: 1,
|
|
779
|
+
hasToolUse: true,
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// No queued messages → continue
|
|
783
|
+
expect(decision).toBe('continue');
|
|
784
|
+
|
|
785
|
+
// Cleanup
|
|
786
|
+
resolveRun(0);
|
|
787
|
+
await p1;
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test('[experimental] FIFO ordering is preserved through checkpoint handoff', async () => {
|
|
791
|
+
const session = makeSession();
|
|
792
|
+
await session.loadFromDb();
|
|
793
|
+
|
|
794
|
+
const processedOrder: string[] = [];
|
|
795
|
+
|
|
796
|
+
const makeHandler = (label: string) => (e: ServerMessage) => {
|
|
797
|
+
if (e.type === 'message_complete' || e.type === 'generation_handoff') processedOrder.push(label);
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// Start first message
|
|
801
|
+
const p1 = session.processMessage('msg-1', [], makeHandler('msg-1'), 'req-1');
|
|
802
|
+
await waitForPendingRun(1);
|
|
803
|
+
|
|
804
|
+
// Enqueue two messages
|
|
805
|
+
session.enqueueMessage('msg-2', [], makeHandler('msg-2'), 'req-2');
|
|
806
|
+
session.enqueueMessage('msg-3', [], makeHandler('msg-3'), 'req-3');
|
|
807
|
+
expect(session.getQueueDepth()).toBe(2);
|
|
808
|
+
|
|
809
|
+
// Simulate the agent loop yielding at the checkpoint (first run)
|
|
810
|
+
const run0 = pendingRuns[0];
|
|
811
|
+
expect(run0.onCheckpoint).toBeDefined();
|
|
812
|
+
const decision = run0.onCheckpoint!({ turnIndex: 0, toolCount: 1, hasToolUse: true });
|
|
813
|
+
expect(decision).toBe('yield');
|
|
814
|
+
|
|
815
|
+
// Complete first run
|
|
816
|
+
resolveRun(0);
|
|
817
|
+
await p1;
|
|
818
|
+
|
|
819
|
+
// msg-2 should be draining next
|
|
820
|
+
await waitForPendingRun(2);
|
|
821
|
+
|
|
822
|
+
// Complete second run (msg-2)
|
|
823
|
+
resolveRun(1);
|
|
824
|
+
await waitForPendingRun(3);
|
|
825
|
+
|
|
826
|
+
// Complete third run (msg-3)
|
|
827
|
+
resolveRun(2);
|
|
828
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
829
|
+
|
|
830
|
+
// FIFO order: msg-1 completes first, then msg-2, then msg-3
|
|
831
|
+
expect(processedOrder).toEqual(['msg-1', 'msg-2', 'msg-3']);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test('queue-full rejection still works during checkpoint handoff', async () => {
|
|
835
|
+
const session = makeSession();
|
|
836
|
+
await session.loadFromDb();
|
|
837
|
+
|
|
838
|
+
// Start processing
|
|
839
|
+
session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
840
|
+
await waitForPendingRun(1);
|
|
841
|
+
|
|
842
|
+
// Fill the queue to MAX_QUEUE_DEPTH
|
|
843
|
+
for (let i = 0; i < MAX_QUEUE_DEPTH; i++) {
|
|
844
|
+
const result = session.enqueueMessage(`queued-${i}`, [], () => {}, `req-q-${i}`);
|
|
845
|
+
expect(result.queued).toBe(true);
|
|
846
|
+
}
|
|
847
|
+
expect(session.getQueueDepth()).toBe(MAX_QUEUE_DEPTH);
|
|
848
|
+
|
|
849
|
+
// Verify checkpoint would yield (there are queued messages)
|
|
850
|
+
const run = pendingRuns[0];
|
|
851
|
+
expect(run.onCheckpoint).toBeDefined();
|
|
852
|
+
expect(run.onCheckpoint!({ turnIndex: 0, toolCount: 1, hasToolUse: true })).toBe('yield');
|
|
853
|
+
|
|
854
|
+
// Next enqueue should still be rejected
|
|
855
|
+
const rejected = session.enqueueMessage('overflow', [], () => {}, 'req-overflow');
|
|
856
|
+
expect(rejected.queued).toBe(false);
|
|
857
|
+
expect(rejected.rejected).toBe(true);
|
|
858
|
+
|
|
859
|
+
// Queue depth unchanged
|
|
860
|
+
expect(session.getQueueDepth()).toBe(MAX_QUEUE_DEPTH);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test('[experimental] active run with repeated tool turns + queued message triggers checkpoint handoff', async () => {
|
|
864
|
+
const session = makeSession();
|
|
865
|
+
await session.loadFromDb();
|
|
866
|
+
|
|
867
|
+
const events1: ServerMessage[] = [];
|
|
868
|
+
const events2: ServerMessage[] = [];
|
|
869
|
+
|
|
870
|
+
// Start processing first message
|
|
871
|
+
const p1 = session.processMessage('msg-1', [], (e) => events1.push(e), 'req-1');
|
|
872
|
+
await waitForPendingRun(1);
|
|
873
|
+
|
|
874
|
+
// Enqueue a second message while the first is processing
|
|
875
|
+
session.enqueueMessage('msg-2', [], (e) => events2.push(e), 'req-2');
|
|
876
|
+
expect(session.hasQueuedMessages()).toBe(true);
|
|
877
|
+
|
|
878
|
+
// Simulate tool-use turns: the agent loop calls onCheckpoint at each turn boundary.
|
|
879
|
+
// Because there is a queued message, the callback should return 'yield'.
|
|
880
|
+
const run = pendingRuns[0];
|
|
881
|
+
expect(run.onCheckpoint).toBeDefined();
|
|
882
|
+
|
|
883
|
+
// Simulate multiple tool-use turns before the checkpoint fires
|
|
884
|
+
// Turn 0 — checkpoint yields because msg-2 is waiting
|
|
885
|
+
const decision = run.onCheckpoint!({
|
|
886
|
+
turnIndex: 0,
|
|
887
|
+
toolCount: 1,
|
|
888
|
+
hasToolUse: true,
|
|
889
|
+
});
|
|
890
|
+
expect(decision).toBe('yield');
|
|
891
|
+
|
|
892
|
+
// Complete the run (AgentLoop resolves after yielding)
|
|
893
|
+
resolveRun(0);
|
|
894
|
+
await p1;
|
|
895
|
+
|
|
896
|
+
// Verify generation_handoff was emitted (not plain message_complete)
|
|
897
|
+
const handoff = events1.find((e) => e.type === 'generation_handoff');
|
|
898
|
+
expect(handoff).toBeDefined();
|
|
899
|
+
expect(handoff).toMatchObject({
|
|
900
|
+
type: 'generation_handoff',
|
|
901
|
+
sessionId: 'conv-1',
|
|
902
|
+
requestId: 'req-1',
|
|
903
|
+
queuedCount: 1,
|
|
904
|
+
});
|
|
905
|
+
// message_complete should NOT be in events1 (handoff replaces it)
|
|
906
|
+
const messageComplete = events1.find((e) => e.type === 'message_complete' && 'sessionId' in e);
|
|
907
|
+
expect(messageComplete).toBeUndefined();
|
|
908
|
+
|
|
909
|
+
// The queued message should subsequently drain
|
|
910
|
+
await waitForPendingRun(2);
|
|
911
|
+
expect(events2.some((e) => e.type === 'message_dequeued')).toBe(true);
|
|
912
|
+
|
|
913
|
+
// Complete the second run
|
|
914
|
+
resolveRun(1);
|
|
915
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test('queued messages still drain FIFO under multiple handoffs', async () => {
|
|
919
|
+
const session = makeSession();
|
|
920
|
+
await session.loadFromDb();
|
|
921
|
+
|
|
922
|
+
const dequeueOrder: string[] = [];
|
|
923
|
+
|
|
924
|
+
const eventsA: ServerMessage[] = [];
|
|
925
|
+
const makeHandler = (label: string) => (e: ServerMessage) => {
|
|
926
|
+
if (e.type === 'message_dequeued') dequeueOrder.push(label);
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// Start processing message A
|
|
930
|
+
const pA = session.processMessage('msg-A', [], (e) => eventsA.push(e), 'req-A');
|
|
931
|
+
await waitForPendingRun(1);
|
|
932
|
+
|
|
933
|
+
// Enqueue messages B, C, D
|
|
934
|
+
session.enqueueMessage('msg-B', [], makeHandler('B'), 'req-B');
|
|
935
|
+
session.enqueueMessage('msg-C', [], makeHandler('C'), 'req-C');
|
|
936
|
+
session.enqueueMessage('msg-D', [], makeHandler('D'), 'req-D');
|
|
937
|
+
expect(session.getQueueDepth()).toBe(3);
|
|
938
|
+
|
|
939
|
+
// Handoff from A -> B
|
|
940
|
+
const runA = pendingRuns[0];
|
|
941
|
+
expect(runA.onCheckpoint).toBeDefined();
|
|
942
|
+
expect(runA.onCheckpoint!({ turnIndex: 0, toolCount: 1, hasToolUse: true })).toBe('yield');
|
|
943
|
+
resolveRun(0);
|
|
944
|
+
await pA;
|
|
945
|
+
|
|
946
|
+
// B should be draining
|
|
947
|
+
await waitForPendingRun(2);
|
|
948
|
+
|
|
949
|
+
// Handoff from B -> C
|
|
950
|
+
const runB = pendingRuns[1];
|
|
951
|
+
expect(runB.onCheckpoint).toBeDefined();
|
|
952
|
+
expect(runB.onCheckpoint!({ turnIndex: 0, toolCount: 1, hasToolUse: true })).toBe('yield');
|
|
953
|
+
resolveRun(1);
|
|
954
|
+
await waitForPendingRun(3);
|
|
955
|
+
|
|
956
|
+
// Handoff from C -> D
|
|
957
|
+
const runC = pendingRuns[2];
|
|
958
|
+
expect(runC.onCheckpoint).toBeDefined();
|
|
959
|
+
// Only D remains, still should yield
|
|
960
|
+
expect(runC.onCheckpoint!({ turnIndex: 0, toolCount: 1, hasToolUse: true })).toBe('yield');
|
|
961
|
+
resolveRun(2);
|
|
962
|
+
await waitForPendingRun(4);
|
|
963
|
+
|
|
964
|
+
// D has no more queued -> checkpoint should return 'continue'
|
|
965
|
+
const runD = pendingRuns[3];
|
|
966
|
+
expect(runD.onCheckpoint).toBeDefined();
|
|
967
|
+
expect(runD.onCheckpoint!({ turnIndex: 0, toolCount: 1, hasToolUse: true })).toBe('continue');
|
|
968
|
+
|
|
969
|
+
resolveRun(3);
|
|
970
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
971
|
+
|
|
972
|
+
// Verify FIFO dequeue order
|
|
973
|
+
expect(dequeueOrder).toEqual(['B', 'C', 'D']);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
test('[experimental] queued persistence failure does not strand later messages', async () => {
|
|
977
|
+
const session = makeSession();
|
|
978
|
+
await session.loadFromDb();
|
|
979
|
+
|
|
980
|
+
const eventsA: ServerMessage[] = [];
|
|
981
|
+
const eventsB: ServerMessage[] = [];
|
|
982
|
+
const eventsC: ServerMessage[] = [];
|
|
983
|
+
|
|
984
|
+
// Start processing message A
|
|
985
|
+
const pA = session.processMessage('msg-A', [], (e) => eventsA.push(e), 'req-A');
|
|
986
|
+
await waitForPendingRun(1);
|
|
987
|
+
|
|
988
|
+
// Enqueue B (empty content — will fail to persist) and C (valid)
|
|
989
|
+
session.enqueueMessage('', [], (e) => eventsB.push(e), 'req-B');
|
|
990
|
+
session.enqueueMessage('msg-C', [], (e) => eventsC.push(e), 'req-C');
|
|
991
|
+
expect(session.getQueueDepth()).toBe(2);
|
|
992
|
+
|
|
993
|
+
// Complete message A — triggers drain. B should fail, C should proceed.
|
|
994
|
+
resolveRun(0);
|
|
995
|
+
await pA;
|
|
996
|
+
|
|
997
|
+
// C should have been dequeued and started a new AgentLoop.run
|
|
998
|
+
await waitForPendingRun(2);
|
|
999
|
+
|
|
1000
|
+
// B should have received an error event
|
|
1001
|
+
const errB = eventsB.find((e) => e.type === 'error');
|
|
1002
|
+
expect(errB).toBeDefined();
|
|
1003
|
+
if (errB && errB.type === 'error') {
|
|
1004
|
+
expect(errB.message).toContain('required');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// C should have received a dequeued event
|
|
1008
|
+
expect(eventsC.some((e) => e.type === 'message_dequeued')).toBe(true);
|
|
1009
|
+
|
|
1010
|
+
// Complete C's run
|
|
1011
|
+
resolveRun(1);
|
|
1012
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1013
|
+
|
|
1014
|
+
// C should have completed successfully
|
|
1015
|
+
expect(eventsC.some((e) => e.type === 'message_complete')).toBe(true);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
test('onCheckpoint callback is passed to both initial and retry runs', async () => {
|
|
1019
|
+
const session = makeSession();
|
|
1020
|
+
await session.loadFromDb();
|
|
1021
|
+
|
|
1022
|
+
// Start processing
|
|
1023
|
+
const p1 = session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
1024
|
+
await waitForPendingRun(1);
|
|
1025
|
+
|
|
1026
|
+
// The first run should have onCheckpoint
|
|
1027
|
+
expect(pendingRuns[0].onCheckpoint).toBeDefined();
|
|
1028
|
+
|
|
1029
|
+
// Simulate an ordering error: emit error + resolve with same length
|
|
1030
|
+
// to trigger the retry path
|
|
1031
|
+
const run0 = pendingRuns[0];
|
|
1032
|
+
run0.onEvent({
|
|
1033
|
+
type: 'error',
|
|
1034
|
+
error: new Error('tool_result block not immediately after tool_use block'),
|
|
1035
|
+
});
|
|
1036
|
+
// Resolve with the same messages (no new messages appended = ordering error)
|
|
1037
|
+
run0.resolve([...run0.messages]);
|
|
1038
|
+
|
|
1039
|
+
// Wait for the retry run
|
|
1040
|
+
await waitForPendingRun(2);
|
|
1041
|
+
|
|
1042
|
+
// The retry run should also have onCheckpoint
|
|
1043
|
+
expect(pendingRuns[1].onCheckpoint).toBeDefined();
|
|
1044
|
+
|
|
1045
|
+
// Complete retry cleanly
|
|
1046
|
+
resolveRun(1);
|
|
1047
|
+
await p1;
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
// ---------------------------------------------------------------------------
|
|
1052
|
+
// Usage requestId correlation
|
|
1053
|
+
// ---------------------------------------------------------------------------
|
|
1054
|
+
|
|
1055
|
+
describe('Session usage requestId correlation', () => {
|
|
1056
|
+
beforeEach(() => {
|
|
1057
|
+
pendingRuns = [];
|
|
1058
|
+
capturedUsageEvents = [];
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test('usage events recorded during a request carry that request ID', async () => {
|
|
1062
|
+
const session = makeSession();
|
|
1063
|
+
await session.loadFromDb();
|
|
1064
|
+
|
|
1065
|
+
const p1 = session.processMessage('msg-1', [], () => {}, 'req-42');
|
|
1066
|
+
await waitForPendingRun(1);
|
|
1067
|
+
|
|
1068
|
+
// Complete the run — this triggers recordUsage with the request's ID
|
|
1069
|
+
resolveRun(0);
|
|
1070
|
+
await p1;
|
|
1071
|
+
|
|
1072
|
+
// The usage event should carry the request ID, not null
|
|
1073
|
+
const mainAgentUsage = capturedUsageEvents.find((e) => e.actor === 'main_agent');
|
|
1074
|
+
expect(mainAgentUsage).toBeDefined();
|
|
1075
|
+
expect(mainAgentUsage!.requestId).toBe('req-42');
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// ---------------------------------------------------------------------------
|
|
1080
|
+
// Terminal trace events on rejection/failure paths
|
|
1081
|
+
// ---------------------------------------------------------------------------
|
|
1082
|
+
|
|
1083
|
+
describe('Terminal trace events on rejection/failure', () => {
|
|
1084
|
+
beforeEach(() => {
|
|
1085
|
+
pendingRuns = [];
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
test('queued persist failure emits request_error trace', async () => {
|
|
1089
|
+
const traceEvents: ServerMessage[] = [];
|
|
1090
|
+
const session = makeSession((msg) => {
|
|
1091
|
+
if ('type' in msg && msg.type === 'trace_event') traceEvents.push(msg);
|
|
1092
|
+
});
|
|
1093
|
+
await session.loadFromDb();
|
|
1094
|
+
|
|
1095
|
+
// Start first message
|
|
1096
|
+
const p1 = session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
1097
|
+
await waitForPendingRun(1);
|
|
1098
|
+
|
|
1099
|
+
// Enqueue empty content (will fail persistUserMessage)
|
|
1100
|
+
session.enqueueMessage('', [], () => {}, 'req-bad');
|
|
1101
|
+
// Enqueue valid message so drain continues
|
|
1102
|
+
session.enqueueMessage('msg-3', [], () => {}, 'req-3');
|
|
1103
|
+
|
|
1104
|
+
// Complete first — triggers drain, empty msg fails persist
|
|
1105
|
+
resolveRun(0);
|
|
1106
|
+
await p1;
|
|
1107
|
+
await waitForPendingRun(2);
|
|
1108
|
+
|
|
1109
|
+
// Should have a request_error trace for the failed persist
|
|
1110
|
+
const errorTrace = traceEvents.find(
|
|
1111
|
+
(e) => 'kind' in e && e.kind === 'request_error' && 'requestId' in e && e.requestId === 'req-bad',
|
|
1112
|
+
);
|
|
1113
|
+
expect(errorTrace).toBeDefined();
|
|
1114
|
+
|
|
1115
|
+
// Cleanup
|
|
1116
|
+
resolveRun(1);
|
|
1117
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// ---------------------------------------------------------------------------
|
|
1122
|
+
// Surface-action queue-full trace emission
|
|
1123
|
+
// ---------------------------------------------------------------------------
|
|
1124
|
+
|
|
1125
|
+
describe('Surface-action queue-full trace', () => {
|
|
1126
|
+
beforeEach(() => {
|
|
1127
|
+
pendingRuns = [];
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test('surface-action queue-full rejection emits request_error trace', async () => {
|
|
1131
|
+
const traceEvents: ServerMessage[] = [];
|
|
1132
|
+
const session = makeSession((msg) => {
|
|
1133
|
+
if ('type' in msg && msg.type === 'trace_event') traceEvents.push(msg);
|
|
1134
|
+
});
|
|
1135
|
+
await session.loadFromDb();
|
|
1136
|
+
|
|
1137
|
+
// Start processing to make the session busy
|
|
1138
|
+
session.processMessage('msg-1', [], () => {}, 'req-1');
|
|
1139
|
+
await waitForPendingRun(1);
|
|
1140
|
+
|
|
1141
|
+
// Fill the queue to MAX_QUEUE_DEPTH
|
|
1142
|
+
for (let i = 0; i < MAX_QUEUE_DEPTH; i++) {
|
|
1143
|
+
const result = session.enqueueMessage(`queued-${i}`, [], () => {}, `req-q-${i}`);
|
|
1144
|
+
expect(result.queued).toBe(true);
|
|
1145
|
+
}
|
|
1146
|
+
expect(session.getQueueDepth()).toBe(MAX_QUEUE_DEPTH);
|
|
1147
|
+
|
|
1148
|
+
// Register a pending surface action so handleSurfaceAction doesn't bail early
|
|
1149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- access private property for testing
|
|
1150
|
+
(session as any).pendingSurfaceActions.set('surf-1', { surfaceType: 'confirmation' });
|
|
1151
|
+
|
|
1152
|
+
// Trigger the surface action — queue is full, should be rejected
|
|
1153
|
+
session.handleSurfaceAction('surf-1', 'confirm');
|
|
1154
|
+
|
|
1155
|
+
// Should have a request_received trace followed by a request_error trace
|
|
1156
|
+
const receivedTrace = traceEvents.find(
|
|
1157
|
+
(e) => 'kind' in e && e.kind === 'request_received',
|
|
1158
|
+
);
|
|
1159
|
+
expect(receivedTrace).toBeDefined();
|
|
1160
|
+
|
|
1161
|
+
const errorTrace = traceEvents.find(
|
|
1162
|
+
(e) => 'kind' in e && e.kind === 'request_error',
|
|
1163
|
+
);
|
|
1164
|
+
expect(errorTrace).toBeDefined();
|
|
1165
|
+
expect(errorTrace).toHaveProperty('attributes');
|
|
1166
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- access trace attributes
|
|
1167
|
+
const attrs = (errorTrace as any).attributes;
|
|
1168
|
+
expect(attrs.reason).toBe('queue_full');
|
|
1169
|
+
expect(attrs.source).toBe('surface_action');
|
|
1170
|
+
|
|
1171
|
+
// Queue depth should not have increased
|
|
1172
|
+
expect(session.getQueueDepth()).toBe(MAX_QUEUE_DEPTH);
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// ---------------------------------------------------------------------------
|
|
1177
|
+
// Host attachment approval tests
|
|
1178
|
+
// ---------------------------------------------------------------------------
|
|
1179
|
+
|
|
1180
|
+
describe('Session host attachment directives', () => {
|
|
1181
|
+
beforeEach(() => {
|
|
1182
|
+
pendingRuns = [];
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test('host attachment prompts and resolves when user allows', async () => {
|
|
1186
|
+
const hostPath = '/tmp/vellum-host-attachment-allow.txt';
|
|
1187
|
+
writeFileSync(hostPath, 'host attachment content');
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
const clientEvents: ServerMessage[] = [];
|
|
1191
|
+
const events: ServerMessage[] = [];
|
|
1192
|
+
const session = makeSession((msg) => clientEvents.push(msg));
|
|
1193
|
+
await session.loadFromDb();
|
|
1194
|
+
|
|
1195
|
+
const p1 = session.processMessage('msg-1', [], (e) => events.push(e), 'req-1');
|
|
1196
|
+
await waitForPendingRun(1);
|
|
1197
|
+
|
|
1198
|
+
const run = pendingRuns[0];
|
|
1199
|
+
const assistantMsg: Message = {
|
|
1200
|
+
role: 'assistant',
|
|
1201
|
+
content: [
|
|
1202
|
+
{
|
|
1203
|
+
type: 'text',
|
|
1204
|
+
text: `Here is your file.\n<vellum-attachment source="host" path="${hostPath}" />`,
|
|
1205
|
+
},
|
|
1206
|
+
],
|
|
1207
|
+
};
|
|
1208
|
+
run.onEvent({ type: 'usage', inputTokens: 10, outputTokens: 5, model: 'mock', providerDurationMs: 100 });
|
|
1209
|
+
run.onEvent({ type: 'message_complete', message: assistantMsg });
|
|
1210
|
+
run.resolve([...run.messages, assistantMsg]);
|
|
1211
|
+
|
|
1212
|
+
await waitForCondition(() => clientEvents.some((e) => e.type === 'confirmation_request'));
|
|
1213
|
+
const confirmation = clientEvents.find((e) => e.type === 'confirmation_request');
|
|
1214
|
+
expect(confirmation).toBeDefined();
|
|
1215
|
+
session.handleConfirmationResponse((confirmation as { requestId: string }).requestId, 'allow');
|
|
1216
|
+
|
|
1217
|
+
await p1;
|
|
1218
|
+
|
|
1219
|
+
expect(session.lastAssistantAttachments).toHaveLength(1);
|
|
1220
|
+
expect(session.lastAssistantAttachments[0].sourceType).toBe('host_file');
|
|
1221
|
+
expect(session.lastAttachmentWarnings).toHaveLength(0);
|
|
1222
|
+
|
|
1223
|
+
const completion = events.find((e) => e.type === 'message_complete');
|
|
1224
|
+
expect(completion).toBeDefined();
|
|
1225
|
+
} finally {
|
|
1226
|
+
rmSync(hostPath, { force: true });
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
test('host attachment denial is non-fatal and emits warning text', async () => {
|
|
1231
|
+
const hostPath = '/tmp/vellum-host-attachment-deny.txt';
|
|
1232
|
+
writeFileSync(hostPath, 'host attachment content');
|
|
1233
|
+
|
|
1234
|
+
try {
|
|
1235
|
+
const clientEvents: ServerMessage[] = [];
|
|
1236
|
+
const events: ServerMessage[] = [];
|
|
1237
|
+
const session = makeSession((msg) => clientEvents.push(msg));
|
|
1238
|
+
await session.loadFromDb();
|
|
1239
|
+
|
|
1240
|
+
const p1 = session.processMessage('msg-1', [], (e) => events.push(e), 'req-1');
|
|
1241
|
+
await waitForPendingRun(1);
|
|
1242
|
+
|
|
1243
|
+
const run = pendingRuns[0];
|
|
1244
|
+
const assistantMsg: Message = {
|
|
1245
|
+
role: 'assistant',
|
|
1246
|
+
content: [
|
|
1247
|
+
{
|
|
1248
|
+
type: 'text',
|
|
1249
|
+
text: `Here is your file.\n<vellum-attachment source="host" path="${hostPath}" />`,
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
};
|
|
1253
|
+
run.onEvent({ type: 'usage', inputTokens: 10, outputTokens: 5, model: 'mock', providerDurationMs: 100 });
|
|
1254
|
+
run.onEvent({ type: 'message_complete', message: assistantMsg });
|
|
1255
|
+
run.resolve([...run.messages, assistantMsg]);
|
|
1256
|
+
|
|
1257
|
+
await waitForCondition(() => clientEvents.some((e) => e.type === 'confirmation_request'));
|
|
1258
|
+
const confirmation = clientEvents.find((e) => e.type === 'confirmation_request');
|
|
1259
|
+
expect(confirmation).toBeDefined();
|
|
1260
|
+
session.handleConfirmationResponse((confirmation as { requestId: string }).requestId, 'deny');
|
|
1261
|
+
|
|
1262
|
+
await p1;
|
|
1263
|
+
|
|
1264
|
+
expect(session.lastAssistantAttachments).toHaveLength(0);
|
|
1265
|
+
expect(session.lastAttachmentWarnings.some((w) => w.includes('access denied by user'))).toBe(true);
|
|
1266
|
+
|
|
1267
|
+
const warningDelta = events.find(
|
|
1268
|
+
(e) => e.type === 'assistant_text_delta' && e.text.includes('Attachment warning:'),
|
|
1269
|
+
);
|
|
1270
|
+
expect(warningDelta).toBeDefined();
|
|
1271
|
+
const completion = events.find((e) => e.type === 'message_complete');
|
|
1272
|
+
expect(completion).toBeDefined();
|
|
1273
|
+
} finally {
|
|
1274
|
+
rmSync(hostPath, { force: true });
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
// ---------------------------------------------------------------------------
|
|
1280
|
+
// Attachment payload emission tests
|
|
1281
|
+
// ---------------------------------------------------------------------------
|
|
1282
|
+
|
|
1283
|
+
describe('Session attachment event payloads', () => {
|
|
1284
|
+
beforeEach(() => {
|
|
1285
|
+
pendingRuns = [];
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
test('message_complete includes assistant attachments', async () => {
|
|
1289
|
+
const events: ServerMessage[] = [];
|
|
1290
|
+
const session = makeSession();
|
|
1291
|
+
await session.loadFromDb();
|
|
1292
|
+
|
|
1293
|
+
const p1 = session.processMessage('msg-1', [], (e) => events.push(e), 'req-1');
|
|
1294
|
+
await waitForPendingRun(1);
|
|
1295
|
+
|
|
1296
|
+
const run = pendingRuns[0];
|
|
1297
|
+
const assistantMsg: Message = {
|
|
1298
|
+
role: 'assistant',
|
|
1299
|
+
content: [{ type: 'text', text: 'Here is your chart.' }],
|
|
1300
|
+
};
|
|
1301
|
+
run.onEvent({
|
|
1302
|
+
type: 'tool_result',
|
|
1303
|
+
toolUseId: 'tool-1',
|
|
1304
|
+
content: 'ok',
|
|
1305
|
+
isError: false,
|
|
1306
|
+
contentBlocks: [
|
|
1307
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock content block
|
|
1308
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'iVBORw0K' } } as any,
|
|
1309
|
+
],
|
|
1310
|
+
});
|
|
1311
|
+
run.onEvent({ type: 'usage', inputTokens: 10, outputTokens: 5, model: 'mock', providerDurationMs: 100 });
|
|
1312
|
+
run.onEvent({ type: 'message_complete', message: assistantMsg });
|
|
1313
|
+
run.resolve([...run.messages, assistantMsg]);
|
|
1314
|
+
|
|
1315
|
+
await p1;
|
|
1316
|
+
|
|
1317
|
+
const completion = events.find((e) => e.type === 'message_complete' && Array.isArray(e.attachments));
|
|
1318
|
+
expect(completion).toBeDefined();
|
|
1319
|
+
const attachments = (completion as { attachments: Array<{ mimeType: string; data: string; id?: string }> }).attachments;
|
|
1320
|
+
expect(attachments).toHaveLength(1);
|
|
1321
|
+
expect(attachments[0].mimeType).toBe('image/png');
|
|
1322
|
+
expect(attachments[0].data).toBe('iVBORw0K');
|
|
1323
|
+
expect(attachments[0].id).toBeDefined();
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
test('generation_handoff includes assistant attachments', async () => {
|
|
1327
|
+
const events1: ServerMessage[] = [];
|
|
1328
|
+
const session = makeSession();
|
|
1329
|
+
await session.loadFromDb();
|
|
1330
|
+
|
|
1331
|
+
const p1 = session.processMessage('msg-1', [], (e) => events1.push(e), 'req-1');
|
|
1332
|
+
await waitForPendingRun(1);
|
|
1333
|
+
|
|
1334
|
+
// Queue a second message so the first run yields via checkpoint handoff.
|
|
1335
|
+
session.enqueueMessage('msg-2', [], () => {}, 'req-2');
|
|
1336
|
+
|
|
1337
|
+
const run = pendingRuns[0];
|
|
1338
|
+
expect(run.onCheckpoint).toBeDefined();
|
|
1339
|
+
expect(run.onCheckpoint!({ turnIndex: 0, toolCount: 1, hasToolUse: true })).toBe('yield');
|
|
1340
|
+
|
|
1341
|
+
const assistantMsg: Message = {
|
|
1342
|
+
role: 'assistant',
|
|
1343
|
+
content: [{ type: 'text', text: 'Handing off with attachment.' }],
|
|
1344
|
+
};
|
|
1345
|
+
run.onEvent({
|
|
1346
|
+
type: 'tool_result',
|
|
1347
|
+
toolUseId: 'tool-1',
|
|
1348
|
+
content: 'ok',
|
|
1349
|
+
isError: false,
|
|
1350
|
+
contentBlocks: [
|
|
1351
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock content block
|
|
1352
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'iVBORw0K' } } as any,
|
|
1353
|
+
],
|
|
1354
|
+
});
|
|
1355
|
+
run.onEvent({ type: 'usage', inputTokens: 10, outputTokens: 5, model: 'mock', providerDurationMs: 100 });
|
|
1356
|
+
run.onEvent({ type: 'message_complete', message: assistantMsg });
|
|
1357
|
+
run.resolve([...run.messages, assistantMsg]);
|
|
1358
|
+
|
|
1359
|
+
await p1;
|
|
1360
|
+
|
|
1361
|
+
const handoff = events1.find((e) => e.type === 'generation_handoff' && Array.isArray(e.attachments));
|
|
1362
|
+
expect(handoff).toBeDefined();
|
|
1363
|
+
const attachments = (handoff as { attachments: Array<{ mimeType: string; data: string; id?: string }> }).attachments;
|
|
1364
|
+
expect(attachments).toHaveLength(1);
|
|
1365
|
+
expect(attachments[0].mimeType).toBe('image/png');
|
|
1366
|
+
expect(attachments[0].data).toBe('iVBORw0K');
|
|
1367
|
+
|
|
1368
|
+
await waitForPendingRun(2);
|
|
1369
|
+
resolveRun(1);
|
|
1370
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
// ---------------------------------------------------------------------------
|
|
1375
|
+
// Regression: cancel semantics + session/global error channel split
|
|
1376
|
+
// ---------------------------------------------------------------------------
|
|
1377
|
+
|
|
1378
|
+
describe('Regression: cancel semantics and error channel split', () => {
|
|
1379
|
+
beforeEach(() => {
|
|
1380
|
+
pendingRuns = [];
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
test('user cancellation emits generation_cancelled, never session_error', async () => {
|
|
1384
|
+
const msgEvents: ServerMessage[] = [];
|
|
1385
|
+
const session = makeSession();
|
|
1386
|
+
await session.loadFromDb();
|
|
1387
|
+
|
|
1388
|
+
// Start processing a message — collect events from the per-message callback
|
|
1389
|
+
const p1 = session.processMessage('msg-1', [], (e) => msgEvents.push(e), 'req-1');
|
|
1390
|
+
await waitForPendingRun(1);
|
|
1391
|
+
|
|
1392
|
+
// User cancels — sets the abort signal
|
|
1393
|
+
session.abort();
|
|
1394
|
+
|
|
1395
|
+
// Resolve the pending run so the abort-check path fires
|
|
1396
|
+
resolveRun(0);
|
|
1397
|
+
await p1;
|
|
1398
|
+
|
|
1399
|
+
// generation_cancelled should be emitted via the per-message callback
|
|
1400
|
+
const cancelEvent = msgEvents.find((e) => e.type === 'generation_cancelled');
|
|
1401
|
+
expect(cancelEvent).toBeDefined();
|
|
1402
|
+
|
|
1403
|
+
// session_error must never appear on cancel
|
|
1404
|
+
const sessionErr = msgEvents.find((e) => e.type === 'session_error');
|
|
1405
|
+
expect(sessionErr).toBeUndefined();
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
test('post-processing failure still attempts turn-boundary commit', async () => {
|
|
1409
|
+
const events: ServerMessage[] = [];
|
|
1410
|
+
const session = makeSession();
|
|
1411
|
+
await session.loadFromDb();
|
|
1412
|
+
linkAttachmentShouldThrow = true;
|
|
1413
|
+
|
|
1414
|
+
const p1 = session.processMessage('msg-1', [], (e) => events.push(e), 'req-1');
|
|
1415
|
+
await waitForPendingRun(1);
|
|
1416
|
+
const run = pendingRuns[0];
|
|
1417
|
+
const assistantMsg: Message = {
|
|
1418
|
+
role: 'assistant',
|
|
1419
|
+
content: [{ type: 'text', text: 'attachment-trigger' }],
|
|
1420
|
+
};
|
|
1421
|
+
run.onEvent({
|
|
1422
|
+
type: 'tool_result',
|
|
1423
|
+
toolUseId: 'tool-1',
|
|
1424
|
+
content: 'ok',
|
|
1425
|
+
isError: false,
|
|
1426
|
+
contentBlocks: [
|
|
1427
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock content block
|
|
1428
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'iVBORw0K' } } as any,
|
|
1429
|
+
],
|
|
1430
|
+
});
|
|
1431
|
+
run.onEvent({ type: 'usage', inputTokens: 10, outputTokens: 5, model: 'mock', providerDurationMs: 100 });
|
|
1432
|
+
run.onEvent({ type: 'message_complete', message: assistantMsg });
|
|
1433
|
+
run.resolve([...run.messages, assistantMsg]);
|
|
1434
|
+
await p1;
|
|
1435
|
+
|
|
1436
|
+
expect(turnCommitCalls).toHaveLength(1);
|
|
1437
|
+
expect(turnCommitCalls[0]).toEqual({
|
|
1438
|
+
workspaceDir: '/tmp',
|
|
1439
|
+
sessionId: 'conv-1',
|
|
1440
|
+
turnNumber: 1,
|
|
1441
|
+
});
|
|
1442
|
+
const err = events.find((e) => e.type === 'error');
|
|
1443
|
+
expect(err).toBeDefined();
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
test('provider failure during processing emits both session_error and generic error', async () => {
|
|
1447
|
+
const allEvents: ServerMessage[] = [];
|
|
1448
|
+
const session = makeSession();
|
|
1449
|
+
await session.loadFromDb();
|
|
1450
|
+
|
|
1451
|
+
const p1 = session.processMessage('msg-1', [], (e) => allEvents.push(e), 'req-1');
|
|
1452
|
+
await waitForPendingRun(1);
|
|
1453
|
+
|
|
1454
|
+
// Simulate a provider failure
|
|
1455
|
+
pendingRuns[0].reject(new Error('Connection refused'));
|
|
1456
|
+
await p1;
|
|
1457
|
+
|
|
1458
|
+
// Should get session_error (structured)
|
|
1459
|
+
const sessionErr = allEvents.find((e) => e.type === 'session_error');
|
|
1460
|
+
expect(sessionErr).toBeDefined();
|
|
1461
|
+
|
|
1462
|
+
// Should also get generic error for backward compatibility
|
|
1463
|
+
const genericErr = allEvents.find((e) => e.type === 'error');
|
|
1464
|
+
expect(genericErr).toBeDefined();
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
test('cancel after queued messages produces no session_error for any queued entry', async () => {
|
|
1468
|
+
const session = makeSession();
|
|
1469
|
+
await session.loadFromDb();
|
|
1470
|
+
|
|
1471
|
+
const eventsPerMsg: ServerMessage[][] = [[], [], []];
|
|
1472
|
+
|
|
1473
|
+
session.processMessage('msg-1', [], (e) => eventsPerMsg[0].push(e), 'req-1');
|
|
1474
|
+
await waitForPendingRun(1);
|
|
1475
|
+
|
|
1476
|
+
session.enqueueMessage('msg-2', [], (e) => eventsPerMsg[1].push(e), 'req-2');
|
|
1477
|
+
session.enqueueMessage('msg-3', [], (e) => eventsPerMsg[2].push(e), 'req-3');
|
|
1478
|
+
|
|
1479
|
+
session.abort();
|
|
1480
|
+
|
|
1481
|
+
// No queued message should have received session_error
|
|
1482
|
+
for (const events of eventsPerMsg) {
|
|
1483
|
+
const sessionErr = events.find((e) => e.type === 'session_error');
|
|
1484
|
+
expect(sessionErr).toBeUndefined();
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
test('commitTurnChanges never resolving within budget -> turn still completes and drains queue', async () => {
|
|
1489
|
+
const session = makeSession();
|
|
1490
|
+
await session.loadFromDb();
|
|
1491
|
+
|
|
1492
|
+
turnCommitHangForever = true;
|
|
1493
|
+
|
|
1494
|
+
try {
|
|
1495
|
+
const events1: ServerMessage[] = [];
|
|
1496
|
+
const events2: ServerMessage[] = [];
|
|
1497
|
+
|
|
1498
|
+
// Start first message (promise intentionally not awaited — we test queue drain behavior)
|
|
1499
|
+
const _p1 = session.processMessage('msg-1', [], (e) => events1.push(e), 'req-1');
|
|
1500
|
+
await waitForPendingRun(1);
|
|
1501
|
+
|
|
1502
|
+
// Enqueue a second message while the first is processing
|
|
1503
|
+
session.enqueueMessage('msg-2', [], (e) => events2.push(e), 'req-2');
|
|
1504
|
+
|
|
1505
|
+
// Complete the first agent loop run
|
|
1506
|
+
resolveRun(0);
|
|
1507
|
+
|
|
1508
|
+
// The turn should still complete (timeout fires) and drain the queue
|
|
1509
|
+
// even though commitTurnChanges never resolves.
|
|
1510
|
+
// The default turnCommitMaxWaitMs is 4000ms in the config mock,
|
|
1511
|
+
// but the mock config doesn't set it, so it defaults to 4000ms.
|
|
1512
|
+
// We wait for the second run to be registered, which proves the
|
|
1513
|
+
// turn completed and the queue drained despite the hanging commit.
|
|
1514
|
+
await waitForPendingRun(2, 10_000);
|
|
1515
|
+
|
|
1516
|
+
// First message should have completed
|
|
1517
|
+
const completion1 = events1.find((e) => e.type === 'message_complete');
|
|
1518
|
+
expect(completion1).toBeDefined();
|
|
1519
|
+
|
|
1520
|
+
// Second message should have been dequeued
|
|
1521
|
+
const dequeued = events2.find((e) => e.type === 'message_dequeued');
|
|
1522
|
+
expect(dequeued).toBeDefined();
|
|
1523
|
+
|
|
1524
|
+
// The turn commit should have been called
|
|
1525
|
+
expect(turnCommitCalls).toHaveLength(1);
|
|
1526
|
+
|
|
1527
|
+
// Complete the second run so the test can clean up
|
|
1528
|
+
turnCommitHangForever = false;
|
|
1529
|
+
resolveRun(1);
|
|
1530
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1531
|
+
} finally {
|
|
1532
|
+
turnCommitHangForever = false;
|
|
1533
|
+
}
|
|
1534
|
+
}, 15_000);
|
|
1535
|
+
});
|