@cursor/july 0.1.96 → 0.1.97
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/README.md +3 -2
- package/dist/bin/agent-serve.js +14 -3
- package/dist/channels/origin/api.d.ts +15 -4
- package/dist/channels/origin/api.d.ts.map +1 -1
- package/dist/channels/origin/api.js +19 -3
- package/dist/channels/origin/binding.d.ts +11 -0
- package/dist/channels/origin/binding.d.ts.map +1 -1
- package/dist/channels/origin/binding.js +41 -25
- package/dist/channels/origin/index.d.ts +1 -0
- package/dist/channels/origin/index.d.ts.map +1 -1
- package/dist/channels/origin/index.js +1 -0
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +3 -0
- package/dist/channels/origin/reactions.d.ts +56 -0
- package/dist/channels/origin/reactions.d.ts.map +1 -0
- package/dist/channels/origin/reactions.js +67 -0
- package/dist/channels/origin/types.d.ts +11 -0
- package/dist/channels/origin/types.d.ts.map +1 -1
- package/dist/channels/slack/approvals.d.ts +16 -0
- package/dist/channels/slack/approvals.d.ts.map +1 -1
- package/dist/channels/slack/approvals.js +86 -0
- package/dist/channels/slack/cli-project.d.ts +19 -0
- package/dist/channels/slack/cli-project.d.ts.map +1 -0
- package/dist/channels/slack/cli-project.js +234 -0
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +97 -39
- package/dist/channels/slack/init.d.ts +3 -2
- package/dist/channels/slack/init.d.ts.map +1 -1
- package/dist/channels/slack/init.js +5 -5
- package/dist/channels/slack/interactive.d.ts +2 -1
- package/dist/channels/slack/interactive.d.ts.map +1 -1
- package/dist/channels/slack/interactive.js +15 -4
- package/dist/channels/slack/manifest.d.ts +1 -1
- package/dist/channels/slack/manifest.js +1 -1
- package/dist/channels/slack/setup.d.ts +1 -1
- package/dist/channels/slack/setup.d.ts.map +1 -1
- package/dist/channels/slack/setup.js +10 -6
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +1 -0
- package/dist/channels/slack/tool-approval-events.d.ts.map +1 -1
- package/dist/channels/slack/tool-approval-events.js +91 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.jXleCmOc.js → app.B3rWNYE1.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BU9nHdnQ.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.8LhyglIT.js → VPLocalSearchBox.C1lhqJJR.js} +1 -1
- package/dist/docs/assets/chunks/{theme.9ged-pr2.js → theme.B40_SXuv.js} +2 -2
- package/dist/docs/assets/{deployment.md.D9msOFOW.js → deployment.md.MFPKqsqI.js} +2 -2
- package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.js → guides_mcp-oauth.md.CN-6YmTJ.js} +4 -4
- package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.lean.js → guides_mcp-oauth.md.CN-6YmTJ.lean.js} +1 -1
- package/dist/docs/assets/guides_slack.md.VDXQV3ja.js +22 -0
- package/dist/docs/assets/guides_slack.md.VDXQV3ja.lean.js +1 -0
- package/dist/docs/assets/index.md.CVeRUOeZ.js +5 -0
- package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.js → reference_agent-config.md.BHeYMgme.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.CCnG-nJx.js → reference_cli.md.RyZf5OTE.js} +9 -6
- package/dist/docs/assets/{reference_cli.md.CCnG-nJx.lean.js → reference_cli.md.RyZf5OTE.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.js → reference_connections.md.DxldvyIB.js} +4 -3
- package/dist/docs/assets/{reference_http-api.md.D89k1mdm.js → reference_http-api.md.5zOAbV86.js} +1 -1
- package/dist/docs/assets/reference_sessions.md.NxFP_CAC.js +1 -0
- package/dist/docs/assets/{reference_tools.md.BHeXn2id.js → reference_tools.md.B84gw9Ii.js} +1 -1
- package/dist/docs/assets/templates_agents-md.md.CXSMZljE.js +2 -0
- package/dist/docs/assets/templates_agents-md.md.CXSMZljE.lean.js +1 -0
- package/dist/docs/assets/troubleshooting.md.DCiPBhYs.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/deployment.md +5 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/convert-automation.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +8 -8
- package/dist/docs/guides/mcp-oauth.md +26 -8
- package/dist/docs/guides/opentelemetry.html +4 -4
- package/dist/docs/guides/slack.html +17 -33
- package/dist/docs/guides/slack.md +92 -201
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/index.md +5 -3
- package/dist/docs/llms-full.txt +299 -653
- package/dist/docs/llms.txt +2 -2
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +6 -6
- package/dist/docs/reference/agent-config.md +2 -2
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +13 -10
- package/dist/docs/reference/cli.md +38 -16
- package/dist/docs/reference/connections.html +9 -8
- package/dist/docs/reference/connections.md +6 -3
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/http-api.md +2 -1
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +5 -5
- package/dist/docs/reference/sessions.md +4 -2
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +6 -6
- package/dist/docs/reference/tools.md +9 -2
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +4 -4
- package/dist/docs/templates/agents-md.html +28 -0
- package/dist/docs/templates/agents-md.md +105 -0
- package/dist/docs/templates/code-wiki.html +5 -5
- package/dist/docs/templates/demo.html +4 -4
- package/dist/docs/templates/pr-autofixer.html +4 -4
- package/dist/docs/templates/security-help.html +4 -4
- package/dist/docs/templates/security-reviewer.html +5 -5
- package/dist/docs/templates/triage.html +4 -4
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/docs/troubleshooting.md +2 -2
- package/dist/evals/run-facts.d.ts.map +1 -1
- package/dist/evals/run-facts.js +27 -0
- package/dist/internal/advertise-tools.d.ts.map +1 -1
- package/dist/internal/advertise-tools.js +15 -9
- package/dist/internal/approval-gate.d.ts +12 -13
- package/dist/internal/approval-gate.d.ts.map +1 -1
- package/dist/internal/approval-gate.js +16 -21
- package/dist/internal/chat-client.js +10 -0
- package/dist/internal/cli-deploy.d.ts +5 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +50 -17
- package/dist/internal/cli-mcp-oauth.js +8 -18
- package/dist/internal/cli-slack-manual.d.ts +24 -2
- package/dist/internal/cli-slack-manual.d.ts.map +1 -1
- package/dist/internal/cli-slack-manual.js +251 -20
- package/dist/internal/cli-slack.d.ts +9 -2
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +29 -82
- package/dist/internal/cursor/account-mcp.d.ts +1 -0
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
- package/dist/internal/cursor/account-mcp.js +17 -0
- package/dist/internal/cursor-agent-template.d.ts +1 -1
- package/dist/internal/cursor-agent-template.d.ts.map +1 -1
- package/dist/internal/cursor-agent-template.js +1 -0
- package/dist/internal/deploy-client.d.ts +17 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +31 -10
- package/dist/internal/env-file.d.ts +19 -0
- package/dist/internal/env-file.d.ts.map +1 -0
- package/dist/internal/env-file.js +90 -0
- package/dist/internal/framework-file-storage.d.ts +1 -1
- package/dist/internal/framework-file-storage.d.ts.map +1 -1
- package/dist/internal/framework-file-storage.js +6 -0
- package/dist/internal/hosted-delivery-protocol.d.ts +7 -0
- package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
- package/dist/internal/hosted-delivery-protocol.js +7 -0
- package/dist/internal/hosted-delivery.d.ts +6 -0
- package/dist/internal/hosted-delivery.d.ts.map +1 -1
- package/dist/internal/hosted-delivery.js +63 -22
- package/dist/internal/hosted-execution-diag.d.ts +48 -0
- package/dist/internal/hosted-execution-diag.d.ts.map +1 -0
- package/dist/internal/hosted-execution-diag.js +150 -0
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +81 -1
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +2 -0
- package/dist/internal/logs-client.d.ts +5 -0
- package/dist/internal/logs-client.d.ts.map +1 -1
- package/dist/internal/logs-client.js +5 -0
- package/dist/internal/mcp-endpoint.js +2 -3
- package/dist/internal/mcp-host.d.ts +10 -0
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +57 -8
- package/dist/internal/mcp-oauth-on-demand.d.ts +68 -0
- package/dist/internal/mcp-oauth-on-demand.d.ts.map +1 -0
- package/dist/internal/mcp-oauth-on-demand.js +287 -0
- package/dist/internal/mcp-oauth-persist.d.ts +42 -0
- package/dist/internal/mcp-oauth-persist.d.ts.map +1 -0
- package/dist/internal/mcp-oauth-persist.js +92 -0
- package/dist/internal/mcp-oauth.d.ts +1 -0
- package/dist/internal/mcp-oauth.d.ts.map +1 -1
- package/dist/internal/mcp-oauth.js +10 -2
- package/dist/internal/otel/collector.d.ts.map +1 -1
- package/dist/internal/otel/collector.js +22 -9
- package/dist/internal/server.d.ts +3 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +49 -22
- package/dist/internal/session-engine.d.ts +88 -3
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +515 -43
- package/dist/internal/sessions-client.d.ts +9 -7
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +3 -1
- package/dist/internal/storage-coordinator.d.ts +7 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +28 -5
- package/dist/internal/storage-roles.d.ts +5 -0
- package/dist/internal/storage-roles.d.ts.map +1 -1
- package/dist/internal/stream-progress.d.ts.map +1 -1
- package/dist/internal/stream-progress.js +10 -0
- package/dist/internal/tool-policy.d.ts +9 -1
- package/dist/internal/tool-policy.d.ts.map +1 -1
- package/dist/internal/tool-policy.js +13 -0
- package/dist/internal/trajectory.d.ts.map +1 -1
- package/dist/internal/trajectory.js +10 -0
- package/dist/internal/turn-outbox.d.ts +24 -0
- package/dist/internal/turn-outbox.d.ts.map +1 -0
- package/dist/internal/turn-outbox.js +80 -0
- package/dist/playground/assets/{index-Cx9CclK_.js → index-DrkI6y5O.js} +45 -45
- package/dist/playground/assets/index-DzNGwm7q.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +1 -1
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +12 -0
- package/dist/storage.d.ts +8 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/types.d.ts +63 -7
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +5 -3
- package/docs/deployment.md +5 -4
- package/docs/guides/mcp-oauth.md +26 -8
- package/docs/guides/slack.md +93 -202
- package/docs/reference/agent-config.md +2 -2
- package/docs/reference/cli.md +38 -16
- package/docs/reference/connections.md +6 -3
- package/docs/reference/http-api.md +2 -1
- package/docs/reference/sessions.md +4 -2
- package/docs/reference/tools.md +9 -2
- package/docs/templates/agents-md.md +110 -0
- package/docs/troubleshooting.md +2 -2
- package/package.json +1 -1
- package/skills/create-agent/SKILL.md +2 -1
- package/skills/mcp-auth/SKILL.md +6 -3
- package/skills/setup-slack/SKILL.md +23 -13
- package/src/bin/agent-serve.ts +13 -1
- package/src/channels/origin/api.ts +24 -5
- package/src/channels/origin/binding.ts +55 -26
- package/src/channels/origin/index.ts +1 -0
- package/src/channels/origin/origin-channel.ts +4 -0
- package/src/channels/origin/reactions.ts +82 -0
- package/src/channels/origin/types.ts +11 -0
- package/src/channels/slack/approvals.ts +105 -0
- package/src/channels/slack/cli-project.ts +267 -0
- package/src/channels/slack/defaults.ts +109 -46
- package/src/channels/slack/init.ts +10 -5
- package/src/channels/slack/interactive.ts +35 -9
- package/src/channels/slack/manifest.ts +1 -1
- package/src/channels/slack/setup.ts +10 -6
- package/src/channels/slack/slack-channel.ts +1 -0
- package/src/channels/slack/tool-approval-events.ts +93 -0
- package/src/evals/run-facts.ts +32 -0
- package/src/internal/advertise-tools.ts +31 -10
- package/src/internal/approval-gate.ts +20 -29
- package/src/internal/chat-client.ts +19 -0
- package/src/internal/cli-deploy.ts +79 -9
- package/src/internal/cli-mcp-oauth.ts +7 -21
- package/src/internal/cli-slack-manual.ts +350 -18
- package/src/internal/cli-slack.ts +40 -84
- package/src/internal/cursor/account-mcp.ts +21 -0
- package/src/internal/cursor-agent-template.ts +1 -0
- package/src/internal/deploy-client.ts +43 -0
- package/src/internal/env-file.ts +84 -0
- package/src/internal/framework-file-storage.ts +7 -1
- package/src/internal/hosted-delivery-protocol.ts +7 -0
- package/src/internal/hosted-delivery.ts +100 -27
- package/src/internal/hosted-execution-diag.ts +207 -0
- package/src/internal/http-channel.ts +99 -0
- package/src/internal/init-project.ts +2 -0
- package/src/internal/logs-client.ts +5 -0
- package/src/internal/mcp-endpoint.ts +13 -4
- package/src/internal/mcp-host.ts +77 -11
- package/src/internal/mcp-oauth-on-demand.ts +352 -0
- package/src/internal/mcp-oauth-persist.ts +127 -0
- package/src/internal/mcp-oauth.ts +17 -1
- package/src/internal/otel/collector.ts +30 -0
- package/src/internal/server.ts +59 -9
- package/src/internal/session-engine.ts +659 -56
- package/src/internal/sessions-client.ts +17 -8
- package/src/internal/storage-coordinator.ts +38 -8
- package/src/internal/storage-roles.ts +7 -0
- package/src/internal/stream-progress.ts +19 -0
- package/src/internal/tool-policy.ts +18 -0
- package/src/internal/trajectory.ts +10 -0
- package/src/internal/turn-outbox.ts +85 -0
- package/src/storage-backends/cursor-hosted.ts +13 -1
- package/src/storage.ts +12 -0
- package/src/types.ts +68 -8
- package/templates/agents-md/README.md +39 -0
- package/templates/agents-md/agent/agent.ts +8 -0
- package/templates/agents-md/agent/channels/github.ts +10 -0
- package/templates/agents-md/agent/channels/queue.ts +28 -0
- package/templates/agents-md/agent/channels/slack.ts +9 -0
- package/templates/agents-md/agent/instructions.md +39 -0
- package/templates/agents-md/agent/lib/collect.ts +439 -0
- package/templates/agents-md/agent/lib/config.ts +8 -0
- package/templates/agents-md/agent/lib/paths.ts +51 -0
- package/templates/agents-md/agent/schedules/weekly.ts +17 -0
- package/templates/agents-md/agent/tools/apply_agents.ts +226 -0
- package/templates/agents-md/evals/agents.eval.ts +135 -0
- package/templates/agents-md/evals/evals.config.ts +5 -0
- package/templates/agents-md/init.json +38 -0
- package/templates/agents-md/package.json +18 -0
- package/templates/agents-md/tsconfig.json +12 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.js +0 -1
- package/dist/docs/assets/guides_slack.md.C32HsdKk.js +0 -38
- package/dist/docs/assets/guides_slack.md.C32HsdKk.lean.js +0 -1
- package/dist/docs/assets/index.md.CI93Ltlb.js +0 -5
- package/dist/docs/assets/reference_sessions.md.D0mIh4KK.js +0 -1
- package/dist/docs/assets/troubleshooting.md.Ctv3T8C2.js +0 -1
- package/dist/docs/design/agsh.md +0 -406
- package/dist/playground/assets/index-B1DDvBeC.css +0 -1
- package/docs/design/agsh.md +0 -406
- /package/dist/docs/assets/{deployment.md.D9msOFOW.lean.js → deployment.md.MFPKqsqI.lean.js} +0 -0
- /package/dist/docs/assets/{index.md.CI93Ltlb.lean.js → index.md.CVeRUOeZ.lean.js} +0 -0
- /package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.lean.js → reference_agent-config.md.BHeYMgme.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.lean.js → reference_connections.md.DxldvyIB.lean.js} +0 -0
- /package/dist/docs/assets/{reference_http-api.md.D89k1mdm.lean.js → reference_http-api.md.5zOAbV86.lean.js} +0 -0
- /package/dist/docs/assets/{reference_sessions.md.D0mIh4KK.lean.js → reference_sessions.md.NxFP_CAC.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.BHeXn2id.lean.js → reference_tools.md.B84gw9Ii.lean.js} +0 -0
- /package/dist/docs/assets/{troubleshooting.md.Ctv3T8C2.lean.js → troubleshooting.md.DCiPBhYs.lean.js} +0 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* event handlers and hooks.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
11
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
11
12
|
import { dirname, join } from "node:path";
|
|
12
13
|
import type { SDKCustomTool, SDKCustomToolResult } from "@cursor/sdk";
|
|
@@ -52,12 +53,15 @@ import {
|
|
|
52
53
|
type DiscoveredTool,
|
|
53
54
|
type HookHandler,
|
|
54
55
|
type HostContext,
|
|
56
|
+
type HostMcpRegistry,
|
|
55
57
|
type HttpChannelDefinition,
|
|
58
|
+
isCursorAccountTransport,
|
|
56
59
|
isHostOauthConnectionTransport,
|
|
57
60
|
isSymbolicConnectionTransport,
|
|
58
61
|
type JsonValue,
|
|
59
62
|
type McpToolAnnotations,
|
|
60
63
|
type PendingApproval,
|
|
64
|
+
type PendingAuth,
|
|
61
65
|
type ReceiveInput,
|
|
62
66
|
type ResolvedAgent,
|
|
63
67
|
type RunnerSubagent,
|
|
@@ -79,7 +83,10 @@ import {
|
|
|
79
83
|
import { ABCollector } from "./ab-collector.js";
|
|
80
84
|
import { foldABStream } from "./ab-fold.js";
|
|
81
85
|
import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
|
|
82
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
type AdvertiseHostMcp,
|
|
88
|
+
resolveAdvertisedTools,
|
|
89
|
+
} from "./advertise-tools.js";
|
|
83
90
|
import { ApprovalGate } from "./approval-gate.js";
|
|
84
91
|
import { ArtifactsStore } from "./artifacts-store.js";
|
|
85
92
|
import { SessionAuthError, samePrincipal } from "./auth.js";
|
|
@@ -125,7 +132,8 @@ import {
|
|
|
125
132
|
parseContinuationToken,
|
|
126
133
|
} from "./ids.js";
|
|
127
134
|
import { validateToolInput } from "./json-schema.js";
|
|
128
|
-
import { McpHost } from "./mcp-host.js";
|
|
135
|
+
import { isMcpNeedsAuthError, McpHost } from "./mcp-host.js";
|
|
136
|
+
import { McpOauthOnDemandHub } from "./mcp-oauth-on-demand.js";
|
|
129
137
|
import type { OtelCollector } from "./otel/collector.js";
|
|
130
138
|
import {
|
|
131
139
|
shouldStartProcessMemoryTelemetry,
|
|
@@ -143,7 +151,7 @@ import { syncSkillsToStore } from "./skills-sync.js";
|
|
|
143
151
|
import { steeringEnabled } from "./steering.js";
|
|
144
152
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
145
153
|
import type { FrameworkSessionStore } from "./storage-roles.js";
|
|
146
|
-
import { decideToolCall,
|
|
154
|
+
import { decideToolCall, directCallEffect } from "./tool-policy.js";
|
|
147
155
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
148
156
|
import {
|
|
149
157
|
type EstimateTurnCostFn,
|
|
@@ -152,6 +160,7 @@ import {
|
|
|
152
160
|
} from "./turn-cost.js";
|
|
153
161
|
import { TurnGovernor } from "./turn-governor.js";
|
|
154
162
|
import { TurnLeaseStore } from "./turn-lease-store.js";
|
|
163
|
+
import { TurnOutbox } from "./turn-outbox.js";
|
|
155
164
|
import {
|
|
156
165
|
buildAgentsMdContent,
|
|
157
166
|
buildCloudMemorySection,
|
|
@@ -258,6 +267,22 @@ export class ApprovalInterruptedError extends Error {
|
|
|
258
267
|
}
|
|
259
268
|
}
|
|
260
269
|
|
|
270
|
+
export class AuthNotFoundError extends Error {
|
|
271
|
+
constructor(sessionId: string, callId: string) {
|
|
272
|
+
super(`No pending MCP auth for session ${sessionId} call ${callId}`);
|
|
273
|
+
this.name = "AuthNotFoundError";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export class AuthInterruptedError extends Error {
|
|
278
|
+
constructor(sessionId: string, callId: string) {
|
|
279
|
+
super(
|
|
280
|
+
`MCP auth for session ${sessionId} call ${callId} is no longer live (host may have restarted while the tool was parked)`
|
|
281
|
+
);
|
|
282
|
+
this.name = "AuthInterruptedError";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
261
286
|
export interface SessionEngineOptions {
|
|
262
287
|
project: AgentProject;
|
|
263
288
|
stateRoot: string;
|
|
@@ -312,6 +337,16 @@ interface ActiveToolContext {
|
|
|
312
337
|
emit: (payload: SessionEventPayload) => void;
|
|
313
338
|
}
|
|
314
339
|
|
|
340
|
+
type McpAuthParkCtx = {
|
|
341
|
+
sessionId: string;
|
|
342
|
+
turnId: string;
|
|
343
|
+
callId: string;
|
|
344
|
+
toolName: string;
|
|
345
|
+
emit: (payload: SessionEventPayload) => void;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const mcpAuthAls = new AsyncLocalStorage<McpAuthParkCtx>();
|
|
349
|
+
|
|
315
350
|
export interface EngineSendOptions extends SendMessageOptions {
|
|
316
351
|
mode?: "chat" | "task";
|
|
317
352
|
/**
|
|
@@ -346,6 +381,8 @@ export class SessionEngine {
|
|
|
346
381
|
readonly askDedupe: SlackAskDedupeStore;
|
|
347
382
|
/** Cross-process turn ownership under `<stateRoot>/turn-leases`. */
|
|
348
383
|
readonly turnLeases: TurnLeaseStore;
|
|
384
|
+
/** Messages for turns another server owns, under `<stateRoot>/outbox`. */
|
|
385
|
+
readonly outbox: TurnOutbox<AdmitTurnOptions>;
|
|
349
386
|
/**
|
|
350
387
|
* Runtime for `agent/storage.ts` (`defineStorage`), when
|
|
351
388
|
* authored. All durable state changes funnel through it (session
|
|
@@ -370,6 +407,13 @@ export class SessionEngine {
|
|
|
370
407
|
/** Sessions with a turn being admitted (before the run promise exists). */
|
|
371
408
|
private readonly pendingTurns = new Set<string>();
|
|
372
409
|
private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
|
|
410
|
+
/**
|
|
411
|
+
* Sessions whose workspace a turn in this process has materialized. A
|
|
412
|
+
* session is busy from admit — before the governor slot and the turn's
|
|
413
|
+
* own materialize — so a read running alongside a turn consults this,
|
|
414
|
+
* not busy, to know whether the workspace is ready to be left alone.
|
|
415
|
+
*/
|
|
416
|
+
private readonly materializedWorkspaces = new Set<string>();
|
|
373
417
|
/** Context used by the cloud turn's HTTP MCP callbacks. */
|
|
374
418
|
private readonly activeToolContexts = new Map<string, ActiveToolContext>();
|
|
375
419
|
/**
|
|
@@ -410,6 +454,11 @@ export class SessionEngine {
|
|
|
410
454
|
private readonly mcpHost: McpHost;
|
|
411
455
|
private readonly platforms: HostPlatforms;
|
|
412
456
|
private readonly approvals = new ApprovalGate();
|
|
457
|
+
private readonly auths = new ApprovalGate<PendingAuth>();
|
|
458
|
+
private readonly oauthHub = new McpOauthOnDemandHub();
|
|
459
|
+
private probeCursorAccount:
|
|
460
|
+
| ((connection: string) => Promise<string | undefined>)
|
|
461
|
+
| undefined;
|
|
413
462
|
private readonly abCollector: ABCollector;
|
|
414
463
|
private readonly otel: OtelCollector | undefined;
|
|
415
464
|
private readonly otelHost: HostOtelSessionApi;
|
|
@@ -458,6 +507,7 @@ export class SessionEngine {
|
|
|
458
507
|
this.turnLeases = new TurnLeaseStore(
|
|
459
508
|
join(options.stateRoot, "turn-leases")
|
|
460
509
|
);
|
|
510
|
+
this.outbox = new TurnOutbox(join(options.stateRoot, "outbox"));
|
|
461
511
|
this.logs = new EventLogStore(
|
|
462
512
|
(sessionId) => this.sessions.eventFilePath(sessionId),
|
|
463
513
|
(sessionId, error) =>
|
|
@@ -565,10 +615,25 @@ export class SessionEngine {
|
|
|
565
615
|
this.cloudServerToolsConnection = connection;
|
|
566
616
|
}
|
|
567
617
|
|
|
618
|
+
setMcpOauthCallbackUrl(url: string): void {
|
|
619
|
+
this.oauthHub.callbackUrl = url;
|
|
620
|
+
this.mcpHost.setOAuthRedirectUrl(url);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
setCursorAccountAuthProbe(
|
|
624
|
+
probe: (connection: string) => Promise<string | undefined>
|
|
625
|
+
): void {
|
|
626
|
+
this.probeCursorAccount = probe;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
get oauthCallbackHub(): McpOauthOnDemandHub {
|
|
630
|
+
return this.oauthHub;
|
|
631
|
+
}
|
|
632
|
+
|
|
568
633
|
/** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
|
|
569
634
|
get host(): HostContext {
|
|
570
635
|
return {
|
|
571
|
-
mcp: this.mcpHost,
|
|
636
|
+
mcp: this.wrapMcp(this.mcpHost),
|
|
572
637
|
github: this.platforms.github,
|
|
573
638
|
slack: this.platforms.slack,
|
|
574
639
|
reminders: this.reminderApi,
|
|
@@ -963,7 +1028,7 @@ export class SessionEngine {
|
|
|
963
1028
|
channelId,
|
|
964
1029
|
mode: sendOptions.mode ?? "chat",
|
|
965
1030
|
purpose,
|
|
966
|
-
|
|
1031
|
+
dryRun: sendOptions.dryRun === true ? true : undefined,
|
|
967
1032
|
continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
|
|
968
1033
|
auth,
|
|
969
1034
|
title: sendOptions.title,
|
|
@@ -981,7 +1046,10 @@ export class SessionEngine {
|
|
|
981
1046
|
this.storage?.sessionRecord(record);
|
|
982
1047
|
await this.appendEvent(record.sessionId, {
|
|
983
1048
|
type: "session.started",
|
|
984
|
-
data: {
|
|
1049
|
+
data: {
|
|
1050
|
+
channelId,
|
|
1051
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
1052
|
+
},
|
|
985
1053
|
});
|
|
986
1054
|
record = await this.enrollABs(record);
|
|
987
1055
|
const since = (await this.logs.get(record.sessionId)).count;
|
|
@@ -1231,7 +1299,14 @@ export class SessionEngine {
|
|
|
1231
1299
|
const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
|
|
1232
1300
|
const admitted = prior
|
|
1233
1301
|
.catch(() => {})
|
|
1234
|
-
.then(() =>
|
|
1302
|
+
.then(async () => {
|
|
1303
|
+
// Another server holds the turn: hand the message to it and coalesce.
|
|
1304
|
+
if (await this.turnLeases.runningElsewhere(sessionId)) {
|
|
1305
|
+
await this.outbox.post(sessionId, { message, options });
|
|
1306
|
+
return true;
|
|
1307
|
+
}
|
|
1308
|
+
return this.admitTurn(sessionId, message, options);
|
|
1309
|
+
});
|
|
1235
1310
|
this.admissionChains.set(
|
|
1236
1311
|
sessionId,
|
|
1237
1312
|
admitted.catch(() => {})
|
|
@@ -1239,6 +1314,20 @@ export class SessionEngine {
|
|
|
1239
1314
|
return admitted;
|
|
1240
1315
|
}
|
|
1241
1316
|
|
|
1317
|
+
/** Admit what other servers handed to this turn's holder. Never awaited by the lifecycle. */
|
|
1318
|
+
private drainOutbox(sessionId: string): void {
|
|
1319
|
+
void this.outbox
|
|
1320
|
+
.takeAll(sessionId)
|
|
1321
|
+
.then((items) => {
|
|
1322
|
+
for (const item of items) {
|
|
1323
|
+
void this.startTurn(sessionId, item.message, item.options).catch(
|
|
1324
|
+
() => {}
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
})
|
|
1328
|
+
.catch(() => {});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1242
1331
|
private enqueueCoalesced(sessionId: string, item: CoalescedQueuedTurn): void {
|
|
1243
1332
|
const queue = this.coalesceQueues.get(sessionId) ?? [];
|
|
1244
1333
|
if (
|
|
@@ -1476,6 +1565,8 @@ export class SessionEngine {
|
|
|
1476
1565
|
let lifecycle!: Promise<TurnOutcome>;
|
|
1477
1566
|
lifecycle = (async (): Promise<TurnOutcome> => {
|
|
1478
1567
|
let outcome: TurnOutcome;
|
|
1568
|
+
const poll = setInterval(() => this.drainOutbox(sessionId), 1_000);
|
|
1569
|
+
poll.unref();
|
|
1479
1570
|
try {
|
|
1480
1571
|
outcome = await this.executeTurn(
|
|
1481
1572
|
record,
|
|
@@ -1489,6 +1580,8 @@ export class SessionEngine {
|
|
|
1489
1580
|
status: "error",
|
|
1490
1581
|
errorMessage: describeError(error),
|
|
1491
1582
|
};
|
|
1583
|
+
} finally {
|
|
1584
|
+
clearInterval(poll);
|
|
1492
1585
|
}
|
|
1493
1586
|
this.lastOutcomes.set(sessionId, outcome);
|
|
1494
1587
|
const wasCurrent = this.activeTurns.get(sessionId) === lifecycle;
|
|
@@ -1518,6 +1611,10 @@ export class SessionEngine {
|
|
|
1518
1611
|
// The drain may have nested a successor turn, which took its own
|
|
1519
1612
|
// reference; the lease outlives this release until that one ends too.
|
|
1520
1613
|
await lease.release();
|
|
1614
|
+
// Handed off between the last poll and the release: nobody else will.
|
|
1615
|
+
if (!abort.signal.aborted) {
|
|
1616
|
+
this.drainOutbox(sessionId);
|
|
1617
|
+
}
|
|
1521
1618
|
return outcome;
|
|
1522
1619
|
})();
|
|
1523
1620
|
this.activeTurns.set(sessionId, lifecycle);
|
|
@@ -1562,6 +1659,7 @@ export class SessionEngine {
|
|
|
1562
1659
|
this.coalesceDrains.has(sessionId) ||
|
|
1563
1660
|
(this.coalesceQueues.get(sessionId)?.length ?? 0) > 0;
|
|
1564
1661
|
this.clearCoalesced(sessionId);
|
|
1662
|
+
await this.outbox.clear(sessionId);
|
|
1565
1663
|
const record = await this.sessions.get(sessionId);
|
|
1566
1664
|
const continuationKey = record?.continuationKey;
|
|
1567
1665
|
if (continuationKey !== undefined && continuationKey !== null) {
|
|
@@ -1795,6 +1893,7 @@ export class SessionEngine {
|
|
|
1795
1893
|
seed: isFirstTurn,
|
|
1796
1894
|
stateRoot: this.stateRoot,
|
|
1797
1895
|
});
|
|
1896
|
+
this.materializedWorkspaces.add(sessionId);
|
|
1798
1897
|
// Must precede the carrier: the harness resolves rules, skills,
|
|
1799
1898
|
// `mcp.json` and hooks from its git root, so a workspace nested in an
|
|
1800
1899
|
// outer repo would load that repo's hooks instead of the ones written
|
|
@@ -1879,11 +1978,14 @@ export class SessionEngine {
|
|
|
1879
1978
|
}
|
|
1880
1979
|
} catch (error) {
|
|
1881
1980
|
this.approvals.cancelSession(sessionId, "deny");
|
|
1981
|
+
this.auths.cancelSession(sessionId, "deny");
|
|
1882
1982
|
outcome = { status: "error", errorMessage: describeError(error) };
|
|
1883
1983
|
} finally {
|
|
1884
1984
|
permit.release();
|
|
1885
1985
|
this.approvals.cancelSession(sessionId, "deny");
|
|
1986
|
+
this.auths.cancelSession(sessionId, "deny");
|
|
1886
1987
|
await this.clearPendingApprovals(sessionId);
|
|
1988
|
+
await this.clearPendingAuths(sessionId);
|
|
1887
1989
|
// Anything still queued belongs to the next turn's boundary drain, not
|
|
1888
1990
|
// to this turn's edge detection.
|
|
1889
1991
|
this.deliveredNudges.delete(sessionId);
|
|
@@ -2518,11 +2620,21 @@ export class SessionEngine {
|
|
|
2518
2620
|
// named passthroughs resolved fresh on every turn — reminder fires and
|
|
2519
2621
|
// post-restart follow-ups included — and a listing failure throws,
|
|
2520
2622
|
// failing the turn closed.
|
|
2521
|
-
for (const tool of await
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2623
|
+
for (const tool of await mcpAuthAls.run(
|
|
2624
|
+
{
|
|
2625
|
+
sessionId: record.sessionId,
|
|
2626
|
+
turnId,
|
|
2627
|
+
callId: newToolCallId("mcp"),
|
|
2628
|
+
toolName: "mcp",
|
|
2629
|
+
emit,
|
|
2630
|
+
},
|
|
2631
|
+
() =>
|
|
2632
|
+
resolveAdvertisedTools({
|
|
2633
|
+
agent,
|
|
2634
|
+
session: this.sessionInfo(record),
|
|
2635
|
+
hostMcp: this.advertiseMcp(),
|
|
2636
|
+
})
|
|
2637
|
+
)) {
|
|
2526
2638
|
register(tool);
|
|
2527
2639
|
}
|
|
2528
2640
|
return tools;
|
|
@@ -2672,7 +2784,16 @@ export class SessionEngine {
|
|
|
2672
2784
|
}
|
|
2673
2785
|
const started = Date.now();
|
|
2674
2786
|
this.logger(`[session] tool ${args.tool.name} start`);
|
|
2675
|
-
const result = await
|
|
2787
|
+
const result = await mcpAuthAls.run(
|
|
2788
|
+
{
|
|
2789
|
+
sessionId: args.record.sessionId,
|
|
2790
|
+
turnId: args.turnId,
|
|
2791
|
+
callId: toolCallId,
|
|
2792
|
+
toolName: args.tool.name,
|
|
2793
|
+
emit: args.emit,
|
|
2794
|
+
},
|
|
2795
|
+
() => execute(validation.value as never, toolCtx)
|
|
2796
|
+
);
|
|
2676
2797
|
this.logger(
|
|
2677
2798
|
`[session] tool ${args.tool.name} done (${Date.now() - started}ms)`
|
|
2678
2799
|
);
|
|
@@ -2708,10 +2829,10 @@ export class SessionEngine {
|
|
|
2708
2829
|
args: args.args,
|
|
2709
2830
|
requestedAt,
|
|
2710
2831
|
};
|
|
2711
|
-
const resolutionPromise = this.approvals.wait(
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2832
|
+
const resolutionPromise = this.approvals.wait(
|
|
2833
|
+
args.sessionId,
|
|
2834
|
+
pendingRecord
|
|
2835
|
+
);
|
|
2715
2836
|
await this.updateRecord(args.sessionId, (current) => ({
|
|
2716
2837
|
...current,
|
|
2717
2838
|
pendingApprovals: [
|
|
@@ -2831,6 +2952,318 @@ export class SessionEngine {
|
|
|
2831
2952
|
}));
|
|
2832
2953
|
}
|
|
2833
2954
|
|
|
2955
|
+
private wrapMcp(inner: McpHost): HostMcpRegistry {
|
|
2956
|
+
return {
|
|
2957
|
+
names: () => inner.names(),
|
|
2958
|
+
listTools: (name) => this.withMcpAuth(name, () => inner.listTools(name)),
|
|
2959
|
+
callTool: (name, tool, args) =>
|
|
2960
|
+
this.withMcpAuth(name, () => inner.callTool(name, tool, args)),
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
private advertiseMcp(): AdvertiseHostMcp {
|
|
2965
|
+
return {
|
|
2966
|
+
listTools: (name) =>
|
|
2967
|
+
this.withMcpListAuth(name, () => this.mcpHost.listTools(name)),
|
|
2968
|
+
callTool: (name, tool, args) =>
|
|
2969
|
+
this.withMcpAuth(name, () => this.mcpHost.callTool(name, tool, args)),
|
|
2970
|
+
oneOff: (connection) => {
|
|
2971
|
+
const inner = this.mcpHost.oneOff(connection);
|
|
2972
|
+
return {
|
|
2973
|
+
listTools: () =>
|
|
2974
|
+
this.withMcpListAuth(connection.name, () => inner.listTools()),
|
|
2975
|
+
callTool: (tool, args) =>
|
|
2976
|
+
this.withMcpAuth(connection.name, () => inner.callTool(tool, args)),
|
|
2977
|
+
};
|
|
2978
|
+
},
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
private withMcpListAuth<T>(
|
|
2983
|
+
connection: string,
|
|
2984
|
+
run: () => Promise<T>
|
|
2985
|
+
): Promise<T> {
|
|
2986
|
+
const ctx = mcpAuthAls.getStore();
|
|
2987
|
+
if (ctx === undefined) {
|
|
2988
|
+
return this.withMcpAuth(connection, run);
|
|
2989
|
+
}
|
|
2990
|
+
const toolName = `mcp:${connection}`;
|
|
2991
|
+
return mcpAuthAls.run(
|
|
2992
|
+
{
|
|
2993
|
+
...ctx,
|
|
2994
|
+
callId: newToolCallId(toolName),
|
|
2995
|
+
toolName,
|
|
2996
|
+
},
|
|
2997
|
+
() => this.withMcpAuth(connection, run)
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
private async withMcpAuth<T>(
|
|
3002
|
+
connection: string,
|
|
3003
|
+
run: () => Promise<T>
|
|
3004
|
+
): Promise<T> {
|
|
3005
|
+
try {
|
|
3006
|
+
return await run();
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
if (!isMcpNeedsAuthError(error)) {
|
|
3009
|
+
throw error;
|
|
3010
|
+
}
|
|
3011
|
+
return await this.parkAndRetryMcp(connection, run, error);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
private async parkAndRetryMcp<T>(
|
|
3016
|
+
connection: string,
|
|
3017
|
+
run: () => Promise<T>,
|
|
3018
|
+
error: unknown
|
|
3019
|
+
): Promise<T> {
|
|
3020
|
+
const ctx = mcpAuthAls.getStore();
|
|
3021
|
+
if (ctx === undefined) {
|
|
3022
|
+
throw error;
|
|
3023
|
+
}
|
|
3024
|
+
const minted = await this.mintAuthUrl(connection, ctx.sessionId);
|
|
3025
|
+
if (minted === undefined) {
|
|
3026
|
+
throw error;
|
|
3027
|
+
}
|
|
3028
|
+
if ("authorized" in minted) {
|
|
3029
|
+
await this.persistHostedMcpOauth(connection);
|
|
3030
|
+
await this.mcpHost.dropClient(connection);
|
|
3031
|
+
return await this.withMcpAuth(connection, run);
|
|
3032
|
+
}
|
|
3033
|
+
if (minted.state !== "") {
|
|
3034
|
+
const bind = this.oauthHub.bind(minted.state, ctx.sessionId, ctx.callId);
|
|
3035
|
+
if (bind === "already_done") {
|
|
3036
|
+
await this.persistHostedMcpOauth(connection);
|
|
3037
|
+
await this.mcpHost.dropClient(connection);
|
|
3038
|
+
return await this.withMcpAuth(connection, run);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
const decision = await this.parkForAuth({
|
|
3042
|
+
sessionId: ctx.sessionId,
|
|
3043
|
+
turnId: ctx.turnId,
|
|
3044
|
+
callId: ctx.callId,
|
|
3045
|
+
toolName: ctx.toolName,
|
|
3046
|
+
connection,
|
|
3047
|
+
authUrl: minted.authUrl,
|
|
3048
|
+
emit: ctx.emit,
|
|
3049
|
+
});
|
|
3050
|
+
if (decision === "deny") {
|
|
3051
|
+
throw new Error(
|
|
3052
|
+
`MCP connection "${connection}" authorization was denied.`
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
await this.persistHostedMcpOauth(connection);
|
|
3056
|
+
await this.mcpHost.dropClient(connection);
|
|
3057
|
+
return await this.withMcpAuth(connection, run);
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
private async persistHostedMcpOauth(connection: string): Promise<void> {
|
|
3061
|
+
const authored = this.authoredConnection(connection);
|
|
3062
|
+
if (
|
|
3063
|
+
authored === undefined ||
|
|
3064
|
+
!isHostOauthConnectionTransport(authored.transport)
|
|
3065
|
+
) {
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
await this.oauthHub.persistFor(connection, authored.transport.url);
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
private authoredConnection(name: string) {
|
|
3072
|
+
return (
|
|
3073
|
+
this.project.agent.connections.find((entry) => entry.name === name) ??
|
|
3074
|
+
this.project.agent.hostConnections?.find((entry) => entry.name === name)
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
private async mintAuthUrl(
|
|
3079
|
+
connection: string,
|
|
3080
|
+
sessionId: string
|
|
3081
|
+
): Promise<
|
|
3082
|
+
{ authorized: true } | { authUrl: string; state: string } | undefined
|
|
3083
|
+
> {
|
|
3084
|
+
const authored = this.authoredConnection(connection);
|
|
3085
|
+
if (authored === undefined) {
|
|
3086
|
+
return undefined;
|
|
3087
|
+
}
|
|
3088
|
+
if (isCursorAccountTransport(authored.transport)) {
|
|
3089
|
+
const probe = this.probeCursorAccount;
|
|
3090
|
+
if (probe === undefined) {
|
|
3091
|
+
return undefined;
|
|
3092
|
+
}
|
|
3093
|
+
const authUrl = await probe(connection);
|
|
3094
|
+
if (authUrl === undefined || authUrl === "") {
|
|
3095
|
+
return undefined;
|
|
3096
|
+
}
|
|
3097
|
+
return { authUrl, state: "" };
|
|
3098
|
+
}
|
|
3099
|
+
if (!isHostOauthConnectionTransport(authored.transport)) {
|
|
3100
|
+
return undefined;
|
|
3101
|
+
}
|
|
3102
|
+
return await this.oauthHub.mint({
|
|
3103
|
+
connection,
|
|
3104
|
+
url: authored.transport.url,
|
|
3105
|
+
type: authored.transport.type === "sse" ? "sse" : "http",
|
|
3106
|
+
headers: authored.transport.headers,
|
|
3107
|
+
sessionId,
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
private async parkForAuth(args: {
|
|
3112
|
+
sessionId: string;
|
|
3113
|
+
turnId: string;
|
|
3114
|
+
callId: string;
|
|
3115
|
+
toolName: string;
|
|
3116
|
+
connection: string;
|
|
3117
|
+
authUrl: string;
|
|
3118
|
+
emit: (payload: SessionEventPayload) => void;
|
|
3119
|
+
}): Promise<ApprovalDecision> {
|
|
3120
|
+
const requestedAt = new Date().toISOString();
|
|
3121
|
+
const pendingRecord: PendingAuth = {
|
|
3122
|
+
callId: args.callId,
|
|
3123
|
+
turnId: args.turnId,
|
|
3124
|
+
toolName: args.toolName,
|
|
3125
|
+
connection: args.connection,
|
|
3126
|
+
authUrl: args.authUrl,
|
|
3127
|
+
requestedAt,
|
|
3128
|
+
};
|
|
3129
|
+
const resolutionPromise = this.auths.wait(args.sessionId, pendingRecord);
|
|
3130
|
+
await this.updateRecord(args.sessionId, (current) => ({
|
|
3131
|
+
...current,
|
|
3132
|
+
pendingAuths: [
|
|
3133
|
+
...(current.pendingAuths ?? []).filter(
|
|
3134
|
+
(item) => item.callId !== args.callId
|
|
3135
|
+
),
|
|
3136
|
+
pendingRecord,
|
|
3137
|
+
],
|
|
3138
|
+
}));
|
|
3139
|
+
args.emit({
|
|
3140
|
+
type: "action.auth_requested",
|
|
3141
|
+
data: {
|
|
3142
|
+
callId: args.callId,
|
|
3143
|
+
toolName: args.toolName,
|
|
3144
|
+
connection: args.connection,
|
|
3145
|
+
authUrl: args.authUrl,
|
|
3146
|
+
},
|
|
3147
|
+
});
|
|
3148
|
+
this.logger(
|
|
3149
|
+
`[session] tool ${args.toolName} awaiting MCP auth ${args.connection} (${args.callId})`
|
|
3150
|
+
);
|
|
3151
|
+
const { decision, by } = await resolutionPromise;
|
|
3152
|
+
await this.updateRecord(args.sessionId, (current) => ({
|
|
3153
|
+
...current,
|
|
3154
|
+
pendingAuths: (current.pendingAuths ?? []).filter(
|
|
3155
|
+
(item) => item.callId !== args.callId
|
|
3156
|
+
),
|
|
3157
|
+
}));
|
|
3158
|
+
args.emit({
|
|
3159
|
+
type: "action.auth_resolved",
|
|
3160
|
+
data: {
|
|
3161
|
+
callId: args.callId,
|
|
3162
|
+
toolName: args.toolName,
|
|
3163
|
+
connection: args.connection,
|
|
3164
|
+
decision,
|
|
3165
|
+
by,
|
|
3166
|
+
},
|
|
3167
|
+
});
|
|
3168
|
+
this.logger(
|
|
3169
|
+
`[session] tool ${args.toolName} MCP auth ${decision} (${args.callId})`
|
|
3170
|
+
);
|
|
3171
|
+
return decision;
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
private async clearPendingAuths(sessionId: string): Promise<void> {
|
|
3175
|
+
await this.oauthHub.abortSession(sessionId);
|
|
3176
|
+
const current = await this.sessions.get(sessionId);
|
|
3177
|
+
if (
|
|
3178
|
+
current === undefined ||
|
|
3179
|
+
current.pendingAuths === undefined ||
|
|
3180
|
+
current.pendingAuths.length === 0
|
|
3181
|
+
) {
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
await this.updateRecord(sessionId, (record) => ({
|
|
3185
|
+
...record,
|
|
3186
|
+
pendingAuths: [],
|
|
3187
|
+
}));
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
async listPendingAuths(sessionId: string): Promise<PendingAuth[]> {
|
|
3191
|
+
const record = await this.sessions.get(sessionId);
|
|
3192
|
+
if (record === undefined) {
|
|
3193
|
+
return [];
|
|
3194
|
+
}
|
|
3195
|
+
const live = this.auths.list(sessionId);
|
|
3196
|
+
if (live.length > 0) {
|
|
3197
|
+
return live;
|
|
3198
|
+
}
|
|
3199
|
+
return record.pendingAuths ?? [];
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
async resolveAuth(
|
|
3203
|
+
sessionId: string,
|
|
3204
|
+
callId: string,
|
|
3205
|
+
decision: ApprovalDecision,
|
|
3206
|
+
auth: AuthContext | null,
|
|
3207
|
+
options?: { asSessionOwner?: boolean }
|
|
3208
|
+
): Promise<PendingAuth> {
|
|
3209
|
+
const record = await this.sessions.get(sessionId);
|
|
3210
|
+
if (record === undefined) {
|
|
3211
|
+
throw new AuthNotFoundError(sessionId, callId);
|
|
3212
|
+
}
|
|
3213
|
+
const ownershipAuth = options?.asSessionOwner === true ? record.auth : auth;
|
|
3214
|
+
if (!samePrincipal(record.auth, ownershipAuth)) {
|
|
3215
|
+
throw new SessionAuthError(sessionId);
|
|
3216
|
+
}
|
|
3217
|
+
const pending =
|
|
3218
|
+
(record.pendingAuths ?? []).find((item) => item.callId === callId) ??
|
|
3219
|
+
this.auths.list(sessionId).find((item) => item.callId === callId);
|
|
3220
|
+
if (pending === undefined) {
|
|
3221
|
+
throw new AuthNotFoundError(sessionId, callId);
|
|
3222
|
+
}
|
|
3223
|
+
const resolvedBy = approvalResolverLabel(auth);
|
|
3224
|
+
if (decision === "deny") {
|
|
3225
|
+
await this.oauthHub.abortCall(sessionId, callId);
|
|
3226
|
+
}
|
|
3227
|
+
if (!this.auths.resolve(sessionId, callId, decision, resolvedBy)) {
|
|
3228
|
+
await this.removePendingAuth(sessionId, callId);
|
|
3229
|
+
throw new AuthInterruptedError(sessionId, callId);
|
|
3230
|
+
}
|
|
3231
|
+
return pending;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
private async removePendingAuth(
|
|
3235
|
+
sessionId: string,
|
|
3236
|
+
callId: string
|
|
3237
|
+
): Promise<void> {
|
|
3238
|
+
await this.updateRecord(sessionId, (current) => ({
|
|
3239
|
+
...current,
|
|
3240
|
+
pendingAuths: (current.pendingAuths ?? []).filter(
|
|
3241
|
+
(item) => item.callId !== callId
|
|
3242
|
+
),
|
|
3243
|
+
}));
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
async runWithMcpPark<T>(
|
|
3247
|
+
sessionId: string,
|
|
3248
|
+
toolName: string,
|
|
3249
|
+
fn: () => Promise<T>
|
|
3250
|
+
): Promise<T> {
|
|
3251
|
+
const active = this.activeToolContexts.get(sessionId);
|
|
3252
|
+
if (active === undefined) {
|
|
3253
|
+
return fn();
|
|
3254
|
+
}
|
|
3255
|
+
return mcpAuthAls.run(
|
|
3256
|
+
{
|
|
3257
|
+
sessionId,
|
|
3258
|
+
turnId: active.turnId,
|
|
3259
|
+
callId: newToolCallId(toolName),
|
|
3260
|
+
toolName,
|
|
3261
|
+
emit: active.emit,
|
|
3262
|
+
},
|
|
3263
|
+
fn
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
2834
3267
|
// ==========================================================================
|
|
2835
3268
|
// Direct tool calls (deterministic invocation, no model turn)
|
|
2836
3269
|
// ==========================================================================
|
|
@@ -2846,8 +3279,10 @@ export class SessionEngine {
|
|
|
2846
3279
|
* and materialized workspace) and the call is recorded on the session's
|
|
2847
3280
|
* event stream as `actions.requested` / `action.result` under a
|
|
2848
3281
|
* per-call `turnId` (the call id), so it shows up in trajectories.
|
|
2849
|
-
*
|
|
2850
|
-
*
|
|
3282
|
+
* While a model turn is running, a read-effect call runs alongside it
|
|
3283
|
+
* (the turn's workspace is read as-is) and a write-effect call is
|
|
3284
|
+
* rejected with {@link SessionBusyError}. Without a session, the tool
|
|
3285
|
+
* runs against an **ephemeral** scratch
|
|
2851
3286
|
* workspace under `<stateRoot>/tool-calls/<callId>` (materialized with
|
|
2852
3287
|
* seed files, removed after the call returns).
|
|
2853
3288
|
*
|
|
@@ -2873,6 +3308,12 @@ export class SessionEngine {
|
|
|
2873
3308
|
session: SessionInfo;
|
|
2874
3309
|
workspaceDir: string;
|
|
2875
3310
|
scratchDir: string | undefined;
|
|
3311
|
+
/**
|
|
3312
|
+
* A model turn is running or queued on the session. Reported, not
|
|
3313
|
+
* enforced: what it means depends on the call — a write waits, a read
|
|
3314
|
+
* (including the catalog) runs alongside the turn.
|
|
3315
|
+
*/
|
|
3316
|
+
busy: boolean;
|
|
2876
3317
|
}> {
|
|
2877
3318
|
if (
|
|
2878
3319
|
options.sessionId !== undefined &&
|
|
@@ -2898,9 +3339,9 @@ export class SessionEngine {
|
|
|
2898
3339
|
);
|
|
2899
3340
|
}
|
|
2900
3341
|
// A token that maps to a live (or restorable) session takes the
|
|
2901
|
-
// session path: same owner check, same
|
|
2902
|
-
//
|
|
2903
|
-
//
|
|
3342
|
+
// session path: same owner check, same busy semantics, same event
|
|
3343
|
+
// recording as an explicit sessionId. Skipping those just because
|
|
3344
|
+
// the caller addressed the session by token would fail open.
|
|
2904
3345
|
record =
|
|
2905
3346
|
(await this.sessions.findByContinuation(
|
|
2906
3347
|
continuation.channelId,
|
|
@@ -2915,19 +3356,14 @@ export class SessionEngine {
|
|
|
2915
3356
|
}
|
|
2916
3357
|
}
|
|
2917
3358
|
if (record !== undefined) {
|
|
2918
|
-
// Direct calls respect the same serialization as follow-up sends: a
|
|
2919
|
-
// running model turn owns the session workspace and event stream.
|
|
2920
|
-
if (
|
|
2921
|
-
this.activeTurns.has(record.sessionId) ||
|
|
2922
|
-
this.pendingTurns.has(record.sessionId)
|
|
2923
|
-
) {
|
|
2924
|
-
throw new SessionBusyError(record.sessionId);
|
|
2925
|
-
}
|
|
2926
3359
|
return {
|
|
2927
3360
|
record,
|
|
2928
3361
|
session: this.sessionInfo(record),
|
|
2929
3362
|
workspaceDir: this.harnessCwd(record),
|
|
2930
3363
|
scratchDir: undefined,
|
|
3364
|
+
busy:
|
|
3365
|
+
this.activeTurns.has(record.sessionId) ||
|
|
3366
|
+
this.pendingTurns.has(record.sessionId),
|
|
2931
3367
|
};
|
|
2932
3368
|
}
|
|
2933
3369
|
// No session behind the token (or no token at all): a synthetic
|
|
@@ -2947,6 +3383,7 @@ export class SessionEngine {
|
|
|
2947
3383
|
},
|
|
2948
3384
|
workspaceDir: scratchDir,
|
|
2949
3385
|
scratchDir,
|
|
3386
|
+
busy: false,
|
|
2950
3387
|
};
|
|
2951
3388
|
}
|
|
2952
3389
|
|
|
@@ -2989,6 +3426,43 @@ export class SessionEngine {
|
|
|
2989
3426
|
};
|
|
2990
3427
|
}
|
|
2991
3428
|
|
|
3429
|
+
/**
|
|
3430
|
+
* One catalog tool by name. Authored tools are local — their schema does
|
|
3431
|
+
* not depend on session identity or connection listings — so a detail
|
|
3432
|
+
* fetch for `ls` must not wait on every advertised MCP server.
|
|
3433
|
+
*/
|
|
3434
|
+
async describeCatalogTool(
|
|
3435
|
+
toolName: string,
|
|
3436
|
+
options: Pick<
|
|
3437
|
+
CallToolOptions,
|
|
3438
|
+
"auth" | "sessionId" | "continuationToken"
|
|
3439
|
+
> = {}
|
|
3440
|
+
): Promise<
|
|
3441
|
+
| { tool: DiscoveredTool }
|
|
3442
|
+
| {
|
|
3443
|
+
tool: undefined;
|
|
3444
|
+
availableTools: string[];
|
|
3445
|
+
connectionErrors: Array<{ connection: string; message: string }>;
|
|
3446
|
+
}
|
|
3447
|
+
> {
|
|
3448
|
+
const authored = this.project.agent.tools.find(
|
|
3449
|
+
(tool) => tool.name === toolName
|
|
3450
|
+
);
|
|
3451
|
+
if (authored !== undefined) {
|
|
3452
|
+
return { tool: authored };
|
|
3453
|
+
}
|
|
3454
|
+
const catalog = await this.listCatalogTools(options);
|
|
3455
|
+
const tool = catalog.tools.find((entry) => entry.name === toolName);
|
|
3456
|
+
if (tool === undefined) {
|
|
3457
|
+
return {
|
|
3458
|
+
tool: undefined,
|
|
3459
|
+
availableTools: catalog.tools.map((entry) => entry.name),
|
|
3460
|
+
connectionErrors: catalog.connectionErrors,
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
return { tool };
|
|
3464
|
+
}
|
|
3465
|
+
|
|
2992
3466
|
async callTool(
|
|
2993
3467
|
toolName: string,
|
|
2994
3468
|
input: unknown = {},
|
|
@@ -3015,7 +3489,7 @@ export class SessionEngine {
|
|
|
3015
3489
|
const advertised = await resolveAdvertisedTools({
|
|
3016
3490
|
agent: this.project.agent,
|
|
3017
3491
|
session,
|
|
3018
|
-
hostMcp: this.
|
|
3492
|
+
hostMcp: this.advertiseMcp(),
|
|
3019
3493
|
});
|
|
3020
3494
|
tool = advertised.find((t) => t.name === toolName);
|
|
3021
3495
|
}
|
|
@@ -3035,6 +3509,19 @@ export class SessionEngine {
|
|
|
3035
3509
|
if (!validation.ok) {
|
|
3036
3510
|
throw new ToolInputError({ toolName, detail: validation.message });
|
|
3037
3511
|
}
|
|
3512
|
+
// A running model turn owns the session workspace: a direct write would
|
|
3513
|
+
// race the harness's own edits, so it waits (409). A read runs alongside
|
|
3514
|
+
// the turn — the operator inspecting a live session is the point — and
|
|
3515
|
+
// its events stay separable by their per-call turnId. The effect can
|
|
3516
|
+
// depend on the input, hence deciding after validation; an MCP tool
|
|
3517
|
+
// nothing classifies is a write here, fail-closed.
|
|
3518
|
+
if (
|
|
3519
|
+
identity.busy &&
|
|
3520
|
+
record !== undefined &&
|
|
3521
|
+
directCallEffect(tool, validation.value) !== "read"
|
|
3522
|
+
) {
|
|
3523
|
+
throw new SessionBusyError(record.sessionId);
|
|
3524
|
+
}
|
|
3038
3525
|
let workspaceUnavailable = false;
|
|
3039
3526
|
try {
|
|
3040
3527
|
await mkdir(workspaceDir, { recursive: true });
|
|
@@ -3053,35 +3540,59 @@ export class SessionEngine {
|
|
|
3053
3540
|
// to write) authored tool may rely on ctx.workspaceDir being the
|
|
3054
3541
|
// real session workspace, so silently relocating it is not safe —
|
|
3055
3542
|
// fail typed instead.
|
|
3543
|
+
//
|
|
3544
|
+
// A shared `local.workspaceDir` (change-monitor's phantom /repo) is
|
|
3545
|
+
// that path by design. Relocating it to scratch would flag a false
|
|
3546
|
+
// session-state warning; keep the borrowed cwd and skip the flag.
|
|
3056
3547
|
const relocatable =
|
|
3057
3548
|
tool.advertised === true ||
|
|
3058
|
-
|
|
3549
|
+
directCallEffect(tool, validation.value) === "read";
|
|
3059
3550
|
if (!relocatable) {
|
|
3060
3551
|
throw new WorkspaceUnavailableError(toolName, error);
|
|
3061
3552
|
}
|
|
3062
|
-
this.
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3553
|
+
if (this.project.agent.local?.workspaceDir !== undefined) {
|
|
3554
|
+
this.logger(
|
|
3555
|
+
`[session] tool ${toolName} harness cwd unavailable (direct ${callId}); keeping borrowed workspace`
|
|
3556
|
+
);
|
|
3557
|
+
} else {
|
|
3558
|
+
this.logger(
|
|
3559
|
+
`[session] tool ${toolName} workspace unavailable (direct ${callId}); using scratch: ${
|
|
3560
|
+
error instanceof Error ? error.message : String(error)
|
|
3561
|
+
}`
|
|
3562
|
+
);
|
|
3563
|
+
scratchDir = join(this.stateRoot, "tool-calls", callId);
|
|
3564
|
+
workspaceDir = scratchDir;
|
|
3565
|
+
workspaceUnavailable = true;
|
|
3566
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
3567
|
+
}
|
|
3071
3568
|
}
|
|
3072
3569
|
// Match what a model turn would see: session workspaces get the same
|
|
3073
3570
|
// scaffold as executeTurn (seeds only before the first turn); scratch
|
|
3074
|
-
// workspaces are materialized fresh.
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3571
|
+
// workspaces are materialized fresh. A read running alongside a turn
|
|
3572
|
+
// that has materialized its workspace leaves it alone — re-seeding
|
|
3573
|
+
// would restore files the agent deleted mid-turn. A session is busy
|
|
3574
|
+
// from admit, though, so a read that lands while the first turn is
|
|
3575
|
+
// still queued or scaffolding materializes the workspace itself, seeds
|
|
3576
|
+
// included (turnCount is already 1 for that pending first turn).
|
|
3577
|
+
const turnMaterialized =
|
|
3578
|
+
record !== undefined &&
|
|
3579
|
+
identity.busy &&
|
|
3580
|
+
this.materializedWorkspaces.has(record.sessionId);
|
|
3581
|
+
if (record === undefined || workspaceUnavailable || !turnMaterialized) {
|
|
3582
|
+
await materializeWorkspace({
|
|
3583
|
+
agent: this.project.agent,
|
|
3584
|
+
workspaceDir:
|
|
3585
|
+
record === undefined || workspaceUnavailable
|
|
3586
|
+
? workspaceDir
|
|
3587
|
+
: this.scaffoldDir(record),
|
|
3588
|
+
seed:
|
|
3589
|
+
record === undefined ||
|
|
3590
|
+
workspaceUnavailable ||
|
|
3591
|
+
record.turnCount === 0 ||
|
|
3592
|
+
(identity.busy && record.turnCount === 1),
|
|
3593
|
+
stateRoot: this.stateRoot,
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3085
3596
|
|
|
3086
3597
|
// A per-call turnId keeps these events visible in trajectories (which
|
|
3087
3598
|
// drop turn-less action events) without attributing them to a model turn.
|
|
@@ -3139,8 +3650,8 @@ export class SessionEngine {
|
|
|
3139
3650
|
result = normalizeToolResult(decision.result);
|
|
3140
3651
|
isError = false;
|
|
3141
3652
|
} else {
|
|
3142
|
-
|
|
3143
|
-
|
|
3653
|
+
const run = () =>
|
|
3654
|
+
execute(validation.value as never, {
|
|
3144
3655
|
toolCallId: callId,
|
|
3145
3656
|
session,
|
|
3146
3657
|
workspaceDir,
|
|
@@ -3165,7 +3676,20 @@ export class SessionEngine {
|
|
|
3165
3676
|
getSession: (channelId, sessionId) =>
|
|
3166
3677
|
this.getChannelSession(channelId, sessionId),
|
|
3167
3678
|
emit,
|
|
3168
|
-
})
|
|
3679
|
+
});
|
|
3680
|
+
result = normalizeToolResult(
|
|
3681
|
+
record !== undefined && emit !== undefined
|
|
3682
|
+
? await mcpAuthAls.run(
|
|
3683
|
+
{
|
|
3684
|
+
sessionId: record.sessionId,
|
|
3685
|
+
turnId: callId,
|
|
3686
|
+
callId,
|
|
3687
|
+
toolName,
|
|
3688
|
+
emit,
|
|
3689
|
+
},
|
|
3690
|
+
run
|
|
3691
|
+
)
|
|
3692
|
+
: await run()
|
|
3169
3693
|
);
|
|
3170
3694
|
isError = isErrorEnvelope(result);
|
|
3171
3695
|
}
|
|
@@ -3585,6 +4109,75 @@ export class SessionEngine {
|
|
|
3585
4109
|
};
|
|
3586
4110
|
}
|
|
3587
4111
|
|
|
4112
|
+
/**
|
|
4113
|
+
* The single newest session visible to `caller` (`GET /v1/sessions?latest=1`).
|
|
4114
|
+
*
|
|
4115
|
+
* Short-circuits the full listing: the local store already holds every
|
|
4116
|
+
* session this host created plus the startup bulk restore, so the common
|
|
4117
|
+
* case answers from memory without a sink round trip. Only when the local
|
|
4118
|
+
* store has nothing for the caller (fresh host, `restore: "off"`) does it
|
|
4119
|
+
* ask the sink — for a bounded newest-written window when the backend
|
|
4120
|
+
* can order by write time, else the full list. A session written by
|
|
4121
|
+
* another replica moments ago can lose to a local one; callers that need
|
|
4122
|
+
* the exact newest row should use the full listing.
|
|
4123
|
+
*/
|
|
4124
|
+
async latestSessionWithSource(
|
|
4125
|
+
caller: AuthContext | null,
|
|
4126
|
+
options?: { includeAll?: boolean }
|
|
4127
|
+
): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
|
|
4128
|
+
const includeAll = options?.includeAll ?? false;
|
|
4129
|
+
const newestVisible = (
|
|
4130
|
+
records: readonly SessionRecord[]
|
|
4131
|
+
): SessionRecord | undefined => {
|
|
4132
|
+
let newest: SessionRecord | undefined;
|
|
4133
|
+
for (const record of records) {
|
|
4134
|
+
if (!(includeAll || samePrincipal(record.auth, caller))) {
|
|
4135
|
+
continue;
|
|
4136
|
+
}
|
|
4137
|
+
if (
|
|
4138
|
+
newest === undefined ||
|
|
4139
|
+
record.updatedAt.localeCompare(newest.updatedAt) > 0
|
|
4140
|
+
) {
|
|
4141
|
+
newest = record;
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
return newest;
|
|
4145
|
+
};
|
|
4146
|
+
const local = newestVisible(await this.sessions.list());
|
|
4147
|
+
if (local !== undefined) {
|
|
4148
|
+
return {
|
|
4149
|
+
sessions: this.summarizeSessionRecords([local], caller, includeAll),
|
|
4150
|
+
source: "local",
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
if (this.storage?.isCursorHosted === true) {
|
|
4154
|
+
try {
|
|
4155
|
+
const window = await this.storage.listNewestSessionsFromSink(
|
|
4156
|
+
LATEST_SESSION_SINK_WINDOW
|
|
4157
|
+
);
|
|
4158
|
+
if (window !== undefined) {
|
|
4159
|
+
const stored = newestVisible(window);
|
|
4160
|
+
if (stored !== undefined) {
|
|
4161
|
+
return {
|
|
4162
|
+
sessions: this.summarizeSessionRecords(
|
|
4163
|
+
[stored],
|
|
4164
|
+
caller,
|
|
4165
|
+
includeAll
|
|
4166
|
+
),
|
|
4167
|
+
source: "cursor-hosted",
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
} catch (error) {
|
|
4172
|
+
this.logger(
|
|
4173
|
+
`[agent-sdk] cursor-hosted newest-session lookup failed; falling back to the full list: ${describeError(error)}`
|
|
4174
|
+
);
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
const listed = await this.listSessionsWithSource(caller, options);
|
|
4178
|
+
return { sessions: listed.sessions.slice(0, 1), source: listed.source };
|
|
4179
|
+
}
|
|
4180
|
+
|
|
3588
4181
|
/**
|
|
3589
4182
|
* Durable sink rows plus local records (local wins on the same id when
|
|
3590
4183
|
* newer — covers debounce windows that have not flushed yet). Newest
|
|
@@ -3638,6 +4231,7 @@ export class SessionEngine {
|
|
|
3638
4231
|
: undefined,
|
|
3639
4232
|
createdAt: record.createdAt,
|
|
3640
4233
|
updatedAt: record.updatedAt,
|
|
4234
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
3641
4235
|
continuationToken:
|
|
3642
4236
|
record.httpContinuationKey !== undefined
|
|
3643
4237
|
? formatContinuationToken(
|
|
@@ -3967,7 +4561,7 @@ export class SessionEngine {
|
|
|
3967
4561
|
channelId: record.channelId,
|
|
3968
4562
|
mode: record.mode,
|
|
3969
4563
|
purpose: record.purpose === "eval" ? "eval" : "live",
|
|
3970
|
-
|
|
4564
|
+
dryRun: record.dryRun === true ? true : undefined,
|
|
3971
4565
|
auth: record.auth,
|
|
3972
4566
|
continuationKey: record.continuationKey,
|
|
3973
4567
|
...(record.abs === undefined ? {} : { abs: record.abs }),
|
|
@@ -4340,6 +4934,8 @@ export class SessionEngine {
|
|
|
4340
4934
|
async close(): Promise<void> {
|
|
4341
4935
|
this.stopMemoryTelemetry?.();
|
|
4342
4936
|
this.approvals.cancelAll("deny");
|
|
4937
|
+
this.auths.cancelAll("deny");
|
|
4938
|
+
await this.oauthHub.close();
|
|
4343
4939
|
await Promise.allSettled([...this.activeTurns.values()]);
|
|
4344
4940
|
await Promise.allSettled([...this.backgroundWork]);
|
|
4345
4941
|
await Promise.allSettled([...this.dispatchChains.values()]);
|
|
@@ -4357,6 +4953,13 @@ export class SessionEngine {
|
|
|
4357
4953
|
/** Parallel event-stream loads during the startup bulk restore. */
|
|
4358
4954
|
const RESTORE_FETCH_CONCURRENCY = 8;
|
|
4359
4955
|
|
|
4956
|
+
/**
|
|
4957
|
+
* Newest-written sink rows fetched for `GET /v1/sessions?latest=1` when the
|
|
4958
|
+
* local store has nothing for the caller. Wide enough that the caller's own
|
|
4959
|
+
* session is normally inside it even when other principals are busier.
|
|
4960
|
+
*/
|
|
4961
|
+
const LATEST_SESSION_SINK_WINDOW = 50;
|
|
4962
|
+
|
|
4360
4963
|
/** NDJSON payload for a restored event stream; undefined when empty. */
|
|
4361
4964
|
function mergePendingImages(
|
|
4362
4965
|
current: Array<{ data: string; mimeType: string }> | undefined,
|