@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
|
@@ -2,17 +2,30 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
5
|
+
import { eq } from "drizzle-orm";
|
|
6
|
+
|
|
5
7
|
// This test exercises v1 PKB injection. `config.memory.v2.enabled` (default
|
|
6
8
|
// `true`) makes the PKB injector go silent — force it off here so the v1
|
|
7
9
|
// injection chain assertions stay meaningful.
|
|
8
10
|
const realLoaderForAssemblyTest = await import("../config/loader.js");
|
|
9
11
|
const realGetConfigForAssemblyTest = realLoaderForAssemblyTest.getConfig;
|
|
12
|
+
// Per-test overrides for `ui.userTimezone` / `ui.detectedTimezone` so the
|
|
13
|
+
// config self-resolution inside `applyRuntimeInjections` can be exercised.
|
|
14
|
+
let assemblyConfiguredUserTimezone: string | null | undefined;
|
|
15
|
+
let assemblyDetectedTimezone: string | null | undefined;
|
|
10
16
|
mock.module("../config/loader.js", () => ({
|
|
11
17
|
...realLoaderForAssemblyTest,
|
|
12
18
|
getConfig: () => {
|
|
13
19
|
const real = realGetConfigForAssemblyTest();
|
|
14
20
|
return {
|
|
15
21
|
...real,
|
|
22
|
+
ui: {
|
|
23
|
+
...real.ui,
|
|
24
|
+
userTimezone:
|
|
25
|
+
assemblyConfiguredUserTimezone ?? real.ui?.userTimezone ?? null,
|
|
26
|
+
detectedTimezone:
|
|
27
|
+
assemblyDetectedTimezone ?? real.ui?.detectedTimezone ?? null,
|
|
28
|
+
},
|
|
16
29
|
memory: { ...real.memory, v2: { ...real.memory.v2, enabled: false } },
|
|
17
30
|
slack: { ...real.slack, botUserId: "U_BOT" },
|
|
18
31
|
};
|
|
@@ -35,17 +48,35 @@ mock.module("../memory/pkb/pkb-search.js", () => ({
|
|
|
35
48
|
},
|
|
36
49
|
}));
|
|
37
50
|
|
|
51
|
+
// `applyRuntimeInjections` computes the `<turn_context>` `current_time` live via
|
|
52
|
+
// `formatTurnTimestamp`; pin it so the rendered block matches the deterministic
|
|
53
|
+
// `buildUnifiedTurnContextBlock` expectations below. The rest of the module
|
|
54
|
+
// (timezone canonicalization/resolution) keeps its real behavior.
|
|
55
|
+
const FIXED_TURN_TIMESTAMP = "2026-04-02T12:00:00Z";
|
|
56
|
+
// Conversation id the runtime-injection tests register their live conversation
|
|
57
|
+
// under; passed explicitly now that `applyRuntimeInjections` requires the
|
|
58
|
+
// caller to name the conversation it resolves through.
|
|
59
|
+
const FALLBACK_CONVERSATION_ID = "runtime-assembly-fallback";
|
|
60
|
+
const realDateContextForAssemblyTest =
|
|
61
|
+
await import("../daemon/date-context.js");
|
|
62
|
+
mock.module("../daemon/date-context.js", () => ({
|
|
63
|
+
...realDateContextForAssemblyTest,
|
|
64
|
+
formatTurnTimestamp: () => FIXED_TURN_TIMESTAMP,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
import {
|
|
68
|
+
clearConversations,
|
|
69
|
+
setConversation,
|
|
70
|
+
} from "../daemon/conversation-registry.js";
|
|
38
71
|
import type {
|
|
39
72
|
ChannelCapabilities,
|
|
40
73
|
SlackTranscriptInputRow,
|
|
41
|
-
UnifiedTurnContextOptions,
|
|
42
74
|
} from "../daemon/conversation-runtime-assembly.js";
|
|
43
75
|
import {
|
|
44
76
|
applyRuntimeInjections,
|
|
45
77
|
assembleSlackActiveThreadFocusBlock,
|
|
46
78
|
assembleSlackChronologicalMessages,
|
|
47
79
|
buildSubagentStatusBlock,
|
|
48
|
-
buildUnifiedTurnContextBlock,
|
|
49
80
|
getSlackCompactionWatermarkForPrefix,
|
|
50
81
|
injectChannelCapabilityContext,
|
|
51
82
|
injectChannelCommandContext,
|
|
@@ -59,20 +90,35 @@ import {
|
|
|
59
90
|
stripInjectionsForCompaction,
|
|
60
91
|
stripNowScratchpad,
|
|
61
92
|
} from "../daemon/conversation-runtime-assembly.js";
|
|
93
|
+
import type { SurfaceData, SurfaceType } from "../daemon/message-protocol.js";
|
|
62
94
|
import { buildPkbReminder } from "../daemon/pkb-reminder-builder.js";
|
|
63
95
|
import type { MessageRow } from "../memory/conversation-crud.js";
|
|
96
|
+
import { getDb } from "../memory/db-connection.js";
|
|
97
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
64
98
|
import { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
|
|
65
99
|
import { getPkbRoot } from "../memory/pkb/types.js";
|
|
100
|
+
import { conversations, messages } from "../memory/schema.js";
|
|
66
101
|
import {
|
|
67
102
|
type SlackMessageMetadata,
|
|
68
103
|
writeSlackMetadata,
|
|
69
104
|
} from "../messaging/providers/slack/message-metadata.js";
|
|
70
105
|
import { parentAlias } from "../messaging/providers/slack/render-transcript.js";
|
|
106
|
+
import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
107
|
+
import {
|
|
108
|
+
buildUnifiedTurnContextBlock,
|
|
109
|
+
type UnifiedTurnContextOptions,
|
|
110
|
+
} from "../plugins/defaults/memory-retrieval/unified-turn-context.js";
|
|
71
111
|
import type { Message } from "../providers/types.js";
|
|
72
112
|
import { wrapUntrustedContent } from "../security/untrusted-content.js";
|
|
113
|
+
import { getSubagentManager } from "../subagent/index.js";
|
|
73
114
|
import type { SubagentState } from "../subagent/types.js";
|
|
74
115
|
import { getWorkspacePromptPath } from "../util/platform.js";
|
|
75
116
|
|
|
117
|
+
// `applyRuntimeInjections` self-resolves the Slack active-thread focus block by
|
|
118
|
+
// reading the live conversation's persisted message rows, so the schema must
|
|
119
|
+
// exist before any Slack-channel assembly test runs.
|
|
120
|
+
initializeDb();
|
|
121
|
+
|
|
76
122
|
// The pkb-reminder injector derives PKB-active state from the workspace itself
|
|
77
123
|
// — `readPkbContext()` returning content behind the personal-memory trust gate
|
|
78
124
|
// — rather than from a threaded flag. Tests that need the reminder to fire seed
|
|
@@ -103,6 +149,66 @@ function clearNowScratchpad(): void {
|
|
|
103
149
|
rmSync(getWorkspacePromptPath("NOW.md"), { force: true });
|
|
104
150
|
}
|
|
105
151
|
|
|
152
|
+
// The workspace-context injector sources its block off the live `Conversation`
|
|
153
|
+
// looked up by `conversationId`. Register a fake instance carrying both a
|
|
154
|
+
// non-dirty workspace context (non-null content, not dirty) so
|
|
155
|
+
// `resolveWorkspaceTopLevelContext` returns it verbatim without rescanning the
|
|
156
|
+
// filesystem, and an active dynamic-page surface, so `applyRuntimeInjections`
|
|
157
|
+
// resolves the `<workspace>` and `<active_workspace>` blocks from it;
|
|
158
|
+
// `clearConversations()` between tests keeps suites that assert the blocks are
|
|
159
|
+
// absent unaffected.
|
|
160
|
+
function seedActiveSurfaceConversation(
|
|
161
|
+
conversationId: string,
|
|
162
|
+
workspaceText: string,
|
|
163
|
+
surfaceId: string,
|
|
164
|
+
data: SurfaceData,
|
|
165
|
+
channelCapabilities?: ChannelCapabilities,
|
|
166
|
+
commandIntent?: { type: string; payload?: string; languageCode?: string },
|
|
167
|
+
currentTurnTemporalSnapshot?: {
|
|
168
|
+
clientTimezone: string | null;
|
|
169
|
+
},
|
|
170
|
+
): void {
|
|
171
|
+
setConversation(conversationId, {
|
|
172
|
+
conversationId,
|
|
173
|
+
workingDir: "/sandbox",
|
|
174
|
+
workspaceTopLevelContext: workspaceText,
|
|
175
|
+
workspaceTopLevelDirty: false,
|
|
176
|
+
currentActiveSurfaceId: surfaceId,
|
|
177
|
+
surfaceState: new Map<
|
|
178
|
+
string,
|
|
179
|
+
{ surfaceType: SurfaceType; data: SurfaceData }
|
|
180
|
+
>([[surfaceId, { surfaceType: "dynamic_page", data }]]),
|
|
181
|
+
channelCapabilities: channelCapabilities ?? undefined,
|
|
182
|
+
commandIntent,
|
|
183
|
+
currentTurnTemporalSnapshot,
|
|
184
|
+
} as never);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function clearWorkspaceContext(): void {
|
|
188
|
+
clearConversations();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// The `<channel_capabilities>` branch and the Slack gates source the channel
|
|
192
|
+
// capabilities off the live `Conversation` looked up by `conversationId`.
|
|
193
|
+
// Register a fake fallback instance carrying the given capabilities (and a
|
|
194
|
+
// non-dirty empty workspace + empty surface so the other live-sourced branches
|
|
195
|
+
// stay inert) so `applyRuntimeInjections` resolves them; `clearConversations()`
|
|
196
|
+
// between tests keeps suites asserting absence unaffected.
|
|
197
|
+
function seedChannelCapabilitiesConversation(
|
|
198
|
+
caps: ChannelCapabilities | null,
|
|
199
|
+
transportHints?: string[],
|
|
200
|
+
): void {
|
|
201
|
+
setConversation("runtime-assembly-fallback", {
|
|
202
|
+
conversationId: "runtime-assembly-fallback",
|
|
203
|
+
workingDir: "/sandbox",
|
|
204
|
+
workspaceTopLevelContext: "",
|
|
205
|
+
workspaceTopLevelDirty: false,
|
|
206
|
+
surfaceState: new Map(),
|
|
207
|
+
channelCapabilities: caps ?? undefined,
|
|
208
|
+
transportHints,
|
|
209
|
+
} as never);
|
|
210
|
+
}
|
|
211
|
+
|
|
106
212
|
// ---------------------------------------------------------------------------
|
|
107
213
|
// resolveChannelCapabilities
|
|
108
214
|
// ---------------------------------------------------------------------------
|
|
@@ -575,6 +681,8 @@ describe("stripChannelCapabilityContext", () => {
|
|
|
575
681
|
// ---------------------------------------------------------------------------
|
|
576
682
|
|
|
577
683
|
describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
684
|
+
afterEach(clearConversations);
|
|
685
|
+
|
|
578
686
|
const baseMessages: Message[] = [
|
|
579
687
|
{
|
|
580
688
|
role: "user",
|
|
@@ -590,8 +698,9 @@ describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
|
590
698
|
supportsVoiceInput: false,
|
|
591
699
|
};
|
|
592
700
|
|
|
701
|
+
seedChannelCapabilitiesConversation(caps);
|
|
593
702
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
594
|
-
|
|
703
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
595
704
|
});
|
|
596
705
|
|
|
597
706
|
expect(result.length).toBe(1);
|
|
@@ -603,8 +712,9 @@ describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
|
603
712
|
});
|
|
604
713
|
|
|
605
714
|
test("does not inject when channelCapabilities is null", async () => {
|
|
715
|
+
seedChannelCapabilitiesConversation(null);
|
|
606
716
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
607
|
-
|
|
717
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
608
718
|
});
|
|
609
719
|
|
|
610
720
|
expect(result.length).toBe(1);
|
|
@@ -612,7 +722,9 @@ describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
|
612
722
|
});
|
|
613
723
|
|
|
614
724
|
test("does not inject when channelCapabilities is omitted", async () => {
|
|
615
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
725
|
+
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
726
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
727
|
+
});
|
|
616
728
|
|
|
617
729
|
expect(result.length).toBe(1);
|
|
618
730
|
expect(result[0].content.length).toBe(1);
|
|
@@ -626,8 +738,9 @@ describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
|
626
738
|
supportsVoiceInput: false,
|
|
627
739
|
};
|
|
628
740
|
|
|
741
|
+
seedChannelCapabilitiesConversation(caps);
|
|
629
742
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
630
|
-
|
|
743
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
631
744
|
});
|
|
632
745
|
|
|
633
746
|
expect(result.length).toBe(1);
|
|
@@ -755,30 +868,25 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
755
868
|
},
|
|
756
869
|
];
|
|
757
870
|
|
|
871
|
+
const channelCapabilities: ChannelCapabilities = {
|
|
872
|
+
channel: "telegram",
|
|
873
|
+
dashboardCapable: false,
|
|
874
|
+
supportsDynamicUi: false,
|
|
875
|
+
supportsVoiceInput: false,
|
|
876
|
+
};
|
|
877
|
+
|
|
758
878
|
const fullOptions = {
|
|
759
|
-
|
|
760
|
-
channelCommandContext: { type: "start" } as const,
|
|
761
|
-
activeSurface: { surfaceId: "sf_1", html: "<div>test</div>" },
|
|
762
|
-
channelCapabilities: {
|
|
763
|
-
channel: "telegram",
|
|
764
|
-
dashboardCapable: false,
|
|
765
|
-
supportsDynamicUi: false,
|
|
766
|
-
supportsVoiceInput: false,
|
|
767
|
-
} as ChannelCapabilities,
|
|
768
|
-
unifiedTurnContext:
|
|
769
|
-
"<turn_context>\ncurrent_time: 2026-03-04 (Tuesday) 12:00:00 +00:00 (UTC)\ninterface: telegram\n</turn_context>",
|
|
879
|
+
// Non-interactive turn so the `<non_interactive_context>` branch fires.
|
|
770
880
|
isNonInteractive: true,
|
|
881
|
+
requestId: "injection-mode-req",
|
|
882
|
+
conversationId: "injection-mode-conv",
|
|
883
|
+
turnIndex: 0,
|
|
771
884
|
// Guardian trust so the personal-memory gate admits the actor regardless
|
|
772
885
|
// of the telegram channel capabilities under test, letting the reminder
|
|
773
886
|
// gate hinge purely on PKB content presence.
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
turnIndex: 0,
|
|
778
|
-
trust: {
|
|
779
|
-
sourceChannel: "vellum" as const,
|
|
780
|
-
trustClass: "guardian" as const,
|
|
781
|
-
},
|
|
887
|
+
trust: {
|
|
888
|
+
sourceChannel: "vellum" as const,
|
|
889
|
+
trustClass: "guardian" as const,
|
|
782
890
|
},
|
|
783
891
|
};
|
|
784
892
|
|
|
@@ -788,10 +896,22 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
788
896
|
beforeEach(() => {
|
|
789
897
|
seedPkbContent();
|
|
790
898
|
seedNowScratchpad("Current focus: shipping PR 3");
|
|
899
|
+
seedActiveSurfaceConversation(
|
|
900
|
+
"injection-mode-conv",
|
|
901
|
+
"<workspace>\nRoot: /sandbox\n</workspace>",
|
|
902
|
+
"sf_1",
|
|
903
|
+
{ html: "<div>test</div>" },
|
|
904
|
+
channelCapabilities,
|
|
905
|
+
{ type: "start" },
|
|
906
|
+
{
|
|
907
|
+
clientTimezone: null,
|
|
908
|
+
},
|
|
909
|
+
);
|
|
791
910
|
});
|
|
792
911
|
afterEach(() => {
|
|
793
912
|
clearPkbContent();
|
|
794
913
|
clearNowScratchpad();
|
|
914
|
+
clearWorkspaceContext();
|
|
795
915
|
});
|
|
796
916
|
|
|
797
917
|
test("full mode (default) includes all injections", async () => {
|
|
@@ -898,6 +1018,34 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
898
1018
|
|
|
899
1019
|
expect(texts).toContain("Hello");
|
|
900
1020
|
});
|
|
1021
|
+
|
|
1022
|
+
test("post-compaction re-injection resolves the live conversation from the conversation id", async () => {
|
|
1023
|
+
// GIVEN the seeded live conversation `injection-mode-conv` whose
|
|
1024
|
+
// conversation-scoped blocks (`<active_workspace>`, `<channel_capabilities>`)
|
|
1025
|
+
// only resolve when `applyRuntimeInjections` finds it by conversation id
|
|
1026
|
+
// AND the agent loop hands the post-compaction hook the turn-identity fields
|
|
1027
|
+
// flat (`requestId`, `conversationId`, `trust`)
|
|
1028
|
+
const { messages: result } = await postCompact({
|
|
1029
|
+
history: baseMessages,
|
|
1030
|
+
requestId: "reinject-req",
|
|
1031
|
+
conversationId: "injection-mode-conv",
|
|
1032
|
+
trust: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
1033
|
+
isNonInteractive: false,
|
|
1034
|
+
mode: "full",
|
|
1035
|
+
modelProfile: null,
|
|
1036
|
+
actorContext: null,
|
|
1037
|
+
});
|
|
1038
|
+
const allText = result[0].content
|
|
1039
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
1040
|
+
.map((b) => b.text)
|
|
1041
|
+
.join("\n");
|
|
1042
|
+
|
|
1043
|
+
// THEN the hook forwards `conversationId` onto the flat injection options,
|
|
1044
|
+
// so the re-injection resolves the same live conversation as the turn's
|
|
1045
|
+
// initial assembly rather than the synthetic fallback.
|
|
1046
|
+
expect(allText).toContain("<active_workspace>");
|
|
1047
|
+
expect(allText).toContain("<channel_capabilities>");
|
|
1048
|
+
});
|
|
901
1049
|
});
|
|
902
1050
|
|
|
903
1051
|
// The now-md default injector emits the `<NOW.md>` block as an
|
|
@@ -1051,7 +1199,7 @@ describe("stripInjectionsForCompaction preserves persistent blocks", () => {
|
|
|
1051
1199
|
).toContain("<turn_context>");
|
|
1052
1200
|
});
|
|
1053
1201
|
|
|
1054
|
-
test("<workspace> blocks
|
|
1202
|
+
test("<workspace> blocks ARE stripped so the injector re-sources them post-compaction", () => {
|
|
1055
1203
|
const messages: Message[] = [
|
|
1056
1204
|
{
|
|
1057
1205
|
role: "user",
|
|
@@ -1067,10 +1215,10 @@ describe("stripInjectionsForCompaction preserves persistent blocks", () => {
|
|
|
1067
1215
|
|
|
1068
1216
|
const result = stripInjectionsForCompaction(messages);
|
|
1069
1217
|
expect(result.length).toBe(1);
|
|
1070
|
-
expect(result[0].content.length).toBe(
|
|
1071
|
-
expect(
|
|
1072
|
-
|
|
1073
|
-
)
|
|
1218
|
+
expect(result[0].content.length).toBe(1);
|
|
1219
|
+
expect((result[0].content[0] as { type: "text"; text: string }).text).toBe(
|
|
1220
|
+
"Hello",
|
|
1221
|
+
);
|
|
1074
1222
|
});
|
|
1075
1223
|
|
|
1076
1224
|
test("legacy <workspace_top_level> blocks ARE stripped for backward compat", () => {
|
|
@@ -1237,7 +1385,9 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1237
1385
|
|
|
1238
1386
|
test("injects NOW.md block when the file has content", async () => {
|
|
1239
1387
|
seedNowScratchpad("Current focus: fix the bug");
|
|
1240
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1388
|
+
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1389
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1390
|
+
});
|
|
1241
1391
|
|
|
1242
1392
|
expect(result.length).toBe(1);
|
|
1243
1393
|
expect(result[0].content.length).toBe(2);
|
|
@@ -1249,7 +1399,9 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1249
1399
|
|
|
1250
1400
|
test("scratchpad appears before user's original text content", async () => {
|
|
1251
1401
|
seedNowScratchpad("scratchpad notes");
|
|
1252
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1402
|
+
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1403
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1404
|
+
});
|
|
1253
1405
|
|
|
1254
1406
|
// Scratchpad comes first (before user content)
|
|
1255
1407
|
expect(
|
|
@@ -1262,7 +1414,9 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1262
1414
|
});
|
|
1263
1415
|
|
|
1264
1416
|
test("does not inject when the NOW.md file is absent", async () => {
|
|
1265
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1417
|
+
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1418
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1419
|
+
});
|
|
1266
1420
|
|
|
1267
1421
|
expect(result.length).toBe(1);
|
|
1268
1422
|
expect(result[0].content.length).toBe(1);
|
|
@@ -1271,6 +1425,7 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1271
1425
|
test("skipped in minimal mode", async () => {
|
|
1272
1426
|
seedNowScratchpad("Current focus: fix the bug");
|
|
1273
1427
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1428
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1274
1429
|
mode: "minimal",
|
|
1275
1430
|
});
|
|
1276
1431
|
|
|
@@ -1694,6 +1849,8 @@ describe("buildUnifiedTurnContextBlock", () => {
|
|
|
1694
1849
|
// ---------------------------------------------------------------------------
|
|
1695
1850
|
|
|
1696
1851
|
describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
1852
|
+
afterEach(clearConversations);
|
|
1853
|
+
|
|
1697
1854
|
const baseMessages: Message[] = [
|
|
1698
1855
|
{
|
|
1699
1856
|
role: "user",
|
|
@@ -1701,12 +1858,42 @@ describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
|
1701
1858
|
},
|
|
1702
1859
|
];
|
|
1703
1860
|
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1861
|
+
const sampleOptions = {
|
|
1862
|
+
interfaceName: "macos",
|
|
1863
|
+
};
|
|
1864
|
+
// The block reflects the options-bag interface and the `current_time` the
|
|
1865
|
+
// injector computes live (pinned to `FIXED_TURN_TIMESTAMP` for this suite).
|
|
1866
|
+
const sampleBlock = buildUnifiedTurnContextBlock({
|
|
1867
|
+
timestamp: FIXED_TURN_TIMESTAMP,
|
|
1868
|
+
...sampleOptions,
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
// Seed the live fallback conversation with the per-turn temporal snapshot
|
|
1872
|
+
// (its presence gates the `<turn_context>` block) and the turn interface
|
|
1873
|
+
// context (so the injector sources the interface label to match
|
|
1874
|
+
// `sampleOptions.interfaceName`).
|
|
1875
|
+
function seedTemporalSnapshot(): void {
|
|
1876
|
+
setConversation("runtime-assembly-fallback", {
|
|
1877
|
+
conversationId: "runtime-assembly-fallback",
|
|
1878
|
+
workingDir: "/sandbox",
|
|
1879
|
+
workspaceTopLevelContext: "",
|
|
1880
|
+
workspaceTopLevelDirty: false,
|
|
1881
|
+
surfaceState: new Map(),
|
|
1882
|
+
currentTurnTemporalSnapshot: {
|
|
1883
|
+
clientTimezone: null,
|
|
1884
|
+
},
|
|
1885
|
+
currentTurnInterfaceContext: {
|
|
1886
|
+
userMessageInterface: "macos",
|
|
1887
|
+
assistantMessageInterface: "macos",
|
|
1888
|
+
},
|
|
1889
|
+
} as never);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
test("injects the turn-context block when the inputs are provided", async () => {
|
|
1893
|
+
seedTemporalSnapshot();
|
|
1706
1894
|
|
|
1707
|
-
test("injects unifiedTurnContext when provided", async () => {
|
|
1708
1895
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1709
|
-
|
|
1896
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1710
1897
|
});
|
|
1711
1898
|
|
|
1712
1899
|
expect(result).toHaveLength(1);
|
|
@@ -1720,25 +1907,21 @@ describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
|
1720
1907
|
);
|
|
1721
1908
|
});
|
|
1722
1909
|
|
|
1723
|
-
test("does not inject when
|
|
1910
|
+
test("does not inject when the timestamp snapshot is absent", async () => {
|
|
1724
1911
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1725
|
-
|
|
1912
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1726
1913
|
});
|
|
1727
1914
|
|
|
1728
1915
|
expect(result).toHaveLength(1);
|
|
1729
1916
|
expect(result[0].content).toHaveLength(1);
|
|
1730
1917
|
});
|
|
1731
1918
|
|
|
1732
|
-
test("does not inject when unifiedTurnContext is omitted", async () => {
|
|
1733
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {});
|
|
1734
|
-
|
|
1735
|
-
expect(result).toHaveLength(1);
|
|
1736
|
-
expect(result[0].content).toHaveLength(1);
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
1919
|
test("injected in full mode", async () => {
|
|
1920
|
+
seedTemporalSnapshot();
|
|
1921
|
+
|
|
1740
1922
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1741
|
-
|
|
1923
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1924
|
+
...sampleOptions,
|
|
1742
1925
|
mode: "full",
|
|
1743
1926
|
});
|
|
1744
1927
|
|
|
@@ -1751,8 +1934,11 @@ describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
|
1751
1934
|
});
|
|
1752
1935
|
|
|
1753
1936
|
test("injected in minimal mode (no mode guard)", async () => {
|
|
1937
|
+
seedTemporalSnapshot();
|
|
1938
|
+
|
|
1754
1939
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1755
|
-
|
|
1940
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1941
|
+
...sampleOptions,
|
|
1756
1942
|
mode: "minimal",
|
|
1757
1943
|
});
|
|
1758
1944
|
|
|
@@ -1765,11 +1951,168 @@ describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
|
1765
1951
|
});
|
|
1766
1952
|
});
|
|
1767
1953
|
|
|
1954
|
+
// ---------------------------------------------------------------------------
|
|
1955
|
+
// applyRuntimeInjections self-resolves the config timezones
|
|
1956
|
+
// ---------------------------------------------------------------------------
|
|
1957
|
+
|
|
1958
|
+
describe("applyRuntimeInjections timezone resolution", () => {
|
|
1959
|
+
afterEach(() => {
|
|
1960
|
+
assemblyConfiguredUserTimezone = undefined;
|
|
1961
|
+
assemblyDetectedTimezone = undefined;
|
|
1962
|
+
clearConversations();
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
const baseMessages: Message[] = [
|
|
1966
|
+
{ role: "user", content: [{ type: "text", text: "Hello there" }] },
|
|
1967
|
+
];
|
|
1968
|
+
|
|
1969
|
+
// Seed the live fallback conversation with the per-turn temporal snapshot the
|
|
1970
|
+
// loop freezes after memory retrieval. `applyRuntimeInjections` sources the
|
|
1971
|
+
// client timezone and the long-absence gap from it, so the turn-context block
|
|
1972
|
+
// only emits when this snapshot is present.
|
|
1973
|
+
function seedTemporalSnapshot(
|
|
1974
|
+
clientTimezone: string | null,
|
|
1975
|
+
timeSinceLastMessage: string | null = null,
|
|
1976
|
+
): void {
|
|
1977
|
+
setConversation("runtime-assembly-fallback", {
|
|
1978
|
+
conversationId: "runtime-assembly-fallback",
|
|
1979
|
+
workingDir: "/sandbox",
|
|
1980
|
+
workspaceTopLevelContext: "",
|
|
1981
|
+
workspaceTopLevelDirty: false,
|
|
1982
|
+
surfaceState: new Map(),
|
|
1983
|
+
currentTurnTemporalSnapshot: {
|
|
1984
|
+
clientTimezone,
|
|
1985
|
+
timeSinceLastMessage,
|
|
1986
|
+
},
|
|
1987
|
+
} as never);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function injectedText(result: { messages: Message[] }): string {
|
|
1991
|
+
return result.messages[0].content
|
|
1992
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
1993
|
+
.map((b) => b.text)
|
|
1994
|
+
.join("\n");
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
test("sources the configured and detected timezones from config, not the options bag", async () => {
|
|
1998
|
+
/**
|
|
1999
|
+
* Verifies the re-homed configured user timezone and detected-timezone
|
|
2000
|
+
* fallback are read from `config.ui` inside `applyRuntimeInjections`
|
|
2001
|
+
* rather than threaded through the options bag. The detected timezone is
|
|
2002
|
+
* the device-timezone fallback used when the client reports none.
|
|
2003
|
+
*/
|
|
2004
|
+
// GIVEN the guardian has configured a user timezone in config
|
|
2005
|
+
assemblyConfiguredUserTimezone = "America/New_York";
|
|
2006
|
+
|
|
2007
|
+
// AND a different server-detected timezone is configured
|
|
2008
|
+
assemblyDetectedTimezone = "America/Los_Angeles";
|
|
2009
|
+
|
|
2010
|
+
// AND the turn's frozen snapshot carries no client timezone (so the
|
|
2011
|
+
// detected timezone is the device-timezone fallback)
|
|
2012
|
+
seedTemporalSnapshot(null);
|
|
2013
|
+
|
|
2014
|
+
// WHEN a turn is assembled
|
|
2015
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2016
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
// THEN the injector surfaces the mismatch using the config-sourced values
|
|
2020
|
+
const injected = injectedText(result);
|
|
2021
|
+
expect(injected).toContain("configured_user_timezone: America/New_York");
|
|
2022
|
+
expect(injected).toContain("client_device_timezone: America/Los_Angeles");
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
test("sources clientTimezone from the conversation's frozen turn snapshot", async () => {
|
|
2026
|
+
/**
|
|
2027
|
+
* Verifies the per-turn client timezone is taken from the conversation's
|
|
2028
|
+
* frozen `currentTurnTemporalSnapshot` and takes precedence over the
|
|
2029
|
+
* config-sourced detected-timezone fallback.
|
|
2030
|
+
*/
|
|
2031
|
+
// GIVEN the configured user timezone and a detected fallback in config
|
|
2032
|
+
assemblyConfiguredUserTimezone = "America/New_York";
|
|
2033
|
+
assemblyDetectedTimezone = "America/Denver";
|
|
2034
|
+
|
|
2035
|
+
// AND the turn's frozen snapshot carries a client timezone
|
|
2036
|
+
seedTemporalSnapshot("America/Los_Angeles");
|
|
2037
|
+
|
|
2038
|
+
// WHEN a turn is assembled
|
|
2039
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2040
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
// THEN the injector surfaces the mismatch using the snapshot's client value
|
|
2044
|
+
const injected = injectedText(result);
|
|
2045
|
+
expect(injected).toContain("configured_user_timezone: America/New_York");
|
|
2046
|
+
expect(injected).toContain("client_device_timezone: America/Los_Angeles");
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
test("omits the mismatch lines when config and client timezone agree", async () => {
|
|
2050
|
+
/**
|
|
2051
|
+
* Verifies no mismatch affordance is emitted when the config-sourced
|
|
2052
|
+
* configured user timezone matches the snapshot's client timezone.
|
|
2053
|
+
*/
|
|
2054
|
+
// GIVEN the configured user timezone matches the client timezone
|
|
2055
|
+
assemblyConfiguredUserTimezone = "America/New_York";
|
|
2056
|
+
|
|
2057
|
+
// AND the turn's frozen snapshot carries the matching client timezone
|
|
2058
|
+
seedTemporalSnapshot("America/New_York");
|
|
2059
|
+
|
|
2060
|
+
// WHEN a turn is assembled
|
|
2061
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2062
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
// THEN no timezone-mismatch lines are injected
|
|
2066
|
+
const injected = injectedText(result);
|
|
2067
|
+
expect(injected).not.toContain("configured_user_timezone:");
|
|
2068
|
+
expect(injected).not.toContain("client_device_timezone:");
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
test("sources time_since_last_message from the conversation's frozen turn snapshot", async () => {
|
|
2072
|
+
/**
|
|
2073
|
+
* Verifies the long-absence gap is taken from the conversation's frozen
|
|
2074
|
+
* `currentTurnTemporalSnapshot` rather than threaded through the options
|
|
2075
|
+
* bag, so every post-compaction re-injection reuses the turn-start value.
|
|
2076
|
+
*/
|
|
2077
|
+
// GIVEN the turn's frozen snapshot carries a long-absence gap
|
|
2078
|
+
seedTemporalSnapshot(null, "2d ago");
|
|
2079
|
+
|
|
2080
|
+
// WHEN a turn is assembled without the gap in the options bag
|
|
2081
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2082
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
// THEN the injector surfaces the gap from the snapshot
|
|
2086
|
+
const injected = injectedText(result);
|
|
2087
|
+
expect(injected).toContain("time_since_last_message: 2d ago");
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
test("omits time_since_last_message when the snapshot carries no gap", async () => {
|
|
2091
|
+
/**
|
|
2092
|
+
* Verifies no `time_since_last_message` line is emitted when the frozen
|
|
2093
|
+
* snapshot's gap is null (the normal back-and-forth case).
|
|
2094
|
+
*/
|
|
2095
|
+
// GIVEN the turn's frozen snapshot carries no long-absence gap
|
|
2096
|
+
seedTemporalSnapshot(null, null);
|
|
2097
|
+
|
|
2098
|
+
// WHEN a turn is assembled
|
|
2099
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2100
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
// THEN no gap line is injected
|
|
2104
|
+
const injected = injectedText(result);
|
|
2105
|
+
expect(injected).not.toContain("time_since_last_message");
|
|
2106
|
+
});
|
|
2107
|
+
});
|
|
2108
|
+
|
|
1768
2109
|
// ---------------------------------------------------------------------------
|
|
1769
2110
|
// applyRuntimeInjections blocks.unifiedTurnContext
|
|
1770
2111
|
// ---------------------------------------------------------------------------
|
|
1771
2112
|
|
|
1772
2113
|
describe("applyRuntimeInjections blocks.unifiedTurnContext", () => {
|
|
2114
|
+
afterEach(clearConversations);
|
|
2115
|
+
|
|
1773
2116
|
const userTailMessages: Message[] = [
|
|
1774
2117
|
{
|
|
1775
2118
|
role: "user",
|
|
@@ -1777,18 +2120,50 @@ describe("applyRuntimeInjections blocks.unifiedTurnContext", () => {
|
|
|
1777
2120
|
},
|
|
1778
2121
|
];
|
|
1779
2122
|
|
|
1780
|
-
const
|
|
1781
|
-
|
|
2123
|
+
const sampleOptions = {
|
|
2124
|
+
interfaceName: "macos",
|
|
2125
|
+
};
|
|
2126
|
+
// The block reflects the options-bag interface and the `current_time` the
|
|
2127
|
+
// injector computes live (pinned to `FIXED_TURN_TIMESTAMP` for this suite).
|
|
2128
|
+
const sampleBlock = buildUnifiedTurnContextBlock({
|
|
2129
|
+
timestamp: FIXED_TURN_TIMESTAMP,
|
|
2130
|
+
...sampleOptions,
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
// Seed the live fallback conversation with the per-turn temporal snapshot
|
|
2134
|
+
// (its presence gates the `<turn_context>` block) and the turn interface
|
|
2135
|
+
// context (so the injector sources the interface label to match
|
|
2136
|
+
// `sampleOptions.interfaceName`).
|
|
2137
|
+
function seedTemporalSnapshot(): void {
|
|
2138
|
+
setConversation("runtime-assembly-fallback", {
|
|
2139
|
+
conversationId: "runtime-assembly-fallback",
|
|
2140
|
+
workingDir: "/sandbox",
|
|
2141
|
+
workspaceTopLevelContext: "",
|
|
2142
|
+
workspaceTopLevelDirty: false,
|
|
2143
|
+
surfaceState: new Map(),
|
|
2144
|
+
currentTurnTemporalSnapshot: {
|
|
2145
|
+
clientTimezone: null,
|
|
2146
|
+
},
|
|
2147
|
+
currentTurnInterfaceContext: {
|
|
2148
|
+
userMessageInterface: "macos",
|
|
2149
|
+
assistantMessageInterface: "macos",
|
|
2150
|
+
},
|
|
2151
|
+
} as never);
|
|
2152
|
+
}
|
|
1782
2153
|
|
|
1783
2154
|
test("captures unifiedTurnContext when tail is a user message", async () => {
|
|
2155
|
+
seedTemporalSnapshot();
|
|
2156
|
+
|
|
1784
2157
|
const result = await applyRuntimeInjections(userTailMessages, {
|
|
1785
|
-
|
|
2158
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1786
2159
|
});
|
|
1787
2160
|
|
|
1788
2161
|
expect(result.blocks.unifiedTurnContext).toBe(sampleBlock);
|
|
1789
2162
|
});
|
|
1790
2163
|
|
|
1791
2164
|
test("does not capture when tail is not a user message", async () => {
|
|
2165
|
+
seedTemporalSnapshot();
|
|
2166
|
+
|
|
1792
2167
|
const assistantTailMessages: Message[] = [
|
|
1793
2168
|
{
|
|
1794
2169
|
role: "user",
|
|
@@ -1801,14 +2176,16 @@ describe("applyRuntimeInjections blocks.unifiedTurnContext", () => {
|
|
|
1801
2176
|
];
|
|
1802
2177
|
|
|
1803
2178
|
const result = await applyRuntimeInjections(assistantTailMessages, {
|
|
1804
|
-
|
|
2179
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1805
2180
|
});
|
|
1806
2181
|
|
|
1807
2182
|
expect(result.blocks.unifiedTurnContext).toBeUndefined();
|
|
1808
2183
|
});
|
|
1809
2184
|
|
|
1810
2185
|
test("does not capture when unifiedTurnContext option is absent", async () => {
|
|
1811
|
-
const result = await applyRuntimeInjections(userTailMessages, {
|
|
2186
|
+
const result = await applyRuntimeInjections(userTailMessages, {
|
|
2187
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2188
|
+
});
|
|
1812
2189
|
|
|
1813
2190
|
expect(result.blocks.unifiedTurnContext).toBeUndefined();
|
|
1814
2191
|
});
|
|
@@ -1844,6 +2221,35 @@ function makeSubagentState(
|
|
|
1844
2221
|
};
|
|
1845
2222
|
}
|
|
1846
2223
|
|
|
2224
|
+
// `applyRuntimeInjections` self-resolves the `<active_subagents>` block from the
|
|
2225
|
+
// global subagent manager keyed by the conversation, so tests seed children
|
|
2226
|
+
// directly into the manager's private maps (mirroring the production source)
|
|
2227
|
+
// rather than threading a pre-built block through options.
|
|
2228
|
+
interface SubagentManagerTestInternals {
|
|
2229
|
+
subagents: Map<string, { state: SubagentState }>;
|
|
2230
|
+
parentToChildren: Map<string, Set<string>>;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function seedSubagentChild(
|
|
2234
|
+
parentConversationId: string,
|
|
2235
|
+
state: SubagentState,
|
|
2236
|
+
): void {
|
|
2237
|
+
const internals =
|
|
2238
|
+
getSubagentManager() as unknown as SubagentManagerTestInternals;
|
|
2239
|
+
internals.subagents.set(state.config.id, { state });
|
|
2240
|
+
const ids =
|
|
2241
|
+
internals.parentToChildren.get(parentConversationId) ?? new Set<string>();
|
|
2242
|
+
ids.add(state.config.id);
|
|
2243
|
+
internals.parentToChildren.set(parentConversationId, ids);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function clearSeededSubagents(): void {
|
|
2247
|
+
const internals =
|
|
2248
|
+
getSubagentManager() as unknown as SubagentManagerTestInternals;
|
|
2249
|
+
internals.subagents.clear();
|
|
2250
|
+
internals.parentToChildren.clear();
|
|
2251
|
+
}
|
|
2252
|
+
|
|
1847
2253
|
describe("buildSubagentStatusBlock", () => {
|
|
1848
2254
|
test("returns null for empty children array", () => {
|
|
1849
2255
|
expect(buildSubagentStatusBlock([])).toBeNull();
|
|
@@ -1921,25 +2327,78 @@ describe("applyRuntimeInjections — subagent status", () => {
|
|
|
1921
2327
|
content: [{ type: "text", text: "user message" }],
|
|
1922
2328
|
};
|
|
1923
2329
|
|
|
1924
|
-
|
|
2330
|
+
afterEach(() => {
|
|
2331
|
+
clearSeededSubagents();
|
|
2332
|
+
clearConversations();
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
test("includes self-resolved subagent status in full mode", async () => {
|
|
2336
|
+
// GIVEN the (fallback) parent conversation has one running child subagent
|
|
2337
|
+
seedSubagentChild(
|
|
2338
|
+
"runtime-assembly-fallback",
|
|
2339
|
+
makeSubagentState({ id: "child-1", label: "worker", status: "running" }),
|
|
2340
|
+
);
|
|
2341
|
+
|
|
2342
|
+
// WHEN runtime injections run in full mode
|
|
1925
2343
|
const { messages: result } = await applyRuntimeInjections([userMsg], {
|
|
1926
|
-
|
|
1927
|
-
"<active_subagents>\n- [running] test\n</active_subagents>",
|
|
2344
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1928
2345
|
mode: "full",
|
|
1929
2346
|
});
|
|
2347
|
+
|
|
2348
|
+
// THEN the tail user message carries the `<active_subagents>` block the
|
|
2349
|
+
// injector self-resolved from the subagent manager
|
|
1930
2350
|
const tail = result[result.length - 1];
|
|
1931
2351
|
const texts = tail.content
|
|
1932
2352
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
1933
2353
|
.map((b) => b.text);
|
|
1934
2354
|
expect(texts.some((t) => t.includes("<active_subagents>"))).toBe(true);
|
|
2355
|
+
expect(texts.some((t) => t.includes('"worker"'))).toBe(true);
|
|
1935
2356
|
});
|
|
1936
2357
|
|
|
1937
2358
|
test("skips subagent status in minimal mode", async () => {
|
|
2359
|
+
// GIVEN the (fallback) parent conversation has one running child subagent
|
|
2360
|
+
seedSubagentChild(
|
|
2361
|
+
"runtime-assembly-fallback",
|
|
2362
|
+
makeSubagentState({ id: "child-1", label: "worker", status: "running" }),
|
|
2363
|
+
);
|
|
2364
|
+
|
|
2365
|
+
// WHEN runtime injections run in minimal mode
|
|
1938
2366
|
const { messages: result } = await applyRuntimeInjections([userMsg], {
|
|
1939
|
-
|
|
1940
|
-
"<active_subagents>\n- [running] test\n</active_subagents>",
|
|
2367
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1941
2368
|
mode: "minimal",
|
|
1942
2369
|
});
|
|
2370
|
+
|
|
2371
|
+
// THEN the subagent status block is gated out
|
|
2372
|
+
const tail = result[result.length - 1];
|
|
2373
|
+
const texts = tail.content
|
|
2374
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
2375
|
+
.map((b) => b.text);
|
|
2376
|
+
expect(texts.some((t) => t.includes("<active_subagents>"))).toBe(false);
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
test("skips subagent status when the conversation is itself a subagent", async () => {
|
|
2380
|
+
// GIVEN the live conversation is itself a subagent (no nesting)
|
|
2381
|
+
setConversation("runtime-assembly-fallback", {
|
|
2382
|
+
conversationId: "runtime-assembly-fallback",
|
|
2383
|
+
workingDir: "/sandbox",
|
|
2384
|
+
workspaceTopLevelContext: "",
|
|
2385
|
+
workspaceTopLevelDirty: false,
|
|
2386
|
+
surfaceState: new Map(),
|
|
2387
|
+
isSubagent: true,
|
|
2388
|
+
} as never);
|
|
2389
|
+
// AND a child is registered under its id
|
|
2390
|
+
seedSubagentChild(
|
|
2391
|
+
"runtime-assembly-fallback",
|
|
2392
|
+
makeSubagentState({ id: "child-1", label: "worker", status: "running" }),
|
|
2393
|
+
);
|
|
2394
|
+
|
|
2395
|
+
// WHEN runtime injections run in full mode
|
|
2396
|
+
const { messages: result } = await applyRuntimeInjections([userMsg], {
|
|
2397
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2398
|
+
mode: "full",
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
// THEN no `<active_subagents>` block is emitted
|
|
1943
2402
|
const tail = result[result.length - 1];
|
|
1944
2403
|
const texts = tail.content
|
|
1945
2404
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
@@ -2012,6 +2471,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
|
|
|
2012
2471
|
|
|
2013
2472
|
function makePkbOptions(overrides: Record<string, unknown> = {}) {
|
|
2014
2473
|
return {
|
|
2474
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2015
2475
|
...overrides,
|
|
2016
2476
|
};
|
|
2017
2477
|
}
|
|
@@ -2298,7 +2758,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
|
|
|
2298
2758
|
// filtered out.
|
|
2299
2759
|
const { messages: initialResult } = await applyRuntimeInjections(
|
|
2300
2760
|
preCompactionMessages,
|
|
2301
|
-
{},
|
|
2761
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2302
2762
|
);
|
|
2303
2763
|
// Unwrap the injected reminder from the last user message.
|
|
2304
2764
|
const initialTexts = extractTexts(initialResult);
|
|
@@ -2332,7 +2792,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
|
|
|
2332
2792
|
// should be filtered, and beta (no longer "in context") should appear.
|
|
2333
2793
|
const { messages: rebuiltResult } = await applyRuntimeInjections(
|
|
2334
2794
|
postCompactionMessages,
|
|
2335
|
-
{},
|
|
2795
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2336
2796
|
);
|
|
2337
2797
|
const rebuiltTexts = extractTexts(rebuiltResult);
|
|
2338
2798
|
const rebuiltReminder = rebuiltTexts.find(
|
|
@@ -2352,6 +2812,19 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
|
|
|
2352
2812
|
// ---------------------------------------------------------------------------
|
|
2353
2813
|
|
|
2354
2814
|
describe("Slack channel chronological rendering — multi-thread", () => {
|
|
2815
|
+
afterEach(() => {
|
|
2816
|
+
clearConversations();
|
|
2817
|
+
// The focus-block tests persist rows under the runtime-assembly fallback
|
|
2818
|
+
// conversation; clear them so later tests resolve against an empty DB.
|
|
2819
|
+
const db = getDb();
|
|
2820
|
+
db.delete(messages)
|
|
2821
|
+
.where(eq(messages.conversationId, "runtime-assembly-fallback"))
|
|
2822
|
+
.run();
|
|
2823
|
+
db.delete(conversations)
|
|
2824
|
+
.where(eq(conversations.id, "runtime-assembly-fallback"))
|
|
2825
|
+
.run();
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2355
2828
|
// Slack ts values are seconds-since-epoch with microsecond precision.
|
|
2356
2829
|
// Pick a few stable anchors so thread aliases (sha-derived) stay
|
|
2357
2830
|
// predictable across the scenarios.
|
|
@@ -2395,6 +2868,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2395
2868
|
content: JSON.stringify([{ type: "text", text: opts.text }]),
|
|
2396
2869
|
createdAt: opts.createdAt,
|
|
2397
2870
|
metadata: Object.keys(outer).length > 0 ? JSON.stringify(outer) : null,
|
|
2871
|
+
clientMessageId: null,
|
|
2398
2872
|
};
|
|
2399
2873
|
}
|
|
2400
2874
|
|
|
@@ -2413,6 +2887,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2413
2887
|
content: JSON.stringify([{ type: "text", text: opts.text }]),
|
|
2414
2888
|
createdAt: opts.createdAt,
|
|
2415
2889
|
metadata: Object.keys(outer).length > 0 ? JSON.stringify(outer) : null,
|
|
2890
|
+
clientMessageId: null,
|
|
2416
2891
|
};
|
|
2417
2892
|
}
|
|
2418
2893
|
|
|
@@ -2428,18 +2903,16 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2428
2903
|
supportsVoiceInput: false,
|
|
2429
2904
|
chatType: "channel",
|
|
2430
2905
|
};
|
|
2431
|
-
const slackChronologicalMessages = loadSlackChronologicalMessages(
|
|
2432
|
-
"conv-1",
|
|
2433
|
-
slackChannelCaps,
|
|
2434
|
-
{ loader: () => rows, trustClass: "guardian" },
|
|
2435
|
-
);
|
|
2436
2906
|
const lastUserMessage: Message = {
|
|
2437
2907
|
role: "user",
|
|
2438
2908
|
content: [{ type: "text", text: "current turn" }],
|
|
2439
2909
|
};
|
|
2910
|
+
// Persist the rows + live conversation so `applyRuntimeInjections`
|
|
2911
|
+
// self-resolves the chronological transcript from conversation state,
|
|
2912
|
+
// exactly as production does.
|
|
2913
|
+
seedSlackChannelConversationWithRows(slackChannelCaps, rows);
|
|
2440
2914
|
const { messages } = await applyRuntimeInjections([lastUserMessage], {
|
|
2441
|
-
|
|
2442
|
-
slackChronologicalMessages,
|
|
2915
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2443
2916
|
});
|
|
2444
2917
|
return messages;
|
|
2445
2918
|
}
|
|
@@ -2726,24 +3199,15 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2726
3199
|
role: "user",
|
|
2727
3200
|
content: [{ type: "text", text: "vellum question" }],
|
|
2728
3201
|
};
|
|
3202
|
+
seedChannelCapabilitiesConversation({
|
|
3203
|
+
channel: "vellum",
|
|
3204
|
+
dashboardCapable: true,
|
|
3205
|
+
supportsDynamicUi: true,
|
|
3206
|
+
supportsVoiceInput: true,
|
|
3207
|
+
});
|
|
2729
3208
|
const { messages: result } = await applyRuntimeInjections(
|
|
2730
3209
|
[lastUserMessage],
|
|
2731
|
-
{
|
|
2732
|
-
channelCapabilities: {
|
|
2733
|
-
channel: "vellum",
|
|
2734
|
-
dashboardCapable: true,
|
|
2735
|
-
supportsDynamicUi: true,
|
|
2736
|
-
supportsVoiceInput: true,
|
|
2737
|
-
},
|
|
2738
|
-
// Even if we accidentally pass a chronological transcript, the
|
|
2739
|
-
// branch must be a no-op for non-slack channels.
|
|
2740
|
-
slackChronologicalMessages: [
|
|
2741
|
-
{
|
|
2742
|
-
role: "user",
|
|
2743
|
-
content: [{ type: "text", text: "should not appear" }],
|
|
2744
|
-
},
|
|
2745
|
-
],
|
|
2746
|
-
},
|
|
3210
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2747
3211
|
);
|
|
2748
3212
|
expect(result.length).toBe(1);
|
|
2749
3213
|
const allText = result[0].content
|
|
@@ -2751,7 +3215,6 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2751
3215
|
.map((b) => b.text)
|
|
2752
3216
|
.join("\n");
|
|
2753
3217
|
expect(allText).toContain("vellum question");
|
|
2754
|
-
expect(allText).not.toContain("should not appear");
|
|
2755
3218
|
});
|
|
2756
3219
|
|
|
2757
3220
|
// ── DMs (chatType === "im") use chronological rendering ────────────────
|
|
@@ -2764,36 +3227,38 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2764
3227
|
role: "user",
|
|
2765
3228
|
content: [{ type: "text", text: "DM question" }],
|
|
2766
3229
|
};
|
|
2767
|
-
const
|
|
2768
|
-
|
|
3230
|
+
const rows: MessageRow[] = [
|
|
3231
|
+
userRow({
|
|
3232
|
+
id: "dm-1",
|
|
3233
|
+
createdAt: 1700000000_000,
|
|
3234
|
+
text: "earlier DM line",
|
|
3235
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3236
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3237
|
+
}),
|
|
3238
|
+
assistantRow({
|
|
3239
|
+
id: "dm-2",
|
|
3240
|
+
createdAt: 1700000005_000,
|
|
3241
|
+
text: "prior reply",
|
|
3242
|
+
slackMeta: buildSlackMeta({ channelTs: T0_REPLY1 }),
|
|
3243
|
+
}),
|
|
3244
|
+
];
|
|
3245
|
+
seedSlackChannelConversationWithRows(
|
|
2769
3246
|
{
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
chatType: "im",
|
|
2776
|
-
},
|
|
2777
|
-
slackChronologicalMessages: [
|
|
2778
|
-
{
|
|
2779
|
-
role: "user",
|
|
2780
|
-
content: [
|
|
2781
|
-
{
|
|
2782
|
-
type: "text",
|
|
2783
|
-
text: "[11/14/23 14:25 @alice]: earlier DM line",
|
|
2784
|
-
},
|
|
2785
|
-
],
|
|
2786
|
-
},
|
|
2787
|
-
{
|
|
2788
|
-
role: "assistant",
|
|
2789
|
-
content: [{ type: "text", text: "prior reply" }],
|
|
2790
|
-
},
|
|
2791
|
-
],
|
|
3247
|
+
channel: "slack",
|
|
3248
|
+
dashboardCapable: false,
|
|
3249
|
+
supportsDynamicUi: false,
|
|
3250
|
+
supportsVoiceInput: false,
|
|
3251
|
+
chatType: "im",
|
|
2792
3252
|
},
|
|
3253
|
+
rows,
|
|
2793
3254
|
);
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
3255
|
+
const { messages: result } = await applyRuntimeInjections(
|
|
3256
|
+
[lastUserMessage],
|
|
3257
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3258
|
+
);
|
|
3259
|
+
// The self-resolved chronological transcript REPLACES the default
|
|
3260
|
+
// runMessages, so the inbound `DM question` text does not appear — only
|
|
3261
|
+
// the rendered transcript lines do (plus any non-Slack injections).
|
|
2797
3262
|
const allText = result
|
|
2798
3263
|
.flatMap((m) => m.content)
|
|
2799
3264
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
@@ -2804,6 +3269,62 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2804
3269
|
expect(allText).not.toContain("DM question");
|
|
2805
3270
|
});
|
|
2806
3271
|
|
|
3272
|
+
// ── Compaction boundary scopes the self-resolved transcript ──────────
|
|
3273
|
+
// The transcript self-resolves from persisted rows scoped by the
|
|
3274
|
+
// conversation's Slack compaction boundary. Rows at or before the
|
|
3275
|
+
// persisted watermark were folded into the summary, so a fresh load
|
|
3276
|
+
// excludes them and surfaces only the post-watermark tail — returning
|
|
3277
|
+
// the compacted view rather than resurrecting folded history.
|
|
3278
|
+
test("self-resolved transcript respects the Slack compaction watermark", async () => {
|
|
3279
|
+
const slackCaps: ChannelCapabilities = {
|
|
3280
|
+
channel: "slack",
|
|
3281
|
+
dashboardCapable: false,
|
|
3282
|
+
supportsDynamicUi: false,
|
|
3283
|
+
supportsVoiceInput: false,
|
|
3284
|
+
chatType: "channel",
|
|
3285
|
+
};
|
|
3286
|
+
const rows: MessageRow[] = [
|
|
3287
|
+
userRow({
|
|
3288
|
+
id: "folded",
|
|
3289
|
+
createdAt: 1700000000_000,
|
|
3290
|
+
text: "folded pre-compaction line",
|
|
3291
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3292
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3293
|
+
}),
|
|
3294
|
+
userRow({
|
|
3295
|
+
id: "fresh",
|
|
3296
|
+
createdAt: 1700000030_000,
|
|
3297
|
+
text: "fresh post-compaction line",
|
|
3298
|
+
slackMeta: buildSlackMeta({ channelTs: T2, displayName: "bob" }),
|
|
3299
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3300
|
+
}),
|
|
3301
|
+
];
|
|
3302
|
+
// A persisted watermark at T0 folds the first row into the summary.
|
|
3303
|
+
seedSlackChannelConversationWithRows(slackCaps, rows, {
|
|
3304
|
+
slackContextCompactionWatermarkTs: T0,
|
|
3305
|
+
contextCompactedMessageCount: 1,
|
|
3306
|
+
});
|
|
3307
|
+
const { messages: result } = await applyRuntimeInjections(
|
|
3308
|
+
[
|
|
3309
|
+
{
|
|
3310
|
+
role: "user",
|
|
3311
|
+
content: [{ type: "text", text: "post-compaction question" }],
|
|
3312
|
+
},
|
|
3313
|
+
],
|
|
3314
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3315
|
+
);
|
|
3316
|
+
const allText = result
|
|
3317
|
+
.flatMap((m) => m.content)
|
|
3318
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
3319
|
+
.map((b) => b.text)
|
|
3320
|
+
.join("\n");
|
|
3321
|
+
// The folded row stays in the summary; only the post-watermark tail is
|
|
3322
|
+
// surfaced, and it replaces the default runMessages.
|
|
3323
|
+
expect(allText).toContain("fresh post-compaction line");
|
|
3324
|
+
expect(allText).not.toContain("folded pre-compaction line");
|
|
3325
|
+
expect(allText).not.toContain("post-compaction question");
|
|
3326
|
+
});
|
|
3327
|
+
|
|
2807
3328
|
// ── Memory-injection carry-through on slack replacement ──────────────
|
|
2808
3329
|
// `graphMemory.prepareMemory` prepends `<memory __injected>` (and
|
|
2809
3330
|
// optional memory-image groups) to the last user message BEFORE the
|
|
@@ -2828,21 +3349,23 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2828
3349
|
type: "text",
|
|
2829
3350
|
text: "<memory __injected>\nrecalled fact about the user\n</memory>",
|
|
2830
3351
|
},
|
|
2831
|
-
{ type: "text", text: "
|
|
3352
|
+
{ type: "text", text: "original turn text" },
|
|
2832
3353
|
],
|
|
2833
3354
|
},
|
|
2834
3355
|
];
|
|
3356
|
+
const rows: MessageRow[] = [
|
|
3357
|
+
userRow({
|
|
3358
|
+
id: "mem-1",
|
|
3359
|
+
createdAt: 1700000000_000,
|
|
3360
|
+
text: "recalled conversation line",
|
|
3361
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3362
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3363
|
+
}),
|
|
3364
|
+
];
|
|
3365
|
+
seedSlackChannelConversationWithRows(slackCaps, rows);
|
|
2835
3366
|
const { messages: result } = await applyRuntimeInjections(
|
|
2836
3367
|
runMessagesWithMemory,
|
|
2837
|
-
{
|
|
2838
|
-
channelCapabilities: slackCaps,
|
|
2839
|
-
slackChronologicalMessages: [
|
|
2840
|
-
{
|
|
2841
|
-
role: "user",
|
|
2842
|
-
content: [{ type: "text", text: "[19:55 alice]: hello there" }],
|
|
2843
|
-
},
|
|
2844
|
-
],
|
|
2845
|
-
},
|
|
3368
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2846
3369
|
);
|
|
2847
3370
|
const tail = result[result.length - 1];
|
|
2848
3371
|
expect(tail.role).toBe("user");
|
|
@@ -2852,15 +3375,15 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2852
3375
|
.join("\n");
|
|
2853
3376
|
expect(allText).toContain("<memory __injected>");
|
|
2854
3377
|
expect(allText).toContain("recalled fact about the user");
|
|
2855
|
-
expect(allText).toContain("
|
|
3378
|
+
expect(allText).toContain("recalled conversation line");
|
|
2856
3379
|
// Memory block must appear before the Slack transcript tail so the
|
|
2857
3380
|
// model sees recalled context ahead of the conversation view.
|
|
2858
3381
|
const memoryIdx = allText.indexOf("<memory __injected>");
|
|
2859
|
-
const transcriptIdx = allText.indexOf("
|
|
3382
|
+
const transcriptIdx = allText.indexOf("recalled conversation line");
|
|
2860
3383
|
expect(memoryIdx).toBeLessThan(transcriptIdx);
|
|
2861
|
-
// The pre-replacement
|
|
2862
|
-
//
|
|
2863
|
-
expect(allText.
|
|
3384
|
+
// The pre-replacement inbound turn text must NOT leak through — only the
|
|
3385
|
+
// carried memory prefix + the self-resolved transcript tail remain.
|
|
3386
|
+
expect(allText).not.toContain("original turn text");
|
|
2864
3387
|
});
|
|
2865
3388
|
|
|
2866
3389
|
test("slack replacement preserves memory-image groups + text block", async () => {
|
|
@@ -2896,17 +3419,19 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2896
3419
|
],
|
|
2897
3420
|
},
|
|
2898
3421
|
];
|
|
3422
|
+
const rows: MessageRow[] = [
|
|
3423
|
+
userRow({
|
|
3424
|
+
id: "mem-img-1",
|
|
3425
|
+
createdAt: 1700000000_000,
|
|
3426
|
+
text: "recalled conversation line",
|
|
3427
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3428
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3429
|
+
}),
|
|
3430
|
+
];
|
|
3431
|
+
seedSlackChannelConversationWithRows(slackCaps, rows);
|
|
2899
3432
|
const { messages: result } = await applyRuntimeInjections(
|
|
2900
3433
|
runMessagesWithMemory,
|
|
2901
|
-
{
|
|
2902
|
-
channelCapabilities: slackCaps,
|
|
2903
|
-
slackChronologicalMessages: [
|
|
2904
|
-
{
|
|
2905
|
-
role: "user",
|
|
2906
|
-
content: [{ type: "text", text: "[19:55 alice]: transcript line" }],
|
|
2907
|
-
},
|
|
2908
|
-
],
|
|
2909
|
-
},
|
|
3434
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2910
3435
|
);
|
|
2911
3436
|
const tail = result[result.length - 1];
|
|
2912
3437
|
expect(tail.role).toBe("user");
|
|
@@ -2921,7 +3446,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2921
3446
|
expect(allText).toContain("<memory_image __injected>");
|
|
2922
3447
|
expect(allText).toContain("</memory_image>");
|
|
2923
3448
|
expect(allText).toContain("<memory __injected>");
|
|
2924
|
-
expect(allText).toContain("
|
|
3449
|
+
expect(allText).toContain("recalled conversation line");
|
|
2925
3450
|
// The original turn text (before the Slack replacement) must NOT
|
|
2926
3451
|
// leak through — only the memory prefix + transcript tail are kept.
|
|
2927
3452
|
expect(allText).not.toContain("original turn text");
|
|
@@ -2935,17 +3460,19 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2935
3460
|
supportsVoiceInput: false,
|
|
2936
3461
|
chatType: "im",
|
|
2937
3462
|
};
|
|
3463
|
+
const rows: MessageRow[] = [
|
|
3464
|
+
userRow({
|
|
3465
|
+
id: "no-prefix-1",
|
|
3466
|
+
createdAt: 1700000000_000,
|
|
3467
|
+
text: "only transcript line",
|
|
3468
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3469
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3470
|
+
}),
|
|
3471
|
+
];
|
|
3472
|
+
seedSlackChannelConversationWithRows(slackCaps, rows);
|
|
2938
3473
|
const { messages: result } = await applyRuntimeInjections(
|
|
2939
3474
|
[{ role: "user", content: [{ type: "text", text: "inbound" }] }],
|
|
2940
|
-
{
|
|
2941
|
-
channelCapabilities: slackCaps,
|
|
2942
|
-
slackChronologicalMessages: [
|
|
2943
|
-
{
|
|
2944
|
-
role: "user",
|
|
2945
|
-
content: [{ type: "text", text: "[19:55 alice]: only transcript" }],
|
|
2946
|
-
},
|
|
2947
|
-
],
|
|
2948
|
-
},
|
|
3475
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2949
3476
|
);
|
|
2950
3477
|
const tail = result[result.length - 1];
|
|
2951
3478
|
const allText = tail.content
|
|
@@ -2953,7 +3480,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2953
3480
|
.map((b) => b.text)
|
|
2954
3481
|
.join("\n");
|
|
2955
3482
|
expect(allText).not.toContain("<memory __injected>");
|
|
2956
|
-
expect(allText).toContain("
|
|
3483
|
+
expect(allText).toContain("only transcript line");
|
|
2957
3484
|
});
|
|
2958
3485
|
|
|
2959
3486
|
// ── transport_hints suppression for slack channels ────────────────────
|
|
@@ -2965,27 +3492,12 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2965
3492
|
supportsVoiceInput: false,
|
|
2966
3493
|
chatType: "channel",
|
|
2967
3494
|
};
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
createdAt: 1700000000_000,
|
|
2972
|
-
text: "Original message",
|
|
2973
|
-
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
2974
|
-
}),
|
|
2975
|
-
];
|
|
2976
|
-
const slackChronologicalMessages = loadSlackChronologicalMessages(
|
|
2977
|
-
"conv-1",
|
|
2978
|
-
slackChannelCaps,
|
|
2979
|
-
{ loader: () => rows, trustClass: "guardian" },
|
|
2980
|
-
);
|
|
2981
|
-
|
|
3495
|
+
seedChannelCapabilitiesConversation(slackChannelCaps, [
|
|
3496
|
+
"thread context: ...",
|
|
3497
|
+
]);
|
|
2982
3498
|
const { messages: result } = await applyRuntimeInjections(
|
|
2983
3499
|
[{ role: "user", content: [{ type: "text", text: "current turn" }] }],
|
|
2984
|
-
{
|
|
2985
|
-
channelCapabilities: slackChannelCaps,
|
|
2986
|
-
slackChronologicalMessages,
|
|
2987
|
-
transportHints: ["thread context: ..."],
|
|
2988
|
-
},
|
|
3500
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2989
3501
|
);
|
|
2990
3502
|
|
|
2991
3503
|
const allText = result
|
|
@@ -3009,12 +3521,10 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3009
3521
|
chatType: "im",
|
|
3010
3522
|
};
|
|
3011
3523
|
|
|
3524
|
+
seedChannelCapabilitiesConversation(slackDmCaps, ["dm context: ..."]);
|
|
3012
3525
|
const { messages: result } = await applyRuntimeInjections(
|
|
3013
3526
|
[{ role: "user", content: [{ type: "text", text: "hi DM" }] }],
|
|
3014
|
-
{
|
|
3015
|
-
channelCapabilities: slackDmCaps,
|
|
3016
|
-
transportHints: ["dm context: ..."],
|
|
3017
|
-
},
|
|
3527
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3018
3528
|
);
|
|
3019
3529
|
|
|
3020
3530
|
const allText = result
|
|
@@ -3028,18 +3538,19 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3028
3538
|
|
|
3029
3539
|
// ── transport_hints kept for non-slack channels ───────────────────────
|
|
3030
3540
|
test("non-slack conversations still receive <transport_hints>", async () => {
|
|
3031
|
-
|
|
3032
|
-
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
3541
|
+
seedChannelCapabilitiesConversation(
|
|
3033
3542
|
{
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
chatType: "private",
|
|
3040
|
-
},
|
|
3041
|
-
transportHints: ["please answer concisely"],
|
|
3543
|
+
channel: "telegram",
|
|
3544
|
+
dashboardCapable: false,
|
|
3545
|
+
supportsDynamicUi: false,
|
|
3546
|
+
supportsVoiceInput: false,
|
|
3547
|
+
chatType: "private",
|
|
3042
3548
|
},
|
|
3549
|
+
["please answer concisely"],
|
|
3550
|
+
);
|
|
3551
|
+
const { messages: result } = await applyRuntimeInjections(
|
|
3552
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
3553
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3043
3554
|
);
|
|
3044
3555
|
const allText = result
|
|
3045
3556
|
.flatMap((m) => m.content)
|
|
@@ -3336,10 +3847,72 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3336
3847
|
// interleaved. The block is non-persisted: replays / re-injections strip
|
|
3337
3848
|
// any prior `<active_thread>` blocks via `RUNTIME_INJECTION_PREFIXES`.
|
|
3338
3849
|
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3850
|
+
interface SeededCompactionState {
|
|
3851
|
+
slackContextCompactionWatermarkTs?: string | null;
|
|
3852
|
+
contextCompactedMessageCount?: number;
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// Persist the Slack rows under the runtime-assembly fallback conversation and
|
|
3856
|
+
// mirror the channel capabilities + trust class + compaction state onto its
|
|
3857
|
+
// live registry entry, so `applyRuntimeInjections` self-resolves the
|
|
3858
|
+
// `<active_thread>` focus block from the conversation's stored rows — the same
|
|
3859
|
+
// way it does in production, where the injector reads the live conversation
|
|
3860
|
+
// rather than receiving a pre-built block.
|
|
3861
|
+
function seedSlackChannelConversationWithRows(
|
|
3862
|
+
caps: ChannelCapabilities,
|
|
3863
|
+
rows: MessageRow[],
|
|
3864
|
+
compaction: SeededCompactionState = {},
|
|
3865
|
+
): void {
|
|
3866
|
+
const db = getDb();
|
|
3867
|
+
const now = Date.now();
|
|
3868
|
+
db.delete(messages)
|
|
3869
|
+
.where(eq(messages.conversationId, "runtime-assembly-fallback"))
|
|
3870
|
+
.run();
|
|
3871
|
+
db.insert(conversations)
|
|
3872
|
+
.values({
|
|
3873
|
+
id: "runtime-assembly-fallback",
|
|
3874
|
+
createdAt: now,
|
|
3875
|
+
updatedAt: now,
|
|
3876
|
+
})
|
|
3877
|
+
.run();
|
|
3878
|
+
if (rows.length > 0) {
|
|
3879
|
+
db.insert(messages)
|
|
3880
|
+
.values(
|
|
3881
|
+
rows.map((r) => ({
|
|
3882
|
+
id: r.id,
|
|
3883
|
+
conversationId: "runtime-assembly-fallback",
|
|
3884
|
+
role: r.role,
|
|
3885
|
+
content: r.content,
|
|
3886
|
+
createdAt: r.createdAt,
|
|
3887
|
+
metadata: r.metadata,
|
|
3888
|
+
})),
|
|
3889
|
+
)
|
|
3890
|
+
.run();
|
|
3891
|
+
}
|
|
3892
|
+
setConversation("runtime-assembly-fallback", {
|
|
3893
|
+
conversationId: "runtime-assembly-fallback",
|
|
3894
|
+
workingDir: "/sandbox",
|
|
3895
|
+
workspaceTopLevelContext: "",
|
|
3896
|
+
workspaceTopLevelDirty: false,
|
|
3897
|
+
surfaceState: new Map(),
|
|
3898
|
+
channelCapabilities: caps,
|
|
3899
|
+
trustContext: { trustClass: "guardian" },
|
|
3900
|
+
slackContextCompactionWatermarkTs:
|
|
3901
|
+
compaction.slackContextCompactionWatermarkTs ?? null,
|
|
3902
|
+
contextCompactedMessageCount:
|
|
3903
|
+
compaction.contextCompactedMessageCount ?? 0,
|
|
3904
|
+
} as never);
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
// Re-run a Slack-channel turn through the public assembly path. The
|
|
3908
|
+
// active-thread focus block self-resolves inside `applyRuntimeInjections`
|
|
3909
|
+
// from the seeded live conversation; `focusBlock` resolves it the same way
|
|
3910
|
+
// (default DB loader, guardian trust + the seeded compaction state) for the
|
|
3911
|
+
// expected-content assertions.
|
|
3912
|
+
async function runSlackChannelAssemblyWithFocus(
|
|
3913
|
+
rows: MessageRow[],
|
|
3914
|
+
compaction: SeededCompactionState = {},
|
|
3915
|
+
): Promise<{
|
|
3343
3916
|
messages: Message[];
|
|
3344
3917
|
focusBlock: string | null;
|
|
3345
3918
|
}> {
|
|
@@ -3350,24 +3923,23 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3350
3923
|
supportsVoiceInput: false,
|
|
3351
3924
|
chatType: "channel",
|
|
3352
3925
|
};
|
|
3353
|
-
|
|
3354
|
-
"conv-1",
|
|
3355
|
-
slackChannelCaps,
|
|
3356
|
-
{ loader: () => rows, trustClass: "guardian" },
|
|
3357
|
-
);
|
|
3926
|
+
seedSlackChannelConversationWithRows(slackChannelCaps, rows, compaction);
|
|
3358
3927
|
const focusBlock = loadSlackActiveThreadFocusBlock(
|
|
3359
|
-
"
|
|
3928
|
+
"runtime-assembly-fallback",
|
|
3360
3929
|
slackChannelCaps,
|
|
3361
|
-
{
|
|
3930
|
+
{
|
|
3931
|
+
trustClass: "guardian",
|
|
3932
|
+
contextCompactedMessageCount: compaction.contextCompactedMessageCount,
|
|
3933
|
+
slackContextCompactionWatermarkTs:
|
|
3934
|
+
compaction.slackContextCompactionWatermarkTs,
|
|
3935
|
+
},
|
|
3362
3936
|
);
|
|
3363
3937
|
const lastUserMessage: Message = {
|
|
3364
3938
|
role: "user",
|
|
3365
3939
|
content: [{ type: "text", text: "current turn" }],
|
|
3366
3940
|
};
|
|
3367
3941
|
const { messages } = await applyRuntimeInjections([lastUserMessage], {
|
|
3368
|
-
|
|
3369
|
-
slackChronologicalMessages,
|
|
3370
|
-
slackActiveThreadFocusBlock: focusBlock,
|
|
3942
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
3371
3943
|
});
|
|
3372
3944
|
return { messages, focusBlock };
|
|
3373
3945
|
}
|
|
@@ -3584,14 +4156,143 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3584
4156
|
}
|
|
3585
4157
|
});
|
|
3586
4158
|
|
|
4159
|
+
test("focus block self-resolves the live conversation's compaction watermark", async () => {
|
|
4160
|
+
/**
|
|
4161
|
+
* The watermark + compacted count live on the live `Conversation`, not in
|
|
4162
|
+
* the injection options, so `applyRuntimeInjections` must read them off the
|
|
4163
|
+
* live conversation to bound the self-resolved `<active_thread>` block.
|
|
4164
|
+
*/
|
|
4165
|
+
// GIVEN a thread whose earliest rows were folded into a prior compaction
|
|
4166
|
+
const rows: MessageRow[] = [
|
|
4167
|
+
userRow({
|
|
4168
|
+
id: "thread-root",
|
|
4169
|
+
createdAt: 1700000000_000,
|
|
4170
|
+
text: "compacted root",
|
|
4171
|
+
slackMeta: buildSlackMeta({
|
|
4172
|
+
channelTs: T0,
|
|
4173
|
+
threadTs: T0,
|
|
4174
|
+
displayName: "alice",
|
|
4175
|
+
}),
|
|
4176
|
+
}),
|
|
4177
|
+
userRow({
|
|
4178
|
+
id: "reply-before",
|
|
4179
|
+
createdAt: 1700000005_000,
|
|
4180
|
+
text: "compacted reply",
|
|
4181
|
+
slackMeta: buildSlackMeta({
|
|
4182
|
+
channelTs: T0_REPLY1,
|
|
4183
|
+
threadTs: T0,
|
|
4184
|
+
displayName: "bob",
|
|
4185
|
+
}),
|
|
4186
|
+
}),
|
|
4187
|
+
userRow({
|
|
4188
|
+
id: "reply-after",
|
|
4189
|
+
createdAt: 1700000020_000,
|
|
4190
|
+
text: "live reply",
|
|
4191
|
+
slackMeta: buildSlackMeta({
|
|
4192
|
+
channelTs: T0_REPLY2,
|
|
4193
|
+
threadTs: T0,
|
|
4194
|
+
displayName: "carol",
|
|
4195
|
+
}),
|
|
4196
|
+
}),
|
|
4197
|
+
];
|
|
4198
|
+
|
|
4199
|
+
// WHEN the live conversation carries a Slack compaction watermark at T1
|
|
4200
|
+
const { messages, focusBlock } = await runSlackChannelAssemblyWithFocus(
|
|
4201
|
+
rows,
|
|
4202
|
+
{
|
|
4203
|
+
slackContextCompactionWatermarkTs: T1,
|
|
4204
|
+
contextCompactedMessageCount: 2,
|
|
4205
|
+
},
|
|
4206
|
+
);
|
|
4207
|
+
|
|
4208
|
+
// THEN the self-resolved focus block keeps only the post-watermark reply
|
|
4209
|
+
expect(focusBlock).not.toBeNull();
|
|
4210
|
+
expect(focusBlock!).toContain("live reply");
|
|
4211
|
+
expect(focusBlock!).not.toContain("compacted root");
|
|
4212
|
+
expect(focusBlock!).not.toContain("compacted reply");
|
|
4213
|
+
// AND `applyRuntimeInjections` injected exactly that self-resolved block,
|
|
4214
|
+
// proving it read the watermark + count off the live conversation.
|
|
4215
|
+
const allText = messages
|
|
4216
|
+
.flatMap((m) => m.content)
|
|
4217
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
4218
|
+
.map((b) => b.text)
|
|
4219
|
+
.join("\n");
|
|
4220
|
+
expect(allText).toContain("<active_thread>");
|
|
4221
|
+
expect(allText).toContain(focusBlock!);
|
|
4222
|
+
});
|
|
4223
|
+
|
|
4224
|
+
test("focus block bounds by the compacted count when there is no watermark", async () => {
|
|
4225
|
+
/**
|
|
4226
|
+
* Conversations compacted before the Slack watermark migration have no
|
|
4227
|
+
* watermark, so the raw `contextCompactedMessageCount` is the sole boundary
|
|
4228
|
+
* the self-resolved focus block uses to drop already-compacted rows.
|
|
4229
|
+
*/
|
|
4230
|
+
// GIVEN a thread whose first two rows were folded into a prior compaction
|
|
4231
|
+
const rows: MessageRow[] = [
|
|
4232
|
+
userRow({
|
|
4233
|
+
id: "thread-root",
|
|
4234
|
+
createdAt: 1700000000_000,
|
|
4235
|
+
text: "compacted root",
|
|
4236
|
+
slackMeta: buildSlackMeta({
|
|
4237
|
+
channelTs: T0,
|
|
4238
|
+
threadTs: T0,
|
|
4239
|
+
displayName: "alice",
|
|
4240
|
+
}),
|
|
4241
|
+
}),
|
|
4242
|
+
userRow({
|
|
4243
|
+
id: "reply-before",
|
|
4244
|
+
createdAt: 1700000005_000,
|
|
4245
|
+
text: "compacted reply",
|
|
4246
|
+
slackMeta: buildSlackMeta({
|
|
4247
|
+
channelTs: T0_REPLY1,
|
|
4248
|
+
threadTs: T0,
|
|
4249
|
+
displayName: "bob",
|
|
4250
|
+
}),
|
|
4251
|
+
}),
|
|
4252
|
+
userRow({
|
|
4253
|
+
id: "reply-after",
|
|
4254
|
+
createdAt: 1700000020_000,
|
|
4255
|
+
text: "live reply",
|
|
4256
|
+
slackMeta: buildSlackMeta({
|
|
4257
|
+
channelTs: T0_REPLY2,
|
|
4258
|
+
threadTs: T0,
|
|
4259
|
+
displayName: "carol",
|
|
4260
|
+
}),
|
|
4261
|
+
}),
|
|
4262
|
+
];
|
|
4263
|
+
|
|
4264
|
+
// WHEN the live conversation has a compacted count of 2 and no watermark
|
|
4265
|
+
const { messages, focusBlock } = await runSlackChannelAssemblyWithFocus(
|
|
4266
|
+
rows,
|
|
4267
|
+
{
|
|
4268
|
+
slackContextCompactionWatermarkTs: null,
|
|
4269
|
+
contextCompactedMessageCount: 2,
|
|
4270
|
+
},
|
|
4271
|
+
);
|
|
4272
|
+
|
|
4273
|
+
// THEN the self-resolved focus block drops the two compacted rows and keeps
|
|
4274
|
+
// only the live reply
|
|
4275
|
+
expect(focusBlock).not.toBeNull();
|
|
4276
|
+
expect(focusBlock!).toContain("live reply");
|
|
4277
|
+
expect(focusBlock!).not.toContain("compacted root");
|
|
4278
|
+
expect(focusBlock!).not.toContain("compacted reply");
|
|
4279
|
+
const allText = messages
|
|
4280
|
+
.flatMap((m) => m.content)
|
|
4281
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
4282
|
+
.map((b) => b.text)
|
|
4283
|
+
.join("\n");
|
|
4284
|
+
expect(allText).toContain("<active_thread>");
|
|
4285
|
+
expect(allText).toContain(focusBlock!);
|
|
4286
|
+
});
|
|
4287
|
+
|
|
3587
4288
|
test("focus block is dropped when injection is replayed (rebuilds re-derive it)", async () => {
|
|
3588
4289
|
// Defensive: the `<active_thread>` block is a per-turn injection. When
|
|
3589
4290
|
// overflow recovery / compaction re-runs `applyRuntimeInjections` on
|
|
3590
4291
|
// already-injected messages, prior `<active_thread>` blocks must be
|
|
3591
4292
|
// stripped so the rebuild's freshly-derived block is the only one
|
|
3592
4293
|
// present. We simulate by building a Slack channel turn, then
|
|
3593
|
-
// running the strip pipeline + applying injections again
|
|
3594
|
-
//
|
|
4294
|
+
// running the strip pipeline + applying injections again to confirm the
|
|
4295
|
+
// rebuild re-derives the block and no duplication occurs.
|
|
3595
4296
|
const rows: MessageRow[] = [
|
|
3596
4297
|
userRow({
|
|
3597
4298
|
id: "m1",
|
|
@@ -3624,19 +4325,11 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3624
4325
|
.join("\n");
|
|
3625
4326
|
expect(strippedTexts).not.toContain("<active_thread>");
|
|
3626
4327
|
|
|
3627
|
-
// Re-run injection
|
|
4328
|
+
// Re-run injection on the stripped messages — the conversation is still
|
|
4329
|
+
// seeded, so the rebuild re-derives the focus block; only ONE
|
|
3628
4330
|
// `<active_thread>` block must end up in the result.
|
|
3629
|
-
const slackChannelCaps: ChannelCapabilities = {
|
|
3630
|
-
channel: "slack",
|
|
3631
|
-
dashboardCapable: false,
|
|
3632
|
-
supportsDynamicUi: false,
|
|
3633
|
-
supportsVoiceInput: false,
|
|
3634
|
-
chatType: "channel",
|
|
3635
|
-
};
|
|
3636
|
-
const newFocus = "<active_thread>\nnewly built\n</active_thread>";
|
|
3637
4331
|
const { messages: reInjected } = await applyRuntimeInjections(stripped, {
|
|
3638
|
-
|
|
3639
|
-
slackActiveThreadFocusBlock: newFocus,
|
|
4332
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
3640
4333
|
});
|
|
3641
4334
|
const reInjectedTexts = reInjected
|
|
3642
4335
|
.flatMap((m) => m.content)
|
|
@@ -3648,24 +4341,22 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3648
4341
|
expect(blockCount).toBe(1);
|
|
3649
4342
|
expect(
|
|
3650
4343
|
reInjectedTexts.find((t) => t.startsWith("<active_thread>")),
|
|
3651
|
-
).toContain("
|
|
4344
|
+
).toContain("Reply in thread A");
|
|
3652
4345
|
});
|
|
3653
4346
|
|
|
3654
|
-
test("non-slack conversations
|
|
3655
|
-
//
|
|
3656
|
-
// `isSlackChannelConversation`).
|
|
3657
|
-
//
|
|
4347
|
+
test("non-slack conversations never get an active-thread focus block", async () => {
|
|
4348
|
+
// The focus block is gated on a Slack *channel* conversation
|
|
4349
|
+
// (`isSlackChannelConversation`). A non-Slack conversation must never
|
|
4350
|
+
// self-resolve an `<active_thread>` block.
|
|
4351
|
+
seedChannelCapabilitiesConversation({
|
|
4352
|
+
channel: "vellum",
|
|
4353
|
+
dashboardCapable: true,
|
|
4354
|
+
supportsDynamicUi: true,
|
|
4355
|
+
supportsVoiceInput: true,
|
|
4356
|
+
});
|
|
3658
4357
|
const { messages: result } = await applyRuntimeInjections(
|
|
3659
4358
|
[{ role: "user", content: [{ type: "text", text: "vellum question" }] }],
|
|
3660
|
-
{
|
|
3661
|
-
channelCapabilities: {
|
|
3662
|
-
channel: "vellum",
|
|
3663
|
-
dashboardCapable: true,
|
|
3664
|
-
supportsDynamicUi: true,
|
|
3665
|
-
supportsVoiceInput: true,
|
|
3666
|
-
},
|
|
3667
|
-
slackActiveThreadFocusBlock: "<active_thread>\nbogus\n</active_thread>",
|
|
3668
|
-
},
|
|
4359
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3669
4360
|
);
|
|
3670
4361
|
const allText = result
|
|
3671
4362
|
.flatMap((m) => m.content)
|
|
@@ -3676,22 +4367,20 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3676
4367
|
expect(allText).toContain("vellum question");
|
|
3677
4368
|
});
|
|
3678
4369
|
|
|
3679
|
-
test("slack DMs
|
|
4370
|
+
test("slack DMs never get an active-thread focus block", async () => {
|
|
3680
4371
|
// Same as above but for Slack DMs (chatType === "im"). The focus
|
|
3681
4372
|
// injection is keyed on `isSlackChannelConversation` which excludes
|
|
3682
4373
|
// DMs, so the block must not appear.
|
|
4374
|
+
seedChannelCapabilitiesConversation({
|
|
4375
|
+
channel: "slack",
|
|
4376
|
+
dashboardCapable: false,
|
|
4377
|
+
supportsDynamicUi: false,
|
|
4378
|
+
supportsVoiceInput: false,
|
|
4379
|
+
chatType: "im",
|
|
4380
|
+
});
|
|
3683
4381
|
const { messages: result } = await applyRuntimeInjections(
|
|
3684
4382
|
[{ role: "user", content: [{ type: "text", text: "DM question" }] }],
|
|
3685
|
-
{
|
|
3686
|
-
channelCapabilities: {
|
|
3687
|
-
channel: "slack",
|
|
3688
|
-
dashboardCapable: false,
|
|
3689
|
-
supportsDynamicUi: false,
|
|
3690
|
-
supportsVoiceInput: false,
|
|
3691
|
-
chatType: "im",
|
|
3692
|
-
},
|
|
3693
|
-
slackActiveThreadFocusBlock: "<active_thread>\nbogus\n</active_thread>",
|
|
3694
|
-
},
|
|
4383
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3695
4384
|
);
|
|
3696
4385
|
const allText = result
|
|
3697
4386
|
.flatMap((m) => m.content)
|
|
@@ -5047,6 +5736,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
|
|
|
5047
5736
|
pkbSearchResults = [];
|
|
5048
5737
|
pkbSearchThrows = null;
|
|
5049
5738
|
const { blocks } = await applyRuntimeInjections(baseMessages, {
|
|
5739
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
5050
5740
|
mode: "full",
|
|
5051
5741
|
});
|
|
5052
5742
|
|
|
@@ -5057,6 +5747,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
|
|
|
5057
5747
|
test("not captured in minimal mode", async () => {
|
|
5058
5748
|
seedPkbContent();
|
|
5059
5749
|
const { blocks } = await applyRuntimeInjections(baseMessages, {
|
|
5750
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
5060
5751
|
mode: "minimal",
|
|
5061
5752
|
});
|
|
5062
5753
|
|
|
@@ -5065,6 +5756,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
|
|
|
5065
5756
|
|
|
5066
5757
|
test("not captured when PKB inactive", async () => {
|
|
5067
5758
|
const { blocks } = await applyRuntimeInjections(baseMessages, {
|
|
5759
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
5068
5760
|
mode: "full",
|
|
5069
5761
|
});
|
|
5070
5762
|
|