@vellumai/assistant 0.11.5-dev.202608232107.2bf6d61 → 0.11.5-dev.202608240233.8a9625a

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.5-dev.202608232107.2bf6d61",
3
+ "version": "0.11.5-dev.202608240233.8a9625a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -65,7 +65,7 @@ import type {
65
65
  import type { MessagingConversationContext } from "../daemon/conversation-messaging.js";
66
66
  import { persistQueuedMessageBody } from "../daemon/conversation-messaging.js";
67
67
  import type { MessageQueue } from "../daemon/conversation-queue-manager.js";
68
- import { isMacOriginatedUserMessage } from "../persistence/conversation-types.js";
68
+ import { isDesktopOriginatedUserMessage } from "../persistence/conversation-types.js";
69
69
 
70
70
  function createWebTurnContext(
71
71
  clientOs: string | undefined,
@@ -179,9 +179,9 @@ describe("client OS surface metadata persistence", () => {
179
179
  * `Conversation.clientOs` is a live field that only a transport-carrying
180
180
  * message refreshes, so a transport-less turn stamps whatever an earlier send
181
181
  * left there. `clientOsFromRequest` separates the two, and
182
- * {@link isMacOriginatedUserMessage} (the row-origin read behind the
182
+ * {@link isDesktopOriginatedUserMessage} (the row-origin read behind the
183
183
  * `chat.assistant_reply` presence gate) requires it before letting an attended
184
- * Mac suppress a push.
184
+ * desktop suppress a push.
185
185
  */
186
186
  describe("client OS per-row evidence marker", () => {
187
187
  beforeEach(() => {
@@ -198,7 +198,20 @@ describe("client OS per-row evidence marker", () => {
198
198
 
199
199
  expect(lastUserMetadata().client).toEqual({ os: "macos" });
200
200
  expect(lastUserMetadata().clientOsFromRequest).toBe(true);
201
- expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(true);
201
+ expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(true);
202
+ });
203
+
204
+ test("recognizes a Windows app turn as desktop-originated", async () => {
205
+ const ctx = createWebTurnContext("windows");
206
+ await persistQueuedMessageBody(ctx, {
207
+ content: "hello",
208
+ requestId: "req-transport-os-windows",
209
+ requestClientOs: "windows",
210
+ });
211
+
212
+ expect(lastUserMetadata().client).toEqual({ os: "windows" });
213
+ expect(lastUserMetadata().clientOsFromRequest).toBe(true);
214
+ expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(true);
202
215
  });
203
216
 
204
217
  // Mirrors the Mac-originated case above: a plain browser tab reports
@@ -214,7 +227,7 @@ describe("client OS per-row evidence marker", () => {
214
227
 
215
228
  expect(lastUserMetadata().client).toEqual({ os: "web" });
216
229
  expect(lastUserMetadata().clientOsFromRequest).toBe(true);
217
- expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(false);
230
+ expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(false);
218
231
  });
219
232
 
220
233
  test("marks an OS this row's own request headers reported", async () => {
@@ -226,14 +239,14 @@ describe("client OS per-row evidence marker", () => {
226
239
  });
227
240
 
228
241
  expect(lastUserMetadata().clientOsFromRequest).toBe(true);
229
- expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(true);
242
+ expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(true);
230
243
  });
231
244
 
232
245
  // The bug shape: a button tapped on the phone reaches a conversation whose
233
246
  // last desktop send left `clientOs: "macos"` behind. The row still carries
234
247
  // the inherited OS for telemetry, but nothing says this turn came from the
235
248
  // Mac, so the reply push must survive.
236
- test("leaves an inherited OS unmarked and not Mac-originated", async () => {
249
+ test("leaves an inherited OS unmarked and not desktop-originated", async () => {
237
250
  const ctx = createWebTurnContext("macos");
238
251
  await persistQueuedMessageBody(ctx, {
239
252
  content: "[User action on card surface: submit]",
@@ -242,7 +255,7 @@ describe("client OS per-row evidence marker", () => {
242
255
 
243
256
  expect(lastUserMetadata().client).toEqual({ os: "macos" });
244
257
  expect(lastUserMetadata().clientOsFromRequest).toBeUndefined();
245
- expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(false);
258
+ expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(false);
246
259
  });
247
260
 
248
261
  // The marker vouches for the `client.os` that actually landed, not for
@@ -258,7 +271,7 @@ describe("client OS per-row evidence marker", () => {
258
271
 
259
272
  expect(lastUserMetadata().client).toEqual({ os: "macos" });
260
273
  expect(lastUserMetadata().clientOsFromRequest).toBeUndefined();
261
- expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(false);
274
+ expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(false);
262
275
  });
263
276
 
264
277
  // The marker is derived from what this persist can see, so a bag value
@@ -273,7 +286,7 @@ describe("client OS per-row evidence marker", () => {
273
286
  });
274
287
 
275
288
  expect(lastUserMetadata().clientOsFromRequest).toBeUndefined();
276
- expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(false);
289
+ expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(false);
277
290
  });
278
291
 
279
292
  test("leaves the marker off for a reported OS outside the vocabulary", async () => {
@@ -71,6 +71,14 @@ describe("resolveVerbosity", () => {
71
71
  ).toBe("rich");
72
72
  });
73
73
 
74
+ test("explicit interfaceHint=windows overrides to rich", () => {
75
+ expect(
76
+ resolveVerbosity("telegram" as NotificationChannel, {
77
+ interfaceHint: "windows",
78
+ }),
79
+ ).toBe("rich");
80
+ });
81
+
74
82
  test("explicit interfaceHint=telegram overrides to compact", () => {
75
83
  expect(
76
84
  resolveVerbosity("vellum" as NotificationChannel, {
@@ -49,6 +49,14 @@ describe("resolveSlash /commands interface-aware help", () => {
49
49
  ]);
50
50
  });
51
51
 
52
+ test("renders desktop command help for Windows", async () => {
53
+ const lines = await resolveCommandsLines(
54
+ makeSlashContext({ userMessageInterface: "windows" }),
55
+ );
56
+ expect(lines.some((line) => line.startsWith("/btw "))).toBe(true);
57
+ expect(lines.some((line) => line.startsWith("/fork "))).toBe(true);
58
+ });
59
+
52
60
  test("renders iOS command help with /fork", async () => {
53
61
  const lines = await resolveCommandsLines(
54
62
  makeSlashContext({ userMessageInterface: "ios" }),
@@ -90,6 +90,12 @@ describe("classifyDiskPressureTurnPolicy", () => {
90
90
  metadata: localOwnerTurn,
91
91
  expected: { action: "allow-cleanup-mode", reason: "local-owner" },
92
92
  },
93
+ {
94
+ name: "locked Windows local owner without trust enters cleanup mode",
95
+ status: status({ acknowledged: true }),
96
+ metadata: { ...localOwnerTurn, sourceInterface: "windows" },
97
+ expected: { action: "allow-cleanup-mode", reason: "local-owner" },
98
+ },
93
99
  {
94
100
  name: "locked guardian enters cleanup mode",
95
101
  status: status(),
@@ -158,7 +158,7 @@ Optionally pass `routing_hints` (a JSON object) to influence routing decisions (
158
158
  2. **`interface` fallback** — if `source_channel` is absent (common for guardian/direct users), map the `interface` value to a channel name:
159
159
  | `interface` value | Channel name |
160
160
  | --- | --- |
161
- | `macos`, `ios` | `vellum` |
161
+ | `macos`, `windows`, `ios` | `vellum` |
162
162
  | `telegram` | `telegram` |
163
163
  | `slack` | `slack` |
164
164
  | `cli` | _(omit — no routable channel)_ |
@@ -108,6 +108,14 @@
108
108
  "description": "Auto-expand completed tool call step groups instead of showing them collapsed",
109
109
  "defaultEnabled": false
110
110
  },
111
+ {
112
+ "id": "inline-assistant-intermediates",
113
+ "scope": "client",
114
+ "key": "inline-assistant-intermediates",
115
+ "label": "Inline Assistant Intermediates",
116
+ "description": "Render a completed response's intermediate activity inline at full size instead of collapsing it behind the Earlier activity disclosure",
117
+ "defaultEnabled": false
118
+ },
111
119
  {
112
120
  "id": "backward-releases",
113
121
  "scope": "assistant",
@@ -295,6 +295,11 @@ function resolveStatusCommand(context: SlashContext): SlashResolution {
295
295
 
296
296
  const CLEAN_HELP_LINE =
297
297
  "/clean — Strip injected runtime context and reset memory injection state (no summarization)";
298
+ const FORK_CAPABLE_INTERFACES = new Set<InterfaceId>([
299
+ "macos",
300
+ "windows",
301
+ "ios",
302
+ ]);
298
303
 
299
304
  function resolveCommandsList(context?: SlashContext): string[] {
300
305
  const fallbackLines = [
@@ -315,21 +320,7 @@ function resolveCommandsList(context?: SlashContext): string[] {
315
320
  return fallbackLines;
316
321
  }
317
322
 
318
- if (context.userMessageInterface === "macos") {
319
- return [
320
- "/commands — List all available commands",
321
- "/compact — Force context compaction immediately",
322
- CLEAN_HELP_LINE,
323
- "/context — Show conversation context usage",
324
- "/model — List or switch inference profile",
325
- "/models — List all available models",
326
- "/status — Show conversation status and context usage",
327
- "/btw — Ask a side question while the assistant is working",
328
- "/fork — Fork the current conversation into a new branch",
329
- ];
330
- }
331
-
332
- if (context.userMessageInterface === "ios") {
323
+ if (FORK_CAPABLE_INTERFACES.has(context.userMessageInterface)) {
333
324
  return [
334
325
  "/commands — List all available commands",
335
326
  "/compact — Force context compaction immediately",
@@ -57,7 +57,13 @@ const BACKGROUND_SOURCES = new Set([
57
57
  "schedule",
58
58
  "task",
59
59
  ]);
60
- const LOCAL_OWNER_INTERFACES = new Set(["macos", "web", "vellum", "cli"]);
60
+ const LOCAL_OWNER_INTERFACES = new Set([
61
+ "macos",
62
+ "windows",
63
+ "web",
64
+ "vellum",
65
+ "cli",
66
+ ]);
61
67
 
62
68
  export function classifyDiskPressureTurnPolicy(
63
69
  status: DiskPressureStatus,
@@ -200,6 +200,17 @@ function makeMacOriginatedMessage(): MessageRow {
200
200
  });
201
201
  }
202
202
 
203
+ function makeWindowsOriginatedMessage(): MessageRow {
204
+ return makeMessage({
205
+ metadata: JSON.stringify({
206
+ userMessageChannel: "vellum",
207
+ userMessageInterface: "web",
208
+ client: { os: "windows" },
209
+ clientOsFromRequest: true,
210
+ }),
211
+ });
212
+ }
213
+
203
214
  /**
204
215
  * A turn opened from a plain browser tab, as opposed to the Electron desktop
205
216
  * renderer sharing the same web bundle: `client.os` is `"web"` only when
@@ -347,7 +358,7 @@ describe("emitAssistantReplyNotification", () => {
347
358
  desktopAttended = true;
348
359
  });
349
360
 
350
- test("marks the signal source-active while a Mac reports itself attended", async () => {
361
+ test("marks the signal source-active while a desktop reports itself attended", async () => {
351
362
  await run();
352
363
 
353
364
  expect(emitCalls).toHaveLength(1);
@@ -357,7 +368,16 @@ describe("emitAssistantReplyNotification", () => {
357
368
  expect(desktopPresenceArgs).toEqual([[]]);
358
369
  });
359
370
 
360
- test("leaves the signal live when no Mac is attended", async () => {
371
+ test("marks a Windows-originated signal source-active while attended", async () => {
372
+ initiatingRow = makeWindowsOriginatedMessage();
373
+
374
+ await run();
375
+
376
+ expect(emitCalls).toHaveLength(1);
377
+ expect(emitCalls[0].attentionHints.visibleInSourceNow).toBe(true);
378
+ });
379
+
380
+ test("leaves the signal live when no desktop is attended", async () => {
361
381
  desktopAttended = false;
362
382
 
363
383
  await run();
@@ -386,10 +406,13 @@ describe("emitAssistantReplyNotification", () => {
386
406
  expect(warnCalls).toHaveLength(1);
387
407
  });
388
408
 
389
- // An attended Mac only speaks for a turn the Mac itself opened: the user
390
- // can send from the phone minutes after last touching the keyboard, which
409
+ // An attended desktop only speaks for a turn that desktop itself opened.
410
+ // The user can send from the phone minutes after last touching the keyboard,
391
411
  // is exactly the reply this producer exists to push.
392
- const NON_MAC_ORIGIN_CASES: Array<{ name: string; metadata: unknown }> = [
412
+ const NON_DESKTOP_ORIGIN_CASES: Array<{
413
+ name: string;
414
+ metadata: unknown;
415
+ }> = [
393
416
  {
394
417
  name: "the iOS app",
395
418
  metadata: { client: { os: "ios" }, clientOsFromRequest: true },
@@ -412,7 +435,7 @@ describe("emitAssistantReplyNotification", () => {
412
435
  // `macos` the conversation's live client state kept from an earlier
413
436
  // desktop send. Without the marker that OS names an earlier turn, not
414
437
  // this one, and the push the user is waiting for on their phone would
415
- // be dropped by the attended Mac.
438
+ // be dropped by the attended desktop.
416
439
  {
417
440
  name: "a row whose macOS OS was inherited, not reported",
418
441
  metadata: { userMessageInterface: "web", client: { os: "macos" } },
@@ -424,7 +447,7 @@ describe("emitAssistantReplyNotification", () => {
424
447
  },
425
448
  ];
426
449
 
427
- for (const { name, metadata } of NON_MAC_ORIGIN_CASES) {
450
+ for (const { name, metadata } of NON_DESKTOP_ORIGIN_CASES) {
428
451
  test(`leaves the signal live for a turn opened from ${name}`, async () => {
429
452
  initiatingRow = makeMessage({ metadata: JSON.stringify(metadata) });
430
453
 
@@ -23,7 +23,7 @@ import {
23
23
  parseMessageMetadata,
24
24
  } from "../persistence/conversation-crud.js";
25
25
  import {
26
- isMacOriginatedUserMessage,
26
+ isDesktopOriginatedUserMessage,
27
27
  isReplyPushIneligibleUserMessage,
28
28
  resolveConversationKind,
29
29
  } from "../persistence/conversation-types.js";
@@ -122,7 +122,7 @@ function readSuppressionMarkers(
122
122
  *
123
123
  * No `actorPrincipalId`: the platform delivers this push to the assistant
124
124
  * owner's device tokens, and a pod has exactly one owner, so any attended
125
- * macOS client is that owner's.
125
+ * desktop client is that owner's.
126
126
  */
127
127
  function readDesktopAttended(rlog: pino.Logger): boolean {
128
128
  try {
@@ -246,12 +246,12 @@ export async function emitAssistantReplyNotification(params: {
246
246
  );
247
247
 
248
248
  // Read as close to the emit as possible: nothing short-circuits on it.
249
- // Presence only speaks for a turn the Mac itself opened, on that row's own
250
- // OS evidence. A turn sent from the phone still needs its push while the
251
- // Mac sits idle within the desktop client's attendance window.
249
+ // Presence only speaks for a turn the desktop itself opened, on that row's
250
+ // own OS evidence. A turn sent from the phone still needs its push while the
251
+ // desktop sits idle within the attendance window.
252
252
  const desktopAttended =
253
253
  isAssistantFeatureFlagEnabled(DESKTOP_PRESENCE_FLAG) &&
254
- isMacOriginatedUserMessage(initiatingMetadata) &&
254
+ isDesktopOriginatedUserMessage(initiatingMetadata) &&
255
255
  readDesktopAttended(rlog);
256
256
 
257
257
  // Conversation-scoped web presence applies regardless of which device
@@ -276,7 +276,7 @@ export async function emitAssistantReplyNotification(params: {
276
276
  // opting into v2.
277
277
  urgency: "medium",
278
278
  isAsyncBackground: false,
279
- // Read weakly, as "at the surface this landed on": the attended Mac
279
+ // Read weakly, as "at the surface this landed on": the attended desktop
280
280
  // or focused web tab that opened the turn renders the reply in-app,
281
281
  // so a redundant push would only duplicate what's already on screen.
282
282
  visibleInSourceNow: desktopAttended || webFocused,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Generates richer seed content for notification conversations than the concise
5
5
  * title/body used in native notification popups. Verbosity adapts to the
6
- * delivery surface: vellum/macos gets flowing prose, telegram gets compact.
6
+ * delivery surface: Vellum desktop gets flowing prose, Telegram gets compact.
7
7
  *
8
8
  * Composes from `copy.title/body` rather than hardcoded English templates
9
9
  * so LLM-localized copy is preserved for non-English users.
@@ -25,7 +25,12 @@ const CHANNEL_DEFAULT_INTERFACE: Record<string, InterfaceId> = {
25
25
  telegram: "telegram",
26
26
  };
27
27
 
28
- const RICH_INTERFACES = new Set<InterfaceId>(["macos", "ios", "web"]);
28
+ const RICH_INTERFACES = new Set<InterfaceId>([
29
+ "macos",
30
+ "windows",
31
+ "ios",
32
+ "web",
33
+ ]);
29
34
 
30
35
  /**
31
36
  * Resolve verbosity level from delivery channel + optional interface hint.
@@ -228,21 +228,22 @@ export function isVoiceSessionUserMessage(
228
228
  }
229
229
 
230
230
  /**
231
- * True when the row that opened the turn was sent from the macOS app, on that
231
+ * True when the row that opened the turn was sent from a desktop app, on that
232
232
  * row's own evidence.
233
233
  *
234
234
  * Two markers, both required. The `client` bag's `os` entry is the only
235
235
  * per-platform attribution on a message: `userMessageInterface` is `"web"` for
236
- * the macOS app, the iOS app, and a desktop browser alike. `clientOsFromRequest`
237
- * says that `os` was reported by this row's request or transport rather than
238
- * inherited from the conversation's live client state, which names the surface
239
- * of an earlier turn: a button tapped on the phone against a conversation last
240
- * sent to from the Mac persists `os: "macos"` with no marker.
236
+ * the desktop apps, the iOS app, and a desktop browser alike.
237
+ * `clientOsFromRequest` says that `os` was reported by this row's request or
238
+ * transport rather than inherited from the conversation's live client state,
239
+ * which names the surface of an earlier turn. A button tapped on the phone
240
+ * against a conversation last sent to from desktop persists the old OS with no
241
+ * marker.
241
242
  *
242
243
  * Origin a row did not report itself is origin unknown. Callers gate
243
- * suppression on this, so unknown has to read as not-macOS.
244
+ * suppression on this, so unknown has to read as not-desktop.
244
245
  */
245
- export function isMacOriginatedUserMessage(
246
+ export function isDesktopOriginatedUserMessage(
246
247
  metadata: Record<string, unknown> | undefined,
247
248
  ): boolean {
248
249
  if (metadata?.clientOsFromRequest !== true) {
@@ -252,7 +253,8 @@ export function isMacOriginatedUserMessage(
252
253
  if (typeof client !== "object" || client === null) {
253
254
  return false;
254
255
  }
255
- return parseClientOs((client as Record<string, unknown>).os) === "macos";
256
+ const clientOs = parseClientOs((client as Record<string, unknown>).os);
257
+ return clientOs === "macos" || clientOs === "windows";
256
258
  }
257
259
 
258
260
  /**
@@ -2,7 +2,7 @@
2
2
  * Tests for the desktop presence policy.
3
3
  *
4
4
  * The invariant under test is fail-open: only a fresh `active` report from a
5
- * `macos` client answers `true`. Every other shape (stale, idle, away, no
5
+ * desktop client answers `true`. Every other shape (stale, idle, away, no
6
6
  * presence, wrong interface, no clients, hub throw) answers `false` so the
7
7
  * push is sent. Scoping the read to an actor principal narrows what counts as
8
8
  * attendance, so it can only ever turn a `true` into a `false`.
@@ -45,6 +45,15 @@ describe("isDesktopAttended", () => {
45
45
  expect(isDesktopAttended()).toBe(true);
46
46
  });
47
47
 
48
+ test("fresh active windows client is attended", () => {
49
+ registerClient({
50
+ clientId: "windows-1",
51
+ interfaceId: "windows",
52
+ presence: "active",
53
+ });
54
+ expect(isDesktopAttended()).toBe(true);
55
+ });
56
+
48
57
  test("active report older than the staleness bound is not attended", () => {
49
58
  registerClient({ clientId: "mac-1", presence: "active" });
50
59
  expect(isDesktopAttended({ now: afterStaleness() })).toBe(false);
@@ -92,9 +101,13 @@ describe("isDesktopAttended", () => {
92
101
  }
93
102
  });
94
103
 
95
- test("one active macos client among several is enough", () => {
104
+ test("one active desktop client among several is enough", () => {
96
105
  registerClient({ clientId: "mac-1", presence: "away" });
97
- registerClient({ clientId: "mac-2", presence: "active" });
106
+ registerClient({
107
+ clientId: "windows-1",
108
+ interfaceId: "windows",
109
+ presence: "active",
110
+ });
98
111
  expect(isDesktopAttended()).toBe(true);
99
112
  });
100
113
  });
@@ -109,6 +122,16 @@ describe("isDesktopAttended scoped to an actor principal", () => {
109
122
  expect(isDesktopAttended({ actorPrincipalId: "actor-a" })).toBe(true);
110
123
  });
111
124
 
125
+ test("matching actor principal on windows is attended", () => {
126
+ registerClient({
127
+ clientId: "windows-1",
128
+ interfaceId: "windows",
129
+ presence: "active",
130
+ actorPrincipalId: "actor-a",
131
+ });
132
+ expect(isDesktopAttended({ actorPrincipalId: "actor-a" })).toBe(true);
133
+ });
134
+
112
135
  test("another actor's attended mac does not count", () => {
113
136
  registerClient({
114
137
  clientId: "mac-1",
@@ -123,7 +146,7 @@ describe("isDesktopAttended scoped to an actor principal", () => {
123
146
  expect(isDesktopAttended({ actorPrincipalId: "actor-a" })).toBe(false);
124
147
  });
125
148
 
126
- test("omitting the principal counts any attended macos client", () => {
149
+ test("omitting the principal counts any attended desktop client", () => {
127
150
  registerClient({
128
151
  clientId: "mac-1",
129
152
  presence: "active",
@@ -2,12 +2,12 @@
2
2
  * Desktop presence policy.
3
3
  *
4
4
  * Answers exactly one question for notification producers: is the user
5
- * demonstrably at their Mac right now? Producers use the answer to skip a
5
+ * demonstrably at their desktop right now? Producers use the answer to skip a
6
6
  * push that the user would see on screen anyway.
7
7
  *
8
8
  * Fails open by design. Presence is in-memory, best-effort, and reported by
9
9
  * a client that can drop off at any time, so anything short of a fresh
10
- * `active` report from a `macos` client answers `false` and the push goes
10
+ * `active` report from a desktop client answers `false` and the push goes
11
11
  * out. A missed notification is strictly worse than a redundant one.
12
12
  */
13
13
 
@@ -15,16 +15,17 @@ import { getLogger } from "../util/logger.js";
15
15
  import { assistantEventHub } from "./assistant-event-hub.js";
16
16
 
17
17
  const log = getLogger("desktop-presence");
18
+ const DESKTOP_INTERFACES = ["macos", "windows"] as const;
18
19
 
19
20
  /**
20
- * Bounds how long suppression can outlive the Mac going to sleep or dropping
21
+ * Bounds how long suppression can outlive the desktop sleeping or dropping
21
22
  * off, while still tolerating two dropped reports at the 30s report interval.
22
23
  */
23
24
  export const PRESENCE_STALE_AFTER_MS = 90_000;
24
25
 
25
26
  export interface DesktopAttendanceOptions {
26
27
  /**
27
- * Only count macOS clients whose verified actor principal matches this id.
28
+ * Only count desktop clients whose verified actor principal matches this id.
28
29
  * A client that connected without a principal (legacy or service token)
29
30
  * never matches a supplied id.
30
31
  */
@@ -34,13 +35,13 @@ export interface DesktopAttendanceOptions {
34
35
  }
35
36
 
36
37
  /**
37
- * Whether some macOS client has reported `active` recently enough to trust.
38
- * Stale, absent, non-macOS, and error reads all answer `false`.
38
+ * Whether some desktop client has reported `active` recently enough to trust.
39
+ * Stale, absent, non-desktop, and error reads all answer `false`.
39
40
  *
40
41
  * Callers whose notification targets one recipient (guardian-scoped or
41
42
  * otherwise per-recipient pushes) must pass that recipient's
42
- * `actorPrincipalId`, or another user's attended Mac suppresses the push.
43
- * Omitting it treats any attended macOS client as attendance, which only
43
+ * `actorPrincipalId`, or another user's attended desktop suppresses the push.
44
+ * Omitting it treats any attended desktop client as attendance, which only
44
45
  * suits notifications with no single recipient.
45
46
  */
46
47
  export function isDesktopAttended(
@@ -48,19 +49,21 @@ export function isDesktopAttended(
48
49
  ): boolean {
49
50
  const { actorPrincipalId, now = new Date() } = options;
50
51
  try {
51
- return assistantEventHub.listClientsByInterface("macos").some((client) => {
52
- if (
53
- actorPrincipalId !== undefined &&
54
- client.actorPrincipalId !== actorPrincipalId
55
- ) {
56
- return false;
57
- }
58
- return (
59
- client.presence?.state === "active" &&
60
- now.getTime() - client.presence.reportedAt.getTime() <=
61
- PRESENCE_STALE_AFTER_MS
62
- );
63
- });
52
+ return DESKTOP_INTERFACES.some((interfaceId) =>
53
+ assistantEventHub.listClientsByInterface(interfaceId).some((client) => {
54
+ if (
55
+ actorPrincipalId !== undefined &&
56
+ client.actorPrincipalId !== actorPrincipalId
57
+ ) {
58
+ return false;
59
+ }
60
+ return (
61
+ client.presence?.state === "active" &&
62
+ now.getTime() - client.presence.reportedAt.getTime() <=
63
+ PRESENCE_STALE_AFTER_MS
64
+ );
65
+ }),
66
+ );
64
67
  } catch (err) {
65
68
  // Returning false sends the push, which is the safe direction.
66
69
  log.warn({ err }, "desktop presence read failed; treating as unattended");