@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091926.ebb2d62
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/docs/activation-funnel-telemetry.md +310 -0
- 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 +273 -121
- package/package.json +2 -2
- package/scripts/sync-llm-catalog.ts +2 -0
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- 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 +23 -8
- 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 +6 -5
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -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 +146 -0
- package/src/__tests__/compaction-events.test.ts +7 -7
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +265 -125
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
- 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 +158 -234
- package/src/__tests__/conversation-agent-loop.test.ts +342 -623
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +6 -1
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
- package/src/__tests__/conversation-process-callsite.test.ts +15 -15
- package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
- package/src/__tests__/conversation-queue.test.ts +10 -10
- package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +7 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
- package/src/__tests__/conversation-speed-override.test.ts +11 -10
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
- package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +5 -4
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- 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 +17 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -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 +5 -1
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- 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__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +91 -81
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +245 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +31 -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 +5 -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__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- 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/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +188 -28
- package/src/acp/resolve-agent.ts +119 -42
- 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 +403 -251
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/responses/conversation-message.ts +72 -13
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/approvals/guardian-request-resolvers.ts +27 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
- package/src/cli/lib/install-from-github.ts +797 -60
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +416 -0
- package/src/cli/lib/plugin-marketplace.ts +215 -0
- package/src/cli/lib/search-plugins.ts +201 -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 +9 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +80 -41
- 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 -3
- package/src/config/feature-flag-registry.json +10 -56
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/loader.ts +5 -22
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -21
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +17 -3
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/compactor.ts +22 -4
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/context/strip-injections.ts +8 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +120 -42
- package/src/daemon/conversation-agent-loop.ts +308 -1297
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +22 -141
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +361 -393
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +165 -11
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +444 -62
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +36 -46
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/server.ts +2 -0
- package/src/daemon/tool-side-effects.ts +15 -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__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +6 -0
- package/src/memory/embedding-backend.ts +5 -22
- 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/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -63
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +9 -2
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +40 -11
- package/src/plugin-api/types.ts +108 -0
- package/src/plugins/defaults/compaction/compact.ts +106 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
- 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 +156 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
- 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/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
- package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -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/external-api.ts +12 -2
- 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/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/model-catalog.ts +57 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +57 -7
- package/src/providers/types.ts +6 -0
- 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 +466 -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__/stt-routes.test.ts +112 -0
- 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 +185 -32
- package/src/runtime/routes/acp-routes.ts +328 -30
- package/src/runtime/routes/app-management-routes.ts +3 -0
- 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/channel-verification-routes.ts +14 -5
- 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 +30 -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/inbound-stages/acl-enforcement.ts +33 -34
- 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 +65 -305
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/services/published-app-updater.ts +30 -8
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +9 -19
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +173 -202
- 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/browser/browser-execution.ts +12 -2
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +32 -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/daemon/wake-target-adapter.ts +0 -253
- package/src/memory/v3/__tests__/assign.test.ts +0 -242
- package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
- package/src/memory/v3/__tests__/core.test.ts +0 -39
- package/src/memory/v3/__tests__/health.test.ts +0 -219
- package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
- package/src/memory/v3/__tests__/needle.test.ts +0 -107
- package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
- package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
- package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
- package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
- package/src/memory/v3/__tests__/router.test.ts +0 -332
- package/src/memory/v3/__tests__/selector.test.ts +0 -470
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
- package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
- package/src/memory/v3/__tests__/tree.test.ts +0 -192
- package/src/memory/v3/assign.ts +0 -268
- package/src/memory/v3/capabilities.ts +0 -124
- package/src/memory/v3/core.ts +0 -26
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/maintain-job.ts +0 -314
- package/src/memory/v3/needle.ts +0 -115
- package/src/memory/v3/orchestrate.ts +0 -126
- package/src/memory/v3/page-content.ts +0 -34
- package/src/memory/v3/provider-blocks.ts +0 -26
- package/src/memory/v3/reconcile.ts +0 -523
- package/src/memory/v3/router.ts +0 -190
- package/src/memory/v3/selection-log-store.ts +0 -84
- package/src/memory/v3/selector.ts +0 -226
- package/src/memory/v3/shadow-plugin.ts +0 -411
- package/src/memory/v3/snapshot.ts +0 -209
- package/src/memory/v3/tree.ts +0 -174
- package/src/memory/v3/types.ts +0 -59
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- 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/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
- 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/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/util/map-limit.ts +0 -27
- /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__/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}/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}/llm-retry.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,36 @@ 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 { LLMSchema } from "../config/schemas/llm.js";
|
|
68
|
+
import {
|
|
69
|
+
clearConversations,
|
|
70
|
+
setConversation,
|
|
71
|
+
} from "../daemon/conversation-registry.js";
|
|
38
72
|
import type {
|
|
39
73
|
ChannelCapabilities,
|
|
40
74
|
SlackTranscriptInputRow,
|
|
41
|
-
UnifiedTurnContextOptions,
|
|
42
75
|
} from "../daemon/conversation-runtime-assembly.js";
|
|
43
76
|
import {
|
|
44
77
|
applyRuntimeInjections,
|
|
45
78
|
assembleSlackActiveThreadFocusBlock,
|
|
46
79
|
assembleSlackChronologicalMessages,
|
|
47
80
|
buildSubagentStatusBlock,
|
|
48
|
-
buildUnifiedTurnContextBlock,
|
|
49
81
|
getSlackCompactionWatermarkForPrefix,
|
|
50
82
|
injectChannelCapabilityContext,
|
|
51
83
|
injectChannelCommandContext,
|
|
@@ -55,24 +87,42 @@ import {
|
|
|
55
87
|
loadSlackChronologicalContext,
|
|
56
88
|
loadSlackChronologicalMessages,
|
|
57
89
|
resolveChannelCapabilities,
|
|
90
|
+
resolveTurnInboundActorContext,
|
|
91
|
+
resolveTurnModelProfileLabel,
|
|
58
92
|
stripChannelCapabilityContext,
|
|
59
93
|
stripInjectionsForCompaction,
|
|
60
94
|
stripNowScratchpad,
|
|
61
95
|
} from "../daemon/conversation-runtime-assembly.js";
|
|
96
|
+
import type { SurfaceData, SurfaceType } from "../daemon/message-protocol.js";
|
|
62
97
|
import { buildPkbReminder } from "../daemon/pkb-reminder-builder.js";
|
|
98
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
63
99
|
import type { MessageRow } from "../memory/conversation-crud.js";
|
|
100
|
+
import { getDb } from "../memory/db-connection.js";
|
|
101
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
64
102
|
import { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
|
|
65
103
|
import { getPkbRoot } from "../memory/pkb/types.js";
|
|
104
|
+
import { conversations, messages } from "../memory/schema.js";
|
|
66
105
|
import {
|
|
67
106
|
type SlackMessageMetadata,
|
|
68
107
|
writeSlackMetadata,
|
|
69
108
|
} from "../messaging/providers/slack/message-metadata.js";
|
|
70
109
|
import { parentAlias } from "../messaging/providers/slack/render-transcript.js";
|
|
110
|
+
import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
|
|
111
|
+
import {
|
|
112
|
+
buildUnifiedTurnContextBlock,
|
|
113
|
+
type UnifiedTurnContextOptions,
|
|
114
|
+
} from "../plugins/defaults/memory-retrieval/unified-turn-context.js";
|
|
71
115
|
import type { Message } from "../providers/types.js";
|
|
72
116
|
import { wrapUntrustedContent } from "../security/untrusted-content.js";
|
|
117
|
+
import { getSubagentManager } from "../subagent/index.js";
|
|
73
118
|
import type { SubagentState } from "../subagent/types.js";
|
|
74
119
|
import { getWorkspacePromptPath } from "../util/platform.js";
|
|
75
120
|
|
|
121
|
+
// `applyRuntimeInjections` self-resolves the Slack active-thread focus block by
|
|
122
|
+
// reading the live conversation's persisted message rows, so the schema must
|
|
123
|
+
// exist before any Slack-channel assembly test runs.
|
|
124
|
+
initializeDb();
|
|
125
|
+
|
|
76
126
|
// The pkb-reminder injector derives PKB-active state from the workspace itself
|
|
77
127
|
// — `readPkbContext()` returning content behind the personal-memory trust gate
|
|
78
128
|
// — rather than from a threaded flag. Tests that need the reminder to fire seed
|
|
@@ -103,6 +153,66 @@ function clearNowScratchpad(): void {
|
|
|
103
153
|
rmSync(getWorkspacePromptPath("NOW.md"), { force: true });
|
|
104
154
|
}
|
|
105
155
|
|
|
156
|
+
// The workspace-context injector sources its block off the live `Conversation`
|
|
157
|
+
// looked up by `conversationId`. Register a fake instance carrying both a
|
|
158
|
+
// non-dirty workspace context (non-null content, not dirty) so
|
|
159
|
+
// `resolveWorkspaceTopLevelContext` returns it verbatim without rescanning the
|
|
160
|
+
// filesystem, and an active dynamic-page surface, so `applyRuntimeInjections`
|
|
161
|
+
// resolves the `<workspace>` and `<active_workspace>` blocks from it;
|
|
162
|
+
// `clearConversations()` between tests keeps suites that assert the blocks are
|
|
163
|
+
// absent unaffected.
|
|
164
|
+
function seedActiveSurfaceConversation(
|
|
165
|
+
conversationId: string,
|
|
166
|
+
workspaceText: string,
|
|
167
|
+
surfaceId: string,
|
|
168
|
+
data: SurfaceData,
|
|
169
|
+
channelCapabilities?: ChannelCapabilities,
|
|
170
|
+
commandIntent?: { type: string; payload?: string; languageCode?: string },
|
|
171
|
+
currentTurnTemporalSnapshot?: {
|
|
172
|
+
clientTimezone: string | null;
|
|
173
|
+
},
|
|
174
|
+
): void {
|
|
175
|
+
setConversation(conversationId, {
|
|
176
|
+
conversationId,
|
|
177
|
+
workingDir: "/sandbox",
|
|
178
|
+
workspaceTopLevelContext: workspaceText,
|
|
179
|
+
workspaceTopLevelDirty: false,
|
|
180
|
+
currentActiveSurfaceId: surfaceId,
|
|
181
|
+
surfaceState: new Map<
|
|
182
|
+
string,
|
|
183
|
+
{ surfaceType: SurfaceType; data: SurfaceData }
|
|
184
|
+
>([[surfaceId, { surfaceType: "dynamic_page", data }]]),
|
|
185
|
+
channelCapabilities: channelCapabilities ?? undefined,
|
|
186
|
+
commandIntent,
|
|
187
|
+
currentTurnTemporalSnapshot,
|
|
188
|
+
} as never);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function clearWorkspaceContext(): void {
|
|
192
|
+
clearConversations();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The `<channel_capabilities>` branch and the Slack gates source the channel
|
|
196
|
+
// capabilities off the live `Conversation` looked up by `conversationId`.
|
|
197
|
+
// Register a fake fallback instance carrying the given capabilities (and a
|
|
198
|
+
// non-dirty empty workspace + empty surface so the other live-sourced branches
|
|
199
|
+
// stay inert) so `applyRuntimeInjections` resolves them; `clearConversations()`
|
|
200
|
+
// between tests keeps suites asserting absence unaffected.
|
|
201
|
+
function seedChannelCapabilitiesConversation(
|
|
202
|
+
caps: ChannelCapabilities | null,
|
|
203
|
+
transportHints?: string[],
|
|
204
|
+
): void {
|
|
205
|
+
setConversation("runtime-assembly-fallback", {
|
|
206
|
+
conversationId: "runtime-assembly-fallback",
|
|
207
|
+
workingDir: "/sandbox",
|
|
208
|
+
workspaceTopLevelContext: "",
|
|
209
|
+
workspaceTopLevelDirty: false,
|
|
210
|
+
surfaceState: new Map(),
|
|
211
|
+
channelCapabilities: caps ?? undefined,
|
|
212
|
+
transportHints,
|
|
213
|
+
} as never);
|
|
214
|
+
}
|
|
215
|
+
|
|
106
216
|
// ---------------------------------------------------------------------------
|
|
107
217
|
// resolveChannelCapabilities
|
|
108
218
|
// ---------------------------------------------------------------------------
|
|
@@ -575,6 +685,8 @@ describe("stripChannelCapabilityContext", () => {
|
|
|
575
685
|
// ---------------------------------------------------------------------------
|
|
576
686
|
|
|
577
687
|
describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
688
|
+
afterEach(clearConversations);
|
|
689
|
+
|
|
578
690
|
const baseMessages: Message[] = [
|
|
579
691
|
{
|
|
580
692
|
role: "user",
|
|
@@ -590,8 +702,9 @@ describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
|
590
702
|
supportsVoiceInput: false,
|
|
591
703
|
};
|
|
592
704
|
|
|
705
|
+
seedChannelCapabilitiesConversation(caps);
|
|
593
706
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
594
|
-
|
|
707
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
595
708
|
});
|
|
596
709
|
|
|
597
710
|
expect(result.length).toBe(1);
|
|
@@ -603,8 +716,9 @@ describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
|
603
716
|
});
|
|
604
717
|
|
|
605
718
|
test("does not inject when channelCapabilities is null", async () => {
|
|
719
|
+
seedChannelCapabilitiesConversation(null);
|
|
606
720
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
607
|
-
|
|
721
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
608
722
|
});
|
|
609
723
|
|
|
610
724
|
expect(result.length).toBe(1);
|
|
@@ -612,7 +726,9 @@ describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
|
612
726
|
});
|
|
613
727
|
|
|
614
728
|
test("does not inject when channelCapabilities is omitted", async () => {
|
|
615
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
729
|
+
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
730
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
731
|
+
});
|
|
616
732
|
|
|
617
733
|
expect(result.length).toBe(1);
|
|
618
734
|
expect(result[0].content.length).toBe(1);
|
|
@@ -626,8 +742,9 @@ describe("applyRuntimeInjections with channelCapabilities", () => {
|
|
|
626
742
|
supportsVoiceInput: false,
|
|
627
743
|
};
|
|
628
744
|
|
|
745
|
+
seedChannelCapabilitiesConversation(caps);
|
|
629
746
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
630
|
-
|
|
747
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
631
748
|
});
|
|
632
749
|
|
|
633
750
|
expect(result.length).toBe(1);
|
|
@@ -755,30 +872,25 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
755
872
|
},
|
|
756
873
|
];
|
|
757
874
|
|
|
875
|
+
const channelCapabilities: ChannelCapabilities = {
|
|
876
|
+
channel: "telegram",
|
|
877
|
+
dashboardCapable: false,
|
|
878
|
+
supportsDynamicUi: false,
|
|
879
|
+
supportsVoiceInput: false,
|
|
880
|
+
};
|
|
881
|
+
|
|
758
882
|
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>",
|
|
883
|
+
// Non-interactive turn so the `<non_interactive_context>` branch fires.
|
|
770
884
|
isNonInteractive: true,
|
|
885
|
+
requestId: "injection-mode-req",
|
|
886
|
+
conversationId: "injection-mode-conv",
|
|
887
|
+
turnIndex: 0,
|
|
771
888
|
// Guardian trust so the personal-memory gate admits the actor regardless
|
|
772
889
|
// of the telegram channel capabilities under test, letting the reminder
|
|
773
890
|
// 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
|
-
},
|
|
891
|
+
trust: {
|
|
892
|
+
sourceChannel: "vellum" as const,
|
|
893
|
+
trustClass: "guardian" as const,
|
|
782
894
|
},
|
|
783
895
|
};
|
|
784
896
|
|
|
@@ -788,10 +900,22 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
788
900
|
beforeEach(() => {
|
|
789
901
|
seedPkbContent();
|
|
790
902
|
seedNowScratchpad("Current focus: shipping PR 3");
|
|
903
|
+
seedActiveSurfaceConversation(
|
|
904
|
+
"injection-mode-conv",
|
|
905
|
+
"<workspace>\nRoot: /sandbox\n</workspace>",
|
|
906
|
+
"sf_1",
|
|
907
|
+
{ html: "<div>test</div>" },
|
|
908
|
+
channelCapabilities,
|
|
909
|
+
{ type: "start" },
|
|
910
|
+
{
|
|
911
|
+
clientTimezone: null,
|
|
912
|
+
},
|
|
913
|
+
);
|
|
791
914
|
});
|
|
792
915
|
afterEach(() => {
|
|
793
916
|
clearPkbContent();
|
|
794
917
|
clearNowScratchpad();
|
|
918
|
+
clearWorkspaceContext();
|
|
795
919
|
});
|
|
796
920
|
|
|
797
921
|
test("full mode (default) includes all injections", async () => {
|
|
@@ -898,6 +1022,34 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
898
1022
|
|
|
899
1023
|
expect(texts).toContain("Hello");
|
|
900
1024
|
});
|
|
1025
|
+
|
|
1026
|
+
test("post-compaction re-injection resolves the live conversation from the conversation id", async () => {
|
|
1027
|
+
// GIVEN the seeded live conversation `injection-mode-conv` whose
|
|
1028
|
+
// conversation-scoped blocks (`<active_workspace>`, `<channel_capabilities>`)
|
|
1029
|
+
// only resolve when `applyRuntimeInjections` finds it by conversation id
|
|
1030
|
+
// AND the agent loop hands the post-compaction hook the turn-identity fields
|
|
1031
|
+
// flat (`requestId`, `conversationId`, `trust`)
|
|
1032
|
+
const { messages: result } = await postCompact({
|
|
1033
|
+
history: baseMessages,
|
|
1034
|
+
requestId: "reinject-req",
|
|
1035
|
+
conversationId: "injection-mode-conv",
|
|
1036
|
+
trust: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
1037
|
+
isNonInteractive: false,
|
|
1038
|
+
mode: "full",
|
|
1039
|
+
modelProfile: null,
|
|
1040
|
+
actorContext: null,
|
|
1041
|
+
});
|
|
1042
|
+
const allText = result[0].content
|
|
1043
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
1044
|
+
.map((b) => b.text)
|
|
1045
|
+
.join("\n");
|
|
1046
|
+
|
|
1047
|
+
// THEN the hook forwards `conversationId` onto the flat injection options,
|
|
1048
|
+
// so the re-injection resolves the same live conversation as the turn's
|
|
1049
|
+
// initial assembly rather than the synthetic fallback.
|
|
1050
|
+
expect(allText).toContain("<active_workspace>");
|
|
1051
|
+
expect(allText).toContain("<channel_capabilities>");
|
|
1052
|
+
});
|
|
901
1053
|
});
|
|
902
1054
|
|
|
903
1055
|
// The now-md default injector emits the `<NOW.md>` block as an
|
|
@@ -1051,7 +1203,7 @@ describe("stripInjectionsForCompaction preserves persistent blocks", () => {
|
|
|
1051
1203
|
).toContain("<turn_context>");
|
|
1052
1204
|
});
|
|
1053
1205
|
|
|
1054
|
-
test("<workspace> blocks
|
|
1206
|
+
test("<workspace> blocks ARE stripped so the injector re-sources them post-compaction", () => {
|
|
1055
1207
|
const messages: Message[] = [
|
|
1056
1208
|
{
|
|
1057
1209
|
role: "user",
|
|
@@ -1067,10 +1219,10 @@ describe("stripInjectionsForCompaction preserves persistent blocks", () => {
|
|
|
1067
1219
|
|
|
1068
1220
|
const result = stripInjectionsForCompaction(messages);
|
|
1069
1221
|
expect(result.length).toBe(1);
|
|
1070
|
-
expect(result[0].content.length).toBe(
|
|
1071
|
-
expect(
|
|
1072
|
-
|
|
1073
|
-
)
|
|
1222
|
+
expect(result[0].content.length).toBe(1);
|
|
1223
|
+
expect((result[0].content[0] as { type: "text"; text: string }).text).toBe(
|
|
1224
|
+
"Hello",
|
|
1225
|
+
);
|
|
1074
1226
|
});
|
|
1075
1227
|
|
|
1076
1228
|
test("legacy <workspace_top_level> blocks ARE stripped for backward compat", () => {
|
|
@@ -1237,7 +1389,9 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1237
1389
|
|
|
1238
1390
|
test("injects NOW.md block when the file has content", async () => {
|
|
1239
1391
|
seedNowScratchpad("Current focus: fix the bug");
|
|
1240
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1392
|
+
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1393
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1394
|
+
});
|
|
1241
1395
|
|
|
1242
1396
|
expect(result.length).toBe(1);
|
|
1243
1397
|
expect(result[0].content.length).toBe(2);
|
|
@@ -1249,7 +1403,9 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1249
1403
|
|
|
1250
1404
|
test("scratchpad appears before user's original text content", async () => {
|
|
1251
1405
|
seedNowScratchpad("scratchpad notes");
|
|
1252
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1406
|
+
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1407
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1408
|
+
});
|
|
1253
1409
|
|
|
1254
1410
|
// Scratchpad comes first (before user content)
|
|
1255
1411
|
expect(
|
|
@@ -1262,7 +1418,9 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1262
1418
|
});
|
|
1263
1419
|
|
|
1264
1420
|
test("does not inject when the NOW.md file is absent", async () => {
|
|
1265
|
-
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1421
|
+
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1422
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1423
|
+
});
|
|
1266
1424
|
|
|
1267
1425
|
expect(result.length).toBe(1);
|
|
1268
1426
|
expect(result[0].content.length).toBe(1);
|
|
@@ -1271,6 +1429,7 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1271
1429
|
test("skipped in minimal mode", async () => {
|
|
1272
1430
|
seedNowScratchpad("Current focus: fix the bug");
|
|
1273
1431
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1432
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1274
1433
|
mode: "minimal",
|
|
1275
1434
|
});
|
|
1276
1435
|
|
|
@@ -1283,6 +1442,138 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
|
|
|
1283
1442
|
});
|
|
1284
1443
|
});
|
|
1285
1444
|
|
|
1445
|
+
// ---------------------------------------------------------------------------
|
|
1446
|
+
// resolveTurnInboundActorContext
|
|
1447
|
+
// ---------------------------------------------------------------------------
|
|
1448
|
+
|
|
1449
|
+
describe("resolveTurnInboundActorContext", () => {
|
|
1450
|
+
/**
|
|
1451
|
+
* No trust context means there is no inbound actor to ground the turn on, so
|
|
1452
|
+
* the actor section is suppressed.
|
|
1453
|
+
*/
|
|
1454
|
+
test("returns null when there is no trust context", () => {
|
|
1455
|
+
// GIVEN a turn with no trust context
|
|
1456
|
+
// WHEN the inbound actor context is resolved
|
|
1457
|
+
const actorContext = resolveTurnInboundActorContext(undefined, "asst-1");
|
|
1458
|
+
|
|
1459
|
+
// THEN there is no actor context to inject
|
|
1460
|
+
expect(actorContext).toBeNull();
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Guardian (owner) turns suppress the actor section — the unified turn context
|
|
1465
|
+
* only renders actor fields for non-guardian actors.
|
|
1466
|
+
*/
|
|
1467
|
+
test("returns null on guardian turns", () => {
|
|
1468
|
+
// GIVEN a guardian trust context (no requester chat id, so the direct
|
|
1469
|
+
// projection branch is taken)
|
|
1470
|
+
const trustContext: TrustContext = {
|
|
1471
|
+
sourceChannel: "vellum",
|
|
1472
|
+
trustClass: "guardian",
|
|
1473
|
+
requesterDisplayName: "Alice",
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
// WHEN the inbound actor context is resolved
|
|
1477
|
+
const actorContext = resolveTurnInboundActorContext(trustContext, "asst-1");
|
|
1478
|
+
|
|
1479
|
+
// THEN the actor section is suppressed for the owner
|
|
1480
|
+
expect(actorContext).toBeNull();
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* A non-guardian trust context without the identity fields needed for the
|
|
1485
|
+
* unified actor-trust lookup is projected directly into the actor context.
|
|
1486
|
+
*/
|
|
1487
|
+
test("projects a non-guardian trust context directly", () => {
|
|
1488
|
+
// GIVEN a trusted-contact trust context lacking requester chat/user ids
|
|
1489
|
+
const trustContext: TrustContext = {
|
|
1490
|
+
sourceChannel: "telegram",
|
|
1491
|
+
trustClass: "trusted_contact",
|
|
1492
|
+
requesterIdentifier: "@bob_handle",
|
|
1493
|
+
requesterDisplayName: "Bob",
|
|
1494
|
+
requesterSenderDisplayName: "Bobby",
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// WHEN the inbound actor context is resolved
|
|
1498
|
+
const actorContext = resolveTurnInboundActorContext(trustContext, "asst-1");
|
|
1499
|
+
|
|
1500
|
+
// THEN the trust context is projected into the model-facing actor context
|
|
1501
|
+
expect(actorContext).toEqual({
|
|
1502
|
+
sourceChannel: "telegram",
|
|
1503
|
+
canonicalActorIdentity: null,
|
|
1504
|
+
actorIdentifier: "@bob_handle",
|
|
1505
|
+
actorDisplayName: "Bob",
|
|
1506
|
+
actorSenderDisplayName: "Bobby",
|
|
1507
|
+
actorMemberDisplayName: undefined,
|
|
1508
|
+
trustClass: "trusted_contact",
|
|
1509
|
+
guardianIdentity: undefined,
|
|
1510
|
+
});
|
|
1511
|
+
});
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// ---------------------------------------------------------------------------
|
|
1515
|
+
// resolveTurnModelProfileLabel
|
|
1516
|
+
// ---------------------------------------------------------------------------
|
|
1517
|
+
|
|
1518
|
+
describe("resolveTurnModelProfileLabel", () => {
|
|
1519
|
+
/**
|
|
1520
|
+
* A null key means the active profile is unchanged since the last notified
|
|
1521
|
+
* one, so there is no `model_profile` line to render this turn.
|
|
1522
|
+
*/
|
|
1523
|
+
test("returns null when the profile key is null", () => {
|
|
1524
|
+
// GIVEN a turn whose profile is unchanged since the last notification
|
|
1525
|
+
const llm = LLMSchema.parse({
|
|
1526
|
+
default: { provider: "anthropic", model: "claude-sonnet-4-7" },
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
// WHEN the label is resolved for a null key
|
|
1530
|
+
const label = resolveTurnModelProfileLabel(null, "mainAgent", llm);
|
|
1531
|
+
|
|
1532
|
+
// THEN there is no profile line to inject
|
|
1533
|
+
expect(label).toBeNull();
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* A known profile renders its configured display label paired with the model
|
|
1538
|
+
* the call site resolves to.
|
|
1539
|
+
*/
|
|
1540
|
+
test("renders the profile's label with the resolved model", () => {
|
|
1541
|
+
// GIVEN a workspace with a labelled "fast" profile
|
|
1542
|
+
const llm = LLMSchema.parse({
|
|
1543
|
+
default: { provider: "anthropic", model: "claude-sonnet-4-7" },
|
|
1544
|
+
profiles: {
|
|
1545
|
+
fast: { label: "Fast", provider: "anthropic", model: "claude-haiku-4" },
|
|
1546
|
+
},
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
// WHEN the label is resolved for that profile at the main-agent call site
|
|
1550
|
+
const label = resolveTurnModelProfileLabel("fast", "mainAgent", llm);
|
|
1551
|
+
|
|
1552
|
+
// THEN the rendered line pairs the display label with the resolved model
|
|
1553
|
+
expect(label).toBe("Fast (claude-haiku-4)");
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* A profile with no configured label falls back to its raw key so the model
|
|
1558
|
+
* still gets a stable identifier.
|
|
1559
|
+
*/
|
|
1560
|
+
test("falls back to the raw key when the profile has no label", () => {
|
|
1561
|
+
// GIVEN a profile that sets a model but no display label
|
|
1562
|
+
const llm = LLMSchema.parse({
|
|
1563
|
+
default: { provider: "anthropic", model: "claude-sonnet-4-7" },
|
|
1564
|
+
profiles: {
|
|
1565
|
+
deep: { provider: "anthropic", model: "claude-opus-4" },
|
|
1566
|
+
},
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
// WHEN the label is resolved for that profile
|
|
1570
|
+
const label = resolveTurnModelProfileLabel("deep", "mainAgent", llm);
|
|
1571
|
+
|
|
1572
|
+
// THEN the raw key stands in for the missing label
|
|
1573
|
+
expect(label).toBe("deep (claude-opus-4)");
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1286
1577
|
// ---------------------------------------------------------------------------
|
|
1287
1578
|
// buildUnifiedTurnContextBlock
|
|
1288
1579
|
// ---------------------------------------------------------------------------
|
|
@@ -1694,6 +1985,8 @@ describe("buildUnifiedTurnContextBlock", () => {
|
|
|
1694
1985
|
// ---------------------------------------------------------------------------
|
|
1695
1986
|
|
|
1696
1987
|
describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
1988
|
+
afterEach(clearConversations);
|
|
1989
|
+
|
|
1697
1990
|
const baseMessages: Message[] = [
|
|
1698
1991
|
{
|
|
1699
1992
|
role: "user",
|
|
@@ -1701,12 +1994,42 @@ describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
|
1701
1994
|
},
|
|
1702
1995
|
];
|
|
1703
1996
|
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1997
|
+
const sampleOptions = {
|
|
1998
|
+
interfaceName: "macos",
|
|
1999
|
+
};
|
|
2000
|
+
// The block reflects the options-bag interface and the `current_time` the
|
|
2001
|
+
// injector computes live (pinned to `FIXED_TURN_TIMESTAMP` for this suite).
|
|
2002
|
+
const sampleBlock = buildUnifiedTurnContextBlock({
|
|
2003
|
+
timestamp: FIXED_TURN_TIMESTAMP,
|
|
2004
|
+
...sampleOptions,
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
// Seed the live fallback conversation with the per-turn temporal snapshot
|
|
2008
|
+
// (its presence gates the `<turn_context>` block) and the turn interface
|
|
2009
|
+
// context (so the injector sources the interface label to match
|
|
2010
|
+
// `sampleOptions.interfaceName`).
|
|
2011
|
+
function seedTemporalSnapshot(): void {
|
|
2012
|
+
setConversation("runtime-assembly-fallback", {
|
|
2013
|
+
conversationId: "runtime-assembly-fallback",
|
|
2014
|
+
workingDir: "/sandbox",
|
|
2015
|
+
workspaceTopLevelContext: "",
|
|
2016
|
+
workspaceTopLevelDirty: false,
|
|
2017
|
+
surfaceState: new Map(),
|
|
2018
|
+
currentTurnTemporalSnapshot: {
|
|
2019
|
+
clientTimezone: null,
|
|
2020
|
+
},
|
|
2021
|
+
currentTurnInterfaceContext: {
|
|
2022
|
+
userMessageInterface: "macos",
|
|
2023
|
+
assistantMessageInterface: "macos",
|
|
2024
|
+
},
|
|
2025
|
+
} as never);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
test("injects the turn-context block when the inputs are provided", async () => {
|
|
2029
|
+
seedTemporalSnapshot();
|
|
1706
2030
|
|
|
1707
|
-
test("injects unifiedTurnContext when provided", async () => {
|
|
1708
2031
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1709
|
-
|
|
2032
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1710
2033
|
});
|
|
1711
2034
|
|
|
1712
2035
|
expect(result).toHaveLength(1);
|
|
@@ -1720,25 +2043,21 @@ describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
|
1720
2043
|
);
|
|
1721
2044
|
});
|
|
1722
2045
|
|
|
1723
|
-
test("does not inject when
|
|
2046
|
+
test("does not inject when the timestamp snapshot is absent", async () => {
|
|
1724
2047
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1725
|
-
|
|
2048
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1726
2049
|
});
|
|
1727
2050
|
|
|
1728
2051
|
expect(result).toHaveLength(1);
|
|
1729
2052
|
expect(result[0].content).toHaveLength(1);
|
|
1730
2053
|
});
|
|
1731
2054
|
|
|
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
2055
|
test("injected in full mode", async () => {
|
|
2056
|
+
seedTemporalSnapshot();
|
|
2057
|
+
|
|
1740
2058
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1741
|
-
|
|
2059
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2060
|
+
...sampleOptions,
|
|
1742
2061
|
mode: "full",
|
|
1743
2062
|
});
|
|
1744
2063
|
|
|
@@ -1751,8 +2070,11 @@ describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
|
1751
2070
|
});
|
|
1752
2071
|
|
|
1753
2072
|
test("injected in minimal mode (no mode guard)", async () => {
|
|
2073
|
+
seedTemporalSnapshot();
|
|
2074
|
+
|
|
1754
2075
|
const { messages: result } = await applyRuntimeInjections(baseMessages, {
|
|
1755
|
-
|
|
2076
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2077
|
+
...sampleOptions,
|
|
1756
2078
|
mode: "minimal",
|
|
1757
2079
|
});
|
|
1758
2080
|
|
|
@@ -1765,11 +2087,168 @@ describe("applyRuntimeInjections with unifiedTurnContext", () => {
|
|
|
1765
2087
|
});
|
|
1766
2088
|
});
|
|
1767
2089
|
|
|
2090
|
+
// ---------------------------------------------------------------------------
|
|
2091
|
+
// applyRuntimeInjections self-resolves the config timezones
|
|
2092
|
+
// ---------------------------------------------------------------------------
|
|
2093
|
+
|
|
2094
|
+
describe("applyRuntimeInjections timezone resolution", () => {
|
|
2095
|
+
afterEach(() => {
|
|
2096
|
+
assemblyConfiguredUserTimezone = undefined;
|
|
2097
|
+
assemblyDetectedTimezone = undefined;
|
|
2098
|
+
clearConversations();
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
const baseMessages: Message[] = [
|
|
2102
|
+
{ role: "user", content: [{ type: "text", text: "Hello there" }] },
|
|
2103
|
+
];
|
|
2104
|
+
|
|
2105
|
+
// Seed the live fallback conversation with the per-turn temporal snapshot the
|
|
2106
|
+
// loop freezes after memory retrieval. `applyRuntimeInjections` sources the
|
|
2107
|
+
// client timezone and the long-absence gap from it, so the turn-context block
|
|
2108
|
+
// only emits when this snapshot is present.
|
|
2109
|
+
function seedTemporalSnapshot(
|
|
2110
|
+
clientTimezone: string | null,
|
|
2111
|
+
timeSinceLastMessage: string | null = null,
|
|
2112
|
+
): void {
|
|
2113
|
+
setConversation("runtime-assembly-fallback", {
|
|
2114
|
+
conversationId: "runtime-assembly-fallback",
|
|
2115
|
+
workingDir: "/sandbox",
|
|
2116
|
+
workspaceTopLevelContext: "",
|
|
2117
|
+
workspaceTopLevelDirty: false,
|
|
2118
|
+
surfaceState: new Map(),
|
|
2119
|
+
currentTurnTemporalSnapshot: {
|
|
2120
|
+
clientTimezone,
|
|
2121
|
+
timeSinceLastMessage,
|
|
2122
|
+
},
|
|
2123
|
+
} as never);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function injectedText(result: { messages: Message[] }): string {
|
|
2127
|
+
return result.messages[0].content
|
|
2128
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
2129
|
+
.map((b) => b.text)
|
|
2130
|
+
.join("\n");
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
test("sources the configured and detected timezones from config, not the options bag", async () => {
|
|
2134
|
+
/**
|
|
2135
|
+
* Verifies the re-homed configured user timezone and detected-timezone
|
|
2136
|
+
* fallback are read from `config.ui` inside `applyRuntimeInjections`
|
|
2137
|
+
* rather than threaded through the options bag. The detected timezone is
|
|
2138
|
+
* the device-timezone fallback used when the client reports none.
|
|
2139
|
+
*/
|
|
2140
|
+
// GIVEN the guardian has configured a user timezone in config
|
|
2141
|
+
assemblyConfiguredUserTimezone = "America/New_York";
|
|
2142
|
+
|
|
2143
|
+
// AND a different server-detected timezone is configured
|
|
2144
|
+
assemblyDetectedTimezone = "America/Los_Angeles";
|
|
2145
|
+
|
|
2146
|
+
// AND the turn's frozen snapshot carries no client timezone (so the
|
|
2147
|
+
// detected timezone is the device-timezone fallback)
|
|
2148
|
+
seedTemporalSnapshot(null);
|
|
2149
|
+
|
|
2150
|
+
// WHEN a turn is assembled
|
|
2151
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2152
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
// THEN the injector surfaces the mismatch using the config-sourced values
|
|
2156
|
+
const injected = injectedText(result);
|
|
2157
|
+
expect(injected).toContain("configured_user_timezone: America/New_York");
|
|
2158
|
+
expect(injected).toContain("client_device_timezone: America/Los_Angeles");
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
test("sources clientTimezone from the conversation's frozen turn snapshot", async () => {
|
|
2162
|
+
/**
|
|
2163
|
+
* Verifies the per-turn client timezone is taken from the conversation's
|
|
2164
|
+
* frozen `currentTurnTemporalSnapshot` and takes precedence over the
|
|
2165
|
+
* config-sourced detected-timezone fallback.
|
|
2166
|
+
*/
|
|
2167
|
+
// GIVEN the configured user timezone and a detected fallback in config
|
|
2168
|
+
assemblyConfiguredUserTimezone = "America/New_York";
|
|
2169
|
+
assemblyDetectedTimezone = "America/Denver";
|
|
2170
|
+
|
|
2171
|
+
// AND the turn's frozen snapshot carries a client timezone
|
|
2172
|
+
seedTemporalSnapshot("America/Los_Angeles");
|
|
2173
|
+
|
|
2174
|
+
// WHEN a turn is assembled
|
|
2175
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2176
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
// THEN the injector surfaces the mismatch using the snapshot's client value
|
|
2180
|
+
const injected = injectedText(result);
|
|
2181
|
+
expect(injected).toContain("configured_user_timezone: America/New_York");
|
|
2182
|
+
expect(injected).toContain("client_device_timezone: America/Los_Angeles");
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
test("omits the mismatch lines when config and client timezone agree", async () => {
|
|
2186
|
+
/**
|
|
2187
|
+
* Verifies no mismatch affordance is emitted when the config-sourced
|
|
2188
|
+
* configured user timezone matches the snapshot's client timezone.
|
|
2189
|
+
*/
|
|
2190
|
+
// GIVEN the configured user timezone matches the client timezone
|
|
2191
|
+
assemblyConfiguredUserTimezone = "America/New_York";
|
|
2192
|
+
|
|
2193
|
+
// AND the turn's frozen snapshot carries the matching client timezone
|
|
2194
|
+
seedTemporalSnapshot("America/New_York");
|
|
2195
|
+
|
|
2196
|
+
// WHEN a turn is assembled
|
|
2197
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2198
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
// THEN no timezone-mismatch lines are injected
|
|
2202
|
+
const injected = injectedText(result);
|
|
2203
|
+
expect(injected).not.toContain("configured_user_timezone:");
|
|
2204
|
+
expect(injected).not.toContain("client_device_timezone:");
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
test("sources time_since_last_message from the conversation's frozen turn snapshot", async () => {
|
|
2208
|
+
/**
|
|
2209
|
+
* Verifies the long-absence gap is taken from the conversation's frozen
|
|
2210
|
+
* `currentTurnTemporalSnapshot` rather than threaded through the options
|
|
2211
|
+
* bag, so every post-compaction re-injection reuses the turn-start value.
|
|
2212
|
+
*/
|
|
2213
|
+
// GIVEN the turn's frozen snapshot carries a long-absence gap
|
|
2214
|
+
seedTemporalSnapshot(null, "2d ago");
|
|
2215
|
+
|
|
2216
|
+
// WHEN a turn is assembled without the gap in the options bag
|
|
2217
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2218
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
// THEN the injector surfaces the gap from the snapshot
|
|
2222
|
+
const injected = injectedText(result);
|
|
2223
|
+
expect(injected).toContain("time_since_last_message: 2d ago");
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
test("omits time_since_last_message when the snapshot carries no gap", async () => {
|
|
2227
|
+
/**
|
|
2228
|
+
* Verifies no `time_since_last_message` line is emitted when the frozen
|
|
2229
|
+
* snapshot's gap is null (the normal back-and-forth case).
|
|
2230
|
+
*/
|
|
2231
|
+
// GIVEN the turn's frozen snapshot carries no long-absence gap
|
|
2232
|
+
seedTemporalSnapshot(null, null);
|
|
2233
|
+
|
|
2234
|
+
// WHEN a turn is assembled
|
|
2235
|
+
const result = await applyRuntimeInjections(baseMessages, {
|
|
2236
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
// THEN no gap line is injected
|
|
2240
|
+
const injected = injectedText(result);
|
|
2241
|
+
expect(injected).not.toContain("time_since_last_message");
|
|
2242
|
+
});
|
|
2243
|
+
});
|
|
2244
|
+
|
|
1768
2245
|
// ---------------------------------------------------------------------------
|
|
1769
2246
|
// applyRuntimeInjections blocks.unifiedTurnContext
|
|
1770
2247
|
// ---------------------------------------------------------------------------
|
|
1771
2248
|
|
|
1772
2249
|
describe("applyRuntimeInjections blocks.unifiedTurnContext", () => {
|
|
2250
|
+
afterEach(clearConversations);
|
|
2251
|
+
|
|
1773
2252
|
const userTailMessages: Message[] = [
|
|
1774
2253
|
{
|
|
1775
2254
|
role: "user",
|
|
@@ -1777,18 +2256,50 @@ describe("applyRuntimeInjections blocks.unifiedTurnContext", () => {
|
|
|
1777
2256
|
},
|
|
1778
2257
|
];
|
|
1779
2258
|
|
|
1780
|
-
const
|
|
1781
|
-
|
|
2259
|
+
const sampleOptions = {
|
|
2260
|
+
interfaceName: "macos",
|
|
2261
|
+
};
|
|
2262
|
+
// The block reflects the options-bag interface and the `current_time` the
|
|
2263
|
+
// injector computes live (pinned to `FIXED_TURN_TIMESTAMP` for this suite).
|
|
2264
|
+
const sampleBlock = buildUnifiedTurnContextBlock({
|
|
2265
|
+
timestamp: FIXED_TURN_TIMESTAMP,
|
|
2266
|
+
...sampleOptions,
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
// Seed the live fallback conversation with the per-turn temporal snapshot
|
|
2270
|
+
// (its presence gates the `<turn_context>` block) and the turn interface
|
|
2271
|
+
// context (so the injector sources the interface label to match
|
|
2272
|
+
// `sampleOptions.interfaceName`).
|
|
2273
|
+
function seedTemporalSnapshot(): void {
|
|
2274
|
+
setConversation("runtime-assembly-fallback", {
|
|
2275
|
+
conversationId: "runtime-assembly-fallback",
|
|
2276
|
+
workingDir: "/sandbox",
|
|
2277
|
+
workspaceTopLevelContext: "",
|
|
2278
|
+
workspaceTopLevelDirty: false,
|
|
2279
|
+
surfaceState: new Map(),
|
|
2280
|
+
currentTurnTemporalSnapshot: {
|
|
2281
|
+
clientTimezone: null,
|
|
2282
|
+
},
|
|
2283
|
+
currentTurnInterfaceContext: {
|
|
2284
|
+
userMessageInterface: "macos",
|
|
2285
|
+
assistantMessageInterface: "macos",
|
|
2286
|
+
},
|
|
2287
|
+
} as never);
|
|
2288
|
+
}
|
|
1782
2289
|
|
|
1783
2290
|
test("captures unifiedTurnContext when tail is a user message", async () => {
|
|
2291
|
+
seedTemporalSnapshot();
|
|
2292
|
+
|
|
1784
2293
|
const result = await applyRuntimeInjections(userTailMessages, {
|
|
1785
|
-
|
|
2294
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1786
2295
|
});
|
|
1787
2296
|
|
|
1788
2297
|
expect(result.blocks.unifiedTurnContext).toBe(sampleBlock);
|
|
1789
2298
|
});
|
|
1790
2299
|
|
|
1791
2300
|
test("does not capture when tail is not a user message", async () => {
|
|
2301
|
+
seedTemporalSnapshot();
|
|
2302
|
+
|
|
1792
2303
|
const assistantTailMessages: Message[] = [
|
|
1793
2304
|
{
|
|
1794
2305
|
role: "user",
|
|
@@ -1801,14 +2312,16 @@ describe("applyRuntimeInjections blocks.unifiedTurnContext", () => {
|
|
|
1801
2312
|
];
|
|
1802
2313
|
|
|
1803
2314
|
const result = await applyRuntimeInjections(assistantTailMessages, {
|
|
1804
|
-
|
|
2315
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1805
2316
|
});
|
|
1806
2317
|
|
|
1807
2318
|
expect(result.blocks.unifiedTurnContext).toBeUndefined();
|
|
1808
2319
|
});
|
|
1809
2320
|
|
|
1810
2321
|
test("does not capture when unifiedTurnContext option is absent", async () => {
|
|
1811
|
-
const result = await applyRuntimeInjections(userTailMessages, {
|
|
2322
|
+
const result = await applyRuntimeInjections(userTailMessages, {
|
|
2323
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2324
|
+
});
|
|
1812
2325
|
|
|
1813
2326
|
expect(result.blocks.unifiedTurnContext).toBeUndefined();
|
|
1814
2327
|
});
|
|
@@ -1844,6 +2357,35 @@ function makeSubagentState(
|
|
|
1844
2357
|
};
|
|
1845
2358
|
}
|
|
1846
2359
|
|
|
2360
|
+
// `applyRuntimeInjections` self-resolves the `<active_subagents>` block from the
|
|
2361
|
+
// global subagent manager keyed by the conversation, so tests seed children
|
|
2362
|
+
// directly into the manager's private maps (mirroring the production source)
|
|
2363
|
+
// rather than threading a pre-built block through options.
|
|
2364
|
+
interface SubagentManagerTestInternals {
|
|
2365
|
+
subagents: Map<string, { state: SubagentState }>;
|
|
2366
|
+
parentToChildren: Map<string, Set<string>>;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
function seedSubagentChild(
|
|
2370
|
+
parentConversationId: string,
|
|
2371
|
+
state: SubagentState,
|
|
2372
|
+
): void {
|
|
2373
|
+
const internals =
|
|
2374
|
+
getSubagentManager() as unknown as SubagentManagerTestInternals;
|
|
2375
|
+
internals.subagents.set(state.config.id, { state });
|
|
2376
|
+
const ids =
|
|
2377
|
+
internals.parentToChildren.get(parentConversationId) ?? new Set<string>();
|
|
2378
|
+
ids.add(state.config.id);
|
|
2379
|
+
internals.parentToChildren.set(parentConversationId, ids);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
function clearSeededSubagents(): void {
|
|
2383
|
+
const internals =
|
|
2384
|
+
getSubagentManager() as unknown as SubagentManagerTestInternals;
|
|
2385
|
+
internals.subagents.clear();
|
|
2386
|
+
internals.parentToChildren.clear();
|
|
2387
|
+
}
|
|
2388
|
+
|
|
1847
2389
|
describe("buildSubagentStatusBlock", () => {
|
|
1848
2390
|
test("returns null for empty children array", () => {
|
|
1849
2391
|
expect(buildSubagentStatusBlock([])).toBeNull();
|
|
@@ -1921,25 +2463,78 @@ describe("applyRuntimeInjections — subagent status", () => {
|
|
|
1921
2463
|
content: [{ type: "text", text: "user message" }],
|
|
1922
2464
|
};
|
|
1923
2465
|
|
|
1924
|
-
|
|
2466
|
+
afterEach(() => {
|
|
2467
|
+
clearSeededSubagents();
|
|
2468
|
+
clearConversations();
|
|
2469
|
+
});
|
|
2470
|
+
|
|
2471
|
+
test("includes self-resolved subagent status in full mode", async () => {
|
|
2472
|
+
// GIVEN the (fallback) parent conversation has one running child subagent
|
|
2473
|
+
seedSubagentChild(
|
|
2474
|
+
"runtime-assembly-fallback",
|
|
2475
|
+
makeSubagentState({ id: "child-1", label: "worker", status: "running" }),
|
|
2476
|
+
);
|
|
2477
|
+
|
|
2478
|
+
// WHEN runtime injections run in full mode
|
|
1925
2479
|
const { messages: result } = await applyRuntimeInjections([userMsg], {
|
|
1926
|
-
|
|
1927
|
-
"<active_subagents>\n- [running] test\n</active_subagents>",
|
|
2480
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1928
2481
|
mode: "full",
|
|
1929
2482
|
});
|
|
2483
|
+
|
|
2484
|
+
// THEN the tail user message carries the `<active_subagents>` block the
|
|
2485
|
+
// injector self-resolved from the subagent manager
|
|
1930
2486
|
const tail = result[result.length - 1];
|
|
1931
2487
|
const texts = tail.content
|
|
1932
2488
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
1933
2489
|
.map((b) => b.text);
|
|
1934
2490
|
expect(texts.some((t) => t.includes("<active_subagents>"))).toBe(true);
|
|
2491
|
+
expect(texts.some((t) => t.includes('"worker"'))).toBe(true);
|
|
1935
2492
|
});
|
|
1936
2493
|
|
|
1937
2494
|
test("skips subagent status in minimal mode", async () => {
|
|
2495
|
+
// GIVEN the (fallback) parent conversation has one running child subagent
|
|
2496
|
+
seedSubagentChild(
|
|
2497
|
+
"runtime-assembly-fallback",
|
|
2498
|
+
makeSubagentState({ id: "child-1", label: "worker", status: "running" }),
|
|
2499
|
+
);
|
|
2500
|
+
|
|
2501
|
+
// WHEN runtime injections run in minimal mode
|
|
1938
2502
|
const { messages: result } = await applyRuntimeInjections([userMsg], {
|
|
1939
|
-
|
|
1940
|
-
"<active_subagents>\n- [running] test\n</active_subagents>",
|
|
2503
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
1941
2504
|
mode: "minimal",
|
|
1942
2505
|
});
|
|
2506
|
+
|
|
2507
|
+
// THEN the subagent status block is gated out
|
|
2508
|
+
const tail = result[result.length - 1];
|
|
2509
|
+
const texts = tail.content
|
|
2510
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
2511
|
+
.map((b) => b.text);
|
|
2512
|
+
expect(texts.some((t) => t.includes("<active_subagents>"))).toBe(false);
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
test("skips subagent status when the conversation is itself a subagent", async () => {
|
|
2516
|
+
// GIVEN the live conversation is itself a subagent (no nesting)
|
|
2517
|
+
setConversation("runtime-assembly-fallback", {
|
|
2518
|
+
conversationId: "runtime-assembly-fallback",
|
|
2519
|
+
workingDir: "/sandbox",
|
|
2520
|
+
workspaceTopLevelContext: "",
|
|
2521
|
+
workspaceTopLevelDirty: false,
|
|
2522
|
+
surfaceState: new Map(),
|
|
2523
|
+
isSubagent: true,
|
|
2524
|
+
} as never);
|
|
2525
|
+
// AND a child is registered under its id
|
|
2526
|
+
seedSubagentChild(
|
|
2527
|
+
"runtime-assembly-fallback",
|
|
2528
|
+
makeSubagentState({ id: "child-1", label: "worker", status: "running" }),
|
|
2529
|
+
);
|
|
2530
|
+
|
|
2531
|
+
// WHEN runtime injections run in full mode
|
|
2532
|
+
const { messages: result } = await applyRuntimeInjections([userMsg], {
|
|
2533
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2534
|
+
mode: "full",
|
|
2535
|
+
});
|
|
2536
|
+
|
|
2537
|
+
// THEN no `<active_subagents>` block is emitted
|
|
1943
2538
|
const tail = result[result.length - 1];
|
|
1944
2539
|
const texts = tail.content
|
|
1945
2540
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
@@ -2012,6 +2607,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
|
|
|
2012
2607
|
|
|
2013
2608
|
function makePkbOptions(overrides: Record<string, unknown> = {}) {
|
|
2014
2609
|
return {
|
|
2610
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2015
2611
|
...overrides,
|
|
2016
2612
|
};
|
|
2017
2613
|
}
|
|
@@ -2298,7 +2894,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
|
|
|
2298
2894
|
// filtered out.
|
|
2299
2895
|
const { messages: initialResult } = await applyRuntimeInjections(
|
|
2300
2896
|
preCompactionMessages,
|
|
2301
|
-
{},
|
|
2897
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2302
2898
|
);
|
|
2303
2899
|
// Unwrap the injected reminder from the last user message.
|
|
2304
2900
|
const initialTexts = extractTexts(initialResult);
|
|
@@ -2332,7 +2928,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
|
|
|
2332
2928
|
// should be filtered, and beta (no longer "in context") should appear.
|
|
2333
2929
|
const { messages: rebuiltResult } = await applyRuntimeInjections(
|
|
2334
2930
|
postCompactionMessages,
|
|
2335
|
-
{},
|
|
2931
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2336
2932
|
);
|
|
2337
2933
|
const rebuiltTexts = extractTexts(rebuiltResult);
|
|
2338
2934
|
const rebuiltReminder = rebuiltTexts.find(
|
|
@@ -2352,6 +2948,19 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
|
|
|
2352
2948
|
// ---------------------------------------------------------------------------
|
|
2353
2949
|
|
|
2354
2950
|
describe("Slack channel chronological rendering — multi-thread", () => {
|
|
2951
|
+
afterEach(() => {
|
|
2952
|
+
clearConversations();
|
|
2953
|
+
// The focus-block tests persist rows under the runtime-assembly fallback
|
|
2954
|
+
// conversation; clear them so later tests resolve against an empty DB.
|
|
2955
|
+
const db = getDb();
|
|
2956
|
+
db.delete(messages)
|
|
2957
|
+
.where(eq(messages.conversationId, "runtime-assembly-fallback"))
|
|
2958
|
+
.run();
|
|
2959
|
+
db.delete(conversations)
|
|
2960
|
+
.where(eq(conversations.id, "runtime-assembly-fallback"))
|
|
2961
|
+
.run();
|
|
2962
|
+
});
|
|
2963
|
+
|
|
2355
2964
|
// Slack ts values are seconds-since-epoch with microsecond precision.
|
|
2356
2965
|
// Pick a few stable anchors so thread aliases (sha-derived) stay
|
|
2357
2966
|
// predictable across the scenarios.
|
|
@@ -2395,6 +3004,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2395
3004
|
content: JSON.stringify([{ type: "text", text: opts.text }]),
|
|
2396
3005
|
createdAt: opts.createdAt,
|
|
2397
3006
|
metadata: Object.keys(outer).length > 0 ? JSON.stringify(outer) : null,
|
|
3007
|
+
clientMessageId: null,
|
|
2398
3008
|
};
|
|
2399
3009
|
}
|
|
2400
3010
|
|
|
@@ -2413,6 +3023,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2413
3023
|
content: JSON.stringify([{ type: "text", text: opts.text }]),
|
|
2414
3024
|
createdAt: opts.createdAt,
|
|
2415
3025
|
metadata: Object.keys(outer).length > 0 ? JSON.stringify(outer) : null,
|
|
3026
|
+
clientMessageId: null,
|
|
2416
3027
|
};
|
|
2417
3028
|
}
|
|
2418
3029
|
|
|
@@ -2428,18 +3039,16 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2428
3039
|
supportsVoiceInput: false,
|
|
2429
3040
|
chatType: "channel",
|
|
2430
3041
|
};
|
|
2431
|
-
const slackChronologicalMessages = loadSlackChronologicalMessages(
|
|
2432
|
-
"conv-1",
|
|
2433
|
-
slackChannelCaps,
|
|
2434
|
-
{ loader: () => rows, trustClass: "guardian" },
|
|
2435
|
-
);
|
|
2436
3042
|
const lastUserMessage: Message = {
|
|
2437
3043
|
role: "user",
|
|
2438
3044
|
content: [{ type: "text", text: "current turn" }],
|
|
2439
3045
|
};
|
|
3046
|
+
// Persist the rows + live conversation so `applyRuntimeInjections`
|
|
3047
|
+
// self-resolves the chronological transcript from conversation state,
|
|
3048
|
+
// exactly as production does.
|
|
3049
|
+
seedSlackChannelConversationWithRows(slackChannelCaps, rows);
|
|
2440
3050
|
const { messages } = await applyRuntimeInjections([lastUserMessage], {
|
|
2441
|
-
|
|
2442
|
-
slackChronologicalMessages,
|
|
3051
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
2443
3052
|
});
|
|
2444
3053
|
return messages;
|
|
2445
3054
|
}
|
|
@@ -2726,24 +3335,15 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2726
3335
|
role: "user",
|
|
2727
3336
|
content: [{ type: "text", text: "vellum question" }],
|
|
2728
3337
|
};
|
|
3338
|
+
seedChannelCapabilitiesConversation({
|
|
3339
|
+
channel: "vellum",
|
|
3340
|
+
dashboardCapable: true,
|
|
3341
|
+
supportsDynamicUi: true,
|
|
3342
|
+
supportsVoiceInput: true,
|
|
3343
|
+
});
|
|
2729
3344
|
const { messages: result } = await applyRuntimeInjections(
|
|
2730
3345
|
[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
|
-
},
|
|
3346
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2747
3347
|
);
|
|
2748
3348
|
expect(result.length).toBe(1);
|
|
2749
3349
|
const allText = result[0].content
|
|
@@ -2751,7 +3351,6 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2751
3351
|
.map((b) => b.text)
|
|
2752
3352
|
.join("\n");
|
|
2753
3353
|
expect(allText).toContain("vellum question");
|
|
2754
|
-
expect(allText).not.toContain("should not appear");
|
|
2755
3354
|
});
|
|
2756
3355
|
|
|
2757
3356
|
// ── DMs (chatType === "im") use chronological rendering ────────────────
|
|
@@ -2764,36 +3363,38 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2764
3363
|
role: "user",
|
|
2765
3364
|
content: [{ type: "text", text: "DM question" }],
|
|
2766
3365
|
};
|
|
2767
|
-
const
|
|
2768
|
-
|
|
3366
|
+
const rows: MessageRow[] = [
|
|
3367
|
+
userRow({
|
|
3368
|
+
id: "dm-1",
|
|
3369
|
+
createdAt: 1700000000_000,
|
|
3370
|
+
text: "earlier DM line",
|
|
3371
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3372
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3373
|
+
}),
|
|
3374
|
+
assistantRow({
|
|
3375
|
+
id: "dm-2",
|
|
3376
|
+
createdAt: 1700000005_000,
|
|
3377
|
+
text: "prior reply",
|
|
3378
|
+
slackMeta: buildSlackMeta({ channelTs: T0_REPLY1 }),
|
|
3379
|
+
}),
|
|
3380
|
+
];
|
|
3381
|
+
seedSlackChannelConversationWithRows(
|
|
2769
3382
|
{
|
|
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
|
-
],
|
|
3383
|
+
channel: "slack",
|
|
3384
|
+
dashboardCapable: false,
|
|
3385
|
+
supportsDynamicUi: false,
|
|
3386
|
+
supportsVoiceInput: false,
|
|
3387
|
+
chatType: "im",
|
|
2792
3388
|
},
|
|
3389
|
+
rows,
|
|
2793
3390
|
);
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
3391
|
+
const { messages: result } = await applyRuntimeInjections(
|
|
3392
|
+
[lastUserMessage],
|
|
3393
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3394
|
+
);
|
|
3395
|
+
// The self-resolved chronological transcript REPLACES the default
|
|
3396
|
+
// runMessages, so the inbound `DM question` text does not appear — only
|
|
3397
|
+
// the rendered transcript lines do (plus any non-Slack injections).
|
|
2797
3398
|
const allText = result
|
|
2798
3399
|
.flatMap((m) => m.content)
|
|
2799
3400
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
@@ -2804,6 +3405,62 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2804
3405
|
expect(allText).not.toContain("DM question");
|
|
2805
3406
|
});
|
|
2806
3407
|
|
|
3408
|
+
// ── Compaction boundary scopes the self-resolved transcript ──────────
|
|
3409
|
+
// The transcript self-resolves from persisted rows scoped by the
|
|
3410
|
+
// conversation's Slack compaction boundary. Rows at or before the
|
|
3411
|
+
// persisted watermark were folded into the summary, so a fresh load
|
|
3412
|
+
// excludes them and surfaces only the post-watermark tail — returning
|
|
3413
|
+
// the compacted view rather than resurrecting folded history.
|
|
3414
|
+
test("self-resolved transcript respects the Slack compaction watermark", async () => {
|
|
3415
|
+
const slackCaps: ChannelCapabilities = {
|
|
3416
|
+
channel: "slack",
|
|
3417
|
+
dashboardCapable: false,
|
|
3418
|
+
supportsDynamicUi: false,
|
|
3419
|
+
supportsVoiceInput: false,
|
|
3420
|
+
chatType: "channel",
|
|
3421
|
+
};
|
|
3422
|
+
const rows: MessageRow[] = [
|
|
3423
|
+
userRow({
|
|
3424
|
+
id: "folded",
|
|
3425
|
+
createdAt: 1700000000_000,
|
|
3426
|
+
text: "folded pre-compaction line",
|
|
3427
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3428
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3429
|
+
}),
|
|
3430
|
+
userRow({
|
|
3431
|
+
id: "fresh",
|
|
3432
|
+
createdAt: 1700000030_000,
|
|
3433
|
+
text: "fresh post-compaction line",
|
|
3434
|
+
slackMeta: buildSlackMeta({ channelTs: T2, displayName: "bob" }),
|
|
3435
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3436
|
+
}),
|
|
3437
|
+
];
|
|
3438
|
+
// A persisted watermark at T0 folds the first row into the summary.
|
|
3439
|
+
seedSlackChannelConversationWithRows(slackCaps, rows, {
|
|
3440
|
+
slackContextCompactionWatermarkTs: T0,
|
|
3441
|
+
contextCompactedMessageCount: 1,
|
|
3442
|
+
});
|
|
3443
|
+
const { messages: result } = await applyRuntimeInjections(
|
|
3444
|
+
[
|
|
3445
|
+
{
|
|
3446
|
+
role: "user",
|
|
3447
|
+
content: [{ type: "text", text: "post-compaction question" }],
|
|
3448
|
+
},
|
|
3449
|
+
],
|
|
3450
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3451
|
+
);
|
|
3452
|
+
const allText = result
|
|
3453
|
+
.flatMap((m) => m.content)
|
|
3454
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
3455
|
+
.map((b) => b.text)
|
|
3456
|
+
.join("\n");
|
|
3457
|
+
// The folded row stays in the summary; only the post-watermark tail is
|
|
3458
|
+
// surfaced, and it replaces the default runMessages.
|
|
3459
|
+
expect(allText).toContain("fresh post-compaction line");
|
|
3460
|
+
expect(allText).not.toContain("folded pre-compaction line");
|
|
3461
|
+
expect(allText).not.toContain("post-compaction question");
|
|
3462
|
+
});
|
|
3463
|
+
|
|
2807
3464
|
// ── Memory-injection carry-through on slack replacement ──────────────
|
|
2808
3465
|
// `graphMemory.prepareMemory` prepends `<memory __injected>` (and
|
|
2809
3466
|
// optional memory-image groups) to the last user message BEFORE the
|
|
@@ -2828,21 +3485,23 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2828
3485
|
type: "text",
|
|
2829
3486
|
text: "<memory __injected>\nrecalled fact about the user\n</memory>",
|
|
2830
3487
|
},
|
|
2831
|
-
{ type: "text", text: "
|
|
3488
|
+
{ type: "text", text: "original turn text" },
|
|
2832
3489
|
],
|
|
2833
3490
|
},
|
|
2834
3491
|
];
|
|
3492
|
+
const rows: MessageRow[] = [
|
|
3493
|
+
userRow({
|
|
3494
|
+
id: "mem-1",
|
|
3495
|
+
createdAt: 1700000000_000,
|
|
3496
|
+
text: "recalled conversation line",
|
|
3497
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3498
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3499
|
+
}),
|
|
3500
|
+
];
|
|
3501
|
+
seedSlackChannelConversationWithRows(slackCaps, rows);
|
|
2835
3502
|
const { messages: result } = await applyRuntimeInjections(
|
|
2836
3503
|
runMessagesWithMemory,
|
|
2837
|
-
{
|
|
2838
|
-
channelCapabilities: slackCaps,
|
|
2839
|
-
slackChronologicalMessages: [
|
|
2840
|
-
{
|
|
2841
|
-
role: "user",
|
|
2842
|
-
content: [{ type: "text", text: "[19:55 alice]: hello there" }],
|
|
2843
|
-
},
|
|
2844
|
-
],
|
|
2845
|
-
},
|
|
3504
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2846
3505
|
);
|
|
2847
3506
|
const tail = result[result.length - 1];
|
|
2848
3507
|
expect(tail.role).toBe("user");
|
|
@@ -2852,15 +3511,15 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2852
3511
|
.join("\n");
|
|
2853
3512
|
expect(allText).toContain("<memory __injected>");
|
|
2854
3513
|
expect(allText).toContain("recalled fact about the user");
|
|
2855
|
-
expect(allText).toContain("
|
|
3514
|
+
expect(allText).toContain("recalled conversation line");
|
|
2856
3515
|
// Memory block must appear before the Slack transcript tail so the
|
|
2857
3516
|
// model sees recalled context ahead of the conversation view.
|
|
2858
3517
|
const memoryIdx = allText.indexOf("<memory __injected>");
|
|
2859
|
-
const transcriptIdx = allText.indexOf("
|
|
3518
|
+
const transcriptIdx = allText.indexOf("recalled conversation line");
|
|
2860
3519
|
expect(memoryIdx).toBeLessThan(transcriptIdx);
|
|
2861
|
-
// The pre-replacement
|
|
2862
|
-
//
|
|
2863
|
-
expect(allText.
|
|
3520
|
+
// The pre-replacement inbound turn text must NOT leak through — only the
|
|
3521
|
+
// carried memory prefix + the self-resolved transcript tail remain.
|
|
3522
|
+
expect(allText).not.toContain("original turn text");
|
|
2864
3523
|
});
|
|
2865
3524
|
|
|
2866
3525
|
test("slack replacement preserves memory-image groups + text block", async () => {
|
|
@@ -2896,17 +3555,19 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2896
3555
|
],
|
|
2897
3556
|
},
|
|
2898
3557
|
];
|
|
3558
|
+
const rows: MessageRow[] = [
|
|
3559
|
+
userRow({
|
|
3560
|
+
id: "mem-img-1",
|
|
3561
|
+
createdAt: 1700000000_000,
|
|
3562
|
+
text: "recalled conversation line",
|
|
3563
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3564
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3565
|
+
}),
|
|
3566
|
+
];
|
|
3567
|
+
seedSlackChannelConversationWithRows(slackCaps, rows);
|
|
2899
3568
|
const { messages: result } = await applyRuntimeInjections(
|
|
2900
3569
|
runMessagesWithMemory,
|
|
2901
|
-
{
|
|
2902
|
-
channelCapabilities: slackCaps,
|
|
2903
|
-
slackChronologicalMessages: [
|
|
2904
|
-
{
|
|
2905
|
-
role: "user",
|
|
2906
|
-
content: [{ type: "text", text: "[19:55 alice]: transcript line" }],
|
|
2907
|
-
},
|
|
2908
|
-
],
|
|
2909
|
-
},
|
|
3570
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2910
3571
|
);
|
|
2911
3572
|
const tail = result[result.length - 1];
|
|
2912
3573
|
expect(tail.role).toBe("user");
|
|
@@ -2921,7 +3582,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2921
3582
|
expect(allText).toContain("<memory_image __injected>");
|
|
2922
3583
|
expect(allText).toContain("</memory_image>");
|
|
2923
3584
|
expect(allText).toContain("<memory __injected>");
|
|
2924
|
-
expect(allText).toContain("
|
|
3585
|
+
expect(allText).toContain("recalled conversation line");
|
|
2925
3586
|
// The original turn text (before the Slack replacement) must NOT
|
|
2926
3587
|
// leak through — only the memory prefix + transcript tail are kept.
|
|
2927
3588
|
expect(allText).not.toContain("original turn text");
|
|
@@ -2935,17 +3596,19 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2935
3596
|
supportsVoiceInput: false,
|
|
2936
3597
|
chatType: "im",
|
|
2937
3598
|
};
|
|
3599
|
+
const rows: MessageRow[] = [
|
|
3600
|
+
userRow({
|
|
3601
|
+
id: "no-prefix-1",
|
|
3602
|
+
createdAt: 1700000000_000,
|
|
3603
|
+
text: "only transcript line",
|
|
3604
|
+
slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
|
|
3605
|
+
extraOuterMetadata: { provenanceTrustClass: "guardian" },
|
|
3606
|
+
}),
|
|
3607
|
+
];
|
|
3608
|
+
seedSlackChannelConversationWithRows(slackCaps, rows);
|
|
2938
3609
|
const { messages: result } = await applyRuntimeInjections(
|
|
2939
3610
|
[{ 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
|
-
},
|
|
3611
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2949
3612
|
);
|
|
2950
3613
|
const tail = result[result.length - 1];
|
|
2951
3614
|
const allText = tail.content
|
|
@@ -2953,7 +3616,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2953
3616
|
.map((b) => b.text)
|
|
2954
3617
|
.join("\n");
|
|
2955
3618
|
expect(allText).not.toContain("<memory __injected>");
|
|
2956
|
-
expect(allText).toContain("
|
|
3619
|
+
expect(allText).toContain("only transcript line");
|
|
2957
3620
|
});
|
|
2958
3621
|
|
|
2959
3622
|
// ── transport_hints suppression for slack channels ────────────────────
|
|
@@ -2965,27 +3628,12 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
2965
3628
|
supportsVoiceInput: false,
|
|
2966
3629
|
chatType: "channel",
|
|
2967
3630
|
};
|
|
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
|
-
|
|
3631
|
+
seedChannelCapabilitiesConversation(slackChannelCaps, [
|
|
3632
|
+
"thread context: ...",
|
|
3633
|
+
]);
|
|
2982
3634
|
const { messages: result } = await applyRuntimeInjections(
|
|
2983
3635
|
[{ role: "user", content: [{ type: "text", text: "current turn" }] }],
|
|
2984
|
-
{
|
|
2985
|
-
channelCapabilities: slackChannelCaps,
|
|
2986
|
-
slackChronologicalMessages,
|
|
2987
|
-
transportHints: ["thread context: ..."],
|
|
2988
|
-
},
|
|
3636
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
2989
3637
|
);
|
|
2990
3638
|
|
|
2991
3639
|
const allText = result
|
|
@@ -3009,12 +3657,10 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3009
3657
|
chatType: "im",
|
|
3010
3658
|
};
|
|
3011
3659
|
|
|
3660
|
+
seedChannelCapabilitiesConversation(slackDmCaps, ["dm context: ..."]);
|
|
3012
3661
|
const { messages: result } = await applyRuntimeInjections(
|
|
3013
3662
|
[{ role: "user", content: [{ type: "text", text: "hi DM" }] }],
|
|
3014
|
-
{
|
|
3015
|
-
channelCapabilities: slackDmCaps,
|
|
3016
|
-
transportHints: ["dm context: ..."],
|
|
3017
|
-
},
|
|
3663
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3018
3664
|
);
|
|
3019
3665
|
|
|
3020
3666
|
const allText = result
|
|
@@ -3028,18 +3674,19 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3028
3674
|
|
|
3029
3675
|
// ── transport_hints kept for non-slack channels ───────────────────────
|
|
3030
3676
|
test("non-slack conversations still receive <transport_hints>", async () => {
|
|
3031
|
-
|
|
3032
|
-
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
3677
|
+
seedChannelCapabilitiesConversation(
|
|
3033
3678
|
{
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
chatType: "private",
|
|
3040
|
-
},
|
|
3041
|
-
transportHints: ["please answer concisely"],
|
|
3679
|
+
channel: "telegram",
|
|
3680
|
+
dashboardCapable: false,
|
|
3681
|
+
supportsDynamicUi: false,
|
|
3682
|
+
supportsVoiceInput: false,
|
|
3683
|
+
chatType: "private",
|
|
3042
3684
|
},
|
|
3685
|
+
["please answer concisely"],
|
|
3686
|
+
);
|
|
3687
|
+
const { messages: result } = await applyRuntimeInjections(
|
|
3688
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
3689
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3043
3690
|
);
|
|
3044
3691
|
const allText = result
|
|
3045
3692
|
.flatMap((m) => m.content)
|
|
@@ -3336,10 +3983,72 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3336
3983
|
// interleaved. The block is non-persisted: replays / re-injections strip
|
|
3337
3984
|
// any prior `<active_thread>` blocks via `RUNTIME_INJECTION_PREFIXES`.
|
|
3338
3985
|
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3986
|
+
interface SeededCompactionState {
|
|
3987
|
+
slackContextCompactionWatermarkTs?: string | null;
|
|
3988
|
+
contextCompactedMessageCount?: number;
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
// Persist the Slack rows under the runtime-assembly fallback conversation and
|
|
3992
|
+
// mirror the channel capabilities + trust class + compaction state onto its
|
|
3993
|
+
// live registry entry, so `applyRuntimeInjections` self-resolves the
|
|
3994
|
+
// `<active_thread>` focus block from the conversation's stored rows — the same
|
|
3995
|
+
// way it does in production, where the injector reads the live conversation
|
|
3996
|
+
// rather than receiving a pre-built block.
|
|
3997
|
+
function seedSlackChannelConversationWithRows(
|
|
3998
|
+
caps: ChannelCapabilities,
|
|
3999
|
+
rows: MessageRow[],
|
|
4000
|
+
compaction: SeededCompactionState = {},
|
|
4001
|
+
): void {
|
|
4002
|
+
const db = getDb();
|
|
4003
|
+
const now = Date.now();
|
|
4004
|
+
db.delete(messages)
|
|
4005
|
+
.where(eq(messages.conversationId, "runtime-assembly-fallback"))
|
|
4006
|
+
.run();
|
|
4007
|
+
db.insert(conversations)
|
|
4008
|
+
.values({
|
|
4009
|
+
id: "runtime-assembly-fallback",
|
|
4010
|
+
createdAt: now,
|
|
4011
|
+
updatedAt: now,
|
|
4012
|
+
})
|
|
4013
|
+
.run();
|
|
4014
|
+
if (rows.length > 0) {
|
|
4015
|
+
db.insert(messages)
|
|
4016
|
+
.values(
|
|
4017
|
+
rows.map((r) => ({
|
|
4018
|
+
id: r.id,
|
|
4019
|
+
conversationId: "runtime-assembly-fallback",
|
|
4020
|
+
role: r.role,
|
|
4021
|
+
content: r.content,
|
|
4022
|
+
createdAt: r.createdAt,
|
|
4023
|
+
metadata: r.metadata,
|
|
4024
|
+
})),
|
|
4025
|
+
)
|
|
4026
|
+
.run();
|
|
4027
|
+
}
|
|
4028
|
+
setConversation("runtime-assembly-fallback", {
|
|
4029
|
+
conversationId: "runtime-assembly-fallback",
|
|
4030
|
+
workingDir: "/sandbox",
|
|
4031
|
+
workspaceTopLevelContext: "",
|
|
4032
|
+
workspaceTopLevelDirty: false,
|
|
4033
|
+
surfaceState: new Map(),
|
|
4034
|
+
channelCapabilities: caps,
|
|
4035
|
+
trustContext: { trustClass: "guardian" },
|
|
4036
|
+
slackContextCompactionWatermarkTs:
|
|
4037
|
+
compaction.slackContextCompactionWatermarkTs ?? null,
|
|
4038
|
+
contextCompactedMessageCount:
|
|
4039
|
+
compaction.contextCompactedMessageCount ?? 0,
|
|
4040
|
+
} as never);
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
// Re-run a Slack-channel turn through the public assembly path. The
|
|
4044
|
+
// active-thread focus block self-resolves inside `applyRuntimeInjections`
|
|
4045
|
+
// from the seeded live conversation; `focusBlock` resolves it the same way
|
|
4046
|
+
// (default DB loader, guardian trust + the seeded compaction state) for the
|
|
4047
|
+
// expected-content assertions.
|
|
4048
|
+
async function runSlackChannelAssemblyWithFocus(
|
|
4049
|
+
rows: MessageRow[],
|
|
4050
|
+
compaction: SeededCompactionState = {},
|
|
4051
|
+
): Promise<{
|
|
3343
4052
|
messages: Message[];
|
|
3344
4053
|
focusBlock: string | null;
|
|
3345
4054
|
}> {
|
|
@@ -3350,24 +4059,23 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3350
4059
|
supportsVoiceInput: false,
|
|
3351
4060
|
chatType: "channel",
|
|
3352
4061
|
};
|
|
3353
|
-
|
|
3354
|
-
"conv-1",
|
|
3355
|
-
slackChannelCaps,
|
|
3356
|
-
{ loader: () => rows, trustClass: "guardian" },
|
|
3357
|
-
);
|
|
4062
|
+
seedSlackChannelConversationWithRows(slackChannelCaps, rows, compaction);
|
|
3358
4063
|
const focusBlock = loadSlackActiveThreadFocusBlock(
|
|
3359
|
-
"
|
|
4064
|
+
"runtime-assembly-fallback",
|
|
3360
4065
|
slackChannelCaps,
|
|
3361
|
-
{
|
|
4066
|
+
{
|
|
4067
|
+
trustClass: "guardian",
|
|
4068
|
+
contextCompactedMessageCount: compaction.contextCompactedMessageCount,
|
|
4069
|
+
slackContextCompactionWatermarkTs:
|
|
4070
|
+
compaction.slackContextCompactionWatermarkTs,
|
|
4071
|
+
},
|
|
3362
4072
|
);
|
|
3363
4073
|
const lastUserMessage: Message = {
|
|
3364
4074
|
role: "user",
|
|
3365
4075
|
content: [{ type: "text", text: "current turn" }],
|
|
3366
4076
|
};
|
|
3367
4077
|
const { messages } = await applyRuntimeInjections([lastUserMessage], {
|
|
3368
|
-
|
|
3369
|
-
slackChronologicalMessages,
|
|
3370
|
-
slackActiveThreadFocusBlock: focusBlock,
|
|
4078
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
3371
4079
|
});
|
|
3372
4080
|
return { messages, focusBlock };
|
|
3373
4081
|
}
|
|
@@ -3584,14 +4292,143 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3584
4292
|
}
|
|
3585
4293
|
});
|
|
3586
4294
|
|
|
4295
|
+
test("focus block self-resolves the live conversation's compaction watermark", async () => {
|
|
4296
|
+
/**
|
|
4297
|
+
* The watermark + compacted count live on the live `Conversation`, not in
|
|
4298
|
+
* the injection options, so `applyRuntimeInjections` must read them off the
|
|
4299
|
+
* live conversation to bound the self-resolved `<active_thread>` block.
|
|
4300
|
+
*/
|
|
4301
|
+
// GIVEN a thread whose earliest rows were folded into a prior compaction
|
|
4302
|
+
const rows: MessageRow[] = [
|
|
4303
|
+
userRow({
|
|
4304
|
+
id: "thread-root",
|
|
4305
|
+
createdAt: 1700000000_000,
|
|
4306
|
+
text: "compacted root",
|
|
4307
|
+
slackMeta: buildSlackMeta({
|
|
4308
|
+
channelTs: T0,
|
|
4309
|
+
threadTs: T0,
|
|
4310
|
+
displayName: "alice",
|
|
4311
|
+
}),
|
|
4312
|
+
}),
|
|
4313
|
+
userRow({
|
|
4314
|
+
id: "reply-before",
|
|
4315
|
+
createdAt: 1700000005_000,
|
|
4316
|
+
text: "compacted reply",
|
|
4317
|
+
slackMeta: buildSlackMeta({
|
|
4318
|
+
channelTs: T0_REPLY1,
|
|
4319
|
+
threadTs: T0,
|
|
4320
|
+
displayName: "bob",
|
|
4321
|
+
}),
|
|
4322
|
+
}),
|
|
4323
|
+
userRow({
|
|
4324
|
+
id: "reply-after",
|
|
4325
|
+
createdAt: 1700000020_000,
|
|
4326
|
+
text: "live reply",
|
|
4327
|
+
slackMeta: buildSlackMeta({
|
|
4328
|
+
channelTs: T0_REPLY2,
|
|
4329
|
+
threadTs: T0,
|
|
4330
|
+
displayName: "carol",
|
|
4331
|
+
}),
|
|
4332
|
+
}),
|
|
4333
|
+
];
|
|
4334
|
+
|
|
4335
|
+
// WHEN the live conversation carries a Slack compaction watermark at T1
|
|
4336
|
+
const { messages, focusBlock } = await runSlackChannelAssemblyWithFocus(
|
|
4337
|
+
rows,
|
|
4338
|
+
{
|
|
4339
|
+
slackContextCompactionWatermarkTs: T1,
|
|
4340
|
+
contextCompactedMessageCount: 2,
|
|
4341
|
+
},
|
|
4342
|
+
);
|
|
4343
|
+
|
|
4344
|
+
// THEN the self-resolved focus block keeps only the post-watermark reply
|
|
4345
|
+
expect(focusBlock).not.toBeNull();
|
|
4346
|
+
expect(focusBlock!).toContain("live reply");
|
|
4347
|
+
expect(focusBlock!).not.toContain("compacted root");
|
|
4348
|
+
expect(focusBlock!).not.toContain("compacted reply");
|
|
4349
|
+
// AND `applyRuntimeInjections` injected exactly that self-resolved block,
|
|
4350
|
+
// proving it read the watermark + count off the live conversation.
|
|
4351
|
+
const allText = messages
|
|
4352
|
+
.flatMap((m) => m.content)
|
|
4353
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
4354
|
+
.map((b) => b.text)
|
|
4355
|
+
.join("\n");
|
|
4356
|
+
expect(allText).toContain("<active_thread>");
|
|
4357
|
+
expect(allText).toContain(focusBlock!);
|
|
4358
|
+
});
|
|
4359
|
+
|
|
4360
|
+
test("focus block bounds by the compacted count when there is no watermark", async () => {
|
|
4361
|
+
/**
|
|
4362
|
+
* Conversations compacted before the Slack watermark migration have no
|
|
4363
|
+
* watermark, so the raw `contextCompactedMessageCount` is the sole boundary
|
|
4364
|
+
* the self-resolved focus block uses to drop already-compacted rows.
|
|
4365
|
+
*/
|
|
4366
|
+
// GIVEN a thread whose first two rows were folded into a prior compaction
|
|
4367
|
+
const rows: MessageRow[] = [
|
|
4368
|
+
userRow({
|
|
4369
|
+
id: "thread-root",
|
|
4370
|
+
createdAt: 1700000000_000,
|
|
4371
|
+
text: "compacted root",
|
|
4372
|
+
slackMeta: buildSlackMeta({
|
|
4373
|
+
channelTs: T0,
|
|
4374
|
+
threadTs: T0,
|
|
4375
|
+
displayName: "alice",
|
|
4376
|
+
}),
|
|
4377
|
+
}),
|
|
4378
|
+
userRow({
|
|
4379
|
+
id: "reply-before",
|
|
4380
|
+
createdAt: 1700000005_000,
|
|
4381
|
+
text: "compacted reply",
|
|
4382
|
+
slackMeta: buildSlackMeta({
|
|
4383
|
+
channelTs: T0_REPLY1,
|
|
4384
|
+
threadTs: T0,
|
|
4385
|
+
displayName: "bob",
|
|
4386
|
+
}),
|
|
4387
|
+
}),
|
|
4388
|
+
userRow({
|
|
4389
|
+
id: "reply-after",
|
|
4390
|
+
createdAt: 1700000020_000,
|
|
4391
|
+
text: "live reply",
|
|
4392
|
+
slackMeta: buildSlackMeta({
|
|
4393
|
+
channelTs: T0_REPLY2,
|
|
4394
|
+
threadTs: T0,
|
|
4395
|
+
displayName: "carol",
|
|
4396
|
+
}),
|
|
4397
|
+
}),
|
|
4398
|
+
];
|
|
4399
|
+
|
|
4400
|
+
// WHEN the live conversation has a compacted count of 2 and no watermark
|
|
4401
|
+
const { messages, focusBlock } = await runSlackChannelAssemblyWithFocus(
|
|
4402
|
+
rows,
|
|
4403
|
+
{
|
|
4404
|
+
slackContextCompactionWatermarkTs: null,
|
|
4405
|
+
contextCompactedMessageCount: 2,
|
|
4406
|
+
},
|
|
4407
|
+
);
|
|
4408
|
+
|
|
4409
|
+
// THEN the self-resolved focus block drops the two compacted rows and keeps
|
|
4410
|
+
// only the live reply
|
|
4411
|
+
expect(focusBlock).not.toBeNull();
|
|
4412
|
+
expect(focusBlock!).toContain("live reply");
|
|
4413
|
+
expect(focusBlock!).not.toContain("compacted root");
|
|
4414
|
+
expect(focusBlock!).not.toContain("compacted reply");
|
|
4415
|
+
const allText = messages
|
|
4416
|
+
.flatMap((m) => m.content)
|
|
4417
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
4418
|
+
.map((b) => b.text)
|
|
4419
|
+
.join("\n");
|
|
4420
|
+
expect(allText).toContain("<active_thread>");
|
|
4421
|
+
expect(allText).toContain(focusBlock!);
|
|
4422
|
+
});
|
|
4423
|
+
|
|
3587
4424
|
test("focus block is dropped when injection is replayed (rebuilds re-derive it)", async () => {
|
|
3588
4425
|
// Defensive: the `<active_thread>` block is a per-turn injection. When
|
|
3589
4426
|
// overflow recovery / compaction re-runs `applyRuntimeInjections` on
|
|
3590
4427
|
// already-injected messages, prior `<active_thread>` blocks must be
|
|
3591
4428
|
// stripped so the rebuild's freshly-derived block is the only one
|
|
3592
4429
|
// present. We simulate by building a Slack channel turn, then
|
|
3593
|
-
// running the strip pipeline + applying injections again
|
|
3594
|
-
//
|
|
4430
|
+
// running the strip pipeline + applying injections again to confirm the
|
|
4431
|
+
// rebuild re-derives the block and no duplication occurs.
|
|
3595
4432
|
const rows: MessageRow[] = [
|
|
3596
4433
|
userRow({
|
|
3597
4434
|
id: "m1",
|
|
@@ -3624,19 +4461,11 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3624
4461
|
.join("\n");
|
|
3625
4462
|
expect(strippedTexts).not.toContain("<active_thread>");
|
|
3626
4463
|
|
|
3627
|
-
// Re-run injection
|
|
4464
|
+
// Re-run injection on the stripped messages — the conversation is still
|
|
4465
|
+
// seeded, so the rebuild re-derives the focus block; only ONE
|
|
3628
4466
|
// `<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
4467
|
const { messages: reInjected } = await applyRuntimeInjections(stripped, {
|
|
3638
|
-
|
|
3639
|
-
slackActiveThreadFocusBlock: newFocus,
|
|
4468
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
3640
4469
|
});
|
|
3641
4470
|
const reInjectedTexts = reInjected
|
|
3642
4471
|
.flatMap((m) => m.content)
|
|
@@ -3648,24 +4477,22 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3648
4477
|
expect(blockCount).toBe(1);
|
|
3649
4478
|
expect(
|
|
3650
4479
|
reInjectedTexts.find((t) => t.startsWith("<active_thread>")),
|
|
3651
|
-
).toContain("
|
|
4480
|
+
).toContain("Reply in thread A");
|
|
3652
4481
|
});
|
|
3653
4482
|
|
|
3654
|
-
test("non-slack conversations
|
|
3655
|
-
//
|
|
3656
|
-
// `isSlackChannelConversation`).
|
|
3657
|
-
//
|
|
4483
|
+
test("non-slack conversations never get an active-thread focus block", async () => {
|
|
4484
|
+
// The focus block is gated on a Slack *channel* conversation
|
|
4485
|
+
// (`isSlackChannelConversation`). A non-Slack conversation must never
|
|
4486
|
+
// self-resolve an `<active_thread>` block.
|
|
4487
|
+
seedChannelCapabilitiesConversation({
|
|
4488
|
+
channel: "vellum",
|
|
4489
|
+
dashboardCapable: true,
|
|
4490
|
+
supportsDynamicUi: true,
|
|
4491
|
+
supportsVoiceInput: true,
|
|
4492
|
+
});
|
|
3658
4493
|
const { messages: result } = await applyRuntimeInjections(
|
|
3659
4494
|
[{ 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
|
-
},
|
|
4495
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3669
4496
|
);
|
|
3670
4497
|
const allText = result
|
|
3671
4498
|
.flatMap((m) => m.content)
|
|
@@ -3676,22 +4503,20 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
3676
4503
|
expect(allText).toContain("vellum question");
|
|
3677
4504
|
});
|
|
3678
4505
|
|
|
3679
|
-
test("slack DMs
|
|
4506
|
+
test("slack DMs never get an active-thread focus block", async () => {
|
|
3680
4507
|
// Same as above but for Slack DMs (chatType === "im"). The focus
|
|
3681
4508
|
// injection is keyed on `isSlackChannelConversation` which excludes
|
|
3682
4509
|
// DMs, so the block must not appear.
|
|
4510
|
+
seedChannelCapabilitiesConversation({
|
|
4511
|
+
channel: "slack",
|
|
4512
|
+
dashboardCapable: false,
|
|
4513
|
+
supportsDynamicUi: false,
|
|
4514
|
+
supportsVoiceInput: false,
|
|
4515
|
+
chatType: "im",
|
|
4516
|
+
});
|
|
3683
4517
|
const { messages: result } = await applyRuntimeInjections(
|
|
3684
4518
|
[{ 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
|
-
},
|
|
4519
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
3695
4520
|
);
|
|
3696
4521
|
const allText = result
|
|
3697
4522
|
.flatMap((m) => m.content)
|
|
@@ -5047,6 +5872,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
|
|
|
5047
5872
|
pkbSearchResults = [];
|
|
5048
5873
|
pkbSearchThrows = null;
|
|
5049
5874
|
const { blocks } = await applyRuntimeInjections(baseMessages, {
|
|
5875
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
5050
5876
|
mode: "full",
|
|
5051
5877
|
});
|
|
5052
5878
|
|
|
@@ -5057,6 +5883,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
|
|
|
5057
5883
|
test("not captured in minimal mode", async () => {
|
|
5058
5884
|
seedPkbContent();
|
|
5059
5885
|
const { blocks } = await applyRuntimeInjections(baseMessages, {
|
|
5886
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
5060
5887
|
mode: "minimal",
|
|
5061
5888
|
});
|
|
5062
5889
|
|
|
@@ -5065,6 +5892,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
|
|
|
5065
5892
|
|
|
5066
5893
|
test("not captured when PKB inactive", async () => {
|
|
5067
5894
|
const { blocks } = await applyRuntimeInjections(baseMessages, {
|
|
5895
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
5068
5896
|
mode: "full",
|
|
5069
5897
|
});
|
|
5070
5898
|
|