@vellumai/assistant 0.12.2-staging.1 → 0.12.2-staging.3
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/desktop-browser-cli.md +33 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +50 -4
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +33 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/credential-routes.test.ts +38 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +6 -2
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +185 -30
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/oauth-commands-routes.test.ts +55 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +69 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +81 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +132 -0
- package/src/calls/voice-session-bridge.ts +56 -26
- package/src/calls/voice-triage-escalate.ts +3 -3
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/oauth/status.ts +5 -0
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +43 -4
- package/src/config/bundled-skills/computer-use/TOOLS.json +3 -3
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +11 -3
- package/src/config/loader.ts +1 -0
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/services.ts +10 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-surfaces.ts +6 -1
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/host-cu-proxy.ts +94 -31
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +143 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +9 -7
- package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +47 -0
- package/src/live-voice/__tests__/session-controls.test.ts +113 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +88 -7
- package/src/live-voice/protocol.ts +69 -0
- package/src/live-voice/session-controls.ts +139 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +95 -0
- package/src/oauth/connection-resolver.test.ts +27 -0
- package/src/oauth/connection-resolver.ts +25 -1
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/seed-providers.ts +121 -2
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/app-management-routes.ts +17 -2
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/credential-routes.ts +1 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/oauth-commands-routes.ts +14 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +3 -4
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/watch/watch-retro.ts +6 -8
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -48,8 +48,8 @@ const SCREENSHOT_OMITTED_MESSAGE =
|
|
|
48
48
|
"Screenshot omitted: the accessibility tree above is current. Pass include_screenshot: true on computer_use_observe to see the screen.";
|
|
49
49
|
|
|
50
50
|
// computer_use_key combos that change only selection/cursor/clipboard state.
|
|
51
|
-
// The AX tree models none of these, so they always produce an empty diff
|
|
52
|
-
// exempt
|
|
51
|
+
// The AX tree models none of these, so they always produce an empty diff and
|
|
52
|
+
// are exempt from the unchanged-tree signal.
|
|
53
53
|
// Stored in canonical form (see canonicalizeKeyCombo): modifier aliases
|
|
54
54
|
// normalized and ordered, so `cmd + a`, `command+a`, `alt+tab`, `tab+shift`
|
|
55
55
|
// all match.
|
|
@@ -129,12 +129,28 @@ export interface ActionRecord {
|
|
|
129
129
|
reasoning?: string;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Steps whose empty diff says nothing about whether they worked: waiting and
|
|
133
|
+
// observing change nothing by design, and an AppleScript reports its own
|
|
134
|
+
// outcome in the execution result, often for an app whose content the tree
|
|
135
|
+
// cannot see. They neither warn nor move the unchanged streak.
|
|
136
|
+
const NO_AX_DIFF_TOOLS = new Set([
|
|
137
|
+
"computer_use_wait",
|
|
138
|
+
"computer_use_observe",
|
|
139
|
+
"computer_use_run_applescript",
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
type ActionIdentity = Pick<ActionRecord, "toolName" | "input">;
|
|
143
|
+
|
|
144
|
+
function isNoDiffTool(action: ActionIdentity | undefined): boolean {
|
|
145
|
+
return action !== undefined && NO_AX_DIFF_TOOLS.has(action.toolName);
|
|
146
|
+
}
|
|
147
|
+
|
|
132
148
|
/**
|
|
133
149
|
* True when `action` is a computer_use_key press whose key only mutates
|
|
134
|
-
* selection/cursor/clipboard state
|
|
150
|
+
* selection/cursor/clipboard state: changes the AX tree cannot represent, so
|
|
135
151
|
* an empty diff is expected rather than a sign the action did nothing.
|
|
136
152
|
*/
|
|
137
|
-
function isNoDiffKeyAction(action:
|
|
153
|
+
function isNoDiffKeyAction(action: ActionIdentity | undefined): boolean {
|
|
138
154
|
if (action?.toolName !== "computer_use_key") {
|
|
139
155
|
return false;
|
|
140
156
|
}
|
|
@@ -225,6 +241,11 @@ export class HostCuProxy {
|
|
|
225
241
|
* that reset cleared.
|
|
226
242
|
*/
|
|
227
243
|
private _resetGeneration = 0;
|
|
244
|
+
/**
|
|
245
|
+
* Desktops a request has been sent to since the last reset, keyed by
|
|
246
|
+
* `observedTargetKey`, so the end of a task can reach each one.
|
|
247
|
+
*/
|
|
248
|
+
private _dispatchedTargets = new Map<string, string | undefined>();
|
|
228
249
|
/**
|
|
229
250
|
* Owned request IDs mapped to whether their observation is scoped, whether
|
|
230
251
|
* the helper was told to skip the screenshot, and when the request was
|
|
@@ -240,6 +261,7 @@ export class HostCuProxy {
|
|
|
240
261
|
resetGeneration: number;
|
|
241
262
|
dispatchedAt: number;
|
|
242
263
|
toolName: string;
|
|
264
|
+
input: Record<string, unknown>;
|
|
243
265
|
step: number;
|
|
244
266
|
}
|
|
245
267
|
>();
|
|
@@ -442,6 +464,7 @@ export class HostCuProxy {
|
|
|
442
464
|
detachAbort = () => signal.removeEventListener("abort", onAbort);
|
|
443
465
|
}
|
|
444
466
|
|
|
467
|
+
this._dispatchedTargets.set(targetKey, resolvedTargetClientId);
|
|
445
468
|
this._ownedRequests.set(requestId, {
|
|
446
469
|
scoped: scopedObservation,
|
|
447
470
|
screenshotSkipped,
|
|
@@ -449,6 +472,7 @@ export class HostCuProxy {
|
|
|
449
472
|
resetGeneration: this._resetGeneration,
|
|
450
473
|
dispatchedAt: Date.now(),
|
|
451
474
|
toolName,
|
|
475
|
+
input,
|
|
452
476
|
step: stepNumber,
|
|
453
477
|
});
|
|
454
478
|
|
|
@@ -532,7 +556,7 @@ export class HostCuProxy {
|
|
|
532
556
|
}
|
|
533
557
|
|
|
534
558
|
// A targeted snapshot has no comparable action/diff baseline; neither it
|
|
535
|
-
// nor the first desktop observation after it can imply
|
|
559
|
+
// nor the first desktop observation after it can imply an unchanged tree.
|
|
536
560
|
// A response dispatched before the last reset belongs to a finished run,
|
|
537
561
|
// so it must not change any state the new run has started building.
|
|
538
562
|
const fromCurrentRun =
|
|
@@ -542,6 +566,11 @@ export class HostCuProxy {
|
|
|
542
566
|
this._consecutiveUnchangedSteps = 0;
|
|
543
567
|
}
|
|
544
568
|
const prevAXTree = this._previousAXTree;
|
|
569
|
+
// Judge the empty diff against the action this request carried, for the
|
|
570
|
+
// same reason the timings line does.
|
|
571
|
+
const action: ActionIdentity | undefined = owned
|
|
572
|
+
? { toolName: owned.toolName, input: owned.input }
|
|
573
|
+
: this.lastRecordedAction();
|
|
545
574
|
const comparableObservation = scopedObservation
|
|
546
575
|
? { ...observation, axDiff: undefined, secondaryWindows: undefined }
|
|
547
576
|
: observation;
|
|
@@ -551,7 +580,7 @@ export class HostCuProxy {
|
|
|
551
580
|
!scopedObservation &&
|
|
552
581
|
owned?.toolName !== POINT_AT_PROXY_TOOL
|
|
553
582
|
) {
|
|
554
|
-
this.updateStateFromObservation(comparableObservation);
|
|
583
|
+
this.updateStateFromObservation(comparableObservation, action);
|
|
555
584
|
// A desktop has had its first look only once pixels from it arrived,
|
|
556
585
|
// so a failed capture leaves the next request asking again.
|
|
557
586
|
if (owned && observation.screenshot) {
|
|
@@ -562,6 +591,7 @@ export class HostCuProxy {
|
|
|
562
591
|
comparableObservation,
|
|
563
592
|
prevAXTree,
|
|
564
593
|
owned?.screenshotSkipped ?? false,
|
|
594
|
+
action,
|
|
565
595
|
);
|
|
566
596
|
interaction.rpcResolve(result);
|
|
567
597
|
return result;
|
|
@@ -592,8 +622,36 @@ export class HostCuProxy {
|
|
|
592
622
|
}
|
|
593
623
|
}
|
|
594
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Finish the task: tell every desktop this task drove that it is over, then
|
|
627
|
+
* reset. The notice rides `host_cu_cancel` with a fresh request ID, which
|
|
628
|
+
* matches nothing in flight; the helper treats any cancel as the run
|
|
629
|
+
* stopping and puts the pointer back where the user left it, instead of
|
|
630
|
+
* waiting out its idle fallback.
|
|
631
|
+
*/
|
|
632
|
+
endTask(conversationId: string): void {
|
|
633
|
+
for (const targetClientId of this._dispatchedTargets.values()) {
|
|
634
|
+
try {
|
|
635
|
+
broadcastMessage(
|
|
636
|
+
{
|
|
637
|
+
type: "host_cu_cancel",
|
|
638
|
+
requestId: uuid(),
|
|
639
|
+
conversationId,
|
|
640
|
+
...(targetClientId != null ? { targetClientId } : {}),
|
|
641
|
+
},
|
|
642
|
+
conversationId,
|
|
643
|
+
{ targetClientId },
|
|
644
|
+
);
|
|
645
|
+
} catch {
|
|
646
|
+
// Best-effort: the helper still returns the pointer after its idle delay.
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
this.reset();
|
|
650
|
+
}
|
|
651
|
+
|
|
595
652
|
/** Reset all CU state. Called on terminal tools (computer_use_done, etc.). */
|
|
596
653
|
reset(): void {
|
|
654
|
+
this._dispatchedTargets.clear();
|
|
597
655
|
this._stepCount = 0;
|
|
598
656
|
this._previousAXTree = undefined;
|
|
599
657
|
this._consecutiveUnchangedSteps = 0;
|
|
@@ -604,20 +662,24 @@ export class HostCuProxy {
|
|
|
604
662
|
|
|
605
663
|
/**
|
|
606
664
|
* Whether the request about to be dispatched should carry a screenshot. The
|
|
607
|
-
* accessibility tree comes back every step. Pixels come back
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
* model
|
|
665
|
+
* accessibility tree comes back every step. Pixels come back after every
|
|
666
|
+
* action, so the model sees what its action did, on the first look since
|
|
667
|
+
* the last reset, for a window- or display-scoped capture, and when the
|
|
668
|
+
* model asks for them. Only a plain observation leaves whether the tree is
|
|
669
|
+
* enough to the model.
|
|
611
670
|
*/
|
|
612
671
|
private shouldAttachScreenshot(
|
|
613
672
|
toolName: string,
|
|
614
673
|
input: Record<string, unknown>,
|
|
615
674
|
targetKey: string,
|
|
616
675
|
): boolean {
|
|
676
|
+
if (toolName !== "computer_use_observe") {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
617
679
|
return (
|
|
618
680
|
!this._observedTargets.has(targetKey) ||
|
|
619
681
|
hasCaptureTarget(input) ||
|
|
620
|
-
|
|
682
|
+
input.include_screenshot === true
|
|
621
683
|
);
|
|
622
684
|
}
|
|
623
685
|
|
|
@@ -634,6 +696,7 @@ export class HostCuProxy {
|
|
|
634
696
|
obs: CuObservationResult,
|
|
635
697
|
previousAXTree?: string,
|
|
636
698
|
screenshotSkipped = false,
|
|
699
|
+
action: ActionIdentity | undefined = this.lastRecordedAction(),
|
|
637
700
|
): ToolExecutionResult {
|
|
638
701
|
const prevTree = previousAXTree;
|
|
639
702
|
const parts: string[] = [];
|
|
@@ -652,24 +715,17 @@ export class HostCuProxy {
|
|
|
652
715
|
parts.push(obs.axDiff);
|
|
653
716
|
parts.push("");
|
|
654
717
|
} else if (prevTree != null && obs.axTree != null) {
|
|
655
|
-
|
|
656
|
-
this._actionHistory.length > 0
|
|
657
|
-
? this._actionHistory[this._actionHistory.length - 1]
|
|
658
|
-
: undefined;
|
|
659
|
-
const isWaitAction = lastAction?.toolName === "computer_use_wait";
|
|
660
|
-
const isNoDiffKey = isNoDiffKeyAction(lastAction);
|
|
661
|
-
|
|
662
|
-
if (!isWaitAction && !isNoDiffKey) {
|
|
718
|
+
if (!isNoDiffTool(action) && !isNoDiffKeyAction(action)) {
|
|
663
719
|
if (
|
|
664
720
|
this._consecutiveUnchangedSteps >=
|
|
665
721
|
CONSECUTIVE_UNCHANGED_WARNING_THRESHOLD
|
|
666
722
|
) {
|
|
667
723
|
parts.push(
|
|
668
|
-
`WARNING: ${this._consecutiveUnchangedSteps} consecutive actions
|
|
724
|
+
`WARNING: the accessibility tree did not change across ${this._consecutiveUnchangedSteps} consecutive actions. If the screenshot does not show them working either, try a different approach.`,
|
|
669
725
|
);
|
|
670
726
|
} else {
|
|
671
727
|
parts.push(
|
|
672
|
-
"
|
|
728
|
+
"The accessibility tree did not change after your last action. Apps that draw their own content (timelines, canvases, spreadsheet grids) change without it, so check the screenshot before deciding the action did nothing.",
|
|
673
729
|
);
|
|
674
730
|
}
|
|
675
731
|
parts.push("");
|
|
@@ -784,19 +840,26 @@ export class HostCuProxy {
|
|
|
784
840
|
// Private helpers
|
|
785
841
|
// ---------------------------------------------------------------------------
|
|
786
842
|
|
|
787
|
-
private
|
|
843
|
+
private lastRecordedAction(): ActionRecord | undefined {
|
|
844
|
+
return this._actionHistory.at(-1);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private updateStateFromObservation(
|
|
848
|
+
obs: CuObservationResult,
|
|
849
|
+
action: ActionIdentity | undefined,
|
|
850
|
+
): void {
|
|
788
851
|
if (this._stepCount > 0) {
|
|
789
|
-
|
|
790
|
-
this._actionHistory.length > 0
|
|
791
|
-
? this._actionHistory[this._actionHistory.length - 1]
|
|
792
|
-
: undefined;
|
|
793
|
-
if (obs.axDiff != null || isNoDiffKeyAction(lastAction)) {
|
|
852
|
+
if (obs.axDiff != null || isNoDiffKeyAction(action)) {
|
|
794
853
|
// A real diff, or an exempt key whose effect is invisible by design,
|
|
795
|
-
// breaks the no-effect streak
|
|
796
|
-
// stale count
|
|
797
|
-
// into a false "consecutive" escalation.
|
|
854
|
+
// breaks the no-effect streak. Clearing it rather than preserving a
|
|
855
|
+
// stale count keeps an intervening cmd+a from bridging two no-op
|
|
856
|
+
// actions into a false "consecutive" escalation.
|
|
798
857
|
this._consecutiveUnchangedSteps = 0;
|
|
799
|
-
} else if (
|
|
858
|
+
} else if (
|
|
859
|
+
!isNoDiffTool(action) &&
|
|
860
|
+
this._previousAXTree != null &&
|
|
861
|
+
obs.axTree != null
|
|
862
|
+
) {
|
|
800
863
|
this._consecutiveUnchangedSteps++;
|
|
801
864
|
}
|
|
802
865
|
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - `daemon/process-message.ts` (create path, prepareConversationForMessage)
|
|
10
10
|
* - `daemon/conversation-process.ts` `drainSingleMessage` (re-add after dequeue)
|
|
11
11
|
* - `daemon/conversation-process.ts` `drainBatch` (re-add after dequeue)
|
|
12
|
+
* - `calls/voice-session-bridge.ts` (tool-capable legs of a live-voice
|
|
13
|
+
* session opened from the macOS desktop client)
|
|
12
14
|
*
|
|
13
15
|
* The create paths additionally instantiate the proxy itself; that
|
|
14
16
|
* instantiation logic is per-proxy-class and stays inline at each create
|
|
@@ -14,6 +14,7 @@ import { getMcpServerManager } from "../mcp/manager.js";
|
|
|
14
14
|
import { migrateLegacyMcpHeaders } from "../mcp/mcp-header-store.js";
|
|
15
15
|
import { signalMcpReloaded } from "../mcp/reload-signal.js";
|
|
16
16
|
import { loadWorkspaceMcpConfig } from "../mcp/workspace-mcp-config.js";
|
|
17
|
+
import { publishMcpChanged } from "../runtime/sync/resource-sync-events.js";
|
|
17
18
|
import { createMcpToolsFromServer } from "../tools/mcp/mcp-tool-factory.js";
|
|
18
19
|
import { registerMcpTools, unregisterAllMcpTools } from "../tools/registry.js";
|
|
19
20
|
import { getLogger } from "../util/logger.js";
|
|
@@ -80,6 +81,7 @@ export async function reconcilePluginMcpServers(): Promise<void> {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
async function doReload(): Promise<McpReloadResult> {
|
|
84
|
+
let teardownStarted = false;
|
|
83
85
|
try {
|
|
84
86
|
const manager = getMcpServerManager();
|
|
85
87
|
|
|
@@ -98,6 +100,7 @@ async function doReload(): Promise<McpReloadResult> {
|
|
|
98
100
|
invalidateConfigCache();
|
|
99
101
|
|
|
100
102
|
// 2. Stop existing MCP servers + unregister their tools
|
|
103
|
+
teardownStarted = true;
|
|
101
104
|
await manager.stop();
|
|
102
105
|
unregisterAllMcpTools();
|
|
103
106
|
|
|
@@ -160,5 +163,9 @@ async function doReload(): Promise<McpReloadResult> {
|
|
|
160
163
|
const error = err instanceof Error ? err.message : String(err);
|
|
161
164
|
log.error({ err }, "MCP reload failed");
|
|
162
165
|
return { success: false, error };
|
|
166
|
+
} finally {
|
|
167
|
+
if (teardownStarted) {
|
|
168
|
+
publishMcpChanged();
|
|
169
|
+
}
|
|
163
170
|
}
|
|
164
171
|
}
|
|
@@ -18,6 +18,7 @@ export const SYNC_TAGS = {
|
|
|
18
18
|
appsList: "apps:list",
|
|
19
19
|
documentsList: "documents:list",
|
|
20
20
|
pluginsList: "plugins:list",
|
|
21
|
+
mcpList: "mcp:list",
|
|
21
22
|
conversationsList: "conversations:list",
|
|
22
23
|
/** Activation-checklist progress: task launches, step counts, completions. */
|
|
23
24
|
activationProgress: "activation:progress",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { maybeDefaultSpeechToManaged } from "../config/managed-speech-defaults.js";
|
|
2
2
|
import { rehydratePlatformCredentials } from "../config/platform-rehydration.js";
|
|
3
3
|
import type { AssistantConfig } from "../config/types.js";
|
|
4
|
+
import { getMcpServerManager } from "../mcp/manager.js";
|
|
4
5
|
import { startConfiguredMcpServers } from "../mcp/startup.js";
|
|
5
6
|
import { gmailMessagingProvider } from "../messaging/providers/gmail/adapter.js";
|
|
6
7
|
import { outlookMessagingProvider } from "../messaging/providers/outlook/adapter.js";
|
|
@@ -9,6 +10,7 @@ import { telegramBotMessagingProvider } from "../messaging/providers/telegram-bo
|
|
|
9
10
|
import { whatsappMessagingProvider } from "../messaging/providers/whatsapp/adapter.js";
|
|
10
11
|
import { registerMessagingProvider } from "../messaging/registry.js";
|
|
11
12
|
import { initializeProviders } from "../providers/registry.js";
|
|
13
|
+
import { publishMcpChanged } from "../runtime/sync/resource-sync-events.js";
|
|
12
14
|
import { validateSubagentRoleAllowlists } from "../subagent/validate-allowlists.js";
|
|
13
15
|
import { initializeTools } from "../tools/registry.js";
|
|
14
16
|
import { getLogger } from "../util/logger.js";
|
|
@@ -63,7 +65,9 @@ export async function initializeProvidersAndTools(
|
|
|
63
65
|
// Start MCP servers — workspace-configured and plugin-declared alike —
|
|
64
66
|
// and register their tools. Shared with the schedule worker, which hosts
|
|
65
67
|
// agent turns in its own process and so needs its own connections.
|
|
68
|
+
getMcpServerManager().setUnexpectedCloseHandler(() => publishMcpChanged());
|
|
66
69
|
await startConfiguredMcpServers();
|
|
70
|
+
publishMcpChanged();
|
|
67
71
|
|
|
68
72
|
log.info("Daemon startup: providers and tools initialized");
|
|
69
73
|
}
|
|
@@ -58,6 +58,7 @@ export function newFakeDesktop(options: FakeDesktopOptions) {
|
|
|
58
58
|
let vncReady = true;
|
|
59
59
|
let chromiumPath: () => Promise<string> = async () => "/fake/chromium";
|
|
60
60
|
const manager = new DesktopSessionManager({
|
|
61
|
+
allocateDebugPort: async () => 9222,
|
|
61
62
|
spawn: (role, request) => {
|
|
62
63
|
if (options.failSpawn?.includes(role)) {
|
|
63
64
|
throw new Error(`spawn ${role} failed`);
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { afterEach, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { DesktopAutomationLease } from "./desktop-automation-lease.js";
|
|
4
|
+
import { DesktopDependencyInstaller } from "./desktop-dependencies.js";
|
|
5
|
+
import type { DesktopSessionManager } from "./desktop-session-manager.js";
|
|
6
|
+
|
|
7
|
+
const context = {
|
|
8
|
+
conversationId: "conv-123",
|
|
9
|
+
sourceActorPrincipalId: "user-123",
|
|
10
|
+
trustClass: "guardian" as const,
|
|
11
|
+
workingDir: "/tmp",
|
|
12
|
+
};
|
|
13
|
+
const cleanups: DesktopAutomationLease[] = [];
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(
|
|
16
|
+
cleanups
|
|
17
|
+
.splice(0)
|
|
18
|
+
.map((lease) => lease.runBrowser(context, operation, true)),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
function fixture() {
|
|
22
|
+
let enabled = true;
|
|
23
|
+
let ready = true;
|
|
24
|
+
const setup = Promise.withResolvers<void>();
|
|
25
|
+
const install = mock(() => setup.promise);
|
|
26
|
+
const installer = new DesktopDependencyInstaller({
|
|
27
|
+
supported: () => true,
|
|
28
|
+
ready: () => ready,
|
|
29
|
+
install,
|
|
30
|
+
notify: async () => {},
|
|
31
|
+
});
|
|
32
|
+
const ensureReady = mock((signal: AbortSignal) =>
|
|
33
|
+
installer.ensureReady(signal),
|
|
34
|
+
);
|
|
35
|
+
const started = mock(async () => {});
|
|
36
|
+
const released = mock(() => {});
|
|
37
|
+
const releaseBrowser = mock(async () => {});
|
|
38
|
+
const lease = new DesktopAutomationLease({
|
|
39
|
+
enabled: () => enabled,
|
|
40
|
+
ready: () => ready,
|
|
41
|
+
ensureReady,
|
|
42
|
+
manager: () =>
|
|
43
|
+
({
|
|
44
|
+
browser: { release: releaseBrowser },
|
|
45
|
+
acquireAutomationSlot: () => ({ ok: true }),
|
|
46
|
+
releaseAutomationSlot: released,
|
|
47
|
+
ensureDesktopRunning: started,
|
|
48
|
+
}) as unknown as DesktopSessionManager,
|
|
49
|
+
});
|
|
50
|
+
cleanups.push(lease);
|
|
51
|
+
return {
|
|
52
|
+
lease,
|
|
53
|
+
ensureReady,
|
|
54
|
+
install,
|
|
55
|
+
failSetup: () => setup.reject(new Error("download failed")),
|
|
56
|
+
completeSetup: () => {
|
|
57
|
+
ready = true;
|
|
58
|
+
setup.resolve();
|
|
59
|
+
},
|
|
60
|
+
started,
|
|
61
|
+
released,
|
|
62
|
+
releaseBrowser,
|
|
63
|
+
disable: () => {
|
|
64
|
+
enabled = false;
|
|
65
|
+
},
|
|
66
|
+
uninstall: () => {
|
|
67
|
+
ready = false;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const operation = async () => ({ content: "ok", isError: false });
|
|
72
|
+
|
|
73
|
+
test("browser session reuses one automation slot and isolates conversations", async () => {
|
|
74
|
+
const f = fixture();
|
|
75
|
+
await f.lease.runBrowser(context, operation);
|
|
76
|
+
await f.lease.runBrowser(context, operation);
|
|
77
|
+
expect(f.started).toHaveBeenCalledTimes(1);
|
|
78
|
+
await expect(
|
|
79
|
+
f.lease.runBrowser({ ...context, conversationId: "conv-456" }, operation),
|
|
80
|
+
).rejects.toThrow("Another conversation");
|
|
81
|
+
expect(f.released).not.toHaveBeenCalled();
|
|
82
|
+
await f.lease.runBrowser(context, operation, true);
|
|
83
|
+
expect(f.released).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
for (const lose of ["disable", "uninstall"] as const) {
|
|
87
|
+
test(`${lose} blocks startup, reuse and startup races`, async () => {
|
|
88
|
+
const unavailable = fixture();
|
|
89
|
+
unavailable[lose]();
|
|
90
|
+
if (lose === "uninstall") {
|
|
91
|
+
unavailable.ensureReady.mockRejectedValueOnce(new Error("setup failed"));
|
|
92
|
+
}
|
|
93
|
+
await expect(
|
|
94
|
+
unavailable.lease.runBrowser(context, operation),
|
|
95
|
+
).rejects.toThrow();
|
|
96
|
+
expect(unavailable.started).not.toHaveBeenCalled();
|
|
97
|
+
const race = fixture();
|
|
98
|
+
race.started.mockImplementationOnce(async () => race[lose]());
|
|
99
|
+
const callback = mock(operation);
|
|
100
|
+
await expect(race.lease.runBrowser(context, callback)).rejects.toThrow();
|
|
101
|
+
expect(callback).not.toHaveBeenCalled();
|
|
102
|
+
expect(race.released).toHaveBeenCalledTimes(1);
|
|
103
|
+
const reused = fixture();
|
|
104
|
+
await reused.lease.runBrowser(context, operation);
|
|
105
|
+
reused[lose]();
|
|
106
|
+
await expect(reused.lease.runBrowser(context, callback)).rejects.toThrow();
|
|
107
|
+
expect(callback).not.toHaveBeenCalled();
|
|
108
|
+
expect(reused.released).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test("cancellation stops running and queued browser commands without blocking a new session", async () => {
|
|
113
|
+
const f = fixture();
|
|
114
|
+
const started = Promise.withResolvers<void>();
|
|
115
|
+
const abort = new AbortController();
|
|
116
|
+
const running = f.lease
|
|
117
|
+
.runBrowser({ ...context, signal: abort.signal }, async (signal) => {
|
|
118
|
+
started.resolve();
|
|
119
|
+
await new Promise<void>((_resolve, reject) => {
|
|
120
|
+
signal.addEventListener("abort", () => reject(new Error("aborted")), {
|
|
121
|
+
once: true,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
return operation();
|
|
125
|
+
})
|
|
126
|
+
.catch((error: Error) => error);
|
|
127
|
+
await started.promise;
|
|
128
|
+
const callback = mock(operation);
|
|
129
|
+
const queued = f.lease.runBrowser(context, callback);
|
|
130
|
+
abort.abort();
|
|
131
|
+
expect(await running).toBeInstanceOf(Error);
|
|
132
|
+
expect((await queued).isError).toBe(true);
|
|
133
|
+
expect(callback).not.toHaveBeenCalled();
|
|
134
|
+
await f.lease.runBrowser(context, callback);
|
|
135
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("failed browser cleanup retains ownership until release succeeds", async () => {
|
|
139
|
+
const f = fixture();
|
|
140
|
+
await f.lease.runBrowser(context, operation);
|
|
141
|
+
f.releaseBrowser.mockRejectedValueOnce(new Error("Chrome busy"));
|
|
142
|
+
await expect(f.lease.runBrowser(context, operation, true)).rejects.toThrow(
|
|
143
|
+
"Chrome busy",
|
|
144
|
+
);
|
|
145
|
+
expect(f.released).not.toHaveBeenCalled();
|
|
146
|
+
await f.lease.runBrowser(context, operation, true);
|
|
147
|
+
expect(f.released).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("first browser call waits for one shared install then executes exactly once", async () => {
|
|
151
|
+
const f = fixture();
|
|
152
|
+
f.uninstall();
|
|
153
|
+
const callback = mock(operation);
|
|
154
|
+
const first = f.lease.runBrowser(context, callback);
|
|
155
|
+
await Bun.sleep(0);
|
|
156
|
+
expect(f.install).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(f.started).not.toHaveBeenCalled();
|
|
158
|
+
expect(callback).not.toHaveBeenCalled();
|
|
159
|
+
f.completeSetup();
|
|
160
|
+
expect((await first).isError).toBe(false);
|
|
161
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
162
|
+
await f.lease.runBrowser(context, callback);
|
|
163
|
+
expect(f.install).toHaveBeenCalledTimes(1);
|
|
164
|
+
expect(f.started).toHaveBeenCalledTimes(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
for (const interrupt of ["cancel", "disable", "failure"] as const) {
|
|
168
|
+
test(`${interrupt} during setup prevents a delayed browser action`, async () => {
|
|
169
|
+
const f = fixture();
|
|
170
|
+
f.uninstall();
|
|
171
|
+
const abort = new AbortController();
|
|
172
|
+
const callback = mock(operation);
|
|
173
|
+
const result = f.lease
|
|
174
|
+
.runBrowser({ ...context, signal: abort.signal }, callback)
|
|
175
|
+
.catch((error: unknown) => error);
|
|
176
|
+
await Bun.sleep(0);
|
|
177
|
+
if (interrupt === "cancel") {
|
|
178
|
+
abort.abort();
|
|
179
|
+
} else if (interrupt === "disable") {
|
|
180
|
+
f.disable();
|
|
181
|
+
} else {
|
|
182
|
+
f.failSetup();
|
|
183
|
+
}
|
|
184
|
+
expect(await result).toBeInstanceOf(Error);
|
|
185
|
+
f.completeSetup();
|
|
186
|
+
await Bun.sleep(0);
|
|
187
|
+
expect(f.started).not.toHaveBeenCalled();
|
|
188
|
+
expect(callback).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
test("disabled, unidentified, cancelled and released browser calls cannot install", async () => {
|
|
193
|
+
const f = fixture();
|
|
194
|
+
f.uninstall();
|
|
195
|
+
const cancelled = new AbortController();
|
|
196
|
+
cancelled.abort();
|
|
197
|
+
for (const caller of [
|
|
198
|
+
{ ...context, trustClass: "unknown" as const },
|
|
199
|
+
{ ...context, sourceActorPrincipalId: undefined },
|
|
200
|
+
{ ...context, signal: cancelled.signal },
|
|
201
|
+
]) {
|
|
202
|
+
await expect(f.lease.runBrowser(caller, operation)).rejects.toThrow();
|
|
203
|
+
}
|
|
204
|
+
await f.lease.runBrowser(context, operation, true);
|
|
205
|
+
f.disable();
|
|
206
|
+
await expect(f.lease.runBrowser(context, operation)).rejects.toThrow();
|
|
207
|
+
expect(f.ensureReady).not.toHaveBeenCalled();
|
|
208
|
+
});
|