@vellumai/assistant 0.11.5 → 0.11.6-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.
Files changed (233) hide show
  1. package/AGENTS.md +5 -1
  2. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  3. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  4. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  5. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
  7. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
  9. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
  10. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  11. package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  12. package/openapi.yaml +421 -15
  13. package/package.json +1 -1
  14. package/scripts/sync-web-search-catalog.ts +6 -0
  15. package/src/__tests__/app-pin-store.test.ts +149 -0
  16. package/src/__tests__/channel-availability-routes.test.ts +23 -1
  17. package/src/__tests__/channel-readiness-discord.test.ts +231 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +126 -0
  19. package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
  20. package/src/__tests__/channel-reply-delivery.test.ts +4 -4
  21. package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
  22. package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
  23. package/src/__tests__/conversation-error.test.ts +17 -0
  24. package/src/__tests__/conversation-seed-composer.test.ts +8 -0
  25. package/src/__tests__/conversation-slash-commands.test.ts +8 -0
  26. package/src/__tests__/disk-pressure-policy.test.ts +6 -0
  27. package/src/__tests__/gemini-provider.test.ts +138 -0
  28. package/src/__tests__/history-repair.test.ts +105 -3
  29. package/src/__tests__/identity-routes.test.ts +1 -0
  30. package/src/__tests__/llm-catalog-parity.test.ts +45 -0
  31. package/src/__tests__/migration-import-from-path.test.ts +349 -0
  32. package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
  33. package/src/__tests__/oauth-commands-routes.test.ts +89 -0
  34. package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
  35. package/src/__tests__/openai-provider.test.ts +18 -0
  36. package/src/__tests__/openai-responses-provider.test.ts +18 -0
  37. package/src/__tests__/platform-callback-registration.test.ts +184 -0
  38. package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
  39. package/src/__tests__/pricing.test.ts +2 -2
  40. package/src/__tests__/public-ingress-urls.test.ts +36 -0
  41. package/src/__tests__/resolve-trust-class.test.ts +0 -48
  42. package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
  43. package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
  44. package/src/__tests__/settings-routes.test.ts +85 -3
  45. package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
  46. package/src/agent/history-repair/history-repair.ts +45 -14
  47. package/src/agent/loop.ts +4 -1
  48. package/src/api/constants/profile-config-validation.ts +60 -0
  49. package/src/api/events/tool-result.ts +6 -1
  50. package/src/api/events/watch-retro-completed.ts +52 -0
  51. package/src/api/index.ts +11 -0
  52. package/src/apps/app-pin-reconciler.ts +92 -0
  53. package/src/apps/app-pin-store.ts +125 -0
  54. package/src/channels/gateway-channel-socket-health.ts +32 -0
  55. package/src/channels/gateway-discord-admission.ts +32 -0
  56. package/src/channels/types.ts +20 -0
  57. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
  58. package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
  59. package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
  60. package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
  61. package/src/cli/commands/channels/index.ts +45 -31
  62. package/src/cli/commands/inference-profiles.ts +56 -3
  63. package/src/cli/commands/inference-providers.ts +28 -2
  64. package/src/cli/commands/oauth/index.help.ts +7 -1
  65. package/src/cli/commands/oauth/request.test.ts +290 -0
  66. package/src/cli/commands/oauth/request.ts +57 -41
  67. package/src/cli/lib/bundled-marketplace.json +14 -1
  68. package/src/cli/lib/open-browser.test.ts +67 -0
  69. package/src/cli/lib/open-browser.ts +24 -5
  70. package/src/config/__tests__/profile-materialization.test.ts +26 -0
  71. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
  72. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  73. package/src/config/feature-flag-registry.json +17 -1
  74. package/src/config/profile-materialization.ts +29 -0
  75. package/src/config/sanitize-for-transfer.ts +16 -0
  76. package/src/config/schemas/llm.ts +7 -0
  77. package/src/config/schemas/services.ts +6 -0
  78. package/src/context/outbound-sanitize.ts +6 -0
  79. package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
  80. package/src/daemon/conversation-error.ts +24 -2
  81. package/src/daemon/conversation-slash.ts +6 -15
  82. package/src/daemon/daemon-control.ts +1 -0
  83. package/src/daemon/disk-pressure-policy.ts +7 -1
  84. package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
  85. package/src/daemon/handlers/config-ingress.ts +115 -5
  86. package/src/daemon/lifecycle.ts +26 -0
  87. package/src/daemon/message-types/web-activity.ts +3 -2
  88. package/src/daemon/trust-context.ts +0 -37
  89. package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
  90. package/src/inbound/platform-callback-registration.ts +28 -2
  91. package/src/inbound/public-ingress-urls.ts +12 -0
  92. package/src/inbound/tunnel-probe.ts +261 -0
  93. package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
  94. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
  95. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
  96. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
  97. package/src/live-voice/live-voice-connection.ts +46 -8
  98. package/src/live-voice/live-voice-manager.ts +25 -0
  99. package/src/live-voice/live-voice-session-manager.ts +318 -2
  100. package/src/live-voice/live-voice-session.ts +52 -2
  101. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
  102. package/src/messaging/providers/channel-transport.ts +64 -47
  103. package/src/messaging/providers/discord/send.test.ts +46 -1
  104. package/src/messaging/providers/discord/send.ts +51 -0
  105. package/src/messaging/providers/discord/transport.ts +26 -3
  106. package/src/messaging/providers/index.ts +22 -47
  107. package/src/messaging/providers/slack/send.test.ts +83 -26
  108. package/src/messaging/providers/slack/send.ts +120 -51
  109. package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
  110. package/src/messaging/providers/slack/stream-tasks.ts +39 -0
  111. package/src/messaging/providers/slack/transport.ts +24 -22
  112. package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
  113. package/src/messaging/providers/telegram-bot/send.ts +43 -0
  114. package/src/messaging/providers/telegram-bot/transport.ts +25 -8
  115. package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
  116. package/src/notifications/adapters/telegram.ts +48 -1
  117. package/src/notifications/assistant-reply-producer.ts +7 -7
  118. package/src/notifications/conversation-seed-composer.ts +7 -2
  119. package/src/oauth/byo-connection.test.ts +63 -0
  120. package/src/oauth/byo-connection.ts +16 -15
  121. package/src/oauth/connection.test.ts +111 -0
  122. package/src/oauth/connection.ts +142 -1
  123. package/src/oauth/platform-connection.test.ts +34 -0
  124. package/src/oauth/platform-connection.ts +28 -5
  125. package/src/oauth/seed-providers.ts +15 -1
  126. package/src/permissions/types.ts +3 -1
  127. package/src/persistence/conversation-crud.ts +46 -0
  128. package/src/persistence/conversation-types.ts +11 -9
  129. package/src/persistence/db-async-query.ts +2 -1
  130. package/src/persistence/db-maintenance.ts +15 -0
  131. package/src/persistence/embeddings/qdrant-manager.ts +1 -0
  132. package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
  133. package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
  134. package/src/persistence/migrations/369-create-app-pins.ts +37 -0
  135. package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
  136. package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
  137. package/src/persistence/schema/index.ts +1 -0
  138. package/src/persistence/schema/infrastructure.ts +17 -0
  139. package/src/persistence/schema/watch.ts +29 -0
  140. package/src/persistence/steps.ts +6 -0
  141. package/src/plugins/mtime-cache.ts +11 -0
  142. package/src/providers/__tests__/retry-network-error.test.ts +84 -0
  143. package/src/providers/connection-resolution.ts +23 -1
  144. package/src/providers/content-blocks.ts +9 -0
  145. package/src/providers/fetch-provider-catalog.ts +19 -0
  146. package/src/providers/gemini/client.ts +13 -5
  147. package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
  148. package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
  149. package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
  150. package/src/providers/inference/adapter-factory.ts +0 -9
  151. package/src/providers/inference/credential-rotation.ts +61 -0
  152. package/src/providers/inference/endpoint-probe.ts +115 -0
  153. package/src/providers/inference/profile-probe.ts +256 -0
  154. package/src/providers/model-catalog.ts +170 -125
  155. package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
  156. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
  157. package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
  158. package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
  159. package/src/providers/openai/api-error-normalization.ts +16 -2
  160. package/src/providers/openai/chat-completions-provider.ts +75 -29
  161. package/src/providers/openai/responses-provider.ts +5 -2
  162. package/src/providers/openrouter/client.ts +0 -1
  163. package/src/providers/provider-send-message.ts +11 -0
  164. package/src/providers/retry.ts +6 -0
  165. package/src/providers/search-provider-catalog.ts +20 -0
  166. package/src/providers/vercel-ai-gateway/client.ts +0 -1
  167. package/src/runtime/AGENTS.md +1 -0
  168. package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
  169. package/src/runtime/__tests__/host-observe.test.ts +302 -0
  170. package/src/runtime/channel-readiness-service.ts +214 -14
  171. package/src/runtime/channel-readiness-types.ts +49 -2
  172. package/src/runtime/channel-reply-delivery.ts +2 -2
  173. package/src/runtime/desktop-presence.ts +24 -21
  174. package/src/runtime/host-observe.ts +246 -0
  175. package/src/runtime/http-server.ts +181 -1
  176. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
  177. package/src/runtime/migrations/staged-import-path.ts +116 -0
  178. package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
  179. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
  180. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
  181. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
  182. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
  183. package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
  184. package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
  185. package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
  186. package/src/runtime/routes/app-management-routes.ts +140 -29
  187. package/src/runtime/routes/channel-availability-routes.ts +1 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +14 -2
  189. package/src/runtime/routes/conversation-query-routes.ts +10 -0
  190. package/src/runtime/routes/guardian-approval-interception.ts +24 -33
  191. package/src/runtime/routes/host-cu-routes.ts +18 -0
  192. package/src/runtime/routes/identity-routes.ts +2 -0
  193. package/src/runtime/routes/inbound-message-handler.ts +10 -7
  194. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
  195. package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
  196. package/src/runtime/routes/index.ts +2 -0
  197. package/src/runtime/routes/inference-profiles-routes.ts +232 -31
  198. package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
  199. package/src/runtime/routes/ingress-status-routes.ts +180 -0
  200. package/src/runtime/routes/live-voice-routes.test.ts +40 -1
  201. package/src/runtime/routes/live-voice-routes.ts +34 -0
  202. package/src/runtime/routes/migration-routes.ts +218 -10
  203. package/src/runtime/routes/oauth-commands-routes.ts +23 -16
  204. package/src/runtime/routes/plugins-routes.ts +12 -28
  205. package/src/runtime/routes/question-routes.ts +6 -0
  206. package/src/runtime/routes/secret-routes.ts +7 -27
  207. package/src/runtime/routes/settings-routes.ts +9 -6
  208. package/src/runtime/routes/watch-routes.ts +807 -0
  209. package/src/runtime/slack-reply-session.test.ts +230 -121
  210. package/src/runtime/slack-reply-session.ts +113 -81
  211. package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
  212. package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
  213. package/src/security/__tests__/untrusted-content.test.ts +42 -0
  214. package/src/security/untrusted-content.ts +28 -9
  215. package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
  216. package/src/telemetry/live-voice-funnel.ts +75 -8
  217. package/src/tools/credentials/store.ts +18 -6
  218. package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
  219. package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
  220. package/src/tools/network/__tests__/web-search.test.ts +97 -2
  221. package/src/tools/network/firecrawl-compat.ts +90 -0
  222. package/src/tools/network/web-fetch.ts +142 -62
  223. package/src/tools/network/web-search.ts +141 -55
  224. package/src/tools/types.ts +2 -1
  225. package/src/util/oauth-request-body.test.ts +74 -0
  226. package/src/util/oauth-request-body.ts +60 -0
  227. package/src/util/worker-process.ts +1 -0
  228. package/src/watch/__tests__/watch-retro.test.ts +665 -0
  229. package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
  230. package/src/watch/__tests__/watch-timeline.test.ts +670 -0
  231. package/src/watch/watch-retro.ts +480 -0
  232. package/src/watch/watch-session-manager.ts +575 -0
  233. package/src/watch/watch-timeline.ts +848 -0
@@ -19,6 +19,12 @@ import { join } from "node:path";
19
19
 
20
20
  import { z } from "zod";
21
21
 
22
+ import {
23
+ type AppPin,
24
+ listAppPins,
25
+ removeAppPin,
26
+ updateAppPin,
27
+ } from "../../apps/app-pin-store.js";
22
28
  import {
23
29
  type AppDefinition,
24
30
  type AppOrigin,
@@ -58,6 +64,7 @@ import {
58
64
  NotFoundError,
59
65
  PayloadTooLargeError,
60
66
  } from "./errors.js";
67
+ import { parseBody } from "./parse-body.js";
61
68
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
62
69
 
63
70
  const log = getLogger("app-management-routes");
@@ -80,17 +87,66 @@ function getSharedAppsDir(): string {
80
87
  // Extracted business logic
81
88
  // ---------------------------------------------------------------------------
82
89
 
83
- interface AppListItem {
84
- id: string;
85
- name: string;
86
- description?: string;
87
- icon?: string;
88
- createdAt: number;
89
- updatedAt: number;
90
- version: string;
91
- contentId: string;
92
- /** "workspace" or "plugin:<name>" — identifies where the app comes from. */
93
- origin: string;
90
+ /**
91
+ * One entry in the app list. Declared as the schema the route publishes, so the
92
+ * wire contract and the type the handlers build against cannot drift apart.
93
+ */
94
+ const appListItemSchema = z.object({
95
+ id: z.string(),
96
+ name: z.string(),
97
+ description: z.string().optional(),
98
+ icon: z.string().optional(),
99
+ createdAt: z.number(),
100
+ updatedAt: z.number(),
101
+ version: z.string(),
102
+ contentId: z.string(),
103
+ origin: z
104
+ .string()
105
+ .describe('"workspace" or "plugin:<name>": where the app comes from'),
106
+ pinSortPosition: z
107
+ .number()
108
+ .optional()
109
+ .describe("Sidebar order, ascending. Absent when the app is not pinned"),
110
+ pinColor: z
111
+ .string()
112
+ .optional()
113
+ .describe("Pin colour id; absent when no colour is set"),
114
+ });
115
+
116
+ type AppListItem = z.infer<typeof appListItemSchema>;
117
+
118
+ /**
119
+ * The pin route's body. `RouteDefinition.requestBody` is a codegen signal and
120
+ * does not validate, so the handler parses against this too; declaring it once
121
+ * keeps what is advertised and what is enforced from drifting apart.
122
+ */
123
+ const appPinBodySchema = z
124
+ .object({
125
+ pinned: z
126
+ .boolean()
127
+ .optional()
128
+ .describe("Pin (true) or unpin (false). Omit to leave unchanged."),
129
+ color: z
130
+ .string()
131
+ .nullable()
132
+ .optional()
133
+ .describe("Colour id, or null to clear. Omit to leave unchanged."),
134
+ })
135
+ .refine(
136
+ (value) => value.pinned !== undefined || value.color !== undefined,
137
+ "pinned or color is required",
138
+ );
139
+
140
+ /** Merge an app's pin, if it has one, onto its list entry. */
141
+ function withPin(item: AppListItem, pin: AppPin | undefined): AppListItem {
142
+ if (!pin) {
143
+ return item;
144
+ }
145
+ return {
146
+ ...item,
147
+ pinSortPosition: pin.sortPosition,
148
+ ...(pin.color !== undefined ? { pinColor: pin.color } : {}),
149
+ };
94
150
  }
95
151
 
96
152
  function workspaceAppItem(a: AppDefinition): AppListItem {
@@ -578,17 +634,23 @@ async function importBundle(
578
634
  // ---------------------------------------------------------------------------
579
635
 
580
636
  function handleListApps({ queryParams }: RouteHandlerArgs) {
637
+ const pinsById = new Map(listAppPins().map((pin) => [pin.appId, pin]));
638
+ const pinned = (item: AppListItem): AppListItem =>
639
+ withPin(item, pinsById.get(item.id));
640
+
581
641
  const conversationId = queryParams?.conversationId;
582
642
  if (conversationId) {
583
643
  // Conversation scoping is a workspace-app concept; plugin apps are not
584
644
  // associated with conversations, so they are omitted from this view.
585
645
  return {
586
- apps: listAppsByConversation(conversationId).map(workspaceAppItem),
646
+ apps: listAppsByConversation(conversationId).map((app) =>
647
+ pinned(workspaceAppItem(app)),
648
+ ),
587
649
  };
588
650
  }
589
651
  const apps: AppListItem[] = [
590
- ...listApps().map(workspaceAppItem),
591
- ...listPluginApps().map(pluginAppItem),
652
+ ...listApps().map((app) => pinned(workspaceAppItem(app))),
653
+ ...listPluginApps().map((app) => pinned(pluginAppItem(app))),
592
654
  ];
593
655
  return { apps };
594
656
  }
@@ -818,10 +880,50 @@ function handleDeleteApp({ pathParams, headers }: RouteHandlerArgs) {
818
880
  const appId = pathParams?.id as string;
819
881
  assertNotPluginApp(appId, "delete a plugin app");
820
882
  deleteApp(appId);
883
+ /* `deleteApp` cannot do this itself: it is filesystem-level and runs where no
884
+ migrated database exists. Deletions that skip this, and plugin apps that
885
+ leave with their plugin, are converged by `apps/app-pin-reconciler.ts`. */
886
+ removeAppPin(appId);
821
887
  publishAppsChanged(getOriginClientId(headers));
822
888
  return { success: true };
823
889
  }
824
890
 
891
+ /**
892
+ * Pin, unpin, or recolour an app. Plugin apps are pinnable: a pin says nothing
893
+ * about the app's content, so `assertNotPluginApp` does not apply here.
894
+ */
895
+ /** Every app id this workspace has, plugin apps included. */
896
+ function existingAppIds(): string[] {
897
+ return [
898
+ ...listApps().map((app) => app.id),
899
+ ...listPluginApps().map((app) => app.id),
900
+ ];
901
+ }
902
+
903
+ function handlePinApp({ pathParams, body = {}, headers }: RouteHandlerArgs) {
904
+ const appId = pathParams?.id as string;
905
+ const { pinned, color } = parseBody(appPinBodySchema, body);
906
+ const existing = existingAppIds();
907
+ /* A pin only means anything against an app that exists: the list starts from
908
+ the real apps, so a pin for anything else could never be read back.
909
+ Unpinning is exempt, since that is how a client clears state left over from
910
+ an app that has since gone. */
911
+ if (pinned === true && !existing.includes(appId)) {
912
+ throw new NotFoundError(`App not found: ${appId}`);
913
+ }
914
+ const pin = updateAppPin(appId, {
915
+ ...(pinned === undefined ? {} : { pinned }),
916
+ ...(color === undefined ? {} : { color }),
917
+ });
918
+ publishAppsChanged(getOriginClientId(headers));
919
+ return {
920
+ success: true,
921
+ appId,
922
+ pinSortPosition: pin?.sortPosition ?? null,
923
+ pinColor: pin?.color ?? null,
924
+ };
925
+ }
926
+
825
927
  function handleGetPreview({ pathParams }: RouteHandlerArgs) {
826
928
  const appId = pathParams?.id as string;
827
929
  const preview = getAppPreview(appId);
@@ -887,21 +989,7 @@ export const ROUTES: RouteDefinition[] = [
887
989
  description: "Filter apps by conversation ID",
888
990
  },
889
991
  ],
890
- responseBody: z.object({
891
- apps: z.array(
892
- z.object({
893
- id: z.string(),
894
- name: z.string(),
895
- description: z.string().optional(),
896
- icon: z.string().optional(),
897
- createdAt: z.number(),
898
- updatedAt: z.number(),
899
- version: z.string(),
900
- contentId: z.string(),
901
- origin: z.string(),
902
- }),
903
- ),
904
- }),
992
+ responseBody: z.object({ apps: z.array(appListItemSchema) }),
905
993
  },
906
994
  {
907
995
  operationId: "apps_open_bundle",
@@ -1146,6 +1234,29 @@ export const ROUTES: RouteDefinition[] = [
1146
1234
  tags: ["apps"],
1147
1235
  responseBody: z.object({ success: z.boolean() }),
1148
1236
  },
1237
+ {
1238
+ operationId: "apps_pin",
1239
+ endpoint: "apps/:id/pin",
1240
+ method: "POST",
1241
+ policy: {
1242
+ requiredScopes: ["settings.write"],
1243
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
1244
+ },
1245
+ handler: handlePinApp,
1246
+ summary: "Pin or unpin an app",
1247
+ description:
1248
+ "Pin an app to the sidebar, unpin it, or set the colour it is tinted " +
1249
+ "with. Pin state is read back from the app list as `pinSortPosition` " +
1250
+ "and `pinColor`.",
1251
+ tags: ["apps"],
1252
+ requestBody: appPinBodySchema,
1253
+ responseBody: z.object({
1254
+ success: z.boolean(),
1255
+ appId: z.string(),
1256
+ pinSortPosition: z.number().nullable(),
1257
+ pinColor: z.string().nullable(),
1258
+ }),
1259
+ },
1149
1260
  {
1150
1261
  operationId: "apps_preview_get",
1151
1262
  endpoint: "apps/:id/preview",
@@ -35,6 +35,7 @@ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
35
35
  const BASE_AVAILABLE_CHANNELS: readonly ChannelId[] = [
36
36
  "slack",
37
37
  "telegram",
38
+ "discord",
38
39
  "phone",
39
40
  ] as const;
40
41
 
@@ -14,6 +14,11 @@ import {
14
14
  getInviteAdapterRegistry,
15
15
  resolveAdapterHandle,
16
16
  } from "../channel-invite-transport.js";
17
+ import {
18
+ CHANNEL_HEALTHS,
19
+ CHECK_KINDS,
20
+ SETUP_STATUSES,
21
+ } from "../channel-readiness-types.js";
17
22
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
18
23
 
19
24
  async function enrichSnapshots(
@@ -32,6 +37,7 @@ async function enrichSnapshots(
32
37
  channel: s.channel,
33
38
  ready: s.ready,
34
39
  setupStatus: s.setupStatus,
40
+ health: s.health,
35
41
  checkedAt: s.checkedAt,
36
42
  stale: s.stale,
37
43
  reasons: s.reasons,
@@ -89,15 +95,21 @@ const readinessCheckSchema = z.object({
89
95
  name: z.string(),
90
96
  passed: z.boolean(),
91
97
  message: z.string().nullable().optional(),
98
+ // `passed` alone cannot say "I could not tell", and dropping this at the
99
+ // boundary is why a client cannot distinguish a channel that is broken from
100
+ // one whose state could not be established.
101
+ indeterminate: z.boolean().optional(),
102
+ kind: z.enum(CHECK_KINDS).optional(),
92
103
  });
93
104
 
94
105
  const readinessSnapshotSchema = z.object({
95
106
  channel: z.enum(CHANNEL_IDS),
96
107
  ready: z.boolean(),
97
- setupStatus: z.string().nullable().optional(),
108
+ setupStatus: z.enum(SETUP_STATUSES).nullable().optional(),
109
+ health: z.enum(CHANNEL_HEALTHS).optional(),
98
110
  checkedAt: z.number().nullable().optional(),
99
111
  stale: z.boolean().optional(),
100
- reasons: z.array(z.string()).optional(),
112
+ reasons: z.array(z.object({ code: z.string(), text: z.string() })).optional(),
101
113
  localChecks: z.array(readinessCheckSchema).optional(),
102
114
  remoteChecks: z.array(readinessCheckSchema).optional(),
103
115
  channelHandle: z.string().nullable().optional(),
@@ -66,6 +66,7 @@ import {
66
66
  getEmbeddingConfigInfo,
67
67
  setEmbeddingConfig,
68
68
  } from "../../daemon/handlers/config-embeddings.js";
69
+ import { dropTunnelRecordsForNewUrl } from "../../daemon/handlers/config-ingress.js";
69
70
  import {
70
71
  getModelInfo,
71
72
  type ModelSetContext,
@@ -1525,6 +1526,10 @@ async function handlePatchConfig({ body }: RouteHandlerArgs) {
1525
1526
 
1526
1527
  const raw = loadRawConfig();
1527
1528
  const patch = body as Record<string, unknown>;
1529
+ const patchedIngressUrl = readPlainObject(patch.ingress)?.publicBaseUrl;
1530
+ if (patchedIngressUrl !== undefined) {
1531
+ dropTunnelRecordsForNewUrl(readPlainObject(raw.ingress), patchedIngressUrl);
1532
+ }
1528
1533
  deepMergeOverwrite(raw, patch);
1529
1534
  scrubRemovedServiceModes(raw);
1530
1535
  seedSttProviderForSparseBlock(raw);
@@ -1615,6 +1620,11 @@ async function handleSetConfig({ body }: RouteHandlerArgs) {
1615
1620
  readPlainObject(readPlainObject(raw.llm)?.profiles)?.[managedEntryName],
1616
1621
  )
1617
1622
  : undefined;
1623
+ // `assistant config set ingress.publicBaseUrl <url>`, what the webhook
1624
+ // diagnostics tell users to run, retargets ingress like the settings route.
1625
+ if (path === "ingress.publicBaseUrl") {
1626
+ dropTunnelRecordsForNewUrl(readPlainObject(raw.ingress), value);
1627
+ }
1618
1628
  setNestedValue(raw, path, value);
1619
1629
  if (
1620
1630
  managedEntryName &&
@@ -5,8 +5,6 @@
5
5
  * This module is the top-level dispatcher. It delegates plain-text messages to
6
6
  * the conversational engine in guardian-text-engine-strategy.ts.
7
7
  */
8
- import type { KnownBlock } from "@slack/types";
9
-
10
8
  import { audienceForReader } from "../../channels/message-audience.js";
11
9
  import type { ChannelId } from "../../channels/types.js";
12
10
  import type { TrustContext } from "../../daemon/trust-context-types.js";
@@ -48,7 +46,7 @@ export interface ApprovalInterceptionParams {
48
46
  approvalCopyGenerator?: ApprovalCopyGenerator;
49
47
  approvalConversationGenerator?: ApprovalConversationGenerator;
50
48
  /** Original approval message timestamp (Slack ts) for editing after resolution. */
51
- approvalMessageTs?: string;
49
+ approvalMessageId?: string;
52
50
  }
53
51
 
54
52
  import type { ApprovalInterceptionResult } from "./approval-interception-types.js";
@@ -79,7 +77,7 @@ export async function handleApprovalInterception(
79
77
  assistantId,
80
78
  approvalCopyGenerator,
81
79
  approvalConversationGenerator,
82
- approvalMessageTs,
80
+ approvalMessageId,
83
81
  } = params;
84
82
 
85
83
  // Slack emoji reactions are handled by the guardian decision
@@ -245,11 +243,11 @@ export async function handleApprovalInterception(
245
243
  );
246
244
 
247
245
  // Edit the original Slack approval message to remove stale buttons
248
- if (sourceChannel === "slack" && approvalMessageTs) {
249
- editStaleSlackApprovalMessage({
246
+ if (approvalMessageId) {
247
+ editStaleApprovalMessage({
250
248
  replyCallbackUrl,
251
249
  chatId: conversationExternalId,
252
- messageTs: approvalMessageTs,
250
+ messageId: approvalMessageId,
253
251
  assistantId,
254
252
  conversationId,
255
253
  });
@@ -264,7 +262,7 @@ export async function handleApprovalInterception(
264
262
  if (result.applied) {
265
263
  // Edit the original Slack approval message to show the decision
266
264
  // and remove stale action buttons.
267
- if (sourceChannel === "slack" && approvalMessageTs) {
265
+ if (approvalMessageId) {
268
266
  const decisionOutcome: "approved" | "denied" =
269
267
  cbDecision.action === "reject" ? "denied" : "approved";
270
268
  const statusEmoji =
@@ -273,11 +271,11 @@ export async function handleApprovalInterception(
273
271
  decisionOutcome === "approved" ? "Approved" : "Denied";
274
272
  editChannelMessage(replyCallbackUrl, {
275
273
  chatId: conversationExternalId,
276
- messageId: approvalMessageTs,
274
+ messageId: approvalMessageId,
277
275
  text: `${statusEmoji} ${statusLabel}`,
278
276
  }).catch((err) => {
279
277
  log.error(
280
- { err, conversationId, messageTs: approvalMessageTs },
278
+ { err, conversationId, messageTs: approvalMessageId },
281
279
  "Failed to edit Slack approval message after decision",
282
280
  );
283
281
  });
@@ -291,11 +289,11 @@ export async function handleApprovalInterception(
291
289
  // Race condition: request was already resolved between the stale check
292
290
  // above and the decision attempt.
293
291
  // Edit the original Slack approval message to remove stale buttons
294
- if (sourceChannel === "slack" && approvalMessageTs) {
295
- editStaleSlackApprovalMessage({
292
+ if (approvalMessageId) {
293
+ editStaleApprovalMessage({
296
294
  replyCallbackUrl,
297
295
  chatId: conversationExternalId,
298
- messageTs: approvalMessageTs,
296
+ messageId: approvalMessageId,
299
297
  assistantId,
300
298
  conversationId,
301
299
  });
@@ -374,39 +372,32 @@ export async function handleApprovalInterception(
374
372
  // ---------------------------------------------------------------------------
375
373
 
376
374
  /**
377
- * Fire-and-forget: edit a stale Slack approval message to indicate it has
378
- * been resolved and remove the action buttons. Used when a button click
379
- * arrives for an already-resolved approval.
375
+ * Fire-and-forget: mark a stale approval message as resolved and drop its
376
+ * action buttons. Used when a button click arrives for an approval that was
377
+ * already decided.
378
+ *
379
+ * Says the message is settled and leaves each channel to render that. Slack
380
+ * reads it as a context block, and a channel with no equivalent shows the
381
+ * text plainly.
380
382
  */
381
- function editStaleSlackApprovalMessage(params: {
383
+ function editStaleApprovalMessage(params: {
382
384
  replyCallbackUrl: string;
383
385
  chatId: string;
384
- messageTs: string;
386
+ messageId: string;
385
387
  assistantId: string;
386
388
  conversationId: string;
387
389
  }): void {
388
- const statusText = "This approval request has been resolved.";
389
- const blocks: KnownBlock[] = [
390
- {
391
- type: "section",
392
- text: { type: "mrkdwn", text: statusText },
393
- },
394
- {
395
- type: "context",
396
- elements: [{ type: "mrkdwn", text: statusText }],
397
- },
398
- ];
399
390
  editChannelMessage(params.replyCallbackUrl, {
400
391
  chatId: params.chatId,
401
- messageId: params.messageTs,
402
- text: statusText,
403
- blocks,
392
+ messageId: params.messageId,
393
+ text: "This approval request has been resolved.",
394
+ emphasis: "muted",
404
395
  }).catch((err) => {
405
396
  log.error(
406
397
  {
407
398
  err,
408
399
  conversationId: params.conversationId,
409
- messageTs: params.messageTs,
400
+ messageId: params.messageId,
410
401
  },
411
402
  "Failed to edit stale Slack approval message",
412
403
  );
@@ -112,6 +112,24 @@ async function handleHostCuResult({ body, headers }: RouteHandlerArgs) {
112
112
  });
113
113
  }
114
114
 
115
+ // Conversation-agnostic observation (see `runtime/host-observe.ts`): the
116
+ // request was raised outside any turn, so there is no conversation and no CU
117
+ // proxy to format the observation. Hand the raw fields to the waiting caller.
118
+ if (peeked.conversationId === undefined) {
119
+ const interaction = pendingInteractions.resolve(requestId, "answered");
120
+ interaction?.rpcResolve?.({
121
+ axTree,
122
+ axDiff,
123
+ screenshot,
124
+ screenshotWidthPx,
125
+ screenshotHeightPx,
126
+ screenWidthPt,
127
+ screenHeightPt,
128
+ executionError,
129
+ });
130
+ return { accepted: true };
131
+ }
132
+
115
133
  const conversation = findConversation(peeked.conversationId);
116
134
  if (!conversation) {
117
135
  pendingInteractions.resolve(requestId, "cancelled");
@@ -158,6 +158,7 @@ async function getDetailedHealth() {
158
158
  capabilities: {
159
159
  memoryOptOut: true,
160
160
  retryLastTurn: true,
161
+ appPins: true,
161
162
  },
162
163
  ...(profiler ? { profiler } : {}),
163
164
  ...migrationHealthFields,
@@ -282,6 +283,7 @@ const cesHealthSchema = z.object({
282
283
  const healthCapabilitiesSchema = z.object({
283
284
  memoryOptOut: z.boolean(),
284
285
  retryLastTurn: z.boolean(),
286
+ appPins: z.boolean(),
285
287
  });
286
288
 
287
289
  const healthDiskSchema = z.object({
@@ -39,7 +39,6 @@ import {
39
39
  import { getDiskPressureStatus } from "../../daemon/disk-pressure-guard.js";
40
40
  import { classifyDiskPressureTurnPolicy } from "../../daemon/disk-pressure-policy.js";
41
41
  import { processMessage } from "../../daemon/process-message.js";
42
- import { mapChatTypeToConversationType } from "../../daemon/trust-context.js";
43
42
  import type { TrustContext } from "../../daemon/trust-context-types.js";
44
43
  import { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
45
44
  import type { Message as ProviderMessage } from "../../messaging/provider-types.js";
@@ -1030,7 +1029,11 @@ export async function handleChannelInbound({
1030
1029
  sourceMetadata.chatType.trim().length > 0
1031
1030
  ? sourceMetadata.chatType.trim()
1032
1031
  : undefined;
1033
- trustCtx.conversationType = mapChatTypeToConversationType(sourceChatType);
1032
+ // Decided by the sending channel, which is the only side that knows what its
1033
+ // own surfaces mean. The daemon reads the answer rather than re-deriving it
1034
+ // from a vocabulary where `channel` means a public room on Slack and a
1035
+ // broadcast feed on Telegram.
1036
+ trustCtx.conversationType = sourceMetadata?.conversationType;
1034
1037
 
1035
1038
  // Preserve locale from sourceMetadata so the model can greet in the user's language
1036
1039
  const sourceLanguageCode =
@@ -1092,8 +1095,8 @@ export async function handleChannelInbound({
1092
1095
  // cleanup. When a Slack block_actions payload is forwarded, the gateway
1093
1096
  // sets sourceMetadata.messageId to the ts of the message containing
1094
1097
  // the button. This lets us edit the message after resolution.
1095
- const approvalMessageTs =
1096
- sourceChannel === "slack" && typeof sourceMetadata?.messageId === "string"
1098
+ const approvalMessageId =
1099
+ typeof sourceMetadata?.messageId === "string"
1097
1100
  ? sourceMetadata.messageId
1098
1101
  : undefined;
1099
1102
 
@@ -1109,7 +1112,7 @@ export async function handleChannelInbound({
1109
1112
  assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
1110
1113
  approvalCopyGenerator,
1111
1114
  approvalConversationGenerator,
1112
- approvalMessageTs,
1115
+ approvalMessageId,
1113
1116
  });
1114
1117
 
1115
1118
  if (approvalResult.handled) {
@@ -1199,10 +1202,10 @@ export async function handleChannelInbound({
1199
1202
  // instead of a silent no-op (JARVIS-299).
1200
1203
  // No channel check: a transport without `edit` declines the call, so a
1201
1204
  // channel gains this the moment it can revise a sent message.
1202
- if (replyCallbackUrl && approvalMessageTs) {
1205
+ if (replyCallbackUrl && approvalMessageId) {
1203
1206
  editChannelMessage(replyCallbackUrl, {
1204
1207
  chatId: conversationExternalId,
1205
- messageId: approvalMessageTs,
1208
+ messageId: approvalMessageId,
1206
1209
  text: "This approval request has been resolved.",
1207
1210
  }).catch((err) => {
1208
1211
  log.error(