@vellumai/assistant 0.12.2-staging.7 → 0.12.2

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 (120) hide show
  1. package/docs/architecture/memory.md +2 -11
  2. package/docs/desktop-browser-cli.md +2 -4
  3. package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
  4. package/node_modules/@vellumai/environments/src/shell.ts +0 -24
  5. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
  6. package/openapi.yaml +2 -6
  7. package/package.json +1 -1
  8. package/scripts/smoke-desktop-browser-cli.ts +0 -1
  9. package/src/__tests__/agent-loop.test.ts +0 -124
  10. package/src/__tests__/approval-interception-trust-gates.test.ts +0 -40
  11. package/src/__tests__/channel-approval.test.ts +14 -9
  12. package/src/__tests__/conversation-agent-loop.test.ts +0 -25
  13. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  14. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -138
  15. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  16. package/src/__tests__/subagent-tool-gate-mode.test.ts +0 -169
  17. package/src/__tests__/terminal-tools.test.ts +0 -8
  18. package/src/__tests__/unicode.test.ts +0 -36
  19. package/src/agent/loop.ts +0 -19
  20. package/src/api/index.ts +0 -6
  21. package/src/approvals/approval-primitive.ts +2 -5
  22. package/src/approvals/scoped-approval-grants.ts +2 -6
  23. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -43
  24. package/src/daemon/conversation-agent-loop.ts +0 -2
  25. package/src/daemon/conversation-tool-setup.ts +1 -57
  26. package/src/daemon/conversation.ts +0 -17
  27. package/src/daemon/daemon-control.ts +6 -2
  28. package/src/daemon/orphan-reaper.ts +3 -4
  29. package/src/daemon/tool-setup-types.ts +0 -6
  30. package/src/daemon/wake-conversation-ops.ts +15 -38
  31. package/src/desktop/desktop-automation-lease.test.ts +0 -143
  32. package/src/desktop/desktop-automation-lease.ts +3 -39
  33. package/src/messaging/provider-message-metadata.ts +3 -3
  34. package/src/notifications/__tests__/copy-composer.test.ts +0 -70
  35. package/src/notifications/copy-composer.ts +3 -11
  36. package/src/oauth/seed-providers.ts +30 -0
  37. package/src/persistence/conversation-plugin-facade.ts +0 -13
  38. package/src/persistence/schema/index.ts +0 -1
  39. package/src/persistence/steps.ts +0 -2
  40. package/src/plugin-api/conversation-turn.ts +7 -31
  41. package/src/plugin-api/index.ts +1 -9
  42. package/src/plugins/defaults/memory/AGENTS.md +2 -14
  43. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +0 -43
  44. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +0 -46
  45. package/src/plugins/defaults/memory/buffer-format.ts +0 -40
  46. package/src/plugins/defaults/memory/context-search/agent-runner.ts +2 -1
  47. package/src/plugins/defaults/memory/context-search/format.ts +1 -2
  48. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +1 -2
  49. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -2
  50. package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
  51. package/src/plugins/defaults/memory/graph/tool-handlers.ts +42 -1
  52. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  53. package/src/plugins/defaults/memory/injectors.ts +3 -4
  54. package/src/plugins/defaults/memory/memory-retrospective-job.ts +181 -55
  55. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +99 -407
  56. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -10
  57. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +7 -107
  58. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +86 -307
  59. package/src/plugins/defaults/memory/substrate/page-index.ts +1 -2
  60. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +49 -89
  61. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  62. package/src/plugins/defaults/memory/tools.ts +1 -1
  63. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  64. package/src/plugins/defaults/memory/v1/graph/extraction.ts +1 -2
  65. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  66. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +0 -5
  67. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +2 -5
  68. package/src/plugins/defaults/memory/v2/reranker.ts +1 -2
  69. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +1 -81
  70. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +0 -87
  71. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +0 -21
  72. package/src/plugins/defaults/memory/v3/card.ts +1 -2
  73. package/src/plugins/defaults/memory/v3/injector.ts +178 -212
  74. package/src/plugins/defaults/memory/v3/orchestrate.ts +22 -86
  75. package/src/plugins/defaults/memory/v3/pool-select.ts +7 -10
  76. package/src/plugins/defaults/memory/v3/sections.ts +1 -2
  77. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +1 -10
  78. package/src/plugins/defaults/tool-result-truncate/terminal.ts +46 -1
  79. package/src/runtime/AGENTS.md +1 -1
  80. package/src/runtime/__tests__/agent-wake.test.ts +1 -86
  81. package/src/runtime/agent-wake.ts +4 -20
  82. package/src/runtime/guardian-action-service.ts +17 -2
  83. package/src/runtime/guardian-reply-router.ts +8 -1
  84. package/src/runtime/routes/channel-route-shared.ts +9 -1
  85. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  86. package/src/runtime/routes/desktop-setup-routes.ts +3 -7
  87. package/src/runtime/routes/guardian-approval-interception.ts +0 -24
  88. package/src/runtime/routes/inbound-message-handler.ts +3 -2
  89. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  90. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -7
  91. package/src/schedule/run-script.ts +2 -2
  92. package/src/tools/host-terminal/host-shell.ts +6 -12
  93. package/src/tools/shared/filesystem/file-ops-service.ts +31 -1
  94. package/src/tools/shared/shell-output.test.ts +0 -10
  95. package/src/tools/shared/shell-output.ts +2 -14
  96. package/src/tools/skills/sandbox-runner.ts +2 -13
  97. package/src/tools/skills/scaffold-managed.ts +1 -2
  98. package/src/tools/terminal/__tests__/safe-env.test.ts +0 -29
  99. package/src/tools/terminal/safe-env.ts +1 -30
  100. package/src/tools/terminal/sanitized-bash.ts +2 -15
  101. package/src/tools/terminal/shell.test.ts +0 -29
  102. package/src/tools/terminal/shell.ts +7 -13
  103. package/src/util/host-process.test.ts +1 -17
  104. package/src/util/host-process.ts +0 -24
  105. package/src/util/unicode.ts +0 -29
  106. package/src/__tests__/db-conversation-tool-surface.test.ts +0 -144
  107. package/src/api/events/desktop-activity-changed.ts +0 -10
  108. package/src/persistence/conversation-tool-surface.ts +0 -86
  109. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +0 -78
  110. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +0 -29
  111. package/src/persistence/schema/conversation-tool-surfaces.ts +0 -22
  112. package/src/plugin-api/plugin-channel-turn-trust.test.ts +0 -133
  113. package/src/plugin-api/plugin-channel-turn-trust.ts +0 -71
  114. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +0 -320
  115. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +0 -17
  116. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +0 -161
  117. package/src/plugins/defaults/memory/buffer-file.ts +0 -354
  118. package/src/plugins/defaults/memory/memory-run-evidence.ts +0 -213
  119. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +0 -34
  120. package/src/tools/terminal/shell-launch.test.ts +0 -162
@@ -7,7 +7,6 @@ import {
7
7
  formatShellOutput,
8
8
  MAX_OUTPUT_LENGTH,
9
9
  OUTPUT_TRUNCATED_TAG,
10
- SHELL_DID_NOT_START_MESSAGE,
11
10
  } from "./shell-output.js";
12
11
 
13
12
  describe("BoundedStdioCollector", () => {
@@ -86,15 +85,6 @@ describe("attachBoundedStdio", () => {
86
85
  });
87
86
  });
88
87
 
89
- describe("formatShellOutput launch start", () => {
90
- test("does not report command_completed when the process never started", () => {
91
- const result = formatShellOutput("", "", 0, false, 120, { started: false });
92
- expect(result.isError).toBe(true);
93
- expect(result.content).toBe(SHELL_DID_NOT_START_MESSAGE);
94
- expect(result.content).not.toContain("<command_completed />");
95
- });
96
- });
97
-
98
88
  describe("formatShellOutput truncation", () => {
99
89
  test("truncates an already-materialized oversized string without a file path", () => {
100
90
  const longOutput = "x".repeat(30_000);
@@ -4,9 +4,6 @@ export const MAX_OUTPUT_LENGTH = 20_000;
4
4
 
5
5
  export const OUTPUT_TRUNCATED_TAG = `<output_truncated limit="20K" />`;
6
6
 
7
- export const SHELL_DID_NOT_START_MESSAGE =
8
- "Error: the shell command did not start. No process was created, so the command did not run.";
9
-
10
7
  export interface ShellOutputResult {
11
8
  content: string;
12
9
  status: string | undefined;
@@ -34,16 +31,8 @@ export function formatShellOutput(
34
31
  code: number | null,
35
32
  timedOut: boolean,
36
33
  timeoutSec: number,
37
- options?: { truncated?: boolean; started?: boolean },
34
+ options?: { truncated?: boolean },
38
35
  ): ShellOutputResult {
39
- if (options?.started === false) {
40
- return {
41
- content: SHELL_DID_NOT_START_MESSAGE,
42
- status: undefined,
43
- isError: true,
44
- };
45
- }
46
-
47
36
  let output = stdout;
48
37
  if (stderr) {
49
38
  output += (output ? "\n" : "") + stderr;
@@ -148,7 +137,6 @@ export class BoundedStdioCollector {
148
137
  code: number | null,
149
138
  timedOut: boolean,
150
139
  timeoutSec: number,
151
- options?: { started?: boolean },
152
140
  ): ShellOutputResult {
153
141
  return formatShellOutput(
154
142
  Buffer.concat(this.stdoutParts).toString(),
@@ -156,7 +144,7 @@ export class BoundedStdioCollector {
156
144
  code,
157
145
  timedOut,
158
146
  timeoutSec,
159
- { truncated: this.truncated, started: options?.started },
147
+ { truncated: this.truncated },
160
148
  );
161
149
  }
162
150
  }
@@ -7,12 +7,9 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
7
7
  import { computeSkillVersionHash } from "../../skills/version-hash.js";
8
8
  import {
9
9
  buildShellInvocation,
10
- buildShellSpawnFlags,
11
10
  terminateProcessTree,
12
- watchShellProcessStart,
13
11
  } from "../../util/host-process.js";
14
12
  import { safeStringSlice } from "../../util/unicode.js";
15
- import { SHELL_DID_NOT_START_MESSAGE } from "../shared/shell-output.js";
16
13
  import { buildSanitizedEnv } from "../terminal/safe-env.js";
17
14
  import type { ToolContext, ToolExecutionResult } from "../types.js";
18
15
 
@@ -163,9 +160,9 @@ function spawnRunner(
163
160
  cwd: runDir,
164
161
  env,
165
162
  stdio: ["ignore", "pipe", "pipe"],
166
- ...buildShellSpawnFlags(),
163
+ detached: true,
164
+ windowsHide: true,
167
165
  });
168
- const launch = watchShellProcessStart(child);
169
166
 
170
167
  const timer = setTimeout(() => {
171
168
  timedOut = true;
@@ -191,14 +188,6 @@ function spawnRunner(
191
188
  clearTimeout(timer);
192
189
  context.signal?.removeEventListener("abort", onAbort);
193
190
 
194
- if (!launch.didStart()) {
195
- resolve({
196
- content: `Failed to spawn skill tool script "${executorPath}": ${SHELL_DID_NOT_START_MESSAGE}`,
197
- isError: true,
198
- });
199
- return;
200
- }
201
-
202
191
  if (timedOut) {
203
192
  resolve({
204
193
  content: `Skill tool script "${executorPath}" timed out after ${timeoutMs}ms`,
@@ -134,8 +134,7 @@ function notifyBackgroundSkillUpdate(args: {
134
134
  sourceEventName: "activity.complete",
135
135
  dedupeKey: `skill-updated:${args.skillId}:${day}`,
136
136
  contextPayload: {
137
- // `summary` feeds the copy composer and `title` is the headline both
138
- // it and the home feed keep; `title`/`body` are also the feed's
137
+ // `summary` feeds the copy composer; `title`/`body` are the home feed's
139
138
  // fallback when no channel copy was rendered. Without them a fully
140
139
  // suppressed delivery (the intended shape for this signal: low urgency,
141
140
  // background, no interruption) leaves the feed writer with no summary
@@ -111,35 +111,6 @@ describe("safe-env Windows forwarding", () => {
111
111
  }
112
112
  });
113
113
 
114
- test("prepends the Windows assistant binary directory when it sits beside the runtime", () => {
115
- const dir = mkdtempSync(join(tmpdir(), "win-assistant-"));
116
- writeFileSync(join(dir, "assistant.exe"), "");
117
- try {
118
- const env = buildSanitizedEnv(
119
- "win32",
120
- { Path: "C:\\Windows\\System32" },
121
- { execPath: join(dir, "vellum-daemon.exe") },
122
- );
123
- expect(env.PATH).toBe(`${dir};C:\\Windows\\System32`);
124
- } finally {
125
- rmSync(dir, { recursive: true, force: true });
126
- }
127
- });
128
-
129
- test("does not invent a Windows assistant path when the sibling binary is absent", () => {
130
- const dir = mkdtempSync(join(tmpdir(), "win-no-assistant-"));
131
- try {
132
- const env = buildSanitizedEnv(
133
- "win32",
134
- { Path: "C:\\Windows\\System32" },
135
- { execPath: join(dir, "vellum-daemon.exe") },
136
- );
137
- expect(env.PATH).toBe("C:\\Windows\\System32");
138
- } finally {
139
- rmSync(dir, { recursive: true, force: true });
140
- }
141
- });
142
-
143
114
  test("matches Windows environment names case-insensitively", () => {
144
115
  const windowsEnv = buildSanitizedEnv("win32", {
145
116
  Path: "C:\\Windows\\System32;C:\\Program Files\\Vellum",
@@ -7,12 +7,8 @@
7
7
  */
8
8
  import { readdirSync, statSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
- import { dirname, join } from "node:path";
11
10
 
12
- import {
13
- pathListDelimiter,
14
- prependUniquePathEntries,
15
- } from "@vellumai/environments/shell";
11
+ import { pathListDelimiter } from "@vellumai/environments/shell";
16
12
 
17
13
  import { getGatewayInternalBaseUrl } from "../../config/env.js";
18
14
  import { getDataDir, getWorkspaceDir } from "../../util/platform.js";
@@ -288,7 +284,6 @@ function appendUniquePathEntries(
288
284
  export function buildSanitizedEnv(
289
285
  hostPlatform: NodeJS.Platform = process.platform,
290
286
  sourceEnv: NodeJS.ProcessEnv = process.env,
291
- options?: { execPath?: string },
292
287
  ): Record<string, string> {
293
288
  const env: Record<string, string> = {};
294
289
  const isKataRuntime = isKataFamilyRuntime(sourceEnv.VELLUM_SANDBOX_RUNTIME);
@@ -365,29 +360,5 @@ export function buildSanitizedEnv(
365
360
  if (!env.LC_ALL) {
366
361
  env.LC_ALL = utf8Locale;
367
362
  }
368
- prependWindowsAssistantDir(
369
- env,
370
- hostPlatform,
371
- options?.execPath ?? process.execPath,
372
- );
373
363
  return env;
374
364
  }
375
-
376
- function prependWindowsAssistantDir(
377
- env: Record<string, string>,
378
- hostPlatform: NodeJS.Platform,
379
- execPath: string,
380
- ): void {
381
- if (hostPlatform !== "win32") {
382
- return;
383
- }
384
- const execDir = dirname(execPath);
385
- try {
386
- if (!statSync(join(execDir, "assistant.exe")).isFile()) {
387
- return;
388
- }
389
- } catch {
390
- return;
391
- }
392
- env.PATH = prependUniquePathEntries(env.PATH, [execDir], hostPlatform);
393
- }
@@ -11,12 +11,9 @@ import { spawn } from "node:child_process";
11
11
 
12
12
  import {
13
13
  buildShellInvocation,
14
- buildShellSpawnFlags,
15
14
  terminateProcessTree,
16
- watchShellProcessStart,
17
15
  } from "../../util/host-process.js";
18
16
  import { getWorkspaceDir } from "../../util/platform.js";
19
- import { SHELL_DID_NOT_START_MESSAGE } from "../shared/shell-output.js";
20
17
  import { buildSanitizedEnv } from "./safe-env.js";
21
18
 
22
19
  export const DEFAULT_SANITIZED_BASH_TIMEOUT_MS = 30_000;
@@ -66,10 +63,10 @@ export function runSanitizedBash(
66
63
  const child = spawn(wrapped.command, wrapped.args, {
67
64
  cwd: getWorkspaceDir(),
68
65
  stdio: ["ignore", "pipe", "pipe"],
66
+ detached: true,
69
67
  env: buildSanitizedEnv(),
70
- ...buildShellSpawnFlags(),
68
+ windowsHide: true,
71
69
  });
72
- const launch = watchShellProcessStart(child);
73
70
 
74
71
  const timer = setTimeout(() => {
75
72
  timedOut = true;
@@ -86,16 +83,6 @@ export function runSanitizedBash(
86
83
 
87
84
  child.on("close", (code) => {
88
85
  clearTimeout(timer);
89
- if (!launch.didStart()) {
90
- finish({
91
- stdout: "",
92
- stderr: "",
93
- exitCode: null,
94
- timedOut: false,
95
- error: SHELL_DID_NOT_START_MESSAGE,
96
- });
97
- return;
98
- }
99
86
  finish({
100
87
  stdout: Buffer.concat(stdoutChunks).toString(),
101
88
  stderr: Buffer.concat(stderrChunks).toString(),
@@ -7,7 +7,6 @@ import { setConfig } from "../../__tests__/helpers/set-config.js";
7
7
  import {
8
8
  MAX_OUTPUT_LENGTH,
9
9
  OUTPUT_TRUNCATED_TAG,
10
- SHELL_DID_NOT_START_MESSAGE,
11
10
  } from "../shared/shell-output.js";
12
11
  import type { ToolContext } from "../types.js";
13
12
 
@@ -208,34 +207,6 @@ describe("foreground stdin handling", () => {
208
207
  });
209
208
  });
210
209
 
211
- describe("foreground command execution", () => {
212
- let tmpDir = "";
213
-
214
- beforeEach(async () => {
215
- tmpDir = await mkdtemp(join(tmpdir(), "bash-exec-"));
216
- });
217
-
218
- afterEach(async () => {
219
- if (tmpDir) {
220
- await rm(tmpDir, { recursive: true, force: true });
221
- }
222
- });
223
-
224
- test("a command that writes a file must actually create it", async () => {
225
- const result = await shellTool.execute(
226
- {
227
- command: "printf ran > proof.txt",
228
- activity: "test",
229
- },
230
- { ...makeContext(), workingDir: tmpDir },
231
- );
232
-
233
- expect(result.isError).toBeFalsy();
234
- expect(result.content).not.toBe(SHELL_DID_NOT_START_MESSAGE);
235
- expect(await readFile(join(tmpDir, "proof.txt"), "utf-8")).toBe("ran");
236
- });
237
- });
238
-
239
210
  describe("streaming stdout cap", () => {
240
211
  let tmpDir = "";
241
212
 
@@ -11,9 +11,7 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
11
11
  import { redactSecrets } from "../../security/secret-scanner.js";
12
12
  import {
13
13
  buildShellInvocation,
14
- buildShellSpawnFlags,
15
14
  terminateProcessTree,
16
- watchShellProcessStart,
17
15
  } from "../../util/host-process.js";
18
16
  import { getLogger } from "../../util/logger.js";
19
17
  import { getDataDir } from "../../util/platform.js";
@@ -326,9 +324,9 @@ export const shellTool = {
326
324
  cwd: context.workingDir,
327
325
  env,
328
326
  stdio: ["ignore", "pipe", "pipe"],
329
- ...buildShellSpawnFlags(),
327
+ detached: true,
328
+ windowsHide: true,
330
329
  });
331
- const launch = watchShellProcessStart(child);
332
330
  const collector = attachBoundedStdio(child);
333
331
 
334
332
  const killTree = buildKillTree(child, {
@@ -369,13 +367,11 @@ export const shellTool = {
369
367
  timedOut,
370
368
  });
371
369
 
372
- const fmtResult = collector.format(code, timedOut, timeoutSec, {
373
- started: launch.didStart(),
374
- });
370
+ const fmtResult = collector.format(code, timedOut, timeoutSec);
375
371
 
376
372
  const status: BackgroundToolCompletedEvent["status"] = aborted
377
373
  ? "cancelled"
378
- : timedOut || fmtResult.isError
374
+ : timedOut
379
375
  ? "failed"
380
376
  : code === 0
381
377
  ? "completed"
@@ -533,9 +529,9 @@ export const shellTool = {
533
529
  cwd: context.workingDir,
534
530
  env,
535
531
  stdio: ["ignore", "pipe", "pipe"],
536
- ...buildShellSpawnFlags(),
532
+ detached: true,
533
+ windowsHide: true,
537
534
  });
538
- const launch = watchShellProcessStart(child);
539
535
  const collector = attachBoundedStdio(child, {
540
536
  onOutput: context.onOutput,
541
537
  });
@@ -577,9 +573,7 @@ export const shellTool = {
577
573
  timedOut,
578
574
  });
579
575
 
580
- const fmtResult = collector.format(code, timedOut, timeoutSec, {
581
- started: launch.didStart(),
582
- });
576
+ const fmtResult = collector.format(code, timedOut, timeoutSec);
583
577
 
584
578
  resolve({
585
579
  content: fmtResult.content,
@@ -1,7 +1,6 @@
1
- import { EventEmitter } from "node:events";
2
1
  import { expect, mock, test } from "bun:test";
3
2
 
4
- import { terminateProcessTree, watchShellProcessStart } from "./host-process.js";
3
+ import { terminateProcessTree } from "./host-process.js";
5
4
 
6
5
  test("Windows process trees use taskkill and fall back to the direct child", () => {
7
6
  const kill = mock(() => true);
@@ -16,18 +15,3 @@ test("Windows process trees use taskkill and fall back to the direct child", ()
16
15
  expect(kill).toHaveBeenCalledTimes(1);
17
16
  expect(kill).toHaveBeenCalledWith();
18
17
  });
19
-
20
- test("watchShellProcessStart is false until spawn or a pid appears", () => {
21
- const child = new EventEmitter() as EventEmitter & { pid?: number };
22
- const launch = watchShellProcessStart(child);
23
- expect(launch.didStart()).toBe(false);
24
-
25
- child.emit("spawn");
26
- expect(launch.didStart()).toBe(true);
27
- });
28
-
29
- test("watchShellProcessStart treats a pid assigned at construction as started", () => {
30
- const child = new EventEmitter() as EventEmitter & { pid?: number };
31
- child.pid = 4242;
32
- expect(watchShellProcessStart(child).didStart()).toBe(true);
33
- });
@@ -2,34 +2,10 @@ import { spawn } from "node:child_process";
2
2
 
3
3
  export {
4
4
  buildShellInvocation,
5
- buildShellSpawnFlags,
6
5
  pathListDelimiter,
7
6
  prependUniquePathEntries,
8
7
  } from "@vellumai/environments/shell";
9
8
 
10
- export interface ShellProcessStartWatch {
11
- didStart(): boolean;
12
- }
13
-
14
- /**
15
- * Record whether a spawned child actually started. A `close` with exit 0 and
16
- * empty stdio is not proof of execution: Windows can emit that when the
17
- * process was never created. Callers must treat `didStart() === false` as a
18
- * launch failure.
19
- */
20
- export function watchShellProcessStart(child: {
21
- pid?: number;
22
- once(event: "spawn", listener: () => void): unknown;
23
- }): ShellProcessStartWatch {
24
- let spawnSeen = child.pid != null;
25
- child.once("spawn", () => {
26
- spawnSeen = true;
27
- });
28
- return {
29
- didStart: () => spawnSeen || child.pid != null,
30
- };
31
- }
32
-
33
9
  export interface KillableProcess {
34
10
  pid?: number;
35
11
  kill: unknown;
@@ -67,35 +67,6 @@ export function safeStringSlice(
67
67
  return str.slice(safeStart, safeEnd);
68
68
  }
69
69
 
70
- /**
71
- * A `[start, end)` character window of at most `maxChars` code units that
72
- * never splits a surrogate pair, for paging through text by offset. A split
73
- * leaves a lone half at each edge, and each encodes to U+FFFD, so the
74
- * character is lost from both this window and the next one paged in after
75
- * it. `charCodeAt` reads the underlying text so a caller need not hold it as
76
- * one string.
77
- */
78
- export function surrogateSafeWindow(
79
- total: number,
80
- charCodeAt: (index: number) => number,
81
- requestedStart: number,
82
- maxChars: number,
83
- ): { start: number; end: number } {
84
- let start = Math.max(0, Math.min(requestedStart, total));
85
- if (start > 0 && start < total && isLowSurrogate(charCodeAt(start))) {
86
- start -= 1;
87
- }
88
-
89
- let end = Math.min(total, start + maxChars);
90
- if (end > start && end < total && isHighSurrogate(charCodeAt(end - 1))) {
91
- // Backing off would empty a one-character window, which stalls paging on
92
- // the same offset, so take the whole pair instead.
93
- end = end - 1 > start ? end - 1 : Math.min(total, end + 1);
94
- }
95
-
96
- return { start, end };
97
- }
98
-
99
70
  /**
100
71
  * Replace every orphaned UTF-16 surrogate in `str` with U+FFFD
101
72
  * (REPLACEMENT CHARACTER).
@@ -1,144 +0,0 @@
1
- /**
2
- * Tests for the per-conversation wire tool-surface record
3
- * (`persistence/conversation-tool-surface.ts`): the resolver records the
4
- * tools array a live turn sends, fork wakes read it back verbatim, and an
5
- * unchanged surface is never rewritten.
6
- */
7
-
8
- import { Database } from "bun:sqlite";
9
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
-
11
- import { drizzle } from "drizzle-orm/bun-sqlite";
12
-
13
- import {
14
- getConversationToolSurface,
15
- hashConversationToolSurface,
16
- recordConversationToolSurface,
17
- } from "../persistence/conversation-tool-surface.js";
18
- import { clearStoredDb, setStoredDb } from "../persistence/db-singleton.js";
19
- import { migrateCreateConversationToolSurfaces } from "../persistence/migrations/378-create-conversation-tool-surfaces.js";
20
- import * as schema from "../persistence/schema/index.js";
21
- import type { ToolDefinition } from "../providers/types.js";
22
-
23
- let sqlite: Database;
24
-
25
- beforeEach(() => {
26
- sqlite = new Database(":memory:");
27
- sqlite.run("PRAGMA foreign_keys = ON");
28
- sqlite.run(/*sql*/ `CREATE TABLE conversations (id TEXT PRIMARY KEY)`);
29
- sqlite.query(`INSERT INTO conversations (id) VALUES (?)`).run("conv-1");
30
- const db = drizzle(sqlite, { schema });
31
- migrateCreateConversationToolSurfaces(db);
32
- setStoredDb("main", db, () => sqlite.close());
33
- });
34
-
35
- afterEach(() => {
36
- clearStoredDb("main");
37
- });
38
-
39
- const TOOLS: ToolDefinition[] = [
40
- {
41
- name: "remember",
42
- description: "Save a fact",
43
- input_schema: { type: "object", properties: { content: {} } },
44
- },
45
- { name: "bell_jingle", description: "Ring", input_schema: {} },
46
- ];
47
-
48
- function storedRow(): { tools_hash: string; updated_at: number } | null {
49
- return sqlite
50
- .query(
51
- `SELECT tools_hash, updated_at FROM conversation_tool_surfaces WHERE conversation_id = ?`,
52
- )
53
- .get("conv-1") as { tools_hash: string; updated_at: number } | null;
54
- }
55
-
56
- describe("recordConversationToolSurface", () => {
57
- test("stores the array and returns its content hash", () => {
58
- const hash = recordConversationToolSurface("conv-1", TOOLS);
59
-
60
- expect(hash).toBe(hashConversationToolSurface(TOOLS));
61
- expect(storedRow()?.tools_hash).toBe(hash);
62
- expect(getConversationToolSurface("conv-1")).toEqual(TOOLS);
63
- });
64
-
65
- test("replays the exact array, including key order and extra fields", () => {
66
- const serverTool = {
67
- type: "web_search_20250305",
68
- name: "web_search",
69
- max_uses: 5,
70
- } as unknown as ToolDefinition;
71
- const tools = [...TOOLS, serverTool];
72
-
73
- recordConversationToolSurface("conv-1", tools);
74
-
75
- expect(JSON.stringify(getConversationToolSurface("conv-1"))).toBe(
76
- JSON.stringify(tools),
77
- );
78
- });
79
-
80
- test("an unchanged surface with a known hash skips the write", () => {
81
- const hash = recordConversationToolSurface("conv-1", TOOLS);
82
- const before = storedRow()!.updated_at;
83
- sqlite
84
- .query(
85
- `UPDATE conversation_tool_surfaces SET updated_at = ? WHERE conversation_id = ?`,
86
- )
87
- .run(before - 1000, "conv-1");
88
-
89
- recordConversationToolSurface("conv-1", TOOLS, hash);
90
-
91
- expect(storedRow()!.updated_at).toBe(before - 1000);
92
- });
93
-
94
- test("an unchanged surface with no known hash compares against the stored row before writing", () => {
95
- recordConversationToolSurface("conv-1", TOOLS);
96
- const marker = 42;
97
- sqlite
98
- .query(
99
- `UPDATE conversation_tool_surfaces SET updated_at = ? WHERE conversation_id = ?`,
100
- )
101
- .run(marker, "conv-1");
102
-
103
- // A freshly loaded conversation knows no hash yet; the stored row matches,
104
- // so nothing is rewritten.
105
- recordConversationToolSurface("conv-1", TOOLS, undefined);
106
-
107
- expect(storedRow()!.updated_at).toBe(marker);
108
- });
109
-
110
- test("a changed surface overwrites the stored one", () => {
111
- const first = recordConversationToolSurface("conv-1", TOOLS);
112
- const changed = TOOLS.slice(0, 1);
113
-
114
- const second = recordConversationToolSurface("conv-1", changed, first);
115
-
116
- expect(second).not.toBe(first);
117
- expect(storedRow()?.tools_hash).toBe(second);
118
- expect(getConversationToolSurface("conv-1")).toEqual(changed);
119
- });
120
- });
121
-
122
- describe("getConversationToolSurface", () => {
123
- test("returns null when no turn has recorded a surface", () => {
124
- expect(getConversationToolSurface("conv-1")).toBeNull();
125
- });
126
-
127
- test("returns null for an unreadable stored payload", () => {
128
- sqlite
129
- .query(
130
- /*sql*/ `INSERT INTO conversation_tool_surfaces (conversation_id, tools_json, tools_hash, updated_at) VALUES (?, ?, ?, ?)`,
131
- )
132
- .run("conv-1", "not json", "h", 1);
133
-
134
- expect(getConversationToolSurface("conv-1")).toBeNull();
135
- });
136
-
137
- test("the row cascades with its conversation", () => {
138
- recordConversationToolSurface("conv-1", TOOLS);
139
-
140
- sqlite.query(`DELETE FROM conversations WHERE id = ?`).run("conv-1");
141
-
142
- expect(getConversationToolSurface("conv-1")).toBeNull();
143
- });
144
- });
@@ -1,10 +0,0 @@
1
- import { z } from "zod";
2
-
3
- /** Transient browser activity changed; refetch desktop status. */
4
- export const DesktopActivityChangedEventSchema = z.object({
5
- type: z.literal("desktop_activity_changed"),
6
- });
7
-
8
- export type DesktopActivityChangedEvent = z.infer<
9
- typeof DesktopActivityChangedEventSchema
10
- >;