@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.
- package/docs/architecture/memory.md +2 -11
- package/docs/desktop-browser-cli.md +2 -4
- package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
- package/node_modules/@vellumai/environments/src/shell.ts +0 -24
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
- package/openapi.yaml +2 -6
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +0 -1
- package/src/__tests__/agent-loop.test.ts +0 -124
- package/src/__tests__/approval-interception-trust-gates.test.ts +0 -40
- package/src/__tests__/channel-approval.test.ts +14 -9
- package/src/__tests__/conversation-agent-loop.test.ts +0 -25
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -138
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/subagent-tool-gate-mode.test.ts +0 -169
- package/src/__tests__/terminal-tools.test.ts +0 -8
- package/src/__tests__/unicode.test.ts +0 -36
- package/src/agent/loop.ts +0 -19
- package/src/api/index.ts +0 -6
- package/src/approvals/approval-primitive.ts +2 -5
- package/src/approvals/scoped-approval-grants.ts +2 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -43
- package/src/daemon/conversation-agent-loop.ts +0 -2
- package/src/daemon/conversation-tool-setup.ts +1 -57
- package/src/daemon/conversation.ts +0 -17
- package/src/daemon/daemon-control.ts +6 -2
- package/src/daemon/orphan-reaper.ts +3 -4
- package/src/daemon/tool-setup-types.ts +0 -6
- package/src/daemon/wake-conversation-ops.ts +15 -38
- package/src/desktop/desktop-automation-lease.test.ts +0 -143
- package/src/desktop/desktop-automation-lease.ts +3 -39
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +0 -70
- package/src/notifications/copy-composer.ts +3 -11
- package/src/oauth/seed-providers.ts +30 -0
- package/src/persistence/conversation-plugin-facade.ts +0 -13
- package/src/persistence/schema/index.ts +0 -1
- package/src/persistence/steps.ts +0 -2
- package/src/plugin-api/conversation-turn.ts +7 -31
- package/src/plugin-api/index.ts +1 -9
- package/src/plugins/defaults/memory/AGENTS.md +2 -14
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +0 -43
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +0 -46
- package/src/plugins/defaults/memory/buffer-format.ts +0 -40
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +2 -1
- package/src/plugins/defaults/memory/context-search/format.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +42 -1
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +3 -4
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +181 -55
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +99 -407
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -10
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +7 -107
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +86 -307
- package/src/plugins/defaults/memory/substrate/page-index.ts +1 -2
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +49 -89
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +1 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +0 -5
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +2 -5
- package/src/plugins/defaults/memory/v2/reranker.ts +1 -2
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +1 -81
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +0 -87
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +0 -21
- package/src/plugins/defaults/memory/v3/card.ts +1 -2
- package/src/plugins/defaults/memory/v3/injector.ts +178 -212
- package/src/plugins/defaults/memory/v3/orchestrate.ts +22 -86
- package/src/plugins/defaults/memory/v3/pool-select.ts +7 -10
- package/src/plugins/defaults/memory/v3/sections.ts +1 -2
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +1 -10
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +46 -1
- package/src/runtime/AGENTS.md +1 -1
- package/src/runtime/__tests__/agent-wake.test.ts +1 -86
- package/src/runtime/agent-wake.ts +4 -20
- package/src/runtime/guardian-action-service.ts +17 -2
- package/src/runtime/guardian-reply-router.ts +8 -1
- package/src/runtime/routes/channel-route-shared.ts +9 -1
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +3 -7
- package/src/runtime/routes/guardian-approval-interception.ts +0 -24
- package/src/runtime/routes/inbound-message-handler.ts +3 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -7
- package/src/schedule/run-script.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +6 -12
- package/src/tools/shared/filesystem/file-ops-service.ts +31 -1
- package/src/tools/shared/shell-output.test.ts +0 -10
- package/src/tools/shared/shell-output.ts +2 -14
- package/src/tools/skills/sandbox-runner.ts +2 -13
- package/src/tools/skills/scaffold-managed.ts +1 -2
- package/src/tools/terminal/__tests__/safe-env.test.ts +0 -29
- package/src/tools/terminal/safe-env.ts +1 -30
- package/src/tools/terminal/sanitized-bash.ts +2 -15
- package/src/tools/terminal/shell.test.ts +0 -29
- package/src/tools/terminal/shell.ts +7 -13
- package/src/util/host-process.test.ts +1 -17
- package/src/util/host-process.ts +0 -24
- package/src/util/unicode.ts +0 -29
- package/src/__tests__/db-conversation-tool-surface.test.ts +0 -144
- package/src/api/events/desktop-activity-changed.ts +0 -10
- package/src/persistence/conversation-tool-surface.ts +0 -86
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +0 -78
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +0 -29
- package/src/persistence/schema/conversation-tool-surfaces.ts +0 -22
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +0 -133
- package/src/plugin-api/plugin-channel-turn-trust.ts +0 -71
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +0 -320
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +0 -17
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +0 -161
- package/src/plugins/defaults/memory/buffer-file.ts +0 -354
- package/src/plugins/defaults/memory/memory-run-evidence.ts +0 -213
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +0 -34
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
});
|
package/src/util/host-process.ts
CHANGED
|
@@ -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;
|
package/src/util/unicode.ts
CHANGED
|
@@ -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
|
-
>;
|