remote-codex 0.11.30 → 0.11.32

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 (37) hide show
  1. package/README.md +12 -4
  2. package/apps/relay-server/dist/index.js +2956 -164
  3. package/apps/supervisor-api/dist/index.js +521 -64
  4. package/apps/supervisor-web/dist/apple-touch-icon.png +0 -0
  5. package/apps/supervisor-web/dist/assets/index-CGHHTNkM.js +21 -0
  6. package/apps/supervisor-web/dist/assets/index-CdjTdnJt.css +1 -0
  7. package/apps/supervisor-web/dist/assets/thread-ui-B9eC2H4u.js +3677 -0
  8. package/apps/supervisor-web/dist/favicon-16x16.png +0 -0
  9. package/apps/supervisor-web/dist/favicon-32x32.png +0 -0
  10. package/apps/supervisor-web/dist/favicon-48x48.png +0 -0
  11. package/apps/supervisor-web/dist/icon-192.png +0 -0
  12. package/apps/supervisor-web/dist/icon-512.png +0 -0
  13. package/apps/supervisor-web/dist/index.html +10 -4
  14. package/apps/supervisor-web/dist/remote-codex-icon.png +0 -0
  15. package/apps/supervisor-web/dist/site.webmanifest +19 -0
  16. package/bin/remote-codex.mjs +6 -3
  17. package/config/codex-model-pricing.json +44 -0
  18. package/package.json +1 -1
  19. package/packages/agent-runtime/src/model-pricing.ts +66 -6
  20. package/packages/agent-runtime/src/types.ts +13 -0
  21. package/packages/claude/src/runtimeAdapter.test.ts +64 -0
  22. package/packages/claude/src/runtimeAdapter.ts +64 -0
  23. package/packages/codex/src/appServerManager.ts +16 -0
  24. package/packages/codex/src/modelPricing.test.ts +84 -0
  25. package/packages/codex/src/runtimeAdapter.test.ts +44 -0
  26. package/packages/codex/src/runtimeAdapter.ts +66 -0
  27. package/packages/codex/src/types.ts +3 -1
  28. package/packages/db/migrations/0029_thread_turn_delivery.sql +19 -0
  29. package/packages/db/src/repositories.ts +96 -1
  30. package/packages/db/src/schema.ts +20 -0
  31. package/packages/opencode/src/runtimeAdapter.ts +15 -0
  32. package/packages/shared/src/index.ts +203 -7
  33. package/scripts/run-web-service.mjs +2 -2
  34. package/scripts/service-manager.mjs +2 -2
  35. package/apps/supervisor-web/dist/assets/index-BnpZn_3_.js +0 -6
  36. package/apps/supervisor-web/dist/assets/index-CJFMmjP5.css +0 -1
  37. package/apps/supervisor-web/dist/assets/thread-ui-C0VPL4Uk.js +0 -3677
@@ -291,19 +291,35 @@ function parsePricingConfig(raw) {
291
291
  if (!isPositiveNumber(entry.inputUsdPerMillion) || !isPositiveNumber(entry.cachedInputUsdPerMillion) || !isPositiveNumber(entry.outputUsdPerMillion) || typeof entry.supportsFastMode !== "boolean") {
292
292
  throw new Error(`Pricing config model "${modelKey2}" has invalid fields.`);
293
293
  }
294
+ if (entry.cacheWriteInputUsdPerMillion !== void 0 && !isPositiveNumber(entry.cacheWriteInputUsdPerMillion)) {
295
+ throw new Error(`Pricing config model "${modelKey2}" cacheWriteInputUsdPerMillion must be a non-negative number.`);
296
+ }
294
297
  if (entry.fastMultiplier !== void 0 && !isPositiveNumber(entry.fastMultiplier)) {
295
298
  throw new Error(`Pricing config model "${modelKey2}" fastMultiplier must be a non-negative number.`);
296
299
  }
297
300
  if (entry.contextWindowTokens !== void 0 && !isPositiveNumber(entry.contextWindowTokens)) {
298
301
  throw new Error(`Pricing config model "${modelKey2}" contextWindowTokens must be a non-negative number.`);
299
302
  }
303
+ for (const field of [
304
+ "longContextThresholdTokens",
305
+ "longContextInputMultiplier",
306
+ "longContextOutputMultiplier"
307
+ ]) {
308
+ if (entry[field] !== void 0 && !isPositiveNumber(entry[field])) {
309
+ throw new Error(`Pricing config model "${modelKey2}" ${field} must be a non-negative number.`);
310
+ }
311
+ }
300
312
  models[modelKey2] = {
301
313
  inputUsdPerMillion: entry.inputUsdPerMillion,
302
314
  cachedInputUsdPerMillion: entry.cachedInputUsdPerMillion,
315
+ ...entry.cacheWriteInputUsdPerMillion !== void 0 ? { cacheWriteInputUsdPerMillion: entry.cacheWriteInputUsdPerMillion } : {},
303
316
  outputUsdPerMillion: entry.outputUsdPerMillion,
304
317
  supportsFastMode: entry.supportsFastMode,
305
318
  ...entry.fastMultiplier !== void 0 ? { fastMultiplier: entry.fastMultiplier } : {},
306
- ...entry.contextWindowTokens !== void 0 ? { contextWindowTokens: entry.contextWindowTokens } : {}
319
+ ...entry.contextWindowTokens !== void 0 ? { contextWindowTokens: entry.contextWindowTokens } : {},
320
+ ...entry.longContextThresholdTokens !== void 0 ? { longContextThresholdTokens: entry.longContextThresholdTokens } : {},
321
+ ...entry.longContextInputMultiplier !== void 0 ? { longContextInputMultiplier: entry.longContextInputMultiplier } : {},
322
+ ...entry.longContextOutputMultiplier !== void 0 ? { longContextOutputMultiplier: entry.longContextOutputMultiplier } : {}
307
323
  };
308
324
  }
309
325
  return {
@@ -396,23 +412,32 @@ function estimateTurnPrice(usage, snapshot) {
396
412
  return null;
397
413
  }
398
414
  const nonCachedInputTokens = Math.max(
399
- usage.total.inputTokens - usage.total.cachedInputTokens,
415
+ usage.total.inputTokens - usage.total.cachedInputTokens - (usage.total.cacheWriteInputTokens ?? 0),
400
416
  0
401
417
  );
402
418
  const cachedInputTokens = Math.max(usage.total.cachedInputTokens, 0);
419
+ const cacheWriteInputTokens = Math.max(
420
+ usage.total.cacheWriteInputTokens ?? 0,
421
+ 0
422
+ );
403
423
  const outputTokens = Math.max(usage.total.outputTokens, 0);
404
- const multiplier = tierKey === "fast" && modelPricing.fastMultiplier !== void 0 ? modelPricing.fastMultiplier : tier.multiplier;
405
- const inputUsd = nonCachedInputTokens * modelPricing.inputUsdPerMillion * multiplier / TOKEN_PRICE_DENOMINATOR;
406
- const cachedInputUsd = cachedInputTokens * modelPricing.cachedInputUsdPerMillion * multiplier / TOKEN_PRICE_DENOMINATOR;
407
- const outputUsd = outputTokens * modelPricing.outputUsdPerMillion * multiplier / TOKEN_PRICE_DENOMINATOR;
424
+ const tierMultiplier = tierKey === "fast" && modelPricing.fastMultiplier !== void 0 ? modelPricing.fastMultiplier : tier.multiplier;
425
+ const usesLongContextPricing = typeof modelPricing.longContextThresholdTokens === "number" && usage.total.inputTokens > modelPricing.longContextThresholdTokens;
426
+ const inputMultiplier = tierMultiplier * (usesLongContextPricing ? modelPricing.longContextInputMultiplier ?? 1 : 1);
427
+ const outputMultiplier = tierMultiplier * (usesLongContextPricing ? modelPricing.longContextOutputMultiplier ?? 1 : 1);
428
+ const inputUsd = nonCachedInputTokens * modelPricing.inputUsdPerMillion * inputMultiplier / TOKEN_PRICE_DENOMINATOR;
429
+ const cachedInputUsd = cachedInputTokens * modelPricing.cachedInputUsdPerMillion * inputMultiplier / TOKEN_PRICE_DENOMINATOR;
430
+ const cacheWriteInputUsd = cacheWriteInputTokens * (modelPricing.cacheWriteInputUsdPerMillion ?? modelPricing.inputUsdPerMillion) * inputMultiplier / TOKEN_PRICE_DENOMINATOR;
431
+ const outputUsd = outputTokens * modelPricing.outputUsdPerMillion * outputMultiplier / TOKEN_PRICE_DENOMINATOR;
408
432
  return {
409
433
  pricingModelKey,
410
434
  pricingTierKey: tierKey,
411
435
  currency: pricingConfig.currency,
412
436
  inputUsd,
413
437
  cachedInputUsd,
438
+ cacheWriteInputUsd,
414
439
  outputUsd,
415
- totalUsd: inputUsd + cachedInputUsd + outputUsd
440
+ totalUsd: inputUsd + cachedInputUsd + cacheWriteInputUsd + outputUsd
416
441
  };
417
442
  }
418
443
 
@@ -6357,6 +6382,7 @@ __export(schema_exports, {
6357
6382
  threadGoals: () => threadGoals,
6358
6383
  threadHistoryItems: () => threadHistoryItems,
6359
6384
  threadPendingSteers: () => threadPendingSteers,
6385
+ threadPromptRequests: () => threadPromptRequests,
6360
6386
  threadTurnMetadata: () => threadTurnMetadata,
6361
6387
  threads: () => threads,
6362
6388
  viewerSessions: () => viewerSessions,
@@ -6393,6 +6419,7 @@ var threads = sqliteTable("threads", {
6393
6419
  fastBaseModel: text("fast_base_model"),
6394
6420
  fastBaseReasoningEffort: text("fast_base_reasoning_effort"),
6395
6421
  collaborationMode: text("collaboration_mode").notNull().default("default"),
6422
+ activeTurnCollaborationMode: text("active_turn_collaboration_mode"),
6396
6423
  approvalMode: text("approval_mode"),
6397
6424
  sandboxMode: text("sandbox_mode"),
6398
6425
  status: text("status"),
@@ -6468,9 +6495,27 @@ var threadPendingSteers = sqliteTable("thread_pending_steers", {
6468
6495
  clientRequestId: text("client_request_id"),
6469
6496
  displayPrompt: text("display_prompt").notNull(),
6470
6497
  submittedPrompt: text("submitted_prompt").notNull(),
6498
+ delivery: text("delivery").notNull().default("steer"),
6499
+ turnConfigJson: text("turn_config_json"),
6471
6500
  createdAt: text("created_at").notNull(),
6472
6501
  updatedAt: text("updated_at").notNull()
6473
6502
  });
6503
+ var threadPromptRequests = sqliteTable(
6504
+ "thread_prompt_requests",
6505
+ {
6506
+ id: text("id").primaryKey(),
6507
+ threadId: text("thread_id").notNull(),
6508
+ clientRequestId: text("client_request_id").notNull(),
6509
+ status: text("status").notNull(),
6510
+ createdAt: text("created_at").notNull(),
6511
+ updatedAt: text("updated_at").notNull()
6512
+ },
6513
+ (table) => ({
6514
+ threadClientRequestUnique: uniqueIndex(
6515
+ "thread_prompt_requests_thread_client_request_idx"
6516
+ ).on(table.threadId, table.clientRequestId)
6517
+ })
6518
+ );
6474
6519
  var threadHistoryItems = sqliteTable(
6475
6520
  "thread_history_items",
6476
6521
  {
@@ -6704,6 +6749,7 @@ function createThreadRecord(db, input) {
6704
6749
  fastBaseModel: input.fastBaseModel ?? null,
6705
6750
  fastBaseReasoningEffort: input.fastBaseReasoningEffort ?? null,
6706
6751
  collaborationMode: input.collaborationMode ?? "default",
6752
+ activeTurnCollaborationMode: input.activeTurnCollaborationMode ?? null,
6707
6753
  approvalMode: input.approvalMode,
6708
6754
  sandboxMode: input.sandboxMode ?? null,
6709
6755
  status: "idle",
@@ -6843,6 +6889,8 @@ function createThreadPendingSteerRecord(db, input) {
6843
6889
  clientRequestId: input.clientRequestId ?? null,
6844
6890
  displayPrompt: input.displayPrompt,
6845
6891
  submittedPrompt: input.submittedPrompt,
6892
+ delivery: input.delivery ?? "steer",
6893
+ turnConfigJson: input.turnConfigJson ?? null,
6846
6894
  createdAt: now,
6847
6895
  updatedAt: now
6848
6896
  };
@@ -6855,6 +6903,48 @@ function deleteThreadPendingSteerRecordById(db, id) {
6855
6903
  function deleteThreadPendingSteerRecordsByThreadId(db, threadId) {
6856
6904
  db.delete(threadPendingSteers).where(eq(threadPendingSteers.threadId, threadId)).run();
6857
6905
  }
6906
+ function getThreadPromptRequestRecord(db, threadId, clientRequestId) {
6907
+ return db.select().from(threadPromptRequests).where(
6908
+ and(
6909
+ eq(threadPromptRequests.threadId, threadId),
6910
+ eq(threadPromptRequests.clientRequestId, clientRequestId)
6911
+ )
6912
+ ).get();
6913
+ }
6914
+ function createThreadPromptRequestRecord(db, threadId, clientRequestId) {
6915
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6916
+ db.insert(threadPromptRequests).values({
6917
+ id: randomUUID(),
6918
+ threadId,
6919
+ clientRequestId,
6920
+ status: "processing",
6921
+ createdAt: now,
6922
+ updatedAt: now
6923
+ }).onConflictDoNothing().run();
6924
+ return getThreadPromptRequestRecord(db, threadId, clientRequestId);
6925
+ }
6926
+ function markThreadPromptRequestAccepted(db, threadId, clientRequestId) {
6927
+ db.update(threadPromptRequests).set({ status: "accepted", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
6928
+ and(
6929
+ eq(threadPromptRequests.threadId, threadId),
6930
+ eq(threadPromptRequests.clientRequestId, clientRequestId)
6931
+ )
6932
+ ).run();
6933
+ }
6934
+ function deleteExpiredThreadPromptRequestRecords(db, cutoff) {
6935
+ return db.delete(threadPromptRequests).where(lt(threadPromptRequests.updatedAt, cutoff)).run();
6936
+ }
6937
+ function deleteThreadPromptRequestRecord(db, threadId, clientRequestId) {
6938
+ db.delete(threadPromptRequests).where(
6939
+ and(
6940
+ eq(threadPromptRequests.threadId, threadId),
6941
+ eq(threadPromptRequests.clientRequestId, clientRequestId)
6942
+ )
6943
+ ).run();
6944
+ }
6945
+ function deleteThreadPromptRequestRecordsByThreadId(db, threadId) {
6946
+ db.delete(threadPromptRequests).where(eq(threadPromptRequests.threadId, threadId)).run();
6947
+ }
6858
6948
  function listThreadActivityNotesByThreadId(db, threadId) {
6859
6949
  return db.select().from(threadActivityNotes).where(eq(threadActivityNotes.threadId, threadId)).orderBy(threadActivityNotes.createdAt).all();
6860
6950
  }
@@ -7838,6 +7928,14 @@ var CodexAppServerManager = class extends EventEmitter3 {
7838
7928
  });
7839
7929
  return response.data.map(mapModel);
7840
7930
  }
7931
+ async readAccount() {
7932
+ await this.ensureReady();
7933
+ return this.client.request("account/read", { refreshToken: false });
7934
+ }
7935
+ async readAccountRateLimits() {
7936
+ await this.ensureReady();
7937
+ return this.client.request("account/rateLimits/read", null);
7938
+ }
7841
7939
  async listThreads() {
7842
7940
  await this.ensureReady();
7843
7941
  const response = await this.client.request("thread/list", {
@@ -10525,6 +10623,16 @@ function mapCodexNotification(event) {
10525
10623
  return null;
10526
10624
  }
10527
10625
  }
10626
+ function formatRateLimitWindowLabel(durationMinutes, fallback) {
10627
+ if (durationMinutes === null) return fallback;
10628
+ if (durationMinutes % (60 * 24) === 0) {
10629
+ return `${durationMinutes / (60 * 24)}d`;
10630
+ }
10631
+ if (durationMinutes % 60 === 0) {
10632
+ return `${durationMinutes / 60}h`;
10633
+ }
10634
+ return `${durationMinutes}m`;
10635
+ }
10528
10636
  function mapCodexRuntimeError(error) {
10529
10637
  if (error instanceof AgentRuntimeError) {
10530
10638
  throw error;
@@ -10642,6 +10750,51 @@ var CodexRuntimeAdapter = class extends EventEmitter4 {
10642
10750
  getStatus() {
10643
10751
  return mapStatus(this.manager.getStatus());
10644
10752
  }
10753
+ async getSubscriptionUsage() {
10754
+ const account = await codexRuntimeCall(() => this.manager.readAccount());
10755
+ if (account.account?.type === "apiKey") {
10756
+ return {
10757
+ provider: "codex",
10758
+ authKind: "apiKey",
10759
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
10760
+ stale: false,
10761
+ windows: []
10762
+ };
10763
+ }
10764
+ if (account.account?.type !== "chatgpt") {
10765
+ return null;
10766
+ }
10767
+ const response = await codexRuntimeCall(
10768
+ () => this.manager.readAccountRateLimits()
10769
+ );
10770
+ const buckets = response.rateLimitsByLimitId;
10771
+ const snapshot = (buckets && (buckets.codex ?? Object.values(buckets)[0])) ?? response.rateLimits;
10772
+ const record = snapshot && typeof snapshot === "object" ? snapshot : {};
10773
+ const windows = ["primary", "secondary"].flatMap((id) => {
10774
+ const value = record[id];
10775
+ if (!value || typeof value !== "object") return [];
10776
+ const window = value;
10777
+ const usedPercent = Number(window.usedPercent);
10778
+ if (!Number.isFinite(usedPercent)) return [];
10779
+ const duration = Number(window.windowDurationMins);
10780
+ const durationMinutes = Number.isFinite(duration) ? duration : null;
10781
+ const resetsAt = Number(window.resetsAt);
10782
+ return [{
10783
+ id,
10784
+ durationMinutes,
10785
+ label: formatRateLimitWindowLabel(durationMinutes, id),
10786
+ usedPercent: Math.max(0, Math.min(100, usedPercent)),
10787
+ resetsAt: Number.isFinite(resetsAt) ? new Date(resetsAt * 1e3).toISOString() : null
10788
+ }];
10789
+ });
10790
+ return {
10791
+ provider: "codex",
10792
+ authKind: "subscription",
10793
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
10794
+ stale: false,
10795
+ windows
10796
+ };
10797
+ }
10645
10798
  start() {
10646
10799
  return codexRuntimeCall(() => this.manager.start());
10647
10800
  }
@@ -11970,6 +12123,7 @@ function addClaudeUsage(left, right) {
11970
12123
  totalTokens: left.totalTokens + right.totalTokens,
11971
12124
  inputTokens: left.inputTokens + right.inputTokens,
11972
12125
  cachedInputTokens: left.cachedInputTokens + right.cachedInputTokens,
12126
+ cacheWriteInputTokens: left.cacheWriteInputTokens + right.cacheWriteInputTokens,
11973
12127
  outputTokens: left.outputTokens + right.outputTokens,
11974
12128
  reasoningOutputTokens: left.reasoningOutputTokens + right.reasoningOutputTokens
11975
12129
  };
@@ -11995,6 +12149,7 @@ function normalizeClaudeUsage(value) {
11995
12149
  totalTokens: totalTokens2,
11996
12150
  inputTokens,
11997
12151
  cachedInputTokens: cacheReadInputTokens,
12152
+ cacheWriteInputTokens: cacheCreationInputTokens,
11998
12153
  outputTokens,
11999
12154
  reasoningOutputTokens: 0
12000
12155
  };
@@ -12346,11 +12501,44 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12346
12501
  sessionModels = /* @__PURE__ */ new Map();
12347
12502
  sessionApprovalModes = /* @__PURE__ */ new Map();
12348
12503
  liveUserPrompts = /* @__PURE__ */ new Map();
12504
+ subscriptionUsageWindows = /* @__PURE__ */ new Map();
12505
+ subscriptionUsageObservedAt = null;
12349
12506
  clientApp;
12350
12507
  sdkLoadError = null;
12351
12508
  getStatus() {
12352
12509
  return { ...this.status };
12353
12510
  }
12511
+ async getSubscriptionUsage() {
12512
+ const observedAt = this.subscriptionUsageObservedAt ?? (/* @__PURE__ */ new Date()).toISOString();
12513
+ const windows = [...this.subscriptionUsageWindows.entries()].map(([id, window]) => ({
12514
+ id,
12515
+ durationMinutes: id === "five_hour" ? 300 : 10080,
12516
+ label: id === "five_hour" ? "5h" : "7d",
12517
+ usedPercent: window.usedPercent,
12518
+ resetsAt: window.resetsAt
12519
+ }));
12520
+ return {
12521
+ provider: "claude",
12522
+ authKind: windows.length > 0 ? "subscription" : "unknown",
12523
+ observedAt,
12524
+ stale: false,
12525
+ windows
12526
+ };
12527
+ }
12528
+ captureRateLimit(message) {
12529
+ if (message.type !== "rate_limit_event") {
12530
+ return;
12531
+ }
12532
+ const info = message.rate_limit_info;
12533
+ if (!info || info.rateLimitType !== "five_hour" && info.rateLimitType !== "seven_day" || typeof info.utilization !== "number" || !Number.isFinite(info.utilization)) {
12534
+ return;
12535
+ }
12536
+ this.subscriptionUsageWindows.set(info.rateLimitType, {
12537
+ usedPercent: Math.max(0, Math.min(100, info.utilization * 100)),
12538
+ resetsAt: typeof info.resetsAt === "number" ? new Date(info.resetsAt * 1e3).toISOString() : null
12539
+ });
12540
+ this.subscriptionUsageObservedAt = (/* @__PURE__ */ new Date()).toISOString();
12541
+ }
12354
12542
  updateToolboxItemsFromSystemInit(message) {
12355
12543
  this.managementSchema.toolboxItems = buildClaudeToolboxItems(
12356
12544
  normalizeClaudeSlashCommands(message.slash_commands)
@@ -12498,6 +12686,7 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12498
12686
  try {
12499
12687
  for await (const message of query) {
12500
12688
  rawMessages.push(message);
12689
+ this.captureRateLimit(message);
12501
12690
  if (message.type === "system" && message.subtype === "init") {
12502
12691
  this.updateToolboxItemsFromSystemInit(message);
12503
12692
  const sessionId = message.session_id;
@@ -12745,6 +12934,7 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12745
12934
  try {
12746
12935
  for await (const message of state.query) {
12747
12936
  rawMessages.push(message);
12937
+ this.captureRateLimit(message);
12748
12938
  this.consumeMessage(state, message);
12749
12939
  const status = queryResultStatus(message);
12750
12940
  if (status) {
@@ -14278,6 +14468,9 @@ function openCodeUsageFromTokens(tokens) {
14278
14468
  const cachedInputTokens = nonNegativeNumberValue(
14279
14469
  tokens.cachedInputTokens ?? tokens.cached_input_tokens ?? cache?.read
14280
14470
  ) ?? 0;
14471
+ const cacheWriteInputTokens = nonNegativeNumberValue(
14472
+ tokens.cacheWriteInputTokens ?? tokens.cache_write_input_tokens ?? tokens.cacheWriteTokens ?? tokens.cache_write_tokens ?? cache?.write
14473
+ ) ?? 0;
14281
14474
  const totalTokens2 = nonNegativeNumberValue(tokens.total ?? tokens.totalTokens ?? tokens.total_tokens) ?? inputTokens + outputTokens;
14282
14475
  if (totalTokens2 <= 0) {
14283
14476
  return null;
@@ -14287,6 +14480,7 @@ function openCodeUsageFromTokens(tokens) {
14287
14480
  totalTokens: totalTokens2,
14288
14481
  inputTokens,
14289
14482
  cachedInputTokens,
14483
+ ...cacheWriteInputTokens > 0 ? { cacheWriteInputTokens } : {},
14290
14484
  outputTokens,
14291
14485
  reasoningOutputTokens
14292
14486
  },
@@ -14344,6 +14538,9 @@ function turnTokenUsage(messages, model) {
14344
14538
  totalTokens: sum.totalTokens + usage.totalTokens,
14345
14539
  inputTokens: sum.inputTokens + usage.inputTokens,
14346
14540
  cachedInputTokens: sum.cachedInputTokens + usage.cachedInputTokens,
14541
+ ...(sum.cacheWriteInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) > 0 ? {
14542
+ cacheWriteInputTokens: (sum.cacheWriteInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0)
14543
+ } : {},
14347
14544
  outputTokens: sum.outputTokens + usage.outputTokens,
14348
14545
  reasoningOutputTokens: sum.reasoningOutputTokens + usage.reasoningOutputTokens
14349
14546
  }), firstRecord);
@@ -16040,6 +16237,7 @@ var ThreadAuxiliaryStateStore = class {
16040
16237
  clientRequestId: record.clientRequestId ?? null,
16041
16238
  turnId: record.turnId,
16042
16239
  prompt: record.displayPrompt,
16240
+ delivery: record.delivery === "continuation" ? "continuation" : "steer",
16043
16241
  createdAt: record.createdAt
16044
16242
  }));
16045
16243
  }
@@ -16056,6 +16254,11 @@ var ThreadAuxiliaryStateStore = class {
16056
16254
  hasPendingSteersForTurn(localThreadId, turnId) {
16057
16255
  return this.listPendingSteerRecordsForTurn(localThreadId, turnId).length > 0;
16058
16256
  }
16257
+ hasQueuedContinuationsForTurn(localThreadId, turnId) {
16258
+ return this.listPendingSteerRecordsForTurn(localThreadId, turnId).some(
16259
+ (record) => record.delivery === "continuation"
16260
+ );
16261
+ }
16059
16262
  deletePendingSteerRecord(localThreadId, id, turnId) {
16060
16263
  deleteThreadPendingSteerRecordById(this.db, id);
16061
16264
  this.callbacks.invalidateThreadDetailCache(localThreadId);
@@ -17311,11 +17514,18 @@ function shouldResetThreadContextUsageForTurnStart(current) {
17311
17514
  }
17312
17515
  function buildTurnTokenBreakdown(payload) {
17313
17516
  const usage = isRecord12(payload) ? payload : null;
17517
+ const inputDetails = isRecord12(
17518
+ usage?.inputTokensDetails ?? usage?.input_tokens_details
17519
+ ) ? usage?.inputTokensDetails ?? usage?.input_tokens_details : null;
17520
+ const cache = isRecord12(usage?.cache) ? usage.cache : null;
17314
17521
  const totalTokens2 = numberOrNull2(usage?.totalTokens ?? usage?.total_tokens);
17315
17522
  const inputTokens = numberOrNull2(usage?.inputTokens ?? usage?.input_tokens);
17316
17523
  const cachedInputTokens = numberOrNull2(
17317
- usage?.cachedInputTokens ?? usage?.cached_input_tokens
17524
+ usage?.cachedInputTokens ?? usage?.cached_input_tokens ?? inputDetails?.cachedTokens ?? inputDetails?.cached_tokens ?? cache?.read
17318
17525
  );
17526
+ const cacheWriteInputTokens = numberOrNull2(
17527
+ usage?.cacheWriteInputTokens ?? usage?.cache_write_input_tokens ?? usage?.cacheWriteTokens ?? usage?.cache_write_tokens ?? usage?.cacheCreationInputTokens ?? usage?.cache_creation_input_tokens ?? inputDetails?.cacheWriteTokens ?? inputDetails?.cache_write_tokens ?? cache?.write
17528
+ ) ?? 0;
17319
17529
  const outputTokens = numberOrNull2(usage?.outputTokens ?? usage?.output_tokens);
17320
17530
  const reasoningOutputTokens = numberOrNull2(
17321
17531
  usage?.reasoningOutputTokens ?? usage?.reasoning_output_tokens
@@ -17327,6 +17537,7 @@ function buildTurnTokenBreakdown(payload) {
17327
17537
  totalTokens: totalTokens2,
17328
17538
  inputTokens,
17329
17539
  cachedInputTokens,
17540
+ cacheWriteInputTokens,
17330
17541
  outputTokens,
17331
17542
  reasoningOutputTokens
17332
17543
  };
@@ -17336,6 +17547,7 @@ function zeroTurnTokenBreakdown() {
17336
17547
  totalTokens: 0,
17337
17548
  inputTokens: 0,
17338
17549
  cachedInputTokens: 0,
17550
+ cacheWriteInputTokens: 0,
17339
17551
  outputTokens: 0,
17340
17552
  reasoningOutputTokens: 0
17341
17553
  };
@@ -17348,6 +17560,10 @@ function subtractTurnTokenBreakdowns(current, previous) {
17348
17560
  current.cachedInputTokens - previous.cachedInputTokens,
17349
17561
  0
17350
17562
  ),
17563
+ cacheWriteInputTokens: Math.max(
17564
+ current.cacheWriteInputTokens - previous.cacheWriteInputTokens,
17565
+ 0
17566
+ ),
17351
17567
  outputTokens: Math.max(current.outputTokens - previous.outputTokens, 0),
17352
17568
  reasoningOutputTokens: Math.max(
17353
17569
  current.reasoningOutputTokens - previous.reasoningOutputTokens,
@@ -17406,12 +17622,16 @@ function cumulativeTotalFromStoredThreadTurnTokenUsageState(state) {
17406
17622
  return null;
17407
17623
  }
17408
17624
  if (!state.baselineTotal) {
17409
- return state.usage.total;
17625
+ return {
17626
+ ...state.usage.total,
17627
+ cacheWriteInputTokens: state.usage.total.cacheWriteInputTokens ?? 0
17628
+ };
17410
17629
  }
17411
17630
  return {
17412
17631
  totalTokens: state.baselineTotal.totalTokens + state.usage.total.totalTokens,
17413
17632
  inputTokens: state.baselineTotal.inputTokens + state.usage.total.inputTokens,
17414
17633
  cachedInputTokens: state.baselineTotal.cachedInputTokens + state.usage.total.cachedInputTokens,
17634
+ cacheWriteInputTokens: state.baselineTotal.cacheWriteInputTokens + (state.usage.total.cacheWriteInputTokens ?? 0),
17415
17635
  outputTokens: state.baselineTotal.outputTokens + state.usage.total.outputTokens,
17416
17636
  reasoningOutputTokens: state.baselineTotal.reasoningOutputTokens + state.usage.total.reasoningOutputTokens
17417
17637
  };
@@ -17838,6 +18058,7 @@ var ThreadRuntimeEventProjector = class {
17838
18058
  const turnItems = event.turn.items;
17839
18059
  updateThreadRecord(db, record.id, {
17840
18060
  providerTurnId: null,
18061
+ activeTurnCollaborationMode: null,
17841
18062
  status: event.turn.status === "failed" ? "failed" : event.turn.status === "interrupted" ? "interrupted" : "idle",
17842
18063
  lastError: event.turn.error?.message ?? null,
17843
18064
  lastTurnCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -17859,7 +18080,9 @@ var ThreadRuntimeEventProjector = class {
17859
18080
  }
17860
18081
  }
17861
18082
  callbacks.clearTerminalPendingRequests(record.id, true);
17862
- if (event.turn.status === "completed" && callbacks.normalizeCollaborationMode(record.collaborationMode) === "plan" && turnItems.some((item) => item.kind === "plan") && !callbacks.hasPendingAskUserQuestion(record.id)) {
18083
+ if (event.turn.status === "completed" && !preservePendingSteers && callbacks.normalizeCollaborationMode(
18084
+ record.activeTurnCollaborationMode ?? record.collaborationMode
18085
+ ) === "plan" && turnItems.some((item) => item.kind === "plan") && !callbacks.hasPendingAskUserQuestion(record.id)) {
17863
18086
  callbacks.createPendingPlanDecisionRequest(record.id, turnId, true);
17864
18087
  } else {
17865
18088
  callbacks.dismissPlanDecisionTurn(record.id);
@@ -17891,6 +18114,7 @@ var ThreadRuntimeEventProjector = class {
17891
18114
  const turnId = liveState.displayTurnIdForRuntimeTurn(record.id, event.providerTurnId) ?? event.providerTurnId;
17892
18115
  updateThreadRecord(db, record.id, {
17893
18116
  providerTurnId: null,
18117
+ activeTurnCollaborationMode: null,
17894
18118
  status: "failed",
17895
18119
  lastError: event.error,
17896
18120
  lastTurnCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -18763,6 +18987,7 @@ function normalizeReasoningEffort(value) {
18763
18987
  case "high":
18764
18988
  case "xhigh":
18765
18989
  case "max":
18990
+ case "ultra":
18766
18991
  return value;
18767
18992
  default:
18768
18993
  return null;
@@ -18779,6 +19004,7 @@ function normalizeReasoningEffort2(value) {
18779
19004
  case "high":
18780
19005
  case "xhigh":
18781
19006
  case "max":
19007
+ case "ultra":
18782
19008
  return value;
18783
19009
  default:
18784
19010
  return null;
@@ -19221,6 +19447,7 @@ var ThreadPromptTurnCoordinator = class {
19221
19447
  model: input.effectiveModel,
19222
19448
  reasoningEffort: input.normalizedReasoning,
19223
19449
  collaborationMode: input.collaborationMode,
19450
+ activeTurnCollaborationMode: input.collaborationMode,
19224
19451
  sandboxMode: input.sandboxMode
19225
19452
  };
19226
19453
  if (isAutoGeneratedTitle(record.title)) {
@@ -19343,7 +19570,8 @@ var ThreadPromptTurnCoordinator = class {
19343
19570
  turnId: steerTurnId,
19344
19571
  clientRequestId: input.clientRequestId,
19345
19572
  displayPrompt: input.displayPrompt,
19346
- submittedPrompt: input.prompt
19573
+ submittedPrompt: input.prompt,
19574
+ delivery: "steer"
19347
19575
  });
19348
19576
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19349
19577
  this.callbacks.emitThreadUpdated(localThreadId, {
@@ -19392,7 +19620,16 @@ var ThreadPromptTurnCoordinator = class {
19392
19620
  turnId: displayTurnId,
19393
19621
  clientRequestId: input.clientRequestId,
19394
19622
  displayPrompt: input.displayPrompt,
19395
- submittedPrompt: input.prompt
19623
+ submittedPrompt: input.prompt,
19624
+ delivery: "continuation",
19625
+ turnConfigJson: JSON.stringify({
19626
+ effectiveModel: input.effectiveModel,
19627
+ normalizedReasoning: input.normalizedReasoning,
19628
+ collaborationMode: input.collaborationMode,
19629
+ sandboxMode: input.sandboxMode,
19630
+ performanceMode: input.performanceMode,
19631
+ startNewTurn: input.collaborationMode !== (record.activeTurnCollaborationMode === "plan" ? "plan" : "default")
19632
+ })
19396
19633
  });
19397
19634
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19398
19635
  this.callbacks.emitThreadUpdated(localThreadId, {
@@ -19598,7 +19835,7 @@ var ThreadSessionCoordinator = class {
19598
19835
  if (!isRemoteThreadBootstrapError(error)) {
19599
19836
  throw error;
19600
19837
  }
19601
- return { status: "bootstrap_unavailable" };
19838
+ return { status: "bootstrap_unavailable", error };
19602
19839
  }
19603
19840
  const effectiveModel = input.resumeInput.model ?? input.currentModel ?? response.model ?? null;
19604
19841
  const resumedReasoning = this.providerRuntime.normalizeReasoningForModel(
@@ -19774,6 +20011,37 @@ var ThreadSessionLifecycleCoordinator = class {
19774
20011
  fastMode: record.fastMode
19775
20012
  });
19776
20013
  if (resumed.status === "bootstrap_unavailable") {
20014
+ if (!this.canRecreateUnmaterializedThread(record, resumed.error)) {
20015
+ return;
20016
+ }
20017
+ const workspace = getWorkspaceRecordById(this.db, record.workspaceId);
20018
+ const model = input.model ?? record.model;
20019
+ if (!workspace || !model) {
20020
+ return;
20021
+ }
20022
+ const recreated = await this.sessionCoordinator.startThreadSession({
20023
+ workspacePath: workspace.absPath,
20024
+ threadInput: {
20025
+ workspaceId: workspace.id,
20026
+ title: record.title,
20027
+ provider: record.provider,
20028
+ model,
20029
+ reasoningEffort: record.reasoningEffort,
20030
+ approvalMode: record.approvalMode ?? "yolo"
20031
+ },
20032
+ defaultTitle: record.title
20033
+ });
20034
+ updateThreadRecord(this.db, record.id, {
20035
+ ...buildThreadPatch(
20036
+ recreated.response.session,
20037
+ model,
20038
+ recreated.response.reasoningEffort ?? recreated.reasoningEffort
20039
+ ),
20040
+ providerSessionId: recreated.response.providerSessionId,
20041
+ sandboxMode: recreated.sandboxMode,
20042
+ isConnected: true
20043
+ });
20044
+ this.callbacks.invalidateThreadDetailCache(localThreadId);
19777
20045
  return;
19778
20046
  }
19779
20047
  updateThreadRecord(
@@ -19797,6 +20065,9 @@ var ThreadSessionLifecycleCoordinator = class {
19797
20065
  }
19798
20066
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19799
20067
  }
20068
+ canRecreateUnmaterializedThread(record, error) {
20069
+ return record.provider === "codex" && record.source === "supervisor" && listThreadTurnMetadataByThreadId(this.db, record.id).length === 0 && error instanceof AgentRuntimeError && error.provider === "codex" && error.code === "remote_error" && /thread not loaded|no rollout found/i.test(error.message);
20070
+ }
19800
20071
  disconnectThread(localThreadId) {
19801
20072
  const record = getThreadRecordById(this.db, localThreadId);
19802
20073
  if (!record) {
@@ -19996,6 +20267,7 @@ var ThreadDeletionCoordinator = class {
19996
20267
  deleteThreadGoalRecordsByThreadId(this.db, localThreadId);
19997
20268
  deleteThreadHistoryItemRecordsByThreadId(this.db, localThreadId);
19998
20269
  deleteThreadPendingSteerRecordsByThreadId(this.db, localThreadId);
20270
+ deleteThreadPromptRequestRecordsByThreadId(this.db, localThreadId);
19999
20271
  deleteThreadTurnMetadataByThreadId(this.db, localThreadId);
20000
20272
  deleteThreadRecord(this.db, localThreadId);
20001
20273
  return { id: localThreadId };
@@ -21921,7 +22193,7 @@ var ThreadService = class {
21921
22193
  normalizeCollaborationMode,
21922
22194
  normalizeReasoningEffort: normalizeReasoningEffort2,
21923
22195
  normalizeThreadGoalStatusForThread: (goal, record) => this.goalCoordinator.normalizeThreadGoalStatusForThread(goal, record),
21924
- shouldPreservePendingSteersForCompletedTurn: (record, turnId) => !this.runtimeSupportsLiveRunningTurnInput(record.provider) && this.auxiliaryState.hasPendingSteersForTurn(record.id, turnId),
22196
+ shouldPreservePendingSteersForCompletedTurn: (record, turnId) => this.shouldPreserveCompletedPendingSteer(record.id, turnId),
21925
22197
  scheduleQueuedContinuationDrain: (localThreadId, turnId) => this.scheduleQueuedContinuationDrain(localThreadId, turnId),
21926
22198
  persistLiveHistoryItem: (localThreadId, turnId, item) => this.historyPersistence.persistLiveHistoryItem(localThreadId, turnId, item),
21927
22199
  persistFinalTurnOrderingHints: (localThreadId, turnId, items) => this.historyPersistence.persistFinalTurnOrderingHints(localThreadId, turnId, items),
@@ -21955,6 +22227,7 @@ var ThreadService = class {
21955
22227
  config;
21956
22228
  liveState = new ThreadLiveStateStore();
21957
22229
  queuedContinuationDrains = /* @__PURE__ */ new Set();
22230
+ promptRequestsInFlight = /* @__PURE__ */ new Map();
21958
22231
  detailAssembler;
21959
22232
  usageAccounting;
21960
22233
  requestCoordinator;
@@ -22273,6 +22546,57 @@ var ThreadService = class {
22273
22546
  return this.getThreadDetail(localThreadId);
22274
22547
  }
22275
22548
  async sendPrompt(localThreadId, input, options = {}) {
22549
+ const clientRequestId = input.clientRequestId?.trim();
22550
+ if (!clientRequestId) {
22551
+ return this.sendPromptOnce(localThreadId, input, options);
22552
+ }
22553
+ const requestKey = `${localThreadId}:${clientRequestId}`;
22554
+ const activeRequest = this.promptRequestsInFlight.get(requestKey);
22555
+ if (activeRequest) {
22556
+ return activeRequest;
22557
+ }
22558
+ const request = this.sendPromptIdempotently(
22559
+ localThreadId,
22560
+ { ...input, clientRequestId },
22561
+ options
22562
+ );
22563
+ this.promptRequestsInFlight.set(requestKey, request);
22564
+ try {
22565
+ return await request;
22566
+ } finally {
22567
+ if (this.promptRequestsInFlight.get(requestKey) === request) {
22568
+ this.promptRequestsInFlight.delete(requestKey);
22569
+ }
22570
+ }
22571
+ }
22572
+ async sendPromptIdempotently(localThreadId, input, options) {
22573
+ deleteExpiredThreadPromptRequestRecords(
22574
+ this.db,
22575
+ new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString()
22576
+ );
22577
+ const existing = getThreadPromptRequestRecord(
22578
+ this.db,
22579
+ localThreadId,
22580
+ input.clientRequestId
22581
+ );
22582
+ if (existing) {
22583
+ const record = this.requireThreadRecord(localThreadId);
22584
+ return this.toThreadDto(
22585
+ record,
22586
+ await this.listLoadedProviderSessionIds(record.provider)
22587
+ );
22588
+ }
22589
+ createThreadPromptRequestRecord(this.db, localThreadId, input.clientRequestId);
22590
+ try {
22591
+ const result = await this.sendPromptOnce(localThreadId, input, options);
22592
+ markThreadPromptRequestAccepted(this.db, localThreadId, input.clientRequestId);
22593
+ return result;
22594
+ } catch (error) {
22595
+ deleteThreadPromptRequestRecord(this.db, localThreadId, input.clientRequestId);
22596
+ throw error;
22597
+ }
22598
+ }
22599
+ async sendPromptOnce(localThreadId, input, options = {}) {
22276
22600
  let record = this.requireThreadRecord(localThreadId);
22277
22601
  await this.importCoordinator.assertImportedThreadReadyForPrompt({
22278
22602
  source: record.source,
@@ -22338,7 +22662,10 @@ var ThreadService = class {
22338
22662
  };
22339
22663
  const hasActiveProviderTurn = Boolean(record.providerTurnId) && (record.status === "running" || !turnConfig.supportsRunningTurnInput && !record.lastTurnCompletedAt) && record.status !== "failed" && record.status !== "interrupted";
22340
22664
  if (hasActiveProviderTurn && record.providerTurnId) {
22341
- if (!turnConfig.supportsRunningTurnInput) {
22665
+ const activeTurnCollaborationMode = normalizeCollaborationMode(
22666
+ record.activeTurnCollaborationMode ?? record.collaborationMode
22667
+ );
22668
+ if (!turnConfig.supportsRunningTurnInput || activeTurnCollaborationMode !== turnConfig.collaborationMode) {
22342
22669
  return this.promptTurnCoordinator.queueContinuationPromptTurn(localThreadId, {
22343
22670
  ...connectedRecord,
22344
22671
  providerTurnId: record.providerTurnId
@@ -22682,7 +23009,7 @@ var ThreadService = class {
22682
23009
  if (!record) {
22683
23010
  return false;
22684
23011
  }
22685
- return !this.runtimeSupportsLiveRunningTurnInput(record.provider) && this.auxiliaryState.hasPendingSteersForTurn(localThreadId, turnId);
23012
+ return this.auxiliaryState.hasQueuedContinuationsForTurn(localThreadId, turnId);
22686
23013
  }
22687
23014
  shouldPreserveMissingPendingSteer(localThreadId, turnId) {
22688
23015
  const record = getThreadRecordById(this.db, localThreadId);
@@ -22692,14 +23019,11 @@ var ThreadService = class {
22692
23019
  if (record.status === "failed" || record.status === "interrupted") {
22693
23020
  return false;
22694
23021
  }
22695
- if (this.runtimeSupportsLiveRunningTurnInput(record.provider)) {
22696
- return false;
22697
- }
22698
23022
  const activeDisplayTurnId = this.liveState.displayTurnIdForRuntimeTurn(
22699
23023
  localThreadId,
22700
23024
  record.providerTurnId
22701
23025
  );
22702
- return record.providerTurnId === turnId || activeDisplayTurnId === turnId;
23026
+ return (record.providerTurnId === turnId || activeDisplayTurnId === turnId) && this.auxiliaryState.hasQueuedContinuationsForTurn(localThreadId, turnId);
22703
23027
  }
22704
23028
  scheduleQueuedContinuationDrain(localThreadId, turnId) {
22705
23029
  const key = `${localThreadId}:${turnId}`;
@@ -22723,7 +23047,7 @@ var ThreadService = class {
22723
23047
  const pending = this.auxiliaryState.listPendingSteerRecordsForTurn(
22724
23048
  localThreadId,
22725
23049
  turnId
22726
- )[0];
23050
+ ).find((entry) => entry.delivery === "continuation");
22727
23051
  if (!pending) {
22728
23052
  return;
22729
23053
  }
@@ -22742,7 +23066,8 @@ var ThreadService = class {
22742
23066
  const developerInstructions = combineDeveloperInstructions([
22743
23067
  pluginDeveloperInstructions(this.pluginService)
22744
23068
  ]);
22745
- const turnConfig = await this.sessionCoordinator.resolvePromptTurnConfig({
23069
+ const queuedConfig = parseQueuedTurnConfig(pending.turnConfigJson);
23070
+ const turnConfig = queuedConfig ?? await this.sessionCoordinator.resolvePromptTurnConfig({
22746
23071
  provider: record.provider,
22747
23072
  currentModel: record.model,
22748
23073
  currentReasoningEffort: record.reasoningEffort,
@@ -22752,14 +23077,16 @@ var ThreadService = class {
22752
23077
  approvalMode: record.approvalMode ?? "yolo",
22753
23078
  promptInput: {}
22754
23079
  });
22755
- const queuedUserItemId = `queued-continuation:${pending.id}:user`;
22756
- this.historyPersistence.persistProjectedHistoryItem(localThreadId, turnId, {
22757
- id: queuedUserItemId,
22758
- kind: "userMessage",
22759
- text: pending.displayPrompt,
22760
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
22761
- sequence: this.liveState.recordTurnItemOrder(localThreadId, turnId, queuedUserItemId)
22762
- });
23080
+ if (!queuedConfig?.startNewTurn) {
23081
+ const queuedUserItemId = `queued-continuation:${pending.id}:user`;
23082
+ this.historyPersistence.persistProjectedHistoryItem(localThreadId, turnId, {
23083
+ id: queuedUserItemId,
23084
+ kind: "userMessage",
23085
+ text: pending.displayPrompt,
23086
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
23087
+ sequence: this.liveState.recordTurnItemOrder(localThreadId, turnId, queuedUserItemId)
23088
+ });
23089
+ }
22763
23090
  await this.promptTurnCoordinator.startPromptTurn(localThreadId, {
22764
23091
  ...record,
22765
23092
  providerSessionId
@@ -22773,8 +23100,7 @@ var ThreadService = class {
22773
23100
  sandboxMode: turnConfig.sandboxMode,
22774
23101
  performanceMode: turnConfig.performanceMode,
22775
23102
  workspacePath: workspace.absPath,
22776
- hidden: true,
22777
- displayTurnId: turnId
23103
+ ...queuedConfig?.startNewTurn ? {} : { hidden: true, displayTurnId: turnId }
22778
23104
  });
22779
23105
  this.auxiliaryState.deletePendingSteerRecord(localThreadId, pending.id, turnId);
22780
23106
  }
@@ -22862,6 +23188,27 @@ var ThreadService = class {
22862
23188
  );
22863
23189
  }
22864
23190
  };
23191
+ function parseQueuedTurnConfig(value) {
23192
+ if (!value) {
23193
+ return null;
23194
+ }
23195
+ try {
23196
+ const parsed = JSON.parse(value);
23197
+ if (parsed.collaborationMode !== "default" && parsed.collaborationMode !== "plan" || parsed.performanceMode !== "fast" && parsed.performanceMode !== "standard" || typeof parsed.startNewTurn !== "boolean") {
23198
+ return null;
23199
+ }
23200
+ return {
23201
+ effectiveModel: typeof parsed.effectiveModel === "string" ? parsed.effectiveModel : null,
23202
+ normalizedReasoning: parsed.normalizedReasoning ?? null,
23203
+ collaborationMode: parsed.collaborationMode,
23204
+ sandboxMode: parsed.sandboxMode ?? "workspace-write",
23205
+ performanceMode: parsed.performanceMode,
23206
+ startNewTurn: parsed.startNewTurn
23207
+ };
23208
+ } catch {
23209
+ return null;
23210
+ }
23211
+ }
22865
23212
 
22866
23213
  // src/routes/agent-runtimes.ts
22867
23214
  import fs15 from "fs/promises";
@@ -22934,6 +23281,17 @@ async function registerAgentRuntimeRoutes(app2) {
22934
23281
  const { provider: provider2 } = providerParamSchema.parse(request.params);
22935
23282
  return runtimeDto(app2, provider2);
22936
23283
  });
23284
+ app2.get("/api/agent-runtimes/:provider/subscription-usage", async (request) => {
23285
+ const { provider: provider2 } = providerParamSchema.parse(request.params);
23286
+ const runtime = app2.services.agentRuntimes.getOptional(provider2);
23287
+ if (!runtime) {
23288
+ throw providerNotConfigured(provider2);
23289
+ }
23290
+ if (!runtime.getSubscriptionUsage) {
23291
+ return { usage: null };
23292
+ }
23293
+ return { usage: await runtime.getSubscriptionUsage() };
23294
+ });
22937
23295
  app2.post("/api/agent-runtimes/:provider/restart", async (request) => {
22938
23296
  const { provider: provider2 } = providerParamSchema.parse(request.params);
22939
23297
  const runtime = app2.services.agentRuntimes.getOptional(provider2);
@@ -23340,9 +23698,11 @@ function parseProviderHostFileParams(params) {
23340
23698
  }
23341
23699
  async function registerSystemRoutes(app2) {
23342
23700
  app2.get("/healthz", async () => {
23701
+ const activeTurnCount = app2.services.database.sqlite.prepare("SELECT COUNT(*) AS count FROM threads WHERE status = 'running'").get();
23343
23702
  return {
23344
23703
  status: "ok",
23345
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
23704
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
23705
+ activeTurnCount: activeTurnCount.count
23346
23706
  };
23347
23707
  });
23348
23708
  app2.get("/readyz", async () => {
@@ -23519,7 +23879,8 @@ var reasoningEffortValues = [
23519
23879
  "medium",
23520
23880
  "high",
23521
23881
  "xhigh",
23522
- "max"
23882
+ "max",
23883
+ "ultra"
23523
23884
  ];
23524
23885
  var createThreadSchema = z5.object({
23525
23886
  workspaceId: z5.string().uuid(),
@@ -24963,7 +25324,16 @@ async function registerWorkspaceRoutes(app2) {
24963
25324
  await cloneRepository(body.gitUrl.trim(), targetPath);
24964
25325
  validated = await validateWorkspacePath(app2.services.config.workspaceRoot, targetPath);
24965
25326
  } else {
24966
- validated = await validateWorkspacePath(app2.services.config.workspaceRoot, body.absPath, {
25327
+ const requestedPath = body.absPath.trim();
25328
+ const isWorkspaceName = !path22.isAbsolute(requestedPath) && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(requestedPath) && requestedPath !== "." && requestedPath !== "..";
25329
+ if (!path22.isAbsolute(requestedPath) && !isWorkspaceName) {
25330
+ throw new HttpError(400, {
25331
+ code: "bad_request",
25332
+ message: "Use a simple directory name, an absolute path, or a Git URL."
25333
+ });
25334
+ }
25335
+ const targetPath = isWorkspaceName ? path22.join(settings.devHome, requestedPath) : requestedPath;
25336
+ validated = await validateWorkspacePath(app2.services.config.workspaceRoot, targetPath, {
24967
25337
  devHome: settings.devHome,
24968
25338
  createMissingLeaf: true
24969
25339
  });
@@ -27915,6 +28285,7 @@ var RelayTunnelClient = class {
27915
28285
  reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
27916
28286
  stopped = false;
27917
28287
  relayClientCleanup = /* @__PURE__ */ new Map();
28288
+ pendingActivity = /* @__PURE__ */ new Map();
27918
28289
  validateConfig() {
27919
28290
  if (!this.config.serverUrl || !this.config.agentToken) {
27920
28291
  throw new Error(
@@ -27929,7 +28300,10 @@ var RelayTunnelClient = class {
27929
28300
  if (this.socket) {
27930
28301
  return;
27931
28302
  }
27932
- const url = new URL("/supervisor/tunnel", this.config.serverUrl ?? void 0);
28303
+ const url = new URL(
28304
+ "/supervisor/tunnel",
28305
+ this.config.serverUrl ?? void 0
28306
+ );
27933
28307
  url.searchParams.set("token", this.config.agentToken ?? "");
27934
28308
  url.searchParams.set("deviceToken", this.config.agentToken ?? "");
27935
28309
  const socket = new WebSocket(url);
@@ -27943,6 +28317,7 @@ var RelayTunnelClient = class {
27943
28317
  this.clearConnectTimeout();
27944
28318
  this.reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
27945
28319
  this.sendHeartbeat();
28320
+ this.flushPendingActivity();
27946
28321
  this.clearHeartbeat();
27947
28322
  this.heartbeatHandle = setInterval(() => {
27948
28323
  this.sendHeartbeat();
@@ -27967,6 +28342,29 @@ var RelayTunnelClient = class {
27967
28342
  this.socket?.close();
27968
28343
  this.socket = null;
27969
28344
  }
28345
+ sendActivity(payload) {
28346
+ const key = `${payload.threadId}\0${payload.turnId}`;
28347
+ const socket = this.socket;
28348
+ if (socket?.readyState !== WebSocket.OPEN) {
28349
+ this.pendingActivity.set(key, payload);
28350
+ return;
28351
+ }
28352
+ const sent = this.sendEnvelope(socket, {
28353
+ type: "relay.activity",
28354
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
28355
+ payload
28356
+ });
28357
+ if (sent) {
28358
+ this.pendingActivity.delete(key);
28359
+ } else {
28360
+ this.pendingActivity.set(key, payload);
28361
+ }
28362
+ }
28363
+ flushPendingActivity() {
28364
+ for (const payload of this.pendingActivity.values()) {
28365
+ this.sendActivity(payload);
28366
+ }
28367
+ }
27970
28368
  sendHeartbeat() {
27971
28369
  const socket = this.socket;
27972
28370
  if (socket?.readyState !== WebSocket.OPEN) {
@@ -27986,9 +28384,12 @@ var RelayTunnelClient = class {
27986
28384
  }
27987
28385
  if (parsed.type !== "relay.request") {
27988
28386
  if (parsed.type === "relay.client.connected") {
27989
- const cleanup = this.handleClientConnected(parsed.clientId, (message) => {
27990
- this.sendClientMessage(parsed.clientId, message);
27991
- });
28387
+ const cleanup = this.handleClientConnected(
28388
+ parsed.clientId,
28389
+ (message) => {
28390
+ this.sendClientMessage(parsed.clientId, message);
28391
+ }
28392
+ );
27992
28393
  this.relayClientCleanup.set(parsed.clientId, cleanup);
27993
28394
  return;
27994
28395
  }
@@ -27998,9 +28399,13 @@ var RelayTunnelClient = class {
27998
28399
  return;
27999
28400
  }
28000
28401
  if (parsed.type === "relay.client.message") {
28001
- await this.handleClientMessage(parsed.clientId, parsed.payload, (message) => {
28002
- this.sendClientMessage(parsed.clientId, message);
28003
- });
28402
+ await this.handleClientMessage(
28403
+ parsed.clientId,
28404
+ parsed.payload,
28405
+ (message) => {
28406
+ this.sendClientMessage(parsed.clientId, message);
28407
+ }
28408
+ );
28004
28409
  return;
28005
28410
  }
28006
28411
  return;
@@ -28010,15 +28415,12 @@ var RelayTunnelClient = class {
28010
28415
  if (socket?.readyState !== WebSocket.OPEN) {
28011
28416
  return;
28012
28417
  }
28013
- this.sendEnvelope(
28014
- socket,
28015
- {
28016
- type: "relay.response",
28017
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
28018
- requestId: parsed.requestId,
28019
- payload: response
28020
- }
28021
- );
28418
+ this.sendEnvelope(socket, {
28419
+ type: "relay.response",
28420
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
28421
+ requestId: parsed.requestId,
28422
+ payload: response
28423
+ });
28022
28424
  }
28023
28425
  sendClientMessage(clientId, message) {
28024
28426
  const socket = this.socket;
@@ -28035,8 +28437,10 @@ var RelayTunnelClient = class {
28035
28437
  sendEnvelope(socket, message) {
28036
28438
  try {
28037
28439
  socket.send(JSON.stringify(message));
28440
+ return true;
28038
28441
  } catch {
28039
28442
  this.closeAndReconnect(socket);
28443
+ return false;
28040
28444
  }
28041
28445
  }
28042
28446
  closeAndReconnect(socket) {
@@ -28104,10 +28508,7 @@ var DEFAULT_WEBVIEW_CORS_ORIGINS = /* @__PURE__ */ new Set([
28104
28508
  "https://localhost",
28105
28509
  "https://appassets.androidplatform.net"
28106
28510
  ]);
28107
- var WEBVIEW_CORS_ALLOW_HEADERS = [
28108
- "authorization",
28109
- "content-type"
28110
- ].join(", ");
28511
+ var WEBVIEW_CORS_ALLOW_HEADERS = ["authorization", "content-type"].join(", ");
28111
28512
  var WEBVIEW_CORS_ALLOW_METHODS = [
28112
28513
  "GET",
28113
28514
  "POST",
@@ -28121,7 +28522,9 @@ function webViewCorsOrigins(env) {
28121
28522
  return null;
28122
28523
  }
28123
28524
  const configured = env.REMOTE_CODEX_WEBVIEW_CORS_ORIGINS?.split(",").map((origin) => origin.trim()).filter(Boolean);
28124
- return new Set(configured?.length ? configured : DEFAULT_WEBVIEW_CORS_ORIGINS);
28525
+ return new Set(
28526
+ configured?.length ? configured : DEFAULT_WEBVIEW_CORS_ORIGINS
28527
+ );
28125
28528
  }
28126
28529
  function applyWebViewCorsHeaders(reply, origin) {
28127
28530
  reply.header("access-control-allow-origin", origin);
@@ -28170,7 +28573,11 @@ function createServiceLifecycle() {
28170
28573
  });
28171
28574
  }
28172
28575
  const repoRoot = findRepoRoot();
28173
- const restartScript = path28.join(repoRoot, "scripts", "service-restart.mjs");
28576
+ const restartScript = path28.join(
28577
+ repoRoot,
28578
+ "scripts",
28579
+ "service-restart.mjs"
28580
+ );
28174
28581
  if (!fs26.existsSync(restartScript) || !fs26.existsSync(path28.join(repoRoot, "pnpm-workspace.yaml"))) {
28175
28582
  throw new HttpError(503, {
28176
28583
  code: "service_unavailable",
@@ -28230,7 +28637,9 @@ function buildApp(options = {}) {
28230
28637
  },
28231
28638
  disableRequestLogging: config.disableRequestLogging
28232
28639
  });
28233
- const allowedWebViewCorsOrigins = webViewCorsOrigins(options.env ?? process.env);
28640
+ const allowedWebViewCorsOrigins = webViewCorsOrigins(
28641
+ options.env ?? process.env
28642
+ );
28234
28643
  app2.addHook("onRequest", async (request, reply) => {
28235
28644
  if (!allowedWebViewCorsOrigins) {
28236
28645
  return;
@@ -28244,15 +28653,22 @@ function buildApp(options = {}) {
28244
28653
  return reply.code(204).send();
28245
28654
  }
28246
28655
  });
28247
- app2.register(multipart, {
28248
- limits: {
28249
- files: MAX_PROMPT_ATTACHMENTS2,
28250
- fileSize: MAX_PROMPT_ATTACHMENT_BYTES2
28656
+ app2.register(
28657
+ multipart,
28658
+ {
28659
+ limits: {
28660
+ files: MAX_PROMPT_ATTACHMENTS2,
28661
+ fileSize: MAX_PROMPT_ATTACHMENT_BYTES2
28662
+ }
28251
28663
  }
28252
- });
28664
+ );
28253
28665
  const backendPluginHost = new BackendPluginHost(app2);
28254
28666
  backendPluginHost.register(createTerminalPluginBackendContribution());
28255
- const relaySocketBridge = createRelaySocketBridge(app2, eventBus, backendPluginHost);
28667
+ const relaySocketBridge = createRelaySocketBridge(
28668
+ app2,
28669
+ eventBus,
28670
+ backendPluginHost
28671
+ );
28256
28672
  const relayTunnelClient = config.mode === "relay" ? options.relayTunnelClient ?? new RelayTunnelClient(
28257
28673
  config.relay,
28258
28674
  createRelayRequestHandler(app2),
@@ -28260,6 +28676,31 @@ function buildApp(options = {}) {
28260
28676
  relaySocketBridge.handleMessage
28261
28677
  ) : null;
28262
28678
  relayTunnelClient?.validateConfig();
28679
+ const cleanupRelayActivity = relayTunnelClient ? eventBus.onThreadEvent((event) => {
28680
+ if (event.type === "thread.turn.started") {
28681
+ relayTunnelClient.sendActivity({
28682
+ kind: "turn_started",
28683
+ threadId: event.threadId,
28684
+ turnId: event.payload.turnId
28685
+ });
28686
+ return;
28687
+ }
28688
+ if (event.type === "thread.turn.completed") {
28689
+ relayTunnelClient.sendActivity({
28690
+ kind: "turn_terminal",
28691
+ threadId: event.threadId,
28692
+ turnId: event.payload.turnId
28693
+ });
28694
+ return;
28695
+ }
28696
+ if (event.type === "thread.turn.failed" && event.payload.willRetry !== true) {
28697
+ relayTunnelClient.sendActivity({
28698
+ kind: "turn_terminal",
28699
+ threadId: event.threadId,
28700
+ turnId: event.payload.turnId
28701
+ });
28702
+ }
28703
+ }) : null;
28263
28704
  app2.decorate("services", {
28264
28705
  config,
28265
28706
  database,
@@ -28440,6 +28881,7 @@ function buildApp(options = {}) {
28440
28881
  });
28441
28882
  });
28442
28883
  app2.addHook("onClose", async () => {
28884
+ cleanupRelayActivity?.();
28443
28885
  await shellService.stop();
28444
28886
  relayTunnelClient?.stop();
28445
28887
  await Promise.all(agentRuntimes.all().map((runtime) => runtime.stop()));
@@ -28612,6 +29054,21 @@ if (fs27.existsSync(".env")) {
28612
29054
  }
28613
29055
  var app = buildApp();
28614
29056
  var { host, port } = app.services.config;
29057
+ var closing = false;
29058
+ async function shutdown(signal) {
29059
+ if (closing) return;
29060
+ closing = true;
29061
+ app.log.info(`Supervisor API received ${signal}; closing cleanly.`);
29062
+ try {
29063
+ await app.close();
29064
+ process.exit(0);
29065
+ } catch (error) {
29066
+ app.log.error(error);
29067
+ process.exit(1);
29068
+ }
29069
+ }
29070
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
29071
+ process.once("SIGINT", () => void shutdown("SIGINT"));
28615
29072
  app.listen({ host, port }).then(() => {
28616
29073
  app.log.info(`Supervisor API listening on http://${host}:${port}`);
28617
29074
  }).catch((error) => {