@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
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { syncMessageToDisk } from "../persistence/conversation-disk-view.js";
|
|
23
23
|
import { backfillMessageIdOnLogs } from "../persistence/llm-request-log-store.js";
|
|
24
24
|
import { resolveMediaSourceData } from "../providers/media-resolve.js";
|
|
25
|
-
import type { Message
|
|
25
|
+
import type { Message } from "../providers/types.js";
|
|
26
26
|
import { broadcastMessage } from "../runtime/assistant-event-hub.js";
|
|
27
27
|
import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
|
|
28
28
|
import type { CompletedBackgroundTool } from "../tools/background-tool-registry.js";
|
|
@@ -350,60 +350,38 @@ export async function persistWakeTriggerMessage(
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
/**
|
|
354
|
-
* How a wake's tool allowlist is applied beyond the allowlist itself. Every
|
|
355
|
-
* field is set on the conversation together with the allowlist and restored
|
|
356
|
-
* with it.
|
|
357
|
-
*/
|
|
358
|
-
export interface WakeToolScopeOptions {
|
|
359
|
-
/** How the allowlist is enforced; see {@link SubagentToolGateMode}. */
|
|
360
|
-
gateMode?: SubagentToolGateMode;
|
|
361
|
-
/**
|
|
362
|
-
* Freezes the client-context inputs for tool-definition resolution
|
|
363
|
-
* (execution-gate-mode cache-parity wakes); see {@link WakeToolContextPin}.
|
|
364
|
-
* Its `requestOrigin`, when set, is stamped onto the conversation's per-turn
|
|
365
|
-
* origin so the permission checker's origin-scoped auto-grants fire.
|
|
366
|
-
*/
|
|
367
|
-
toolContextPin?: WakeToolContextPin;
|
|
368
|
-
/**
|
|
369
|
-
* Skills whose bundled tools join the turn's active set
|
|
370
|
-
* (`allowedToolNames`) so they are callable without a prior `skill_load`.
|
|
371
|
-
*/
|
|
372
|
-
preactivateSkillIds?: readonly string[];
|
|
373
|
-
/**
|
|
374
|
-
* Sent verbatim as the wake's wire tool array in place of the resolved one
|
|
375
|
-
* (the source conversation's recorded surface, replayed for provider
|
|
376
|
-
* prompt-cache parity). Changes nothing about what may execute.
|
|
377
|
-
*/
|
|
378
|
-
wireToolDefinitions?: readonly ToolDefinition[];
|
|
379
|
-
}
|
|
380
|
-
|
|
381
353
|
/**
|
|
382
354
|
* Temporarily restrict the tools visible/executable during a wake by
|
|
383
355
|
* reusing the conversation's subagent allowlist slot. Returns a restore
|
|
384
356
|
* callback that reinstates the previous allowlist so the wake can release
|
|
385
357
|
* the scope before any queued user turn is drained.
|
|
358
|
+
*
|
|
359
|
+
* `gateMode` controls how the allowlist is enforced — see
|
|
360
|
+
* {@link SubagentToolGateMode}. `toolContextPin`, when provided
|
|
361
|
+
* (execution-gate-mode cache-parity wakes), freezes the client-context
|
|
362
|
+
* inputs for tool-definition resolution — see {@link WakeToolContextPin}.
|
|
363
|
+
* Its `requestOrigin`, when set, is stamped onto the conversation's per-turn
|
|
364
|
+
* origin so the permission checker's origin-scoped auto-grants fire for the
|
|
365
|
+
* wake. `preactivateSkillIds`, when non-empty, activates those skills' bundled
|
|
366
|
+
* tools in the turn's active set (`allowedToolNames`) for the wake so they are
|
|
367
|
+
* callable without a prior `skill_load`. All are set and restored alongside the
|
|
368
|
+
* allowlist.
|
|
386
369
|
*/
|
|
387
370
|
export function scopeWakeAllowedTools(
|
|
388
371
|
conversation: Conversation,
|
|
389
372
|
tools: ReadonlySet<string>,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
preactivateSkillIds,
|
|
394
|
-
wireToolDefinitions,
|
|
395
|
-
}: WakeToolScopeOptions = {},
|
|
373
|
+
gateMode: SubagentToolGateMode = "wire",
|
|
374
|
+
toolContextPin?: WakeToolContextPin,
|
|
375
|
+
preactivateSkillIds?: readonly string[],
|
|
396
376
|
): () => void {
|
|
397
377
|
const previous = conversation.subagentAllowedTools;
|
|
398
378
|
const previousGateMode = conversation.subagentToolGateMode;
|
|
399
379
|
const previousToolContextPin = conversation.toolContextPin;
|
|
400
380
|
const previousRequestOrigin = conversation.currentTurnRequestOrigin;
|
|
401
381
|
const previousPreactivated = conversation.preactivatedSkillIds;
|
|
402
|
-
const previousWireToolReplay = conversation.wireToolReplay;
|
|
403
382
|
conversation.setSubagentAllowedTools(new Set(tools));
|
|
404
383
|
conversation.subagentToolGateMode = gateMode;
|
|
405
384
|
conversation.toolContextPin = toolContextPin;
|
|
406
|
-
conversation.wireToolReplay = wireToolDefinitions;
|
|
407
385
|
if (toolContextPin?.requestOrigin !== undefined) {
|
|
408
386
|
conversation.currentTurnRequestOrigin = toolContextPin.requestOrigin;
|
|
409
387
|
}
|
|
@@ -416,6 +394,5 @@ export function scopeWakeAllowedTools(
|
|
|
416
394
|
conversation.toolContextPin = previousToolContextPin;
|
|
417
395
|
conversation.currentTurnRequestOrigin = previousRequestOrigin;
|
|
418
396
|
conversation.setPreactivatedSkillIds(previousPreactivated);
|
|
419
|
-
conversation.wireToolReplay = previousWireToolReplay;
|
|
420
397
|
};
|
|
421
398
|
}
|
|
@@ -35,9 +35,7 @@ function fixture() {
|
|
|
35
35
|
const started = mock(async () => {});
|
|
36
36
|
const released = mock(() => {});
|
|
37
37
|
const releaseBrowser = mock(async () => {});
|
|
38
|
-
const notify = mock(async () => {});
|
|
39
38
|
const lease = new DesktopAutomationLease({
|
|
40
|
-
notify,
|
|
41
39
|
enabled: () => enabled,
|
|
42
40
|
ready: () => ready,
|
|
43
41
|
ensureReady,
|
|
@@ -52,7 +50,6 @@ function fixture() {
|
|
|
52
50
|
cleanups.push(lease);
|
|
53
51
|
return {
|
|
54
52
|
lease,
|
|
55
|
-
notify,
|
|
56
53
|
ensureReady,
|
|
57
54
|
install,
|
|
58
55
|
failSetup: () => setup.reject(new Error("download failed")),
|
|
@@ -131,7 +128,6 @@ test("cancellation stops running and queued browser commands without blocking a
|
|
|
131
128
|
const callback = mock(operation);
|
|
132
129
|
const queued = f.lease.runBrowser(context, callback);
|
|
133
130
|
abort.abort();
|
|
134
|
-
expect(f.lease.isActive).toBe(false);
|
|
135
131
|
expect(await running).toBeInstanceOf(Error);
|
|
136
132
|
expect((await queued).isError).toBe(true);
|
|
137
133
|
expect(callback).not.toHaveBeenCalled();
|
|
@@ -210,142 +206,3 @@ test("disabled, unidentified, cancelled and released browser calls cannot instal
|
|
|
210
206
|
await expect(f.lease.runBrowser(context, operation)).rejects.toThrow();
|
|
211
207
|
expect(f.ensureReady).not.toHaveBeenCalled();
|
|
212
208
|
});
|
|
213
|
-
|
|
214
|
-
test("activity follows the browser lease and clears before failed cleanup", async () => {
|
|
215
|
-
const f = fixture();
|
|
216
|
-
expect(f.lease.isActive).toBe(false);
|
|
217
|
-
await f.lease.runBrowser(context, operation);
|
|
218
|
-
expect(f.lease.isActive).toBe(true);
|
|
219
|
-
expect(f.notify).toHaveBeenCalledTimes(1);
|
|
220
|
-
await f.lease.runBrowser(context, operation);
|
|
221
|
-
expect(f.notify).toHaveBeenCalledTimes(1);
|
|
222
|
-
f.releaseBrowser.mockRejectedValueOnce(new Error("cleanup failed"));
|
|
223
|
-
await expect(f.lease.runBrowser(context, operation, true)).rejects.toThrow(
|
|
224
|
-
"cleanup failed",
|
|
225
|
-
);
|
|
226
|
-
expect(f.lease.isActive).toBe(false);
|
|
227
|
-
expect(f.notify).toHaveBeenCalledTimes(2);
|
|
228
|
-
await f.lease.runBrowser(context, operation, true);
|
|
229
|
-
expect(f.notify).toHaveBeenCalledTimes(2);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("turn completion releases only its own desktop before cleanup finishes", async () => {
|
|
233
|
-
const f = fixture();
|
|
234
|
-
await f.lease.runBrowser(context, operation);
|
|
235
|
-
f.lease.releaseForConversation("conv-456");
|
|
236
|
-
expect(f.lease.isActive).toBe(true);
|
|
237
|
-
expect(f.notify).toHaveBeenCalledTimes(1);
|
|
238
|
-
|
|
239
|
-
const cleanup = Promise.withResolvers<void>();
|
|
240
|
-
f.releaseBrowser.mockImplementationOnce(() => cleanup.promise);
|
|
241
|
-
f.lease.releaseForConversation(context.conversationId);
|
|
242
|
-
expect(f.lease.isActive).toBe(false);
|
|
243
|
-
expect(f.notify).toHaveBeenCalledTimes(2);
|
|
244
|
-
await Bun.sleep(0);
|
|
245
|
-
expect(f.released).not.toHaveBeenCalled();
|
|
246
|
-
|
|
247
|
-
const callback = mock(operation);
|
|
248
|
-
const next = f.lease.runBrowser(context, callback);
|
|
249
|
-
await Bun.sleep(0);
|
|
250
|
-
expect(callback).not.toHaveBeenCalled();
|
|
251
|
-
cleanup.resolve();
|
|
252
|
-
expect((await next).isError).toBe(false);
|
|
253
|
-
expect(f.released).toHaveBeenCalledTimes(1);
|
|
254
|
-
expect(f.started).toHaveBeenCalledTimes(2);
|
|
255
|
-
expect(f.lease.isActive).toBe(true);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("turn completion cancels running and queued desktop work", async () => {
|
|
259
|
-
const f = fixture();
|
|
260
|
-
const started = Promise.withResolvers<void>();
|
|
261
|
-
const running = f.lease
|
|
262
|
-
.runBrowser(context, async (signal) => {
|
|
263
|
-
started.resolve();
|
|
264
|
-
await new Promise<void>((_resolve, reject) => {
|
|
265
|
-
signal.addEventListener("abort", () => reject(new Error("aborted")), {
|
|
266
|
-
once: true,
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
return operation();
|
|
270
|
-
})
|
|
271
|
-
.catch((error: unknown) => error);
|
|
272
|
-
await started.promise;
|
|
273
|
-
const callback = mock(operation);
|
|
274
|
-
const queued = f.lease.runBrowser(context, callback);
|
|
275
|
-
f.lease.releaseForConversation(context.conversationId);
|
|
276
|
-
expect(f.lease.isActive).toBe(false);
|
|
277
|
-
expect(await running).toBeInstanceOf(Error);
|
|
278
|
-
expect((await queued).isError).toBe(true);
|
|
279
|
-
expect(callback).not.toHaveBeenCalled();
|
|
280
|
-
await f.lease.runBrowser(context, callback);
|
|
281
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("turn handoff retries failed cleanup without another turn or restart", async () => {
|
|
285
|
-
const f = fixture();
|
|
286
|
-
await f.lease.runBrowser(context, operation);
|
|
287
|
-
f.releaseBrowser.mockRejectedValueOnce(new Error("Chrome cleanup timed out"));
|
|
288
|
-
f.lease.releaseForConversation(context.conversationId);
|
|
289
|
-
expect(f.lease.isActive).toBe(false);
|
|
290
|
-
await Bun.sleep(0);
|
|
291
|
-
expect(f.released).not.toHaveBeenCalled();
|
|
292
|
-
await Bun.sleep(1_100);
|
|
293
|
-
expect(f.released).toHaveBeenCalledTimes(1);
|
|
294
|
-
const nextContext = { ...context, conversationId: "conv-456" };
|
|
295
|
-
expect((await f.lease.runBrowser(nextContext, operation)).isError).toBe(
|
|
296
|
-
false,
|
|
297
|
-
);
|
|
298
|
-
await f.lease.runBrowser(nextContext, operation, true);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test("a delayed cleanup retry cannot release a replacement owner", async () => {
|
|
302
|
-
const f = fixture();
|
|
303
|
-
await f.lease.runBrowser(context, operation);
|
|
304
|
-
f.releaseBrowser.mockRejectedValueOnce(new Error("Chrome cleanup timed out"));
|
|
305
|
-
f.lease.releaseForConversation(context.conversationId);
|
|
306
|
-
await f.lease.runBrowser(context, operation, true);
|
|
307
|
-
const nextContext = { ...context, conversationId: "conv-456" };
|
|
308
|
-
await f.lease.runBrowser(nextContext, operation);
|
|
309
|
-
await Bun.sleep(1_100);
|
|
310
|
-
expect(f.lease.isActive).toBe(true);
|
|
311
|
-
expect(f.released).toHaveBeenCalledTimes(1);
|
|
312
|
-
await f.lease.runBrowser(nextContext, operation, true);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test("cleanup retries preserve commands queued behind an explicit handoff", async () => {
|
|
316
|
-
const f = fixture();
|
|
317
|
-
await f.lease.runBrowser(context, operation);
|
|
318
|
-
f.releaseBrowser.mockRejectedValueOnce(new Error("Chrome cleanup timed out"));
|
|
319
|
-
f.lease.releaseForConversation(context.conversationId);
|
|
320
|
-
await Bun.sleep(0);
|
|
321
|
-
const cleanup = Promise.withResolvers<void>();
|
|
322
|
-
f.releaseBrowser.mockImplementationOnce(() => cleanup.promise);
|
|
323
|
-
const detached = f.lease.runBrowser(context, operation, true);
|
|
324
|
-
await Bun.sleep(0);
|
|
325
|
-
const callback = mock(operation);
|
|
326
|
-
const next = f.lease.runBrowser(context, callback);
|
|
327
|
-
await Bun.sleep(1_100);
|
|
328
|
-
cleanup.resolve();
|
|
329
|
-
await detached;
|
|
330
|
-
expect((await next).isError).toBe(false);
|
|
331
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
332
|
-
expect(f.lease.isActive).toBe(true);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test("turn completion cancels commands queued behind an in-progress detach", async () => {
|
|
336
|
-
const f = fixture();
|
|
337
|
-
await f.lease.runBrowser(context, operation);
|
|
338
|
-
const cleanup = Promise.withResolvers<void>();
|
|
339
|
-
f.releaseBrowser.mockImplementationOnce(() => cleanup.promise);
|
|
340
|
-
const detached = f.lease.runBrowser(context, operation, true);
|
|
341
|
-
await Bun.sleep(0);
|
|
342
|
-
const callback = mock(operation);
|
|
343
|
-
const queued = f.lease.runBrowser(context, callback);
|
|
344
|
-
f.lease.releaseForConversation(context.conversationId);
|
|
345
|
-
cleanup.resolve();
|
|
346
|
-
await detached;
|
|
347
|
-
expect((await queued).isError).toBe(true);
|
|
348
|
-
expect(callback).not.toHaveBeenCalled();
|
|
349
|
-
await f.lease.runBrowser(context, callback);
|
|
350
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
351
|
-
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getConfig } from "../config/loader.js";
|
|
2
|
-
import { broadcastMessage } from "../runtime/assistant-event-hub.js";
|
|
3
2
|
import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
4
3
|
import { getLogger } from "../util/logger.js";
|
|
5
4
|
import { desktopDependencyInstaller } from "./desktop-dependencies.js";
|
|
@@ -36,29 +35,14 @@ export class DesktopAutomationLease {
|
|
|
36
35
|
ready: () => boolean;
|
|
37
36
|
ensureReady: (signal: AbortSignal) => Promise<void>;
|
|
38
37
|
manager: () => DesktopSessionManager;
|
|
39
|
-
notify: () => Promise<unknown>;
|
|
40
38
|
} = {
|
|
41
39
|
enabled: () => isVirtualDesktopEnabled(getConfig()),
|
|
42
40
|
ready: () => desktopDependencyInstaller.getStatus().state === "ready",
|
|
43
41
|
ensureReady: (signal) => desktopDependencyInstaller.ensureReady(signal),
|
|
44
42
|
manager: getDesktopSessionManager,
|
|
45
|
-
notify: async () =>
|
|
46
|
-
broadcastMessage({ type: "desktop_activity_changed" }),
|
|
47
43
|
},
|
|
48
44
|
) {}
|
|
49
45
|
|
|
50
|
-
get isActive(): boolean {
|
|
51
|
-
return this.owner !== null && !this.owner.abort.signal.aborted;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private notify(): void {
|
|
55
|
-
void this.deps
|
|
56
|
-
.notify()
|
|
57
|
-
.catch((err) =>
|
|
58
|
-
log.warn({ err }, "Desktop browser activity notification failed"),
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
46
|
private exclusive<T>(run: () => Promise<T>): Promise<T> {
|
|
63
47
|
const next = this.tail.then(run, run);
|
|
64
48
|
this.tail = next.catch(() => {});
|
|
@@ -100,25 +84,9 @@ export class DesktopAutomationLease {
|
|
|
100
84
|
}
|
|
101
85
|
this.generation += 1;
|
|
102
86
|
owner.abort.abort();
|
|
103
|
-
this.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
private releaseCancelledOwner(owner: Owner): void {
|
|
107
|
-
void this.exclusive(async () => {
|
|
108
|
-
if (this.owner === owner) {
|
|
109
|
-
await this.release();
|
|
110
|
-
}
|
|
111
|
-
}).catch((err) => {
|
|
112
|
-
log.warn({ err }, "Desktop browser session cleanup failed");
|
|
113
|
-
const retry = setTimeout(() => this.releaseCancelledOwner(owner), 1_000);
|
|
114
|
-
retry.unref?.();
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
releaseForConversation(conversationId: string): void {
|
|
119
|
-
if (this.owner?.conversationId === conversationId) {
|
|
120
|
-
this.cancel(this.owner);
|
|
121
|
-
}
|
|
87
|
+
void this.exclusive(() => this.release()).catch((err) =>
|
|
88
|
+
log.warn({ err }, "Desktop browser session cleanup failed"),
|
|
89
|
+
);
|
|
122
90
|
}
|
|
123
91
|
|
|
124
92
|
private bindCancellation(owner: Owner, signal?: AbortSignal): void {
|
|
@@ -154,10 +122,6 @@ export class DesktopAutomationLease {
|
|
|
154
122
|
throw new Error("The desktop is busy or shutting down");
|
|
155
123
|
}
|
|
156
124
|
this.owner = owner;
|
|
157
|
-
owner.abort.signal.addEventListener("abort", () => this.notify(), {
|
|
158
|
-
once: true,
|
|
159
|
-
});
|
|
160
|
-
this.notify();
|
|
161
125
|
this.bindCancellation(owner, context.signal);
|
|
162
126
|
const cancel = () => this.cancel(owner);
|
|
163
127
|
this.watchdog = setInterval(() => {
|
|
@@ -30,9 +30,9 @@ import { CHANNEL_IDS } from "../channels/types.js";
|
|
|
30
30
|
* The schema passes through what it does not name, so a provider carries its
|
|
31
31
|
* own fields (Slack's file markers and timezone labels) on this same object
|
|
32
32
|
* and validates them with its own schema. This is how `SourceMetadataSchema`
|
|
33
|
-
* carries `
|
|
34
|
-
* row, no per-provider sub-envelope, and no second copy of
|
|
35
|
-
* here.
|
|
33
|
+
* already carries `slackBotMentioned` and the email fields on the wire: one
|
|
34
|
+
* object per row, no per-provider sub-envelope, and no second copy of
|
|
35
|
+
* anything named here.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
const providerReactionMetadataSchema = z.object({
|
|
@@ -503,76 +503,6 @@ describe("composeFallbackCopy plugin schedule templates", () => {
|
|
|
503
503
|
});
|
|
504
504
|
});
|
|
505
505
|
|
|
506
|
-
// ── activity.complete rendering ───────────────────────────────────────
|
|
507
|
-
|
|
508
|
-
describe("activity.complete copy", () => {
|
|
509
|
-
function completeSignal(
|
|
510
|
-
contextPayload: Record<string, unknown>,
|
|
511
|
-
): NotificationSignal {
|
|
512
|
-
return makeSignal({
|
|
513
|
-
sourceEventName: "activity.complete",
|
|
514
|
-
contextPayload,
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
test("a producer-authored title survives the fallback path", () => {
|
|
519
|
-
// The home feed reads the payload title first; the popup must say the
|
|
520
|
-
// same thing when the classifier was unreachable.
|
|
521
|
-
const copy = composeFallbackCopy(
|
|
522
|
-
completeSignal({
|
|
523
|
-
title: "Skill updated: Weekly Report Export",
|
|
524
|
-
summary: "Added the retry after an expired session.",
|
|
525
|
-
}),
|
|
526
|
-
CHANNELS,
|
|
527
|
-
);
|
|
528
|
-
expect(copy.vellum?.title).toBe("Skill updated: Weekly Report Export");
|
|
529
|
-
expect(copy.vellum?.body).toBe("Added the retry after an expired session.");
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
test("the pass-through's requestedTitle is honored when no title is set", () => {
|
|
533
|
-
const copy = composeFallbackCopy(
|
|
534
|
-
completeSignal({
|
|
535
|
-
requestedTitle: "Nightly export finished",
|
|
536
|
-
summary: "Wrote 12 files to the reports folder.",
|
|
537
|
-
}),
|
|
538
|
-
CHANNELS,
|
|
539
|
-
);
|
|
540
|
-
expect(copy.vellum?.title).toBe("Nightly export finished");
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
test("without an authored title the headline is the summary's first sentence", () => {
|
|
544
|
-
const copy = composeFallbackCopy(
|
|
545
|
-
completeSignal({
|
|
546
|
-
summary:
|
|
547
|
-
"Added the retry after an expired session. Dropped the login step.",
|
|
548
|
-
}),
|
|
549
|
-
CHANNELS,
|
|
550
|
-
);
|
|
551
|
-
expect(copy.vellum?.title).toBe(
|
|
552
|
-
"Added the retry after an expired session.",
|
|
553
|
-
);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
test("an authored title the normalizer rejects falls back to the derived one", () => {
|
|
557
|
-
const copy = composeFallbackCopy(
|
|
558
|
-
completeSignal({
|
|
559
|
-
title: "Skill\nupdated",
|
|
560
|
-
summary: "Added the retry after an expired session.",
|
|
561
|
-
}),
|
|
562
|
-
CHANNELS,
|
|
563
|
-
);
|
|
564
|
-
expect(copy.vellum?.title).toBe(
|
|
565
|
-
"Added the retry after an expired session.",
|
|
566
|
-
);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test("neither a title nor a summary still says something happened", () => {
|
|
570
|
-
const copy = composeFallbackCopy(completeSignal({}), CHANNELS);
|
|
571
|
-
expect(copy.vellum?.title).toBe("Activity complete");
|
|
572
|
-
expect(copy.vellum?.body).toBe("An activity has completed");
|
|
573
|
-
});
|
|
574
|
-
});
|
|
575
|
-
|
|
576
506
|
// ── deriveTitle ───────────────────────────────────────────────────────
|
|
577
507
|
|
|
578
508
|
describe("deriveTitle", () => {
|
|
@@ -346,22 +346,14 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
|
|
|
346
346
|
|
|
347
347
|
// Titled by what was done rather than by the kind of event: the summary's
|
|
348
348
|
// first sentence is the outcome ("Finished the fuel-system diagnostic app"),
|
|
349
|
-
// which is what a reader scanning the bell wants to see. A
|
|
350
|
-
//
|
|
351
|
-
// order the home feed reads it, so the headline is the same whether or not
|
|
352
|
-
// the classifier ran. A summary with neither still gets a title that says
|
|
353
|
-
// something happened.
|
|
349
|
+
// which is what a reader scanning the bell wants to see. A summary without
|
|
350
|
+
// one still gets a title that says something happened.
|
|
354
351
|
"activity.complete": (payload) => {
|
|
355
352
|
const summary = nonEmpty(
|
|
356
353
|
typeof payload.summary === "string" ? payload.summary : undefined,
|
|
357
354
|
);
|
|
358
|
-
const authored = normalizeTitle(
|
|
359
|
-
readPayloadString(payload, "title") ??
|
|
360
|
-
readPayloadString(payload, "requestedTitle") ??
|
|
361
|
-
"",
|
|
362
|
-
);
|
|
363
355
|
return {
|
|
364
|
-
title:
|
|
356
|
+
title: summary ? deriveTitle(summary) : "Activity complete",
|
|
365
357
|
body: summary ?? "An activity has completed",
|
|
366
358
|
};
|
|
367
359
|
},
|
|
@@ -1398,11 +1398,20 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1398
1398
|
"read_customers",
|
|
1399
1399
|
"write_customers",
|
|
1400
1400
|
"read_inventory",
|
|
1401
|
+
"write_inventory",
|
|
1401
1402
|
"read_locations",
|
|
1402
1403
|
"read_fulfillments",
|
|
1404
|
+
"write_fulfillments",
|
|
1403
1405
|
"read_discounts",
|
|
1404
1406
|
"write_discounts",
|
|
1405
1407
|
"read_price_rules",
|
|
1408
|
+
"write_price_rules",
|
|
1409
|
+
"read_content",
|
|
1410
|
+
"write_content",
|
|
1411
|
+
"read_reports",
|
|
1412
|
+
// Location, shipping, and marketing writes stay opt-in from the scope
|
|
1413
|
+
// picker: they change store setup rather than day-to-day operations,
|
|
1414
|
+
// and keeping them out of the defaults keeps the install consent short.
|
|
1406
1415
|
// write_themes covers listing, duplicating, and publishing themes.
|
|
1407
1416
|
// Writing theme files (settings, JSON templates, Liquid) also requires
|
|
1408
1417
|
// Shopify's theme-code exemption on the app itself, which is granted
|
|
@@ -1449,6 +1458,7 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1449
1458
|
description: "Adjust inventory levels and items",
|
|
1450
1459
|
},
|
|
1451
1460
|
{ scope: "read_locations", description: "Read store locations" },
|
|
1461
|
+
{ scope: "write_locations", description: "Add and edit store locations" },
|
|
1452
1462
|
{ scope: "read_fulfillments", description: "Read fulfillment services" },
|
|
1453
1463
|
{
|
|
1454
1464
|
scope: "write_fulfillments",
|
|
@@ -1468,6 +1478,14 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1468
1478
|
},
|
|
1469
1479
|
{ scope: "read_files", description: "Read files uploaded to the store" },
|
|
1470
1480
|
{ scope: "write_files", description: "Upload and update files" },
|
|
1481
|
+
{
|
|
1482
|
+
scope: "read_content",
|
|
1483
|
+
description: "Read blog posts, pages, and comments",
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
scope: "write_content",
|
|
1487
|
+
description: "Create and update blog posts, pages, and comments",
|
|
1488
|
+
},
|
|
1471
1489
|
{ scope: "read_gift_cards", description: "Read gift cards" },
|
|
1472
1490
|
{
|
|
1473
1491
|
scope: "write_gift_cards",
|
|
@@ -1481,11 +1499,23 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1481
1499
|
scope: "read_shipping",
|
|
1482
1500
|
description: "Read shipping and delivery carrier services",
|
|
1483
1501
|
},
|
|
1502
|
+
{
|
|
1503
|
+
scope: "write_shipping",
|
|
1504
|
+
description: "Create and update delivery carrier services",
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
scope: "read_reports",
|
|
1508
|
+
description: "Run ShopifyQL sales and analytics queries",
|
|
1509
|
+
},
|
|
1484
1510
|
{ scope: "read_analytics", description: "Read analytics and reports" },
|
|
1485
1511
|
{
|
|
1486
1512
|
scope: "read_marketing_events",
|
|
1487
1513
|
description: "Read marketing events and activities",
|
|
1488
1514
|
},
|
|
1515
|
+
{
|
|
1516
|
+
scope: "write_marketing_events",
|
|
1517
|
+
description: "Create and update marketing events and activities",
|
|
1518
|
+
},
|
|
1489
1519
|
{ scope: "read_order_edits", description: "Read order edits" },
|
|
1490
1520
|
{
|
|
1491
1521
|
scope: "write_order_edits",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ToolDefinition } from "../providers/types.js";
|
|
2
1
|
import type {
|
|
3
2
|
AddMessageOptions,
|
|
4
3
|
ConversationRow,
|
|
@@ -53,18 +52,6 @@ export async function getMessages(
|
|
|
53
52
|
return fn(conversationId);
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
/**
|
|
57
|
-
* The tool definitions the conversation's most recent live turn sent to the
|
|
58
|
-
* provider, or `null` when none has been recorded.
|
|
59
|
-
*/
|
|
60
|
-
export async function getConversationToolSurface(
|
|
61
|
-
conversationId: string,
|
|
62
|
-
): Promise<ToolDefinition[] | null> {
|
|
63
|
-
const { getConversationToolSurface: fn } =
|
|
64
|
-
await import("./conversation-tool-surface.js");
|
|
65
|
-
return fn(conversationId);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
55
|
/** Whether the conversation currently has a turn in flight. */
|
|
69
56
|
export async function isConversationProcessing(id: string): Promise<boolean> {
|
|
70
57
|
const { isConversationProcessing: fn } =
|
|
@@ -5,7 +5,6 @@ export * from "./calls.js";
|
|
|
5
5
|
export * from "./contacts.js";
|
|
6
6
|
export * from "./conversation-groups.js";
|
|
7
7
|
export * from "./conversation-starters.js";
|
|
8
|
-
export * from "./conversation-tool-surfaces.js";
|
|
9
8
|
export * from "./conversations.js";
|
|
10
9
|
export * from "./documents.js";
|
|
11
10
|
export * from "./guardian.js";
|
package/src/persistence/steps.ts
CHANGED
|
@@ -485,7 +485,6 @@ import { migrateChannelInboundMessageIdIndex } from "./migrations/374-channel-in
|
|
|
485
485
|
import { migrateCreateChannelOutboundPosts } from "./migrations/375-create-channel-outbound-posts.js";
|
|
486
486
|
import { migrateNotificationDeliveriesCanonicalMessageId } from "./migrations/376-notification-deliveries-canonical-message-id.js";
|
|
487
487
|
import { migrateAddSubagentBudgetStopReason } from "./migrations/377-add-subagent-budget-stop-reason.js";
|
|
488
|
-
import { migrateCreateConversationToolSurfaces } from "./migrations/378-create-conversation-tool-surfaces.js";
|
|
489
488
|
import type { MigrationStep } from "./migrations/run-migrations.js";
|
|
490
489
|
|
|
491
490
|
export const migrationSteps: MigrationStep[] = [
|
|
@@ -1612,5 +1611,4 @@ export const migrationSteps: MigrationStep[] = [
|
|
|
1612
1611
|
// this as a no-op.
|
|
1613
1612
|
dependsOn: ["migrateCreateSubagentsTable"],
|
|
1614
1613
|
},
|
|
1615
|
-
migrateCreateConversationToolSurfaces,
|
|
1616
1614
|
];
|
|
@@ -284,20 +284,13 @@ export async function runConversationTurn(
|
|
|
284
284
|
const { resolveChannelCapabilities } =
|
|
285
285
|
await import("../daemon/conversation-runtime-assembly.js");
|
|
286
286
|
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
287
|
+
// Plugin-driven turns run as the guardian: plugins are installed by the
|
|
288
|
+
// guardian, so their conversations inherit guardian trust. This lets the
|
|
289
|
+
// existing non-interactive auto-approve machinery handle tool permissions
|
|
290
|
+
// (the conversation is already non-interactive via `isInteractive: false`
|
|
291
|
+
// below) without requiring a client to approve prompts.
|
|
291
292
|
const { INTERNAL_GUARDIAN_TRUST_CONTEXT } =
|
|
292
293
|
await import("../daemon/trust-context.js");
|
|
293
|
-
const { resolvePluginChannelTurnTrust } =
|
|
294
|
-
await import("./plugin-channel-turn-trust.js");
|
|
295
|
-
const { prepareChannelInboundContent } =
|
|
296
|
-
await import("../runtime/routes/inbound-stages/inbound-content-prep.js");
|
|
297
|
-
|
|
298
|
-
const trustContext = options.channel
|
|
299
|
-
? await resolvePluginChannelTurnTrust(options.channel)
|
|
300
|
-
: INTERNAL_GUARDIAN_TRUST_CONTEXT;
|
|
301
294
|
|
|
302
295
|
// A channel address resolves through the same binding an inbound message
|
|
303
296
|
// uses, so a turn addressed by chat lands in that chat's conversation
|
|
@@ -318,7 +311,7 @@ export async function runConversationTurn(
|
|
|
318
311
|
? !channelConversation.created
|
|
319
312
|
: getConversation(conversationId) != null;
|
|
320
313
|
const conversation = await getOrCreateConversation(conversationId, {
|
|
321
|
-
trustContext,
|
|
314
|
+
trustContext: INTERNAL_GUARDIAN_TRUST_CONTEXT,
|
|
322
315
|
...(options.conversationType
|
|
323
316
|
? { conversationType: options.conversationType }
|
|
324
317
|
: {}),
|
|
@@ -343,23 +336,10 @@ export async function runConversationTurn(
|
|
|
343
336
|
|
|
344
337
|
// Convert ContentBlock[] input to the text + attachments shape the
|
|
345
338
|
// conversation's processMessage path expects.
|
|
346
|
-
const { text
|
|
339
|
+
const { text, attachments } = extractContentAndAttachments(
|
|
347
340
|
options.content,
|
|
348
341
|
resolveMediaSourceData,
|
|
349
342
|
);
|
|
350
|
-
const inboundContent = options.channel
|
|
351
|
-
? prepareChannelInboundContent({
|
|
352
|
-
trimmedContent: rawText,
|
|
353
|
-
trustClass: trustContext.trustClass,
|
|
354
|
-
sourceChannel: options.channel.sourceChannel,
|
|
355
|
-
requesterIdentifier:
|
|
356
|
-
options.channel.username ??
|
|
357
|
-
options.channel.externalUserId ??
|
|
358
|
-
undefined,
|
|
359
|
-
})
|
|
360
|
-
: { content: rawText, displayContent: undefined };
|
|
361
|
-
const text = inboundContent.content;
|
|
362
|
-
const displayContent = inboundContent.displayContent;
|
|
363
343
|
|
|
364
344
|
// The channel this turn speaks on. Runtime assembly reads the per-turn
|
|
365
345
|
// context first and falls back to the conversation's `originChannel`, then
|
|
@@ -429,8 +409,6 @@ export async function runConversationTurn(
|
|
|
429
409
|
requestId,
|
|
430
410
|
isInteractive: false,
|
|
431
411
|
metadata,
|
|
432
|
-
trustContext,
|
|
433
|
-
...(displayContent ? { displayContent } : {}),
|
|
434
412
|
});
|
|
435
413
|
if (enqueueResult.rejected) {
|
|
436
414
|
throw new Error(
|
|
@@ -454,8 +432,6 @@ export async function runConversationTurn(
|
|
|
454
432
|
onEvent,
|
|
455
433
|
isInteractive: false,
|
|
456
434
|
metadata,
|
|
457
|
-
trustContext,
|
|
458
|
-
...(displayContent ? { displayContent } : {}),
|
|
459
435
|
...(options.callSite ? { callSite: options.callSite } : {}),
|
|
460
436
|
});
|
|
461
437
|
|