@cursor/july 0.1.104 → 0.1.105

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.
Files changed (161) hide show
  1. package/dist/channels/deployments/deployments-channel.d.ts.map +1 -1
  2. package/dist/channels/deployments/deployments-channel.js +1 -0
  3. package/dist/channels/deployments/types.d.ts +2 -0
  4. package/dist/channels/deployments/types.d.ts.map +1 -1
  5. package/dist/docs/404.html +2 -2
  6. package/dist/docs/ab.html +4 -4
  7. package/dist/docs/assets/{app.DbMWUyGb.js → app.BVwaffdK.js} +1 -1
  8. package/dist/docs/assets/chunks/@localSearchIndexroot.CJxMklAy.js +1 -0
  9. package/dist/docs/assets/chunks/{VPLocalSearchBox.DcNeTh9o.js → VPLocalSearchBox.WuWTZ1x8.js} +1 -1
  10. package/dist/docs/assets/chunks/{theme.DMjhY3gl.js → theme.BYdI_6sF.js} +2 -2
  11. package/dist/docs/assets/guides_grokbot-agents.md.DVqdhBKb.js +9 -0
  12. package/dist/docs/assets/guides_grokbot-agents.md.DVqdhBKb.lean.js +1 -0
  13. package/dist/docs/assets/index.md.BW_6tOgR.js +5 -0
  14. package/dist/docs/assets/index.md.BW_6tOgR.lean.js +1 -0
  15. package/dist/docs/assets/{reference_cli.md.CJNc_xYK.js → reference_cli.md.C6Lo8SEd.js} +2 -1
  16. package/dist/docs/assets/{reference_extensions.md.DykHW3qL.js → reference_extensions.md.CGmMLblt.js} +1 -1
  17. package/dist/docs/assets/templates_grokbot-agents.md.c1s6Flp9.js +3 -0
  18. package/dist/docs/assets/templates_grokbot-agents.md.c1s6Flp9.lean.js +1 -0
  19. package/dist/docs/building-with-agents.html +4 -4
  20. package/dist/docs/concepts.html +4 -4
  21. package/dist/docs/deployment.html +4 -4
  22. package/dist/docs/evals.html +4 -4
  23. package/dist/docs/guides/agent-to-agent.html +4 -4
  24. package/dist/docs/guides/cloud-agents.html +5 -5
  25. package/dist/docs/guides/convert-automation.html +4 -4
  26. package/dist/docs/guides/github.html +4 -4
  27. package/dist/docs/guides/grokbot-agents.html +35 -0
  28. package/dist/docs/guides/grokbot-agents.md +88 -0
  29. package/dist/docs/guides/human-in-the-loop.html +4 -4
  30. package/dist/docs/guides/mcp-oauth.html +4 -4
  31. package/dist/docs/guides/opentelemetry.html +5 -5
  32. package/dist/docs/guides/slack.html +4 -4
  33. package/dist/docs/guides/webhooks.html +4 -4
  34. package/dist/docs/hashmap.json +1 -1
  35. package/dist/docs/hillclimbing.html +4 -4
  36. package/dist/docs/index.html +6 -6
  37. package/dist/docs/index.md +6 -1
  38. package/dist/docs/llms-full.txt +156 -2
  39. package/dist/docs/llms.txt +2 -0
  40. package/dist/docs/quickstart.html +4 -4
  41. package/dist/docs/reference/agent-config.html +4 -4
  42. package/dist/docs/reference/artifacts.html +4 -4
  43. package/dist/docs/reference/channels.html +4 -4
  44. package/dist/docs/reference/cli.html +7 -6
  45. package/dist/docs/reference/cli.md +2 -1
  46. package/dist/docs/reference/connections.html +4 -4
  47. package/dist/docs/reference/extensions.html +6 -6
  48. package/dist/docs/reference/extensions.md +2 -0
  49. package/dist/docs/reference/hooks.html +4 -4
  50. package/dist/docs/reference/http-api.html +4 -4
  51. package/dist/docs/reference/instructions.html +4 -4
  52. package/dist/docs/reference/playground.html +4 -4
  53. package/dist/docs/reference/project-layout.html +4 -4
  54. package/dist/docs/reference/prompt.html +4 -4
  55. package/dist/docs/reference/result.html +4 -4
  56. package/dist/docs/reference/schedules.html +4 -4
  57. package/dist/docs/reference/sessions.html +4 -4
  58. package/dist/docs/reference/skills.html +4 -4
  59. package/dist/docs/reference/subagents.html +4 -4
  60. package/dist/docs/reference/tools.html +4 -4
  61. package/dist/docs/scaffolding-agents.html +4 -4
  62. package/dist/docs/skills/ab.html +4 -4
  63. package/dist/docs/skills/create-agent.html +4 -4
  64. package/dist/docs/skills/debug.html +4 -4
  65. package/dist/docs/skills/evals.html +4 -4
  66. package/dist/docs/skills/framework-map.html +4 -4
  67. package/dist/docs/skills/github.html +4 -4
  68. package/dist/docs/skills/hillclimb.html +4 -4
  69. package/dist/docs/skills/index.html +4 -4
  70. package/dist/docs/skills/mcp-auth.html +4 -4
  71. package/dist/docs/skills/otel.html +4 -4
  72. package/dist/docs/skills/setup-slack.html +4 -4
  73. package/dist/docs/storage.html +4 -4
  74. package/dist/docs/templates/agentic-owners.html +4 -4
  75. package/dist/docs/templates/agents-md.html +4 -4
  76. package/dist/docs/templates/code-wiki.html +5 -5
  77. package/dist/docs/templates/demo.html +5 -5
  78. package/dist/docs/templates/grokbot-agents.html +29 -0
  79. package/dist/docs/templates/grokbot-agents.md +48 -0
  80. package/dist/docs/templates/pr-autofixer.html +4 -4
  81. package/dist/docs/templates/security-help.html +4 -4
  82. package/dist/docs/templates/security-reviewer.html +4 -4
  83. package/dist/docs/templates/triage.html +4 -4
  84. package/dist/docs/troubleshooting.html +4 -4
  85. package/dist/files.d.ts +7 -2
  86. package/dist/files.d.ts.map +1 -1
  87. package/dist/files.js +7 -2
  88. package/dist/internal/cursor-agent-template.d.ts +1 -1
  89. package/dist/internal/cursor-agent-template.d.ts.map +1 -1
  90. package/dist/internal/cursor-agent-template.js +1 -0
  91. package/dist/internal/framework-file-storage.d.ts +3 -1
  92. package/dist/internal/framework-file-storage.d.ts.map +1 -1
  93. package/dist/internal/framework-file-storage.js +3 -1
  94. package/dist/internal/hosted-delivery.d.ts +7 -0
  95. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  96. package/dist/internal/hosted-delivery.js +29 -1
  97. package/dist/internal/hosted-managed-automation.d.ts.map +1 -1
  98. package/dist/internal/hosted-managed-automation.js +27 -2
  99. package/dist/internal/init-project.d.ts.map +1 -1
  100. package/dist/internal/init-project.js +4 -1
  101. package/dist/internal/sdk-runner.d.ts +12 -0
  102. package/dist/internal/sdk-runner.d.ts.map +1 -1
  103. package/dist/internal/sdk-runner.js +18 -0
  104. package/dist/internal/server.d.ts.map +1 -1
  105. package/dist/internal/server.js +11 -3
  106. package/dist/internal/session-engine.d.ts +8 -0
  107. package/dist/internal/session-engine.d.ts.map +1 -1
  108. package/dist/internal/session-engine.js +132 -52
  109. package/dist/internal/store-mirror.d.ts +2 -2
  110. package/dist/internal/store-mirror.js +2 -2
  111. package/dist/internal/tenant-slack/relay.d.ts +2 -0
  112. package/dist/internal/tenant-slack/relay.d.ts.map +1 -1
  113. package/dist/internal/tenant-slack/relay.js +18 -8
  114. package/dist/internal/workspace-sync.d.ts +31 -6
  115. package/dist/internal/workspace-sync.d.ts.map +1 -1
  116. package/dist/internal/workspace-sync.js +67 -12
  117. package/dist/internal/workspace.d.ts +9 -0
  118. package/dist/internal/workspace.d.ts.map +1 -1
  119. package/dist/internal/workspace.js +16 -0
  120. package/dist/playground/assets/{index-8YiozU89.js → index-BaQzjaNK.js} +36 -36
  121. package/dist/playground/assets/index-CceqnLTX.css +1 -0
  122. package/dist/playground/index.html +2 -2
  123. package/dist/types.d.ts +9 -8
  124. package/dist/types.d.ts.map +1 -1
  125. package/docs/README.md +6 -1
  126. package/docs/guides/grokbot-agents.md +93 -0
  127. package/docs/reference/cli.md +2 -1
  128. package/docs/reference/extensions.md +2 -0
  129. package/docs/templates/grokbot-agents.md +53 -0
  130. package/package.json +10 -1
  131. package/src/channels/deployments/deployments-channel.ts +1 -0
  132. package/src/channels/deployments/types.ts +2 -0
  133. package/src/files.ts +9 -2
  134. package/src/internal/cursor-agent-template.ts +1 -0
  135. package/src/internal/framework-file-storage.ts +3 -1
  136. package/src/internal/hosted-delivery.ts +53 -1
  137. package/src/internal/hosted-managed-automation.ts +49 -5
  138. package/src/internal/init-project.ts +4 -1
  139. package/src/internal/sdk-runner.ts +19 -0
  140. package/src/internal/server.ts +16 -0
  141. package/src/internal/session-engine.ts +168 -56
  142. package/src/internal/store-mirror.ts +2 -2
  143. package/src/internal/tenant-slack/relay.ts +28 -4
  144. package/src/internal/workspace-sync.ts +84 -12
  145. package/src/internal/workspace.ts +20 -0
  146. package/src/types.ts +9 -8
  147. package/templates/grokbot-agents/README.md +26 -0
  148. package/templates/grokbot-agents/agent/agent.ts +6 -0
  149. package/templates/grokbot-agents/agent/extensions/grokbot.ts +5 -0
  150. package/templates/grokbot-agents/agent/instructions.md +7 -0
  151. package/templates/grokbot-agents/evals/evals.config.ts +5 -0
  152. package/templates/grokbot-agents/evals/grokbot-agents.eval.ts +18 -0
  153. package/templates/grokbot-agents/init.json +18 -0
  154. package/templates/grokbot-agents/package.json +17 -0
  155. package/templates/grokbot-agents/tsconfig.json +12 -0
  156. package/dist/docs/assets/chunks/@localSearchIndexroot.Bqsulb9K.js +0 -1
  157. package/dist/docs/assets/index.md.D75GWhPl.js +0 -5
  158. package/dist/docs/assets/index.md.D75GWhPl.lean.js +0 -1
  159. package/dist/playground/assets/index-MbMxXg-y.css +0 -1
  160. /package/dist/docs/assets/{reference_cli.md.CJNc_xYK.lean.js → reference_cli.md.C6Lo8SEd.lean.js} +0 -0
  161. /package/dist/docs/assets/{reference_extensions.md.DykHW3qL.lean.js → reference_extensions.md.CGmMLblt.lean.js} +0 -0
@@ -358,6 +358,13 @@ export function registerHostedDeliveryRoute(input: {
358
358
  flushStartLog?(startLogId: string): Promise<void>;
359
359
  /** Snapshot of the process log ring at persist time. */
360
360
  snapshotLogs?(): LogRingEntry[];
361
+ /**
362
+ * Drop the cached SDK agent handle before the HTTP reply. Hosted v2
363
+ * hibernates this process as soon as the reply lands; leaving the handle
364
+ * in memory freezes a dead Connect/HTTP2 session that the next wake
365
+ * then reuses.
366
+ */
367
+ evictSdkSession?(sessionId: string): void;
361
368
  /**
362
369
  * Wait until guest egress works before authored admission/execution.
363
370
  * Defaults to {@link waitForGuestNetworkReady}. Tests inject a stub.
@@ -463,8 +470,18 @@ export function registerHostedDeliveryRoute(input: {
463
470
  if (unconsumed !== undefined) {
464
471
  return finish(unconsumed);
465
472
  }
466
- const delivery = parseHostedAdmission(await readJsonObject(request));
473
+ const rawAdmission = await readJsonObject(request);
474
+ const delivery = parseHostedAdmission(rawAdmission);
467
475
  if (delivery === undefined) {
476
+ const rawKeys =
477
+ typeof rawAdmission === "object" &&
478
+ rawAdmission !== null &&
479
+ !Array.isArray(rawAdmission)
480
+ ? Object.keys(rawAdmission as Record<string, unknown>)
481
+ : [];
482
+ console.info(
483
+ `[agent-sdk] hosted admission rejected: envelope failed to parse keys=${JSON.stringify(rawKeys)} hasManagedAutomation=${String(rawKeys.includes("managedAutomation"))}`
484
+ );
468
485
  return finish(
469
486
  errorResponse(
470
487
  400,
@@ -473,6 +490,9 @@ export function registerHostedDeliveryRoute(input: {
473
490
  )
474
491
  );
475
492
  }
493
+ console.info(
494
+ `[agent-sdk] hosted admission received deliveryId=${delivery.deliveryId} channelId=${delivery.channelId} channelKind=${delivery.channelKind} eventType=${delivery.eventType} sessionMode=${delivery.sessionMode} managedAutomation=${delivery.managedAutomation === undefined ? "<absent>" : `automationUuid=${delivery.managedAutomation.automationUuid} mcpServerNames=${String(delivery.managedAutomation.mcpServerNames.length)} slackChannels=${delivery.managedAutomation.slackChannels === undefined ? "<absent>" : JSON.stringify(delivery.managedAutomation.slackChannels)}`}`
495
+ );
476
496
  recordHostedManagedAutomation(delivery);
477
497
  const channel = input.channels.find(
478
498
  (candidate) => candidate.id === delivery.channelId
@@ -624,6 +644,7 @@ export function registerHostedDeliveryRoute(input: {
624
644
  sessionId: "",
625
645
  turnStatus: HOSTED_EXECUTION_DIAG_RUNNING_STATUS,
626
646
  });
647
+ let sessionId = "";
627
648
  try {
628
649
  const args = input.buildHandlerArgs(
629
650
  admitted.send.channelId,
@@ -635,10 +656,19 @@ export function registerHostedDeliveryRoute(input: {
635
656
  key: admitted.send.mailboxContinuationKey,
636
657
  },
637
658
  });
659
+ sessionId = session.id;
638
660
  const outcome = await session.waitForCompletion();
639
661
  // Failed turns drain too: their record/continuation updates must
640
662
  // survive the park just like a finished turn's.
641
663
  await input.drainSessionDurability?.(session.id);
664
+ // Evict before the reply: TAS parks and hibernates this process
665
+ // as soon as the POST returns. A leftover SDK handle freezes a
666
+ // dead Connect/HTTP2 session that the next wake then reuses.
667
+ evictHostedSdkSessions(
668
+ input.evictSdkSession,
669
+ session.id,
670
+ execution.sessionPublicId
671
+ );
642
672
  await writeDiag({
643
673
  sessionId: session.id,
644
674
  turnStatus: outcome.status,
@@ -671,6 +701,11 @@ export function registerHostedDeliveryRoute(input: {
671
701
  },
672
702
  };
673
703
  } catch (error) {
704
+ evictHostedSdkSessions(
705
+ input.evictSdkSession,
706
+ sessionId,
707
+ execution.sessionPublicId
708
+ );
674
709
  await writeDiag({
675
710
  sessionId: "",
676
711
  turnStatus: "error",
@@ -687,6 +722,23 @@ export function registerHostedDeliveryRoute(input: {
687
722
  });
688
723
  }
689
724
 
725
+ function evictHostedSdkSessions(
726
+ evict: ((sessionId: string) => void) | undefined,
727
+ ...ids: Array<string | undefined>
728
+ ): void {
729
+ if (evict === undefined) {
730
+ return;
731
+ }
732
+ const seen = new Set<string>();
733
+ for (const id of ids) {
734
+ if (id === undefined || id === "" || seen.has(id)) {
735
+ continue;
736
+ }
737
+ seen.add(id);
738
+ evict(id);
739
+ }
740
+ }
741
+
690
742
  function hostedErrorForUnfinishedTurn(
691
743
  errorMessage: string | undefined
692
744
  ): typeof GUEST_CONNECT_STALLED | "turn_failed" {
@@ -17,10 +17,36 @@
17
17
 
18
18
  import type { HostedAdmissionManagedAutomation } from "./hosted-delivery-protocol.js";
19
19
 
20
- const managedAutomationByDelivery = new Map<
21
- string,
22
- { managedAutomation: HostedAdmissionManagedAutomation; expiresAt: number }
23
- >();
20
+ type ManagedAutomationEntry = {
21
+ managedAutomation: HostedAdmissionManagedAutomation;
22
+ expiresAt: number;
23
+ };
24
+
25
+ /**
26
+ * Process-wide slot for the table. Hosted engines run the compiled CLI
27
+ * (`dist/`), which records at admission, while first-party factory agents
28
+ * relative-import this file from `src/` and read at finalize — two module
29
+ * instances in one process. A module-local Map would be recorded in one
30
+ * copy and read from the other (always undefined, so a fork never sees its
31
+ * Slack channels or MCP servers); the shared symbol keeps one table.
32
+ */
33
+ const MANAGED_AUTOMATION_TABLE_KEY = Symbol.for(
34
+ "cursor.agentkit.hostedManagedAutomationByDelivery"
35
+ );
36
+
37
+ function managedAutomationTable(): Map<string, ManagedAutomationEntry> {
38
+ const slots = globalThis as unknown as Record<
39
+ symbol,
40
+ Map<string, ManagedAutomationEntry> | undefined
41
+ >;
42
+ let table = slots[MANAGED_AUTOMATION_TABLE_KEY];
43
+ if (table === undefined) {
44
+ table = new Map();
45
+ slots[MANAGED_AUTOMATION_TABLE_KEY] = table;
46
+ }
47
+ return table;
48
+ }
49
+
24
50
  /** Long enough to outlive a slow review run kicked off by the delivery. */
25
51
  const MANAGED_AUTOMATION_TTL_MS = 60 * 60_000;
26
52
  const MANAGED_AUTOMATION_MAX_ENTRIES = 10_000;
@@ -30,8 +56,12 @@ export function recordHostedManagedAutomation(delivery: {
30
56
  managedAutomation?: HostedAdmissionManagedAutomation;
31
57
  }): void {
32
58
  if (delivery.managedAutomation === undefined) {
59
+ console.info(
60
+ `[agent-sdk] hosted managedAutomation not recorded deliveryId=${delivery.deliveryId}: admission envelope carried no managedAutomation slice (non-managed delivery, or the control plane resolved no MCP servers and no Slack channels)`
61
+ );
33
62
  return;
34
63
  }
64
+ const managedAutomationByDelivery = managedAutomationTable();
35
65
  const now = Date.now();
36
66
  for (const [key, entry] of managedAutomationByDelivery) {
37
67
  if (entry.expiresAt <= now) {
@@ -39,12 +69,19 @@ export function recordHostedManagedAutomation(delivery: {
39
69
  }
40
70
  }
41
71
  if (managedAutomationByDelivery.size >= MANAGED_AUTOMATION_MAX_ENTRIES) {
72
+ console.info(
73
+ `[agent-sdk] hosted managedAutomation table full (${String(managedAutomationByDelivery.size)} entries); clearing before recording deliveryId=${delivery.deliveryId}`
74
+ );
42
75
  managedAutomationByDelivery.clear();
43
76
  }
44
77
  managedAutomationByDelivery.set(delivery.deliveryId, {
45
78
  managedAutomation: delivery.managedAutomation,
46
79
  expiresAt: now + MANAGED_AUTOMATION_TTL_MS,
47
80
  });
81
+ const slackChannels = delivery.managedAutomation.slackChannels;
82
+ console.info(
83
+ `[agent-sdk] hosted managedAutomation recorded deliveryId=${delivery.deliveryId} automationUuid=${delivery.managedAutomation.automationUuid} mcpServerNames=${String(delivery.managedAutomation.mcpServerNames.length)} slackChannels=${slackChannels === undefined ? "<absent>" : JSON.stringify(slackChannels)} ttlMs=${String(MANAGED_AUTOMATION_TTL_MS)} tableSize=${String(managedAutomationByDelivery.size)}`
84
+ );
48
85
  }
49
86
 
50
87
  /**
@@ -55,11 +92,18 @@ export function recordHostedManagedAutomation(delivery: {
55
92
  export function getHostedManagedAutomation(
56
93
  deliveryId: string
57
94
  ): HostedAdmissionManagedAutomation | undefined {
95
+ const managedAutomationByDelivery = managedAutomationTable();
58
96
  const entry = managedAutomationByDelivery.get(deliveryId);
59
97
  if (entry === undefined) {
98
+ console.info(
99
+ `[agent-sdk] hosted managedAutomation lookup miss deliveryId=${deliveryId}: no entry (never recorded in this process, or evicted) tableSize=${String(managedAutomationByDelivery.size)} knownDeliveryIds=${JSON.stringify([...managedAutomationByDelivery.keys()].slice(0, 10))}`
100
+ );
60
101
  return undefined;
61
102
  }
62
103
  if (entry.expiresAt <= Date.now()) {
104
+ console.info(
105
+ `[agent-sdk] hosted managedAutomation lookup expired deliveryId=${deliveryId} automationUuid=${entry.managedAutomation.automationUuid} expiredMsAgo=${String(Date.now() - entry.expiresAt)}`
106
+ );
63
107
  managedAutomationByDelivery.delete(deliveryId);
64
108
  return undefined;
65
109
  }
@@ -68,5 +112,5 @@ export function getHostedManagedAutomation(
68
112
 
69
113
  /** Drop recorded managed-automation slices (tests only). */
70
114
  export function resetHostedManagedAutomationForTest(): void {
71
- managedAutomationByDelivery.clear();
115
+ managedAutomationTable().clear();
72
116
  }
@@ -77,6 +77,7 @@ const TEMPLATES: Readonly<Record<TemplateName, TemplateEntry>> = {
77
77
  "agents-md": { dir: "agents-md" },
78
78
  "code-wiki": { dir: "code-wiki" },
79
79
  demo: { dir: "demo" },
80
+ "grokbot-agents": { dir: "grokbot-agents" },
80
81
  "pr-autofixer": { dir: "pr-autofixer" },
81
82
  "security-help": { dir: "security-help" },
82
83
  "security-reviewer": { dir: "security-reviewer" },
@@ -711,6 +712,7 @@ export function printInitGuide(): void {
711
712
  Scaffold a project, then open the playground:
712
713
  npx ${PACKAGE_NAME} init ./my-agent # installs deps + links agent-sdk onto PATH
713
714
  npx ${PACKAGE_NAME} init ./my-demo --template demo # record a PR walkthrough
715
+ npx ${PACKAGE_NAME} init ./grokbot --template grokbot-agents # talk to Grok Bot agents
714
716
  npx ${PACKAGE_NAME} init ./code-wiki --template code-wiki # keep wiki pages current after merge
715
717
  npx ${PACKAGE_NAME} init ./agents-md --template agents-md # keep AGENTS.md current from last week's PRs and Slack
716
718
  npx ${PACKAGE_NAME} init ./my-reviewer --template security-reviewer # review PRs for security bugs
@@ -719,8 +721,9 @@ Scaffold a project, then open the playground:
719
721
  npx ${PACKAGE_NAME} init ./my-triage --template triage-jira # comment on Jira issues
720
722
  npx ${PACKAGE_NAME} init ./my-owners --template agentic-owners # review PRs via owners policies
721
723
  npx ${PACKAGE_NAME} init ./pr-autofixer --template pr-autofixer # fix PRs on a cloud VM
722
- # TTY asks for GitHub repos. Scripts pass --var:
724
+ # TTY asks the template's questions. Scripts pass --var:
723
725
  # --var repos=acme/widgets,acme/api
726
+ # --var agents="Plan Keeper, Chief of Staff"
724
727
  cd ./my-agent && agent-sdk dev # or: npx ${PACKAGE_NAME} dev
725
728
 
726
729
  Recommended loop once the project exists (from the project directory):
@@ -432,6 +432,25 @@ export class CursorSdkRunner implements AgentRunner {
432
432
  return { warmed: true };
433
433
  }
434
434
 
435
+ /**
436
+ * Drop the cached SDK agent for this session, if one is open.
437
+ *
438
+ * Hosted v2 parks and hibernates this process as soon as the execution
439
+ * reply lands on non-interactive channels. A leftover handle still holds
440
+ * the pre-freeze Connect/HTTP2 session; the next wake then reuses it and
441
+ * `AgentService/Run` never starts. Call after `waitForCompletion` so the
442
+ * hibernate snapshot has an empty cache. Slack's interactive hot-window
443
+ * still reuses handles because that path does not go through hosted
444
+ * delivery.
445
+ */
446
+ evictSession(sessionId: string): void {
447
+ const cached = this.agents.get(sessionId);
448
+ if (cached === undefined) {
449
+ return;
450
+ }
451
+ this.evictAgent(sessionId, cached.agent);
452
+ }
453
+
435
454
  async dispose(): Promise<void> {
436
455
  const releases = this.prewarmReleases.splice(0);
437
456
  await Promise.allSettled(releases.map((release) => release()));
@@ -918,6 +918,13 @@ export async function startServer(
918
918
  ...(accountRefs !== undefined && accountBridge !== undefined
919
919
  ? { accountBridge, accountRefs }
920
920
  : {}),
921
+ ...(sdkRunner instanceof CursorSdkRunner
922
+ ? {
923
+ evictSdkSession: (sessionId: string) => {
924
+ sdkRunner.evictSession(sessionId);
925
+ },
926
+ }
927
+ : {}),
921
928
  });
922
929
  for (const channel of project.channels) {
923
930
  const definition = channel.definition;
@@ -1440,6 +1447,12 @@ function mountAgentRoutes(
1440
1447
  * here so the Temporal host can read a stable path after the POST.
1441
1448
  */
1442
1449
  diagStateRoot?: string;
1450
+ /**
1451
+ * Drop the cached SDK agent handle after a hosted turn so hibernate
1452
+ * cannot snapshot a frozen Connect/HTTP2 session. Injected test
1453
+ * runners may omit this.
1454
+ */
1455
+ evictSdkSession?: (sessionId: string) => void;
1443
1456
  }
1444
1457
  ): void {
1445
1458
  const { slug, project, engine, scheduleRunner, reminderRunner } = runtime;
@@ -1523,6 +1536,9 @@ function mountAgentRoutes(
1523
1536
  flushStartLog,
1524
1537
  }),
1525
1538
  snapshotLogs: () => logRing.since(),
1539
+ ...(args.evictSdkSession === undefined
1540
+ ? {}
1541
+ : { evictSdkSession: args.evictSdkSession }),
1526
1542
  });
1527
1543
  }
1528
1544
  // Playground lists/opens Slack (and other channel) sessions across
@@ -202,13 +202,19 @@ import { resolveAgentV2Hosting } from "./v2-hosting.js";
202
202
  import {
203
203
  buildAgentsMdContent,
204
204
  buildCloudMemorySection,
205
+ buildCloudSessionWorkspaceSection,
205
206
  buildCloudWorkspaceSection,
206
207
  ensureOwnGitRoot,
207
208
  isNestedInGitRepo,
208
209
  materializeWorkspace,
209
210
  SERVER_TOOLS_MCP_SERVER_NAME,
210
211
  } from "./workspace.js";
211
- import { syncWorkspaceToStore } from "./workspace-sync.js";
212
+ import {
213
+ syncSessionWorkspaceFiles,
214
+ syncWorkspaceToStore,
215
+ WorkspaceFileRefusal,
216
+ wipeSessionWorkspace,
217
+ } from "./workspace-sync.js";
212
218
 
213
219
  export const HTTP_CHANNEL_ID = "http";
214
220
  export { SCHEDULE_CHANNEL_PREFIX } from "./prompt-context.js";
@@ -222,6 +228,7 @@ interface CloudStoreTarget {
222
228
  mountPath: string;
223
229
  skillsPrefix: string;
224
230
  workspacePrefix: string;
231
+ sessionWorkspacePrefix: (sessionId: string) => string;
225
232
  label: string;
226
233
  }
227
234
 
@@ -2254,10 +2261,13 @@ export class SessionEngine {
2254
2261
  await this.baselineDeliveredNudges(record);
2255
2262
  const pendingFiles = record.pendingWorkspaceFiles;
2256
2263
  const pendingImages = record.pendingImages;
2257
- if (
2258
- pendingFiles !== undefined &&
2259
- Object.keys(pendingFiles).length > 0
2260
- ) {
2264
+ const runtime = resolveSessionRuntime({
2265
+ agentRuntime: agent.runtime,
2266
+ cloudOverride: record.cloudOverride,
2267
+ });
2268
+ const hasPendingFiles =
2269
+ pendingFiles !== undefined && Object.keys(pendingFiles).length > 0;
2270
+ if (hasPendingFiles && runtime !== "cloud") {
2261
2271
  const root = await this.resolveFileSystem({
2262
2272
  sessionId,
2263
2273
  continuationKey: record.continuationKey ?? undefined,
@@ -2267,13 +2277,14 @@ export class SessionEngine {
2267
2277
  await writeWorkspaceFilesToRoot(root, pendingFiles);
2268
2278
  }
2269
2279
  if (
2270
- (pendingFiles !== undefined &&
2271
- Object.keys(pendingFiles).length > 0) ||
2280
+ (hasPendingFiles && runtime !== "cloud") ||
2272
2281
  (pendingImages !== undefined && pendingImages.length > 0)
2273
2282
  ) {
2274
2283
  await this.updateRecord(sessionId, (current) => ({
2275
2284
  ...current,
2276
2285
  pendingWorkspaceFiles:
2286
+ hasPendingFiles &&
2287
+ runtime !== "cloud" &&
2277
2288
  current.pendingWorkspaceFiles === pendingFiles
2278
2289
  ? undefined
2279
2290
  : current.pendingWorkspaceFiles,
@@ -2441,6 +2452,7 @@ export class SessionEngine {
2441
2452
  data: { message: outcome.errorMessage ?? "task failed" },
2442
2453
  }
2443
2454
  );
2455
+ await this.wipeCloudSessionWorkspace(sessionId);
2444
2456
  } else {
2445
2457
  await this.appendEvent(sessionId, {
2446
2458
  type: "session.waiting",
@@ -2495,6 +2507,68 @@ export class SessionEngine {
2495
2507
  );
2496
2508
  }
2497
2509
 
2510
+ /**
2511
+ * Replace this session's store prefix with the pending `workspaceFiles`.
2512
+ * Undefined when the write did not land; the prompt must not name the
2513
+ * mount, matching ensureCloudWorkspaceSynced.
2514
+ */
2515
+ private async syncCloudSessionWorkspaceFiles(
2516
+ store: CloudStoreTarget,
2517
+ sessionId: string,
2518
+ files: NonNullable<SessionRecord["pendingWorkspaceFiles"]>
2519
+ ): Promise<{ dir: string; relPaths: string[] } | undefined> {
2520
+ const workspacePrefix = store.sessionWorkspacePrefix(sessionId);
2521
+ try {
2522
+ const relPaths = await syncSessionWorkspaceFiles({
2523
+ sink: store.sink,
2524
+ files,
2525
+ workspacePrefix,
2526
+ });
2527
+ return {
2528
+ dir: `${store.mountPath}/${workspacePrefix}`,
2529
+ relPaths,
2530
+ };
2531
+ } catch (error) {
2532
+ this.logger(
2533
+ `[workspace] ${store.label} session files sync failed: ${error instanceof Error ? error.message : String(error)}`
2534
+ );
2535
+ if (error instanceof WorkspaceFileRefusal) {
2536
+ await this.clearPendingWorkspaceFiles(sessionId, files);
2537
+ }
2538
+ return undefined;
2539
+ }
2540
+ }
2541
+
2542
+ private async wipeCloudSessionWorkspace(sessionId: string): Promise<void> {
2543
+ const store = this.cloudStore();
2544
+ if (store === undefined) {
2545
+ return;
2546
+ }
2547
+ try {
2548
+ await wipeSessionWorkspace({
2549
+ sink: store.sink,
2550
+ workspacePrefix: store.sessionWorkspacePrefix(sessionId),
2551
+ });
2552
+ } catch (error) {
2553
+ this.logger(
2554
+ `[workspace] ${store.label} session files wipe failed: ${error instanceof Error ? error.message : String(error)}`
2555
+ );
2556
+ }
2557
+ }
2558
+
2559
+ private async clearPendingWorkspaceFiles(
2560
+ sessionId: string,
2561
+ pending: NonNullable<SessionRecord["pendingWorkspaceFiles"]>
2562
+ ): Promise<void> {
2563
+ await this.updateRecord(sessionId, (current) => ({
2564
+ ...current,
2565
+ pendingWorkspaceFiles:
2566
+ current.pendingWorkspaceFiles === pending
2567
+ ? undefined
2568
+ : current.pendingWorkspaceFiles,
2569
+ }));
2570
+ }
2571
+
2498
2572
  /**
2499
2573
  * With files authored, every first cloud turn re-copies the authored
2500
2574
  * bytes and the memo only dedupes concurrent first turns. The mount is
@@ -2547,6 +2621,8 @@ export class SessionEngine {
2547
2621
  mountPath: DEPLOYMENT_STORE_MOUNT_PATH,
2548
2622
  skillsPrefix: agentStoreKeys.skillsPrefix(),
2549
2623
  workspacePrefix: agentStoreKeys.workspacePrefix(),
2624
+ sessionWorkspacePrefix: (sessionId: string) =>
2625
+ agentStoreKeys.sessionWorkspacePrefix(sessionId),
2550
2626
  label: "deployment store",
2551
2627
  };
2552
2628
  if (v2Hosting.kind === "cursor-managed") {
@@ -2569,6 +2645,11 @@ export class SessionEngine {
2569
2645
  mountPath: USER_STORE_MOUNT_PATH,
2570
2646
  skillsPrefix: agentStoreKeys.userSkillsPrefix(this.project.name),
2571
2647
  workspacePrefix: agentStoreKeys.userWorkspacePrefix(this.project.name),
2648
+ sessionWorkspacePrefix: (sessionId: string) =>
2649
+ agentStoreKeys.userSessionWorkspacePrefix(
2650
+ this.project.name,
2651
+ sessionId
2652
+ ),
2572
2653
  label: "user store",
2573
2654
  };
2574
2655
  }
@@ -2690,34 +2771,39 @@ export class SessionEngine {
2690
2771
  });
2691
2772
  turnPrompt = `${contextBlock}\n\n${prompt}`;
2692
2773
  }
2693
- if (isFirstTurn) {
2694
- if (runtime === "cloud") {
2695
- // Cloud VMs have no materialized workspace: ship the full context,
2696
- // including agent tool script bodies. Authored skills and workspace
2697
- // seeds land on the store mount (skills for native discovery, seeds
2698
- // for the model's file tools), and on hosted deployments the shared
2699
- // memory journal is on that mount too — say where. When the turn
2700
- // carries the session-bound server-tool MCP endpoint, the preamble
2701
- // catalogs server tools under that MCP server name *before* the
2702
- // agent-tool "write the script if missing" recipe, so a model told
2703
- // to "use <server tool>" calls MCP instead of inventing a .sh.
2704
- const preamble = buildAgentsMdContent(agent, {
2774
+ if (runtime === "cloud") {
2775
+ // Cloud VMs have no materialized workspace: ship the full context,
2776
+ // including agent tool script bodies. Authored skills and workspace
2777
+ // seeds land on the store mount (skills for native discovery, seeds
2778
+ // for the model's file tools), per-turn workspaceFiles land under
2779
+ // the session prefix, and on hosted deployments the shared memory
2780
+ // journal is on that mount too — say where. When the turn carries
2781
+ // the session-bound server-tool MCP endpoint, the preamble catalogs
2782
+ // server tools under that MCP server name *before* the agent-tool
2783
+ // "write the script if missing" recipe, so a model told to "use
2784
+ // <server tool>" calls MCP instead of inventing a .sh.
2785
+ const hosted = isCursorHostedFilesAvailable();
2786
+ const store =
2787
+ hosted || isCursorUserStoreFilesAvailable()
2788
+ ? this.cloudStore()
2789
+ : undefined;
2790
+ let preamble = "";
2791
+ const sections: string[] = [];
2792
+ const pending = record.pendingWorkspaceFiles;
2793
+ if (isFirstTurn) {
2794
+ preamble = buildAgentsMdContent(agent, {
2705
2795
  includeScripts: true,
2706
2796
  serverToolsMcpName:
2707
2797
  this.cloudServerToolsConnection === undefined
2708
2798
  ? undefined
2709
2799
  : SERVER_TOOLS_MCP_SERVER_NAME,
2710
2800
  });
2711
- const hosted = isCursorHostedFilesAvailable();
2712
- // Synced whenever a store is reachable, not just when skills or
2713
- // seeds exist: removing the last one has to delete its store copies
2714
- // too.
2715
- const store =
2716
- hosted || isCursorUserStoreFilesAvailable()
2717
- ? this.cloudStore()
2718
- : undefined;
2719
- let sections = "";
2720
- if (store !== undefined) {
2801
+ }
2802
+ if (store !== undefined) {
2803
+ if (isFirstTurn) {
2804
+ // Synced whenever a store is reachable, not just when skills or
2805
+ // seeds exist: removing the last one has to delete its store copies
2806
+ // too.
2721
2807
  const [, seedsSynced] = await Promise.all([
2722
2808
  this.ensureCloudSkillsSynced(store),
2723
2809
  this.ensureCloudWorkspaceSynced(store),
@@ -2725,37 +2811,63 @@ export class SessionEngine {
2725
2811
  // Only name the mount when the bytes landed; a failed sync must
2726
2812
  // not send the model looking for files that are not there.
2727
2813
  if (seedsSynced && agent.seedFiles.length > 0) {
2728
- const dir = `${store.mountPath}/${store.workspacePrefix}`;
2729
- sections += `\n\n${buildCloudWorkspaceSection(dir)}`;
2814
+ sections.push(
2815
+ buildCloudWorkspaceSection(
2816
+ `${store.mountPath}/${store.workspacePrefix}`
2817
+ )
2818
+ );
2730
2819
  }
2731
2820
  }
2732
- if (hosted) {
2733
- sections += `\n\n${buildCloudMemorySection(this.project.name)}`;
2821
+ if (pending !== undefined && Object.keys(pending).length > 0) {
2822
+ const synced = await this.syncCloudSessionWorkspaceFiles(
2823
+ store,
2824
+ record.sessionId,
2825
+ pending
2826
+ );
2827
+ if (synced !== undefined) {
2828
+ sections.push(
2829
+ buildCloudSessionWorkspaceSection(synced.dir, synced.relPaths)
2830
+ );
2831
+ await this.clearPendingWorkspaceFiles(record.sessionId, pending);
2832
+ }
2734
2833
  }
2735
- turnPrompt = `${preamble}${sections}\n\n${turnPrompt}`;
2736
- } else if (runtime === "local" && authoredRoot) {
2737
- // Virtual roots skip materialize, so AGENTS.md never lands on disk.
2738
- // Ship the same preamble the borrowed-cwd local path uses.
2739
- const preamble = buildAgentsMdContent(
2740
- { ...agent, tools: [...agent.tools, ...synthesized] },
2741
- { includeScripts: true }
2742
- );
2743
- const workspaceInfo = `<user_info>\nWorkspace Path: /\n</user_info>`;
2744
- turnPrompt =
2745
- preamble === ""
2746
- ? `${workspaceInfo}\n\n${turnPrompt}`
2747
- : `${workspaceInfo}\n\n${preamble}\n\n${turnPrompt}`;
2748
- } else if (runtime === "local" && instructionsInvisibleToHarness) {
2749
- // Local workspace whose AGENTS.md the harness will not load. With a
2750
- // borrowed harness cwd, agent tool scripts live only in the durable
2751
- // scaffold dir — embed bodies like cloud. Otherwise the catalog can
2752
- // reference on-disk paths under the harness cwd. Grokbot turns are
2753
- // excluded: their instructions ride the request's `instructions`
2754
- // field into the Sand system prompt instead.
2755
- const includeScripts = this.hasBorrowedWorkspace(record);
2756
- const preamble = buildAgentsMdContent(agent, { includeScripts });
2757
- turnPrompt = `${preamble}\n\n${turnPrompt}`;
2834
+ } else if (pending !== undefined && Object.keys(pending).length > 0) {
2835
+ await this.clearPendingWorkspaceFiles(record.sessionId, pending);
2836
+ }
2837
+ if (isFirstTurn && hosted) {
2838
+ sections.push(buildCloudMemorySection(this.project.name));
2758
2839
  }
2840
+ if (preamble !== "" || sections.length > 0) {
2841
+ turnPrompt = [preamble, ...sections, turnPrompt]
2842
+ .filter((part) => part !== "")
2843
+ .join("\n\n");
2844
+ }
2845
+ } else if (isFirstTurn && runtime === "local" && authoredRoot) {
2846
+ // Virtual roots skip materialize, so AGENTS.md never lands on disk.
2847
+ // Ship the same preamble the borrowed-cwd local path uses.
2848
+ const preamble = buildAgentsMdContent(
2849
+ { ...agent, tools: [...agent.tools, ...synthesized] },
2850
+ { includeScripts: true }
2851
+ );
2852
+ const workspaceInfo = `<user_info>\nWorkspace Path: /\n</user_info>`;
2853
+ turnPrompt =
2854
+ preamble === ""
2855
+ ? `${workspaceInfo}\n\n${turnPrompt}`
2856
+ : `${workspaceInfo}\n\n${preamble}\n\n${turnPrompt}`;
2857
+ } else if (
2858
+ isFirstTurn &&
2859
+ runtime === "local" &&
2860
+ instructionsInvisibleToHarness
2861
+ ) {
2862
+ // Local workspace whose AGENTS.md the harness will not load. With a
2863
+ // borrowed harness cwd, agent tool scripts live only in the durable
2864
+ // scaffold dir — embed bodies like cloud. Otherwise the catalog can
2865
+ // reference on-disk paths under the harness cwd. Grokbot turns are
2866
+ // excluded: their instructions ride the request's `instructions`
2867
+ // field into the Sand system prompt instead.
2868
+ const includeScripts = this.hasBorrowedWorkspace(record);
2869
+ const preamble = buildAgentsMdContent(agent, { includeScripts });
2870
+ turnPrompt = `${preamble}\n\n${turnPrompt}`;
2759
2871
  }
2760
2872
  return {
2761
2873
  sessionId: record.sessionId,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Mirror an authored file tree onto an Agent Store prefix. Shared by the
3
- * skills and workspace seed syncs so both keep the same write-then-sweep
4
- * discipline and the same path refusals.
3
+ * skills, workspace seed, and session workspaceFile syncs so they keep the
4
+ * same write-then-sweep discipline and the same path refusals.
5
5
  */
6
6
 
7
7
  import { posix } from "node:path";