@cursor/july 0.1.28 → 0.1.30

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 (122) hide show
  1. package/dist/bin/agent-serve.js +20 -2
  2. package/dist/channels/slack/slack-channel.d.ts +19 -1
  3. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  4. package/dist/channels/slack/slack-channel.js +29 -0
  5. package/dist/docs/404.html +1 -1
  6. package/dist/docs/ab.html +2 -2
  7. package/dist/docs/assets/{app.CqjrYaT2.js → app.Cm4lsMc4.js} +1 -1
  8. package/dist/docs/assets/chunks/@localSearchIndexroot.CbQshWFK.js +1 -0
  9. package/dist/docs/assets/chunks/{VPLocalSearchBox.ag-MwUel.js → VPLocalSearchBox.CYUFXspH.js} +1 -1
  10. package/dist/docs/assets/chunks/{theme.BS-DJWat.js → theme.CTjkX9vc.js} +2 -2
  11. package/dist/docs/building-with-agents.html +2 -2
  12. package/dist/docs/concepts.html +2 -2
  13. package/dist/docs/deployment.html +2 -2
  14. package/dist/docs/evals.html +2 -2
  15. package/dist/docs/example-agents/approval-buddy.html +2 -2
  16. package/dist/docs/example-agents/benny.html +2 -2
  17. package/dist/docs/example-agents/bugbot.html +2 -2
  18. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  19. package/dist/docs/example-agents/codeowners-review.html +2 -2
  20. package/dist/docs/example-agents/concierge.html +2 -2
  21. package/dist/docs/example-agents/fsd.html +2 -2
  22. package/dist/docs/example-agents/index.html +2 -2
  23. package/dist/docs/example-agents/knowledge-base.html +2 -2
  24. package/dist/docs/example-agents/oncall.html +2 -2
  25. package/dist/docs/example-agents/security-reviewer.html +2 -2
  26. package/dist/docs/example-agents/slack-agent.html +2 -2
  27. package/dist/docs/example-agents/weather-agent.html +2 -2
  28. package/dist/docs/guides/agent-to-agent.html +2 -2
  29. package/dist/docs/guides/cloud-runtime.html +2 -2
  30. package/dist/docs/guides/github.html +2 -2
  31. package/dist/docs/guides/human-in-the-loop.html +2 -2
  32. package/dist/docs/guides/mcp-oauth.html +2 -2
  33. package/dist/docs/guides/slack.html +2 -2
  34. package/dist/docs/guides/webhooks.html +2 -2
  35. package/dist/docs/hillclimbing.html +2 -2
  36. package/dist/docs/index.html +2 -2
  37. package/dist/docs/quickstart.html +2 -2
  38. package/dist/docs/reference/agent-config.html +2 -2
  39. package/dist/docs/reference/channels.html +2 -2
  40. package/dist/docs/reference/cli.html +2 -2
  41. package/dist/docs/reference/connections.html +2 -2
  42. package/dist/docs/reference/hooks.html +2 -2
  43. package/dist/docs/reference/http-api.html +2 -2
  44. package/dist/docs/reference/instructions.html +2 -2
  45. package/dist/docs/reference/playground.html +2 -2
  46. package/dist/docs/reference/project-layout.html +2 -2
  47. package/dist/docs/reference/prompt.html +2 -2
  48. package/dist/docs/reference/schedules.html +2 -2
  49. package/dist/docs/reference/sessions.html +2 -2
  50. package/dist/docs/reference/skills.html +2 -2
  51. package/dist/docs/reference/subagents.html +2 -2
  52. package/dist/docs/reference/tools.html +2 -2
  53. package/dist/docs/scaffolding-agents.html +2 -2
  54. package/dist/docs/storage.html +2 -2
  55. package/dist/docs/troubleshooting.html +2 -2
  56. package/dist/internal/cloud-turn-cost.d.ts +34 -0
  57. package/dist/internal/cloud-turn-cost.d.ts.map +1 -0
  58. package/dist/internal/cloud-turn-cost.js +106 -0
  59. package/dist/internal/discovery.d.ts.map +1 -1
  60. package/dist/internal/discovery.js +1 -1
  61. package/dist/internal/event-log.d.ts +6 -0
  62. package/dist/internal/event-log.d.ts.map +1 -1
  63. package/dist/internal/event-log.js +16 -0
  64. package/dist/internal/host-platforms.d.ts +13 -0
  65. package/dist/internal/host-platforms.d.ts.map +1 -1
  66. package/dist/internal/host-platforms.js +25 -16
  67. package/dist/internal/http-channel.d.ts.map +1 -1
  68. package/dist/internal/http-channel.js +3 -1
  69. package/dist/internal/local-control-plane.d.ts +25 -0
  70. package/dist/internal/local-control-plane.d.ts.map +1 -0
  71. package/dist/internal/local-control-plane.js +172 -0
  72. package/dist/internal/platform-schedule-sync.d.ts +17 -0
  73. package/dist/internal/platform-schedule-sync.d.ts.map +1 -0
  74. package/dist/internal/platform-schedule-sync.js +72 -0
  75. package/dist/internal/platform-timers.d.ts +28 -0
  76. package/dist/internal/platform-timers.d.ts.map +1 -0
  77. package/dist/internal/platform-timers.js +120 -0
  78. package/dist/internal/reminder-runner.d.ts +16 -12
  79. package/dist/internal/reminder-runner.d.ts.map +1 -1
  80. package/dist/internal/reminder-runner.js +112 -61
  81. package/dist/internal/reminder-store.d.ts +5 -4
  82. package/dist/internal/reminder-store.d.ts.map +1 -1
  83. package/dist/internal/reminder-store.js +5 -4
  84. package/dist/internal/sdk-runner.d.ts.map +1 -1
  85. package/dist/internal/sdk-runner.js +44 -1
  86. package/dist/internal/server.d.ts.map +1 -1
  87. package/dist/internal/server.js +105 -11
  88. package/dist/internal/session-engine.d.ts +22 -1
  89. package/dist/internal/session-engine.d.ts.map +1 -1
  90. package/dist/internal/session-engine.js +110 -4
  91. package/dist/internal/storage-coordinator.d.ts +4 -15
  92. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  93. package/dist/internal/storage-coordinator.js +4 -30
  94. package/dist/playground/assets/index-CquRB-l0.js +85 -0
  95. package/dist/playground/assets/{index-Bul1WfrB.css → index-Dj0bWpkn.css} +1 -1
  96. package/dist/playground/index.html +2 -2
  97. package/dist/storage.d.ts +0 -3
  98. package/dist/storage.d.ts.map +1 -1
  99. package/dist/storage.js +0 -3
  100. package/dist/types.d.ts +11 -2
  101. package/dist/types.d.ts.map +1 -1
  102. package/package.json +1 -1
  103. package/src/bin/agent-serve.ts +21 -2
  104. package/src/channels/slack/slack-channel.ts +46 -0
  105. package/src/internal/cloud-turn-cost.ts +158 -0
  106. package/src/internal/discovery.ts +1 -0
  107. package/src/internal/event-log.ts +17 -0
  108. package/src/internal/host-platforms.ts +40 -18
  109. package/src/internal/http-channel.ts +3 -1
  110. package/src/internal/local-control-plane.ts +188 -0
  111. package/src/internal/platform-schedule-sync.ts +81 -0
  112. package/src/internal/platform-timers.ts +173 -0
  113. package/src/internal/reminder-runner.ts +131 -67
  114. package/src/internal/reminder-store.ts +5 -4
  115. package/src/internal/sdk-runner.ts +49 -1
  116. package/src/internal/server.ts +121 -6
  117. package/src/internal/session-engine.ts +138 -3
  118. package/src/internal/storage-coordinator.ts +4 -41
  119. package/src/storage.ts +0 -5
  120. package/src/types.ts +11 -2
  121. package/dist/docs/assets/chunks/@localSearchIndexroot.VWQoPFJ1.js +0 -1
  122. package/dist/playground/assets/index-CkN74U_L.js +0 -87
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import { randomBytes } from "node:crypto";
8
- import { join } from "node:path";
9
8
  import type {
10
9
  AuthContext,
11
10
  ReminderCreateInput,
@@ -15,6 +14,7 @@ import type {
15
14
  ReminderUntilContext,
16
15
  } from "../types.js";
17
16
  import { parseDuration } from "./duration.js";
17
+ import type { PlatformTimers } from "./platform-timers.js";
18
18
  import {
19
19
  defaultBusyPolicy,
20
20
  MAX_TIMER_MS,
@@ -22,11 +22,7 @@ import {
22
22
  ReminderValidationError,
23
23
  resolveSchedule,
24
24
  } from "./reminder-schedule.js";
25
- import {
26
- type ReminderBusyPolicy,
27
- type ReminderRecord,
28
- ReminderStore,
29
- } from "./reminder-store.js";
25
+ import type { ReminderBusyPolicy, ReminderRecord } from "./reminder-store.js";
30
26
  import { APP_AUTH } from "./schedule-runner.js";
31
27
  import { SessionBusyError, type SessionEngine } from "./session-engine.js";
32
28
 
@@ -98,7 +94,7 @@ function buildFirePrompt(record: ReminderRecord): string {
98
94
  }
99
95
 
100
96
  export class ReminderRunner {
101
- private readonly store: ReminderStore;
97
+ private readonly records = new Map<string, ReminderRecord>();
102
98
  private readonly handlers = new Map<string, LiveHandlers>();
103
99
  private readonly timers = new Map<string, NodeJS.Timeout>();
104
100
  private readonly firing = new Set<string>();
@@ -108,28 +104,70 @@ export class ReminderRunner {
108
104
 
109
105
  constructor(
110
106
  private readonly engine: SessionEngine,
111
- private readonly logger: (line: string) => void = () => {}
112
- ) {
113
- this.store = new ReminderStore(join(engine.stateRoot, "reminders"));
114
- }
107
+ private readonly logger: (line: string) => void = () => {},
108
+ private readonly platformTimers?: PlatformTimers
109
+ ) {}
115
110
 
116
- get storeRoot(): string {
117
- return join(this.engine.stateRoot, "reminders");
111
+ /**
112
+ * Load reminder documents from the control plane into memory. Does not
113
+ * arm in-process timers — the CP owns clocks.
114
+ */
115
+ async hydrate(): Promise<void> {
116
+ if (this.platformTimers === undefined) {
117
+ return;
118
+ }
119
+ let remote: ReminderRecord[] = [];
120
+ try {
121
+ remote = await this.platformTimers.listReminders();
122
+ } catch (error) {
123
+ this.logger(
124
+ `[agentkit] reminders: control-plane list failed: ${
125
+ error instanceof Error ? error.message : String(error)
126
+ }`
127
+ );
128
+ return;
129
+ }
130
+ this.records.clear();
131
+ for (const record of remote) {
132
+ this.records.set(record.id, record);
133
+ }
134
+ let disarmedOrphan = 0;
135
+ for (const record of [...this.records.values()]) {
136
+ if (record.status !== "armed") {
137
+ continue;
138
+ }
139
+ if (
140
+ (record.hasRunHandler === true || record.hasUntilFn === true) &&
141
+ this.handlers.get(record.id) === undefined
142
+ ) {
143
+ await this.disarm(record, "handler_lost_on_restart");
144
+ disarmedOrphan += 1;
145
+ }
146
+ }
147
+ if (remote.length > 0 || disarmedOrphan > 0) {
148
+ this.logger(
149
+ `[agentkit] reminders: hydrated ${remote.length} reminder(s) from control plane` +
150
+ (disarmedOrphan > 0
151
+ ? `; disarmed ${disarmedOrphan} without live handlers`
152
+ : "")
153
+ );
154
+ }
118
155
  }
119
156
 
120
- /** Start deadline timers for armed reminders (call after restore). */
157
+ /** Start deadline timers for armed reminders (tests / no control plane). */
121
158
  async start(): Promise<void> {
122
159
  if (this.started) return;
123
160
  this.started = true;
124
- // Materialize sink-backed reminders onto local disk before arming
125
- // (local wins when both sides have the same id).
126
- await this.hydrateFromStorage();
127
- const armed = await this.store.listArmed();
161
+ await this.hydrate();
162
+ if (this.platformTimers !== undefined) {
163
+ return;
164
+ }
165
+ const armed = [...this.records.values()].filter(
166
+ (r) => r.status === "armed"
167
+ );
128
168
  let restored = 0;
129
169
  let disarmedOrphan = 0;
130
170
  for (const record of armed) {
131
- // Run / function-until handlers are in-memory only. After restart they
132
- // are gone — disarm rather than falling back to a generic prompt fire.
133
171
  if (
134
172
  (record.hasRunHandler === true || record.hasUntilFn === true) &&
135
173
  this.handlers.get(record.id) === undefined
@@ -151,43 +189,52 @@ export class ReminderRunner {
151
189
  }
152
190
  }
153
191
 
154
- /**
155
- * Pull reminder records from `defineStorage` into `<stateRoot>/reminders`
156
- * when local disk is empty for that id. Failures are logged and skipped.
157
- */
158
- private async hydrateFromStorage(): Promise<void> {
159
- const storage = this.engine.storage;
160
- if (storage === undefined || !storage.canBulkRestore) {
161
- return;
192
+ private async persist(record: ReminderRecord): Promise<void> {
193
+ if (this.platformTimers !== undefined) {
194
+ await this.platformTimers.putReminder(record);
162
195
  }
163
- let hydrated = 0;
164
- try {
165
- const remote = await storage.listReminders();
166
- for (const record of remote) {
167
- const local = await this.store.get(record.id);
168
- if (local !== undefined) {
169
- continue;
196
+ this.records.set(record.id, record);
197
+ }
198
+
199
+ private async getRecord(id: string): Promise<ReminderRecord | undefined> {
200
+ if (this.platformTimers !== undefined) {
201
+ try {
202
+ const remote = await this.platformTimers.getReminder(id);
203
+ if (remote === undefined) {
204
+ this.records.delete(id);
205
+ return undefined;
170
206
  }
171
- // Local write only — re-mirroring would just echo the sink.
172
- await this.store.save(record);
173
- hydrated += 1;
207
+ this.records.set(id, remote);
208
+ return remote;
209
+ } catch (error) {
210
+ this.logger(
211
+ `[agentkit] reminders: control-plane get failed for ${id}: ${
212
+ error instanceof Error ? error.message : String(error)
213
+ }`
214
+ );
215
+ return this.records.get(id);
174
216
  }
175
- } catch (error) {
176
- const message = error instanceof Error ? error.message : String(error);
177
- this.logger(`[agentkit] reminders: storage hydrate failed: ${message}`);
178
- return;
179
- }
180
- if (hydrated > 0) {
181
- this.logger(
182
- `[agentkit] reminders: hydrated ${hydrated} reminder(s) from storage`
183
- );
184
217
  }
218
+ return this.records.get(id);
185
219
  }
186
220
 
187
- /** Persist locally and mirror into the authored storage sink when present. */
188
- private async persist(record: ReminderRecord): Promise<void> {
189
- await this.store.save(record);
190
- this.engine.storage?.reminder(record);
221
+ private async allRecords(): Promise<ReminderRecord[]> {
222
+ if (this.platformTimers !== undefined) {
223
+ try {
224
+ const remote = await this.platformTimers.listReminders();
225
+ this.records.clear();
226
+ for (const record of remote) {
227
+ this.records.set(record.id, record);
228
+ }
229
+ } catch (error) {
230
+ this.logger(
231
+ `[agentkit] reminders: control-plane list failed: ${
232
+ error instanceof Error ? error.message : String(error)
233
+ }`
234
+ );
235
+ }
236
+ }
237
+ return [...this.records.values()];
191
238
  }
192
239
 
193
240
  stop(): void {
@@ -256,7 +303,7 @@ export class ReminderRunner {
256
303
  run: input.run,
257
304
  untilFn,
258
305
  });
259
- if (this.started) {
306
+ if (this.started && this.platformTimers === undefined) {
260
307
  this.armTimer(record);
261
308
  }
262
309
 
@@ -268,7 +315,7 @@ export class ReminderRunner {
268
315
  purpose?: string;
269
316
  continuationToken?: string;
270
317
  }): Promise<ReminderInfo[]> {
271
- const all = await this.store.list();
318
+ const all = await this.allRecords();
272
319
  return all
273
320
  .filter((r) => {
274
321
  if (filter?.status !== undefined && r.status !== filter.status) {
@@ -301,7 +348,7 @@ export class ReminderRunner {
301
348
  id: string,
302
349
  reason = "explicit_cancel"
303
350
  ): Promise<ReminderInfo | undefined> {
304
- const record = await this.store.get(id);
351
+ const record = await this.getRecord(id);
305
352
  if (record === undefined) {
306
353
  return undefined;
307
354
  }
@@ -325,7 +372,7 @@ export class ReminderRunner {
325
372
  continuationToken: string,
326
373
  reason: string
327
374
  ): Promise<number> {
328
- const armed = await this.store.listArmed();
375
+ const armed = (await this.allRecords()).filter((r) => r.status === "armed");
329
376
  let n = 0;
330
377
  for (const record of armed) {
331
378
  if (record.continuationToken === continuationToken) {
@@ -347,7 +394,7 @@ export class ReminderRunner {
347
394
  oldToken: string,
348
395
  newToken: string
349
396
  ): Promise<number> {
350
- const armed = await this.store.listArmed();
397
+ const armed = (await this.allRecords()).filter((r) => r.status === "armed");
351
398
  let n = 0;
352
399
  for (const record of armed) {
353
400
  if (
@@ -366,17 +413,30 @@ export class ReminderRunner {
366
413
  return n;
367
414
  }
368
415
 
369
- /** Fire one reminder immediately (dev / hillclimb). */
370
- async dispatch(reminderId: string): Promise<{
416
+ /** Fire one reminder immediately (dev / hillclimb / control plane). */
417
+ async dispatch(
418
+ reminderId: string,
419
+ opts: { expectedFireCount?: number } = {}
420
+ ): Promise<{
371
421
  reminderId: string;
372
422
  outcome: "fired" | "noop";
373
423
  reason?: string;
374
424
  }> {
375
- const record = await this.store.get(reminderId);
425
+ const record = await this.getRecord(reminderId);
376
426
  if (record === undefined) {
377
- const ids = (await this.store.list()).map((r) => r.id);
427
+ const ids = (await this.allRecords()).map((r) => r.id);
378
428
  throw new UnknownReminderError(reminderId, ids);
379
429
  }
430
+ if (
431
+ opts.expectedFireCount !== undefined &&
432
+ record.fireCount !== opts.expectedFireCount
433
+ ) {
434
+ return {
435
+ reminderId,
436
+ outcome: "noop",
437
+ reason: "stale_fire_count",
438
+ };
439
+ }
380
440
  const result = await this.fire(record.id, { force: true });
381
441
  return { reminderId, ...result };
382
442
  }
@@ -401,7 +461,7 @@ export class ReminderRunner {
401
461
  // Keep recurring reminders alive after unexpected throws.
402
462
  // One-shots stay armed so a failed wake can be retried / dispatched.
403
463
  try {
404
- const current = await this.store.get(record.id);
464
+ const current = await this.getRecord(record.id);
405
465
  if (current === undefined || current.status !== "armed") return;
406
466
  const oneShot =
407
467
  current.scheduleKind === "delay" || current.scheduleKind === "at";
@@ -442,7 +502,7 @@ export class ReminderRunner {
442
502
  }
443
503
  this.firing.add(id);
444
504
  try {
445
- const record = await this.store.get(id);
505
+ const record = await this.getRecord(id);
446
506
  if (record === undefined || record.status !== "armed") {
447
507
  this.clearTimer(id);
448
508
  return { outcome: "noop", reason: "not_armed" };
@@ -493,7 +553,7 @@ export class ReminderRunner {
493
553
  return { outcome: "fired", reason: "skipped_busy" };
494
554
  }
495
555
  await this.whenSessionIdle(record.channelId, record.continuationToken);
496
- const still = await this.store.get(id);
556
+ const still = await this.getRecord(id);
497
557
  if (still === undefined || still.status !== "armed") {
498
558
  return { outcome: "noop", reason: "disarmed_while_waiting" };
499
559
  }
@@ -511,7 +571,7 @@ export class ReminderRunner {
511
571
  }
512
572
 
513
573
  // Cancel / disarm during the handler must win over reschedule.
514
- const latest = await this.store.get(id);
574
+ const latest = await this.getRecord(id);
515
575
  if (result.action === "stop") {
516
576
  if (latest !== undefined && latest.status === "armed") {
517
577
  await this.disarm(latest, "run_stop");
@@ -740,7 +800,7 @@ export class ReminderRunner {
740
800
  _outcome: "skip" | "delivered",
741
801
  reschedule?: { delay?: string | number; at?: Date | string }
742
802
  ): Promise<void> {
743
- const current = await this.store.get(record.id);
803
+ const current = await this.getRecord(record.id);
744
804
  if (current === undefined || current.status !== "armed") {
745
805
  return;
746
806
  }
@@ -781,7 +841,9 @@ export class ReminderRunner {
781
841
  updatedAt: new Date().toISOString(),
782
842
  };
783
843
  await this.persist(updated);
784
- this.armTimer(updated);
844
+ if (this.platformTimers === undefined) {
845
+ this.armTimer(updated);
846
+ }
785
847
  return;
786
848
  }
787
849
 
@@ -817,7 +879,9 @@ export class ReminderRunner {
817
879
  updatedAt: new Date().toISOString(),
818
880
  };
819
881
  await this.persist(updated);
820
- this.armTimer(updated);
882
+ if (this.platformTimers === undefined) {
883
+ this.armTimer(updated);
884
+ }
821
885
  }
822
886
  }
823
887
 
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Durable reminder instance store under `<stateRoot>/reminders/`.
2
+ * Reminder document store used by the local control plane
3
+ * (`<stateRoot>/control-plane/reminders/`).
3
4
  *
4
5
  * Handlers (`run` / function `until`) are not serializable — they live only
5
- * in the in-memory {@link ReminderRunner} registry. Disk holds schedule,
6
- * prompt, target, and status so one-shots / recurring ticks survive restart
7
- * for prompt-form reminders.
6
+ * in the in-memory {@link ReminderRunner} registry. The control plane holds
7
+ * schedule, prompt, target, and status so prompt-form reminders survive
8
+ * restart.
8
9
  */
9
10
 
10
11
  import {
@@ -27,6 +27,7 @@ import {
27
27
  type TurnOutcome,
28
28
  type TurnUsage,
29
29
  } from "../types.js";
30
+ import { mapSdkTokenUsage, usageTokenSum } from "./cloud-turn-cost.js";
30
31
 
31
32
  /** Internal seam between the session engine and the Cursor SDK harness. */
32
33
  export interface AgentRunner {
@@ -215,7 +216,7 @@ export class CursorSdkRunner implements AgentRunner {
215
216
  }
216
217
  try {
217
218
  const result = await run.wait();
218
- const outcome = toTurnOutcome(result);
219
+ let outcome = toTurnOutcome(result);
219
220
  if (outcome.status === "error") {
220
221
  // An error result usually means the handle's transport is poisoned
221
222
  // — most often a cached access token that aged past its 60-minute
@@ -224,6 +225,16 @@ export class CursorSdkRunner implements AgentRunner {
224
225
  // attempt resumes the conversation from the store on a fresh
225
226
  // transport instead of failing every turn until a restart.
226
227
  this.evictAgent(request.sessionId, agentPromise);
228
+ } else if (
229
+ request.runtime === "cloud" &&
230
+ outcome.status === "finished" &&
231
+ usageTokenSum(outcome.usage) <= 0 &&
232
+ result.id !== ""
233
+ ) {
234
+ // Cloud `wait()` drops usage after stream reconnect / getRun replay,
235
+ // or returns an all-zero stub. `getUsage` still has the run's tokens
236
+ // (eventually); without them traces/ABs never record an estimated cost.
237
+ outcome = await fillMissingCloudUsage(agent, result.id, outcome);
227
238
  }
228
239
  return outcome;
229
240
  } finally {
@@ -553,6 +564,43 @@ function toTurnOutcome(result: RunResult): TurnOutcome {
553
564
  };
554
565
  }
555
566
 
567
+ /** Billing can lag briefly after `wait()`; retry before giving up on cost. */
568
+ const CLOUD_USAGE_RETRY_DELAYS_MS = [0, 400, 1200] as const;
569
+
570
+ /**
571
+ * Cloud `wait()` often omits usage after a stream drop. Pull the run's
572
+ * tokens from the usage API so the engine can still estimate cost.
573
+ * Failures are ignored: a missing estimate is better than failing the turn.
574
+ */
575
+ async function fillMissingCloudUsage(
576
+ agent: SDKAgent,
577
+ runId: string,
578
+ outcome: TurnOutcome
579
+ ): Promise<TurnOutcome> {
580
+ for (const delayMs of CLOUD_USAGE_RETRY_DELAYS_MS) {
581
+ if (delayMs > 0) {
582
+ await sleep(delayMs);
583
+ }
584
+ try {
585
+ const billed = await agent.getUsage({ runId });
586
+ const mapped = mapSdkTokenUsage(billed.usage);
587
+ if (usageTokenSum(mapped) <= 0) {
588
+ continue;
589
+ }
590
+ return { ...outcome, usage: mapped };
591
+ } catch {
592
+ // Transient usage-API failures should still burn the retry budget.
593
+ }
594
+ }
595
+ return outcome;
596
+ }
597
+
598
+ function sleep(ms: number): Promise<void> {
599
+ return new Promise((resolve) => {
600
+ setTimeout(resolve, ms);
601
+ });
602
+ }
603
+
556
604
  export function runErrorMessage(result: Pick<RunResult, "error">): string {
557
605
  return result.error?.message?.trim() || "The Cursor agent run failed";
558
606
  }
@@ -98,10 +98,19 @@ import {
98
98
  requireSessionOwner,
99
99
  } from "./http-channel.js";
100
100
  import { validateToolInput } from "./json-schema.js";
101
+ import {
102
+ createLocalControlPlane,
103
+ type LocalControlPlane,
104
+ } from "./local-control-plane.js";
101
105
  import { createLogRing, type LogRing } from "./log-ring.js";
102
106
  import { createAgentMcpEndpoint, MCP_ENDPOINT_PATH } from "./mcp-endpoint.js";
103
107
  import { isLoopbackAddress } from "./net.js";
104
108
  import { collectPeerConnections } from "./peer-connections.js";
109
+ import {
110
+ PLATFORM_SCHEDULE_CATALOG_FILENAME,
111
+ syncHostedPlatformSchedules,
112
+ } from "./platform-schedule-sync.js";
113
+ import { createCursorHostedPlatformTimers } from "./platform-timers.js";
105
114
  import {
106
115
  type AgentIndexEntry,
107
116
  renderAgentsIndexHtml,
@@ -155,6 +164,7 @@ interface MountedRuntime {
155
164
  engine: SessionEngine;
156
165
  scheduleRunner: ScheduleRunner;
157
166
  reminderRunner: ReminderRunner;
167
+ controlPlane?: LocalControlPlane;
158
168
  /** Playground eval batches (`/v1/dev/evals`); loopbackUrl set after listen. */
159
169
  evalRuns: EvalRunStore;
160
170
  }
@@ -196,6 +206,7 @@ export async function startServer(
196
206
  );
197
207
  const dev = options.dev ?? false;
198
208
  const playground = options.playground ?? true;
209
+ const noControlPlane = options.noControlPlane === true;
199
210
  const runSchedules = options.schedules ?? !dev;
200
211
  const runReminders = options.reminders ?? !dev;
201
212
 
@@ -408,7 +419,21 @@ export async function startServer(
408
419
  githubAuthWarmed = true;
409
420
  }
410
421
  const scheduleRunner = new ScheduleRunner(engine, project, logger);
411
- const reminderRunner = new ReminderRunner(engine, logger);
422
+ let reminderRunner!: ReminderRunner;
423
+ const localControlPlane = noControlPlane
424
+ ? undefined
425
+ : await createLocalControlPlane({
426
+ fireSchedule: (name) => scheduleRunner.dispatch(name),
427
+ fireReminder: (id, fireCount) =>
428
+ reminderRunner.dispatch(id, { expectedFireCount: fireCount }),
429
+ stateDir: join(stateRoot, "control-plane"),
430
+ armReminderClocks: runReminders,
431
+ logger,
432
+ });
433
+ const timerClient = noControlPlane
434
+ ? createCursorHostedPlatformTimers()
435
+ : localControlPlane;
436
+ reminderRunner = new ReminderRunner(engine, logger, timerClient);
412
437
  const evalRuns = new EvalRunStore(
413
438
  project.rootDir,
414
439
  logger,
@@ -443,12 +468,16 @@ export async function startServer(
443
468
  scheduleRunner,
444
469
  reminderRunner,
445
470
  evalRuns,
471
+ ...(localControlPlane === undefined
472
+ ? {}
473
+ : { controlPlane: localControlPlane }),
446
474
  };
447
475
  runtimes.push(runtime);
448
476
  const accountRefs = accountRefsBySlug.get(slug);
449
477
  mountAgentRoutes(router, runtime, {
450
478
  dev,
451
479
  playground,
480
+ noControlPlane,
452
481
  multi,
453
482
  logger,
454
483
  logRing,
@@ -509,12 +538,18 @@ export async function startServer(
509
538
  }
510
539
  }
511
540
  }
512
- if (runSchedules) {
513
- scheduleRunner.start();
541
+ if (timerClient !== undefined && runSchedules) {
542
+ await syncHostedPlatformSchedules({
543
+ timers: timerClient,
544
+ schedules: project.schedules,
545
+ catalogPath: join(stateRoot, PLATFORM_SCHEDULE_CATALOG_FILENAME),
546
+ logger,
547
+ });
514
548
  }
515
- if (runReminders) {
516
- await reminderRunner.start();
549
+ if (timerClient !== undefined) {
550
+ await reminderRunner.hydrate();
517
551
  }
552
+ localControlPlane?.startClocks();
518
553
  }
519
554
 
520
555
  if (docsSite) {
@@ -806,6 +841,7 @@ export async function startServer(
806
841
  await cursorEventRelay?.stop();
807
842
  await cursorSlackRelay?.stop();
808
843
  for (const runtime of runtimes) {
844
+ runtime.controlPlane?.stop();
809
845
  runtime.scheduleRunner.stop();
810
846
  runtime.reminderRunner.stop();
811
847
  // Before the engines close their storage coordinators, so the
@@ -849,6 +885,7 @@ function mountAgentRoutes(
849
885
  args: {
850
886
  dev: boolean;
851
887
  playground: boolean;
888
+ noControlPlane?: boolean;
852
889
  multi: boolean;
853
890
  logger: (line: string) => void;
854
891
  logRing: LogRing;
@@ -860,7 +897,7 @@ function mountAgentRoutes(
860
897
  }
861
898
  ): void {
862
899
  const { slug, project, engine, scheduleRunner, reminderRunner } = runtime;
863
- const { dev, playground, multi, logger, logRing } = args;
900
+ const { dev, playground, noControlPlane, multi, logger, logRing } = args;
864
901
  // "" (single) -> "/v1/..."; "my-agent" (multi) -> "/my-agent/v1/...".
865
902
  const base = slug === "" ? "" : `/${slug}`;
866
903
 
@@ -893,6 +930,18 @@ function mountAgentRoutes(
893
930
  ? [allowAll()]
894
931
  : [localDevStrict()];
895
932
  const httpAuth = project.httpChannel.auth ?? defaultAuth;
933
+ // Temporal posts this marker on a direct pod port-token fetch. The alias
934
+ // proxy strips it, so webhook/playground tokens cannot present it.
935
+ const platformTimerFireAuth: AuthPolicy = (request) => {
936
+ if (request.headers.get("x-agent-serve-timed-work") === "1") {
937
+ return {
938
+ authenticator: "app",
939
+ principalId: "agent-serve:timed-work",
940
+ principalType: "runtime",
941
+ };
942
+ }
943
+ return null;
944
+ };
896
945
  // Playground lists/opens Slack (and other channel) sessions across
897
946
  // principals when `--dev` (loopback) or `--allow-anonymous` (trusted
898
947
  // shared host, e.g. corp Okta ALB). Never when bearer / custom channel
@@ -1294,6 +1343,72 @@ function mountAgentRoutes(
1294
1343
  },
1295
1344
  });
1296
1345
 
1346
+ if (noControlPlane === true) {
1347
+ router.add({
1348
+ method: "POST",
1349
+ pattern: `${base}/v1/internal/schedules/:scheduleId/fire`,
1350
+ auth: [platformTimerFireAuth],
1351
+ handler: async (_request, ctx) => {
1352
+ const scheduleId = ctx.params.scheduleId ?? "";
1353
+ try {
1354
+ const { sessionIds } = await scheduleRunner.dispatch(scheduleId);
1355
+ return jsonResponse({ scheduleId, sessionIds });
1356
+ } catch (error) {
1357
+ if (error instanceof UnknownScheduleError) {
1358
+ return jsonResponse(
1359
+ {
1360
+ ok: false,
1361
+ error: "unknown_schedule",
1362
+ availableScheduleIds: error.availableScheduleIds,
1363
+ },
1364
+ { status: 404 }
1365
+ );
1366
+ }
1367
+ throw error;
1368
+ }
1369
+ },
1370
+ });
1371
+ router.add({
1372
+ method: "POST",
1373
+ pattern: `${base}/v1/internal/reminders/:reminderId/fire`,
1374
+ auth: [platformTimerFireAuth],
1375
+ handler: async (request, ctx) => {
1376
+ const reminderId = ctx.params.reminderId ?? "";
1377
+ const fireCountRaw = new URL(request.url).searchParams.get("fireCount");
1378
+ const expectedFireCount =
1379
+ fireCountRaw === null ? undefined : Number(fireCountRaw);
1380
+ try {
1381
+ const result = await reminderRunner.dispatch(
1382
+ reminderId,
1383
+ expectedFireCount !== undefined &&
1384
+ Number.isInteger(expectedFireCount)
1385
+ ? { expectedFireCount }
1386
+ : {}
1387
+ );
1388
+ return jsonResponse(result);
1389
+ } catch (error) {
1390
+ if (error instanceof UnknownReminderError) {
1391
+ return jsonResponse(
1392
+ {
1393
+ ok: false,
1394
+ error: "unknown_reminder",
1395
+ availableReminderIds: error.availableReminderIds,
1396
+ },
1397
+ { status: 404 }
1398
+ );
1399
+ }
1400
+ if (error instanceof ReminderValidationError) {
1401
+ return jsonResponse(
1402
+ { ok: false, error: "invalid_reminder", message: error.message },
1403
+ { status: 400 }
1404
+ );
1405
+ }
1406
+ throw error;
1407
+ }
1408
+ },
1409
+ });
1410
+ }
1411
+
1297
1412
  if (dev) {
1298
1413
  router.add({
1299
1414
  method: "POST",