@vellumai/assistant 0.11.1 → 0.11.2-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Producer-to-finalizer state-chain coverage for the memory retrospective
|
|
3
|
+
* (LUM-3013).
|
|
4
|
+
*
|
|
5
|
+
* These tests run the REAL chain: `runForkBasedRetrospective` (real fork +
|
|
6
|
+
* real `memory_retrospective_state` rows in the test DB) invokes the REAL
|
|
7
|
+
* `wakeAgentForOpportunity`, which runs a REAL `AgentLoop` whose provider is
|
|
8
|
+
* scripted per test. Provider rejections therefore exercise the loop's actual
|
|
9
|
+
* catch/`stopTurn` path (no manually emitted events), and finalization reads
|
|
10
|
+
* and writes the actual persisted rows.
|
|
11
|
+
*
|
|
12
|
+
* What is stubbed, and why it is outside the contract under proof:
|
|
13
|
+
* - The wake's conversation resolution: the module mock below re-exports the
|
|
14
|
+
* real `wakeAgentForOpportunity` with an injected `resolveTarget` that
|
|
15
|
+
* yields a structural conversation double wrapping the real `AgentLoop`
|
|
16
|
+
* (the default resolver is daemon-hydration machinery, not state logic).
|
|
17
|
+
* - `recordUsage` (cost ledger) and `resolveEffectiveContextWindow` /
|
|
18
|
+
* disk-pressure status (config lookups): observability and gating leaves.
|
|
19
|
+
*
|
|
20
|
+
* The LUM-3013 data-integrity contract proven here:
|
|
21
|
+
* 1. A provider rejection reaches `invoked: false` BEFORE any state
|
|
22
|
+
* mutation: no cursor advance, no remembered-log change, no
|
|
23
|
+
* prior-retrospective deletion; the orphan fork is cleaned up.
|
|
24
|
+
* 2. The failed window stays retryable: a later run over the same window
|
|
25
|
+
* succeeds and advances state.
|
|
26
|
+
* 3. A usable-output control advances state (cursor, remembered log) and
|
|
27
|
+
* GCs the superseded prior retrospective.
|
|
28
|
+
* 4. A run whose checkpoint went live before a later rejection stays
|
|
29
|
+
* `invoked: true` (its side effects landed and are honored).
|
|
30
|
+
*/
|
|
31
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
32
|
+
|
|
33
|
+
import { AgentLoop } from "../../../../agent/loop.js";
|
|
34
|
+
import type { Conversation } from "../../../../daemon/conversation.js";
|
|
35
|
+
import {
|
|
36
|
+
addMessage,
|
|
37
|
+
createConversation,
|
|
38
|
+
forkConversationForRetrospective,
|
|
39
|
+
getConversation,
|
|
40
|
+
getMessages,
|
|
41
|
+
} from "../../../../persistence/conversation-crud.js";
|
|
42
|
+
import { getDb, getMemoryDb } from "../../../../persistence/db-connection.js";
|
|
43
|
+
import { initializeDb } from "../../../../persistence/db-init.js";
|
|
44
|
+
import {
|
|
45
|
+
conversations,
|
|
46
|
+
memoryJobs,
|
|
47
|
+
memoryRetrospectiveState,
|
|
48
|
+
messages as messagesTable,
|
|
49
|
+
} from "../../../../persistence/schema/index.js";
|
|
50
|
+
import type {
|
|
51
|
+
Message,
|
|
52
|
+
Provider,
|
|
53
|
+
ProviderResponse,
|
|
54
|
+
SendMessageOptions,
|
|
55
|
+
ToolDefinition,
|
|
56
|
+
} from "../../../../providers/types.js";
|
|
57
|
+
// Capture the real wake before the module mock below re-links the module
|
|
58
|
+
// for every importer (including memory-retrospective-job).
|
|
59
|
+
import { wakeAgentForOpportunity as importedWake } from "../../../../runtime/agent-wake.js";
|
|
60
|
+
import * as realAgentWakeModule from "../../../../runtime/agent-wake.js";
|
|
61
|
+
import {
|
|
62
|
+
MEMORY_RETROSPECTIVE_FORK_SOURCE,
|
|
63
|
+
MEMORY_RETROSPECTIVE_GROUP_ID,
|
|
64
|
+
} from "../memory-retrospective-constants.js";
|
|
65
|
+
import { runForkBasedRetrospective } from "../memory-retrospective-job.js";
|
|
66
|
+
import {
|
|
67
|
+
getRetrospectiveState,
|
|
68
|
+
upsertRetrospectiveState,
|
|
69
|
+
} from "../memory-retrospective-state.js";
|
|
70
|
+
|
|
71
|
+
const realWake = importedWake;
|
|
72
|
+
|
|
73
|
+
// ── Scripted provider ────────────────────────────────────────────────
|
|
74
|
+
//
|
|
75
|
+
// Each entry is either a canned response or a thrown rejection, consumed in
|
|
76
|
+
// order; the last entry repeats. Reassigned per test.
|
|
77
|
+
type ProviderStep = { response: ProviderResponse } | { reject: Error };
|
|
78
|
+
let providerScript: ProviderStep[] = [];
|
|
79
|
+
let providerCallCount = 0;
|
|
80
|
+
|
|
81
|
+
const scriptedProvider: Provider = {
|
|
82
|
+
name: "mock-provider",
|
|
83
|
+
async sendMessage(
|
|
84
|
+
_messages: Message[],
|
|
85
|
+
_options?: SendMessageOptions,
|
|
86
|
+
): Promise<ProviderResponse> {
|
|
87
|
+
const step =
|
|
88
|
+
providerScript[providerCallCount] ??
|
|
89
|
+
providerScript[providerScript.length - 1];
|
|
90
|
+
providerCallCount++;
|
|
91
|
+
if (!step) {
|
|
92
|
+
throw new Error("provider script exhausted");
|
|
93
|
+
}
|
|
94
|
+
if ("reject" in step) {
|
|
95
|
+
throw step.reject;
|
|
96
|
+
}
|
|
97
|
+
return step.response;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function textResponse(text: string): ProviderResponse {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text }],
|
|
104
|
+
model: "mock-model",
|
|
105
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
106
|
+
stopReason: "end_turn",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function rememberToolUseResponse(fact: string): ProviderResponse {
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "tool_use",
|
|
115
|
+
id: "tu-1",
|
|
116
|
+
name: "remember",
|
|
117
|
+
input: { content: fact },
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
model: "mock-model",
|
|
121
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
122
|
+
stopReason: "tool_use",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const rememberToolDef: ToolDefinition[] = [
|
|
127
|
+
{
|
|
128
|
+
name: "remember",
|
|
129
|
+
description: "Save a fact to memory",
|
|
130
|
+
input_schema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: { content: { type: "string" } },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// ── Conversation double wrapping a REAL AgentLoop ────────────────────
|
|
138
|
+
//
|
|
139
|
+
// Structural double for the fork conversation the wake resolves: only the
|
|
140
|
+
// members the wake touches, with `agentLoop` a real `AgentLoop` over the
|
|
141
|
+
// scripted provider so provider failures exercise the loop's genuine
|
|
142
|
+
// catch/stopTurn path. Tail persistence, state reads/writes, and fork
|
|
143
|
+
// cleanup all run against the real test DB.
|
|
144
|
+
function makeForkConversationDouble(forkId: string): Conversation {
|
|
145
|
+
const rows = getMessages(forkId);
|
|
146
|
+
const messages: Message[] = rows.map((row) => {
|
|
147
|
+
let content: unknown = row.content;
|
|
148
|
+
if (typeof content === "string") {
|
|
149
|
+
try {
|
|
150
|
+
content = JSON.parse(content);
|
|
151
|
+
} catch {
|
|
152
|
+
// Keep the raw string when not JSON.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { role: row.role, content } as Message;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const agentLoop = new AgentLoop({
|
|
159
|
+
provider: scriptedProvider,
|
|
160
|
+
systemPrompt: "chain-test system prompt",
|
|
161
|
+
conversationId: forkId,
|
|
162
|
+
tools: rememberToolDef,
|
|
163
|
+
toolExecutor: async () => ({ content: "Saved.", isError: false }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let processing = false;
|
|
167
|
+
let activeAllowedTools: Set<string> | undefined;
|
|
168
|
+
let persistentTrust: unknown;
|
|
169
|
+
let turnTrust: unknown;
|
|
170
|
+
|
|
171
|
+
const conversation = {
|
|
172
|
+
conversationId: forkId,
|
|
173
|
+
agentLoop,
|
|
174
|
+
provider: scriptedProvider,
|
|
175
|
+
usageStats: {},
|
|
176
|
+
messages,
|
|
177
|
+
getMessages: () => messages,
|
|
178
|
+
isProcessing: () => processing,
|
|
179
|
+
setProcessing: (on: boolean) => {
|
|
180
|
+
processing = on;
|
|
181
|
+
},
|
|
182
|
+
waitForIdle: async () => true,
|
|
183
|
+
get subagentAllowedTools() {
|
|
184
|
+
return activeAllowedTools;
|
|
185
|
+
},
|
|
186
|
+
setSubagentAllowedTools: (tools: Set<string> | undefined) => {
|
|
187
|
+
activeAllowedTools = tools;
|
|
188
|
+
},
|
|
189
|
+
preactivatedSkillIds: undefined,
|
|
190
|
+
setPreactivatedSkillIds: () => {},
|
|
191
|
+
subagentToolGateMode: undefined,
|
|
192
|
+
toolContextPin: undefined,
|
|
193
|
+
currentTurnRequestOrigin: undefined,
|
|
194
|
+
wakePersonaOverride: undefined,
|
|
195
|
+
currentCallSite: undefined,
|
|
196
|
+
currentTurnOverrideProfile: undefined,
|
|
197
|
+
hasNoClient: false,
|
|
198
|
+
get currentTurnTrustContext() {
|
|
199
|
+
return turnTrust;
|
|
200
|
+
},
|
|
201
|
+
set currentTurnTrustContext(value: unknown) {
|
|
202
|
+
turnTrust = value;
|
|
203
|
+
},
|
|
204
|
+
get trustContext() {
|
|
205
|
+
return persistentTrust;
|
|
206
|
+
},
|
|
207
|
+
setTrustContext: (ctx: unknown) => {
|
|
208
|
+
persistentTrust = ctx ?? undefined;
|
|
209
|
+
},
|
|
210
|
+
maybeCompact: async () => null,
|
|
211
|
+
contextWindowManager: {
|
|
212
|
+
estimateInputTokens: () => 1_000,
|
|
213
|
+
},
|
|
214
|
+
getTurnChannelContext: () => null,
|
|
215
|
+
getTurnInterfaceContext: () => null,
|
|
216
|
+
buildCurrentSystemPrompt: () => "chain-test system prompt",
|
|
217
|
+
modelOverride: undefined,
|
|
218
|
+
drainQueue: async () => {},
|
|
219
|
+
};
|
|
220
|
+
return conversation as unknown as Conversation;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Module mocks ─────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
// Re-export the REAL wake with the conversation resolver injected, so the
|
|
226
|
+
// retrospective job's unmodified `wakeAgentForOpportunity(...)` call runs the
|
|
227
|
+
// complete production wake logic against the real-AgentLoop double above.
|
|
228
|
+
mock.module("../../../../runtime/agent-wake.js", () => ({
|
|
229
|
+
...realAgentWakeModule,
|
|
230
|
+
wakeAgentForOpportunity: (
|
|
231
|
+
opts: Parameters<typeof realWake>[0],
|
|
232
|
+
): ReturnType<typeof realWake> =>
|
|
233
|
+
realWake(opts, {
|
|
234
|
+
resolveTarget: async (wakeOpts) =>
|
|
235
|
+
makeForkConversationDouble(wakeOpts.conversationId),
|
|
236
|
+
}),
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
// Config-lookup leaves outside the state contract under proof.
|
|
240
|
+
mock.module("../../../../config/llm-context-resolution.js", () => ({
|
|
241
|
+
resolveEffectiveContextWindow: () => ({ maxInputTokens: 200_000 }),
|
|
242
|
+
}));
|
|
243
|
+
mock.module("../../../../daemon/conversation-usage.js", () => ({
|
|
244
|
+
recordUsage: () => {},
|
|
245
|
+
}));
|
|
246
|
+
mock.module("../../../../daemon/disk-pressure-guard.js", () => ({
|
|
247
|
+
getDiskPressureStatus: () => ({
|
|
248
|
+
enabled: false,
|
|
249
|
+
state: "disabled",
|
|
250
|
+
locked: false,
|
|
251
|
+
acknowledged: false,
|
|
252
|
+
overrideActive: false,
|
|
253
|
+
effectivelyLocked: false,
|
|
254
|
+
lockId: null,
|
|
255
|
+
usagePercent: null,
|
|
256
|
+
thresholdPercent: 95,
|
|
257
|
+
path: null,
|
|
258
|
+
lastCheckedAt: null,
|
|
259
|
+
blockedCapabilities: [],
|
|
260
|
+
error: null,
|
|
261
|
+
}),
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
await initializeDb();
|
|
265
|
+
|
|
266
|
+
// ── Fixtures ─────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
const chainConfig = {
|
|
269
|
+
memory: {
|
|
270
|
+
v2: { enabled: true },
|
|
271
|
+
retrospective: {
|
|
272
|
+
keepSupersededRuns: false,
|
|
273
|
+
matchConversationProfile: false,
|
|
274
|
+
promptPath: null,
|
|
275
|
+
requireUserActivity: false,
|
|
276
|
+
sweepIntervalMs: 8 * 60 * 60 * 1000,
|
|
277
|
+
sweepLookbackMs: 7 * 24 * 60 * 60 * 1000,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
ui: {},
|
|
281
|
+
} as unknown as Parameters<typeof runForkBasedRetrospective>[1];
|
|
282
|
+
|
|
283
|
+
const PRIOR_FACT = "User's cat is named Mochi";
|
|
284
|
+
|
|
285
|
+
interface ChainFixture {
|
|
286
|
+
sourceId: string;
|
|
287
|
+
priorRetroId: string;
|
|
288
|
+
seededCursorId: string;
|
|
289
|
+
newMessageId: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Stage the full pre-run world in the real DB: a source conversation with
|
|
294
|
+
* processed history, a genuine prior retrospective fork, a seeded state row
|
|
295
|
+
* (cursor + remembered log), and one new unprocessed user message.
|
|
296
|
+
*/
|
|
297
|
+
async function stageChainFixture(): Promise<ChainFixture> {
|
|
298
|
+
const source = await createConversation("Chain source");
|
|
299
|
+
const first = await addMessage(
|
|
300
|
+
source.id,
|
|
301
|
+
"user",
|
|
302
|
+
JSON.stringify([{ type: "text", text: "Hi, my cat is named Mochi." }]),
|
|
303
|
+
);
|
|
304
|
+
await addMessage(
|
|
305
|
+
source.id,
|
|
306
|
+
"assistant",
|
|
307
|
+
JSON.stringify([{ type: "text", text: "Noted!" }]),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const prior = await forkConversationForRetrospective({
|
|
311
|
+
conversationId: source.id,
|
|
312
|
+
throughMessageId: first.id,
|
|
313
|
+
source: MEMORY_RETROSPECTIVE_FORK_SOURCE,
|
|
314
|
+
title: "Chain source (Retrospective)",
|
|
315
|
+
conversationType: "background",
|
|
316
|
+
groupId: MEMORY_RETROSPECTIVE_GROUP_ID,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const processedThrough = await addMessage(
|
|
320
|
+
source.id,
|
|
321
|
+
"assistant",
|
|
322
|
+
JSON.stringify([{ type: "text", text: "Anything else?" }]),
|
|
323
|
+
);
|
|
324
|
+
await upsertRetrospectiveState({
|
|
325
|
+
conversationId: source.id,
|
|
326
|
+
lastProcessedMessageId: processedThrough.id,
|
|
327
|
+
lastRunAt: Date.now() - 60 * 60 * 1000,
|
|
328
|
+
rememberedLog: [PRIOR_FACT],
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const fresh = await addMessage(
|
|
332
|
+
source.id,
|
|
333
|
+
"user",
|
|
334
|
+
JSON.stringify([{ type: "text", text: "I just moved to Lisbon." }]),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
sourceId: source.id,
|
|
339
|
+
priorRetroId: prior.id,
|
|
340
|
+
seededCursorId: processedThrough.id,
|
|
341
|
+
newMessageId: fresh.id,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Fork conversations currently rooted at the given source. */
|
|
346
|
+
function listRetroForksOf(sourceId: string): string[] {
|
|
347
|
+
const db = getDb();
|
|
348
|
+
return db
|
|
349
|
+
.select({ id: conversations.id })
|
|
350
|
+
.from(conversations)
|
|
351
|
+
.all()
|
|
352
|
+
.map((row) => row.id)
|
|
353
|
+
.filter((id) => {
|
|
354
|
+
const conv = getConversation(id);
|
|
355
|
+
return (
|
|
356
|
+
conv?.source === MEMORY_RETROSPECTIVE_FORK_SOURCE &&
|
|
357
|
+
conv.forkParentConversationId === sourceId
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function resetTables(): void {
|
|
363
|
+
const db = getDb();
|
|
364
|
+
getMemoryDb()!.delete(memoryRetrospectiveState).run();
|
|
365
|
+
getMemoryDb()!.delete(memoryJobs).run();
|
|
366
|
+
db.delete(messagesTable).run();
|
|
367
|
+
db.delete(conversations).run();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
beforeEach(() => {
|
|
371
|
+
resetTables();
|
|
372
|
+
providerScript = [];
|
|
373
|
+
providerCallCount = 0;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
describe("memory retrospective wake state chain (real AgentLoop)", () => {
|
|
379
|
+
test("provider rejection: no cursor advance, no log mutation, prior kept, fork cleaned up, window retryable", async () => {
|
|
380
|
+
const fixture = await stageChainFixture();
|
|
381
|
+
const stateBefore = getRetrospectiveState(fixture.sourceId);
|
|
382
|
+
|
|
383
|
+
// The real loop swallows this into agent_loop_exit("error"); nothing is
|
|
384
|
+
// scripted at the event level.
|
|
385
|
+
providerScript = [
|
|
386
|
+
{ reject: new Error("400 this model does not support image input") },
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
const outcome = await runForkBasedRetrospective(
|
|
390
|
+
fixture.sourceId,
|
|
391
|
+
chainConfig,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(outcome.kind).toBe("wake_failed");
|
|
395
|
+
if (outcome.kind === "wake_failed") {
|
|
396
|
+
expect(outcome.reason).toBe("run_error");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// (1) No successful finalization: cursor and remembered log unchanged.
|
|
400
|
+
const stateAfter = getRetrospectiveState(fixture.sourceId);
|
|
401
|
+
expect(stateAfter?.lastProcessedMessageId).toBe(fixture.seededCursorId);
|
|
402
|
+
expect(stateAfter?.rememberedLog).toEqual([PRIOR_FACT]);
|
|
403
|
+
// Cooldown applies to the failed attempt.
|
|
404
|
+
expect(stateAfter!.lastRunAt).toBeGreaterThan(stateBefore!.lastRunAt!);
|
|
405
|
+
|
|
406
|
+
// Prior retrospective (the dedup-baseline fallback) survives.
|
|
407
|
+
expect(getConversation(fixture.priorRetroId)).not.toBeNull();
|
|
408
|
+
// This run's own orphan fork is deleted; the prior is the only fork left.
|
|
409
|
+
expect(listRetroForksOf(fixture.sourceId)).toEqual([fixture.priorRetroId]);
|
|
410
|
+
|
|
411
|
+
// (2) The window stays retryable: the same slice succeeds on retry.
|
|
412
|
+
providerScript = [
|
|
413
|
+
{ response: textResponse("Reviewed. Nothing new worth saving.") },
|
|
414
|
+
];
|
|
415
|
+
providerCallCount = 0;
|
|
416
|
+
const retry = await runForkBasedRetrospective(
|
|
417
|
+
fixture.sourceId,
|
|
418
|
+
chainConfig,
|
|
419
|
+
);
|
|
420
|
+
expect(retry.kind).toBe("invoked");
|
|
421
|
+
const stateRetried = getRetrospectiveState(fixture.sourceId);
|
|
422
|
+
expect(stateRetried?.lastProcessedMessageId).toBe(fixture.newMessageId);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("clean empty reply (HTTP-200, content: []): fails closed, state preserved, window retryable", async () => {
|
|
426
|
+
const fixture = await stageChainFixture();
|
|
427
|
+
|
|
428
|
+
// A well-formed provider success whose finalized output is unusable:
|
|
429
|
+
// empty content array, clean end_turn. The real loop exits
|
|
430
|
+
// no_tool_calls; the retrospective's requireUsableOutput turns that
|
|
431
|
+
// into a failed, retryable pass instead of consuming the window.
|
|
432
|
+
providerScript = [
|
|
433
|
+
{
|
|
434
|
+
response: {
|
|
435
|
+
content: [],
|
|
436
|
+
model: "mock-model",
|
|
437
|
+
usage: { inputTokens: 10, outputTokens: 0 },
|
|
438
|
+
stopReason: "end_turn",
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
const outcome = await runForkBasedRetrospective(
|
|
444
|
+
fixture.sourceId,
|
|
445
|
+
chainConfig,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
expect(outcome.kind).toBe("wake_failed");
|
|
449
|
+
if (outcome.kind === "wake_failed") {
|
|
450
|
+
expect(outcome.reason).toBe("no_output");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// No finalization: cursor, log, and the prior retrospective untouched;
|
|
454
|
+
// the orphan fork is cleaned up.
|
|
455
|
+
const state = getRetrospectiveState(fixture.sourceId);
|
|
456
|
+
expect(state?.lastProcessedMessageId).toBe(fixture.seededCursorId);
|
|
457
|
+
expect(state?.rememberedLog).toEqual([PRIOR_FACT]);
|
|
458
|
+
expect(getConversation(fixture.priorRetroId)).not.toBeNull();
|
|
459
|
+
expect(listRetroForksOf(fixture.sourceId)).toEqual([fixture.priorRetroId]);
|
|
460
|
+
|
|
461
|
+
// The window stays retryable and a usable reply consumes it.
|
|
462
|
+
providerScript = [{ response: textResponse("Reviewed; nothing new.") }];
|
|
463
|
+
providerCallCount = 0;
|
|
464
|
+
const retry = await runForkBasedRetrospective(
|
|
465
|
+
fixture.sourceId,
|
|
466
|
+
chainConfig,
|
|
467
|
+
);
|
|
468
|
+
expect(retry.kind).toBe("invoked");
|
|
469
|
+
expect(
|
|
470
|
+
getRetrospectiveState(fixture.sourceId)?.lastProcessedMessageId,
|
|
471
|
+
).toBe(fixture.newMessageId);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("usable-output control: cursor advances, remembered log grows, prior retrospective is GC'd", async () => {
|
|
475
|
+
const fixture = await stageChainFixture();
|
|
476
|
+
|
|
477
|
+
providerScript = [
|
|
478
|
+
{ response: rememberToolUseResponse("User moved to Lisbon") },
|
|
479
|
+
{ response: textResponse("Saved the move to memory.") },
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
const outcome = await runForkBasedRetrospective(
|
|
483
|
+
fixture.sourceId,
|
|
484
|
+
chainConfig,
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
expect(outcome.kind).toBe("invoked");
|
|
488
|
+
if (outcome.kind === "invoked") {
|
|
489
|
+
expect(outcome.cutoffMessageId).toBe(fixture.newMessageId);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// (3) Real finalization against real rows: cursor advanced, this run's
|
|
493
|
+
// remember (extracted from the fork's persisted tail) appended to the
|
|
494
|
+
// cumulative log, superseded prior GC'd.
|
|
495
|
+
const state = getRetrospectiveState(fixture.sourceId);
|
|
496
|
+
expect(state?.lastProcessedMessageId).toBe(fixture.newMessageId);
|
|
497
|
+
expect(state?.rememberedLog).toEqual([PRIOR_FACT, "User moved to Lisbon"]);
|
|
498
|
+
expect(getConversation(fixture.priorRetroId)).toBeNull();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("rejection after a live checkpoint stays invoked: landed side effects are honored", async () => {
|
|
502
|
+
const fixture = await stageChainFixture();
|
|
503
|
+
|
|
504
|
+
// First call: a full remember tool round (checkpoint goes live and the
|
|
505
|
+
// tail is persisted). Second call: provider rejection, swallowed by the
|
|
506
|
+
// real loop into a no-output stop.
|
|
507
|
+
providerScript = [
|
|
508
|
+
{ response: rememberToolUseResponse("User moved to Lisbon") },
|
|
509
|
+
{ reject: new Error("500 provider died mid-run") },
|
|
510
|
+
];
|
|
511
|
+
|
|
512
|
+
const outcome = await runForkBasedRetrospective(
|
|
513
|
+
fixture.sourceId,
|
|
514
|
+
chainConfig,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// (4) The run's `remember` executed, so it must not read as skipped:
|
|
518
|
+
// the wake reports invoked and finalization honors the landed work.
|
|
519
|
+
expect(outcome.kind).toBe("invoked");
|
|
520
|
+
const state = getRetrospectiveState(fixture.sourceId);
|
|
521
|
+
expect(state?.lastProcessedMessageId).toBe(fixture.newMessageId);
|
|
522
|
+
expect(state?.rememberedLog).toEqual([PRIOR_FACT, "User moved to Lisbon"]);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
@@ -156,7 +156,7 @@ const HOST_TIER_IMPORT_ALLOWLIST: readonly HostTierImportExemption[] = [
|
|
|
156
156
|
{ path: "src/runtime/routes/consolidation-routes.ts", tiers: ["substrate"] },
|
|
157
157
|
{
|
|
158
158
|
path: "src/runtime/routes/conversation-query-routes.ts",
|
|
159
|
-
tiers: ["
|
|
159
|
+
tiers: ["v2", "v3"],
|
|
160
160
|
},
|
|
161
161
|
{ path: "src/runtime/routes/global-search-routes.ts", tiers: ["v1"] },
|
|
162
162
|
{ path: "src/runtime/routes/secret-routes.ts", tiers: ["substrate"] },
|
package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts
CHANGED
|
@@ -498,9 +498,11 @@ describe("ConversationGraphMemory.prepareMemory — v2 routing (per-turn path)",
|
|
|
498
498
|
).toBe(false);
|
|
499
499
|
});
|
|
500
500
|
|
|
501
|
-
test("config on with empty Qdrant hits → no v2 block, v1 fallback skipped", async () => {
|
|
501
|
+
test("config on with empty Qdrant hits → no retrieved concepts in the v2 block, v1 fallback skipped", async () => {
|
|
502
502
|
// No `stageTurn` call — every channel returns `{ points: [] }` so the
|
|
503
|
-
// candidate set is empty and
|
|
503
|
+
// candidate set is empty and retrieval contributes nothing. Always-candidate
|
|
504
|
+
// skills are pinned rather than retrieved, so the block may still carry
|
|
505
|
+
// their cards; what must be absent is any retrieved concept section.
|
|
504
506
|
const memory = makeMemory();
|
|
505
507
|
const config = makeConfig(true);
|
|
506
508
|
const messages = makeMessages();
|
|
@@ -512,8 +514,11 @@ describe("ConversationGraphMemory.prepareMemory — v2 routing (per-turn path)",
|
|
|
512
514
|
noopEvent,
|
|
513
515
|
);
|
|
514
516
|
|
|
515
|
-
expect(result.injectedBlockText).
|
|
516
|
-
expect(result.runMessages).
|
|
517
|
+
expect(result.injectedBlockText ?? "").not.toContain("# memory/concepts/");
|
|
518
|
+
expect(JSON.stringify(result.runMessages)).not.toContain(
|
|
519
|
+
"memory/concepts/",
|
|
520
|
+
);
|
|
521
|
+
expect(retrieveForTurnMock).not.toHaveBeenCalled();
|
|
517
522
|
});
|
|
518
523
|
});
|
|
519
524
|
|
|
@@ -25,6 +25,7 @@ import type { SqliteRetryOptions } from "../../../util/sqlite-retry.js";
|
|
|
25
25
|
import * as hostSqliteRetry from "../../../util/sqlite-retry.js";
|
|
26
26
|
import * as hostStripCommentLines from "../../../util/strip-comment-lines.js";
|
|
27
27
|
import * as hostTruncate from "../../../util/truncate.js";
|
|
28
|
+
import * as hostWorkerCompute from "../../../util/worker-compute.js";
|
|
28
29
|
import * as hostWorkerMemory from "../../../util/worker-memory.js";
|
|
29
30
|
|
|
30
31
|
export { BackendUnavailableError } from "../../../util/errors.js";
|
|
@@ -78,3 +79,7 @@ export function truncate(str: string, maxLen: number, suffix?: string): string {
|
|
|
78
79
|
export function workerMemoryEnv(): Record<string, string | undefined> {
|
|
79
80
|
return hostWorkerMemory.workerMemoryEnv();
|
|
80
81
|
}
|
|
82
|
+
|
|
83
|
+
export function workerComputeEnv(): Record<string, string> {
|
|
84
|
+
return hostWorkerCompute.workerComputeEnv();
|
|
85
|
+
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// accounting; once newer real messages arrive it is copied into the next
|
|
24
24
|
// run's fork as inert prefix context.
|
|
25
25
|
|
|
26
|
-
import { and, eq, like } from "drizzle-orm";
|
|
26
|
+
import { and, eq, gt, like, or } from "drizzle-orm";
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
29
|
countMessagesAfter,
|
|
@@ -85,6 +85,110 @@ export function countRetrospectiveMessagesAfter(
|
|
|
85
85
|
return Math.max(0, total - cardCount);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* True when any block in a parsed content array is something other than a
|
|
90
|
+
* tool result (`tool_result` / `web_search_tool_result`). Both are transport
|
|
91
|
+
* blocks for tool output riding on user-role rows, not user-authored
|
|
92
|
+
* activity; an empty array carries nothing.
|
|
93
|
+
*/
|
|
94
|
+
function blocksCarryNonToolResult(
|
|
95
|
+
blocks: ReadonlyArray<{ type?: unknown }>,
|
|
96
|
+
): boolean {
|
|
97
|
+
return blocks.some(
|
|
98
|
+
(block) =>
|
|
99
|
+
block.type !== "tool_result" && block.type !== "web_search_tool_result",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* True when the slice contains at least one user-authored message: a
|
|
105
|
+
* user-role row whose resolved content carries a non-tool_result block. Tool
|
|
106
|
+
* results ride on user-role rows, so a bare role check would count any
|
|
107
|
+
* tool-using assistant stretch as user activity.
|
|
108
|
+
*/
|
|
109
|
+
export function messagesHaveUserActivity(
|
|
110
|
+
rows: ReadonlyArray<Pick<MessageRow, "role" | "content">>,
|
|
111
|
+
): boolean {
|
|
112
|
+
return rows.some(
|
|
113
|
+
(row) => row.role === "user" && blocksCarryNonToolResult(row.content),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Existence probe for user-authored activity strictly after the
|
|
119
|
+
* `(createdAt, id)` cursor: at least one user-role row whose content carries
|
|
120
|
+
* a non-tool_result block. Powers the `memory.retrospective.requireUserActivity`
|
|
121
|
+
* enqueue gate, so only user rows are loaded. Cursor semantics mirror
|
|
122
|
+
* `countMessagesAfter`: a null/`""` reference scans the whole conversation,
|
|
123
|
+
* and a vanished reference means no new work.
|
|
124
|
+
*
|
|
125
|
+
* Operates on the raw `content` column: rows that do not parse to a block
|
|
126
|
+
* array (file-backed `{ ref }` rows, legacy plain strings) count as
|
|
127
|
+
* user-authored, so the gate fails toward running the retrospective.
|
|
128
|
+
*/
|
|
129
|
+
export function hasQualifyingUserMessageAfter(
|
|
130
|
+
conversationId: string,
|
|
131
|
+
afterMessageId: string | null,
|
|
132
|
+
): boolean {
|
|
133
|
+
const cursorId =
|
|
134
|
+
afterMessageId === null || afterMessageId === "" ? null : afterMessageId;
|
|
135
|
+
const db = getDb();
|
|
136
|
+
|
|
137
|
+
let ref: { createdAt: number } | undefined;
|
|
138
|
+
if (cursorId !== null) {
|
|
139
|
+
ref = db
|
|
140
|
+
.select({ createdAt: messages.createdAt })
|
|
141
|
+
.from(messages)
|
|
142
|
+
.where(eq(messages.id, cursorId))
|
|
143
|
+
.get();
|
|
144
|
+
if (!ref) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const afterCursor =
|
|
150
|
+
cursorId !== null && ref
|
|
151
|
+
? [
|
|
152
|
+
or(
|
|
153
|
+
gt(messages.createdAt, ref.createdAt),
|
|
154
|
+
and(
|
|
155
|
+
eq(messages.createdAt, ref.createdAt),
|
|
156
|
+
gt(messages.id, cursorId),
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
]
|
|
160
|
+
: [];
|
|
161
|
+
const rows = db
|
|
162
|
+
.select({ content: messages.content })
|
|
163
|
+
.from(messages)
|
|
164
|
+
.where(
|
|
165
|
+
and(
|
|
166
|
+
eq(messages.conversationId, conversationId),
|
|
167
|
+
eq(messages.role, "user"),
|
|
168
|
+
...afterCursor,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
.all();
|
|
172
|
+
|
|
173
|
+
return rows.some((row) => rawUserContentCarriesActivity(row.content));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Raw-column twin of the block check used by {@link messagesHaveUserActivity}. */
|
|
177
|
+
function rawUserContentCarriesActivity(raw: string | null): boolean {
|
|
178
|
+
if (raw === null || raw === "") {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const parsed: unknown = JSON.parse(raw);
|
|
183
|
+
if (!Array.isArray(parsed)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return blocksCarryNonToolResult(parsed as Array<{ type?: unknown }>);
|
|
187
|
+
} catch {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
88
192
|
/**
|
|
89
193
|
* Count skill-card rows strictly after the `(createdAt, id)` cursor. A LIKE
|
|
90
194
|
* prefilter keeps the scan from loading every row's metadata (cards are rare
|