@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
|
@@ -42,11 +42,9 @@
|
|
|
42
42
|
* - infrastructure failure (selector provider unavailable — e.g. a transient
|
|
43
43
|
* CES credential blip drops the API key — or no usable `tool_use` / schema
|
|
44
44
|
* mismatch surviving the short re-prompt retry) → throw
|
|
45
|
-
* {@link MemoryV3RetrievalUnavailableError}. The
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* that prefix and queues a notice that the turn drew on core memories
|
|
49
|
-
* only.
|
|
45
|
+
* {@link MemoryV3RetrievalUnavailableError}. The live injector treats this
|
|
46
|
+
* as a logged memory miss for the turn; shadow/observation callers swallow
|
|
47
|
+
* it so v2 retrieval can serve the turn.
|
|
50
48
|
*/
|
|
51
49
|
|
|
52
50
|
import type {
|
|
@@ -54,12 +52,12 @@ import type {
|
|
|
54
52
|
Message,
|
|
55
53
|
ToolUseContent,
|
|
56
54
|
} from "@vellumai/plugin-api";
|
|
57
|
-
import { getConfiguredProvider
|
|
55
|
+
import { getConfiguredProvider } from "@vellumai/plugin-api";
|
|
58
56
|
import { z } from "zod";
|
|
59
57
|
|
|
60
58
|
import { classifyConversationError } from "../../../../daemon/conversation-error.js";
|
|
61
59
|
import type { PendingConversationNotice } from "../../../../daemon/conversation-notices.js";
|
|
62
|
-
import { redactLogString, truncate } from "../host-utils.js";
|
|
60
|
+
import { redactLogString, safeStringSlice, truncate } from "../host-utils.js";
|
|
63
61
|
import {
|
|
64
62
|
cachedTextBlock,
|
|
65
63
|
extractToolUse,
|
|
@@ -502,9 +500,8 @@ export interface PoolSelection {
|
|
|
502
500
|
*
|
|
503
501
|
* An omitted `ids` keeps ALL candidates (the recall-safe "all of these are
|
|
504
502
|
* relevant" signal, `keptAll: true`); an explicit `[]` keeps none; an
|
|
505
|
-
* infrastructure failure (after a short re-prompt retry)
|
|
506
|
-
*
|
|
507
|
-
* stable prefix unjudged in its place.
|
|
503
|
+
* infrastructure failure (after a short re-prompt retry) keeps none, degrading
|
|
504
|
+
* to the deterministic recall lanes the orchestrator unions in.
|
|
508
505
|
*
|
|
509
506
|
* `systemPrompt` is the selector's instruction scaffold; it defaults to the
|
|
510
507
|
* bundled {@link SYSTEM_PROMPT} and is overridable via `memory.v3.selectorPromptPath`
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
getMessages,
|
|
33
33
|
listInstalledSkills,
|
|
34
34
|
parseMessageMetadata,
|
|
35
|
-
safeStringSlice,
|
|
36
35
|
stringifyMessageContent,
|
|
37
36
|
VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
|
|
38
37
|
} from "@vellumai/plugin-api";
|
|
@@ -45,7 +44,7 @@ import {
|
|
|
45
44
|
timeLatencySubSpan,
|
|
46
45
|
} from "../../../../daemon/turn-latency-sub-spans.js";
|
|
47
46
|
import { enqueueMemoryJob } from "../../../../persistence/jobs-store.js";
|
|
48
|
-
import { stripCommentLines } from "../host-utils.js";
|
|
47
|
+
import { safeStringSlice, stripCommentLines } from "../host-utils.js";
|
|
49
48
|
import { getLogger } from "../logging.js";
|
|
50
49
|
import { type MemorySqlite, memorySqliteOrNull } from "../memory-db.js";
|
|
51
50
|
import { getWorkspaceDir, getWorkspacePromptPath } from "../paths.js";
|
|
@@ -636,16 +635,8 @@ interface SelectionRow {
|
|
|
636
635
|
* The row holds one section per slug: the selection's FIRST selected section
|
|
637
636
|
* (pool order) stands for the page; a page selected with no section (a card,
|
|
638
637
|
* 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.
|
|
644
638
|
*/
|
|
645
639
|
export function attributeSelections(result: OrchestrateResult): SelectionRow[] {
|
|
646
|
-
if (result.selectorFailure) {
|
|
647
|
-
return [];
|
|
648
|
-
}
|
|
649
640
|
const core = new Set<Slug>(result.lanes.core);
|
|
650
641
|
const hot = new Set<Slug>(result.lanes.hot);
|
|
651
642
|
const fresh = new Set<Slug>(result.lanes.fresh);
|
|
@@ -6,7 +6,52 @@
|
|
|
6
6
|
* This module is side-effect free: importing it does not register any plugin.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const HIGH_SURROGATE_START = 0xd800;
|
|
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
|
+
}
|
|
10
55
|
|
|
11
56
|
/**
|
|
12
57
|
* 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
|
+
When writing skills that need to call daemon configuration endpoints, use `curl` with the runtime HTTP API (JWT-authenticated via `Authorization: Bearer <jwt>`). The assistant already knows how to use `curl`.
|
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
* route captured calls back to the originating conversation's probe arrays.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { beforeEach, describe, expect, mock,
|
|
21
|
+
import { beforeEach, describe, expect, mock, 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";
|
|
25
24
|
|
|
26
25
|
// ── Per-conversation capture registry ────────────────────────────────
|
|
27
26
|
//
|
|
@@ -502,13 +501,6 @@ function makeWakeConversation(options: {
|
|
|
502
501
|
`tools:${snapshotAllowedTools()?.join(",") ?? "all"}`,
|
|
503
502
|
);
|
|
504
503
|
},
|
|
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
|
-
},
|
|
512
504
|
get wakePersonaOverride() {
|
|
513
505
|
return wakePersonaOverride;
|
|
514
506
|
},
|
|
@@ -1365,42 +1357,6 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1365
1357
|
expect(conversation.toolContextPin).toBeUndefined();
|
|
1366
1358
|
});
|
|
1367
1359
|
|
|
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
|
-
|
|
1404
1360
|
test("defaults to the wire gate mode when toolGateMode is absent", async () => {
|
|
1405
1361
|
let gateModeDuringRun: string | undefined;
|
|
1406
1362
|
const conversation = makeWakeConversation({
|
|
@@ -1561,47 +1517,6 @@ describe("wakeAgentForOpportunity", () => {
|
|
|
1561
1517
|
expect(conversation.pushedMessages[2]).toEqual(followupAssistant);
|
|
1562
1518
|
});
|
|
1563
1519
|
|
|
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
|
-
|
|
1605
1520
|
test("marks processing true during the run and false afterwards", async () => {
|
|
1606
1521
|
const conversation = makeWakeConversation({
|
|
1607
1522
|
baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
@@ -107,7 +107,6 @@ import {
|
|
|
107
107
|
persistWakeTriggerMessage,
|
|
108
108
|
scopeWakeAllowedTools,
|
|
109
109
|
} from "../daemon/wake-conversation-ops.js";
|
|
110
|
-
import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
|
|
111
110
|
import {
|
|
112
111
|
recordCompactionEndBestEffort,
|
|
113
112
|
recordCompactionStartBestEffort,
|
|
@@ -119,7 +118,7 @@ import {
|
|
|
119
118
|
setAgentLoopExitReasonOnLatestLog,
|
|
120
119
|
} from "../persistence/llm-request-log-store.js";
|
|
121
120
|
import type { SystemPromptPersonaOverride } from "../prompts/system-prompt.js";
|
|
122
|
-
import type { Message
|
|
121
|
+
import type { Message } from "../providers/types.js";
|
|
123
122
|
import {
|
|
124
123
|
type UntrustedContentSource,
|
|
125
124
|
wrapUntrustedContent,
|
|
@@ -351,17 +350,6 @@ export interface WakeOptions {
|
|
|
351
350
|
* skill-management authoring tools callable directly.
|
|
352
351
|
*/
|
|
353
352
|
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[];
|
|
365
353
|
/**
|
|
366
354
|
* Explicit persona/channel slugs for the wake's system-prompt build,
|
|
367
355
|
* applied to the conversation for the duration of the run and restored
|
|
@@ -1422,7 +1410,6 @@ export async function wakeAgentForOpportunity(
|
|
|
1422
1410
|
* is one function rather than a rebuild bolted onto either half.
|
|
1423
1411
|
*/
|
|
1424
1412
|
const restoreWakeTurnScope = (): void => {
|
|
1425
|
-
desktopAutomationLease.releaseForConversation(conversationId);
|
|
1426
1413
|
restoreWakeAllowedTools();
|
|
1427
1414
|
clearWakePersonaOverride();
|
|
1428
1415
|
syncWakeLoopSystemPrompt();
|
|
@@ -1435,12 +1422,9 @@ export async function wakeAgentForOpportunity(
|
|
|
1435
1422
|
restoreWakeToolScope = scopeWakeAllowedTools(
|
|
1436
1423
|
conversation,
|
|
1437
1424
|
new Set(opts.allowedTools),
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
preactivateSkillIds: opts.preactivateSkillIds,
|
|
1442
|
-
wireToolDefinitions: opts.wireToolDefinitions,
|
|
1443
|
-
},
|
|
1425
|
+
opts.toolGateMode,
|
|
1426
|
+
opts.toolContextPin,
|
|
1427
|
+
opts.preactivateSkillIds,
|
|
1444
1428
|
);
|
|
1445
1429
|
return true;
|
|
1446
1430
|
} catch (err) {
|
|
@@ -16,6 +16,21 @@ 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
|
+
|
|
19
34
|
// ---------------------------------------------------------------------------
|
|
20
35
|
// Types
|
|
21
36
|
// ---------------------------------------------------------------------------
|
|
@@ -83,8 +98,8 @@ export async function processGuardianDecision(
|
|
|
83
98
|
): Promise<ProcessGuardianDecisionResult> {
|
|
84
99
|
const { requestId, conversationId, channel, actorContext } = params;
|
|
85
100
|
|
|
86
|
-
// 1.
|
|
87
|
-
const action = params.action;
|
|
101
|
+
// 1. Canonicalize legacy actions, then validate
|
|
102
|
+
const action = LEGACY_ACTION_MAP[params.action] ?? params.action;
|
|
88
103
|
if (!isApprovalAction(action)) {
|
|
89
104
|
return {
|
|
90
105
|
ok: false,
|
|
@@ -153,6 +153,12 @@ 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
|
+
|
|
156
162
|
interface ParsedCallback {
|
|
157
163
|
requestId: string;
|
|
158
164
|
action: ApprovalAction;
|
|
@@ -164,7 +170,8 @@ function parseCallbackAction(data: string): ParsedCallback | null {
|
|
|
164
170
|
return null;
|
|
165
171
|
}
|
|
166
172
|
const requestId = parts[1];
|
|
167
|
-
const
|
|
173
|
+
const rawAction = parts.slice(2).join(":");
|
|
174
|
+
const action = LEGACY_CALLBACK_MAP[rawAction] ?? rawAction;
|
|
168
175
|
if (!requestId || !isApprovalAction(action)) {
|
|
169
176
|
return null;
|
|
170
177
|
}
|
|
@@ -29,6 +29,13 @@ 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
|
+
|
|
32
39
|
export function parseCallbackData(
|
|
33
40
|
data: string,
|
|
34
41
|
sourceChannel?: string,
|
|
@@ -38,7 +45,8 @@ export function parseCallbackData(
|
|
|
38
45
|
return null;
|
|
39
46
|
}
|
|
40
47
|
const requestId = parts[1];
|
|
41
|
-
const
|
|
48
|
+
const rawAction = parts.slice(2).join(":");
|
|
49
|
+
const action = LEGACY_CALLBACK_MAP[rawAction] ?? rawAction;
|
|
42
50
|
if (!requestId || !isApprovalAction(action)) {
|
|
43
51
|
return null;
|
|
44
52
|
}
|
|
@@ -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" }
|
|
83
|
+
: { state: "installing", stage: "packages" },
|
|
84
84
|
);
|
|
85
85
|
expect(route.method === "GET" ? status : start).toHaveBeenCalledTimes(1);
|
|
86
86
|
expect(route.method === "GET" ? start : status).not.toHaveBeenCalled();
|
|
@@ -1,7 +1,6 @@
|
|
|
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";
|
|
5
4
|
import { desktopDependencyInstaller } from "../../desktop/desktop-dependencies.js";
|
|
6
5
|
import { isVirtualDesktopEnabled } from "../../desktop/virtual-desktop-feature.js";
|
|
7
6
|
import { GATEWAY_PRINCIPALS } from "../auth/route-policy.js";
|
|
@@ -10,7 +9,6 @@ import type { RouteDefinition } from "./types.js";
|
|
|
10
9
|
|
|
11
10
|
const statusSchema = z.object({
|
|
12
11
|
state: z.enum(["required", "installing", "ready", "failed", "unsupported"]),
|
|
13
|
-
automationActive: z.boolean().optional(),
|
|
14
12
|
stage: z.enum(["packages", "chrome", "checking"]).optional(),
|
|
15
13
|
});
|
|
16
14
|
|
|
@@ -26,11 +24,9 @@ export const ROUTES: RouteDefinition[] = ["GET", "POST"].map((method) => ({
|
|
|
26
24
|
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
27
25
|
);
|
|
28
26
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
: desktopDependencyInstaller.start();
|
|
33
|
-
return { ...status, automationActive: desktopAutomationLease.isActive };
|
|
27
|
+
return method === "GET"
|
|
28
|
+
? desktopDependencyInstaller.getStatus()
|
|
29
|
+
: desktopDependencyInstaller.start();
|
|
34
30
|
},
|
|
35
31
|
summary:
|
|
36
32
|
method === "GET"
|
|
@@ -299,30 +299,6 @@ 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
|
-
}
|
|
326
302
|
}
|
|
327
303
|
|
|
328
304
|
// ── Conversational approval engine for plain-text messages ──
|
|
@@ -1531,7 +1531,8 @@ export async function handleChannelInbound({
|
|
|
1531
1531
|
sourceMetadata.account.length > 0
|
|
1532
1532
|
? sourceMetadata.account
|
|
1533
1533
|
: undefined;
|
|
1534
|
-
const
|
|
1534
|
+
const slackBotMentioned =
|
|
1535
|
+
sourceChannel === "slack" && sourceMetadata?.slackBotMentioned === true;
|
|
1535
1536
|
|
|
1536
1537
|
// ── DM cold-start backfill ──
|
|
1537
1538
|
// First time a Slack DM without thread_ts lands in a conversation that
|
|
@@ -1624,7 +1625,7 @@ export async function handleChannelInbound({
|
|
|
1624
1625
|
approvalCopyGenerator,
|
|
1625
1626
|
chatType: sourceChatType,
|
|
1626
1627
|
clientTimezone: inboundClientTimezone,
|
|
1627
|
-
|
|
1628
|
+
slackBotMentioned,
|
|
1628
1629
|
slackInbound,
|
|
1629
1630
|
channelInbound,
|
|
1630
1631
|
});
|
|
@@ -1169,7 +1169,7 @@ describe("channel activity timing", () => {
|
|
|
1169
1169
|
externalChatId: channelId,
|
|
1170
1170
|
trustCtx,
|
|
1171
1171
|
metadataHints: [],
|
|
1172
|
-
|
|
1172
|
+
slackBotMentioned: true,
|
|
1173
1173
|
replyCallbackUrl: `https://example.test/deliver/slack?channel=${channelId}&threadTs=${threadTs}`,
|
|
1174
1174
|
});
|
|
1175
1175
|
|
|
@@ -105,11 +105,8 @@ 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
|
-
|
|
110
|
-
* (`sourceMetadata.botMentioned`). Absent means not established.
|
|
111
|
-
*/
|
|
112
|
-
botMentioned?: boolean;
|
|
108
|
+
/** Slack app_mention/direct bot mention signal from the gateway. */
|
|
109
|
+
slackBotMentioned?: boolean;
|
|
113
110
|
/**
|
|
114
111
|
* Slack-specific inbound metadata extracted at the HTTP boundary. Threaded
|
|
115
112
|
* through to `persistUserMessage` so the row can be tagged with a
|
|
@@ -169,7 +166,7 @@ export function processChannelMessageInBackground(
|
|
|
169
166
|
sourceLanguageCode,
|
|
170
167
|
chatType,
|
|
171
168
|
clientTimezone,
|
|
172
|
-
|
|
169
|
+
slackBotMentioned,
|
|
173
170
|
slackInbound,
|
|
174
171
|
channelInbound,
|
|
175
172
|
slackReactionRowMeta,
|
|
@@ -207,7 +204,7 @@ export function processChannelMessageInBackground(
|
|
|
207
204
|
initiatorUserId: slackInbound?.actorExternalUserId,
|
|
208
205
|
startImmediately: shouldShowActivityImmediately({
|
|
209
206
|
chatType,
|
|
210
|
-
botMentioned,
|
|
207
|
+
botMentioned: slackBotMentioned,
|
|
211
208
|
}),
|
|
212
209
|
});
|
|
213
210
|
const stopApprovalWatcher = replyCallbackUrl
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { buildSanitizedEnv } from "../tools/terminal/safe-env.js";
|
|
2
2
|
import {
|
|
3
3
|
buildShellInvocation,
|
|
4
|
-
buildShellSpawnFlags,
|
|
5
4
|
terminateProcessTree,
|
|
6
5
|
} from "../util/host-process.js";
|
|
7
6
|
import { getLogger } from "../util/logger.js";
|
|
@@ -65,9 +64,10 @@ export async function runScript(
|
|
|
65
64
|
const shell = buildShellInvocation(command);
|
|
66
65
|
const proc = Bun.spawn([shell.command, ...shell.args], {
|
|
67
66
|
cwd,
|
|
68
|
-
|
|
67
|
+
detached: true,
|
|
69
68
|
stdout: "pipe",
|
|
70
69
|
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,10 +23,8 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
|
|
|
23
23
|
import { redactSecrets } from "../../security/secret-scanner.js";
|
|
24
24
|
import {
|
|
25
25
|
buildShellInvocation,
|
|
26
|
-
buildShellSpawnFlags,
|
|
27
26
|
prependUniquePathEntries,
|
|
28
27
|
terminateProcessTree,
|
|
29
|
-
watchShellProcessStart,
|
|
30
28
|
} from "../../util/host-process.js";
|
|
31
29
|
import { getLogger } from "../../util/logger.js";
|
|
32
30
|
import type { CompletedBackgroundTool } from "../background-tool-registry.js";
|
|
@@ -457,9 +455,9 @@ export const hostShellTool = {
|
|
|
457
455
|
cwd: workingDir,
|
|
458
456
|
env: hostEnv,
|
|
459
457
|
stdio: ["ignore", "pipe", "pipe"],
|
|
460
|
-
|
|
458
|
+
detached: true,
|
|
459
|
+
windowsHide: true,
|
|
461
460
|
});
|
|
462
|
-
const launch = watchShellProcessStart(child);
|
|
463
461
|
|
|
464
462
|
const collector = attachBoundedStdio(child);
|
|
465
463
|
let timedOut = false;
|
|
@@ -485,9 +483,7 @@ export const hostShellTool = {
|
|
|
485
483
|
}
|
|
486
484
|
completed = true;
|
|
487
485
|
clearTimeout(timer);
|
|
488
|
-
const result = collector.format(code, timedOut, timeoutSec
|
|
489
|
-
started: launch.didStart(),
|
|
490
|
-
});
|
|
486
|
+
const result = collector.format(code, timedOut, timeoutSec);
|
|
491
487
|
// Cancel takes precedence over the SIGKILL-induced error result.
|
|
492
488
|
const status = aborted
|
|
493
489
|
? "cancelled"
|
|
@@ -633,9 +629,9 @@ export const hostShellTool = {
|
|
|
633
629
|
cwd: workingDir,
|
|
634
630
|
env: hostEnv,
|
|
635
631
|
stdio: ["ignore", "pipe", "pipe"],
|
|
636
|
-
|
|
632
|
+
detached: true,
|
|
633
|
+
windowsHide: true,
|
|
637
634
|
});
|
|
638
|
-
const launch = watchShellProcessStart(child);
|
|
639
635
|
const collector = attachBoundedStdio(child, {
|
|
640
636
|
onOutput: context.onOutput,
|
|
641
637
|
});
|
|
@@ -661,9 +657,7 @@ export const hostShellTool = {
|
|
|
661
657
|
clearTimeout(timer);
|
|
662
658
|
context.signal?.removeEventListener("abort", onAbort);
|
|
663
659
|
|
|
664
|
-
const result = collector.format(code, timedOut, timeoutSec
|
|
665
|
-
started: launch.didStart(),
|
|
666
|
-
});
|
|
660
|
+
const result = collector.format(code, timedOut, timeoutSec);
|
|
667
661
|
|
|
668
662
|
resolve({
|
|
669
663
|
content: result.content,
|
|
@@ -4,7 +4,6 @@ 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";
|
|
8
7
|
import { isAbortLikeError } from "../abort.js";
|
|
9
8
|
import { applyEdit } from "./edit-engine.js";
|
|
10
9
|
import * as Err from "./errors.js";
|
|
@@ -103,6 +102,37 @@ function truncationNotice(
|
|
|
103
102
|
return `\n\n[Truncated: characters ${start}-${end} of ${totalChars}. Read on with start_index=${end}.]`;
|
|
104
103
|
}
|
|
105
104
|
|
|
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
|
+
|
|
106
136
|
export class FileSystemOps {
|
|
107
137
|
private policy: PathPolicy;
|
|
108
138
|
private sizeLimit: number | undefined;
|