@vellumai/assistant 0.8.8 → 0.8.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
|
@@ -28,6 +28,20 @@ let mockUiConfig: { userTimezone?: string; detectedTimezone?: string } = {};
|
|
|
28
28
|
|
|
29
29
|
// ── Module mocks (must precede imports of the module under test) ─────
|
|
30
30
|
|
|
31
|
+
// The real AgentLoop resolves the per-conversation ContextWindowManager from
|
|
32
|
+
// the compaction store keyed by conversationId. These orchestrator tests build
|
|
33
|
+
// fake conversations whose manager is a canned stub, so register each stub in a
|
|
34
|
+
// map the mocked store reads from.
|
|
35
|
+
const fakeContextWindowManagers = new Map<string, unknown>();
|
|
36
|
+
mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
|
|
37
|
+
createContextWindowManager: () => undefined,
|
|
38
|
+
getContextWindowManager: (conversationId: string) =>
|
|
39
|
+
fakeContextWindowManagers.get(conversationId),
|
|
40
|
+
disposeContextWindowManager: (conversationId: string) => {
|
|
41
|
+
fakeContextWindowManagers.delete(conversationId);
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
|
|
31
45
|
mock.module("../util/logger.js", () => ({
|
|
32
46
|
getLogger: () =>
|
|
33
47
|
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
@@ -100,35 +114,38 @@ mock.module("../context/token-estimator.js", () => ({
|
|
|
100
114
|
let mockReducerStepFn:
|
|
101
115
|
| ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
|
|
102
116
|
| null = null;
|
|
103
|
-
mock.module(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
mock.module(
|
|
118
|
+
"../plugins/defaults/compaction/context-overflow-reducer.js",
|
|
119
|
+
() => ({
|
|
120
|
+
createInitialReducerState: () => ({
|
|
121
|
+
appliedTiers: [],
|
|
122
|
+
injectionMode: "full" as const,
|
|
123
|
+
exhausted: false,
|
|
124
|
+
}),
|
|
125
|
+
reduceContextOverflow: async (
|
|
126
|
+
msgs: Message[],
|
|
127
|
+
cfg: unknown,
|
|
128
|
+
state: unknown,
|
|
129
|
+
) => {
|
|
130
|
+
if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
|
|
131
|
+
return {
|
|
132
|
+
messages: msgs,
|
|
133
|
+
tier: "forced_compaction",
|
|
134
|
+
state: {
|
|
135
|
+
appliedTiers: [
|
|
136
|
+
"forced_compaction",
|
|
137
|
+
"tool_result_truncation",
|
|
138
|
+
"media_stubbing",
|
|
139
|
+
"injection_downgrade",
|
|
140
|
+
],
|
|
141
|
+
injectionMode: "full",
|
|
142
|
+
exhausted: true,
|
|
143
|
+
},
|
|
144
|
+
estimatedTokens: 1000,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
108
147
|
}),
|
|
109
|
-
|
|
110
|
-
msgs: Message[],
|
|
111
|
-
cfg: unknown,
|
|
112
|
-
state: unknown,
|
|
113
|
-
) => {
|
|
114
|
-
if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
|
|
115
|
-
return {
|
|
116
|
-
messages: msgs,
|
|
117
|
-
tier: "forced_compaction",
|
|
118
|
-
state: {
|
|
119
|
-
appliedTiers: [
|
|
120
|
-
"forced_compaction",
|
|
121
|
-
"tool_result_truncation",
|
|
122
|
-
"media_stubbing",
|
|
123
|
-
"injection_downgrade",
|
|
124
|
-
],
|
|
125
|
-
injectionMode: "full",
|
|
126
|
-
exhausted: true,
|
|
127
|
-
},
|
|
128
|
-
estimatedTokens: 1000,
|
|
129
|
-
};
|
|
130
|
-
},
|
|
131
|
-
}));
|
|
148
|
+
);
|
|
132
149
|
|
|
133
150
|
// Policy: default to fail_gracefully
|
|
134
151
|
let mockOverflowAction: string = "fail_gracefully";
|
|
@@ -312,16 +329,14 @@ let mockInjectionBlocks: {
|
|
|
312
329
|
pkbSystemReminder?: string;
|
|
313
330
|
unifiedTurnContext?: string;
|
|
314
331
|
} = {};
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}),
|
|
324
|
-
);
|
|
332
|
+
const defaultApplyRuntimeInjectionsImpl = async (
|
|
333
|
+
msgs: Message[],
|
|
334
|
+
_options?: unknown,
|
|
335
|
+
) => ({
|
|
336
|
+
messages: msgs,
|
|
337
|
+
blocks: { ...mockInjectionBlocks },
|
|
338
|
+
});
|
|
339
|
+
const applyRuntimeInjectionsMock = mock(defaultApplyRuntimeInjectionsImpl);
|
|
325
340
|
let mockSlackChronologicalContext: {
|
|
326
341
|
renderedMessages: Array<{
|
|
327
342
|
message: Message;
|
|
@@ -358,7 +373,6 @@ const getSlackCompactionWatermarkForPrefixMock = mock(
|
|
|
358
373
|
);
|
|
359
374
|
mock.module("../daemon/conversation-runtime-assembly.js", () => ({
|
|
360
375
|
applyRuntimeInjections: applyRuntimeInjectionsMock,
|
|
361
|
-
buildUnifiedTurnContextBlock: buildUnifiedTurnContextBlockMock,
|
|
362
376
|
stripInjectionsForCompaction: (msgs: Message[]) => msgs,
|
|
363
377
|
isSlackChannelConversation: () => false,
|
|
364
378
|
getSlackCompactionWatermarkForPrefix:
|
|
@@ -523,22 +537,11 @@ mock.module("../memory/llm-request-log-store.js", () => ({
|
|
|
523
537
|
setAgentLoopExitReasonOnLatestLog: setAgentLoopExitReasonOnLatestLogMock,
|
|
524
538
|
}));
|
|
525
539
|
|
|
526
|
-
let mockHasProactiveArtifactCompleted = true;
|
|
527
|
-
let mockTryClaimProactiveArtifactTrigger = false;
|
|
528
|
-
const runProactiveArtifactJobMock = mock(
|
|
529
|
-
async (_params: Record<string, unknown>) => {},
|
|
530
|
-
);
|
|
531
|
-
mock.module("../proactive-artifact/index.js", () => ({
|
|
532
|
-
hasProactiveArtifactCompleted: () => mockHasProactiveArtifactCompleted,
|
|
533
|
-
tryClaimProactiveArtifactTrigger: () => mockTryClaimProactiveArtifactTrigger,
|
|
534
|
-
runProactiveArtifactJob: runProactiveArtifactJobMock,
|
|
535
|
-
}));
|
|
536
|
-
|
|
537
540
|
// ── Imports (after mocks) ────────────────────────────────────────────
|
|
538
541
|
|
|
539
542
|
import { AgentLoop } from "../agent/loop.js";
|
|
543
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
540
544
|
import {
|
|
541
|
-
type AgentLoopConversationContext,
|
|
542
545
|
applyCompactionResult,
|
|
543
546
|
runAgentLoopImpl,
|
|
544
547
|
} from "../daemon/conversation-agent-loop.js";
|
|
@@ -552,13 +555,13 @@ import {
|
|
|
552
555
|
// ── Test helpers ─────────────────────────────────────────────────────
|
|
553
556
|
|
|
554
557
|
function makeCtx(
|
|
555
|
-
overrides?: Partial<
|
|
558
|
+
overrides?: Partial<Conversation> & {
|
|
556
559
|
providerResponses?: ScriptedResponse[];
|
|
557
560
|
loopProvider?: Provider;
|
|
558
561
|
loopTools?: ToolDefinition[];
|
|
559
562
|
toolExecutor?: LoopToolExecutor;
|
|
560
563
|
},
|
|
561
|
-
):
|
|
564
|
+
): Conversation {
|
|
562
565
|
const {
|
|
563
566
|
providerResponses,
|
|
564
567
|
loopProvider,
|
|
@@ -589,13 +592,15 @@ function makeCtx(
|
|
|
589
592
|
providerResponses ?? [textResponse("response")],
|
|
590
593
|
loopProviderName,
|
|
591
594
|
).provider;
|
|
592
|
-
const agentLoop = new AgentLoop(
|
|
595
|
+
const agentLoop = new AgentLoop({
|
|
596
|
+
provider: provider,
|
|
597
|
+
systemPrompt: "system prompt",
|
|
593
598
|
conversationId,
|
|
594
599
|
tools: loopTools ?? [],
|
|
595
600
|
toolExecutor,
|
|
596
601
|
});
|
|
597
602
|
|
|
598
|
-
|
|
603
|
+
const ctx = {
|
|
599
604
|
conversationId: "test-conv",
|
|
600
605
|
messages: [
|
|
601
606
|
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
@@ -616,15 +621,24 @@ function makeCtx(
|
|
|
616
621
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
617
622
|
stopReason: "end_turn",
|
|
618
623
|
}),
|
|
619
|
-
} as unknown as
|
|
624
|
+
} as unknown as Conversation["provider"],
|
|
620
625
|
systemPrompt: "system prompt",
|
|
621
626
|
|
|
622
627
|
contextWindowManager: {
|
|
628
|
+
updateConfig: () => {},
|
|
623
629
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
624
630
|
maybeCompact: async () => ({ compacted: false }),
|
|
625
|
-
} as unknown as
|
|
626
|
-
|
|
627
|
-
|
|
631
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
632
|
+
conversationType: mockConversationRow?.conversationType ?? undefined,
|
|
633
|
+
source: mockConversationRow?.source ?? undefined,
|
|
634
|
+
contextSummary: mockConversationRow?.contextSummary ?? null,
|
|
635
|
+
contextCompactedMessageCount:
|
|
636
|
+
mockConversationRow?.contextCompactedMessageCount ?? 0,
|
|
637
|
+
contextCompactedAt: mockConversationRow?.contextCompactedAt ?? null,
|
|
638
|
+
slackContextCompactionWatermarkTs:
|
|
639
|
+
mockConversationRow?.slackContextCompactionWatermarkTs ?? null,
|
|
640
|
+
lastNotifiedInferenceProfile:
|
|
641
|
+
mockConversationRow?.lastNotifiedInferenceProfile ?? null,
|
|
628
642
|
|
|
629
643
|
memoryPolicy: { scopeId: "default", includeDefaultFallback: true },
|
|
630
644
|
|
|
@@ -636,8 +650,6 @@ function makeCtx(
|
|
|
636
650
|
currentTurnSurfaces: [],
|
|
637
651
|
|
|
638
652
|
workingDir: "/tmp",
|
|
639
|
-
workspaceTopLevelContext: null,
|
|
640
|
-
workspaceTopLevelDirty: false,
|
|
641
653
|
channelCapabilities: undefined,
|
|
642
654
|
commandIntent: undefined,
|
|
643
655
|
trustContext: undefined,
|
|
@@ -647,15 +659,15 @@ function makeCtx(
|
|
|
647
659
|
preactivatedSkillIds: undefined,
|
|
648
660
|
skillProjectionState: new Map(),
|
|
649
661
|
skillProjectionCache:
|
|
650
|
-
new Map() as unknown as
|
|
662
|
+
new Map() as unknown as Conversation["skillProjectionCache"],
|
|
651
663
|
|
|
652
664
|
traceEmitter: {
|
|
653
665
|
emit: () => {},
|
|
654
|
-
} as unknown as
|
|
666
|
+
} as unknown as Conversation["traceEmitter"],
|
|
655
667
|
profiler: {
|
|
656
668
|
startRequest: () => {},
|
|
657
669
|
emitSummary: () => {},
|
|
658
|
-
} as unknown as
|
|
670
|
+
} as unknown as Conversation["profiler"],
|
|
659
671
|
usageStats: {
|
|
660
672
|
totalInputTokens: 0,
|
|
661
673
|
totalOutputTokens: 0,
|
|
@@ -668,13 +680,12 @@ function makeCtx(
|
|
|
668
680
|
lastAttachmentWarnings: [],
|
|
669
681
|
|
|
670
682
|
hasNoClient: false,
|
|
671
|
-
prompter: {} as unknown as
|
|
672
|
-
queue: {} as unknown as
|
|
683
|
+
prompter: {} as unknown as Conversation["prompter"],
|
|
684
|
+
queue: {} as unknown as Conversation["queue"],
|
|
673
685
|
|
|
674
686
|
getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
|
|
675
687
|
commitTurnChanges: async () => {},
|
|
676
688
|
|
|
677
|
-
refreshWorkspaceTopLevelContextIfNeeded: () => {},
|
|
678
689
|
markWorkspaceTopLevelDirty: () => {},
|
|
679
690
|
emitActivityState: () => {},
|
|
680
691
|
getQueueDepth: () => 0,
|
|
@@ -701,10 +712,34 @@ function makeCtx(
|
|
|
701
712
|
}),
|
|
702
713
|
retrackCachedNodes: () => {},
|
|
703
714
|
recordPkbQueryVectors: () => {},
|
|
704
|
-
} as unknown as
|
|
715
|
+
} as unknown as Conversation["graphMemory"],
|
|
705
716
|
|
|
706
717
|
...ctxOverrides,
|
|
707
|
-
} as
|
|
718
|
+
} as unknown as Conversation;
|
|
719
|
+
fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
|
|
720
|
+
return ctx;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
type CompactionResult = Parameters<typeof applyCompactionResult>[1];
|
|
724
|
+
|
|
725
|
+
function makeCompactionResult(
|
|
726
|
+
overrides?: Partial<CompactionResult>,
|
|
727
|
+
): CompactionResult {
|
|
728
|
+
return {
|
|
729
|
+
messages: [{ role: "user", content: [{ type: "text", text: "summary" }] }],
|
|
730
|
+
compactedPersistedMessages: 4,
|
|
731
|
+
previousEstimatedInputTokens: 12000,
|
|
732
|
+
estimatedInputTokens: 3000,
|
|
733
|
+
maxInputTokens: 100000,
|
|
734
|
+
thresholdTokens: 80000,
|
|
735
|
+
compactedMessages: 4,
|
|
736
|
+
summaryCalls: 1,
|
|
737
|
+
summaryInputTokens: 100,
|
|
738
|
+
summaryOutputTokens: 20,
|
|
739
|
+
summaryModel: "mock-model",
|
|
740
|
+
summaryText: "summary",
|
|
741
|
+
...overrides,
|
|
742
|
+
};
|
|
708
743
|
}
|
|
709
744
|
|
|
710
745
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
@@ -738,13 +773,12 @@ beforeEach(() => {
|
|
|
738
773
|
title: null,
|
|
739
774
|
};
|
|
740
775
|
mockMessageById = null;
|
|
741
|
-
mockHasProactiveArtifactCompleted = true;
|
|
742
|
-
mockTryClaimProactiveArtifactTrigger = false;
|
|
743
|
-
runProactiveArtifactJobMock.mockClear();
|
|
744
776
|
setConversationHistoryStrippedAtMock.mockClear();
|
|
745
777
|
setConversationHistoryStrippedAtMock.mockImplementation(() => {});
|
|
746
778
|
applyRuntimeInjectionsMock.mockClear();
|
|
747
|
-
|
|
779
|
+
applyRuntimeInjectionsMock.mockImplementation(
|
|
780
|
+
defaultApplyRuntimeInjectionsImpl,
|
|
781
|
+
);
|
|
748
782
|
resolveTurnTimezoneContextMock.mockClear();
|
|
749
783
|
formatTurnTimestampMock.mockClear();
|
|
750
784
|
mockSlackChronologicalContext = null;
|
|
@@ -757,11 +791,10 @@ beforeEach(() => {
|
|
|
757
791
|
projectAssistantMessageMock.mockClear();
|
|
758
792
|
publishSyncInvalidationMock.mockClear();
|
|
759
793
|
mockMessageById = null;
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
// hitting the bare terminals.
|
|
794
|
+
// The compaction pipeline runs through the plugin registry; reset and
|
|
795
|
+
// re-register every default so it dispatches to middleware backed by the
|
|
796
|
+
// mocked collaborators these tests install (`syncMessageToDisk`, etc.)
|
|
797
|
+
// instead of hitting the bare terminal.
|
|
765
798
|
resetPluginRegistryAndRegisterDefaults();
|
|
766
799
|
});
|
|
767
800
|
|
|
@@ -785,7 +818,7 @@ describe("session-agent-loop", () => {
|
|
|
785
818
|
});
|
|
786
819
|
});
|
|
787
820
|
|
|
788
|
-
test("
|
|
821
|
+
test("freezes the client timezone snapshot on the conversation, not the options bag", async () => {
|
|
789
822
|
mockUiConfig = {
|
|
790
823
|
userTimezone: "US/Eastern",
|
|
791
824
|
detectedTimezone: "US/Central",
|
|
@@ -802,17 +835,25 @@ describe("session-agent-loop", () => {
|
|
|
802
835
|
|
|
803
836
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
804
837
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
expect(turnContextOptions).toMatchObject({
|
|
812
|
-
configuredUserTimezone: "America/New_York",
|
|
838
|
+
// The turn-start client timezone and the long-absence gap are frozen on
|
|
839
|
+
// the conversation as `currentTurnTemporalSnapshot`; the live
|
|
840
|
+
// `clientTimezone` would otherwise be clobbered mid-turn. `current_time`
|
|
841
|
+
// is not frozen — it is computed fresh at each injection inside
|
|
842
|
+
// `applyRuntimeInjections`.
|
|
843
|
+
expect(ctx.currentTurnTemporalSnapshot).toEqual({
|
|
813
844
|
clientTimezone: "America/Los_Angeles",
|
|
814
|
-
|
|
815
|
-
});
|
|
845
|
+
timeSinceLastMessage: null,
|
|
846
|
+
});
|
|
847
|
+
// Neither the timezones nor the long-absence gap are threaded through the
|
|
848
|
+
// options bag — `applyRuntimeInjections` sources the client timezone and
|
|
849
|
+
// gap from the snapshot and the config timezones from config
|
|
850
|
+
// (`ui.userTimezone`, `ui.detectedTimezone`).
|
|
851
|
+
const injectionOptions = applyRuntimeInjectionsMock.mock.calls[0]?.[1];
|
|
852
|
+
expect(injectionOptions).not.toHaveProperty("timestamp");
|
|
853
|
+
expect(injectionOptions).not.toHaveProperty("clientTimezone");
|
|
854
|
+
expect(injectionOptions).not.toHaveProperty("configuredUserTimezone");
|
|
855
|
+
expect(injectionOptions).not.toHaveProperty("detectedTimezone");
|
|
856
|
+
expect(injectionOptions).not.toHaveProperty("timeSinceLastMessage");
|
|
816
857
|
});
|
|
817
858
|
});
|
|
818
859
|
|
|
@@ -826,61 +867,24 @@ describe("session-agent-loop", () => {
|
|
|
826
867
|
});
|
|
827
868
|
});
|
|
828
869
|
|
|
829
|
-
describe("proactive artifact trigger", () => {
|
|
830
|
-
test("does not start proactive artifact jobs after foreground user turns", async () => {
|
|
831
|
-
mockConversationRow = {
|
|
832
|
-
...mockConversationRow,
|
|
833
|
-
id: "test-conv",
|
|
834
|
-
conversationType: "standard",
|
|
835
|
-
};
|
|
836
|
-
mockMessageById = {
|
|
837
|
-
id: "user-msg-1",
|
|
838
|
-
conversationId: "test-conv",
|
|
839
|
-
createdAt: 1000,
|
|
840
|
-
};
|
|
841
|
-
mockHasProactiveArtifactCompleted = false;
|
|
842
|
-
mockTryClaimProactiveArtifactTrigger = true;
|
|
843
|
-
|
|
844
|
-
// A two-call agent turn: the model invokes `app_create`, then wraps up
|
|
845
|
-
// with a final text reply.
|
|
846
|
-
const ctx = makeCtx({
|
|
847
|
-
conversationId: "test-conv",
|
|
848
|
-
providerResponses: [
|
|
849
|
-
{
|
|
850
|
-
content: [
|
|
851
|
-
{ type: "text", text: "I'll build that app." },
|
|
852
|
-
{
|
|
853
|
-
type: "tool_use",
|
|
854
|
-
id: "tool-1",
|
|
855
|
-
name: "app_create",
|
|
856
|
-
input: { name: "Flow" },
|
|
857
|
-
},
|
|
858
|
-
],
|
|
859
|
-
model: "mock-model",
|
|
860
|
-
usage: { inputTokens: 10, outputTokens: 5 },
|
|
861
|
-
stopReason: "tool_use",
|
|
862
|
-
},
|
|
863
|
-
textResponse("Done."),
|
|
864
|
-
],
|
|
865
|
-
toolExecutor: async () => ({ content: "{}", isError: false }),
|
|
866
|
-
});
|
|
867
|
-
await runAgentLoopImpl(
|
|
868
|
-
ctx,
|
|
869
|
-
"build a kanban app",
|
|
870
|
-
"user-msg-1",
|
|
871
|
-
() => {},
|
|
872
|
-
{
|
|
873
|
-
isUserMessage: true,
|
|
874
|
-
},
|
|
875
|
-
);
|
|
876
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
877
|
-
|
|
878
|
-
expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
|
|
879
|
-
});
|
|
880
|
-
});
|
|
881
|
-
|
|
882
870
|
describe("disk pressure injection context", () => {
|
|
883
|
-
|
|
871
|
+
// The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
|
|
872
|
+
// turn (the disk-pressure-warning injector reads it via the per-conversation
|
|
873
|
+
// registry) and resets it in the turn-end cleanup path. Snapshot the flag at
|
|
874
|
+
// each `applyRuntimeInjections` call so assertions observe its value while
|
|
875
|
+
// injection runs, not the post-turn reset.
|
|
876
|
+
function captureCleanupFlagDuringInjection(ctx: {
|
|
877
|
+
diskPressureCleanupModeActive?: boolean;
|
|
878
|
+
}): () => Array<boolean | undefined> {
|
|
879
|
+
const observed: Array<boolean | undefined> = [];
|
|
880
|
+
applyRuntimeInjectionsMock.mockImplementation(async (msgs: Message[]) => {
|
|
881
|
+
observed.push(ctx.diskPressureCleanupModeActive);
|
|
882
|
+
return { messages: msgs, blocks: { ...mockInjectionBlocks } };
|
|
883
|
+
});
|
|
884
|
+
return () => observed;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
test("sets the cleanup-mode flag on the conversation for cleanup-mode turns", async () => {
|
|
884
888
|
mockDiskPressureDecision = {
|
|
885
889
|
action: "allow-cleanup-mode",
|
|
886
890
|
reason: "guardian",
|
|
@@ -901,8 +905,9 @@ describe("session-agent-loop", () => {
|
|
|
901
905
|
trustContext: {
|
|
902
906
|
sourceChannel: "telegram",
|
|
903
907
|
trustClass: "guardian",
|
|
904
|
-
} as
|
|
908
|
+
} as Conversation["trustContext"],
|
|
905
909
|
});
|
|
910
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
906
911
|
|
|
907
912
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
908
913
|
|
|
@@ -921,21 +926,16 @@ describe("session-agent-loop", () => {
|
|
|
921
926
|
},
|
|
922
927
|
}),
|
|
923
928
|
);
|
|
924
|
-
|
|
925
|
-
.calls[0]![1] as {
|
|
926
|
-
diskPressureContext?: { cleanupModeActive: boolean } | null;
|
|
927
|
-
};
|
|
928
|
-
expect(firstInjectionOptions.diskPressureContext).toEqual({
|
|
929
|
-
cleanupModeActive: true,
|
|
930
|
-
});
|
|
929
|
+
expect(cleanupFlagDuringInjection()).toEqual([true]);
|
|
931
930
|
});
|
|
932
931
|
|
|
933
|
-
test("
|
|
932
|
+
test("sets the cleanup-mode flag on the conversation for local-owner turns", async () => {
|
|
934
933
|
mockDiskPressureDecision = {
|
|
935
934
|
action: "allow-cleanup-mode",
|
|
936
935
|
reason: "local-owner",
|
|
937
936
|
};
|
|
938
937
|
const ctx = makeCtx();
|
|
938
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
939
939
|
|
|
940
940
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
941
941
|
|
|
@@ -947,16 +947,10 @@ describe("session-agent-loop", () => {
|
|
|
947
947
|
trustContext: null,
|
|
948
948
|
}),
|
|
949
949
|
);
|
|
950
|
-
|
|
951
|
-
.calls[0]![1] as {
|
|
952
|
-
diskPressureContext?: { cleanupModeActive: boolean } | null;
|
|
953
|
-
};
|
|
954
|
-
expect(firstInjectionOptions.diskPressureContext).toEqual({
|
|
955
|
-
cleanupModeActive: true,
|
|
956
|
-
});
|
|
950
|
+
expect(cleanupFlagDuringInjection()).toEqual([true]);
|
|
957
951
|
});
|
|
958
952
|
|
|
959
|
-
test("keeps cleanup
|
|
953
|
+
test("keeps the cleanup-mode flag set across overflow recovery reinjection", async () => {
|
|
960
954
|
mockDiskPressureDecision = {
|
|
961
955
|
action: "allow-cleanup-mode",
|
|
962
956
|
reason: "guardian",
|
|
@@ -976,20 +970,16 @@ describe("session-agent-loop", () => {
|
|
|
976
970
|
trustContext: {
|
|
977
971
|
sourceChannel: "telegram",
|
|
978
972
|
trustClass: "guardian",
|
|
979
|
-
} as
|
|
973
|
+
} as Conversation["trustContext"],
|
|
980
974
|
});
|
|
975
|
+
const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
|
|
981
976
|
|
|
982
977
|
await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
|
|
983
978
|
|
|
984
979
|
expect(applyRuntimeInjectionsMock.mock.calls.length).toBeGreaterThan(1);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
};
|
|
989
|
-
expect(options.diskPressureContext).toEqual({
|
|
990
|
-
cleanupModeActive: true,
|
|
991
|
-
});
|
|
992
|
-
}
|
|
980
|
+
const flags = cleanupFlagDuringInjection();
|
|
981
|
+
expect(flags.length).toBeGreaterThan(1);
|
|
982
|
+
expect(flags.every((flag) => flag === true)).toBe(true);
|
|
993
983
|
});
|
|
994
984
|
|
|
995
985
|
test("blocks policy-denied turns before runtime injection or model execution", async () => {
|
|
@@ -1008,7 +998,7 @@ describe("session-agent-loop", () => {
|
|
|
1008
998
|
emit: (...args: unknown[]) => {
|
|
1009
999
|
traceEvents.push(args);
|
|
1010
1000
|
},
|
|
1011
|
-
} as unknown as
|
|
1001
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1012
1002
|
});
|
|
1013
1003
|
const runSpy = spyOn(ctx.agentLoop, "run");
|
|
1014
1004
|
|
|
@@ -1169,7 +1159,7 @@ describe("session-agent-loop", () => {
|
|
|
1169
1159
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1170
1160
|
stopReason: "end_turn",
|
|
1171
1161
|
}),
|
|
1172
|
-
} as unknown as
|
|
1162
|
+
} as unknown as Conversation["provider"],
|
|
1173
1163
|
});
|
|
1174
1164
|
|
|
1175
1165
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1218,7 +1208,7 @@ describe("session-agent-loop", () => {
|
|
|
1218
1208
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1219
1209
|
stopReason: "end_turn",
|
|
1220
1210
|
}),
|
|
1221
|
-
} as unknown as
|
|
1211
|
+
} as unknown as Conversation["provider"],
|
|
1222
1212
|
});
|
|
1223
1213
|
|
|
1224
1214
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1284,7 +1274,7 @@ describe("session-agent-loop", () => {
|
|
|
1284
1274
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1285
1275
|
stopReason: "end_turn",
|
|
1286
1276
|
}),
|
|
1287
|
-
} as unknown as
|
|
1277
|
+
} as unknown as Conversation["provider"],
|
|
1288
1278
|
});
|
|
1289
1279
|
|
|
1290
1280
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1335,7 +1325,7 @@ describe("session-agent-loop", () => {
|
|
|
1335
1325
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1336
1326
|
stopReason: "end_turn",
|
|
1337
1327
|
}),
|
|
1338
|
-
} as unknown as
|
|
1328
|
+
} as unknown as Conversation["provider"],
|
|
1339
1329
|
traceEmitter: {
|
|
1340
1330
|
emit: (
|
|
1341
1331
|
event: string,
|
|
@@ -1346,7 +1336,7 @@ describe("session-agent-loop", () => {
|
|
|
1346
1336
|
traceEvents.push({ label, attrs: payload.attributes ?? {} });
|
|
1347
1337
|
}
|
|
1348
1338
|
},
|
|
1349
|
-
} as unknown as
|
|
1339
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1350
1340
|
});
|
|
1351
1341
|
|
|
1352
1342
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1393,7 +1383,7 @@ describe("session-agent-loop", () => {
|
|
|
1393
1383
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1394
1384
|
stopReason: "end_turn",
|
|
1395
1385
|
}),
|
|
1396
|
-
} as unknown as
|
|
1386
|
+
} as unknown as Conversation["provider"],
|
|
1397
1387
|
traceEmitter: {
|
|
1398
1388
|
emit: (
|
|
1399
1389
|
event: string,
|
|
@@ -1404,7 +1394,7 @@ describe("session-agent-loop", () => {
|
|
|
1404
1394
|
traceEvents.push({ label, attrs: payload.attributes ?? {} });
|
|
1405
1395
|
}
|
|
1406
1396
|
},
|
|
1407
|
-
} as unknown as
|
|
1397
|
+
} as unknown as Conversation["traceEmitter"],
|
|
1408
1398
|
});
|
|
1409
1399
|
|
|
1410
1400
|
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
|
|
@@ -1460,7 +1450,7 @@ describe("session-agent-loop", () => {
|
|
|
1460
1450
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1461
1451
|
stopReason: "end_turn",
|
|
1462
1452
|
}),
|
|
1463
|
-
} as unknown as
|
|
1453
|
+
} as unknown as Conversation["provider"],
|
|
1464
1454
|
});
|
|
1465
1455
|
|
|
1466
1456
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1521,9 +1511,21 @@ describe("session-agent-loop", () => {
|
|
|
1521
1511
|
},
|
|
1522
1512
|
});
|
|
1523
1513
|
|
|
1524
|
-
//
|
|
1525
|
-
// the turn
|
|
1526
|
-
|
|
1514
|
+
// The provider rejects the first call as too large; the convergence
|
|
1515
|
+
// reducer compacts and the rerun completes the turn, forwarding the
|
|
1516
|
+
// compaction's cache-aware usage to recordUsage.
|
|
1517
|
+
const { provider } = createMockProvider([
|
|
1518
|
+
new Error("context_length_exceeded"),
|
|
1519
|
+
textResponse("recovered"),
|
|
1520
|
+
]);
|
|
1521
|
+
const ctx = makeCtx({
|
|
1522
|
+
loopProvider: provider,
|
|
1523
|
+
contextWindowManager: {
|
|
1524
|
+
updateConfig: () => {},
|
|
1525
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1526
|
+
maybeCompact: async () => ({ compacted: false }),
|
|
1527
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1528
|
+
});
|
|
1527
1529
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1528
1530
|
|
|
1529
1531
|
const compactorCall = recordUsageMock.mock.calls.find(
|
|
@@ -1605,9 +1607,10 @@ describe("session-agent-loop", () => {
|
|
|
1605
1607
|
const ctx = makeCtx({
|
|
1606
1608
|
loopProvider: provider,
|
|
1607
1609
|
contextWindowManager: {
|
|
1610
|
+
updateConfig: () => {},
|
|
1608
1611
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1609
1612
|
maybeCompact: async () => ({ compacted: false }),
|
|
1610
|
-
} as unknown as
|
|
1613
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1611
1614
|
});
|
|
1612
1615
|
|
|
1613
1616
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1626,6 +1629,7 @@ describe("session-agent-loop", () => {
|
|
|
1626
1629
|
const ctx = makeCtx({
|
|
1627
1630
|
providerResponses: [new Error("context_length_exceeded")],
|
|
1628
1631
|
contextWindowManager: {
|
|
1632
|
+
updateConfig: () => {},
|
|
1629
1633
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1630
1634
|
// Compaction succeeds but context is still too large
|
|
1631
1635
|
maybeCompact: async () => ({
|
|
@@ -1645,7 +1649,7 @@ describe("session-agent-loop", () => {
|
|
|
1645
1649
|
summaryOutputTokens: 200,
|
|
1646
1650
|
summaryModel: "mock-model",
|
|
1647
1651
|
}),
|
|
1648
|
-
} as unknown as
|
|
1652
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1649
1653
|
});
|
|
1650
1654
|
|
|
1651
1655
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1685,9 +1689,10 @@ describe("session-agent-loop", () => {
|
|
|
1685
1689
|
const ctx = makeCtx({
|
|
1686
1690
|
loopProvider: provider,
|
|
1687
1691
|
contextWindowManager: {
|
|
1692
|
+
updateConfig: () => {},
|
|
1688
1693
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1689
1694
|
maybeCompact: async () => ({ compacted: false }),
|
|
1690
|
-
} as unknown as
|
|
1695
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1691
1696
|
});
|
|
1692
1697
|
|
|
1693
1698
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1734,6 +1739,7 @@ describe("session-agent-loop", () => {
|
|
|
1734
1739
|
],
|
|
1735
1740
|
hasNoClient: true,
|
|
1736
1741
|
contextWindowManager: {
|
|
1742
|
+
updateConfig: () => {},
|
|
1737
1743
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1738
1744
|
maybeCompact: async () => ({
|
|
1739
1745
|
compacted: true,
|
|
@@ -1752,7 +1758,7 @@ describe("session-agent-loop", () => {
|
|
|
1752
1758
|
summaryOutputTokens: 100,
|
|
1753
1759
|
summaryModel: "mock-model",
|
|
1754
1760
|
}),
|
|
1755
|
-
} as unknown as
|
|
1761
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1756
1762
|
});
|
|
1757
1763
|
|
|
1758
1764
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1834,6 +1840,7 @@ describe("session-agent-loop", () => {
|
|
|
1834
1840
|
toolExecutor: async () => ({ content: "data", isError: false }),
|
|
1835
1841
|
hasNoClient: true,
|
|
1836
1842
|
contextWindowManager: {
|
|
1843
|
+
updateConfig: () => {},
|
|
1837
1844
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1838
1845
|
maybeCompact: async () => ({
|
|
1839
1846
|
compacted: true,
|
|
@@ -1852,7 +1859,7 @@ describe("session-agent-loop", () => {
|
|
|
1852
1859
|
summaryOutputTokens: 100,
|
|
1853
1860
|
summaryModel: "mock-model",
|
|
1854
1861
|
}),
|
|
1855
|
-
} as unknown as
|
|
1862
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1856
1863
|
});
|
|
1857
1864
|
|
|
1858
1865
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1919,9 +1926,10 @@ describe("session-agent-loop", () => {
|
|
|
1919
1926
|
const ctx = makeCtx({
|
|
1920
1927
|
providerResponses: [new Error("context_length_exceeded")],
|
|
1921
1928
|
contextWindowManager: {
|
|
1929
|
+
updateConfig: () => {},
|
|
1922
1930
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1923
1931
|
maybeCompact: async () => ({ compacted: false }),
|
|
1924
|
-
} as unknown as
|
|
1932
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
1925
1933
|
});
|
|
1926
1934
|
|
|
1927
1935
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
@@ -1929,48 +1937,6 @@ describe("session-agent-loop", () => {
|
|
|
1929
1937
|
// maxAttempts is 3 — reducer should be called at most 3 times
|
|
1930
1938
|
expect(reducerCalls).toBeLessThanOrEqual(3);
|
|
1931
1939
|
});
|
|
1932
|
-
|
|
1933
|
-
test("preflight budget evaluation invokes reducer before provider call", async () => {
|
|
1934
|
-
const events: ServerMessage[] = [];
|
|
1935
|
-
let reducerCalls = 0;
|
|
1936
|
-
|
|
1937
|
-
// Set token estimate above budget (100000 * 0.95 = 95000)
|
|
1938
|
-
mockEstimateTokens = 96000;
|
|
1939
|
-
|
|
1940
|
-
mockReducerStepFn = (msgs: Message[]) => {
|
|
1941
|
-
reducerCalls++;
|
|
1942
|
-
return {
|
|
1943
|
-
messages: msgs,
|
|
1944
|
-
tier: "forced_compaction",
|
|
1945
|
-
state: {
|
|
1946
|
-
appliedTiers: ["forced_compaction"],
|
|
1947
|
-
injectionMode: "full",
|
|
1948
|
-
exhausted: true,
|
|
1949
|
-
},
|
|
1950
|
-
estimatedTokens: 50000,
|
|
1951
|
-
};
|
|
1952
|
-
};
|
|
1953
|
-
|
|
1954
|
-
// After the preflight reducer brings the estimate under budget, the loop
|
|
1955
|
-
// completes the turn in a single provider call.
|
|
1956
|
-
const { provider, calls } = createMockProvider([textResponse("ok")]);
|
|
1957
|
-
const ctx = makeCtx({
|
|
1958
|
-
loopProvider: provider,
|
|
1959
|
-
contextWindowManager: {
|
|
1960
|
-
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
1961
|
-
maybeCompact: async () => ({ compacted: false }),
|
|
1962
|
-
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
1963
|
-
});
|
|
1964
|
-
|
|
1965
|
-
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1966
|
-
|
|
1967
|
-
// Reducer should have been called during preflight
|
|
1968
|
-
expect(reducerCalls).toBeGreaterThanOrEqual(1);
|
|
1969
|
-
// Agent loop should still succeed in a single provider call
|
|
1970
|
-
expect(calls.length).toBe(1);
|
|
1971
|
-
const complete = events.find((e) => e.type === "message_complete");
|
|
1972
|
-
expect(complete).toBeDefined();
|
|
1973
|
-
});
|
|
1974
1940
|
});
|
|
1975
1941
|
|
|
1976
1942
|
describe("provider ordering error retry", () => {
|
|
@@ -2024,7 +1990,7 @@ describe("session-agent-loop", () => {
|
|
|
2024
1990
|
],
|
|
2025
1991
|
toolExecutor: async () => ({ content: "file content", isError: false }),
|
|
2026
1992
|
canHandoffAtCheckpoint: () => true,
|
|
2027
|
-
} as unknown as Partial<
|
|
1993
|
+
} as unknown as Partial<Conversation>);
|
|
2028
1994
|
|
|
2029
1995
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2030
1996
|
|
|
@@ -2055,7 +2021,7 @@ describe("session-agent-loop", () => {
|
|
|
2055
2021
|
],
|
|
2056
2022
|
toolExecutor: async () => ({ content: "content", isError: false }),
|
|
2057
2023
|
canHandoffAtCheckpoint: () => false,
|
|
2058
|
-
} as unknown as Partial<
|
|
2024
|
+
} as unknown as Partial<Conversation>);
|
|
2059
2025
|
|
|
2060
2026
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2061
2027
|
|
|
@@ -2206,7 +2172,7 @@ describe("session-agent-loop", () => {
|
|
|
2206
2172
|
drainQueue: (reason: string) => {
|
|
2207
2173
|
drainReason = reason;
|
|
2208
2174
|
},
|
|
2209
|
-
} as unknown as Partial<
|
|
2175
|
+
} as unknown as Partial<Conversation>);
|
|
2210
2176
|
|
|
2211
2177
|
// WHEN the orchestrator runs the turn to completion
|
|
2212
2178
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
@@ -2648,9 +2614,10 @@ describe("session-agent-loop", () => {
|
|
|
2648
2614
|
textResponse("retry succeeded"),
|
|
2649
2615
|
],
|
|
2650
2616
|
contextWindowManager: {
|
|
2617
|
+
updateConfig: () => {},
|
|
2651
2618
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
2652
2619
|
maybeCompact: async () => ({ compacted: false }),
|
|
2653
|
-
} as unknown as
|
|
2620
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
2654
2621
|
});
|
|
2655
2622
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2656
2623
|
|
|
@@ -3112,7 +3079,7 @@ describe("session-agent-loop", () => {
|
|
|
3112
3079
|
});
|
|
3113
3080
|
|
|
3114
3081
|
describe("Slack compaction watermarks", () => {
|
|
3115
|
-
test("
|
|
3082
|
+
test("first-call compaction runs in the loop and drops the Slack watermark", async () => {
|
|
3116
3083
|
const renderedSlackMessages: Message[] = [
|
|
3117
3084
|
{
|
|
3118
3085
|
role: "user",
|
|
@@ -3140,9 +3107,12 @@ describe("session-agent-loop", () => {
|
|
|
3140
3107
|
})),
|
|
3141
3108
|
compactableStartIndex: 0,
|
|
3142
3109
|
};
|
|
3143
|
-
const shouldCompactInputs: Message[][] = [];
|
|
3144
3110
|
const maybeCompactInputs: Message[][] = [];
|
|
3145
3111
|
|
|
3112
|
+
// Sits above the loop's first-call gate threshold (~80.75k), so the
|
|
3113
|
+
// loop's first-call gate owns the turn-start compaction.
|
|
3114
|
+
mockEstimateTokens = 90_000;
|
|
3115
|
+
|
|
3146
3116
|
const ctx = makeCtx({
|
|
3147
3117
|
channelCapabilities: {
|
|
3148
3118
|
channel: "slack",
|
|
@@ -3154,18 +3124,19 @@ describe("session-agent-loop", () => {
|
|
|
3154
3124
|
trustContext: {
|
|
3155
3125
|
sourceChannel: "slack",
|
|
3156
3126
|
trustClass: "guardian",
|
|
3157
|
-
} as
|
|
3127
|
+
} as Conversation["trustContext"],
|
|
3158
3128
|
getTurnChannelContext: () => ({
|
|
3159
3129
|
userMessageChannel: "slack" as const,
|
|
3160
3130
|
assistantMessageChannel: "slack" as const,
|
|
3161
3131
|
}),
|
|
3162
3132
|
contextWindowManager: {
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
return { needed: true, estimatedTokens: 95_000 };
|
|
3166
|
-
},
|
|
3133
|
+
updateConfig: () => {},
|
|
3134
|
+
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3167
3135
|
maybeCompact: async (messages: Message[]) => {
|
|
3168
3136
|
maybeCompactInputs.push(messages);
|
|
3137
|
+
// Drop back under budget so the post-compaction provider call
|
|
3138
|
+
// proceeds.
|
|
3139
|
+
mockEstimateTokens = 1000;
|
|
3169
3140
|
return {
|
|
3170
3141
|
compacted: true,
|
|
3171
3142
|
messages: [
|
|
@@ -3173,10 +3144,10 @@ describe("session-agent-loop", () => {
|
|
|
3173
3144
|
role: "user",
|
|
3174
3145
|
content: [{ type: "text", text: "summary" }],
|
|
3175
3146
|
},
|
|
3176
|
-
messages[
|
|
3147
|
+
messages[messages.length - 1]!,
|
|
3177
3148
|
],
|
|
3178
3149
|
compactedPersistedMessages: 2,
|
|
3179
|
-
previousEstimatedInputTokens:
|
|
3150
|
+
previousEstimatedInputTokens: 90_000,
|
|
3180
3151
|
estimatedInputTokens: 5_000,
|
|
3181
3152
|
maxInputTokens: 100_000,
|
|
3182
3153
|
thresholdTokens: 80_000,
|
|
@@ -3189,292 +3160,24 @@ describe("session-agent-loop", () => {
|
|
|
3189
3160
|
summaryFailed: false,
|
|
3190
3161
|
};
|
|
3191
3162
|
},
|
|
3192
|
-
} as unknown as
|
|
3163
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3193
3164
|
});
|
|
3194
3165
|
|
|
3195
|
-
await runAgentLoopImpl(
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
mockSlackChronologicalContext,
|
|
3201
|
-
2,
|
|
3202
|
-
);
|
|
3203
|
-
expect(updateConversationSlackContextWatermarkMock).toHaveBeenCalledWith(
|
|
3204
|
-
"test-conv",
|
|
3205
|
-
"1700000020.000000",
|
|
3206
|
-
expect.any(Number),
|
|
3207
|
-
);
|
|
3208
|
-
const firstInjectionOptions = applyRuntimeInjectionsMock.mock
|
|
3209
|
-
.calls[0]![1] as {
|
|
3210
|
-
slackChronologicalMessages?: Message[] | null;
|
|
3211
|
-
};
|
|
3212
|
-
expect(firstInjectionOptions.slackChronologicalMessages).toBeNull();
|
|
3213
|
-
});
|
|
3214
|
-
|
|
3215
|
-
test("overflow reducer Slack compaction persists watermark from rendered context", async () => {
|
|
3216
|
-
const renderedSlackMessages: Message[] = [
|
|
3217
|
-
{
|
|
3218
|
-
role: "user",
|
|
3219
|
-
content: [{ type: "text", text: "first rendered Slack row" }],
|
|
3220
|
-
},
|
|
3221
|
-
{
|
|
3222
|
-
role: "user",
|
|
3223
|
-
content: [{ type: "text", text: "second rendered Slack row" }],
|
|
3224
|
-
},
|
|
3225
|
-
{
|
|
3226
|
-
role: "user",
|
|
3227
|
-
content: [{ type: "text", text: "retained Slack row" }],
|
|
3228
|
-
},
|
|
3229
|
-
];
|
|
3230
|
-
mockSlackChronologicalContext = {
|
|
3231
|
-
messages: renderedSlackMessages,
|
|
3232
|
-
renderedMessages: renderedSlackMessages.map((message, index) => ({
|
|
3233
|
-
message,
|
|
3234
|
-
sourceChannelTs: [
|
|
3235
|
-
"1700000010.000000",
|
|
3236
|
-
"1700000020.000000",
|
|
3237
|
-
"1700000030.000000",
|
|
3238
|
-
][index]!,
|
|
3239
|
-
tagLineProvenance: "none",
|
|
3240
|
-
})),
|
|
3241
|
-
compactableStartIndex: 0,
|
|
3242
|
-
};
|
|
3243
|
-
mockEstimateTokens = 120_000;
|
|
3244
|
-
mockReducerStepFn = (_msgs: Message[]) => ({
|
|
3245
|
-
messages: [
|
|
3246
|
-
{
|
|
3247
|
-
role: "user",
|
|
3248
|
-
content: [{ type: "text", text: "summary" }],
|
|
3249
|
-
},
|
|
3250
|
-
renderedSlackMessages[2]!,
|
|
3251
|
-
],
|
|
3252
|
-
tier: "forced_compaction",
|
|
3253
|
-
state: {
|
|
3254
|
-
appliedTiers: ["forced_compaction"],
|
|
3255
|
-
injectionMode: "full",
|
|
3256
|
-
exhausted: false,
|
|
3257
|
-
},
|
|
3258
|
-
estimatedTokens: 5_000,
|
|
3259
|
-
compactionResult: {
|
|
3260
|
-
compacted: true,
|
|
3261
|
-
messages: [
|
|
3262
|
-
{
|
|
3263
|
-
role: "user",
|
|
3264
|
-
content: [{ type: "text", text: "summary" }],
|
|
3265
|
-
},
|
|
3266
|
-
renderedSlackMessages[2]!,
|
|
3267
|
-
],
|
|
3268
|
-
compactedPersistedMessages: 2,
|
|
3269
|
-
previousEstimatedInputTokens: 120_000,
|
|
3270
|
-
estimatedInputTokens: 5_000,
|
|
3271
|
-
maxInputTokens: 100_000,
|
|
3272
|
-
thresholdTokens: 80_000,
|
|
3273
|
-
compactedMessages: 2,
|
|
3274
|
-
summaryCalls: 1,
|
|
3275
|
-
summaryInputTokens: 100,
|
|
3276
|
-
summaryOutputTokens: 20,
|
|
3277
|
-
summaryModel: "mock-model",
|
|
3278
|
-
summaryText: "summary",
|
|
3279
|
-
summaryFailed: false,
|
|
3280
|
-
},
|
|
3281
|
-
});
|
|
3282
|
-
|
|
3283
|
-
const ctx = makeCtx({
|
|
3284
|
-
channelCapabilities: {
|
|
3285
|
-
channel: "slack",
|
|
3286
|
-
dashboardCapable: false,
|
|
3287
|
-
supportsDynamicUi: false,
|
|
3288
|
-
supportsVoiceInput: false,
|
|
3289
|
-
chatType: "channel",
|
|
3290
|
-
},
|
|
3291
|
-
trustContext: {
|
|
3292
|
-
sourceChannel: "slack",
|
|
3293
|
-
trustClass: "guardian",
|
|
3294
|
-
} as AgentLoopConversationContext["trustContext"],
|
|
3295
|
-
getTurnChannelContext: () => ({
|
|
3296
|
-
userMessageChannel: "slack" as const,
|
|
3297
|
-
assistantMessageChannel: "slack" as const,
|
|
3298
|
-
}),
|
|
3299
|
-
contextWindowManager: {
|
|
3300
|
-
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3301
|
-
maybeCompact: async () => ({ compacted: false }),
|
|
3302
|
-
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
3303
|
-
});
|
|
3304
|
-
|
|
3305
|
-
await runAgentLoopImpl(ctx, "next reply", "user-msg-overflow", () => {});
|
|
3306
|
-
|
|
3307
|
-
expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
|
|
3308
|
-
mockSlackChronologicalContext,
|
|
3309
|
-
2,
|
|
3310
|
-
);
|
|
3311
|
-
expect(updateConversationSlackContextWatermarkMock).toHaveBeenCalledWith(
|
|
3312
|
-
"test-conv",
|
|
3313
|
-
"1700000020.000000",
|
|
3314
|
-
expect.any(Number),
|
|
3166
|
+
await runAgentLoopImpl(
|
|
3167
|
+
ctx,
|
|
3168
|
+
"next reply",
|
|
3169
|
+
"user-msg-first-call",
|
|
3170
|
+
() => {},
|
|
3315
3171
|
);
|
|
3316
|
-
const reinjectionOptions = applyRuntimeInjectionsMock.mock.calls.find(
|
|
3317
|
-
(call) => {
|
|
3318
|
-
const options = call[1] as {
|
|
3319
|
-
slackChronologicalMessages?: Message[] | null;
|
|
3320
|
-
};
|
|
3321
|
-
return options.slackChronologicalMessages === null;
|
|
3322
|
-
},
|
|
3323
|
-
)?.[1] as { slackChronologicalMessages?: Message[] | null } | undefined;
|
|
3324
|
-
expect(reinjectionOptions?.slackChronologicalMessages).toBeNull();
|
|
3325
|
-
});
|
|
3326
3172
|
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
content: [{ type: "text", text: "second rendered Slack row" }],
|
|
3336
|
-
},
|
|
3337
|
-
{
|
|
3338
|
-
role: "user",
|
|
3339
|
-
content: [{ type: "text", text: "third rendered Slack row" }],
|
|
3340
|
-
},
|
|
3341
|
-
{
|
|
3342
|
-
role: "user",
|
|
3343
|
-
content: [{ type: "text", text: "retained Slack row" }],
|
|
3344
|
-
},
|
|
3345
|
-
];
|
|
3346
|
-
mockSlackChronologicalContext = {
|
|
3347
|
-
messages: renderedSlackMessages,
|
|
3348
|
-
renderedMessages: renderedSlackMessages.map((message, index) => ({
|
|
3349
|
-
message,
|
|
3350
|
-
sourceChannelTs: [
|
|
3351
|
-
"1700000010.000000",
|
|
3352
|
-
"1700000020.000000",
|
|
3353
|
-
"1700000030.000000",
|
|
3354
|
-
"1700000040.000000",
|
|
3355
|
-
][index]!,
|
|
3356
|
-
tagLineProvenance: "none",
|
|
3357
|
-
})),
|
|
3358
|
-
compactableStartIndex: 0,
|
|
3359
|
-
};
|
|
3360
|
-
|
|
3361
|
-
const firstSummaryMessage: Message = {
|
|
3362
|
-
role: "user",
|
|
3363
|
-
content: [{ type: "text", text: "first summary" }],
|
|
3364
|
-
};
|
|
3365
|
-
const firstCompactedMessages: Message[] = [
|
|
3366
|
-
firstSummaryMessage,
|
|
3367
|
-
renderedSlackMessages[2]!,
|
|
3368
|
-
renderedSlackMessages[3]!,
|
|
3369
|
-
];
|
|
3370
|
-
const secondSummaryMessage: Message = {
|
|
3371
|
-
role: "user",
|
|
3372
|
-
content: [{ type: "text", text: "second summary" }],
|
|
3373
|
-
};
|
|
3374
|
-
const secondCompactedMessages: Message[] = [
|
|
3375
|
-
secondSummaryMessage,
|
|
3376
|
-
renderedSlackMessages[3]!,
|
|
3377
|
-
];
|
|
3378
|
-
const reducerInputs: Message[][] = [];
|
|
3379
|
-
mockEstimateTokens = 120_000;
|
|
3380
|
-
mockReducerStepFn = (msgs: Message[]) => {
|
|
3381
|
-
reducerInputs.push(msgs);
|
|
3382
|
-
mockEstimateTokens = 1000;
|
|
3383
|
-
return {
|
|
3384
|
-
messages: secondCompactedMessages,
|
|
3385
|
-
tier: "forced_compaction",
|
|
3386
|
-
state: {
|
|
3387
|
-
appliedTiers: ["forced_compaction"],
|
|
3388
|
-
injectionMode: "full",
|
|
3389
|
-
exhausted: false,
|
|
3390
|
-
},
|
|
3391
|
-
estimatedTokens: 5_000,
|
|
3392
|
-
compactionResult: {
|
|
3393
|
-
compacted: true,
|
|
3394
|
-
messages: secondCompactedMessages,
|
|
3395
|
-
compactedPersistedMessages: 1,
|
|
3396
|
-
previousEstimatedInputTokens: 120_000,
|
|
3397
|
-
estimatedInputTokens: 5_000,
|
|
3398
|
-
maxInputTokens: 100_000,
|
|
3399
|
-
thresholdTokens: 80_000,
|
|
3400
|
-
compactedMessages: 1,
|
|
3401
|
-
summaryCalls: 1,
|
|
3402
|
-
summaryInputTokens: 100,
|
|
3403
|
-
summaryOutputTokens: 20,
|
|
3404
|
-
summaryModel: "mock-model",
|
|
3405
|
-
summaryText: "second summary",
|
|
3406
|
-
summaryFailed: false,
|
|
3407
|
-
},
|
|
3408
|
-
};
|
|
3409
|
-
};
|
|
3410
|
-
|
|
3411
|
-
const ctx = makeCtx({
|
|
3412
|
-
channelCapabilities: {
|
|
3413
|
-
channel: "slack",
|
|
3414
|
-
dashboardCapable: false,
|
|
3415
|
-
supportsDynamicUi: false,
|
|
3416
|
-
supportsVoiceInput: false,
|
|
3417
|
-
chatType: "channel",
|
|
3418
|
-
},
|
|
3419
|
-
trustContext: {
|
|
3420
|
-
sourceChannel: "slack",
|
|
3421
|
-
trustClass: "guardian",
|
|
3422
|
-
} as AgentLoopConversationContext["trustContext"],
|
|
3423
|
-
getTurnChannelContext: () => ({
|
|
3424
|
-
userMessageChannel: "slack" as const,
|
|
3425
|
-
assistantMessageChannel: "slack" as const,
|
|
3426
|
-
}),
|
|
3427
|
-
contextWindowManager: {
|
|
3428
|
-
shouldCompact: () => ({ needed: true, estimatedTokens: 120_000 }),
|
|
3429
|
-
maybeCompact: async (messages: Message[]) => {
|
|
3430
|
-
expect(messages).toBe(renderedSlackMessages);
|
|
3431
|
-
return {
|
|
3432
|
-
compacted: true,
|
|
3433
|
-
messages: firstCompactedMessages,
|
|
3434
|
-
compactedPersistedMessages: 2,
|
|
3435
|
-
previousEstimatedInputTokens: 120_000,
|
|
3436
|
-
estimatedInputTokens: 60_000,
|
|
3437
|
-
maxInputTokens: 100_000,
|
|
3438
|
-
thresholdTokens: 80_000,
|
|
3439
|
-
compactedMessages: 2,
|
|
3440
|
-
summaryCalls: 1,
|
|
3441
|
-
summaryInputTokens: 100,
|
|
3442
|
-
summaryOutputTokens: 20,
|
|
3443
|
-
summaryModel: "mock-model",
|
|
3444
|
-
summaryText: "first summary",
|
|
3445
|
-
summaryFailed: false,
|
|
3446
|
-
};
|
|
3447
|
-
},
|
|
3448
|
-
} as unknown as AgentLoopConversationContext["contextWindowManager"],
|
|
3449
|
-
});
|
|
3450
|
-
|
|
3451
|
-
await runAgentLoopImpl(ctx, "next reply", "user-msg-repeat", () => {});
|
|
3452
|
-
|
|
3453
|
-
expect(reducerInputs[0]).toBe(firstCompactedMessages);
|
|
3454
|
-
expect(getSlackCompactionWatermarkForPrefixMock.mock.calls).toEqual([
|
|
3455
|
-
[mockSlackChronologicalContext, 2],
|
|
3456
|
-
[
|
|
3457
|
-
{
|
|
3458
|
-
renderedMessages: [
|
|
3459
|
-
{
|
|
3460
|
-
message: firstSummaryMessage,
|
|
3461
|
-
sourceChannelTs: null,
|
|
3462
|
-
tagLineProvenance: "none",
|
|
3463
|
-
},
|
|
3464
|
-
mockSlackChronologicalContext!.renderedMessages[2],
|
|
3465
|
-
mockSlackChronologicalContext!.renderedMessages[3],
|
|
3466
|
-
],
|
|
3467
|
-
messages: firstCompactedMessages,
|
|
3468
|
-
compactableStartIndex: 1,
|
|
3469
|
-
},
|
|
3470
|
-
1,
|
|
3471
|
-
],
|
|
3472
|
-
]);
|
|
3473
|
-
expect(updateConversationSlackContextWatermarkMock.mock.calls).toEqual([
|
|
3474
|
-
["test-conv", "1700000020.000000", expect.any(Number)],
|
|
3475
|
-
["test-conv", "1700000030.000000", expect.any(Number)],
|
|
3476
|
-
]);
|
|
3477
|
-
expect(loadSlackChronologicalContextMock).toHaveBeenCalledTimes(1);
|
|
3173
|
+
// The loop compacts its own stripped working history, never the loaded
|
|
3174
|
+
// Slack transcript array, so provenance cannot be projected and the
|
|
3175
|
+
// watermark is left untouched — matching mid-loop compaction.
|
|
3176
|
+
expect(maybeCompactInputs.length).toBeGreaterThan(0);
|
|
3177
|
+
expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
|
|
3178
|
+
expect(
|
|
3179
|
+
updateConversationSlackContextWatermarkMock,
|
|
3180
|
+
).not.toHaveBeenCalled();
|
|
3478
3181
|
});
|
|
3479
3182
|
|
|
3480
3183
|
test("mid-loop Slack compaction does not persist watermark from mismatched loaded context", async () => {
|
|
@@ -3542,32 +3245,16 @@ describe("session-agent-loop", () => {
|
|
|
3542
3245
|
trustContext: {
|
|
3543
3246
|
sourceChannel: "slack",
|
|
3544
3247
|
trustClass: "guardian",
|
|
3545
|
-
} as
|
|
3248
|
+
} as Conversation["trustContext"],
|
|
3546
3249
|
getTurnChannelContext: () => ({
|
|
3547
3250
|
userMessageChannel: "slack" as const,
|
|
3548
3251
|
assistantMessageChannel: "slack" as const,
|
|
3549
3252
|
}),
|
|
3550
3253
|
contextWindowManager: {
|
|
3254
|
+
updateConfig: () => {},
|
|
3551
3255
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3552
3256
|
maybeCompact: async (messages: Message[]) => {
|
|
3553
3257
|
maybeCompactInputs.push(messages);
|
|
3554
|
-
if (messages === renderedSlackMessages) {
|
|
3555
|
-
return {
|
|
3556
|
-
compacted: false,
|
|
3557
|
-
messages,
|
|
3558
|
-
compactedPersistedMessages: 0,
|
|
3559
|
-
previousEstimatedInputTokens: 1000,
|
|
3560
|
-
estimatedInputTokens: 1000,
|
|
3561
|
-
maxInputTokens: 100_000,
|
|
3562
|
-
thresholdTokens: 80_000,
|
|
3563
|
-
compactedMessages: 0,
|
|
3564
|
-
summaryCalls: 0,
|
|
3565
|
-
summaryInputTokens: 0,
|
|
3566
|
-
summaryOutputTokens: 0,
|
|
3567
|
-
summaryModel: "",
|
|
3568
|
-
summaryText: "",
|
|
3569
|
-
};
|
|
3570
|
-
}
|
|
3571
3258
|
// The mid-loop gate compacted its in-loop basis; drop the estimate
|
|
3572
3259
|
// back under budget so the post-compaction provider call proceeds.
|
|
3573
3260
|
mockEstimateTokens = 1000;
|
|
@@ -3593,15 +3280,16 @@ describe("session-agent-loop", () => {
|
|
|
3593
3280
|
summaryFailed: false,
|
|
3594
3281
|
};
|
|
3595
3282
|
},
|
|
3596
|
-
} as unknown as
|
|
3283
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3597
3284
|
});
|
|
3598
3285
|
|
|
3599
3286
|
await runAgentLoopImpl(ctx, "next reply", "user-msg-mid-loop", () => {});
|
|
3600
3287
|
|
|
3601
|
-
expect(maybeCompactInputs[0]).toBe(renderedSlackMessages);
|
|
3602
3288
|
// The mid-loop gate compacts the loop's own in-loop history, never the
|
|
3603
|
-
// loaded Slack rows — the mismatch this test guards against.
|
|
3604
|
-
|
|
3289
|
+
// loaded Slack rows — the mismatch this test guards against. No
|
|
3290
|
+
// start-of-turn compaction runs, so this is the turn's only compaction.
|
|
3291
|
+
expect(maybeCompactInputs).toHaveLength(1);
|
|
3292
|
+
expect(maybeCompactInputs[0]).not.toBe(renderedSlackMessages);
|
|
3605
3293
|
expect(getSlackCompactionWatermarkForPrefixMock).toHaveBeenCalledWith(
|
|
3606
3294
|
null,
|
|
3607
3295
|
2,
|
|
@@ -3611,7 +3299,7 @@ describe("session-agent-loop", () => {
|
|
|
3611
3299
|
).not.toHaveBeenCalled();
|
|
3612
3300
|
});
|
|
3613
3301
|
|
|
3614
|
-
test("next inbound Slack turn
|
|
3302
|
+
test("next inbound Slack turn loads chronological context using the persisted watermark", async () => {
|
|
3615
3303
|
mockConversationRow = {
|
|
3616
3304
|
...mockConversationRow,
|
|
3617
3305
|
contextSummary: "## Summary\n- compacted Slack context",
|
|
@@ -3671,7 +3359,7 @@ describe("session-agent-loop", () => {
|
|
|
3671
3359
|
trustContext: {
|
|
3672
3360
|
sourceChannel: "slack",
|
|
3673
3361
|
trustClass: "guardian",
|
|
3674
|
-
} as
|
|
3362
|
+
} as Conversation["trustContext"],
|
|
3675
3363
|
getTurnChannelContext: () => ({
|
|
3676
3364
|
userMessageChannel: "slack" as const,
|
|
3677
3365
|
assistantMessageChannel: "slack" as const,
|
|
@@ -3690,26 +3378,9 @@ describe("session-agent-loop", () => {
|
|
|
3690
3378
|
trustClass: "guardian",
|
|
3691
3379
|
}),
|
|
3692
3380
|
);
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
};
|
|
3697
|
-
expect(firstInjectionOptions.slackChronologicalMessages).toBe(
|
|
3698
|
-
mockSlackChronologicalContext.messages,
|
|
3699
|
-
);
|
|
3700
|
-
const rendered = firstInjectionOptions
|
|
3701
|
-
.slackChronologicalMessages!.flatMap((message) => message.content)
|
|
3702
|
-
.filter((block): block is { type: "text"; text: string } => {
|
|
3703
|
-
return block.type === "text";
|
|
3704
|
-
})
|
|
3705
|
-
.map((block) => block.text)
|
|
3706
|
-
.join("\n");
|
|
3707
|
-
expect(rendered).toContain("compacted Slack context");
|
|
3708
|
-
expect(rendered).toContain("after watermark reply");
|
|
3709
|
-
expect(rendered).not.toContain("before watermark");
|
|
3710
|
-
});
|
|
3711
|
-
|
|
3712
|
-
test("subsequent Slack turn keeps long-thread compaction summary and filtered tail", async () => {
|
|
3381
|
+
});
|
|
3382
|
+
|
|
3383
|
+
test("subsequent Slack turn loads chronological context using the persisted long-thread watermark", async () => {
|
|
3713
3384
|
mockConversationRow = {
|
|
3714
3385
|
...mockConversationRow,
|
|
3715
3386
|
contextSummary: "## Summary\n- compacted long Slack thread",
|
|
@@ -3779,7 +3450,7 @@ describe("session-agent-loop", () => {
|
|
|
3779
3450
|
trustContext: {
|
|
3780
3451
|
sourceChannel: "slack",
|
|
3781
3452
|
trustClass: "guardian",
|
|
3782
|
-
} as
|
|
3453
|
+
} as Conversation["trustContext"],
|
|
3783
3454
|
getTurnChannelContext: () => ({
|
|
3784
3455
|
userMessageChannel: "slack" as const,
|
|
3785
3456
|
assistantMessageChannel: "slack" as const,
|
|
@@ -3802,21 +3473,6 @@ describe("session-agent-loop", () => {
|
|
|
3802
3473
|
slackContextCompactionWatermarkTs: "1700000080.000000",
|
|
3803
3474
|
}),
|
|
3804
3475
|
);
|
|
3805
|
-
const firstInjectionOptions = applyRuntimeInjectionsMock.mock
|
|
3806
|
-
.calls[0]![1] as {
|
|
3807
|
-
slackChronologicalMessages?: Message[] | null;
|
|
3808
|
-
};
|
|
3809
|
-
const rendered = firstInjectionOptions
|
|
3810
|
-
.slackChronologicalMessages!.flatMap((message) => message.content)
|
|
3811
|
-
.filter((block): block is { type: "text"; text: string } => {
|
|
3812
|
-
return block.type === "text";
|
|
3813
|
-
})
|
|
3814
|
-
.map((block) => block.text)
|
|
3815
|
-
.join("\n");
|
|
3816
|
-
expect(rendered).toContain("compacted long Slack thread");
|
|
3817
|
-
expect(rendered).toContain("reply after compaction");
|
|
3818
|
-
expect(rendered).not.toContain("pre-compaction");
|
|
3819
|
-
expect(rendered).not.toContain("original root");
|
|
3820
3476
|
});
|
|
3821
3477
|
|
|
3822
3478
|
test("applyCompactionResult records Slack timestamp watermark when provided", async () => {
|
|
@@ -3858,6 +3514,59 @@ describe("session-agent-loop", () => {
|
|
|
3858
3514
|
true,
|
|
3859
3515
|
);
|
|
3860
3516
|
});
|
|
3517
|
+
|
|
3518
|
+
test("applyCompactionResult advances the persisted count from the trusted in-context boundary", async () => {
|
|
3519
|
+
// Trusted views slice past the already-compacted prefix, so a further
|
|
3520
|
+
// compaction advances the persisted count from the mirrored DB boundary.
|
|
3521
|
+
|
|
3522
|
+
// GIVEN a trusted (guardian) conversation that has already compacted 5
|
|
3523
|
+
// persisted messages, so its in-context history starts past that prefix
|
|
3524
|
+
const ctx = makeCtx({
|
|
3525
|
+
contextCompactedMessageCount: 5,
|
|
3526
|
+
trustContext: {
|
|
3527
|
+
trustClass: "guardian",
|
|
3528
|
+
} as Conversation["trustContext"],
|
|
3529
|
+
});
|
|
3530
|
+
|
|
3531
|
+
// WHEN a turn compacts 4 more in-context messages
|
|
3532
|
+
await applyCompactionResult(
|
|
3533
|
+
ctx,
|
|
3534
|
+
makeCompactionResult({ compactedPersistedMessages: 4 }),
|
|
3535
|
+
() => {},
|
|
3536
|
+
"req-1",
|
|
3537
|
+
);
|
|
3538
|
+
|
|
3539
|
+
// THEN the persisted count advances from the prior boundary (5 + 4)
|
|
3540
|
+
expect(ctx.contextCompactedMessageCount).toBe(9);
|
|
3541
|
+
});
|
|
3542
|
+
|
|
3543
|
+
test("applyCompactionResult resets the persisted count to the unsliced boundary for untrusted views", async () => {
|
|
3544
|
+
// Untrusted views render history unsliced (boundary 0), so a compaction
|
|
3545
|
+
// must record only the new summary's prefix instead of adding to the raw
|
|
3546
|
+
// mirror — otherwise future loads slice past unsummarized rows.
|
|
3547
|
+
|
|
3548
|
+
// GIVEN an untrusted view of a conversation whose raw DB count mirrors a
|
|
3549
|
+
// 5-message compacted prefix — but untrusted views render that history
|
|
3550
|
+
// unsliced (boundary 0), so the compactor operates on the full list
|
|
3551
|
+
const ctx = makeCtx({
|
|
3552
|
+
contextCompactedMessageCount: 5,
|
|
3553
|
+
trustContext: {
|
|
3554
|
+
trustClass: "unknown",
|
|
3555
|
+
} as Conversation["trustContext"],
|
|
3556
|
+
});
|
|
3557
|
+
|
|
3558
|
+
// WHEN that turn compacts 4 in-context messages
|
|
3559
|
+
await applyCompactionResult(
|
|
3560
|
+
ctx,
|
|
3561
|
+
makeCompactionResult({ compactedPersistedMessages: 4 }),
|
|
3562
|
+
() => {},
|
|
3563
|
+
"req-1",
|
|
3564
|
+
);
|
|
3565
|
+
|
|
3566
|
+
// THEN the persisted count reflects only the new summary's prefix (0 + 4)
|
|
3567
|
+
// rather than double-counting the raw mirror (which would yield 9)
|
|
3568
|
+
expect(ctx.contextCompactedMessageCount).toBe(4);
|
|
3569
|
+
});
|
|
3861
3570
|
});
|
|
3862
3571
|
|
|
3863
3572
|
describe("compaction-strip marker persistence", () => {
|
|
@@ -3894,9 +3603,10 @@ describe("session-agent-loop", () => {
|
|
|
3894
3603
|
],
|
|
3895
3604
|
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
3896
3605
|
contextWindowManager: {
|
|
3606
|
+
updateConfig: () => {},
|
|
3897
3607
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3898
3608
|
maybeCompact: async () => ({ compacted: false }),
|
|
3899
|
-
} as unknown as
|
|
3609
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3900
3610
|
});
|
|
3901
3611
|
|
|
3902
3612
|
// WHEN the orchestrator runs the turn to completion
|
|
@@ -3943,9 +3653,10 @@ describe("session-agent-loop", () => {
|
|
|
3943
3653
|
],
|
|
3944
3654
|
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
3945
3655
|
contextWindowManager: {
|
|
3656
|
+
updateConfig: () => {},
|
|
3946
3657
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
3947
3658
|
maybeCompact: async () => ({ compacted: false }),
|
|
3948
|
-
} as unknown as
|
|
3659
|
+
} as unknown as Conversation["contextWindowManager"],
|
|
3949
3660
|
});
|
|
3950
3661
|
|
|
3951
3662
|
// Must not throw — the strip-site marker write is wrapped in try/catch.
|