@vellumai/assistant 0.11.7-dev.202608282003.8ce5421 → 0.11.7-dev.202608282312.37594b7

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 (58) hide show
  1. package/ARCHITECTURE.md +9 -9
  2. package/Dockerfile +1 -0
  3. package/docs/trusted-contact-access.md +13 -12
  4. package/knip.json +1 -0
  5. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  6. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  7. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  8. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  9. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  10. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  11. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  12. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +40 -18
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -4
  14. package/package.json +3 -1
  15. package/src/__tests__/guardian-card-withdrawal.test.ts +128 -5
  16. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  17. package/src/__tests__/guardian-gateway-sim.test.ts +72 -0
  18. package/src/__tests__/guardian-gateway-sim.ts +28 -15
  19. package/src/approvals/guardian-card-withdrawal.ts +61 -18
  20. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  21. package/src/avatar/svg-compositor.ts +1 -1
  22. package/src/avatar/traits-png-sync.ts +1 -1
  23. package/src/calls/guardian-action-sweep.ts +6 -1
  24. package/src/channels/__tests__/gateway-guardian-requests.test.ts +12 -12
  25. package/src/channels/gateway-guardian-requests.ts +14 -13
  26. package/src/daemon/conversation-surfaces.ts +12 -2
  27. package/src/daemon/daemon-control.ts +6 -18
  28. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +7 -0
  29. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  30. package/src/messaging/providers/slack/api.test.ts +10 -0
  31. package/src/messaging/providers/slack/auth.ts +32 -6
  32. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  33. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  34. package/src/monitoring/control.ts +2 -2
  35. package/src/notifications/guardian-delivery-recorder.ts +10 -3
  36. package/src/permissions/question-guardian-request.test.ts +1 -1
  37. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  38. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  39. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  40. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  41. package/src/routes/__tests__/control.test.ts +4 -3
  42. package/src/routes/control.ts +1 -1
  43. package/src/routes/route-host-client.ts +1 -1
  44. package/src/runtime/AGENTS.md +23 -11
  45. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  46. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  47. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  48. package/src/runtime/routes/avatar-routes.ts +1 -1
  49. package/src/runtime/routes/guardian-expiry-sweep.ts +80 -31
  50. package/src/runtime/routes/monitoring-routes.ts +0 -3
  51. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  52. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  53. package/src/schedule/scheduler.ts +1 -1
  54. package/src/schedule/worker-control.ts +2 -2
  55. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  56. package/src/util/worker-ownership.ts +76 -9
  57. package/src/util/worker-process.ts +7 -18
  58. package/src/workspace/git-service.ts +1 -0
package/ARCHITECTURE.md CHANGED
@@ -203,7 +203,7 @@ The guardian request system provides a channel-agnostic, unified domain for all
203
203
 
204
204
  **Resolver registry:** Kind-specific resolvers (`src/approvals/guardian-request-resolvers.ts`) handle side effects after CAS resolution. Built-in resolvers: `tool_approval` (channel/desktop approval path), `pending_question` (voice call question path), and `access_request` (trusted-contact verification session creation). New request kinds register resolvers without touching the core primitive.
205
205
 
206
- **Expiry:** the daemon's 60-second sweep (`src/runtime/routes/guardian-expiry-sweep.ts`) asks the gateway to CAS-expire pending requests past `expiresAt` (`guardian_requests_sweep_expired`) and fans out card withdrawal + requester notices from the returned rows; interaction-bound kinds (`tool_approval`, `pending_question`) are expired at daemon boot via `guardian_requests_expire_interaction_bound` since they die with the in-memory pending-interactions map. `src/calls/guardian-action-sweep.ts` sends the guardian-facing expiry notices for voice questions.
206
+ **Expiry:** the daemon's 60-second sweep (`src/runtime/routes/guardian-expiry-sweep.ts`) lists a bounded batch of past-deadline pending requests (`guardian_requests_list_expired_pending`, read-only), fans out card withdrawal + requester notices per row, and only then confirms each with the per-request `guardian_requests_expire` CAS, so a lost response or crash leaves the row listed for the next round; interaction-bound kinds (`tool_approval`, `pending_question`) are expired at daemon boot via `guardian_requests_expire_interaction_bound` since they die with the in-memory pending-interactions map, and the boot call never touches persistent kinds. `src/calls/guardian-action-sweep.ts` sends the guardian-facing expiry notices for voice questions.
207
207
 
208
208
  **Key source files:**
209
209
 
@@ -267,19 +267,19 @@ Guardian requests (tool approvals, access requests, voice ASK_GUARDIAN questions
267
267
 
268
268
  **Voice consultation timeout:** When a voice call's ASK_GUARDIAN consultation times out before the guardian responds, the call controller expires the request via the gateway (`guardian_requests_expire` — deliveries expire with it), sends expiry notices to every surface the approval card reached (vellum conversations get an assistant message; external channels get a direct channel reply), and injects a `[GUARDIAN_TIMEOUT]` instruction so the model apologizes to the caller and offers a message or callback on the next voice turn.
269
269
 
270
- **Background expiry sweep:** Independent of any active call, a 60-second sweep asks the gateway to CAS-expire pending guardian requests past their `expiresAt` (`guardian_requests_sweep_expired` — a concurrent decision that wins the race is never overwritten), then fans out the side effects per expired row: withdrawing the approval cards on every surface, notifying the requester, and releasing any in-memory pending interaction.
270
+ **Background expiry sweep:** Independent of any active call, a 60-second sweep lists pending guardian requests past their `expiresAt` (`guardian_requests_list_expired_pending`), runs the side effects per row (withdrawing the approval cards on every surface, notifying the requester, releasing any in-memory pending interaction), and then confirms each with the per-request `guardian_requests_expire` CAS; a decision arriving past the deadline loses to expiry atomically, since the decide CAS guards on `expires_at`.
271
271
 
272
272
  **Generated messaging requirement:** All user-facing copy in the guardian timeout/expiry path is generated through the `guardian-action-message-composer.ts` composition system, which uses a 2-tier priority chain: (1) daemon-injected LLM generator for natural, varied text; (2) deterministic fallback templates for reliability. No hardcoded user-facing strings exist in the flow files outside of internal log messages and LLM-instruction prompts. A guard test (`guardian-action-no-hardcoded-copy.test.ts`) enforces this invariant.
273
273
 
274
274
  **Key source files:**
275
275
 
276
- | File | Purpose |
277
- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
278
- | `src/channels/gateway-guardian-requests.ts` | Gateway-backed guardian-request client: typed wrappers over the `guardian_requests_*` IPC routes that own request/delivery state and expiry |
279
- | `src/runtime/routes/guardian-expiry-sweep.ts` | Periodic 60s sweep: gateway CAS-expiry (`guardian_requests_sweep_expired`) plus card-withdrawal, requester-notice, and pending-interaction fan-out |
280
- | `src/calls/call-controller.ts` | Voice timeout handling: expires the request, sends expiry notices, injects `[GUARDIAN_TIMEOUT]` instruction for the generated voice response |
281
- | `src/calls/guardian-action-sweep.ts` | Expiry notice delivery to guardian destinations (vellum conversation message or direct channel reply) |
282
- | `src/runtime/guardian-action-message-composer.ts` | 2-tier text generation: daemon-injected LLM generator with deterministic fallback templates |
276
+ | File | Purpose |
277
+ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
278
+ | `src/channels/gateway-guardian-requests.ts` | Gateway-backed guardian-request client: typed wrappers over the `guardian_requests_*` IPC routes that own request/delivery state and expiry |
279
+ | `src/runtime/routes/guardian-expiry-sweep.ts` | Periodic 60s sweep: bounded expired-pending list, per-row card-withdrawal, requester-notice, and pending-interaction fan-out, then per-request expire CAS |
280
+ | `src/calls/call-controller.ts` | Voice timeout handling: expires the request, sends expiry notices, injects `[GUARDIAN_TIMEOUT]` instruction for the generated voice response |
281
+ | `src/calls/guardian-action-sweep.ts` | Expiry notice delivery to guardian destinations (vellum conversation message or direct channel reply) |
282
+ | `src/runtime/guardian-action-message-composer.ts` | 2-tier text generation: daemon-injected LLM generator with deterministic fallback templates |
283
283
 
284
284
  ### WhatsApp Channel (Meta Cloud API)
285
285
 
package/Dockerfile CHANGED
@@ -117,6 +117,7 @@ RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
117
117
  COPY --from=builder /app /app
118
118
 
119
119
  # Copy source after installing dependencies for better layer caching.
120
+ COPY packages/avatar-catalog /app/packages/avatar-catalog
120
121
  COPY packages/avatar-manifest /app/packages/avatar-manifest
121
122
  COPY packages/ces-client /app/packages/ces-client
122
123
  COPY packages/service-contracts /app/packages/service-contracts
@@ -68,12 +68,12 @@ requested -> pending_guardian -> verification_pending -> active | denied | expir
68
68
 
69
69
  Identity binding ensures the verification code can only be consumed by the intended recipient on the intended channel. The binding fields are set on the gateway `channel_verification_sessions` record when the session is created; the check itself is `checkIdentityMatch` (`gateway/src/verification/identity-match.ts`).
70
70
 
71
- | Channel | Identity fields | Binding behavior |
72
- | --------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
73
- | Telegram | `expectedExternalUserId` = Telegram user ID, `expectedChatId` = Telegram chat ID | Both are taken from the original denied message's metadata. The `identityBindingStatus` is `'bound'`. Verification requires the actor's user ID or chat ID to match (user ID required when both are set on a shared chat). |
74
- | Voice | `expectedPhoneE164` = phone number in E.164 format | Phone-based identity binding. Verification requires the caller's number to match the expected phone. |
71
+ | Channel | Identity fields | Binding behavior |
72
+ | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | Telegram | `expectedExternalUserId` = Telegram user ID, `expectedChatId` = Telegram chat ID | Both are taken from the original denied message's metadata. The `identityBindingStatus` is `'bound'`. Verification requires the actor's user ID or chat ID to match (user ID required when both are set on a shared chat). |
74
+ | Voice | `expectedPhoneE164` = phone number in E.164 format | Phone-based identity binding. Verification requires the caller's number to match the expected phone. |
75
75
  | Discord | `expectedExternalUserId` = Discord user snowflake. `expectedChatId` is unset on the guardian-initiated and trusted-contact paths, and is the originating guild channel on the access-request approval path (`mint_outbound_session`), which sets it for every channel alike. | The code is delivered to a DM and answered there, but the DM channel does not exist until it is opened, so those two paths have no chat id to record at mint time. Where the approval path does set one, it is a room rather than an identity and never becomes a sufficient credential on its own: `checkIdentityMatch` requires the `expectedExternalUserId` match whenever both fields are set. That path also writes the room into `destinationAddress`, where it describes nothing the code is ever sent to (see LUM-3110). |
76
- | Bootstrap | none yet (`identityBindingStatus: 'pending_bootstrap'`) | Unbound deep-link sessions use a 32-byte hex token instead of a numeric code; the identity is bound when the token is redeemed (`verification_sessions_bind_identity`). |
76
+ | Bootstrap | none yet (`identityBindingStatus: 'pending_bootstrap'`) | Unbound deep-link sessions use a 32-byte hex token instead of a numeric code; the identity is bound when the token is redeemed (`verification_sessions_bind_identity`). |
77
77
 
78
78
  **Anti-oracle invariant:** When identity verification fails, the error message is identical to the "invalid or expired code" message. This prevents attackers from distinguishing between a wrong code and a wrong identity, which would leak information about which identities have pending sessions.
79
79
 
@@ -129,10 +129,10 @@ Separately, the admitted-mode introduction **nudge** (for a contact who cleared
129
129
 
130
130
  ### Stage: `expired`
131
131
 
132
- | Store | Table | Record |
133
- | ------------------------------------------------ | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
134
- | gateway `guardian-request-store.ts` (gateway DB) | `guardian_requests` | Updated to `status: 'expired'` via `guardian_requests_sweep_expired` (the daemon's sweep runs every 60s). |
135
- | `session-store.ts` (gateway DB) | `channel_verification_sessions` | Expires naturally: `expiresAt < Date.now()` makes it invisible to `findPendingSessionByHash()`. |
132
+ | Store | Table | Record |
133
+ | ------------------------------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
134
+ | gateway `guardian-request-store.ts` (gateway DB) | `guardian_requests` | Updated to `status: 'expired'` via the per-request `guardian_requests_expire` CAS, after the daemon's 60s sweep has run that row's side effects. |
135
+ | `session-store.ts` (gateway DB) | `channel_verification_sessions` | Expires naturally: `expiresAt < Date.now()` makes it invisible to `findPendingSessionByHash()`. |
136
136
 
137
137
  ### Invites (alternative path)
138
138
 
@@ -216,10 +216,11 @@ sequenceDiagram
216
216
 
217
217
  else Guardian never responds
218
218
  Note over A: runGuardianExpirySweep()<br/>runs every 60 seconds
219
- A->>GW: guardian_requests_sweep_expired (IPC)
220
- GW->>GW: Approval TTL elapsed → status: 'expired' (CAS)
219
+ A->>GW: guardian_requests_list_expired_pending (IPC)
221
220
  A->>A: Withdraw approval cards on all surfaces
222
221
  A-->>U: "Your access request has expired."
222
+ A->>GW: guardian_requests_expire (IPC)
223
+ GW->>GW: status: 'expired' (CAS, the fan-out's receipt)
223
224
 
224
225
  else Code expires (requester never enters it)
225
226
  Note over GW: Verification session TTL: 10 min
@@ -232,7 +233,7 @@ sequenceDiagram
232
233
 
233
234
  ### Guardian never responds
234
235
 
235
- - The `runGuardianExpirySweep()` timer (`runtime/routes/guardian-expiry-sweep.ts`) runs every 60 seconds; the gateway CAS-transitions pending requests past their `expiresAt` to `status: 'expired'` (`guardian_requests_sweep_expired`) and returns the expired rows.
236
+ - The `runGuardianExpirySweep()` timer (`runtime/routes/guardian-expiry-sweep.ts`) runs every 60 seconds; the gateway returns a bounded, read-only batch of pending requests past their `expiresAt` (`guardian_requests_list_expired_pending`), and the daemon confirms each row with the per-request `guardian_requests_expire` CAS only after running its side effects, so the fan-out is recoverable from state.
236
237
  - The daemon then withdraws the now-stale approval cards on every surface and notifies the requester that the request expired.
237
238
 
238
239
  ### Verification code expires
package/knip.json CHANGED
@@ -15,6 +15,7 @@
15
15
  "ignore": ["src/config/preloaded-apps/**"],
16
16
  "ignoreDependencies": [
17
17
  "@microsoft/api-extractor",
18
+ "@vellumai/avatar-catalog",
18
19
  "@vellumai/avatar-manifest",
19
20
  "@vellumai/ces-client",
20
21
  "@vellumai/credential-storage",
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@vellumai/avatar-catalog",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./colors": "./src/colors.ts"
10
+ },
11
+ "scripts": {
12
+ "typecheck": "bunx tsc --noEmit",
13
+ "test": "bun test src/"
14
+ },
15
+ "devDependencies": {
16
+ "@types/bun": "1.3.11",
17
+ "typescript": "5.9.3"
18
+ }
19
+ }
@@ -1,43 +1,18 @@
1
- interface BodyShapeDefinition {
2
- id: string;
3
- viewBox: { width: number; height: number };
4
- faceCenter: { x: number; y: number };
5
- svgPath: string;
6
- }
7
-
8
- interface EyePathDefinition {
9
- svgPath: string;
10
- color: string; // hex e.g. "#F2F2F2"
11
- }
12
-
13
- interface EyeStyleDefinition {
14
- id: string;
15
- sourceViewBox: { width: number; height: number };
16
- eyeCenter: { x: number; y: number };
17
- paths: EyePathDefinition[];
18
- }
19
-
20
- interface ColorDefinition {
21
- id: string;
22
- hex: string; // e.g. "#4C9B50"
23
- }
24
-
25
- interface FaceCenterOverride {
26
- bodyShape: string;
27
- eyeStyle: string;
28
- faceCenter: { x: number; y: number };
29
- }
30
-
31
- interface CharacterComponents {
32
- bodyShapes: BodyShapeDefinition[];
33
- eyeStyles: EyeStyleDefinition[];
34
- colors: ColorDefinition[];
35
- faceCenterOverrides: FaceCenterOverride[];
36
- }
1
+ import { AVATAR_COLORS } from "./colors.js";
2
+ import type { CharacterComponents } from "./types.js";
37
3
 
38
4
  const SCLERA = "#F2F2F2";
39
5
  const PUPIL = "#1A1A1A";
40
6
 
7
+ /**
8
+ * Every body shape, eye style, and face-centre override an avatar can be
9
+ * composed from, plus the palette re-exposed on `colors`.
10
+ *
11
+ * This is the single definition. The daemon serves it over
12
+ * `/avatar/character-components`; clients that must compose an avatar before
13
+ * the daemon answers (a hatching screen, a chooser card) import it directly
14
+ * so no copy has to be kept in step with this one.
15
+ */
41
16
  export function getCharacterComponents(): CharacterComponents {
42
17
  return {
43
18
  bodyShapes: [
@@ -369,14 +344,7 @@ export function getCharacterComponents(): CharacterComponents {
369
344
  },
370
345
  ],
371
346
 
372
- colors: [
373
- { id: "green", hex: "#4C9B50" },
374
- { id: "orange", hex: "#E9642F" },
375
- { id: "pink", hex: "#DB4B77" },
376
- { id: "purple", hex: "#A665C9" },
377
- { id: "teal", hex: "#0E9B8B" },
378
- { id: "yellow", hex: "#E9C91A" },
379
- ],
347
+ colors: AVATAR_COLORS,
380
348
 
381
349
  faceCenterOverrides: [
382
350
  // Ghost body — shift non-native eyes from y=167 -> y=200
@@ -0,0 +1,19 @@
1
+ import type { ColorDefinition } from "./types.js";
2
+
3
+ /**
4
+ * The avatar colour palette, in the order the picker offers it.
5
+ *
6
+ * Its own module because the palette is the one part of the catalog that
7
+ * boot-path code needs: a sidebar pin resolves a stored colour id on first
8
+ * render, and the accent CSS variable falls back to the first entry before
9
+ * the daemon's own catalog arrives. Importing the shapes to reach six hex
10
+ * values would put ~47 kB of SVG path data on that path.
11
+ */
12
+ export const AVATAR_COLORS: ColorDefinition[] = [
13
+ { id: "green", hex: "#4C9B50" },
14
+ { id: "orange", hex: "#E9642F" },
15
+ { id: "pink", hex: "#DB4B77" },
16
+ { id: "purple", hex: "#A665C9" },
17
+ { id: "teal", hex: "#0E9B8B" },
18
+ { id: "yellow", hex: "#E9C91A" },
19
+ ];
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @vellumai/avatar-catalog: the single definition of the components an
3
+ * assistant's character avatar is composed from.
4
+ *
5
+ * The daemon serves this catalog over `/avatar/character-components` and
6
+ * renders PNGs from it; clients bundle it because an avatar has to compose
7
+ * before the daemon can answer (the hatching screen, the assistant chooser,
8
+ * a transcript's subagent chips). Bundling is why this is a package rather
9
+ * than a daemon module every client copies.
10
+ *
11
+ * The palette is deliberately not re-exported here. It lives behind
12
+ * `@vellumai/avatar-catalog/colors`, and importing it from that subpath is
13
+ * what keeps boot-path code (a sidebar pin resolving a stored colour id, the
14
+ * accent variable's fallback) off the ~47 kB of SVG path data the shapes
15
+ * carry. Re-exporting it would make the expensive import the convenient one.
16
+ *
17
+ * The shapes' types stay internal for the same reason they were private to
18
+ * the daemon module this package replaces: the wire contract for this data is
19
+ * the route's `responseBody` schema, which clients consume through their
20
+ * generated OpenAPI types. A hand-written type exported from here would be a
21
+ * second name for that shape, and the closer one to reach for.
22
+ *
23
+ * Leaf package: no dependencies, no runtime imports.
24
+ */
25
+ export { getCharacterComponents } from "./catalog.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shapes for the catalog literal in this package.
3
+ *
4
+ * Declared here rather than derived from the generated OpenAPI types because
5
+ * this is a leaf package with no dependencies, and the clients that consume
6
+ * those generated types depend on it rather than the other way round. The
7
+ * canonical wire shape is the `avatar/character-components` route's
8
+ * `responseBody` schema; these are not exported so no consumer picks them up
9
+ * as a second name for it.
10
+ */
11
+
12
+ export interface BodyShapeDefinition {
13
+ id: string;
14
+ viewBox: { width: number; height: number };
15
+ faceCenter: { x: number; y: number };
16
+ svgPath: string;
17
+ }
18
+
19
+ export interface EyePathDefinition {
20
+ svgPath: string;
21
+ color: string; // hex e.g. "#F2F2F2"
22
+ }
23
+
24
+ export interface EyeStyleDefinition {
25
+ id: string;
26
+ sourceViewBox: { width: number; height: number };
27
+ eyeCenter: { x: number; y: number };
28
+ paths: EyePathDefinition[];
29
+ }
30
+
31
+ export interface ColorDefinition {
32
+ id: string;
33
+ hex: string; // e.g. "#4C9B50"
34
+ }
35
+
36
+ export interface FaceCenterOverride {
37
+ bodyShape: string;
38
+ eyeStyle: string;
39
+ faceCenter: { x: number; y: number };
40
+ }
41
+
42
+ export interface CharacterComponents {
43
+ bodyShapes: BodyShapeDefinition[];
44
+ eyeStyles: EyeStyleDefinition[];
45
+ colors: ColorDefinition[];
46
+ faceCenterOverrides: FaceCenterOverride[];
47
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src",
16
+ "types": ["bun"]
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -31,8 +31,7 @@ import {
31
31
  ListGuardianRequestsIpcParamsSchema,
32
32
  ListPendingGuardianRequestsByDestinationIpcParamsSchema,
33
33
  ListPendingGuardianRequestsByScopeIpcParamsSchema,
34
- SweepExpiredGuardianRequestsIpcParamsSchema,
35
- SweepExpiredGuardianRequestsIpcResponseSchema,
34
+ ListExpiredPendingGuardianRequestsIpcParamsSchema,
36
35
  UpdateGuardianRequestIpcParamsSchema,
37
36
  type GuardianRequestWire,
38
37
  } from "../guardian-request-contract.js";
@@ -439,29 +438,20 @@ describe("decide IPC schemas", () => {
439
438
  });
440
439
 
441
440
  describe("expiry IPC schemas", () => {
442
- test("expire and sweep round-trip", () => {
441
+ test("expire and expired-pending list round-trip", () => {
443
442
  expect(ExpireGuardianRequestIpcParamsSchema.parse({ id: "req-1" })).toEqual(
444
443
  { id: "req-1" },
445
444
  );
446
445
 
447
446
  expect(
448
- SweepExpiredGuardianRequestsIpcParamsSchema.parse({
447
+ ListExpiredPendingGuardianRequestsIpcParamsSchema.parse({
449
448
  now: 1_700_000_000_000,
449
+ limit: 50,
450
450
  }),
451
- ).toEqual({ now: 1_700_000_000_000 });
452
- expect(SweepExpiredGuardianRequestsIpcParamsSchema.parse({})).toEqual({});
453
- // The sweep returns full rows so the daemon fan-out never re-reads.
454
- const expiredRow = { ...accessRequest, status: "expired" as const };
455
- expect(
456
- SweepExpiredGuardianRequestsIpcResponseSchema.parse({
457
- expired: [expiredRow],
458
- }),
459
- ).toEqual({ expired: [expiredRow] });
460
- expect(() =>
461
- SweepExpiredGuardianRequestsIpcResponseSchema.parse({
462
- expired: ["req-1"],
463
- }),
464
- ).toThrow();
451
+ ).toEqual({ now: 1_700_000_000_000, limit: 50 });
452
+ expect(ListExpiredPendingGuardianRequestsIpcParamsSchema.parse({})).toEqual(
453
+ {},
454
+ );
465
455
 
466
456
  expect(
467
457
  ExpireInteractionBoundIpcResponseSchema.parse({ expired: 3 }),
@@ -189,7 +189,7 @@ export const GUARDIAN_REQUESTS_IPC_METHODS = {
189
189
  decide: "guardian_requests_decide",
190
190
  expire: "guardian_requests_expire",
191
191
  expireInteractionBound: "guardian_requests_expire_interaction_bound",
192
- sweepExpired: "guardian_requests_sweep_expired",
192
+ listExpiredPending: "guardian_requests_list_expired_pending",
193
193
  createDelivery: "guardian_requests_create_delivery",
194
194
  updateDelivery: "guardian_requests_update_delivery",
195
195
  listDeliveries: "guardian_requests_list_deliveries",
@@ -201,6 +201,32 @@ export const GUARDIAN_REQUESTS_IPC_METHODS = {
201
201
  getByPendingQuestion: "guardian_requests_get_by_pending_question",
202
202
  } as const;
203
203
 
204
+ /**
205
+ * The delivery-row statuses this codebase writes and matches on.
206
+ *
207
+ * `GuardianRequestDeliverySchema.status` stays a plain `z.string()` rather
208
+ * than an enum built from these: the column is persisted, so a row written
209
+ * by a different build must still read rather than throw on parse. These
210
+ * constants exist so a call site names a value instead of spelling it, not
211
+ * to close the set.
212
+ *
213
+ * `pending` is the creation default; `sent` and `failed` carry the
214
+ * notification delivery result the recorder maps in; `expired` is stamped
215
+ * when the request itself expires; `withdrawn` is the daemon's per-surface
216
+ * receipt that a card was durably withdrawn, which the gateway's
217
+ * per-request expire preserves and a retrying withdrawal skips.
218
+ */
219
+ export const DELIVERY_STATUS = {
220
+ pending: "pending",
221
+ sent: "sent",
222
+ failed: "failed",
223
+ expired: "expired",
224
+ withdrawn: "withdrawn",
225
+ } as const;
226
+
227
+ export type DeliveryStatus =
228
+ (typeof DELIVERY_STATUS)[keyof typeof DELIVERY_STATUS];
229
+
204
230
  export type GuardianRequestsIpcMethod =
205
231
  (typeof GUARDIAN_REQUESTS_IPC_METHODS)[keyof typeof GUARDIAN_REQUESTS_IPC_METHODS];
206
232
 
@@ -418,7 +444,7 @@ const OUTCOME_TYPES_BY_DECISION_STATUS: Record<
418
444
  /**
419
445
  * Request for `guardian_requests_decide` (status CAS + optional ACL outcome).
420
446
  * Decisions only resolve a pending request to approved/denied — expiry has
421
- * `guardian_requests_expire`/`_sweep_expired` — so a malformed call can never
447
+ * `guardian_requests_expire`/`_list_expired_pending`, so a malformed call can never
422
448
  * apply an `aclOutcome` while leaving the request decidable again. The
423
449
  * outcome type must agree with the status: activation/minting only on
424
450
  * approval, seeding/blocking only on denial.
@@ -508,26 +534,22 @@ export type ExpireInteractionBoundIpcResponse = z.infer<
508
534
  typeof ExpireInteractionBoundIpcResponseSchema
509
535
  >;
510
536
 
511
- /** Request for `guardian_requests_sweep_expired` (`now` defaults gateway-side). */
512
- export const SweepExpiredGuardianRequestsIpcParamsSchema = z.object({
513
- now: z.number().optional(),
514
- });
515
-
516
- export type SweepExpiredGuardianRequestsIpcParams = z.infer<
517
- typeof SweepExpiredGuardianRequestsIpcParamsSchema
518
- >;
519
-
520
537
  /**
521
- * Response for `guardian_requests_sweep_expired`: the full expired rows, so
522
- * the daemon's card-withdrawal/notification fan-out never needs a follow-up
523
- * read that could fail after the status flip and strand the side effects.
538
+ * Request for `guardian_requests_list_expired_pending` (`now` defaults
539
+ * gateway-side; `limit` bounds the batch and is capped gateway-side).
540
+ * Read-only: the rows stay `pending` until the daemon has run each one's
541
+ * expiry side effects and confirms with `guardian_requests_expire`, so a
542
+ * lost response leaves the work discoverable by the next sweep round
543
+ * instead of silently done. Past-deadline pending rows are undecidable
544
+ * either way: every decision path checks `expiresAt` before any write.
524
545
  */
525
- export const SweepExpiredGuardianRequestsIpcResponseSchema = z.object({
526
- expired: z.array(GuardianRequestSchema),
546
+ export const ListExpiredPendingGuardianRequestsIpcParamsSchema = z.object({
547
+ now: z.number().optional(),
548
+ limit: z.number().optional(),
527
549
  });
528
550
 
529
- export type SweepExpiredGuardianRequestsIpcResponse = z.infer<
530
- typeof SweepExpiredGuardianRequestsIpcResponseSchema
551
+ export type ListExpiredPendingGuardianRequestsIpcParams = z.infer<
552
+ typeof ListExpiredPendingGuardianRequestsIpcParamsSchema
531
553
  >;
532
554
 
533
555
  // ---------------------------------------------------------------------------
@@ -268,8 +268,8 @@ export {
268
268
  ListGuardianRequestsIpcParamsSchema,
269
269
  ListPendingGuardianRequestsByDestinationIpcParamsSchema,
270
270
  ListPendingGuardianRequestsByScopeIpcParamsSchema,
271
- SweepExpiredGuardianRequestsIpcParamsSchema,
272
- SweepExpiredGuardianRequestsIpcResponseSchema,
271
+ DELIVERY_STATUS,
272
+ ListExpiredPendingGuardianRequestsIpcParamsSchema,
273
273
  UpdateGuardianRequestDeliveryIpcParamsSchema,
274
274
  UpdateGuardianRequestIpcParamsSchema,
275
275
  } from "./guardian-request-contract.js";
@@ -279,6 +279,7 @@ export type {
279
279
  CreateGuardianRequestIpcParams,
280
280
  DecideGuardianRequestIpcParams,
281
281
  DecideGuardianRequestIpcResponse,
282
+ DeliveryStatus,
282
283
  ExpireGuardianRequestIpcParams,
283
284
  ExpireInteractionBoundIpcParams,
284
285
  ExpireInteractionBoundIpcResponse,
@@ -306,8 +307,7 @@ export type {
306
307
  ListGuardianRequestsIpcParams,
307
308
  ListPendingGuardianRequestsByDestinationIpcParams,
308
309
  ListPendingGuardianRequestsByScopeIpcParams,
309
- SweepExpiredGuardianRequestsIpcParams,
310
- SweepExpiredGuardianRequestsIpcResponse,
310
+ ListExpiredPendingGuardianRequestsIpcParams,
311
311
  UpdateGuardianRequestDeliveryIpcParams,
312
312
  UpdateGuardianRequestIpcParams,
313
313
  } from "./guardian-request-contract.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.7-dev.202608282003.8ce5421",
3
+ "version": "0.11.7-dev.202608282312.37594b7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -43,6 +43,7 @@
43
43
  "@qdrant/js-client-rest": "1.17.0",
44
44
  "@resvg/resvg-js": "2.6.2",
45
45
  "@slack/types": "2.22.0",
46
+ "@vellumai/avatar-catalog": "file:../packages/avatar-catalog",
46
47
  "@vellumai/avatar-manifest": "file:../packages/avatar-manifest",
47
48
  "@vellumai/ces-client": "file:../packages/ces-client",
48
49
  "@vellumai/credential-storage": "file:../packages/credential-storage",
@@ -87,6 +88,7 @@
87
88
  "node": ">=20.12.0"
88
89
  },
89
90
  "bundledDependencies": [
91
+ "@vellumai/avatar-catalog",
90
92
  "@vellumai/avatar-manifest",
91
93
  "@vellumai/ces-client",
92
94
  "@vellumai/credential-storage",