@vellumai/assistant 0.4.2 → 0.4.4

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 (221) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +124 -10
  3. package/README.md +43 -35
  4. package/docs/trusted-contact-access.md +20 -0
  5. package/package.json +1 -1
  6. package/scripts/ipc/generate-swift.ts +1 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  8. package/src/__tests__/access-request-decision.test.ts +0 -1
  9. package/src/__tests__/actor-token-service.test.ts +1099 -0
  10. package/src/__tests__/agent-loop.test.ts +51 -0
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
  14. package/src/__tests__/call-controller.test.ts +49 -0
  15. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  16. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  17. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  18. package/src/__tests__/call-routes-http.test.ts +0 -25
  19. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  21. package/src/__tests__/channel-guardian.test.ts +0 -86
  22. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  23. package/src/__tests__/checker.test.ts +33 -12
  24. package/src/__tests__/config-schema.test.ts +6 -0
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  27. package/src/__tests__/conversation-routes.test.ts +12 -3
  28. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  29. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  30. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  31. package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
  32. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  33. package/src/__tests__/guardian-outbound-http.test.ts +4 -5
  34. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  35. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  36. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  37. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  38. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  39. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  40. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  42. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  43. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  44. package/src/__tests__/non-member-access-request.test.ts +159 -9
  45. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  46. package/src/__tests__/notification-decision-strategy.test.ts +106 -2
  47. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  48. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  49. package/src/__tests__/relay-server.test.ts +1475 -33
  50. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  51. package/src/__tests__/session-agent-loop.test.ts +1 -0
  52. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  53. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  54. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  55. package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
  56. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  57. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  58. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  59. package/src/__tests__/tool-executor.test.ts +21 -2
  60. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  61. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  62. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  63. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  64. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  65. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  66. package/src/__tests__/twilio-config.test.ts +2 -13
  67. package/src/__tests__/twilio-routes.test.ts +4 -3
  68. package/src/__tests__/update-bulletin.test.ts +0 -1
  69. package/src/agent/loop.ts +1 -1
  70. package/src/approvals/guardian-decision-primitive.ts +12 -3
  71. package/src/approvals/guardian-request-resolvers.ts +169 -11
  72. package/src/calls/call-constants.ts +29 -0
  73. package/src/calls/call-controller.ts +11 -3
  74. package/src/calls/call-domain.ts +33 -11
  75. package/src/calls/call-pointer-message-composer.ts +154 -0
  76. package/src/calls/call-pointer-messages.ts +106 -27
  77. package/src/calls/guardian-dispatch.ts +4 -2
  78. package/src/calls/relay-server.ts +921 -112
  79. package/src/calls/twilio-config.ts +4 -11
  80. package/src/calls/twilio-routes.ts +4 -6
  81. package/src/calls/types.ts +3 -1
  82. package/src/calls/voice-session-bridge.ts +4 -3
  83. package/src/cli/core-commands.ts +7 -4
  84. package/src/cli.ts +5 -4
  85. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  86. package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
  87. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  88. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  89. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  90. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  91. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  92. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  93. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  94. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  96. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  97. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
  98. package/src/config/calls-schema.ts +36 -0
  99. package/src/config/env.ts +22 -0
  100. package/src/config/feature-flag-registry.json +8 -8
  101. package/src/config/schema.ts +2 -2
  102. package/src/config/skills.ts +11 -0
  103. package/src/config/system-prompt.ts +11 -1
  104. package/src/config/templates/SOUL.md +2 -0
  105. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  106. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
  107. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  108. package/src/daemon/call-pointer-generators.ts +59 -0
  109. package/src/daemon/computer-use-session.ts +2 -5
  110. package/src/daemon/handlers/apps.ts +76 -20
  111. package/src/daemon/handlers/config-channels.ts +9 -61
  112. package/src/daemon/handlers/config-inbox.ts +11 -3
  113. package/src/daemon/handlers/config-ingress.ts +28 -3
  114. package/src/daemon/handlers/config-telegram.ts +12 -0
  115. package/src/daemon/handlers/config.ts +2 -6
  116. package/src/daemon/handlers/index.ts +2 -1
  117. package/src/daemon/handlers/pairing.ts +2 -0
  118. package/src/daemon/handlers/publish.ts +11 -46
  119. package/src/daemon/handlers/sessions.ts +59 -5
  120. package/src/daemon/handlers/shared.ts +17 -2
  121. package/src/daemon/ipc-contract/apps.ts +1 -0
  122. package/src/daemon/ipc-contract/inbox.ts +4 -0
  123. package/src/daemon/ipc-contract/integrations.ts +1 -97
  124. package/src/daemon/ipc-contract/messages.ts +47 -1
  125. package/src/daemon/ipc-contract/notifications.ts +11 -0
  126. package/src/daemon/ipc-contract-inventory.json +2 -4
  127. package/src/daemon/lifecycle.ts +17 -0
  128. package/src/daemon/server.ts +16 -2
  129. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  130. package/src/daemon/session-agent-loop.ts +24 -12
  131. package/src/daemon/session-lifecycle.ts +1 -1
  132. package/src/daemon/session-process.ts +11 -1
  133. package/src/daemon/session-runtime-assembly.ts +6 -1
  134. package/src/daemon/session-surfaces.ts +32 -3
  135. package/src/daemon/session.ts +88 -1
  136. package/src/daemon/tool-side-effects.ts +22 -0
  137. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  138. package/src/home-base/prebuilt/index.html +40 -0
  139. package/src/inbound/platform-callback-registration.ts +157 -0
  140. package/src/memory/canonical-guardian-store.ts +1 -1
  141. package/src/memory/conversation-crud.ts +2 -1
  142. package/src/memory/conversation-title-service.ts +16 -2
  143. package/src/memory/db-init.ts +8 -0
  144. package/src/memory/delivery-crud.ts +2 -1
  145. package/src/memory/guardian-action-store.ts +2 -1
  146. package/src/memory/guardian-approvals.ts +3 -2
  147. package/src/memory/ingress-invite-store.ts +12 -2
  148. package/src/memory/ingress-member-store.ts +4 -3
  149. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  150. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  151. package/src/memory/migrations/index.ts +2 -0
  152. package/src/memory/schema.ts +26 -5
  153. package/src/messaging/provider-types.ts +24 -0
  154. package/src/messaging/provider.ts +7 -0
  155. package/src/messaging/providers/gmail/adapter.ts +127 -0
  156. package/src/messaging/providers/sms/adapter.ts +40 -37
  157. package/src/notifications/adapters/macos.ts +45 -2
  158. package/src/notifications/broadcaster.ts +16 -0
  159. package/src/notifications/copy-composer.ts +50 -2
  160. package/src/notifications/decision-engine.ts +22 -9
  161. package/src/notifications/destination-resolver.ts +16 -2
  162. package/src/notifications/emit-signal.ts +18 -9
  163. package/src/notifications/guardian-question-mode.ts +419 -0
  164. package/src/notifications/signal.ts +14 -3
  165. package/src/permissions/checker.ts +13 -1
  166. package/src/permissions/prompter.ts +14 -0
  167. package/src/providers/anthropic/client.ts +20 -0
  168. package/src/providers/provider-send-message.ts +15 -3
  169. package/src/runtime/access-request-helper.ts +82 -4
  170. package/src/runtime/actor-token-service.ts +234 -0
  171. package/src/runtime/actor-token-store.ts +236 -0
  172. package/src/runtime/actor-trust-resolver.ts +2 -2
  173. package/src/runtime/assistant-scope.ts +10 -0
  174. package/src/runtime/channel-approvals.ts +5 -3
  175. package/src/runtime/channel-readiness-service.ts +23 -64
  176. package/src/runtime/channel-readiness-types.ts +3 -4
  177. package/src/runtime/channel-retry-sweep.ts +4 -1
  178. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  179. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  180. package/src/runtime/guardian-context-resolver.ts +82 -0
  181. package/src/runtime/guardian-outbound-actions.ts +5 -7
  182. package/src/runtime/guardian-reply-router.ts +67 -30
  183. package/src/runtime/guardian-vellum-migration.ts +57 -0
  184. package/src/runtime/http-server.ts +75 -31
  185. package/src/runtime/http-types.ts +13 -0
  186. package/src/runtime/ingress-service.ts +14 -0
  187. package/src/runtime/invite-redemption-service.ts +10 -1
  188. package/src/runtime/local-actor-identity.ts +76 -0
  189. package/src/runtime/middleware/actor-token.ts +271 -0
  190. package/src/runtime/middleware/twilio-validation.ts +2 -4
  191. package/src/runtime/routes/approval-routes.ts +82 -7
  192. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  193. package/src/runtime/routes/call-routes.ts +2 -1
  194. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  195. package/src/runtime/routes/channel-route-shared.ts +3 -3
  196. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  197. package/src/runtime/routes/conversation-routes.ts +142 -53
  198. package/src/runtime/routes/events-routes.ts +22 -8
  199. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  200. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  201. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  202. package/src/runtime/routes/inbound-conversation.ts +4 -3
  203. package/src/runtime/routes/inbound-message-handler.ts +147 -5
  204. package/src/runtime/routes/ingress-routes.ts +2 -0
  205. package/src/runtime/routes/integration-routes.ts +7 -15
  206. package/src/runtime/routes/pairing-routes.ts +163 -0
  207. package/src/runtime/routes/twilio-routes.ts +934 -0
  208. package/src/runtime/tool-grant-request-helper.ts +3 -1
  209. package/src/security/oauth2.ts +27 -2
  210. package/src/security/token-manager.ts +46 -10
  211. package/src/tools/browser/browser-execution.ts +4 -3
  212. package/src/tools/browser/browser-handoff.ts +10 -18
  213. package/src/tools/browser/browser-manager.ts +80 -25
  214. package/src/tools/browser/browser-screencast.ts +35 -119
  215. package/src/tools/calls/call-start.ts +2 -1
  216. package/src/tools/permission-checker.ts +15 -4
  217. package/src/tools/terminal/parser.ts +12 -0
  218. package/src/tools/tool-approval-handler.ts +244 -19
  219. package/src/workspace/git-service.ts +19 -0
  220. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  221. package/src/daemon/handlers/config-twilio.ts +0 -1082
@@ -25,6 +25,32 @@ const log = getLogger('session-surfaces');
25
25
  const MAX_UNDO_DEPTH = 10;
26
26
  const TASK_PROGRESS_TEMPLATE_FIELDS = ['title', 'status', 'steps'] as const;
27
27
 
28
+ /**
29
+ * Migrate dynamic_page fields from the top-level tool input into `data`.
30
+ *
31
+ * The LLM sometimes sends `html`, `width`, `height`, or `preview` at the
32
+ * top level instead of nested inside `data`. Without this normalization the
33
+ * surface opens blank because `rawData` is `{}`.
34
+ */
35
+ function normalizeDynamicPageShowData(input: Record<string, unknown>, rawData: Record<string, unknown>): DynamicPageSurfaceData {
36
+ const normalized: Record<string, unknown> = { ...rawData };
37
+
38
+ if (typeof normalized.html !== 'string' && typeof input.html === 'string') {
39
+ normalized.html = input.html;
40
+ }
41
+ if (normalized.width == null && input.width != null) {
42
+ normalized.width = input.width;
43
+ }
44
+ if (normalized.height == null && input.height != null) {
45
+ normalized.height = input.height;
46
+ }
47
+ if (!isPlainObject(normalized.preview) && isPlainObject(input.preview)) {
48
+ normalized.preview = input.preview;
49
+ }
50
+
51
+ return normalized as unknown as DynamicPageSurfaceData;
52
+ }
53
+
28
54
  function normalizeCardShowData(input: Record<string, unknown>, rawData: Record<string, unknown>): CardSurfaceData {
29
55
  const normalized: Record<string, unknown> = { ...rawData };
30
56
 
@@ -105,7 +131,7 @@ export interface SurfaceSessionContext {
105
131
  sendToClient(msg: ServerMessage): void;
106
132
  pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
107
133
  lastSurfaceAction: Map<string, { actionId: string; data?: Record<string, unknown> }>;
108
- surfaceState: Map<string, { surfaceType: SurfaceType; data: SurfaceData }>;
134
+ surfaceState: Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>;
109
135
  surfaceUndoStacks: Map<string, string[]>;
110
136
  currentTurnSurfaces: Array<{
111
137
  surfaceId: string;
@@ -592,7 +618,9 @@ export async function surfaceProxyResolver(
592
618
  const rawData = isPlainObject(input.data) ? input.data : {};
593
619
  const data = (surfaceType === 'card'
594
620
  ? normalizeCardShowData(input, rawData)
595
- : rawData) as SurfaceData;
621
+ : surfaceType === 'dynamic_page'
622
+ ? normalizeDynamicPageShowData(input, rawData)
623
+ : rawData) as SurfaceData;
596
624
  const actions = input.actions as Array<{ id: string; label: string; style?: string }> | undefined;
597
625
  // Interactive surfaces default to awaiting user action.
598
626
  const hasActions = Array.isArray(actions) && actions.length > 0;
@@ -604,7 +632,7 @@ export async function surfaceProxyResolver(
604
632
  const awaitAction = (input.await_action as boolean) ?? isInteractive;
605
633
 
606
634
  // Track surface state for ui_update merging
607
- ctx.surfaceState.set(surfaceId, { surfaceType, data });
635
+ ctx.surfaceState.set(surfaceId, { surfaceType, data, title });
608
636
 
609
637
  const display = (input.display as string) === 'panel' ? 'panel' : 'inline';
610
638
 
@@ -754,6 +782,7 @@ export async function surfaceProxyResolver(
754
782
  ctx.surfaceState.set(surfaceId, {
755
783
  surfaceType: 'dynamic_page',
756
784
  data: surfaceData,
785
+ title: app.name,
757
786
  });
758
787
 
759
788
  ctx.sendToClient({
@@ -36,6 +36,7 @@ import type { Message } from '../providers/types.js';
36
36
  import type { Provider } from '../providers/types.js';
37
37
  import { ToolExecutor } from '../tools/executor.js';
38
38
  import type { AssistantAttachmentDraft } from './assistant-attachments.js';
39
+ import type { AssistantActivityState, ConfirmationStateChanged } from './ipc-contract/messages.js';
39
40
  import type { ServerMessage, SurfaceData,SurfaceType, UsageStats, UserMessageAttachment } from './ipc-protocol.js';
40
41
  import {
41
42
  classifyResponseTierAsync,
@@ -146,7 +147,7 @@ export class Session {
146
147
  /** @internal */ commandIntent?: { type: string; payload?: string; languageCode?: string };
147
148
  /** @internal */ pendingSurfaceActions = new Map<string, { surfaceType: SurfaceType }>();
148
149
  /** @internal */ lastSurfaceAction = new Map<string, { actionId: string; data?: Record<string, unknown> }>();
149
- /** @internal */ surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>();
150
+ /** @internal */ surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>();
150
151
  /** @internal */ surfaceUndoStacks = new Map<string, string[]>();
151
152
  /** @internal */ withSurface = createSurfaceMutex();
152
153
  /** @internal */ currentTurnSurfaces: Array<{ surfaceId: string; surfaceType: SurfaceType; title?: string; data: SurfaceData; actions?: Array<{ id: string; label: string; style?: string }>; display?: string }> = [];
@@ -161,6 +162,16 @@ export class Session {
161
162
  public lastAttachmentWarnings: string[] = [];
162
163
  /** @internal */ currentTurnChannelContext: TurnChannelContext | null = null;
163
164
  /** @internal */ currentTurnInterfaceContext: TurnInterfaceContext | null = null;
165
+ /** @internal */ activityVersion = 0;
166
+ /**
167
+ * Optional callback invoked whenever a server-authoritative state signal
168
+ * (confirmation_state_changed or assistant_activity_state) is emitted.
169
+ *
170
+ * HTTP/SSE sessions set this so the hub publisher receives these events —
171
+ * without it, the signals only travel through `sendToClient`, which is a
172
+ * no-op for socketless sessions.
173
+ */
174
+ private onStateSignal?: (msg: ServerMessage) => void;
164
175
 
165
176
  constructor(
166
177
  conversationId: string,
@@ -180,6 +191,22 @@ export class Session {
180
191
  this.memoryPolicy = memoryPolicy ? { ...memoryPolicy } : { ...DEFAULT_MEMORY_POLICY };
181
192
  this.traceEmitter = new TraceEmitter(conversationId, sendToClient);
182
193
  this.prompter = new PermissionPrompter(sendToClient);
194
+ this.prompter.setOnStateChanged((requestId, state, source) => {
195
+ // Route through emitConfirmationStateChanged so the onStateSignal
196
+ // listener publishes to the SSE hub for HTTP/SSE consumers.
197
+ this.emitConfirmationStateChanged({
198
+ sessionId: this.conversationId,
199
+ requestId,
200
+ state,
201
+ source,
202
+ });
203
+ // Emit activity state transitions for confirmation lifecycle
204
+ if (state === 'pending') {
205
+ this.emitActivityState('awaiting_confirmation', 'confirmation_requested', 'assistant_turn');
206
+ } else if (state === 'timed_out') {
207
+ this.emitActivityState('thinking', 'confirmation_resolved', 'assistant_turn');
208
+ }
209
+ });
183
210
  this.secretPrompter = new SecretPrompter(sendToClient);
184
211
 
185
212
  // Register watch/call notifiers (reads ctx properties lazily)
@@ -356,6 +383,17 @@ export class Session {
356
383
  return this.sendToClient;
357
384
  }
358
385
 
386
+ /**
387
+ * Register a callback for server-authoritative state signals
388
+ * (confirmation_state_changed, assistant_activity_state).
389
+ *
390
+ * This enables HTTP/SSE sessions to receive these events through the
391
+ * hub publisher, since `sendToClient` is a no-op for socketless sessions.
392
+ */
393
+ setStateSignalListener(listener: (msg: ServerMessage) => void): void {
394
+ this.onStateSignal = listener;
395
+ }
396
+
359
397
  setSandboxOverride(enabled: boolean | undefined): void {
360
398
  this.sandboxOverride = enabled;
361
399
  }
@@ -453,6 +491,11 @@ export class Session {
453
491
  selectedPattern?: string,
454
492
  selectedScope?: string,
455
493
  decisionContext?: string,
494
+ emissionContext?: {
495
+ source?: ConfirmationStateChanged['source'];
496
+ causedByRequestId?: string;
497
+ decisionText?: string;
498
+ },
456
499
  ): void {
457
500
  this.prompter.resolveConfirmation(
458
501
  requestId,
@@ -461,12 +504,56 @@ export class Session {
461
504
  selectedScope,
462
505
  decisionContext,
463
506
  );
507
+
508
+ // Emit authoritative confirmation state and activity transition centrally
509
+ // so ALL callers (IPC handlers, /v1/confirm, channel bridges) get
510
+ // consistent events without duplicating emission logic.
511
+ const resolvedState = (decision === 'deny' || decision === 'always_deny')
512
+ ? 'denied' as const
513
+ : 'approved' as const;
514
+ this.emitConfirmationStateChanged({
515
+ sessionId: this.conversationId,
516
+ requestId,
517
+ state: resolvedState,
518
+ source: emissionContext?.source ?? 'button',
519
+ ...(emissionContext?.causedByRequestId ? { causedByRequestId: emissionContext.causedByRequestId } : {}),
520
+ ...(emissionContext?.decisionText ? { decisionText: emissionContext.decisionText } : {}),
521
+ });
522
+ this.emitActivityState('thinking', 'confirmation_resolved', 'assistant_turn');
464
523
  }
465
524
 
466
525
  handleSecretResponse(requestId: string, value?: string, delivery?: 'store' | 'transient_send'): void {
467
526
  this.secretPrompter.resolveSecret(requestId, value, delivery);
468
527
  }
469
528
 
529
+ // ── Server-authoritative state signals ─────────────────────────────
530
+
531
+ emitConfirmationStateChanged(params: Omit<ConfirmationStateChanged, 'type'>): void {
532
+ const msg: ServerMessage = { type: 'confirmation_state_changed', ...params } as ServerMessage;
533
+ this.sendToClient(msg);
534
+ this.onStateSignal?.(msg);
535
+ }
536
+
537
+ emitActivityState(
538
+ phase: AssistantActivityState['phase'],
539
+ reason: AssistantActivityState['reason'],
540
+ anchor: AssistantActivityState['anchor'] = 'assistant_turn',
541
+ requestId?: string,
542
+ ): void {
543
+ this.activityVersion++;
544
+ const msg: ServerMessage = {
545
+ type: 'assistant_activity_state',
546
+ sessionId: this.conversationId,
547
+ activityVersion: this.activityVersion,
548
+ phase,
549
+ anchor,
550
+ requestId,
551
+ reason,
552
+ } as ServerMessage;
553
+ this.sendToClient(msg);
554
+ this.onStateSignal?.(msg);
555
+ }
556
+
470
557
  setChannelCapabilities(caps: ChannelCapabilities | null): void {
471
558
  this.channelCapabilities = caps ?? undefined;
472
559
  }
@@ -68,6 +68,19 @@ function registerHook(toolNames: string | string[], hook: PostExecutionHook): vo
68
68
  }
69
69
  }
70
70
 
71
+ // Broadcast app_files_changed when a new app is created so clients
72
+ // (e.g. macOS "Things" sidebar) refresh their app list immediately.
73
+ registerHook('app_create', (_name, _input, result, { ctx, broadcastToAllClients }) => {
74
+ try {
75
+ const parsed = JSON.parse(result.content) as { id?: string };
76
+ if (parsed.id) {
77
+ handleAppChange(ctx, parsed.id, broadcastToAllClients);
78
+ }
79
+ } catch {
80
+ // Result wasn't valid JSON — skip the broadcast.
81
+ }
82
+ });
83
+
71
84
  // Auto-refresh workspace surfaces when a persisted app is updated.
72
85
  // If no surface is currently showing the app, auto-open it.
73
86
  registerHook('app_update', (_name, input, _result, { ctx, broadcastToAllClients }) => {
@@ -77,6 +90,15 @@ registerHook('app_update', (_name, input, _result, { ctx, broadcastToAllClients
77
90
  }
78
91
  });
79
92
 
93
+ // Broadcast app_files_changed when an app is deleted so clients remove it
94
+ // from their cached app lists.
95
+ registerHook('app_delete', (_name, input, _result, { broadcastToAllClients }) => {
96
+ const appId = input.app_id as string | undefined;
97
+ if (appId) {
98
+ broadcastToAllClients?.({ type: 'app_files_changed', appId });
99
+ }
100
+ });
101
+
80
102
  // Broadcast tasks_changed so connected clients (e.g. macOS Tasks window)
81
103
  // auto-refresh when the LLM mutates the task queue via tools
82
104
  registerHook(