@vellumai/assistant 0.4.34 → 0.4.36
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +4 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +91 -43
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +806 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +491 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +133 -242
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +177 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +175 -145
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +12 -1
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
- package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
- package/src/memory/migrations/038-actor-token-records.ts +8 -1
- package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
- package/src/memory/migrations/110-channel-guardian.ts +27 -6
- package/src/memory/migrations/112-assistant-inbox.ts +39 -15
- package/src/memory/migrations/114-notifications.ts +37 -15
- package/src/memory/migrations/117-conversation-attention.ts +33 -9
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/schema-introspection.ts +18 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +5 -9
- package/src/runtime/http-types.ts +13 -1
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/surface-content-routes.ts +104 -0
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -61,6 +61,23 @@ import {
|
|
|
61
61
|
export { handleUserMessage } from "./session-user-message.js";
|
|
62
62
|
import { handleUserMessage } from "./session-user-message.js";
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Extract a valid ChannelId from a binding's sourceChannel, which may carry a
|
|
66
|
+
* `notification:` namespace prefix (e.g. `"notification:telegram"` -> `"telegram"`).
|
|
67
|
+
* Returns the ChannelId if valid, or null otherwise.
|
|
68
|
+
*/
|
|
69
|
+
function parseBindingSourceChannel(
|
|
70
|
+
sourceChannel: string,
|
|
71
|
+
): import("../../channels/types.js").ChannelId | null {
|
|
72
|
+
if (isChannelId(sourceChannel)) return sourceChannel;
|
|
73
|
+
const NOTIFICATION_PREFIX = "notification:";
|
|
74
|
+
if (sourceChannel.startsWith(NOTIFICATION_PREFIX)) {
|
|
75
|
+
const inner = sourceChannel.slice(NOTIFICATION_PREFIX.length);
|
|
76
|
+
if (isChannelId(inner)) return inner;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
export function syncCanonicalStatusFromIpcConfirmationDecision(
|
|
65
82
|
requestId: string,
|
|
66
83
|
decision: ConfirmationResponse["decision"],
|
|
@@ -283,10 +300,12 @@ export function handleSessionList(
|
|
|
283
300
|
updatedAt: c.updatedAt,
|
|
284
301
|
threadType: normalizeThreadType(c.threadType),
|
|
285
302
|
source: c.source ?? "user",
|
|
286
|
-
...(binding &&
|
|
303
|
+
...(binding && parseBindingSourceChannel(binding.sourceChannel)
|
|
287
304
|
? {
|
|
288
305
|
channelBinding: {
|
|
289
|
-
sourceChannel:
|
|
306
|
+
sourceChannel: parseBindingSourceChannel(
|
|
307
|
+
binding.sourceChannel,
|
|
308
|
+
)!,
|
|
290
309
|
externalChatId: binding.externalChatId,
|
|
291
310
|
externalUserId: binding.externalUserId,
|
|
292
311
|
displayName: binding.displayName,
|
|
@@ -70,6 +70,14 @@ export interface HistoryToolCall {
|
|
|
70
70
|
isError?: boolean;
|
|
71
71
|
/** Base64-encoded image data from tool contentBlocks (e.g. browser_screenshot). */
|
|
72
72
|
imageData?: string;
|
|
73
|
+
/** Unix ms when the tool started executing. */
|
|
74
|
+
startedAt?: number;
|
|
75
|
+
/** Unix ms when the tool completed. */
|
|
76
|
+
completedAt?: number;
|
|
77
|
+
/** Confirmation decision for this tool call: "approved" | "denied" | "timed_out". */
|
|
78
|
+
confirmationDecision?: string;
|
|
79
|
+
/** Friendly label for the confirmation (e.g. "Edit File", "Run Command"). */
|
|
80
|
+
confirmationLabel?: string;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
export interface HistorySurface {
|
|
@@ -468,6 +476,15 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
|
|
|
468
476
|
: {};
|
|
469
477
|
const id = typeof block.id === "string" ? block.id : "";
|
|
470
478
|
const entry: HistoryToolCall = { name, input };
|
|
479
|
+
// Extract persisted timing/confirmation metadata
|
|
480
|
+
if (typeof block._startedAt === "number")
|
|
481
|
+
entry.startedAt = block._startedAt;
|
|
482
|
+
if (typeof block._completedAt === "number")
|
|
483
|
+
entry.completedAt = block._completedAt;
|
|
484
|
+
if (typeof block._confirmationDecision === "string")
|
|
485
|
+
entry.confirmationDecision = block._confirmationDecision;
|
|
486
|
+
if (typeof block._confirmationLabel === "string")
|
|
487
|
+
entry.confirmationLabel = block._confirmationLabel;
|
|
471
488
|
toolCalls.push(entry);
|
|
472
489
|
if (id) pendingToolUses.set(id, entry);
|
|
473
490
|
contentOrder.push(`tool:${toolCalls.length - 1}`);
|
|
@@ -238,6 +238,8 @@ export interface ForkSharedAppResponse {
|
|
|
238
238
|
export interface BundleAppResponse {
|
|
239
239
|
type: "bundle_app_response";
|
|
240
240
|
bundlePath: string;
|
|
241
|
+
/** Base64-encoded PNG of the generated app icon, if available. */
|
|
242
|
+
iconImageBase64?: string;
|
|
241
243
|
manifest: {
|
|
242
244
|
format_version: number;
|
|
243
245
|
name: string;
|
|
@@ -217,6 +217,14 @@ export interface WatchCompleteRequest {
|
|
|
217
217
|
watchId: string;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/** Server → Client: bootstrap failure during learn-mode recording setup. */
|
|
221
|
+
export interface RideShotgunError {
|
|
222
|
+
type: "ride_shotgun_error";
|
|
223
|
+
watchId: string;
|
|
224
|
+
sessionId: string;
|
|
225
|
+
message: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
220
228
|
// --- Domain-level union aliases (consumed by the barrel file) ---
|
|
221
229
|
|
|
222
230
|
export type _ComputerUseClientMessages =
|
|
@@ -236,6 +244,7 @@ export type _ComputerUseServerMessages =
|
|
|
236
244
|
| TaskRouted
|
|
237
245
|
| RideShotgunProgress
|
|
238
246
|
| RideShotgunResult
|
|
247
|
+
| RideShotgunError
|
|
239
248
|
| WatchStarted
|
|
240
249
|
| WatchCompleteRequest
|
|
241
250
|
| RecordingStart
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Contact management: list, get,
|
|
1
|
+
// Contact management: list, get, update channel status, and delete.
|
|
2
2
|
|
|
3
3
|
// === Client → Server ===
|
|
4
4
|
|
|
5
5
|
export interface ContactsRequest {
|
|
6
6
|
type: "contacts";
|
|
7
|
-
action: "list" | "get" | "update_channel";
|
|
8
|
-
/** Contact ID (get
|
|
7
|
+
action: "list" | "get" | "update_channel" | "delete";
|
|
8
|
+
/** Contact ID (get and delete). */
|
|
9
9
|
contactId?: string;
|
|
10
10
|
/** Channel ID (update_channel only). */
|
|
11
11
|
channelId?: string;
|
|
@@ -25,6 +25,8 @@ export interface ContactsInviteRequest {
|
|
|
25
25
|
status?: string;
|
|
26
26
|
/** Invitee's first name (voice invite create only). */
|
|
27
27
|
friendName?: string;
|
|
28
|
+
/** Contact display name for personalizing invite instructions (create only). */
|
|
29
|
+
contactName?: string;
|
|
28
30
|
/** Guardian's first name (voice invite create only). */
|
|
29
31
|
guardianName?: string;
|
|
30
32
|
}
|
|
@@ -80,6 +80,8 @@ export interface ToolUseStart {
|
|
|
80
80
|
toolName: string;
|
|
81
81
|
input: Record<string, unknown>;
|
|
82
82
|
sessionId?: string;
|
|
83
|
+
/** The tool_use block ID for client-side correlation. */
|
|
84
|
+
toolUseId?: string;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export interface ToolOutputChunk {
|
|
@@ -240,6 +242,8 @@ export interface ConfirmationStateChanged {
|
|
|
240
242
|
causedByRequestId?: string;
|
|
241
243
|
/** Normalized user text for analytics/debug (e.g. "approve", "deny"). */
|
|
242
244
|
decisionText?: string;
|
|
245
|
+
/** The tool_use block ID this confirmation applies to, for disambiguating parallel tool calls. */
|
|
246
|
+
toolUseId?: string;
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
/**
|
|
@@ -288,6 +288,14 @@ export interface HistoryResponseToolCall {
|
|
|
288
288
|
isError?: boolean;
|
|
289
289
|
/** Base64-encoded image data from tool contentBlocks (e.g. browser_screenshot). */
|
|
290
290
|
imageData?: string;
|
|
291
|
+
/** Unix ms when the tool started executing. */
|
|
292
|
+
startedAt?: number;
|
|
293
|
+
/** Unix ms when the tool completed. */
|
|
294
|
+
completedAt?: number;
|
|
295
|
+
/** Confirmation decision for this tool call: "approved" | "denied" | "timed_out". */
|
|
296
|
+
confirmationDecision?: string;
|
|
297
|
+
/** Friendly label for the confirmation (e.g. "Edit File", "Run Command"). */
|
|
298
|
+
confirmationLabel?: string;
|
|
291
299
|
}
|
|
292
300
|
|
|
293
301
|
export interface HistoryResponseSurface {
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
import { ensureVellumGuardianBinding } from "../runtime/guardian-vellum-migration.js";
|
|
48
48
|
import { RuntimeHttpServer } from "../runtime/http-server.js";
|
|
49
49
|
import { startScheduler } from "../schedule/scheduler.js";
|
|
50
|
-
import { migrateKeychainToEncrypted } from "../security/keychain-to-encrypted-migration.js";
|
|
51
50
|
import { getLogger, initLogger } from "../util/logger.js";
|
|
52
51
|
import {
|
|
53
52
|
ensureDataDir,
|
|
@@ -143,10 +142,6 @@ export async function runDaemon(): Promise<void> {
|
|
|
143
142
|
migrateToWorkspaceLayout();
|
|
144
143
|
ensureDataDir();
|
|
145
144
|
|
|
146
|
-
// Copy any existing macOS keychain secrets into the encrypted file store
|
|
147
|
-
// before config loads, so the new encrypted-store-first read path sees them.
|
|
148
|
-
migrateKeychainToEncrypted();
|
|
149
|
-
|
|
150
145
|
// Load (or generate + persist) the auth signing key so tokens survive
|
|
151
146
|
// daemon restarts. Must happen after ensureDataDir() creates the
|
|
152
147
|
// protected directory.
|
|
@@ -2,6 +2,11 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import type * as net from "node:net";
|
|
3
3
|
|
|
4
4
|
import { autoNavigate } from "../tools/browser/auto-navigate.js";
|
|
5
|
+
import {
|
|
6
|
+
type CdpSession,
|
|
7
|
+
ensureChromeWithCdp,
|
|
8
|
+
minimizeChromeWindow,
|
|
9
|
+
} from "../tools/browser/chrome-cdp.js";
|
|
5
10
|
import { NetworkRecorder } from "../tools/browser/network-recorder.js";
|
|
6
11
|
import type { SessionRecording } from "../tools/browser/network-recording-types.js";
|
|
7
12
|
import { saveRecording } from "../tools/browser/recording-store.js";
|
|
@@ -24,6 +29,9 @@ const log = getLogger("ride-shotgun-handler");
|
|
|
24
29
|
/** Active network recorders keyed by watchId. */
|
|
25
30
|
const activeRecorders = new Map<string, NetworkRecorder>();
|
|
26
31
|
|
|
32
|
+
/** Active CDP sessions keyed by watchId — tracks browser ownership for cleanup. */
|
|
33
|
+
const activeCdpSessions = new Map<string, CdpSession>();
|
|
34
|
+
|
|
27
35
|
/** Active progress interval timers keyed by watchId, cleared on session completion. */
|
|
28
36
|
const activeProgressIntervals = new Map<string, NodeJS.Timeout>();
|
|
29
37
|
|
|
@@ -71,20 +79,48 @@ async function completeSession(session: WatchSession): Promise<void> {
|
|
|
71
79
|
|
|
72
80
|
// In learn mode, stop recording and save — skip the LLM summary (not needed)
|
|
73
81
|
if (session.isLearnMode && session.recordingId) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
session.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
const hasRecorder = activeRecorders.has(watchId);
|
|
83
|
+
|
|
84
|
+
if (hasRecorder) {
|
|
85
|
+
session.savedRecordingPath = await finalizeLearnRecording(
|
|
86
|
+
watchId,
|
|
87
|
+
session,
|
|
88
|
+
session.recordingId,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Clean up the CDP session — minimize if we launched Chrome, leave it alone otherwise
|
|
93
|
+
const cdpSession = activeCdpSessions.get(watchId);
|
|
94
|
+
if (cdpSession) {
|
|
95
|
+
activeCdpSessions.delete(watchId);
|
|
96
|
+
if (cdpSession.launchedByUs) {
|
|
97
|
+
try {
|
|
98
|
+
await minimizeChromeWindow(cdpSession.baseUrl);
|
|
99
|
+
log.info({ watchId }, "Minimized assistant-launched Chrome window");
|
|
100
|
+
} catch (err) {
|
|
101
|
+
log.debug({ err, watchId }, "Failed to minimize Chrome window");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Use bootstrapFailureReason as the primary discriminator — hasRecorder
|
|
107
|
+
// alone can't distinguish "browser never launched" from "recorder failed
|
|
108
|
+
// after retries" since both leave activeRecorders empty.
|
|
109
|
+
const summary = session.bootstrapFailureReason
|
|
110
|
+
? `Learn session failed — ${session.bootstrapFailureReason}`
|
|
111
|
+
: session.savedRecordingPath
|
|
82
112
|
? "Learn session completed — recording saved."
|
|
83
|
-
: "Learn session completed — recording failed to save."
|
|
84
|
-
|
|
113
|
+
: "Learn session completed — recording failed to save.";
|
|
114
|
+
|
|
115
|
+
lastSummaryBySession.set(sessionId, summary);
|
|
85
116
|
session.status = "completed";
|
|
86
117
|
log.info(
|
|
87
|
-
{
|
|
118
|
+
{
|
|
119
|
+
watchId,
|
|
120
|
+
sessionId,
|
|
121
|
+
hasRecorder,
|
|
122
|
+
bootstrapFailureReason: session.bootstrapFailureReason,
|
|
123
|
+
},
|
|
88
124
|
"Learn session complete — firing completion notifier",
|
|
89
125
|
);
|
|
90
126
|
fireWatchCompletionNotifier(sessionId, session);
|
|
@@ -142,10 +178,67 @@ export async function handleRideShotgunStart(
|
|
|
142
178
|
"Session created and stored in watchSessions map",
|
|
143
179
|
);
|
|
144
180
|
|
|
145
|
-
// In learn mode,
|
|
146
|
-
// Retry a few times since Chrome may still be starting up after the Swift client restarts it.
|
|
181
|
+
// In learn mode, ensure Chrome is available with CDP, then connect for network recording.
|
|
147
182
|
if (isLearnMode) {
|
|
148
183
|
const startRecording = async () => {
|
|
184
|
+
// Ensure Chrome is running with CDP — launches it if needed
|
|
185
|
+
let cdpSession: CdpSession;
|
|
186
|
+
try {
|
|
187
|
+
cdpSession = await ensureChromeWithCdp({
|
|
188
|
+
startUrl: targetDomain ? `https://${targetDomain}` : undefined,
|
|
189
|
+
});
|
|
190
|
+
// If session completed while we were awaiting Chrome, skip storing to avoid a stale map entry
|
|
191
|
+
if (session.status !== "active") {
|
|
192
|
+
log.info(
|
|
193
|
+
{ watchId, status: session.status },
|
|
194
|
+
"Session no longer active after CDP launch — skipping recording",
|
|
195
|
+
);
|
|
196
|
+
// If we launched Chrome, minimize it since completeSession already ran and won't find it
|
|
197
|
+
if (cdpSession.launchedByUs) {
|
|
198
|
+
try {
|
|
199
|
+
await minimizeChromeWindow(cdpSession.baseUrl);
|
|
200
|
+
log.info(
|
|
201
|
+
{ watchId },
|
|
202
|
+
"Minimized assistant-launched Chrome window (post-session)",
|
|
203
|
+
);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
log.debug(
|
|
206
|
+
{ err, watchId },
|
|
207
|
+
"Failed to minimize Chrome window (post-session)",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
activeCdpSessions.set(watchId, cdpSession);
|
|
214
|
+
log.info(
|
|
215
|
+
{
|
|
216
|
+
watchId,
|
|
217
|
+
launchedByUs: cdpSession.launchedByUs,
|
|
218
|
+
baseUrl: cdpSession.baseUrl,
|
|
219
|
+
},
|
|
220
|
+
"CDP session established",
|
|
221
|
+
);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
log.warn(
|
|
224
|
+
{ err, watchId },
|
|
225
|
+
"Failed to ensure Chrome with CDP — cannot start recording",
|
|
226
|
+
);
|
|
227
|
+
ctx.send(socket, {
|
|
228
|
+
type: "ride_shotgun_error",
|
|
229
|
+
watchId,
|
|
230
|
+
sessionId,
|
|
231
|
+
message:
|
|
232
|
+
"Failed to start browser — Chrome CDP could not be launched.",
|
|
233
|
+
});
|
|
234
|
+
// Fail-fast: complete the session immediately instead of waiting for timeout
|
|
235
|
+
session.bootstrapFailureReason = "browser could not be started.";
|
|
236
|
+
await completeSession(session);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const cdpBaseUrl = cdpSession.baseUrl;
|
|
241
|
+
|
|
149
242
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
150
243
|
// Check if session is still active before each attempt
|
|
151
244
|
if (session.status !== "active") {
|
|
@@ -156,7 +249,7 @@ export async function handleRideShotgunStart(
|
|
|
156
249
|
return;
|
|
157
250
|
}
|
|
158
251
|
try {
|
|
159
|
-
const recorder = new NetworkRecorder(targetDomain);
|
|
252
|
+
const recorder = new NetworkRecorder(targetDomain, cdpBaseUrl);
|
|
160
253
|
recorder.loginSignals = getLoginSignals(targetDomain);
|
|
161
254
|
await recorder.startDirect();
|
|
162
255
|
// If session completed while we were connecting, stop immediately to avoid leak
|
|
@@ -248,7 +341,7 @@ export async function handleRideShotgunStart(
|
|
|
248
341
|
clearInterval(checkInterval);
|
|
249
342
|
}
|
|
250
343
|
}, 1000);
|
|
251
|
-
navigateXPages(abortSignal)
|
|
344
|
+
navigateXPages({ abortSignal, cdpBaseUrl })
|
|
252
345
|
.then((completed) => {
|
|
253
346
|
clearInterval(checkInterval);
|
|
254
347
|
log.info(
|
|
@@ -275,16 +368,20 @@ export async function handleRideShotgunStart(
|
|
|
275
368
|
clearInterval(checkInterval);
|
|
276
369
|
}
|
|
277
370
|
}, 1000);
|
|
278
|
-
autoNavigate(navDomain,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
371
|
+
autoNavigate(navDomain, {
|
|
372
|
+
abortSignal,
|
|
373
|
+
onProgress: (progress) => {
|
|
374
|
+
// Send progress to connected client
|
|
375
|
+
if (progress.type === "visiting" && progress.url) {
|
|
376
|
+
const shortUrl = progress.url.replace(/^https?:\/\//, "");
|
|
377
|
+
ctx.send(socket, {
|
|
378
|
+
type: "ride_shotgun_progress",
|
|
379
|
+
watchId,
|
|
380
|
+
message: `[${progress.pageNumber || "?"}] ${shortUrl}`,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
cdpBaseUrl,
|
|
288
385
|
})
|
|
289
386
|
.then((visited) => {
|
|
290
387
|
clearInterval(checkInterval);
|
|
@@ -326,6 +423,15 @@ export async function handleRideShotgunStart(
|
|
|
326
423
|
{ err, watchId },
|
|
327
424
|
"Failed to start network recording after 10 attempts",
|
|
328
425
|
);
|
|
426
|
+
ctx.send(socket, {
|
|
427
|
+
type: "ride_shotgun_error",
|
|
428
|
+
watchId,
|
|
429
|
+
sessionId,
|
|
430
|
+
message: "Failed to start network recording after 10 attempts.",
|
|
431
|
+
});
|
|
432
|
+
session.bootstrapFailureReason =
|
|
433
|
+
"network recording could not be started after 10 attempts.";
|
|
434
|
+
await completeSession(session);
|
|
329
435
|
}
|
|
330
436
|
}
|
|
331
437
|
}
|
|
@@ -336,6 +442,14 @@ export async function handleRideShotgunStart(
|
|
|
336
442
|
|
|
337
443
|
// Set timeout for duration expiry
|
|
338
444
|
session.timeoutHandle = setTimeout(() => {
|
|
445
|
+
if (
|
|
446
|
+
session.isLearnMode &&
|
|
447
|
+
!activeRecorders.has(watchId) &&
|
|
448
|
+
!session.bootstrapFailureReason
|
|
449
|
+
) {
|
|
450
|
+
session.bootstrapFailureReason =
|
|
451
|
+
"session timed out before recording could start.";
|
|
452
|
+
}
|
|
339
453
|
completeSession(session);
|
|
340
454
|
}, durationSeconds * 1000);
|
|
341
455
|
|
|
@@ -67,6 +67,22 @@ export interface EventHandlerState {
|
|
|
67
67
|
firstThinkingDeltaEmitted: boolean;
|
|
68
68
|
/** Name of the last completed tool, used to generate contextual statusText. */
|
|
69
69
|
lastCompletedToolName: string | undefined;
|
|
70
|
+
/** Tracks tool_use_id → timing data for persisting on content blocks. */
|
|
71
|
+
readonly toolCallTimestamps: Map<
|
|
72
|
+
string,
|
|
73
|
+
{ startedAt: number; completedAt?: number }
|
|
74
|
+
>;
|
|
75
|
+
/** The tool_use_id of the currently executing tool (set in handleToolUse, cleared in handleToolResult). */
|
|
76
|
+
currentToolUseId: string | undefined;
|
|
77
|
+
/** Maps confirmation requestId → tool_use_id for linking decisions to tools. */
|
|
78
|
+
readonly requestIdToToolUseId: Map<string, string>;
|
|
79
|
+
/** Stores confirmation outcomes keyed by tool_use_id. */
|
|
80
|
+
readonly toolConfirmationOutcomes: Map<
|
|
81
|
+
string,
|
|
82
|
+
{ decision: string; label: string }
|
|
83
|
+
>;
|
|
84
|
+
/** tool_use_ids emitted in the current turn (populated in handleToolUse, cleared after annotation). */
|
|
85
|
+
currentTurnToolUseIds: string[];
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
/** Immutable context shared across event handlers within a single agent loop run. */
|
|
@@ -108,6 +124,11 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
108
124
|
firstTextDeltaEmitted: false,
|
|
109
125
|
firstThinkingDeltaEmitted: false,
|
|
110
126
|
lastCompletedToolName: undefined,
|
|
127
|
+
toolCallTimestamps: new Map(),
|
|
128
|
+
currentToolUseId: undefined,
|
|
129
|
+
requestIdToToolUseId: new Map(),
|
|
130
|
+
toolConfirmationOutcomes: new Map(),
|
|
131
|
+
currentTurnToolUseIds: [],
|
|
111
132
|
};
|
|
112
133
|
}
|
|
113
134
|
|
|
@@ -253,6 +274,9 @@ export function handleToolUse(
|
|
|
253
274
|
): void {
|
|
254
275
|
state.toolUseIdToName.set(event.id, event.name);
|
|
255
276
|
state.currentTurnToolNames.push(event.name);
|
|
277
|
+
state.toolCallTimestamps.set(event.id, { startedAt: Date.now() });
|
|
278
|
+
state.currentToolUseId = event.id;
|
|
279
|
+
state.currentTurnToolUseIds.push(event.id);
|
|
256
280
|
const statusText = `Running ${friendlyToolName(event.name)}`;
|
|
257
281
|
deps.ctx.emitActivityState(
|
|
258
282
|
"tool_running",
|
|
@@ -266,6 +290,7 @@ export function handleToolUse(
|
|
|
266
290
|
toolName: event.name,
|
|
267
291
|
input: event.input,
|
|
268
292
|
sessionId: deps.ctx.conversationId,
|
|
293
|
+
toolUseId: event.id,
|
|
269
294
|
});
|
|
270
295
|
}
|
|
271
296
|
|
|
@@ -392,6 +417,11 @@ export function handleToolResult(
|
|
|
392
417
|
contentBlocks: event.contentBlocks,
|
|
393
418
|
});
|
|
394
419
|
|
|
420
|
+
// Record tool completion timestamp
|
|
421
|
+
const ts = state.toolCallTimestamps.get(event.toolUseId);
|
|
422
|
+
if (ts) ts.completedAt = Date.now();
|
|
423
|
+
state.currentToolUseId = undefined;
|
|
424
|
+
|
|
395
425
|
const toolName = state.toolUseIdToName.get(event.toolUseId);
|
|
396
426
|
if (toolName === "file_write" || toolName === "bash") {
|
|
397
427
|
deps.ctx.markWorkspaceTopLevelDirty();
|
|
@@ -433,6 +463,68 @@ export function handleToolResult(
|
|
|
433
463
|
deps.reqId,
|
|
434
464
|
statusText,
|
|
435
465
|
);
|
|
466
|
+
|
|
467
|
+
// Once all tools for this turn have completed, annotate the persisted
|
|
468
|
+
// assistant message with timing and confirmation metadata.
|
|
469
|
+
const allToolsDone = state.currentTurnToolUseIds.every((id) => {
|
|
470
|
+
const ts = state.toolCallTimestamps.get(id);
|
|
471
|
+
return ts && ts.completedAt != null;
|
|
472
|
+
});
|
|
473
|
+
if (allToolsDone && state.currentTurnToolUseIds.length > 0) {
|
|
474
|
+
annotatePersistedAssistantMessage(state);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* After all tools for the current turn complete, fetch the persisted assistant
|
|
480
|
+
* message, annotate its tool_use blocks with timing and confirmation metadata,
|
|
481
|
+
* and update the DB. This runs post-tool-execution so the metadata maps are
|
|
482
|
+
* fully populated (unlike message_complete which fires before tools run).
|
|
483
|
+
*/
|
|
484
|
+
function annotatePersistedAssistantMessage(state: EventHandlerState): void {
|
|
485
|
+
const messageId = state.lastAssistantMessageId;
|
|
486
|
+
if (!messageId) return;
|
|
487
|
+
|
|
488
|
+
const row = conversationStore.getMessageById(messageId);
|
|
489
|
+
if (!row) return;
|
|
490
|
+
|
|
491
|
+
let content: ContentBlock[];
|
|
492
|
+
try {
|
|
493
|
+
content = JSON.parse(row.content) as ContentBlock[];
|
|
494
|
+
} catch {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let modified = false;
|
|
499
|
+
for (const block of content) {
|
|
500
|
+
if (block.type === "tool_use") {
|
|
501
|
+
const rec = block as unknown as Record<string, unknown>;
|
|
502
|
+
const id = rec.id as string | undefined;
|
|
503
|
+
if (!id) continue;
|
|
504
|
+
|
|
505
|
+
const ts = state.toolCallTimestamps.get(id);
|
|
506
|
+
if (ts) {
|
|
507
|
+
rec._startedAt = ts.startedAt;
|
|
508
|
+
if (ts.completedAt != null) {
|
|
509
|
+
rec._completedAt = ts.completedAt;
|
|
510
|
+
}
|
|
511
|
+
modified = true;
|
|
512
|
+
}
|
|
513
|
+
const confirmation = state.toolConfirmationOutcomes.get(id);
|
|
514
|
+
if (confirmation) {
|
|
515
|
+
rec._confirmationDecision = confirmation.decision;
|
|
516
|
+
rec._confirmationLabel = confirmation.label;
|
|
517
|
+
modified = true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (modified) {
|
|
523
|
+
conversationStore.updateMessageContent(messageId, JSON.stringify(content));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Clear for the next turn
|
|
527
|
+
state.currentTurnToolUseIds = [];
|
|
436
528
|
}
|
|
437
529
|
|
|
438
530
|
export function handleError(
|
|
@@ -465,6 +557,9 @@ export async function handleMessageComplete(
|
|
|
465
557
|
deps: EventHandlerDeps,
|
|
466
558
|
event: Extract<AgentEvent, { type: "message_complete" }>,
|
|
467
559
|
): Promise<void> {
|
|
560
|
+
// Reset per-turn tool tracking for the new turn.
|
|
561
|
+
state.currentTurnToolUseIds = [];
|
|
562
|
+
|
|
468
563
|
// Flush any remaining directive display buffer
|
|
469
564
|
if (state.pendingDirectiveDisplayBuffer.length > 0) {
|
|
470
565
|
deps.onEvent({
|
|
@@ -533,6 +628,11 @@ export async function handleMessageComplete(
|
|
|
533
628
|
);
|
|
534
629
|
}
|
|
535
630
|
|
|
631
|
+
// NOTE: Tool timing/confirmation annotations are NOT applied here because
|
|
632
|
+
// message_complete fires BEFORE tool_use/tool_result events. The annotations
|
|
633
|
+
// are applied in handleToolResult after all tools for the turn complete,
|
|
634
|
+
// then the persisted message is updated via updateMessageContent.
|
|
635
|
+
|
|
536
636
|
// Build content with UI surfaces
|
|
537
637
|
const contentWithSurfaces: ContentBlock[] = [...cleanedBlocks];
|
|
538
638
|
for (const surface of deps.ctx.currentTurnSurfaces) {
|
|
@@ -118,6 +118,27 @@ import type { TraceEmitter } from "./trace-emitter.js";
|
|
|
118
118
|
|
|
119
119
|
const log = getLogger("session-agent-loop");
|
|
120
120
|
|
|
121
|
+
/** Title-cased friendly labels for tool names, used in confirmation chips. */
|
|
122
|
+
const TOOL_FRIENDLY_LABEL: Record<string, string> = {
|
|
123
|
+
bash: "Run Command",
|
|
124
|
+
web_search: "Web Search",
|
|
125
|
+
web_fetch: "Web Fetch",
|
|
126
|
+
file_read: "Read File",
|
|
127
|
+
file_write: "Write File",
|
|
128
|
+
file_edit: "Edit File",
|
|
129
|
+
browser_navigate: "Browser",
|
|
130
|
+
browser_click: "Browser",
|
|
131
|
+
browser_type: "Browser",
|
|
132
|
+
browser_screenshot: "Browser",
|
|
133
|
+
browser_scroll: "Browser",
|
|
134
|
+
browser_wait: "Browser",
|
|
135
|
+
app_create: "Create App",
|
|
136
|
+
app_update: "Update App",
|
|
137
|
+
skill_load: "Load Skill",
|
|
138
|
+
app_file_edit: "Edit App File",
|
|
139
|
+
app_file_write: "Write App File",
|
|
140
|
+
};
|
|
141
|
+
|
|
121
142
|
type GitServiceInitializer = {
|
|
122
143
|
ensureInitialized(): Promise<void>;
|
|
123
144
|
};
|
|
@@ -222,6 +243,18 @@ export interface AgentLoopSessionContext {
|
|
|
222
243
|
: never,
|
|
223
244
|
): void;
|
|
224
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Optional callback invoked by the Session when a confirmation state changes.
|
|
248
|
+
* The agent loop registers this to track requestId → toolUseId mappings
|
|
249
|
+
* and record confirmation outcomes for persistence.
|
|
250
|
+
*/
|
|
251
|
+
onConfirmationOutcome?: (
|
|
252
|
+
requestId: string,
|
|
253
|
+
state: string,
|
|
254
|
+
toolName?: string,
|
|
255
|
+
toolUseId?: string,
|
|
256
|
+
) => void;
|
|
257
|
+
|
|
225
258
|
getWorkspaceGitService?: (workspaceDir: string) => GitServiceInitializer;
|
|
226
259
|
commitTurnChanges?: typeof commitTurnChanges;
|
|
227
260
|
|
|
@@ -432,6 +465,44 @@ export async function runAgentLoopImpl(
|
|
|
432
465
|
}
|
|
433
466
|
|
|
434
467
|
const state = createEventHandlerState();
|
|
468
|
+
|
|
469
|
+
// Register confirmation outcome tracker so the agent loop can link
|
|
470
|
+
// confirmation decisions to tool_use_ids for persistence.
|
|
471
|
+
ctx.onConfirmationOutcome = (
|
|
472
|
+
requestId,
|
|
473
|
+
confirmationState,
|
|
474
|
+
toolName,
|
|
475
|
+
toolUseId,
|
|
476
|
+
) => {
|
|
477
|
+
if (confirmationState === "pending") {
|
|
478
|
+
// Use the toolUseId passed from the prompter (which knows which tool
|
|
479
|
+
// requested confirmation) instead of the ambient state.currentToolUseId,
|
|
480
|
+
// which is unreliable when multiple tools execute in parallel.
|
|
481
|
+
const resolvedToolUseId = toolUseId ?? state.currentToolUseId;
|
|
482
|
+
if (resolvedToolUseId) {
|
|
483
|
+
state.requestIdToToolUseId.set(requestId, resolvedToolUseId);
|
|
484
|
+
}
|
|
485
|
+
} else if (
|
|
486
|
+
confirmationState === "approved" ||
|
|
487
|
+
confirmationState === "denied" ||
|
|
488
|
+
confirmationState === "timed_out"
|
|
489
|
+
) {
|
|
490
|
+
const resolvedId =
|
|
491
|
+
state.requestIdToToolUseId.get(requestId) ?? toolUseId;
|
|
492
|
+
if (resolvedId) {
|
|
493
|
+
const name = state.toolUseIdToName.get(resolvedId) ?? toolName ?? "";
|
|
494
|
+
// Build a friendly label from the tool name
|
|
495
|
+
const label =
|
|
496
|
+
TOOL_FRIENDLY_LABEL[name] ??
|
|
497
|
+
name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
498
|
+
state.toolConfirmationOutcomes.set(resolvedId, {
|
|
499
|
+
decision: confirmationState,
|
|
500
|
+
label,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
435
506
|
let runMessages = ctx.messages;
|
|
436
507
|
|
|
437
508
|
const memoryResult = await prepareMemoryContext(
|
|
@@ -1347,6 +1418,7 @@ export async function runAgentLoopImpl(
|
|
|
1347
1418
|
|
|
1348
1419
|
ctx.abortController = null;
|
|
1349
1420
|
ctx.processing = false;
|
|
1421
|
+
ctx.onConfirmationOutcome = undefined;
|
|
1350
1422
|
ctx.surfaceActionRequestIds.delete(ctx.currentRequestId ?? "");
|
|
1351
1423
|
ctx.currentRequestId = undefined;
|
|
1352
1424
|
ctx.currentActiveSurfaceId = undefined;
|