@vellumai/assistant 0.12.1-dev.202609141913.5e16607 → 0.12.1-dev.202609142215.1dfc46d

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.
Files changed (120) hide show
  1. package/ARCHITECTURE.md +9 -9
  2. package/openapi.yaml +35 -2
  3. package/package.json +2 -1
  4. package/src/__tests__/anthropic-provider.test.ts +0 -63
  5. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  6. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  7. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  8. package/src/__tests__/computer-use-tools.test.ts +2 -2
  9. package/src/__tests__/heartbeat-service.test.ts +56 -40
  10. package/src/__tests__/host-cu-proxy.test.ts +539 -0
  11. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  12. package/src/__tests__/oauth-commands-routes.test.ts +113 -0
  13. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  14. package/src/__tests__/openai-provider.test.ts +1 -0
  15. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  16. package/src/__tests__/qdrant-manager.test.ts +132 -0
  17. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  18. package/src/__tests__/skills.test.ts +9 -4
  19. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  20. package/src/__tests__/unicode.test.ts +13 -0
  21. package/src/acp/agent-process.ts +2 -1
  22. package/src/agent/loop.ts +1 -0
  23. package/src/api/events/user-message-echo.ts +7 -1
  24. package/src/api/responses/conversation-message.ts +5 -0
  25. package/src/avatar/identity-avatar.ts +7 -2
  26. package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
  27. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  28. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  29. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  30. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  31. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  32. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  33. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  34. package/src/config/bundled-skills/skill-management/TOOLS.json +4 -0
  35. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  36. package/src/credential-health/credential-health-service.ts +11 -17
  37. package/src/daemon/conversation-error.ts +3 -2
  38. package/src/daemon/conversation-media-retry.ts +2 -1
  39. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  40. package/src/daemon/conversation-surfaces.ts +5 -1
  41. package/src/daemon/dictation-profile-store.ts +2 -1
  42. package/src/daemon/handlers/shared.ts +2 -1
  43. package/src/daemon/handlers/skills.ts +6 -3
  44. package/src/daemon/host-cu-proxy.ts +186 -19
  45. package/src/daemon/host-proxy-preactivation.ts +2 -1
  46. package/src/daemon/reaction-history-render.ts +2 -1
  47. package/src/desktop/desktop-chrome-policy.test.ts +9 -1
  48. package/src/desktop/desktop-chrome-policy.ts +6 -1
  49. package/src/heartbeat/heartbeat-service.ts +21 -3
  50. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  51. package/src/live-voice/live-voice-photo.ts +25 -6
  52. package/src/oauth/AGENTS.md +2 -0
  53. package/src/oauth/byo-connection.test.ts +60 -0
  54. package/src/oauth/scope-utils.ts +24 -0
  55. package/src/oauth/seed-providers.ts +39 -25
  56. package/src/persistence/conversation-queries.ts +2 -2
  57. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  58. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  59. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  60. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  61. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  62. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  63. package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
  64. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  65. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  66. package/src/plugins/defaults/memory/startup.ts +22 -1
  67. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  68. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  69. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  70. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  71. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  72. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  73. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +39 -3
  74. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  75. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  76. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  77. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  78. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  79. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  80. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  81. package/src/providers/anthropic/client.ts +0 -47
  82. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  83. package/src/providers/openai/chat-completions-provider.ts +20 -1
  84. package/src/providers/openrouter/client.ts +20 -0
  85. package/src/providers/outbound-request-sanitize.ts +96 -0
  86. package/src/providers/retry.ts +24 -3
  87. package/src/providers/unparseable-tool-args.ts +5 -1
  88. package/src/runtime/AGENTS.md +1 -1
  89. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  90. package/src/runtime/agent-wake.ts +2 -1
  91. package/src/runtime/assistant-event-hub.ts +21 -3
  92. package/src/runtime/host-observe.ts +11 -1
  93. package/src/runtime/routes/conversation-routes.ts +16 -3
  94. package/src/runtime/routes/diagnostics-routes.ts +6 -1
  95. package/src/runtime/routes/events-routes.ts +4 -0
  96. package/src/runtime/routes/host-cu-routes.ts +6 -0
  97. package/src/runtime/routes/oauth-commands-routes.ts +36 -0
  98. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  99. package/src/runtime/routes/workspace-routes.ts +46 -63
  100. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  101. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  102. package/src/runtime/routes/workspace-utils.ts +17 -0
  103. package/src/schedule/run-script.ts +2 -1
  104. package/src/schedule/schedule-store.ts +9 -2
  105. package/src/security/untrusted-content.ts +3 -1
  106. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  107. package/src/tools/browser/browser-execution.ts +1 -1
  108. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  109. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  110. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  111. package/src/tools/browser/network-recorder.ts +2 -1
  112. package/src/tools/computer-use/definitions.ts +117 -0
  113. package/src/tools/filesystem/search.ts +2 -1
  114. package/src/tools/network/web-search.ts +6 -1
  115. package/src/tools/skills/scaffold-managed.ts +61 -3
  116. package/src/types/host-capabilities.ts +5 -3
  117. package/src/util/unicode.ts +15 -1
  118. package/src/watch/watch-timeline.ts +2 -2
  119. package/src/workflows/run-manager.ts +2 -1
  120. package/src/workspace/git-service.ts +1 -1
package/ARCHITECTURE.md CHANGED
@@ -1406,15 +1406,15 @@ skills/<skill-id>/
1406
1406
 
1407
1407
  The following capabilities ship as bundled skills in `assistant/src/config/bundled-skills/`:
1408
1408
 
1409
- | Skill ID | Tools | Purpose |
1410
- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1411
- | `browser` | `browser_navigate`, `browser_snapshot`, `browser_screenshot`, `browser_close`, `browser_click`, `browser_type`, `browser_press_key`, `browser_wait_for`, `browser_extract`, `browser_fill_credential` | Headless browser automation — web scraping, form filling, interaction (previously core-registered as `headless-browser`; now skill-provided with default allow rules) |
1412
- | `gmail` | Gmail search, archive, send, etc. | Email management via OAuth2 integration |
1413
- | `computer-use` | `computer_use_observe`, `computer_use_click`, `computer_use_type_text`, `computer_use_key`, `computer_use_scroll`, `computer_use_drag`, `computer_use_wait`, `computer_use_open_app`, `computer_use_run_applescript`, `computer_use_done`, `computer_use_respond` | Computer-use proxy tools preactivated via `preactivatedSkillIds` in desktop sessions. Each tool forwards actions to a compatible connected desktop host via `HostCuProxy`, which handles request/resolve proxying, step counting, loop detection, and observation formatting in the same agent loop. |
1414
- | `weather` | `get-weather` | Fetch current weather data |
1415
- | `app-builder` | `app_create`, `app_delete`, `app_refresh`, `app_generate_icon` | Dynamic app authoring — create and manage persistent apps; file editing uses generic file tools plus `app_refresh` (activated via `skill_load app-builder`; `app_open` remains a core proxy tool) |
1416
- | `self-upgrade` | (instruction-only) | Self-improvement workflow |
1417
- | `start-the-day` | (instruction-only) | Morning briefing routine |
1409
+ | Skill ID | Tools | Purpose |
1410
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1411
+ | `browser` | `browser_navigate`, `browser_snapshot`, `browser_screenshot`, `browser_close`, `browser_click`, `browser_type`, `browser_press_key`, `browser_wait_for`, `browser_extract`, `browser_fill_credential` | Headless browser automation: web scraping, form filling, interaction (previously core-registered as `headless-browser`; now skill-provided with default allow rules) |
1412
+ | `gmail` | Gmail search, archive, send, etc. | Email management via OAuth2 integration |
1413
+ | `computer-use` | `computer_use_observe`, `computer_use_click`, `computer_use_type_text`, `computer_use_key`, `computer_use_scroll`, `computer_use_drag`, `computer_use_wait`, `computer_use_open_app`, `computer_use_run_applescript`, `computer_use_sequence`, `computer_use_done`, `computer_use_respond` | Computer-use proxy tools preactivated via `preactivatedSkillIds` in desktop sessions. Each tool forwards actions to a compatible connected desktop host via `HostCuProxy`, which handles request/resolve proxying, step counting, loop detection, and observation formatting in the same agent loop. |
1414
+ | `weather` | `get-weather` | Fetch current weather data |
1415
+ | `app-builder` | `app_create`, `app_delete`, `app_refresh`, `app_generate_icon` | Dynamic app authoring: create and manage persistent apps; file editing uses generic file tools plus `app_refresh` (activated via `skill_load app-builder`; `app_open` remains a core proxy tool) |
1416
+ | `self-upgrade` | (instruction-only) | Self-improvement workflow |
1417
+ | `start-the-day` | (instruction-only) | Morning briefing routine |
1418
1418
 
1419
1419
  ### Activation and Projection Flow
1420
1420
 
package/openapi.yaml CHANGED
@@ -15981,6 +15981,13 @@ paths:
15981
15981
  type: string
15982
15982
  userGuidance:
15983
15983
  type: string
15984
+ timings:
15985
+ type: object
15986
+ propertyNames:
15987
+ type: string
15988
+ additionalProperties:
15989
+ type: number
15990
+ description: Per-phase helper timings in milliseconds
15984
15991
  required:
15985
15992
  - requestId
15986
15993
  responses:
@@ -22068,6 +22075,9 @@ paths:
22068
22075
  type: boolean
22069
22076
  noResponse:
22070
22077
  type: boolean
22078
+ cameraFrame:
22079
+ type: boolean
22080
+ const: true
22071
22081
  assistantTextVisibility:
22072
22082
  type: string
22073
22083
  enum:
@@ -37047,8 +37057,18 @@ paths:
37047
37057
  schema:
37048
37058
  type: string
37049
37059
  description:
37050
- "Compute recursive byte size for each directory entry (true/false). Budget-bounded — large subtrees may
37051
- return size: null."
37060
+ "Compute recursive byte size for each directory entry (true/false). Budget-bounded: large subtrees may
37061
+ return size: null. Not applied to a recursive listing, which carries every file for the client to sum."
37062
+ - name: recursive
37063
+ in: query
37064
+ required: false
37065
+ schema:
37066
+ type: string
37067
+ description:
37068
+ "List the whole subtree in one response (true/false). Depth-first, each directory's entries in listing
37069
+ order, so grouping by parent path recovers per-directory listings. Bounded: sets truncated when the entry
37070
+ cap or deadline stops the walk, and does not enter directories the workspace gitignore rules exclude, which
37071
+ are named in skipped."
37052
37072
  responses:
37053
37073
  "200":
37054
37074
  description: Successful response
@@ -37092,6 +37112,19 @@ paths:
37092
37112
  - modifiedAt
37093
37113
  additionalProperties: false
37094
37114
  description: Directory entry objects
37115
+ truncated:
37116
+ description:
37117
+ A recursive walk stopped at its entry cap or deadline, so deeper entries are missing. Absent from
37118
+ assistants that predate recursive listings.
37119
+ type: boolean
37120
+ skipped:
37121
+ description:
37122
+ Directories a recursive walk listed but did not enter, whether because a workspace gitignore rule excludes
37123
+ them, they are symlinks, or they could not be read. Each can still be listed on its own. Absent
37124
+ from assistants that predate recursive listings.
37125
+ type: array
37126
+ items:
37127
+ type: string
37095
37128
  required:
37096
37129
  - path
37097
37130
  - entries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.1-dev.202609141913.5e16607",
3
+ "version": "0.12.1-dev.202609142215.1dfc46d",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -62,6 +62,7 @@
62
62
  "dotenv": "17.3.1",
63
63
  "drizzle-orm": "0.45.2",
64
64
  "hast-util-to-html": "9.0.5",
65
+ "ignore": "7.0.9",
65
66
  "jszip": "3.10.1",
66
67
  "marked": "18.0.0",
67
68
  "mdast-util-to-hast": "13.2.1",
@@ -3029,69 +3029,6 @@ describe("AnthropicProvider - internal attachment id", () => {
3029
3029
  });
3030
3030
  });
3031
3031
 
3032
- // ---------------------------------------------------------------------------
3033
- // Tests — Orphaned UTF-16 surrogate sanitization
3034
- // ---------------------------------------------------------------------------
3035
-
3036
- describe("AnthropicProvider — surrogate sanitization", () => {
3037
- let provider: AnthropicProvider;
3038
-
3039
- beforeEach(() => {
3040
- lastStreamParams = null;
3041
- provider = new AnthropicProvider("sk-ant-test", "claude-sonnet-4-6");
3042
- });
3043
-
3044
- test("strips orphaned high surrogate from a tool result before sending", async () => {
3045
- // An orphaned high surrogate — the exact shape that triggers Anthropic's
3046
- // "no low surrogate in string" 400. The mock's JSON.parse(JSON.stringify)
3047
- // on line ~44 would throw if sanitization didn't happen.
3048
- const LONE_HIGH = "\uD83C";
3049
- const messages: Message[] = [
3050
- toolUseMsg("tu1", "bash"),
3051
- toolResultMsg("tu1", `shell output ${LONE_HIGH} more output`),
3052
- userMsg("what happened?"),
3053
- ];
3054
-
3055
- await provider.sendMessage(messages);
3056
-
3057
- const sent = lastStreamParams!.messages as Array<{
3058
- role: string;
3059
- content: Array<{ type: string; content?: string; text?: string }>;
3060
- }>;
3061
- // Find the tool_result block in the captured payload and assert no orphans.
3062
- const toolResult = sent
3063
- .flatMap((m) => m.content)
3064
- .find((b) => b.type === "tool_result");
3065
- expect(toolResult).toBeDefined();
3066
- expect(toolResult!.content).toBeDefined();
3067
- const content = toolResult!.content as string;
3068
- for (let i = 0; i < content.length; i++) {
3069
- const code = content.charCodeAt(i);
3070
- if (code >= 0xd800 && code <= 0xdbff) {
3071
- const next = i + 1 < content.length ? content.charCodeAt(i + 1) : 0;
3072
- expect(next >= 0xdc00 && next <= 0xdfff).toBe(true);
3073
- i++;
3074
- } else {
3075
- expect(code < 0xdc00 || code > 0xdfff).toBe(true);
3076
- }
3077
- }
3078
- });
3079
-
3080
- test("clean payloads are not copied unnecessarily", async () => {
3081
- // When there are no orphans, the sanitizer should be a no-op. We can't
3082
- // easily assert reference equality through the mock boundary (the mock
3083
- // JSON-round-trips params for capture), but we can at least confirm the
3084
- // call succeeds without error on ordinary payloads containing valid
3085
- // surrogate pairs (emoji).
3086
- const EMOJI = "\uD83C\uDF89";
3087
- await provider.sendMessage([userMsg(`hello ${EMOJI} world`)]);
3088
- const sent = lastStreamParams!.messages as Array<{
3089
- content: Array<{ text?: string }>;
3090
- }>;
3091
- expect(sent[0].content[0].text).toContain(EMOJI);
3092
- });
3093
- });
3094
-
3095
3032
  // ---------------------------------------------------------------------------
3096
3033
  // Haiku model gating
3097
3034
  // ---------------------------------------------------------------------------
@@ -125,7 +125,10 @@ describe("AssistantEventHub — machineName", () => {
125
125
  });
126
126
  });
127
127
 
128
- describe("window capture negotiation", () => {
128
+ describe.each([
129
+ ["x-vellum-cu-window-capture", "host_cu_window_capture"],
130
+ ["x-vellum-cu-sequence", "host_cu_sequence"],
131
+ ] as const)("%s negotiation", (header, capability) => {
129
132
  test.each([
130
133
  ["macos", undefined, false],
131
134
  ["macos", "0", false],
@@ -145,18 +148,14 @@ describe("window capture negotiation", () => {
145
148
  headers: {
146
149
  "x-vellum-client-id": "client-1",
147
150
  "x-vellum-interface-id": interfaceId,
148
- ...(advertised
149
- ? { "x-vellum-cu-window-capture": advertised }
150
- : {}),
151
+ ...(advertised ? { [header]: advertised } : {}),
151
152
  },
152
153
  abortSignal: ac.signal,
153
154
  },
154
155
  { hub },
155
156
  );
156
157
  expect(
157
- hub
158
- .getClientById("client-1")
159
- ?.capabilities.includes("host_cu_window_capture"),
158
+ hub.getClientById("client-1")?.capabilities.includes(capability),
160
159
  ).toBe(supported);
161
160
  } finally {
162
161
  ac.abort();
@@ -690,3 +690,47 @@ test("window requests never reach an older overlapping connection with the same
690
690
  supported.dispose();
691
691
  }
692
692
  });
693
+
694
+ test("batched action requests never reach an older overlapping connection with the same device ID", async () => {
695
+ const receivedLegacy: unknown[] = [];
696
+ const receivedSupported: unknown[] = [];
697
+ const legacy = assistantEventHub.subscribe({
698
+ type: "client",
699
+ clientId: "sequence-client",
700
+ interfaceId: "macos",
701
+ capabilities: ["host_cu"],
702
+ callback: (event) => {
703
+ receivedLegacy.push(event);
704
+ },
705
+ });
706
+ const supported = assistantEventHub.subscribe({
707
+ type: "client",
708
+ clientId: "sequence-client",
709
+ interfaceId: "macos",
710
+ capabilities: ["host_cu", "host_cu_sequence"],
711
+ callback: (event) => {
712
+ receivedSupported.push(event);
713
+ },
714
+ });
715
+ try {
716
+ broadcastMessage(
717
+ {
718
+ type: "host_cu_request",
719
+ requestId: "sequence-request",
720
+ conversationId: "sequence-session",
721
+ toolName: "computer_use_sequence",
722
+ input: { actions: [{ action: "key", key: "cmd+n" }] },
723
+ stepNumber: 1,
724
+ targetClientId: "sequence-client",
725
+ },
726
+ "sequence-session",
727
+ { targetClientId: "sequence-client" },
728
+ );
729
+ await new Promise((resolve) => setTimeout(resolve, 0));
730
+ expect(receivedLegacy).toHaveLength(0);
731
+ expect(receivedSupported).toHaveLength(1);
732
+ } finally {
733
+ legacy.dispose();
734
+ supported.dispose();
735
+ }
736
+ });
@@ -29,7 +29,7 @@ const manifestPath = resolve(
29
29
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
30
30
 
31
31
  describe("computer-use skill manifest regression", () => {
32
- test("manifest has exactly 11 tools", () => {
32
+ test("manifest has exactly 12 tools", () => {
33
33
  expect(manifest.tools).toHaveLength(COMPUTER_USE_TOOL_COUNT);
34
34
  });
35
35
 
@@ -37,8 +37,8 @@ const ctx: ToolContext = {
37
37
  // ── Tool definitions ────────────────────────────────────────────────
38
38
 
39
39
  describe("computer-use tool definitions", () => {
40
- test("allComputerUseTools contains 11 tools", () => {
41
- expect(allComputerUseTools.length).toBe(11);
40
+ test("allComputerUseTools contains 12 tools", () => {
41
+ expect(allComputerUseTools.length).toBe(12);
42
42
  });
43
43
 
44
44
  test("all tools belong to computer-use category", () => {
@@ -557,52 +557,52 @@ describe("HeartbeatService", () => {
557
557
  expect(processMessageCalls).toHaveLength(1);
558
558
  });
559
559
 
560
- test("active hours handles overnight window", async () => {
561
- setHeartbeatConfig({ activeHoursStart: 22, activeHoursEnd: 6 });
560
+ test("active hours handles overnight window", async () => {
561
+ setHeartbeatConfig({ activeHoursStart: 22, activeHoursEnd: 6 });
562
562
 
563
- // 23:00 should be within the window
564
- const service = createService({ getCurrentHour: () => 23 });
565
- await service.runOnce();
566
- expect(processMessageCalls).toHaveLength(1);
563
+ // 23:00 should be within the window
564
+ const service = createService({ getCurrentHour: () => 23 });
565
+ await service.runOnce();
566
+ expect(processMessageCalls).toHaveLength(1);
567
567
 
568
- // 10:00 should be outside the window
569
- processMessageCalls.length = 0;
570
- createdConversations.length = 0;
571
- const service2 = createService({ getCurrentHour: () => 10 });
572
- await service2.runOnce();
573
- expect(processMessageCalls).toHaveLength(0);
574
- });
568
+ // 10:00 should be outside the window
569
+ processMessageCalls.length = 0;
570
+ createdConversations.length = 0;
571
+ const service2 = createService({ getCurrentHour: () => 10 });
572
+ await service2.runOnce();
573
+ expect(processMessageCalls).toHaveLength(0);
574
+ });
575
575
 
576
- test("active hours guard uses user timezone in interval mode", async () => {
577
- setConfig("ui", { detectedTimezone: "America/Los_Angeles" });
578
- setHeartbeatConfig({
579
- cronExpression: null,
580
- timezone: null,
581
- activeHoursStart: 8,
582
- activeHoursEnd: 22,
583
- });
576
+ test("active hours guard uses user timezone in interval mode", async () => {
577
+ setConfig("ui", { detectedTimezone: "America/Los_Angeles" });
578
+ setHeartbeatConfig({
579
+ cronExpression: null,
580
+ timezone: null,
581
+ activeHoursStart: 8,
582
+ activeHoursEnd: 22,
583
+ });
584
584
 
585
- // 23:00 UTC is 16:00 Pacific (inside 8-22) and 23:00 host-local
586
- // (outside 8-22). If the guard still used the host clock this would skip.
587
- const inside = createService({
588
- now: () => new Date("2026-09-08T23:00:00Z"),
589
- getCurrentHour: () => 23,
590
- });
591
- expect(await inside.runOnce()).toBe(true);
592
- expect(processMessageCalls).toHaveLength(1);
585
+ // 23:00 UTC is 16:00 Pacific (inside 8-22) and 23:00 host-local
586
+ // (outside 8-22). If the guard still used the host clock this would skip.
587
+ const inside = createService({
588
+ now: () => new Date("2026-09-08T23:00:00Z"),
589
+ getCurrentHour: () => 23,
590
+ });
591
+ expect(await inside.runOnce()).toBe(true);
592
+ expect(processMessageCalls).toHaveLength(1);
593
593
 
594
- processMessageCalls.length = 0;
595
- createdConversations.length = 0;
594
+ processMessageCalls.length = 0;
595
+ createdConversations.length = 0;
596
596
 
597
- // 06:00 UTC is 23:00 Pacific, outside 8-22, even though 6 is inside
598
- // a naive UTC 8-22 window.
599
- const outside = createService({
600
- now: () => new Date("2026-09-09T06:00:00Z"),
601
- getCurrentHour: () => 6,
602
- });
603
- expect(await outside.runOnce()).toBe(false);
604
- expect(processMessageCalls).toHaveLength(0);
597
+ // 06:00 UTC is 23:00 Pacific, outside 8-22, even though 6 is inside
598
+ // a naive UTC 8-22 window.
599
+ const outside = createService({
600
+ now: () => new Date("2026-09-09T06:00:00Z"),
601
+ getCurrentHour: () => 6,
605
602
  });
603
+ expect(await outside.runOnce()).toBe(false);
604
+ expect(processMessageCalls).toHaveLength(0);
605
+ });
606
606
 
607
607
  test("overlap prevention works", async () => {
608
608
  let resolveFirst: () => void;
@@ -1004,7 +1004,23 @@ describe("HeartbeatService", () => {
1004
1004
  "slack",
1005
1005
  ]);
1006
1006
 
1007
- expect(prompt).toContain("google, slack");
1007
+ expect(prompt).toContain(
1008
+ "google (integration, acts as the connected person), slack (integration, acts as the connected person)",
1009
+ );
1010
+ });
1011
+
1012
+ test("prompt tells an integration apart from the channel bot of the same name", () => {
1013
+ const service = createService();
1014
+ const { prompt } = service.buildPrompt("- Check things", ["slack"]);
1015
+
1016
+ expect(prompt).toContain(
1017
+ "slack (integration, acts as the connected person)",
1018
+ );
1019
+ expect(prompt).not.toContain("slack_channel");
1020
+ expect(prompt).toContain("A channel bot is the assistant's own identity");
1021
+
1022
+ const bot = service.buildPrompt("- Check things", ["slack_channel"]);
1023
+ expect(bot.prompt).toContain("slack_channel (the slack channel bot)");
1008
1024
  });
1009
1025
  });
1010
1026