@vellumai/assistant 0.10.3 → 0.10.4-staging.1
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/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -11,7 +11,12 @@ const mockProfiles = {
|
|
|
11
11
|
"cost-optimized": {},
|
|
12
12
|
disabled: { status: "disabled" },
|
|
13
13
|
"quality-optimized": {},
|
|
14
|
+
frontier: {},
|
|
14
15
|
};
|
|
16
|
+
// The advisor consult defaults to `llm.advisorProfile`. Set here so the
|
|
17
|
+
// advisor tests can assert the default and an explicit `inference_profile`
|
|
18
|
+
// override against the same shared config.
|
|
19
|
+
const mockAdvisorProfile = "frontier";
|
|
15
20
|
mock.module("../config/loader.js", () => ({
|
|
16
21
|
getConfigReadOnly: () => ({
|
|
17
22
|
llm: { profiles: mockProfiles },
|
|
@@ -24,13 +29,30 @@ mock.module("../config/loader.js", () => ({
|
|
|
24
29
|
model: "claude-opus-4-7",
|
|
25
30
|
},
|
|
26
31
|
profiles: mockProfiles,
|
|
32
|
+
advisorProfile: mockAdvisorProfile,
|
|
27
33
|
},
|
|
28
34
|
rateLimit: { maxRequestsPerMinute: 0 },
|
|
35
|
+
tools: { exclude: [] },
|
|
36
|
+
memory: { enabled: true },
|
|
29
37
|
}),
|
|
30
38
|
}));
|
|
39
|
+
|
|
40
|
+
// Mock the conversation registry so the advisor consult can resolve a fake
|
|
41
|
+
// parent conversation (snapshot messages + system prompt) without a live
|
|
42
|
+
// Conversation. Other executors in this suite never call `findConversation`.
|
|
43
|
+
let mockFindConversation: (conversationId: string) =>
|
|
44
|
+
| {
|
|
45
|
+
messages: Array<{ role: string; content: unknown[] }>;
|
|
46
|
+
getCurrentSystemPrompt: () => string;
|
|
47
|
+
}
|
|
48
|
+
| undefined = () => undefined;
|
|
49
|
+
mock.module("../daemon/conversation-registry.js", () => ({
|
|
50
|
+
findConversation: (conversationId: string) =>
|
|
51
|
+
mockFindConversation(conversationId),
|
|
52
|
+
}));
|
|
31
53
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
setConversationProcessingStartedAt: () => {},
|
|
55
|
+
isConversationProcessing: () => false,
|
|
34
56
|
setConversationOriginChannelIfUnset: () => {},
|
|
35
57
|
updateConversationContextWindow: () => {},
|
|
36
58
|
deleteMessageById: () => {},
|
|
@@ -58,7 +80,10 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
58
80
|
}));
|
|
59
81
|
|
|
60
82
|
import { getSubagentManager } from "../subagent/index.js";
|
|
61
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
SubagentAbortedError,
|
|
85
|
+
SubagentManager,
|
|
86
|
+
} from "../subagent/manager.js";
|
|
62
87
|
import type { SubagentState } from "../subagent/types.js";
|
|
63
88
|
import { executeSubagentAbort } from "../tools/subagent/abort.js";
|
|
64
89
|
import { executeSubagentMessage } from "../tools/subagent/message.js";
|
|
@@ -1564,8 +1589,378 @@ describe("Subagent role-based spawn", () => {
|
|
|
1564
1589
|
"coder",
|
|
1565
1590
|
"planner",
|
|
1566
1591
|
"investigator",
|
|
1592
|
+
"advisor",
|
|
1567
1593
|
]);
|
|
1568
1594
|
// role is not required
|
|
1569
1595
|
expect(def.input_schema.required).not.toContain("role");
|
|
1570
1596
|
});
|
|
1571
1597
|
});
|
|
1598
|
+
|
|
1599
|
+
// ── Advisor-role consult ────────────────────────────────────────────
|
|
1600
|
+
|
|
1601
|
+
describe("Subagent advisor-role consult", () => {
|
|
1602
|
+
type Block = { type: string; [k: string]: unknown };
|
|
1603
|
+
type CapturedAwait = {
|
|
1604
|
+
config: Record<string, unknown>;
|
|
1605
|
+
opts?: { signal?: AbortSignal; onText?: (chunk: string) => void };
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Stub `manager.spawnAndAwait` to capture the config + opts and resolve to
|
|
1610
|
+
* `advice`. Restores the original on cleanup. Returns the captured-call ref.
|
|
1611
|
+
*/
|
|
1612
|
+
function stubAwait(advice: string | (() => Promise<string>)): {
|
|
1613
|
+
captured: { current?: CapturedAwait };
|
|
1614
|
+
restore: () => void;
|
|
1615
|
+
} {
|
|
1616
|
+
const manager = getSubagentManager();
|
|
1617
|
+
const original = manager.spawnAndAwait.bind(manager);
|
|
1618
|
+
const captured: { current?: CapturedAwait } = {};
|
|
1619
|
+
manager.spawnAndAwait = (async (
|
|
1620
|
+
config: Record<string, unknown>,
|
|
1621
|
+
_send: unknown,
|
|
1622
|
+
opts?: CapturedAwait["opts"],
|
|
1623
|
+
) => {
|
|
1624
|
+
captured.current = { config, opts };
|
|
1625
|
+
return typeof advice === "function" ? await advice() : advice;
|
|
1626
|
+
}) as typeof manager.spawnAndAwait;
|
|
1627
|
+
return {
|
|
1628
|
+
captured,
|
|
1629
|
+
restore: () => {
|
|
1630
|
+
manager.spawnAndAwait = original;
|
|
1631
|
+
},
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
test("advisor role returns guidance synchronously as the tool result", async () => {
|
|
1636
|
+
mockFindConversation = () => ({
|
|
1637
|
+
messages: [{ role: "user", content: [{ type: "text", text: "Help" }] }],
|
|
1638
|
+
getCurrentSystemPrompt: () => "PARENT SYSTEM PROMPT",
|
|
1639
|
+
});
|
|
1640
|
+
const { captured, restore } = stubAwait("Here is my advice.");
|
|
1641
|
+
try {
|
|
1642
|
+
const result = await executeSubagentSpawn(
|
|
1643
|
+
{
|
|
1644
|
+
label: "Consult",
|
|
1645
|
+
objective: "advise me",
|
|
1646
|
+
role: "advisor",
|
|
1647
|
+
},
|
|
1648
|
+
makeContext("advisor-sess-1", { sendToClient: () => {} }),
|
|
1649
|
+
);
|
|
1650
|
+
expect(result.isError).toBe(false);
|
|
1651
|
+
expect(result.content).toBe("Here is my advice.");
|
|
1652
|
+
// Ran synchronously through spawnAndAwait, not fire-and-forget spawn.
|
|
1653
|
+
expect(captured.current).toBeDefined();
|
|
1654
|
+
expect(captured.current!.config.fork).toBe(true);
|
|
1655
|
+
expect(captured.current!.config.role).toBe("advisor");
|
|
1656
|
+
// Framing embeds the executor prompt as advisor system prompt context.
|
|
1657
|
+
expect(captured.current!.config.systemPromptOverride).toContain(
|
|
1658
|
+
"PARENT SYSTEM PROMPT",
|
|
1659
|
+
);
|
|
1660
|
+
} finally {
|
|
1661
|
+
restore();
|
|
1662
|
+
mockFindConversation = () => undefined;
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
test("advisor inherits and sanitizes the parent transcript", async () => {
|
|
1667
|
+
// Parent in-memory history carries a thinking block (must be stripped) and
|
|
1668
|
+
// a completed tool_use/tool_result pair (must be preserved).
|
|
1669
|
+
mockFindConversation = () => ({
|
|
1670
|
+
messages: [
|
|
1671
|
+
{ role: "user", content: [{ type: "text", text: "Do the task" }] },
|
|
1672
|
+
{
|
|
1673
|
+
role: "assistant",
|
|
1674
|
+
content: [
|
|
1675
|
+
{ type: "thinking", thinking: "secret reasoning" },
|
|
1676
|
+
{ type: "text", text: "Working on it" },
|
|
1677
|
+
{ type: "tool_use", id: "t1", name: "bash", input: {} },
|
|
1678
|
+
],
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
role: "user",
|
|
1682
|
+
content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }],
|
|
1683
|
+
},
|
|
1684
|
+
],
|
|
1685
|
+
getCurrentSystemPrompt: () => "SYS",
|
|
1686
|
+
});
|
|
1687
|
+
const { captured, restore } = stubAwait("advice");
|
|
1688
|
+
try {
|
|
1689
|
+
await executeSubagentSpawn(
|
|
1690
|
+
{ label: "Consult", objective: "x", role: "advisor" },
|
|
1691
|
+
makeContext("advisor-sess-2", { sendToClient: () => {} }),
|
|
1692
|
+
);
|
|
1693
|
+
const msgs = captured.current!.config.parentMessages as Array<{
|
|
1694
|
+
role: string;
|
|
1695
|
+
content: Block[];
|
|
1696
|
+
}>;
|
|
1697
|
+
const allBlocks = msgs.flatMap((m) => m.content);
|
|
1698
|
+
// Thinking is stripped; the completed tool_use/tool_result pair survives.
|
|
1699
|
+
expect(allBlocks.some((b) => b.type === "thinking")).toBe(false);
|
|
1700
|
+
expect(allBlocks.some((b) => b.type === "tool_use")).toBe(true);
|
|
1701
|
+
expect(allBlocks.some((b) => b.type === "tool_result")).toBe(true);
|
|
1702
|
+
} finally {
|
|
1703
|
+
restore();
|
|
1704
|
+
mockFindConversation = () => undefined;
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
test("in-flight plan is visible to the advisor with no dangling tool_use", async () => {
|
|
1709
|
+
// The in-memory snapshot ends on the user turn — the in-flight assistant
|
|
1710
|
+
// turn (this turn's plan + the pending advisor tool_use) lives only in the
|
|
1711
|
+
// DB at consult time. It must be appended, and its dangling tool_use stripped.
|
|
1712
|
+
mockFindConversation = () => ({
|
|
1713
|
+
messages: [
|
|
1714
|
+
{ role: "user", content: [{ type: "text", text: "Plan the work" }] },
|
|
1715
|
+
],
|
|
1716
|
+
getCurrentSystemPrompt: () => "SYS",
|
|
1717
|
+
});
|
|
1718
|
+
mockGetMessages = (convId: string) => {
|
|
1719
|
+
if (convId !== "advisor-sess-3") return null;
|
|
1720
|
+
return [
|
|
1721
|
+
{
|
|
1722
|
+
role: "user",
|
|
1723
|
+
content: JSON.stringify([{ type: "text", text: "Plan the work" }]),
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
role: "assistant",
|
|
1727
|
+
content: JSON.stringify([
|
|
1728
|
+
{ type: "text", text: "My plan: step 1, step 2." },
|
|
1729
|
+
{
|
|
1730
|
+
type: "tool_use",
|
|
1731
|
+
id: "adv-1",
|
|
1732
|
+
name: "subagent_spawn",
|
|
1733
|
+
input: {},
|
|
1734
|
+
},
|
|
1735
|
+
]),
|
|
1736
|
+
},
|
|
1737
|
+
];
|
|
1738
|
+
};
|
|
1739
|
+
const { captured, restore } = stubAwait("advice");
|
|
1740
|
+
try {
|
|
1741
|
+
await executeSubagentSpawn(
|
|
1742
|
+
{ label: "Consult", objective: "x", role: "advisor" },
|
|
1743
|
+
makeContext("advisor-sess-3", { sendToClient: () => {} }),
|
|
1744
|
+
);
|
|
1745
|
+
const msgs = captured.current!.config.parentMessages as Array<{
|
|
1746
|
+
role: string;
|
|
1747
|
+
content: Block[];
|
|
1748
|
+
}>;
|
|
1749
|
+
const allBlocks = msgs.flatMap((m) => m.content);
|
|
1750
|
+
// The plan text the model wrote this turn is present in the consult.
|
|
1751
|
+
expect(
|
|
1752
|
+
allBlocks.some(
|
|
1753
|
+
(b) => b.type === "text" && b.text === "My plan: step 1, step 2.",
|
|
1754
|
+
),
|
|
1755
|
+
).toBe(true);
|
|
1756
|
+
// No dangling tool_use is sent.
|
|
1757
|
+
expect(allBlocks.some((b) => b.type === "tool_use")).toBe(false);
|
|
1758
|
+
} finally {
|
|
1759
|
+
restore();
|
|
1760
|
+
mockFindConversation = () => undefined;
|
|
1761
|
+
mockGetMessages = () => null;
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
test("advisor defaults to llm.advisorProfile (forced)", async () => {
|
|
1766
|
+
mockFindConversation = () => ({
|
|
1767
|
+
messages: [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
|
|
1768
|
+
getCurrentSystemPrompt: () => "SYS",
|
|
1769
|
+
});
|
|
1770
|
+
const { captured, restore } = stubAwait("advice");
|
|
1771
|
+
try {
|
|
1772
|
+
await executeSubagentSpawn(
|
|
1773
|
+
{ label: "Consult", objective: "x", role: "advisor" },
|
|
1774
|
+
makeContext("advisor-sess-4", { sendToClient: () => {} }),
|
|
1775
|
+
);
|
|
1776
|
+
expect(captured.current!.config.overrideProfile).toBe("frontier");
|
|
1777
|
+
expect(captured.current!.config.forceOverrideProfile).toBe(true);
|
|
1778
|
+
} finally {
|
|
1779
|
+
restore();
|
|
1780
|
+
mockFindConversation = () => undefined;
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
test("advisor respects an explicit inference_profile over advisorProfile", async () => {
|
|
1785
|
+
mockFindConversation = () => ({
|
|
1786
|
+
messages: [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
|
|
1787
|
+
getCurrentSystemPrompt: () => "SYS",
|
|
1788
|
+
});
|
|
1789
|
+
const { captured, restore } = stubAwait("advice");
|
|
1790
|
+
try {
|
|
1791
|
+
await executeSubagentSpawn(
|
|
1792
|
+
{
|
|
1793
|
+
label: "Consult",
|
|
1794
|
+
objective: "x",
|
|
1795
|
+
role: "advisor",
|
|
1796
|
+
inference_profile: "quality-optimized",
|
|
1797
|
+
},
|
|
1798
|
+
makeContext("advisor-sess-5", { sendToClient: () => {} }),
|
|
1799
|
+
);
|
|
1800
|
+
expect(captured.current!.config.overrideProfile).toBe(
|
|
1801
|
+
"quality-optimized",
|
|
1802
|
+
);
|
|
1803
|
+
expect(captured.current!.config.forceOverrideProfile).toBe(true);
|
|
1804
|
+
} finally {
|
|
1805
|
+
restore();
|
|
1806
|
+
mockFindConversation = () => undefined;
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
test("advisor forwards streamed chunks to the tool's onOutput sink", async () => {
|
|
1811
|
+
mockFindConversation = () => ({
|
|
1812
|
+
messages: [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
|
|
1813
|
+
getCurrentSystemPrompt: () => "SYS",
|
|
1814
|
+
});
|
|
1815
|
+
const { captured, restore } = stubAwait("advice");
|
|
1816
|
+
const chunks: string[] = [];
|
|
1817
|
+
const onOutput = (c: string) => chunks.push(c);
|
|
1818
|
+
try {
|
|
1819
|
+
await executeSubagentSpawn(
|
|
1820
|
+
{ label: "Consult", objective: "x", role: "advisor" },
|
|
1821
|
+
makeContext("advisor-sess-6", { sendToClient: () => {}, onOutput }),
|
|
1822
|
+
);
|
|
1823
|
+
// onText is a progress-recording wrapper (resets the idle deadline), not
|
|
1824
|
+
// onOutput itself — but invoking it must still forward to onOutput.
|
|
1825
|
+
expect(captured.current!.opts?.onText).toBeInstanceOf(Function);
|
|
1826
|
+
captured.current!.opts?.onText?.("hello");
|
|
1827
|
+
expect(chunks).toEqual(["hello"]);
|
|
1828
|
+
expect(captured.current!.opts?.signal).toBeInstanceOf(AbortSignal);
|
|
1829
|
+
} finally {
|
|
1830
|
+
restore();
|
|
1831
|
+
mockFindConversation = () => undefined;
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
test("advisor degrades benignly when the consult throws (incl. depth limit)", async () => {
|
|
1836
|
+
mockFindConversation = () => ({
|
|
1837
|
+
messages: [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
|
|
1838
|
+
getCurrentSystemPrompt: () => "SYS",
|
|
1839
|
+
});
|
|
1840
|
+
const { restore } = stubAwait(async () => {
|
|
1841
|
+
throw new Error(
|
|
1842
|
+
"Cannot spawn subagent: parent is itself a subagent (max depth 1).",
|
|
1843
|
+
);
|
|
1844
|
+
});
|
|
1845
|
+
try {
|
|
1846
|
+
const result = await executeSubagentSpawn(
|
|
1847
|
+
{ label: "Consult", objective: "x", role: "advisor" },
|
|
1848
|
+
makeContext("advisor-sess-7", { sendToClient: () => {} }),
|
|
1849
|
+
);
|
|
1850
|
+
// Never fail the turn — benign non-error notice.
|
|
1851
|
+
expect(result.isError).toBe(false);
|
|
1852
|
+
expect(result.content).toContain("advisor unavailable");
|
|
1853
|
+
expect(result.content).toContain("parent is itself a subagent");
|
|
1854
|
+
} finally {
|
|
1855
|
+
restore();
|
|
1856
|
+
mockFindConversation = () => undefined;
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
test("advisor returns partial guidance (with a cut-off note) when the consult times out", async () => {
|
|
1861
|
+
mockFindConversation = () => ({
|
|
1862
|
+
messages: [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
|
|
1863
|
+
getCurrentSystemPrompt: () => "SYS",
|
|
1864
|
+
});
|
|
1865
|
+
// A timeout surfaces as SubagentAbortedError carrying the partial text the
|
|
1866
|
+
// advisor streamed before being cut off; that text must be salvaged.
|
|
1867
|
+
const { restore } = stubAwait(async () => {
|
|
1868
|
+
throw new SubagentAbortedError(
|
|
1869
|
+
"Lead with the data model, then wire reminders last.",
|
|
1870
|
+
);
|
|
1871
|
+
});
|
|
1872
|
+
try {
|
|
1873
|
+
const result = await executeSubagentSpawn(
|
|
1874
|
+
{ label: "Consult", objective: "x", role: "advisor" },
|
|
1875
|
+
makeContext("advisor-sess-timeout", { sendToClient: () => {} }),
|
|
1876
|
+
);
|
|
1877
|
+
expect(result.isError).toBe(false);
|
|
1878
|
+
expect(result.content).toContain(
|
|
1879
|
+
"Lead with the data model, then wire reminders last.",
|
|
1880
|
+
);
|
|
1881
|
+
expect(result.content).toContain("may be cut off");
|
|
1882
|
+
// Not the generic unavailable degrade — real guidance was preserved.
|
|
1883
|
+
expect(result.content).not.toContain("advisor unavailable");
|
|
1884
|
+
} finally {
|
|
1885
|
+
restore();
|
|
1886
|
+
mockFindConversation = () => undefined;
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
test("advisor still degrades when a timeout yields no partial text", async () => {
|
|
1891
|
+
mockFindConversation = () => ({
|
|
1892
|
+
messages: [{ role: "user", content: [{ type: "text", text: "Hi" }] }],
|
|
1893
|
+
getCurrentSystemPrompt: () => "SYS",
|
|
1894
|
+
});
|
|
1895
|
+
// Aborted before producing any text → empty partial → fall through to the
|
|
1896
|
+
// benign "advisor unavailable" notice.
|
|
1897
|
+
const { restore } = stubAwait(async () => {
|
|
1898
|
+
throw new SubagentAbortedError(" ");
|
|
1899
|
+
});
|
|
1900
|
+
try {
|
|
1901
|
+
const result = await executeSubagentSpawn(
|
|
1902
|
+
{ label: "Consult", objective: "x", role: "advisor" },
|
|
1903
|
+
makeContext("advisor-sess-timeout-empty", { sendToClient: () => {} }),
|
|
1904
|
+
);
|
|
1905
|
+
expect(result.isError).toBe(false);
|
|
1906
|
+
expect(result.content).toContain("advisor unavailable");
|
|
1907
|
+
} finally {
|
|
1908
|
+
restore();
|
|
1909
|
+
mockFindConversation = () => undefined;
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
test("advisor degrades benignly when no client is connected", async () => {
|
|
1914
|
+
// No sendToClient → the shared client guard fires before the advisor branch.
|
|
1915
|
+
const result = await executeSubagentSpawn(
|
|
1916
|
+
{ label: "Consult", objective: "x", role: "advisor" },
|
|
1917
|
+
makeContext("advisor-sess-8"),
|
|
1918
|
+
);
|
|
1919
|
+
expect(result.isError).toBe(true);
|
|
1920
|
+
expect(result.content).toContain("No client connected");
|
|
1921
|
+
});
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
// ── Advisor role tool-less enforcement ──────────────────────────────
|
|
1925
|
+
|
|
1926
|
+
describe("Advisor role is tool-less", () => {
|
|
1927
|
+
test("the advisor role's empty allowlist yields zero tools (not 'no filter')", async () => {
|
|
1928
|
+
// An empty allowlist must mean ZERO tools, not "no restriction". Build a
|
|
1929
|
+
// resolveTools callback with the advisor's empty allowlist and confirm no
|
|
1930
|
+
// tool survives — including a fake skill tool the projection would add.
|
|
1931
|
+
const { createResolveToolsCallback } =
|
|
1932
|
+
await import("../daemon/conversation-tool-setup.js");
|
|
1933
|
+
const { SUBAGENT_ROLE_REGISTRY } = await import("../subagent/types.js");
|
|
1934
|
+
const advisorAllowed = SUBAGENT_ROLE_REGISTRY.advisor.allowedTools;
|
|
1935
|
+
expect(advisorAllowed).toEqual([]);
|
|
1936
|
+
|
|
1937
|
+
const toolDefs = [
|
|
1938
|
+
{ name: "bash", description: "", input_schema: { type: "object" } },
|
|
1939
|
+
{ name: "file_read", description: "", input_schema: { type: "object" } },
|
|
1940
|
+
];
|
|
1941
|
+
const ctx = {
|
|
1942
|
+
skillProjectionState: new Map<string, string>(),
|
|
1943
|
+
skillProjectionCache: new Map(),
|
|
1944
|
+
coreToolNames: new Set(toolDefs.map((d) => d.name)),
|
|
1945
|
+
toolsDisabledDepth: 0,
|
|
1946
|
+
// The advisor role applies `new Set(allowedTools)` — empty Set here.
|
|
1947
|
+
subagentAllowedTools: new Set<string>(advisorAllowed),
|
|
1948
|
+
// Default (absent) gate mode is "wire": the allowlist filters the wire
|
|
1949
|
+
// tool list, so an empty Set leaves nothing.
|
|
1950
|
+
isSubagent: true,
|
|
1951
|
+
} as unknown as Parameters<typeof createResolveToolsCallback>[1];
|
|
1952
|
+
|
|
1953
|
+
const resolve = createResolveToolsCallback(
|
|
1954
|
+
toolDefs as unknown as Parameters<typeof createResolveToolsCallback>[0],
|
|
1955
|
+
ctx,
|
|
1956
|
+
);
|
|
1957
|
+
expect(resolve).toBeDefined();
|
|
1958
|
+
const resolved = resolve!([]);
|
|
1959
|
+
expect(resolved).toEqual([]);
|
|
1960
|
+
// The per-turn execution gate is likewise empty.
|
|
1961
|
+
expect(
|
|
1962
|
+
(ctx as unknown as { allowedToolNames?: Set<string> }).allowedToolNames
|
|
1963
|
+
?.size ?? 0,
|
|
1964
|
+
).toBe(0);
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
@@ -98,7 +98,6 @@ import {
|
|
|
98
98
|
saveRawConfig,
|
|
99
99
|
setNestedValue,
|
|
100
100
|
} from "../config/loader.js";
|
|
101
|
-
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
102
101
|
import {
|
|
103
102
|
type ChannelCapabilities,
|
|
104
103
|
loadSlackChronologicalContext,
|
|
@@ -121,6 +120,7 @@ import {
|
|
|
121
120
|
} from "../runtime/routes/inbound-message-handler.js";
|
|
122
121
|
import {
|
|
123
122
|
handleChannelInbound,
|
|
123
|
+
seedContactChannel,
|
|
124
124
|
setAdapterProcessMessage,
|
|
125
125
|
} from "./helpers/channel-test-adapter.js";
|
|
126
126
|
|
|
@@ -1902,7 +1902,7 @@ function resetHttpState(): void {
|
|
|
1902
1902
|
}
|
|
1903
1903
|
|
|
1904
1904
|
function seedHttpActiveMember(chatId = HTTP_SLACK_CHANNEL_ID): void {
|
|
1905
|
-
|
|
1905
|
+
seedContactChannel({
|
|
1906
1906
|
sourceChannel: "slack",
|
|
1907
1907
|
externalUserId: HTTP_SLACK_USER_ID,
|
|
1908
1908
|
externalChatId: chatId,
|
|
@@ -1913,7 +1913,7 @@ function seedHttpActiveMember(chatId = HTTP_SLACK_CHANNEL_ID): void {
|
|
|
1913
1913
|
}
|
|
1914
1914
|
|
|
1915
1915
|
function seedHttpGuardianMember(chatId = HTTP_SLACK_CHANNEL_ID): void {
|
|
1916
|
-
|
|
1916
|
+
seedContactChannel({
|
|
1917
1917
|
sourceChannel: "slack",
|
|
1918
1918
|
externalUserId: HTTP_SLACK_USER_ID,
|
|
1919
1919
|
externalChatId: chatId,
|
|
@@ -59,14 +59,27 @@ const reserveMessageMock = mock(
|
|
|
59
59
|
);
|
|
60
60
|
const updateMessageContentMock = mock((_id: string, _content: string) => {});
|
|
61
61
|
|
|
62
|
+
// Stand-in for the `conversations.seq` column. The DB-backed
|
|
63
|
+
// `recordConversationPersistedSeq` / `getConversationPersistedSeq` are mocked
|
|
64
|
+
// over this map with the same monotonic, ignore-non-positive semantics so the
|
|
65
|
+
// handler's persisted-seq writes are observable without a real database.
|
|
66
|
+
const persistedSeqByConversation = new Map<string, number>();
|
|
67
|
+
|
|
62
68
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
setConversationProcessingStartedAt: () => {},
|
|
70
|
+
isConversationProcessing: () => false,
|
|
65
71
|
getConversation: () => null,
|
|
66
72
|
getMessageById: () => null,
|
|
67
73
|
updateMessageContent: updateMessageContentMock,
|
|
68
74
|
provenanceFromTrustContext: () => ({}),
|
|
69
75
|
reserveMessage: reserveMessageMock,
|
|
76
|
+
recordConversationPersistedSeq: (id: string, seq: number) => {
|
|
77
|
+
if (!Number.isFinite(seq) || seq <= 0) return;
|
|
78
|
+
const prev = persistedSeqByConversation.get(id);
|
|
79
|
+
if (prev == null || prev < seq) persistedSeqByConversation.set(id, seq);
|
|
80
|
+
},
|
|
81
|
+
getConversationPersistedSeq: (id: string) =>
|
|
82
|
+
persistedSeqByConversation.get(id) ?? null,
|
|
70
83
|
}));
|
|
71
84
|
|
|
72
85
|
mock.module("../memory/conversation-disk-view.js", () => ({
|
|
@@ -102,11 +115,11 @@ import {
|
|
|
102
115
|
handleToolUsePreviewStart,
|
|
103
116
|
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
104
117
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
118
|
+
import { getConversationPersistedSeq } from "../memory/conversation-crud.js";
|
|
105
119
|
import type { AssistantEvent } from "../runtime/assistant-event.js";
|
|
106
120
|
import {
|
|
107
121
|
_resetStreamStateForTesting,
|
|
108
122
|
getCurrentSeq,
|
|
109
|
-
getPersistedSeq,
|
|
110
123
|
stampAndBuffer,
|
|
111
124
|
} from "../runtime/assistant-stream-state.js";
|
|
112
125
|
|
|
@@ -324,6 +337,7 @@ describe("tool preview lifecycle", () => {
|
|
|
324
337
|
describe("persisted seq advances on tool_use_start", () => {
|
|
325
338
|
beforeEach(() => {
|
|
326
339
|
_resetStreamStateForTesting();
|
|
340
|
+
persistedSeqByConversation.clear();
|
|
327
341
|
});
|
|
328
342
|
|
|
329
343
|
test("advances the conversation's persisted seq to the tool_use_start seq", () => {
|
|
@@ -374,8 +388,8 @@ describe("tool preview lifecycle", () => {
|
|
|
374
388
|
(e) => e.type === "tool_use_start",
|
|
375
389
|
);
|
|
376
390
|
expect(toolUseStart).toBeDefined();
|
|
377
|
-
expect(
|
|
378
|
-
expect(
|
|
391
|
+
expect(getConversationPersistedSeq(conversationId)).toBe(getCurrentSeq());
|
|
392
|
+
expect(getConversationPersistedSeq(conversationId)).toBe(
|
|
379
393
|
(toolUseStart as unknown as AssistantEvent).seq ?? null,
|
|
380
394
|
);
|
|
381
395
|
});
|
|
@@ -386,6 +400,7 @@ describe("tool preview lifecycle", () => {
|
|
|
386
400
|
|
|
387
401
|
beforeEach(() => {
|
|
388
402
|
_resetStreamStateForTesting();
|
|
403
|
+
persistedSeqByConversation.clear();
|
|
389
404
|
});
|
|
390
405
|
|
|
391
406
|
/** onEvent that stamps conversation-scoped events like the runtime hub. */
|
|
@@ -474,8 +489,8 @@ describe("tool preview lifecycle", () => {
|
|
|
474
489
|
(e) => e.type === "assistant_thinking_delta",
|
|
475
490
|
);
|
|
476
491
|
expect(thinkingDelta).toBeDefined();
|
|
477
|
-
expect(
|
|
478
|
-
expect(
|
|
492
|
+
expect(getConversationPersistedSeq(conversationId)).toBe(getCurrentSeq());
|
|
493
|
+
expect(getConversationPersistedSeq(conversationId)).toBe(
|
|
479
494
|
(thinkingDelta as unknown as AssistantEvent).seq ?? null,
|
|
480
495
|
);
|
|
481
496
|
});
|
|
@@ -505,8 +520,8 @@ describe("tool preview lifecycle", () => {
|
|
|
505
520
|
// THEN the persisted seq equals the just-stamped tool_result seq
|
|
506
521
|
const toolResult = events.find((e) => e.type === "tool_result");
|
|
507
522
|
expect(toolResult).toBeDefined();
|
|
508
|
-
expect(
|
|
509
|
-
expect(
|
|
523
|
+
expect(getConversationPersistedSeq(conversationId)).toBe(getCurrentSeq());
|
|
524
|
+
expect(getConversationPersistedSeq(conversationId)).toBe(
|
|
510
525
|
(toolResult as unknown as AssistantEvent).seq ?? null,
|
|
511
526
|
);
|
|
512
527
|
});
|
|
@@ -539,7 +554,7 @@ describe("tool preview lifecycle", () => {
|
|
|
539
554
|
events.find((e) => e.type === "assistant_thinking_delta"),
|
|
540
555
|
).toBeUndefined();
|
|
541
556
|
expect(state.lastPersistedContentSeq).toBeUndefined();
|
|
542
|
-
expect(
|
|
557
|
+
expect(getConversationPersistedSeq(conversationId)).toBeNull();
|
|
543
558
|
});
|
|
544
559
|
});
|
|
545
560
|
|
|
@@ -548,6 +563,7 @@ describe("tool preview lifecycle", () => {
|
|
|
548
563
|
|
|
549
564
|
beforeEach(() => {
|
|
550
565
|
_resetStreamStateForTesting();
|
|
566
|
+
persistedSeqByConversation.clear();
|
|
551
567
|
reserveMessageMock.mockClear();
|
|
552
568
|
updateMessageContentMock.mockClear();
|
|
553
569
|
});
|
|
@@ -43,8 +43,8 @@ let mockedRowContent = "";
|
|
|
43
43
|
const updates: Array<{ id: string; content: string }> = [];
|
|
44
44
|
|
|
45
45
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
setConversationProcessingStartedAt: () => {},
|
|
47
|
+
isConversationProcessing: () => false,
|
|
48
48
|
addMessage: () => ({ id: "mock-msg-id" }),
|
|
49
49
|
getMessageById: (id: string) =>
|
|
50
50
|
mockedRowContent ? { id, content: mockedRowContent } : null,
|
|
@@ -53,6 +53,8 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
53
53
|
},
|
|
54
54
|
provenanceFromTrustContext: () => ({}),
|
|
55
55
|
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
56
|
+
recordConversationPersistedSeq: () => {},
|
|
57
|
+
getConversationPersistedSeq: () => null,
|
|
56
58
|
}));
|
|
57
59
|
|
|
58
60
|
mock.module("../memory/llm-request-log-store.js", () => ({
|
|
@@ -62,7 +64,6 @@ mock.module("../memory/llm-request-log-store.js", () => ({
|
|
|
62
64
|
|
|
63
65
|
mock.module("../runtime/assistant-stream-state.js", () => ({
|
|
64
66
|
getCurrentSeq: () => 0,
|
|
65
|
-
recordPersistedSeq: () => {},
|
|
66
67
|
}));
|
|
67
68
|
|
|
68
69
|
// ── Imports (after mocks) ─────────────────────────────────────────────────────
|