@vellumai/assistant 0.11.2-dev.202608060701.ede4331 → 0.11.2-dev.202608062308.5442cf1

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 (152) hide show
  1. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  2. package/node_modules/@vellumai/environments/package.json +2 -1
  3. package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  4. package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  5. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  6. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
  7. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
  8. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +2 -0
  9. package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +15 -1
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +17 -2
  12. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  13. package/openapi.yaml +81 -4
  14. package/package.json +1 -1
  15. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  16. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  17. package/src/__tests__/canned-reply-release.test.ts +86 -0
  18. package/src/__tests__/channel-readiness-telegram-remote.test.ts +154 -0
  19. package/src/__tests__/client-os-metadata-persistence.test.ts +4 -4
  20. package/src/__tests__/conversation-agent-loop.test.ts +130 -1
  21. package/src/__tests__/conversation-fork-referential.test.ts +352 -0
  22. package/src/__tests__/conversation-key-store-origin.test.ts +105 -0
  23. package/src/__tests__/conversation-origin-at-creation.test.ts +153 -0
  24. package/src/__tests__/conversation-queue.test.ts +80 -0
  25. package/src/__tests__/conversation-retry-route.test.ts +2 -0
  26. package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
  27. package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
  28. package/src/__tests__/credential-prompt-route.test.ts +57 -7
  29. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  30. package/src/__tests__/document-sync-tags.test.ts +379 -0
  31. package/src/__tests__/document-update-default-surface.test.ts +28 -0
  32. package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
  33. package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
  34. package/src/__tests__/list-messages-queued.test.ts +43 -0
  35. package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
  36. package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
  37. package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
  38. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
  39. package/src/__tests__/tool-side-effects-documents.test.ts +180 -0
  40. package/src/__tests__/turn-tail-chain.test.ts +135 -0
  41. package/src/api/constants/document-tools.ts +55 -0
  42. package/src/api/index.ts +7 -0
  43. package/src/channels/config.ts +7 -2
  44. package/src/channels/types.ts +7 -1
  45. package/src/cli/commands/credentials.help.ts +7 -0
  46. package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
  47. package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
  48. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  49. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
  50. package/src/config/feature-flag-registry.json +1 -1
  51. package/src/config/sanitize-for-transfer.ts +11 -0
  52. package/src/config/schema.ts +4 -0
  53. package/src/config/schemas/channels.ts +13 -0
  54. package/src/config/schemas/memory-retrospective.ts +25 -0
  55. package/src/config/schemas/plugin-updates.ts +55 -0
  56. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
  57. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
  58. package/src/daemon/conversation-agent-loop-handlers.ts +3 -4
  59. package/src/daemon/conversation-agent-loop.ts +214 -144
  60. package/src/daemon/conversation-lifecycle.ts +40 -20
  61. package/src/daemon/conversation-messaging.ts +17 -16
  62. package/src/daemon/conversation-process.ts +36 -9
  63. package/src/daemon/conversation-runtime-assembly.ts +2 -2
  64. package/src/daemon/conversation-store.ts +52 -3
  65. package/src/daemon/conversation-turn-finalize.ts +134 -89
  66. package/src/daemon/conversation.ts +66 -22
  67. package/src/daemon/external-plugins-bootstrap.ts +7 -1
  68. package/src/daemon/handlers/conversations.ts +24 -6
  69. package/src/daemon/handlers/skills.ts +10 -0
  70. package/src/daemon/message-types/conversations.ts +6 -7
  71. package/src/daemon/message-types/sync.ts +1 -0
  72. package/src/daemon/tool-side-effects.ts +14 -1
  73. package/src/daemon/turn-tail-chain.ts +73 -0
  74. package/src/ipc/__tests__/socket-path.test.ts +21 -0
  75. package/src/ipc/assistant-server.ts +17 -10
  76. package/src/ipc/cli-client.ts +10 -1
  77. package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
  78. package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
  79. package/src/ipc/socket-cleanup.ts +9 -7
  80. package/src/ipc/socket-path.ts +4 -96
  81. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
  82. package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
  83. package/src/live-voice/live-voice-connection.ts +7 -1
  84. package/src/live-voice/live-voice-photo.ts +169 -0
  85. package/src/live-voice/live-voice-session.ts +48 -3
  86. package/src/live-voice/protocol.ts +66 -1
  87. package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
  88. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
  89. package/src/messaging/providers/callback-routing.ts +1 -0
  90. package/src/messaging/providers/discord/api.ts +222 -0
  91. package/src/messaging/providers/discord/render.test.ts +204 -0
  92. package/src/messaging/providers/discord/render.ts +195 -0
  93. package/src/messaging/providers/discord/send.test.ts +179 -0
  94. package/src/messaging/providers/discord/send.ts +212 -0
  95. package/src/messaging/providers/discord/transport.ts +68 -0
  96. package/src/messaging/providers/index.ts +3 -1
  97. package/src/messaging/providers/retry-policy.test.ts +247 -0
  98. package/src/messaging/providers/retry-policy.ts +206 -0
  99. package/src/messaging/providers/telegram-bot/api.ts +47 -130
  100. package/src/messaging/providers/whatsapp/api.ts +47 -136
  101. package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
  102. package/src/monitoring/plugin-auto-update.ts +359 -0
  103. package/src/monitoring/worker.ts +13 -0
  104. package/src/notifications/__tests__/assistant-reply-producer.test.ts +1 -0
  105. package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
  106. package/src/persistence/conversation-crud.ts +222 -66
  107. package/src/persistence/conversation-key-store.ts +15 -1
  108. package/src/persistence/conversation-lineage.ts +226 -0
  109. package/src/persistence/conversation-types.ts +49 -1
  110. package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
  111. package/src/persistence/schema/conversations.ts +10 -0
  112. package/src/persistence/steps.ts +2 -0
  113. package/src/plugin-api/index.ts +7 -0
  114. package/src/plugin-api/webhook-url.ts +151 -0
  115. package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
  116. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
  117. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +17 -0
  118. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +1 -0
  119. package/src/plugins/defaults/memory/jobs-worker.ts +10 -0
  120. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
  121. package/src/plugins/defaults/memory/memory-retrospective-job.ts +14 -0
  122. package/src/plugins/types.ts +3 -3
  123. package/src/runtime/AGENTS.md +1 -1
  124. package/src/runtime/__tests__/agent-wake.test.ts +16 -16
  125. package/src/runtime/agent-wake.ts +58 -24
  126. package/src/runtime/channel-readiness-service.ts +59 -3
  127. package/src/runtime/channel-readiness-types.ts +18 -0
  128. package/src/runtime/routes/__tests__/plugins-routes.test.ts +84 -0
  129. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
  130. package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
  131. package/src/runtime/routes/canned-reply-release.ts +54 -0
  132. package/src/runtime/routes/conversation-list-routes.ts +7 -0
  133. package/src/runtime/routes/conversation-routes.ts +151 -110
  134. package/src/runtime/routes/credential-prompt-routes.ts +23 -2
  135. package/src/runtime/routes/documents-routes.ts +44 -1
  136. package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
  137. package/src/runtime/routes/plugins-routes.ts +50 -0
  138. package/src/runtime/routes/retrospective-routes.ts +9 -3
  139. package/src/runtime/routes/skills-routes.ts +79 -0
  140. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  141. package/src/runtime/services/conversation-serializer.ts +24 -9
  142. package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
  143. package/src/runtime/sync/resource-sync-events.ts +79 -1
  144. package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
  145. package/src/runtime/sync/worker-daemon-notify.ts +44 -6
  146. package/src/skills/skill-history.test.ts +269 -0
  147. package/src/skills/skill-history.ts +250 -0
  148. package/src/telegram/__tests__/webhook-health.test.ts +73 -1
  149. package/src/telegram/webhook-health.ts +101 -3
  150. package/src/telemetry/types.ts +3 -4
  151. package/src/tools/document/document-tool.ts +11 -4
  152. package/src/workspace/git-service.ts +23 -0
@@ -21,6 +21,12 @@
21
21
  * `Date#toISOString()`.
22
22
  */
23
23
 
24
+ /**
25
+ * Pairing-challenge TTL in milliseconds (10 minutes): the gateway's challenge
26
+ * store enforces it and the `vellum pair` CLI renders it in user-facing copy.
27
+ */
28
+ export const REMOTE_WEB_PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
29
+
24
30
  /** `POST /v1/remote-web/pairing-challenge` request body. */
25
31
  export interface RemoteWebPairingChallengeRequest {
26
32
  /** Public https base URL the scanning device can reach the assistant at. */
@@ -5,7 +5,8 @@
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
8
- ".": "./src/index.ts"
8
+ ".": "./src/index.ts",
9
+ "./seeds": "./src/seeds.ts"
9
10
  },
10
11
  "scripts": {
11
12
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pins the environment-to-cloud-hub mapping in one place. Both consumers
3
+ * (the Capacitor shell's `server.url` and the CLI remote-web edge's
4
+ * `hubUrl`) read this helper, so this matrix is the single behavioral pin
5
+ * for the served URLs.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+
10
+ import { cloudAssistantHubUrl, SEEDS } from "../index.js";
11
+
12
+ describe("cloudAssistantHubUrl", () => {
13
+ test("cloud environments resolve to their own web origin", () => {
14
+ expect(cloudAssistantHubUrl("production")).toBe(
15
+ "https://www.vellum.ai/assistant",
16
+ );
17
+ expect(cloudAssistantHubUrl("staging")).toBe(
18
+ "https://staging-assistant.vellum.ai/assistant",
19
+ );
20
+ });
21
+
22
+ test("every other environment falls back to the dev SPA", () => {
23
+ for (const name of ["dev", "test", "local", "unknown-env", undefined]) {
24
+ expect(cloudAssistantHubUrl(name)).toBe(
25
+ "https://dev-assistant.vellum.ai/assistant",
26
+ );
27
+ }
28
+ });
29
+
30
+ test("cloud hub URLs derive from the seed web URLs", () => {
31
+ for (const name of ["production", "staging"]) {
32
+ expect(cloudAssistantHubUrl(name)).toBe(
33
+ `${SEEDS[name].webUrl}/assistant`,
34
+ );
35
+ }
36
+ expect(cloudAssistantHubUrl("local")).toBe(`${SEEDS.dev.webUrl}/assistant`);
37
+ });
38
+ });
@@ -24,6 +24,35 @@ function portBlock(base: number): PortMap {
24
24
  };
25
25
  }
26
26
 
27
+ /**
28
+ * Base URL of the cloud-hosted assistant SPA (the "hub") for a deployment
29
+ * environment. The Capacitor mobile shell bakes this into native builds as
30
+ * `server.url`, and the CLI's remote-web edge stamps it into the served
31
+ * config as `hubUrl`, so the environment-to-hub mapping lives here exactly
32
+ * once.
33
+ *
34
+ * Only the cloud deployments (production, staging) serve the hosted SPA at
35
+ * their own web origin; every other environment (dev, test, local, unknown)
36
+ * falls back to the dev SPA, whose host serves remote clients that cannot
37
+ * reach a local or per-developer web URL.
38
+ *
39
+ * The `/assistant` suffix is deliberate: booting on the bare host lands on
40
+ * the marketing page, whose CTA redirects to `www.vellum.ai/assistant` and
41
+ * bounces non-prod shells off their own host.
42
+ *
43
+ * NOTE: this module is loaded by Capacitor's single-file TS config loader
44
+ * (via the `@vellumai/environments/seeds` subpath), which cannot follow the
45
+ * package index's `.js`-suffixed runtime re-exports. Keep this file's
46
+ * imports type-only so it stays loadable on its own.
47
+ */
48
+ export function cloudAssistantHubUrl(envName: string | undefined): string {
49
+ const seed =
50
+ envName === "production" || envName === "staging"
51
+ ? SEEDS[envName]
52
+ : SEEDS.dev;
53
+ return `${seed.webUrl}/assistant`;
54
+ }
55
+
27
56
  /**
28
57
  * Built-in environment definitions and the source of truth for the
29
58
  * set of known environment names.
@@ -21,6 +21,12 @@
21
21
  * `Date#toISOString()`.
22
22
  */
23
23
 
24
+ /**
25
+ * Pairing-challenge TTL in milliseconds (10 minutes): the gateway's challenge
26
+ * store enforces it and the `vellum pair` CLI renders it in user-facing copy.
27
+ */
28
+ export const REMOTE_WEB_PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
29
+
24
30
  /** `POST /v1/remote-web/pairing-challenge` request body. */
25
31
  export interface RemoteWebPairingChallengeRequest {
26
32
  /** Public https base URL the scanning device can reach the assistant at. */
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { removeIpcEndpointFile, resolveIpcEndpoint } from "./endpoint.js";
6
+ const windowsEndpoint = (name: string, workspaceDir: string) => resolveIpcEndpoint(name, { workspaceDir, platform: "win32" });
7
+ describe("resolveIpcEndpoint", () => {
8
+ test("keeps long POSIX fallbacks deterministic and bounded", () => {
9
+ const options = {
10
+ workspaceDir: `/tmp/${"a".repeat(120)}`,
11
+ platform: "darwin" as const,
12
+ };
13
+ const endpoint = resolveIpcEndpoint("assistant", options);
14
+ expect(resolveIpcEndpoint("assistant", options)).toEqual(endpoint);
15
+ expect(Buffer.byteLength(endpoint.path)).toBeLessThanOrEqual(103);
16
+ });
17
+ test("returns normalized, isolated, bounded Windows pipes", () => {
18
+ const first = windowsEndpoint("assistant", "C:\\one");
19
+ expect(resolveIpcEndpoint("assistant", { workspaceDir: "c:\\ONE\\", platform: "win32", env: { ASSISTANT_IPC_SOCKET_DIR: "C:\\ignored" } })).toEqual(first);
20
+ expect(first.path.length).toBeLessThanOrEqual(256);
21
+ expect(windowsEndpoint("assistant", "C:\\two").path).not.toBe(first.path);
22
+ expect(windowsEndpoint("gateway", "C:\\one").path).not.toBe(first.path);
23
+ });
24
+ test("cleans socket files but skips named pipes", () => {
25
+ const dir = mkdtempSync(join(tmpdir(), "vellum-ipc-cleanup-"));
26
+ const socketPath = join(dir, "assistant.sock");
27
+ try {
28
+ writeFileSync(socketPath, "stale");
29
+ removeIpcEndpointFile(socketPath);
30
+ expect(existsSync(socketPath)).toBe(false);
31
+ removeIpcEndpointFile("\\\\.\\pipe\\vellum-assistant-test");
32
+ } finally {
33
+ rmSync(dir, { recursive: true, force: true });
34
+ }
35
+ });
36
+ });
@@ -0,0 +1,142 @@
1
+ import { createHash } from "node:crypto";
2
+ import { unlinkSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { posix, win32 } from "node:path";
5
+
6
+ const DARWIN_UNIX_SOCKET_MAX_PATH_BYTES = 103;
7
+ const DEFAULT_UNIX_SOCKET_MAX_PATH_BYTES = 107;
8
+ const IPC_TMP_DIR_NAME = "vellum-ipc";
9
+ const join = posix.join;
10
+
11
+ export type IpcSocketPathSource =
12
+ | "env-override"
13
+ | "workspace"
14
+ | "tmp-hash"
15
+ | "tmp-short-hash"
16
+ | "windows-named-pipe";
17
+
18
+ export interface IpcSocketPathResolution {
19
+ path: string;
20
+ source: IpcSocketPathSource;
21
+ }
22
+
23
+ export interface IpcEndpointOptions {
24
+ workspaceDir: string;
25
+ env?: Record<string, string | undefined>;
26
+ platform?: NodeJS.Platform;
27
+ }
28
+
29
+ function getUnixSocketMaxPathBytes(platform: NodeJS.Platform): number {
30
+ return platform === "darwin"
31
+ ? DARWIN_UNIX_SOCKET_MAX_PATH_BYTES
32
+ : DEFAULT_UNIX_SOCKET_MAX_PATH_BYTES;
33
+ }
34
+
35
+ function isPathWithinSocketLimit(path: string, maxPathBytes: number): boolean {
36
+ return Buffer.byteLength(path, "utf8") <= maxPathBytes;
37
+ }
38
+
39
+ /**
40
+ * Derive the env var name and socket filename from a socket name.
41
+ *
42
+ * Examples (hyphens in the name become underscores in the env var):
43
+ */
44
+ function deriveSocketNames(socketName: string): {
45
+ envVar: string;
46
+ fileName: string;
47
+ } {
48
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(socketName)) {
49
+ throw new Error(`Invalid IPC endpoint name: ${socketName}`);
50
+ }
51
+ const envVar = `${socketName.toUpperCase().replace(/-/g, "_")}_IPC_SOCKET_DIR`;
52
+ const fileName = `${socketName}.sock`;
53
+ return { envVar, fileName };
54
+ }
55
+
56
+ function resolveWindowsNamedPipe(
57
+ socketName: string,
58
+ workspaceDir: string,
59
+ ): IpcSocketPathResolution {
60
+ const workspaceIdentity = win32
61
+ .normalize(workspaceDir)
62
+ .replace(/\\+$/, "")
63
+ .toLowerCase();
64
+ const hash = createHash("sha256")
65
+ .update(`${workspaceIdentity}\0${socketName}`)
66
+ .digest("hex")
67
+ .slice(0, 24);
68
+ return {
69
+ path: `\\\\.\\pipe\\vellum-${socketName}-${hash}`,
70
+ source: "windows-named-pipe",
71
+ };
72
+ }
73
+
74
+ export function isNamedPipePath(endpointPath: string): boolean {
75
+ return /^\\\\[.?]\\pipe\\/.test(endpointPath);
76
+ }
77
+
78
+ export function removeIpcEndpointFile(endpointPath: string): void {
79
+ if (isNamedPipePath(endpointPath)) {
80
+ return;
81
+ }
82
+ try {
83
+ unlinkSync(endpointPath);
84
+ } catch {
85
+ // Already absent.
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Resolve the path to an IPC socket file.
91
+ *
92
+ * Resolution order:
93
+ * POSIX uses overrides, workspace paths, then bounded temporary paths.
94
+ * Windows uses deterministic named pipes.
95
+ */
96
+ export function resolveIpcEndpoint(
97
+ socketName: string,
98
+ options: IpcEndpointOptions,
99
+ ): IpcSocketPathResolution {
100
+ const { envVar, fileName } = deriveSocketNames(socketName);
101
+ const platform = options.platform ?? process.platform;
102
+ if (platform === "win32") {
103
+ return resolveWindowsNamedPipe(socketName, options.workspaceDir);
104
+ }
105
+
106
+ // Explicit override via env var.
107
+ const envSocketDir = (options.env ?? process.env)[envVar]?.trim();
108
+ if (envSocketDir) {
109
+ return {
110
+ path: join(envSocketDir, fileName),
111
+ source: "env-override",
112
+ };
113
+ }
114
+
115
+ const maxPathBytes = getUnixSocketMaxPathBytes(platform);
116
+ const workspacePath = join(options.workspaceDir, fileName);
117
+
118
+ if (isPathWithinSocketLimit(workspacePath, maxPathBytes)) {
119
+ return {
120
+ path: workspacePath,
121
+ source: "workspace",
122
+ };
123
+ }
124
+
125
+ // Workspace path exceeds AF_UNIX limit - fall back to tmpdir.
126
+ const hash = createHash("sha256")
127
+ .update(workspacePath)
128
+ .digest("hex")
129
+ .slice(0, 12);
130
+ const hashedPath = join(tmpdir(), IPC_TMP_DIR_NAME, `${hash}-${fileName}`);
131
+ if (isPathWithinSocketLimit(hashedPath, maxPathBytes)) {
132
+ return {
133
+ path: hashedPath,
134
+ source: "tmp-hash",
135
+ };
136
+ }
137
+
138
+ return {
139
+ path: join(tmpdir(), `v-${hash}.sock`),
140
+ source: "tmp-short-hash",
141
+ };
142
+ }
@@ -4,6 +4,8 @@ export {
4
4
  type SocketWatchdogOptions,
5
5
  type SocketWatchdogLogger,
6
6
  } from "./socket-watchdog.js";
7
+ export { isNamedPipePath, removeIpcEndpointFile, resolveIpcEndpoint } from "./endpoint.js";
8
+ export { ipcListenOptions } from "./listen-options.js";
7
9
  export {
8
10
  IpcFrameReader,
9
11
  writeLegacyMessage,
@@ -0,0 +1,3 @@
1
+ export function ipcListenOptions(path: string) {
2
+ return { path, readableAll: false, writableAll: false } as const;
3
+ }
@@ -46,6 +46,7 @@ interface TestHarness {
46
46
 
47
47
  interface BuildOptions {
48
48
  intervalMs?: number;
49
+ socketPathOverride?: string;
49
50
  createServerOverride?: () => Server;
50
51
  /** Override `getServer` to simulate races. */
51
52
  getServerOverride?: () => Server | null;
@@ -75,7 +76,7 @@ function buildHarness(opts: BuildOptions): TestHarness {
75
76
  };
76
77
 
77
78
  const watchdog = new SocketWatchdog({
78
- socketPath,
79
+ socketPath: opts.socketPathOverride ?? socketPath,
79
80
  intervalMs: opts.intervalMs ?? 0,
80
81
  getServer: opts.getServerOverride ?? (() => serverRef.current),
81
82
  createServer: opts.createServerOverride ?? defaultFactory,
@@ -190,6 +191,19 @@ describe("SocketWatchdog", () => {
190
191
  expect(harness.rebinds).toHaveLength(0);
191
192
  });
192
193
 
194
+ test("named pipes skip filesystem watchdog behavior", async () => {
195
+ const server = createServer();
196
+ harness = buildHarness({
197
+ socketPathOverride: "\\\\.\\pipe\\vellum-assistant-test",
198
+ getServerOverride: () => server,
199
+ createServerOverride: () => {
200
+ throw new Error("must not rebind");
201
+ },
202
+ });
203
+ harness.spawnedServers.push(server);
204
+ expect(await harness.watchdog.rebindIfMissing()).toBe(false);
205
+ });
206
+
193
207
  test("rebindIfMissing recreates the listener when the path is gone", async () => {
194
208
  harness = buildHarness({});
195
209
  const initial = await startInitialServer(harness);
@@ -16,6 +16,9 @@ import { existsSync, mkdirSync, unlinkSync } from "node:fs";
16
16
  import type { Server } from "node:net";
17
17
  import { dirname } from "node:path";
18
18
 
19
+ import { isNamedPipePath } from "./endpoint.js";
20
+ import { ipcListenOptions } from "./listen-options.js";
21
+
19
22
  /**
20
23
  * Minimal logger surface (pino-compatible). Each method receives a context
21
24
  * object plus an optional human-readable message.
@@ -66,6 +69,9 @@ const DEFAULT_INTERVAL_MS = 5000;
66
69
  * `mkdir` only applies the mode to directories it creates.
67
70
  */
68
71
  export function ensureSocketDir(socketPath: string): void {
72
+ if (isNamedPipePath(socketPath)) {
73
+ return;
74
+ }
69
75
  const socketDir = dirname(socketPath);
70
76
  if (!existsSync(socketDir)) {
71
77
  mkdirSync(socketDir, { recursive: true, mode: 0o700 });
@@ -109,7 +115,13 @@ export class SocketWatchdog {
109
115
  * watchdog is already running.
110
116
  */
111
117
  start(): void {
112
- if (this.intervalMs <= 0 || this.handle !== null) return;
118
+ if (
119
+ this.intervalMs <= 0 ||
120
+ this.handle !== null ||
121
+ isNamedPipePath(this.socketPath)
122
+ ) {
123
+ return;
124
+ }
113
125
  this.handle = setInterval(() => {
114
126
  // The async entry path of rebindIfMissing performs filesystem work
115
127
  // (ensureSocketDir, createServer) before its inner try/catch, so a
@@ -143,6 +155,9 @@ export class SocketWatchdog {
143
155
  * is not running, or a shutdown/restart raced the rebind.
144
156
  */
145
157
  async rebindIfMissing(): Promise<boolean> {
158
+ if (isNamedPipePath(this.socketPath)) {
159
+ return false;
160
+ }
146
161
  const initialServer = this.getServer();
147
162
  if (initialServer === null) return false;
148
163
  if (existsSync(this.socketPath)) return false;
@@ -167,7 +182,7 @@ export class SocketWatchdog {
167
182
  };
168
183
  newServer.once("error", onError);
169
184
  newServer.once("listening", onListening);
170
- newServer.listen(this.socketPath);
185
+ newServer.listen(ipcListenOptions(this.socketPath));
171
186
  });
172
187
  } catch (err) {
173
188
  this.log.error(
@@ -21,6 +21,12 @@
21
21
  * `Date#toISOString()`.
22
22
  */
23
23
 
24
+ /**
25
+ * Pairing-challenge TTL in milliseconds (10 minutes): the gateway's challenge
26
+ * store enforces it and the `vellum pair` CLI renders it in user-facing copy.
27
+ */
28
+ export const REMOTE_WEB_PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
29
+
24
30
  /** `POST /v1/remote-web/pairing-challenge` request body. */
25
31
  export interface RemoteWebPairingChallengeRequest {
26
32
  /** Public https base URL the scanning device can reach the assistant at. */
package/openapi.yaml CHANGED
@@ -6772,6 +6772,9 @@ paths:
6772
6772
  - messageId
6773
6773
  - title
6774
6774
  additionalProperties: false
6775
+ historyOrphaned:
6776
+ type: boolean
6777
+ const: true
6775
6778
  archivedAt:
6776
6779
  type: number
6777
6780
  surfacedAt:
@@ -7132,6 +7135,9 @@ paths:
7132
7135
  - messageId
7133
7136
  - title
7134
7137
  additionalProperties: false
7138
+ historyOrphaned:
7139
+ type: boolean
7140
+ const: true
7135
7141
  archivedAt:
7136
7142
  type: number
7137
7143
  surfacedAt:
@@ -8342,6 +8348,9 @@ paths:
8342
8348
  - messageId
8343
8349
  - title
8344
8350
  additionalProperties: false
8351
+ historyOrphaned:
8352
+ type: boolean
8353
+ const: true
8345
8354
  archivedAt:
8346
8355
  type: number
8347
8356
  surfacedAt:
@@ -20067,8 +20076,8 @@ paths:
20067
20076
  type: string
20068
20077
  clientOs:
20069
20078
  description:
20070
- Client OS surface ("web" | "ios" | "macos" | "android"), reported separately from `interface`. Drives the
20071
- per-turn `client_os` context only; does not affect transport/host-proxy capabilities.
20079
+ Client OS surface ("web" | "ios" | "macos" | "windows" | "android"), reported separately from `interface`.
20080
+ Drives the per-turn `client_os` context only; does not affect transport/host-proxy capabilities.
20072
20081
  type: string
20073
20082
  visibleAppId:
20074
20083
  description:
@@ -25219,8 +25228,9 @@ paths:
25219
25228
  per conversation after activity (time/message thresholds, pre-compaction) — there is no global schedule, so
25220
25229
  `nextRunAt` is always null and no run-now endpoint exists. `intervalMs` is the per-conversation time threshold
25221
25230
  (`memory.retrospective.timeThresholdMs`), `lastRunAt` is the `createdAt` of the most recent retrospective
25222
- conversation across both legacy and fork sources, and `available` gates on `memory.enabled` (not
25223
- `memory.v2.enabled`).
25231
+ conversation across both legacy and fork sources, `available` gates on `memory.enabled` (not
25232
+ `memory.v2.enabled`), and `enabled` narrows that by `memory.retrospective.enabled`, the per-pass kill switch
25233
+ that stops retrospectives without disabling the rest of memory.
25224
25234
  tags:
25225
25235
  - retrospective
25226
25236
  responses:
@@ -29405,6 +29415,73 @@ paths:
29405
29415
  - isBinary
29406
29416
  - content
29407
29417
  additionalProperties: false
29418
+ /v1/skills/{id}/history:
29419
+ get:
29420
+ operationId: skills_by_id_history_get
29421
+ summary: Get skill revision history
29422
+ description:
29423
+ "Return a skill's recent updates, newest first, each with a combined diff across the skill directory.
29424
+ Read-only: revisions come from the workspace git repository, which already retains prior content. Commits whose
29425
+ only in-skill change is the `lastUsedAt` usage stamp are omitted, so entries are edits rather than loads."
29426
+ tags:
29427
+ - skills
29428
+ parameters:
29429
+ - name: id
29430
+ in: path
29431
+ required: true
29432
+ schema:
29433
+ type: string
29434
+ - name: limit
29435
+ in: query
29436
+ required: false
29437
+ schema:
29438
+ type: integer
29439
+ description: Maximum revisions to return, newest first. Defaults to 20 and is clamped to 100.
29440
+ responses:
29441
+ "200":
29442
+ description: Successful response
29443
+ content:
29444
+ application/json:
29445
+ schema:
29446
+ type: object
29447
+ properties:
29448
+ skillId:
29449
+ type: string
29450
+ description: The skill these revisions belong to
29451
+ revisions:
29452
+ type: array
29453
+ items:
29454
+ type: object
29455
+ properties:
29456
+ id:
29457
+ type: string
29458
+ description: Opaque revision identifier
29459
+ changedAt:
29460
+ type: string
29461
+ description: ISO-8601 time of the update
29462
+ files:
29463
+ type: array
29464
+ items:
29465
+ type: string
29466
+ description: Paths changed, relative to the skill directory
29467
+ diff:
29468
+ type: string
29469
+ description: Unified diff of this update, scoped to the skill
29470
+ required:
29471
+ - id
29472
+ - changedAt
29473
+ - files
29474
+ - diff
29475
+ additionalProperties: false
29476
+ description: Recent updates, newest first
29477
+ truncatedByCompaction:
29478
+ type: boolean
29479
+ description: Older history was squashed away, so the oldest entry is a floor rather than the skill's creation
29480
+ required:
29481
+ - skillId
29482
+ - revisions
29483
+ - truncatedByCompaction
29484
+ additionalProperties: false
29408
29485
  /v1/skills/{id}/inspect:
29409
29486
  get:
29410
29487
  operationId: skills_by_id_inspect_get
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.2-dev.202608060701.ede4331",
3
+ "version": "0.11.2-dev.202608062308.5442cf1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -88,6 +88,7 @@ function makeTarget(): Conversation {
88
88
  getTurnChannelContext: () => null,
89
89
  getTurnInterfaceContext: () => null,
90
90
  drainQueue: async () => {},
91
+ kickDrainQueue: async () => {},
91
92
  // Pre-run auto-compaction gate — no-op for these tests.
92
93
  maybeCompact: async () => null,
93
94
  };
@@ -100,6 +100,7 @@ function makeTarget(): {
100
100
  getTurnChannelContext: () => null,
101
101
  getTurnInterfaceContext: () => null,
102
102
  drainQueue: async () => {},
103
+ kickDrainQueue: async () => {},
103
104
  // Pre-run auto-compaction gate — no-op for these tests.
104
105
  maybeCompact: async () => null,
105
106
  buildCurrentSystemPrompt: () => "mock-system-prompt",
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The canned-reply routes (canned first greeting, unknown slash command) hold
3
+ * the processing lock themselves and release it from a deferred callback that
4
+ * also publishes their client event burst. That release is the only one those
5
+ * turns get: no agent loop runs, so a throw escaping the burst would latch the
6
+ * conversation "processing" for the life of the daemon and queue every later
7
+ * send behind an idle conversation.
8
+ */
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import type { QueueDrainReason } from "../daemon/conversation-queue-manager.js";
12
+ import { scheduleCannedReplyRelease } from "../runtime/routes/canned-reply-release.js";
13
+
14
+ function makeConversation() {
15
+ let processing = true;
16
+ const drainOrigins: string[] = [];
17
+ return {
18
+ isProcessing: () => processing,
19
+ drainOrigins,
20
+ target: {
21
+ setProcessing: (value: boolean) => {
22
+ processing = value;
23
+ },
24
+ kickDrainQueue: (_reason?: QueueDrainReason, origin?: string) => {
25
+ drainOrigins.push(origin ?? "");
26
+ return Promise.resolve();
27
+ },
28
+ },
29
+ };
30
+ }
31
+
32
+ /** Let the scheduled `setTimeout(..., 0)` callback run. */
33
+ function nextTick(): Promise<void> {
34
+ return new Promise((resolve) => setTimeout(resolve, 0));
35
+ }
36
+
37
+ describe("scheduleCannedReplyRelease", () => {
38
+ test("releases and drains after the deferred event burst", async () => {
39
+ const conversation = makeConversation();
40
+ const order: string[] = [];
41
+
42
+ scheduleCannedReplyRelease({
43
+ conversation: {
44
+ setProcessing: (value: boolean) => {
45
+ order.push("release");
46
+ conversation.target.setProcessing(value);
47
+ },
48
+ kickDrainQueue: conversation.target.kickDrainQueue,
49
+ },
50
+ origin: "canned_greeting",
51
+ emit: () => {
52
+ order.push("emit");
53
+ },
54
+ afterRelease: () => {
55
+ order.push("after");
56
+ },
57
+ });
58
+
59
+ // Nothing runs synchronously: the HTTP response has to reach the client
60
+ // before its events do.
61
+ expect(order).toEqual([]);
62
+
63
+ await nextTick();
64
+
65
+ expect(order).toEqual(["emit", "release", "after"]);
66
+ expect(conversation.isProcessing()).toBe(false);
67
+ expect(conversation.drainOrigins).toEqual(["canned_greeting"]);
68
+ });
69
+
70
+ test("still releases and drains when a broadcast throws", async () => {
71
+ const conversation = makeConversation();
72
+
73
+ scheduleCannedReplyRelease({
74
+ conversation: conversation.target,
75
+ origin: "slash_command",
76
+ emit: () => {
77
+ throw new Error("broadcast exploded");
78
+ },
79
+ });
80
+
81
+ await nextTick();
82
+
83
+ expect(conversation.isProcessing()).toBe(false);
84
+ expect(conversation.drainOrigins).toEqual(["slash_command"]);
85
+ });
86
+ });