@vellumai/assistant 0.12.2-staging.6 → 0.12.2-staging.7
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 +11 -2
- package/docs/desktop-browser-cli.md +4 -2
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/openapi.yaml +6 -2
- package/package.json +2 -2
- package/scripts/postinstall.ts +33 -0
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/src/__tests__/agent-loop.test.ts +124 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/conversation-agent-loop.test.ts +25 -0
- package/src/__tests__/db-conversation-tool-surface.test.ts +144 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/subagent-tool-gate-mode.test.ts +169 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +19 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/index.ts +6 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-tool-setup.ts +57 -1
- package/src/daemon/conversation.ts +17 -0
- package/src/daemon/daemon-control.ts +2 -6
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +38 -15
- package/src/desktop/desktop-automation-lease.test.ts +143 -0
- package/src/desktop/desktop-automation-lease.ts +39 -3
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/persistence/conversation-plugin-facade.ts +13 -0
- package/src/persistence/conversation-tool-surface.ts +86 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +22 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/steps.ts +2 -0
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +9 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +14 -2
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +320 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +55 -181
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- 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 +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +21 -0
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +86 -22
- package/src/plugins/defaults/memory/v3/pool-select.ts +10 -7
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/runtime/AGENTS.md +1 -1
- package/src/runtime/__tests__/agent-wake.test.ts +86 -1
- package/src/runtime/agent-wake.ts +20 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +7 -3
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/schedule/run-script.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +2 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +29 -0
- package/src/tools/terminal/safe-env.ts +30 -1
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
getMessages,
|
|
33
33
|
listInstalledSkills,
|
|
34
34
|
parseMessageMetadata,
|
|
35
|
+
safeStringSlice,
|
|
35
36
|
stringifyMessageContent,
|
|
36
37
|
VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
|
|
37
38
|
} from "@vellumai/plugin-api";
|
|
@@ -44,7 +45,7 @@ import {
|
|
|
44
45
|
timeLatencySubSpan,
|
|
45
46
|
} from "../../../../daemon/turn-latency-sub-spans.js";
|
|
46
47
|
import { enqueueMemoryJob } from "../../../../persistence/jobs-store.js";
|
|
47
|
-
import {
|
|
48
|
+
import { stripCommentLines } from "../host-utils.js";
|
|
48
49
|
import { getLogger } from "../logging.js";
|
|
49
50
|
import { type MemorySqlite, memorySqliteOrNull } from "../memory-db.js";
|
|
50
51
|
import { getWorkspaceDir, getWorkspacePromptPath } from "../paths.js";
|
|
@@ -635,8 +636,16 @@ interface SelectionRow {
|
|
|
635
636
|
* The row holds one section per slug: the selection's FIRST selected section
|
|
636
637
|
* (pool order) stands for the page; a page selected with no section (a card,
|
|
637
638
|
* or a section-less edge or learned line) logs none.
|
|
639
|
+
*
|
|
640
|
+
* A turn whose selector could not run (`selectorFailure`) logs no rows: its
|
|
641
|
+
* selections are the stable prefix kept unjudged, and the selection table is
|
|
642
|
+
* what the hot set's frecency and the learned-edge graph read as judgments.
|
|
643
|
+
* The pool record still carries the turn for the inspector.
|
|
638
644
|
*/
|
|
639
645
|
export function attributeSelections(result: OrchestrateResult): SelectionRow[] {
|
|
646
|
+
if (result.selectorFailure) {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
640
649
|
const core = new Set<Slug>(result.lanes.core);
|
|
641
650
|
const hot = new Set<Slug>(result.lanes.hot);
|
|
642
651
|
const fresh = new Set<Slug>(result.lanes.fresh);
|
|
@@ -6,52 +6,7 @@
|
|
|
6
6
|
* This module is side-effect free: importing it does not register any plugin.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const HIGH_SURROGATE_END = 0xdbff;
|
|
11
|
-
const LOW_SURROGATE_START = 0xdc00;
|
|
12
|
-
const LOW_SURROGATE_END = 0xdfff;
|
|
13
|
-
|
|
14
|
-
function isHighSurrogate(code: number): boolean {
|
|
15
|
-
return code >= HIGH_SURROGATE_START && code <= HIGH_SURROGATE_END;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isLowSurrogate(code: number): boolean {
|
|
19
|
-
return code >= LOW_SURROGATE_START && code <= LOW_SURROGATE_END;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Slice a string by code-unit indices without splitting a surrogate pair, so
|
|
24
|
-
* the result never ends or begins mid-emoji. Indices are clamped to the
|
|
25
|
-
* string bounds; a cut that would land between a high/low surrogate is nudged
|
|
26
|
-
* inward to the nearest whole code point.
|
|
27
|
-
*/
|
|
28
|
-
function safeStringSlice(
|
|
29
|
-
str: string,
|
|
30
|
-
start = 0,
|
|
31
|
-
end: number = str.length,
|
|
32
|
-
): string {
|
|
33
|
-
let safeStart = Math.max(0, Math.min(str.length, start));
|
|
34
|
-
let safeEnd = Math.max(safeStart, Math.min(str.length, end));
|
|
35
|
-
|
|
36
|
-
if (safeEnd < str.length && safeEnd > safeStart) {
|
|
37
|
-
const lastCode = str.charCodeAt(safeEnd - 1);
|
|
38
|
-
if (isHighSurrogate(lastCode)) {
|
|
39
|
-
safeEnd--;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (safeStart > 0 && safeStart < str.length) {
|
|
44
|
-
const firstCode = str.charCodeAt(safeStart);
|
|
45
|
-
if (isLowSurrogate(firstCode)) {
|
|
46
|
-
safeStart++;
|
|
47
|
-
if (safeStart > safeEnd) {
|
|
48
|
-
safeEnd = safeStart;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return str.slice(safeStart, safeEnd);
|
|
54
|
-
}
|
|
9
|
+
import { safeStringSlice } from "@vellumai/plugin-api";
|
|
55
10
|
|
|
56
11
|
/**
|
|
57
12
|
* Minimum number of characters to preserve when truncating.
|
package/src/runtime/AGENTS.md
CHANGED
|
@@ -303,4 +303,4 @@ The provider-level rate limiter (`providers/ratelimit.ts`) also logs warnings (m
|
|
|
303
303
|
|
|
304
304
|
HTTP is the sole transport for client-daemon communication. The runtime HTTP server (`assistant/src/runtime/http-server.ts`) is the canonical API surface. Clients connect via HTTP for request/response operations and SSE (`GET /v1/events`) for streaming server-to-client events.
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
A skill never calls the runtime HTTP API directly: every request from a skill targets the gateway, and `gateway-only-guard.test.ts` fails CI on a runtime-port URL in a skill. For configuration reads, use the CLI (`assistant config get`); for control-plane writes with no CLI verb, use `$INTERNAL_GATEWAY_BASE_URL`. See "Gateway-Only API Consumption" in `gateway/AGENTS.md` for the rule and its exceptions.
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* route captured calls back to the originating conversation's probe arrays.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
21
|
+
import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
22
22
|
|
|
23
23
|
import type { DiskPressureStatus } from "../../daemon/disk-pressure-guard.js";
|
|
24
|
+
import { desktopAutomationLease } from "../../desktop/desktop-automation-lease.js";
|
|
24
25
|
|
|
25
26
|
// ── Per-conversation capture registry ────────────────────────────────
|
|
26
27
|
//
|
|
@@ -501,6 +502,13 @@ function makeWakeConversation(options: {
|
|
|
501
502
|
`tools:${snapshotAllowedTools()?.join(",") ?? "all"}`,
|
|
502
503
|
);
|
|
503
504
|
},
|
|
505
|
+
// Mirrors Conversation.setPreactivatedSkillIds; the wake's tool-scope
|
|
506
|
+
// restore calls it, and a double without it throws inside the restore
|
|
507
|
+
// closure, leaving every field restored after it untouched.
|
|
508
|
+
preactivatedSkillIds: undefined as string[] | undefined,
|
|
509
|
+
setPreactivatedSkillIds(ids: string[] | undefined) {
|
|
510
|
+
this.preactivatedSkillIds = ids;
|
|
511
|
+
},
|
|
504
512
|
get wakePersonaOverride() {
|
|
505
513
|
return wakePersonaOverride;
|
|
506
514
|
},
|
|
@@ -1357,6 +1365,42 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1357
1365
|
expect(conversation.toolContextPin).toBeUndefined();
|
|
1358
1366
|
});
|
|
1359
1367
|
|
|
1368
|
+
test("applies wireToolDefinitions alongside the allowlist and restores it after the wake", async () => {
|
|
1369
|
+
const replay = [
|
|
1370
|
+
{ name: "remember", description: "Save", input_schema: {} },
|
|
1371
|
+
{ name: "bell_jingle", description: "Ring", input_schema: {} },
|
|
1372
|
+
];
|
|
1373
|
+
let replayDuringRun: unknown;
|
|
1374
|
+
const conversation = makeWakeConversation({
|
|
1375
|
+
runImpl: async (input) => {
|
|
1376
|
+
replayDuringRun = conversation.wireToolReplay;
|
|
1377
|
+
return runResult([
|
|
1378
|
+
...input,
|
|
1379
|
+
{ role: "assistant", content: [{ type: "text", text: "Saved." }] },
|
|
1380
|
+
]);
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
const result = await wakeAgentForOpportunity(
|
|
1385
|
+
{
|
|
1386
|
+
conversationId: conversation.conversationId,
|
|
1387
|
+
hint: "review for memories",
|
|
1388
|
+
source: "memory-retrospective",
|
|
1389
|
+
allowedTools: ["remember"],
|
|
1390
|
+
toolGateMode: "execution",
|
|
1391
|
+
wireToolDefinitions: replay,
|
|
1392
|
+
},
|
|
1393
|
+
{ resolveTarget: async () => conversation },
|
|
1394
|
+
);
|
|
1395
|
+
|
|
1396
|
+
expect(result).toEqual({ invoked: true, producedToolCalls: false });
|
|
1397
|
+
// The replay array is live on the conversation for the duration of the
|
|
1398
|
+
// run (the tool resolver returns it as the wire array)...
|
|
1399
|
+
expect(replayDuringRun).toEqual(replay);
|
|
1400
|
+
// ...and cleared alongside the allowlist + gate mode after the wake.
|
|
1401
|
+
expect(conversation.wireToolReplay).toBeUndefined();
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1360
1404
|
test("defaults to the wire gate mode when toolGateMode is absent", async () => {
|
|
1361
1405
|
let gateModeDuringRun: string | undefined;
|
|
1362
1406
|
const conversation = makeWakeConversation({
|
|
@@ -1517,6 +1561,47 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1517
1561
|
expect(conversation.pushedMessages[2]).toEqual(followupAssistant);
|
|
1518
1562
|
});
|
|
1519
1563
|
|
|
1564
|
+
for (const fails of [false, true]) {
|
|
1565
|
+
test(`releases desktop control before a ${fails ? "failed" : "completed"} wake drains the queue`, async () => {
|
|
1566
|
+
const conversation = makeWakeConversation({
|
|
1567
|
+
scriptedAssistant: {
|
|
1568
|
+
role: "assistant",
|
|
1569
|
+
content: [{ type: "text", text: "reply" }],
|
|
1570
|
+
},
|
|
1571
|
+
...(fails
|
|
1572
|
+
? {
|
|
1573
|
+
runImpl: async () => {
|
|
1574
|
+
throw new Error("wake failed");
|
|
1575
|
+
},
|
|
1576
|
+
}
|
|
1577
|
+
: {}),
|
|
1578
|
+
});
|
|
1579
|
+
const release = spyOn(desktopAutomationLease, "releaseForConversation");
|
|
1580
|
+
const setProcessing = conversation.setProcessing.bind(conversation);
|
|
1581
|
+
conversation.setProcessing = (processing) => {
|
|
1582
|
+
if (!processing) {
|
|
1583
|
+
expect(release).toHaveBeenCalledWith(conversation.conversationId);
|
|
1584
|
+
}
|
|
1585
|
+
setProcessing(processing);
|
|
1586
|
+
};
|
|
1587
|
+
try {
|
|
1588
|
+
const result = await wakeAgentForOpportunity(
|
|
1589
|
+
{
|
|
1590
|
+
conversationId: conversation.conversationId,
|
|
1591
|
+
hint: "x",
|
|
1592
|
+
source: "unit-test",
|
|
1593
|
+
},
|
|
1594
|
+
{ resolveTarget: async () => conversation },
|
|
1595
|
+
);
|
|
1596
|
+
expect(result.invoked).toBe(!fails);
|
|
1597
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
1598
|
+
expect(conversation.drainQueueCalls).toBe(1);
|
|
1599
|
+
} finally {
|
|
1600
|
+
release.mockRestore();
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1520
1605
|
test("marks processing true during the run and false afterwards", async () => {
|
|
1521
1606
|
const conversation = makeWakeConversation({
|
|
1522
1607
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
persistWakeTriggerMessage,
|
|
108
108
|
scopeWakeAllowedTools,
|
|
109
109
|
} from "../daemon/wake-conversation-ops.js";
|
|
110
|
+
import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
|
|
110
111
|
import {
|
|
111
112
|
recordCompactionEndBestEffort,
|
|
112
113
|
recordCompactionStartBestEffort,
|
|
@@ -118,7 +119,7 @@ import {
|
|
|
118
119
|
setAgentLoopExitReasonOnLatestLog,
|
|
119
120
|
} from "../persistence/llm-request-log-store.js";
|
|
120
121
|
import type { SystemPromptPersonaOverride } from "../prompts/system-prompt.js";
|
|
121
|
-
import type { Message } from "../providers/types.js";
|
|
122
|
+
import type { Message, ToolDefinition } from "../providers/types.js";
|
|
122
123
|
import {
|
|
123
124
|
type UntrustedContentSource,
|
|
124
125
|
wrapUntrustedContent,
|
|
@@ -350,6 +351,17 @@ export interface WakeOptions {
|
|
|
350
351
|
* skill-management authoring tools callable directly.
|
|
351
352
|
*/
|
|
352
353
|
preactivateSkillIds?: readonly string[];
|
|
354
|
+
/**
|
|
355
|
+
* Tool definitions to send verbatim as the wake's wire tool array, in place
|
|
356
|
+
* of the ones the conversation would resolve for itself. Used by fork-based
|
|
357
|
+
* memory retrospectives to replay the SOURCE conversation's recorded surface
|
|
358
|
+
* (`getConversationToolSurface`) so the provider prompt-cache prefix matches
|
|
359
|
+
* the source's live turns byte for byte. Definitions only: what may execute
|
|
360
|
+
* is still decided by `allowedTools` and the turn's own active set. Applied
|
|
361
|
+
* and restored alongside `allowedTools`; ignored when `allowedTools` is
|
|
362
|
+
* absent.
|
|
363
|
+
*/
|
|
364
|
+
wireToolDefinitions?: readonly ToolDefinition[];
|
|
353
365
|
/**
|
|
354
366
|
* Explicit persona/channel slugs for the wake's system-prompt build,
|
|
355
367
|
* applied to the conversation for the duration of the run and restored
|
|
@@ -1410,6 +1422,7 @@ export async function wakeAgentForOpportunity(
|
|
|
1410
1422
|
* is one function rather than a rebuild bolted onto either half.
|
|
1411
1423
|
*/
|
|
1412
1424
|
const restoreWakeTurnScope = (): void => {
|
|
1425
|
+
desktopAutomationLease.releaseForConversation(conversationId);
|
|
1413
1426
|
restoreWakeAllowedTools();
|
|
1414
1427
|
clearWakePersonaOverride();
|
|
1415
1428
|
syncWakeLoopSystemPrompt();
|
|
@@ -1422,9 +1435,12 @@ export async function wakeAgentForOpportunity(
|
|
|
1422
1435
|
restoreWakeToolScope = scopeWakeAllowedTools(
|
|
1423
1436
|
conversation,
|
|
1424
1437
|
new Set(opts.allowedTools),
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1438
|
+
{
|
|
1439
|
+
gateMode: opts.toolGateMode,
|
|
1440
|
+
toolContextPin: opts.toolContextPin,
|
|
1441
|
+
preactivateSkillIds: opts.preactivateSkillIds,
|
|
1442
|
+
wireToolDefinitions: opts.wireToolDefinitions,
|
|
1443
|
+
},
|
|
1428
1444
|
);
|
|
1429
1445
|
return true;
|
|
1430
1446
|
} catch (err) {
|
|
@@ -16,21 +16,6 @@ import {
|
|
|
16
16
|
isApprovalAction,
|
|
17
17
|
} from "./channel-approval-types.js";
|
|
18
18
|
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Constants
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Legacy actions that map to canonical ones during client rollout.
|
|
25
|
-
* All temporal/persistent approval variants collapse to approve_once.
|
|
26
|
-
* Keep until all clients are updated and no in-flight buttons remain.
|
|
27
|
-
*/
|
|
28
|
-
const LEGACY_ACTION_MAP: Record<string, string> = {
|
|
29
|
-
approve_10m: "approve_once",
|
|
30
|
-
approve_conversation: "approve_once",
|
|
31
|
-
approve_always: "approve_once",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
19
|
// ---------------------------------------------------------------------------
|
|
35
20
|
// Types
|
|
36
21
|
// ---------------------------------------------------------------------------
|
|
@@ -98,8 +83,8 @@ export async function processGuardianDecision(
|
|
|
98
83
|
): Promise<ProcessGuardianDecisionResult> {
|
|
99
84
|
const { requestId, conversationId, channel, actorContext } = params;
|
|
100
85
|
|
|
101
|
-
// 1.
|
|
102
|
-
const action =
|
|
86
|
+
// 1. Validate the action
|
|
87
|
+
const action = params.action;
|
|
103
88
|
if (!isApprovalAction(action)) {
|
|
104
89
|
return {
|
|
105
90
|
ok: false,
|
|
@@ -153,12 +153,6 @@ export interface GuardianReplyResult {
|
|
|
153
153
|
// Callback data parser — format: "apr:<requestId>:<action>"
|
|
154
154
|
// ---------------------------------------------------------------------------
|
|
155
155
|
|
|
156
|
-
const LEGACY_CALLBACK_MAP: Record<string, string> = {
|
|
157
|
-
approve_10m: "approve_once",
|
|
158
|
-
approve_conversation: "approve_once",
|
|
159
|
-
approve_always: "approve_once",
|
|
160
|
-
};
|
|
161
|
-
|
|
162
156
|
interface ParsedCallback {
|
|
163
157
|
requestId: string;
|
|
164
158
|
action: ApprovalAction;
|
|
@@ -170,8 +164,7 @@ function parseCallbackAction(data: string): ParsedCallback | null {
|
|
|
170
164
|
return null;
|
|
171
165
|
}
|
|
172
166
|
const requestId = parts[1];
|
|
173
|
-
const
|
|
174
|
-
const action = LEGACY_CALLBACK_MAP[rawAction] ?? rawAction;
|
|
167
|
+
const action = parts.slice(2).join(":");
|
|
175
168
|
if (!requestId || !isApprovalAction(action)) {
|
|
176
169
|
return null;
|
|
177
170
|
}
|
|
@@ -29,13 +29,6 @@ export function requiredDecisionKeywords(
|
|
|
29
29
|
// Callback data parser — format: "apr:<requestId>:<action>"
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
|
|
32
|
-
/** Map legacy callback actions to canonical ones for in-flight buttons. */
|
|
33
|
-
const LEGACY_CALLBACK_MAP: Record<string, string> = {
|
|
34
|
-
approve_10m: "approve_once",
|
|
35
|
-
approve_conversation: "approve_once",
|
|
36
|
-
approve_always: "approve_once",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
32
|
export function parseCallbackData(
|
|
40
33
|
data: string,
|
|
41
34
|
sourceChannel?: string,
|
|
@@ -45,8 +38,7 @@ export function parseCallbackData(
|
|
|
45
38
|
return null;
|
|
46
39
|
}
|
|
47
40
|
const requestId = parts[1];
|
|
48
|
-
const
|
|
49
|
-
const action = LEGACY_CALLBACK_MAP[rawAction] ?? rawAction;
|
|
41
|
+
const action = parts.slice(2).join(":");
|
|
50
42
|
if (!requestId || !isApprovalAction(action)) {
|
|
51
43
|
return null;
|
|
52
44
|
}
|
|
@@ -79,8 +79,8 @@ describe("desktop setup route feature gate", () => {
|
|
|
79
79
|
setOverridesForTesting({ "assistant-desktop": true });
|
|
80
80
|
expect(route.handler({})).toEqual(
|
|
81
81
|
route.method === "GET"
|
|
82
|
-
? { state: "required" }
|
|
83
|
-
: { state: "installing", stage: "packages" },
|
|
82
|
+
? { state: "required", automationActive: false }
|
|
83
|
+
: { state: "installing", stage: "packages", automationActive: false },
|
|
84
84
|
);
|
|
85
85
|
expect(route.method === "GET" ? status : start).toHaveBeenCalledTimes(1);
|
|
86
86
|
expect(route.method === "GET" ? start : status).not.toHaveBeenCalled();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import { getConfig } from "../../config/loader.js";
|
|
4
|
+
import { desktopAutomationLease } from "../../desktop/desktop-automation-lease.js";
|
|
4
5
|
import { desktopDependencyInstaller } from "../../desktop/desktop-dependencies.js";
|
|
5
6
|
import { isVirtualDesktopEnabled } from "../../desktop/virtual-desktop-feature.js";
|
|
6
7
|
import { GATEWAY_PRINCIPALS } from "../auth/route-policy.js";
|
|
@@ -9,6 +10,7 @@ import type { RouteDefinition } from "./types.js";
|
|
|
9
10
|
|
|
10
11
|
const statusSchema = z.object({
|
|
11
12
|
state: z.enum(["required", "installing", "ready", "failed", "unsupported"]),
|
|
13
|
+
automationActive: z.boolean().optional(),
|
|
12
14
|
stage: z.enum(["packages", "chrome", "checking"]).optional(),
|
|
13
15
|
});
|
|
14
16
|
|
|
@@ -24,9 +26,11 @@ export const ROUTES: RouteDefinition[] = ["GET", "POST"].map((method) => ({
|
|
|
24
26
|
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
25
27
|
);
|
|
26
28
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const status =
|
|
30
|
+
method === "GET"
|
|
31
|
+
? desktopDependencyInstaller.getStatus()
|
|
32
|
+
: desktopDependencyInstaller.start();
|
|
33
|
+
return { ...status, automationActive: desktopAutomationLease.isActive };
|
|
30
34
|
},
|
|
31
35
|
summary:
|
|
32
36
|
method === "GET"
|
|
@@ -299,6 +299,30 @@ export async function handleApprovalInterception(
|
|
|
299
299
|
|
|
300
300
|
return { handled: true, type: "stale_ignored" };
|
|
301
301
|
}
|
|
302
|
+
|
|
303
|
+
// An approval callback that names no approval action is a button press
|
|
304
|
+
// the daemon does not serve (an id outside the decision vocabulary, or an
|
|
305
|
+
// answer token aimed at a card this rail does not resolve). It is never
|
|
306
|
+
// text: the channel normalizers copy callback data into `content`, so
|
|
307
|
+
// letting it fall through would hand the raw callback to the
|
|
308
|
+
// conversational and natural-language parsers, which can read a token
|
|
309
|
+
// like `approve_always` as an approval. Consume it as a stale button.
|
|
310
|
+
if (callbackData.startsWith("apr:")) {
|
|
311
|
+
log.warn(
|
|
312
|
+
{ conversationId, callbackData },
|
|
313
|
+
"Approval callback carries no approval action, ignoring stale button press",
|
|
314
|
+
);
|
|
315
|
+
if (approvalMessageId) {
|
|
316
|
+
editStaleApprovalMessage({
|
|
317
|
+
replyCallbackUrl,
|
|
318
|
+
chatId: conversationExternalId,
|
|
319
|
+
messageId: approvalMessageId,
|
|
320
|
+
assistantId,
|
|
321
|
+
conversationId,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return { handled: true, type: "stale_ignored" };
|
|
325
|
+
}
|
|
302
326
|
}
|
|
303
327
|
|
|
304
328
|
// ── Conversational approval engine for plain-text messages ──
|
|
@@ -1531,8 +1531,7 @@ export async function handleChannelInbound({
|
|
|
1531
1531
|
sourceMetadata.account.length > 0
|
|
1532
1532
|
? sourceMetadata.account
|
|
1533
1533
|
: undefined;
|
|
1534
|
-
const
|
|
1535
|
-
sourceChannel === "slack" && sourceMetadata?.slackBotMentioned === true;
|
|
1534
|
+
const botMentioned = sourceMetadata?.botMentioned === true;
|
|
1536
1535
|
|
|
1537
1536
|
// ── DM cold-start backfill ──
|
|
1538
1537
|
// First time a Slack DM without thread_ts lands in a conversation that
|
|
@@ -1625,7 +1624,7 @@ export async function handleChannelInbound({
|
|
|
1625
1624
|
approvalCopyGenerator,
|
|
1626
1625
|
chatType: sourceChatType,
|
|
1627
1626
|
clientTimezone: inboundClientTimezone,
|
|
1628
|
-
|
|
1627
|
+
botMentioned,
|
|
1629
1628
|
slackInbound,
|
|
1630
1629
|
channelInbound,
|
|
1631
1630
|
});
|
|
@@ -1169,7 +1169,7 @@ describe("channel activity timing", () => {
|
|
|
1169
1169
|
externalChatId: channelId,
|
|
1170
1170
|
trustCtx,
|
|
1171
1171
|
metadataHints: [],
|
|
1172
|
-
|
|
1172
|
+
botMentioned: true,
|
|
1173
1173
|
replyCallbackUrl: `https://example.test/deliver/slack?channel=${channelId}&threadTs=${threadTs}`,
|
|
1174
1174
|
});
|
|
1175
1175
|
|
|
@@ -105,8 +105,11 @@ export interface BackgroundProcessingParams {
|
|
|
105
105
|
chatType?: string;
|
|
106
106
|
/** IANA timezone reported by the active client for the current turn. */
|
|
107
107
|
clientTimezone?: string;
|
|
108
|
-
/**
|
|
109
|
-
|
|
108
|
+
/**
|
|
109
|
+
* The message addresses the assistant by name, as stated by the channel
|
|
110
|
+
* (`sourceMetadata.botMentioned`). Absent means not established.
|
|
111
|
+
*/
|
|
112
|
+
botMentioned?: boolean;
|
|
110
113
|
/**
|
|
111
114
|
* Slack-specific inbound metadata extracted at the HTTP boundary. Threaded
|
|
112
115
|
* through to `persistUserMessage` so the row can be tagged with a
|
|
@@ -166,7 +169,7 @@ export function processChannelMessageInBackground(
|
|
|
166
169
|
sourceLanguageCode,
|
|
167
170
|
chatType,
|
|
168
171
|
clientTimezone,
|
|
169
|
-
|
|
172
|
+
botMentioned,
|
|
170
173
|
slackInbound,
|
|
171
174
|
channelInbound,
|
|
172
175
|
slackReactionRowMeta,
|
|
@@ -204,7 +207,7 @@ export function processChannelMessageInBackground(
|
|
|
204
207
|
initiatorUserId: slackInbound?.actorExternalUserId,
|
|
205
208
|
startImmediately: shouldShowActivityImmediately({
|
|
206
209
|
chatType,
|
|
207
|
-
botMentioned
|
|
210
|
+
botMentioned,
|
|
208
211
|
}),
|
|
209
212
|
});
|
|
210
213
|
const stopApprovalWatcher = replyCallbackUrl
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildSanitizedEnv } from "../tools/terminal/safe-env.js";
|
|
2
2
|
import {
|
|
3
3
|
buildShellInvocation,
|
|
4
|
+
buildShellSpawnFlags,
|
|
4
5
|
terminateProcessTree,
|
|
5
6
|
} from "../util/host-process.js";
|
|
6
7
|
import { getLogger } from "../util/logger.js";
|
|
@@ -64,10 +65,9 @@ export async function runScript(
|
|
|
64
65
|
const shell = buildShellInvocation(command);
|
|
65
66
|
const proc = Bun.spawn([shell.command, ...shell.args], {
|
|
66
67
|
cwd,
|
|
67
|
-
|
|
68
|
+
...buildShellSpawnFlags(),
|
|
68
69
|
stdout: "pipe",
|
|
69
70
|
stderr: "pipe",
|
|
70
|
-
windowsHide: true,
|
|
71
71
|
env: {
|
|
72
72
|
...buildSanitizedEnv(),
|
|
73
73
|
// __SCHEDULE_ID lets a saved command find its own dir; __SCHEDULE_RUN_ID
|
|
@@ -23,8 +23,10 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
|
|
|
23
23
|
import { redactSecrets } from "../../security/secret-scanner.js";
|
|
24
24
|
import {
|
|
25
25
|
buildShellInvocation,
|
|
26
|
+
buildShellSpawnFlags,
|
|
26
27
|
prependUniquePathEntries,
|
|
27
28
|
terminateProcessTree,
|
|
29
|
+
watchShellProcessStart,
|
|
28
30
|
} from "../../util/host-process.js";
|
|
29
31
|
import { getLogger } from "../../util/logger.js";
|
|
30
32
|
import type { CompletedBackgroundTool } from "../background-tool-registry.js";
|
|
@@ -455,9 +457,9 @@ export const hostShellTool = {
|
|
|
455
457
|
cwd: workingDir,
|
|
456
458
|
env: hostEnv,
|
|
457
459
|
stdio: ["ignore", "pipe", "pipe"],
|
|
458
|
-
|
|
459
|
-
windowsHide: true,
|
|
460
|
+
...buildShellSpawnFlags(),
|
|
460
461
|
});
|
|
462
|
+
const launch = watchShellProcessStart(child);
|
|
461
463
|
|
|
462
464
|
const collector = attachBoundedStdio(child);
|
|
463
465
|
let timedOut = false;
|
|
@@ -483,7 +485,9 @@ export const hostShellTool = {
|
|
|
483
485
|
}
|
|
484
486
|
completed = true;
|
|
485
487
|
clearTimeout(timer);
|
|
486
|
-
const result = collector.format(code, timedOut, timeoutSec
|
|
488
|
+
const result = collector.format(code, timedOut, timeoutSec, {
|
|
489
|
+
started: launch.didStart(),
|
|
490
|
+
});
|
|
487
491
|
// Cancel takes precedence over the SIGKILL-induced error result.
|
|
488
492
|
const status = aborted
|
|
489
493
|
? "cancelled"
|
|
@@ -629,9 +633,9 @@ export const hostShellTool = {
|
|
|
629
633
|
cwd: workingDir,
|
|
630
634
|
env: hostEnv,
|
|
631
635
|
stdio: ["ignore", "pipe", "pipe"],
|
|
632
|
-
|
|
633
|
-
windowsHide: true,
|
|
636
|
+
...buildShellSpawnFlags(),
|
|
634
637
|
});
|
|
638
|
+
const launch = watchShellProcessStart(child);
|
|
635
639
|
const collector = attachBoundedStdio(child, {
|
|
636
640
|
onOutput: context.onOutput,
|
|
637
641
|
});
|
|
@@ -657,7 +661,9 @@ export const hostShellTool = {
|
|
|
657
661
|
clearTimeout(timer);
|
|
658
662
|
context.signal?.removeEventListener("abort", onAbort);
|
|
659
663
|
|
|
660
|
-
const result = collector.format(code, timedOut, timeoutSec
|
|
664
|
+
const result = collector.format(code, timedOut, timeoutSec, {
|
|
665
|
+
started: launch.didStart(),
|
|
666
|
+
});
|
|
661
667
|
|
|
662
668
|
resolve({
|
|
663
669
|
content: result.content,
|
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
|
|
6
6
|
import { ensureDir, pathExists } from "../../../util/fs.js";
|
|
7
|
+
import { surrogateSafeWindow } from "../../../util/unicode.js";
|
|
7
8
|
import { isAbortLikeError } from "../abort.js";
|
|
8
9
|
import { applyEdit } from "./edit-engine.js";
|
|
9
10
|
import * as Err from "./errors.js";
|
|
@@ -102,37 +103,6 @@ function truncationNotice(
|
|
|
102
103
|
return `\n\n[Truncated: characters ${start}-${end} of ${totalChars}. Read on with start_index=${end}.]`;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
const isHighSurrogate = (code: number): boolean =>
|
|
106
|
-
code >= 0xd800 && code <= 0xdbff;
|
|
107
|
-
const isLowSurrogate = (code: number): boolean =>
|
|
108
|
-
code >= 0xdc00 && code <= 0xdfff;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Character window that never splits a surrogate pair. A split leaves a lone
|
|
112
|
-
* half at each edge, and each encodes to U+FFFD, so the character is lost from
|
|
113
|
-
* both this window and the next one paged in after it.
|
|
114
|
-
*/
|
|
115
|
-
export function surrogateSafeWindow(
|
|
116
|
-
total: number,
|
|
117
|
-
charCodeAt: (index: number) => number,
|
|
118
|
-
requestedStart: number,
|
|
119
|
-
maxChars: number,
|
|
120
|
-
): { start: number; end: number } {
|
|
121
|
-
let start = Math.max(0, Math.min(requestedStart, total));
|
|
122
|
-
if (start > 0 && start < total && isLowSurrogate(charCodeAt(start))) {
|
|
123
|
-
start -= 1;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let end = Math.min(total, start + maxChars);
|
|
127
|
-
if (end > start && end < total && isHighSurrogate(charCodeAt(end - 1))) {
|
|
128
|
-
// Backing off would empty a one-character window, which stalls paging on
|
|
129
|
-
// the same offset, so take the whole pair instead.
|
|
130
|
-
end = end - 1 > start ? end - 1 : Math.min(total, end + 1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { start, end };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
106
|
export class FileSystemOps {
|
|
137
107
|
private policy: PathPolicy;
|
|
138
108
|
private sizeLimit: number | undefined;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
formatShellOutput,
|
|
8
8
|
MAX_OUTPUT_LENGTH,
|
|
9
9
|
OUTPUT_TRUNCATED_TAG,
|
|
10
|
+
SHELL_DID_NOT_START_MESSAGE,
|
|
10
11
|
} from "./shell-output.js";
|
|
11
12
|
|
|
12
13
|
describe("BoundedStdioCollector", () => {
|
|
@@ -85,6 +86,15 @@ describe("attachBoundedStdio", () => {
|
|
|
85
86
|
});
|
|
86
87
|
});
|
|
87
88
|
|
|
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
|
+
|
|
88
98
|
describe("formatShellOutput truncation", () => {
|
|
89
99
|
test("truncates an already-materialized oversized string without a file path", () => {
|
|
90
100
|
const longOutput = "x".repeat(30_000);
|