remote-codex 0.11.31 → 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.
@@ -6382,6 +6382,7 @@ __export(schema_exports, {
6382
6382
  threadGoals: () => threadGoals,
6383
6383
  threadHistoryItems: () => threadHistoryItems,
6384
6384
  threadPendingSteers: () => threadPendingSteers,
6385
+ threadPromptRequests: () => threadPromptRequests,
6385
6386
  threadTurnMetadata: () => threadTurnMetadata,
6386
6387
  threads: () => threads,
6387
6388
  viewerSessions: () => viewerSessions,
@@ -6418,6 +6419,7 @@ var threads = sqliteTable("threads", {
6418
6419
  fastBaseModel: text("fast_base_model"),
6419
6420
  fastBaseReasoningEffort: text("fast_base_reasoning_effort"),
6420
6421
  collaborationMode: text("collaboration_mode").notNull().default("default"),
6422
+ activeTurnCollaborationMode: text("active_turn_collaboration_mode"),
6421
6423
  approvalMode: text("approval_mode"),
6422
6424
  sandboxMode: text("sandbox_mode"),
6423
6425
  status: text("status"),
@@ -6493,9 +6495,27 @@ var threadPendingSteers = sqliteTable("thread_pending_steers", {
6493
6495
  clientRequestId: text("client_request_id"),
6494
6496
  displayPrompt: text("display_prompt").notNull(),
6495
6497
  submittedPrompt: text("submitted_prompt").notNull(),
6498
+ delivery: text("delivery").notNull().default("steer"),
6499
+ turnConfigJson: text("turn_config_json"),
6496
6500
  createdAt: text("created_at").notNull(),
6497
6501
  updatedAt: text("updated_at").notNull()
6498
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
+ );
6499
6519
  var threadHistoryItems = sqliteTable(
6500
6520
  "thread_history_items",
6501
6521
  {
@@ -6729,6 +6749,7 @@ function createThreadRecord(db, input) {
6729
6749
  fastBaseModel: input.fastBaseModel ?? null,
6730
6750
  fastBaseReasoningEffort: input.fastBaseReasoningEffort ?? null,
6731
6751
  collaborationMode: input.collaborationMode ?? "default",
6752
+ activeTurnCollaborationMode: input.activeTurnCollaborationMode ?? null,
6732
6753
  approvalMode: input.approvalMode,
6733
6754
  sandboxMode: input.sandboxMode ?? null,
6734
6755
  status: "idle",
@@ -6868,6 +6889,8 @@ function createThreadPendingSteerRecord(db, input) {
6868
6889
  clientRequestId: input.clientRequestId ?? null,
6869
6890
  displayPrompt: input.displayPrompt,
6870
6891
  submittedPrompt: input.submittedPrompt,
6892
+ delivery: input.delivery ?? "steer",
6893
+ turnConfigJson: input.turnConfigJson ?? null,
6871
6894
  createdAt: now,
6872
6895
  updatedAt: now
6873
6896
  };
@@ -6880,6 +6903,48 @@ function deleteThreadPendingSteerRecordById(db, id) {
6880
6903
  function deleteThreadPendingSteerRecordsByThreadId(db, threadId) {
6881
6904
  db.delete(threadPendingSteers).where(eq(threadPendingSteers.threadId, threadId)).run();
6882
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
+ }
6883
6948
  function listThreadActivityNotesByThreadId(db, threadId) {
6884
6949
  return db.select().from(threadActivityNotes).where(eq(threadActivityNotes.threadId, threadId)).orderBy(threadActivityNotes.createdAt).all();
6885
6950
  }
@@ -7863,6 +7928,14 @@ var CodexAppServerManager = class extends EventEmitter3 {
7863
7928
  });
7864
7929
  return response.data.map(mapModel);
7865
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
+ }
7866
7939
  async listThreads() {
7867
7940
  await this.ensureReady();
7868
7941
  const response = await this.client.request("thread/list", {
@@ -10550,6 +10623,16 @@ function mapCodexNotification(event) {
10550
10623
  return null;
10551
10624
  }
10552
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
+ }
10553
10636
  function mapCodexRuntimeError(error) {
10554
10637
  if (error instanceof AgentRuntimeError) {
10555
10638
  throw error;
@@ -10667,6 +10750,51 @@ var CodexRuntimeAdapter = class extends EventEmitter4 {
10667
10750
  getStatus() {
10668
10751
  return mapStatus(this.manager.getStatus());
10669
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
+ }
10670
10798
  start() {
10671
10799
  return codexRuntimeCall(() => this.manager.start());
10672
10800
  }
@@ -12373,11 +12501,44 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12373
12501
  sessionModels = /* @__PURE__ */ new Map();
12374
12502
  sessionApprovalModes = /* @__PURE__ */ new Map();
12375
12503
  liveUserPrompts = /* @__PURE__ */ new Map();
12504
+ subscriptionUsageWindows = /* @__PURE__ */ new Map();
12505
+ subscriptionUsageObservedAt = null;
12376
12506
  clientApp;
12377
12507
  sdkLoadError = null;
12378
12508
  getStatus() {
12379
12509
  return { ...this.status };
12380
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
+ }
12381
12542
  updateToolboxItemsFromSystemInit(message) {
12382
12543
  this.managementSchema.toolboxItems = buildClaudeToolboxItems(
12383
12544
  normalizeClaudeSlashCommands(message.slash_commands)
@@ -12525,6 +12686,7 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12525
12686
  try {
12526
12687
  for await (const message of query) {
12527
12688
  rawMessages.push(message);
12689
+ this.captureRateLimit(message);
12528
12690
  if (message.type === "system" && message.subtype === "init") {
12529
12691
  this.updateToolboxItemsFromSystemInit(message);
12530
12692
  const sessionId = message.session_id;
@@ -12772,6 +12934,7 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12772
12934
  try {
12773
12935
  for await (const message of state.query) {
12774
12936
  rawMessages.push(message);
12937
+ this.captureRateLimit(message);
12775
12938
  this.consumeMessage(state, message);
12776
12939
  const status = queryResultStatus(message);
12777
12940
  if (status) {
@@ -16074,6 +16237,7 @@ var ThreadAuxiliaryStateStore = class {
16074
16237
  clientRequestId: record.clientRequestId ?? null,
16075
16238
  turnId: record.turnId,
16076
16239
  prompt: record.displayPrompt,
16240
+ delivery: record.delivery === "continuation" ? "continuation" : "steer",
16077
16241
  createdAt: record.createdAt
16078
16242
  }));
16079
16243
  }
@@ -16090,6 +16254,11 @@ var ThreadAuxiliaryStateStore = class {
16090
16254
  hasPendingSteersForTurn(localThreadId, turnId) {
16091
16255
  return this.listPendingSteerRecordsForTurn(localThreadId, turnId).length > 0;
16092
16256
  }
16257
+ hasQueuedContinuationsForTurn(localThreadId, turnId) {
16258
+ return this.listPendingSteerRecordsForTurn(localThreadId, turnId).some(
16259
+ (record) => record.delivery === "continuation"
16260
+ );
16261
+ }
16093
16262
  deletePendingSteerRecord(localThreadId, id, turnId) {
16094
16263
  deleteThreadPendingSteerRecordById(this.db, id);
16095
16264
  this.callbacks.invalidateThreadDetailCache(localThreadId);
@@ -17889,6 +18058,7 @@ var ThreadRuntimeEventProjector = class {
17889
18058
  const turnItems = event.turn.items;
17890
18059
  updateThreadRecord(db, record.id, {
17891
18060
  providerTurnId: null,
18061
+ activeTurnCollaborationMode: null,
17892
18062
  status: event.turn.status === "failed" ? "failed" : event.turn.status === "interrupted" ? "interrupted" : "idle",
17893
18063
  lastError: event.turn.error?.message ?? null,
17894
18064
  lastTurnCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -17910,7 +18080,9 @@ var ThreadRuntimeEventProjector = class {
17910
18080
  }
17911
18081
  }
17912
18082
  callbacks.clearTerminalPendingRequests(record.id, true);
17913
- 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)) {
17914
18086
  callbacks.createPendingPlanDecisionRequest(record.id, turnId, true);
17915
18087
  } else {
17916
18088
  callbacks.dismissPlanDecisionTurn(record.id);
@@ -17942,6 +18114,7 @@ var ThreadRuntimeEventProjector = class {
17942
18114
  const turnId = liveState.displayTurnIdForRuntimeTurn(record.id, event.providerTurnId) ?? event.providerTurnId;
17943
18115
  updateThreadRecord(db, record.id, {
17944
18116
  providerTurnId: null,
18117
+ activeTurnCollaborationMode: null,
17945
18118
  status: "failed",
17946
18119
  lastError: event.error,
17947
18120
  lastTurnCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -19274,6 +19447,7 @@ var ThreadPromptTurnCoordinator = class {
19274
19447
  model: input.effectiveModel,
19275
19448
  reasoningEffort: input.normalizedReasoning,
19276
19449
  collaborationMode: input.collaborationMode,
19450
+ activeTurnCollaborationMode: input.collaborationMode,
19277
19451
  sandboxMode: input.sandboxMode
19278
19452
  };
19279
19453
  if (isAutoGeneratedTitle(record.title)) {
@@ -19396,7 +19570,8 @@ var ThreadPromptTurnCoordinator = class {
19396
19570
  turnId: steerTurnId,
19397
19571
  clientRequestId: input.clientRequestId,
19398
19572
  displayPrompt: input.displayPrompt,
19399
- submittedPrompt: input.prompt
19573
+ submittedPrompt: input.prompt,
19574
+ delivery: "steer"
19400
19575
  });
19401
19576
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19402
19577
  this.callbacks.emitThreadUpdated(localThreadId, {
@@ -19445,7 +19620,16 @@ var ThreadPromptTurnCoordinator = class {
19445
19620
  turnId: displayTurnId,
19446
19621
  clientRequestId: input.clientRequestId,
19447
19622
  displayPrompt: input.displayPrompt,
19448
- 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
+ })
19449
19633
  });
19450
19634
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19451
19635
  this.callbacks.emitThreadUpdated(localThreadId, {
@@ -19651,7 +19835,7 @@ var ThreadSessionCoordinator = class {
19651
19835
  if (!isRemoteThreadBootstrapError(error)) {
19652
19836
  throw error;
19653
19837
  }
19654
- return { status: "bootstrap_unavailable" };
19838
+ return { status: "bootstrap_unavailable", error };
19655
19839
  }
19656
19840
  const effectiveModel = input.resumeInput.model ?? input.currentModel ?? response.model ?? null;
19657
19841
  const resumedReasoning = this.providerRuntime.normalizeReasoningForModel(
@@ -19827,6 +20011,37 @@ var ThreadSessionLifecycleCoordinator = class {
19827
20011
  fastMode: record.fastMode
19828
20012
  });
19829
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);
19830
20045
  return;
19831
20046
  }
19832
20047
  updateThreadRecord(
@@ -19850,6 +20065,9 @@ var ThreadSessionLifecycleCoordinator = class {
19850
20065
  }
19851
20066
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19852
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
+ }
19853
20071
  disconnectThread(localThreadId) {
19854
20072
  const record = getThreadRecordById(this.db, localThreadId);
19855
20073
  if (!record) {
@@ -20049,6 +20267,7 @@ var ThreadDeletionCoordinator = class {
20049
20267
  deleteThreadGoalRecordsByThreadId(this.db, localThreadId);
20050
20268
  deleteThreadHistoryItemRecordsByThreadId(this.db, localThreadId);
20051
20269
  deleteThreadPendingSteerRecordsByThreadId(this.db, localThreadId);
20270
+ deleteThreadPromptRequestRecordsByThreadId(this.db, localThreadId);
20052
20271
  deleteThreadTurnMetadataByThreadId(this.db, localThreadId);
20053
20272
  deleteThreadRecord(this.db, localThreadId);
20054
20273
  return { id: localThreadId };
@@ -21974,7 +22193,7 @@ var ThreadService = class {
21974
22193
  normalizeCollaborationMode,
21975
22194
  normalizeReasoningEffort: normalizeReasoningEffort2,
21976
22195
  normalizeThreadGoalStatusForThread: (goal, record) => this.goalCoordinator.normalizeThreadGoalStatusForThread(goal, record),
21977
- shouldPreservePendingSteersForCompletedTurn: (record, turnId) => !this.runtimeSupportsLiveRunningTurnInput(record.provider) && this.auxiliaryState.hasPendingSteersForTurn(record.id, turnId),
22196
+ shouldPreservePendingSteersForCompletedTurn: (record, turnId) => this.shouldPreserveCompletedPendingSteer(record.id, turnId),
21978
22197
  scheduleQueuedContinuationDrain: (localThreadId, turnId) => this.scheduleQueuedContinuationDrain(localThreadId, turnId),
21979
22198
  persistLiveHistoryItem: (localThreadId, turnId, item) => this.historyPersistence.persistLiveHistoryItem(localThreadId, turnId, item),
21980
22199
  persistFinalTurnOrderingHints: (localThreadId, turnId, items) => this.historyPersistence.persistFinalTurnOrderingHints(localThreadId, turnId, items),
@@ -22008,6 +22227,7 @@ var ThreadService = class {
22008
22227
  config;
22009
22228
  liveState = new ThreadLiveStateStore();
22010
22229
  queuedContinuationDrains = /* @__PURE__ */ new Set();
22230
+ promptRequestsInFlight = /* @__PURE__ */ new Map();
22011
22231
  detailAssembler;
22012
22232
  usageAccounting;
22013
22233
  requestCoordinator;
@@ -22326,6 +22546,57 @@ var ThreadService = class {
22326
22546
  return this.getThreadDetail(localThreadId);
22327
22547
  }
22328
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 = {}) {
22329
22600
  let record = this.requireThreadRecord(localThreadId);
22330
22601
  await this.importCoordinator.assertImportedThreadReadyForPrompt({
22331
22602
  source: record.source,
@@ -22391,7 +22662,10 @@ var ThreadService = class {
22391
22662
  };
22392
22663
  const hasActiveProviderTurn = Boolean(record.providerTurnId) && (record.status === "running" || !turnConfig.supportsRunningTurnInput && !record.lastTurnCompletedAt) && record.status !== "failed" && record.status !== "interrupted";
22393
22664
  if (hasActiveProviderTurn && record.providerTurnId) {
22394
- if (!turnConfig.supportsRunningTurnInput) {
22665
+ const activeTurnCollaborationMode = normalizeCollaborationMode(
22666
+ record.activeTurnCollaborationMode ?? record.collaborationMode
22667
+ );
22668
+ if (!turnConfig.supportsRunningTurnInput || activeTurnCollaborationMode !== turnConfig.collaborationMode) {
22395
22669
  return this.promptTurnCoordinator.queueContinuationPromptTurn(localThreadId, {
22396
22670
  ...connectedRecord,
22397
22671
  providerTurnId: record.providerTurnId
@@ -22735,7 +23009,7 @@ var ThreadService = class {
22735
23009
  if (!record) {
22736
23010
  return false;
22737
23011
  }
22738
- return !this.runtimeSupportsLiveRunningTurnInput(record.provider) && this.auxiliaryState.hasPendingSteersForTurn(localThreadId, turnId);
23012
+ return this.auxiliaryState.hasQueuedContinuationsForTurn(localThreadId, turnId);
22739
23013
  }
22740
23014
  shouldPreserveMissingPendingSteer(localThreadId, turnId) {
22741
23015
  const record = getThreadRecordById(this.db, localThreadId);
@@ -22745,14 +23019,11 @@ var ThreadService = class {
22745
23019
  if (record.status === "failed" || record.status === "interrupted") {
22746
23020
  return false;
22747
23021
  }
22748
- if (this.runtimeSupportsLiveRunningTurnInput(record.provider)) {
22749
- return false;
22750
- }
22751
23022
  const activeDisplayTurnId = this.liveState.displayTurnIdForRuntimeTurn(
22752
23023
  localThreadId,
22753
23024
  record.providerTurnId
22754
23025
  );
22755
- return record.providerTurnId === turnId || activeDisplayTurnId === turnId;
23026
+ return (record.providerTurnId === turnId || activeDisplayTurnId === turnId) && this.auxiliaryState.hasQueuedContinuationsForTurn(localThreadId, turnId);
22756
23027
  }
22757
23028
  scheduleQueuedContinuationDrain(localThreadId, turnId) {
22758
23029
  const key = `${localThreadId}:${turnId}`;
@@ -22776,7 +23047,7 @@ var ThreadService = class {
22776
23047
  const pending = this.auxiliaryState.listPendingSteerRecordsForTurn(
22777
23048
  localThreadId,
22778
23049
  turnId
22779
- )[0];
23050
+ ).find((entry) => entry.delivery === "continuation");
22780
23051
  if (!pending) {
22781
23052
  return;
22782
23053
  }
@@ -22795,7 +23066,8 @@ var ThreadService = class {
22795
23066
  const developerInstructions = combineDeveloperInstructions([
22796
23067
  pluginDeveloperInstructions(this.pluginService)
22797
23068
  ]);
22798
- const turnConfig = await this.sessionCoordinator.resolvePromptTurnConfig({
23069
+ const queuedConfig = parseQueuedTurnConfig(pending.turnConfigJson);
23070
+ const turnConfig = queuedConfig ?? await this.sessionCoordinator.resolvePromptTurnConfig({
22799
23071
  provider: record.provider,
22800
23072
  currentModel: record.model,
22801
23073
  currentReasoningEffort: record.reasoningEffort,
@@ -22805,14 +23077,16 @@ var ThreadService = class {
22805
23077
  approvalMode: record.approvalMode ?? "yolo",
22806
23078
  promptInput: {}
22807
23079
  });
22808
- const queuedUserItemId = `queued-continuation:${pending.id}:user`;
22809
- this.historyPersistence.persistProjectedHistoryItem(localThreadId, turnId, {
22810
- id: queuedUserItemId,
22811
- kind: "userMessage",
22812
- text: pending.displayPrompt,
22813
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
22814
- sequence: this.liveState.recordTurnItemOrder(localThreadId, turnId, queuedUserItemId)
22815
- });
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
+ }
22816
23090
  await this.promptTurnCoordinator.startPromptTurn(localThreadId, {
22817
23091
  ...record,
22818
23092
  providerSessionId
@@ -22826,8 +23100,7 @@ var ThreadService = class {
22826
23100
  sandboxMode: turnConfig.sandboxMode,
22827
23101
  performanceMode: turnConfig.performanceMode,
22828
23102
  workspacePath: workspace.absPath,
22829
- hidden: true,
22830
- displayTurnId: turnId
23103
+ ...queuedConfig?.startNewTurn ? {} : { hidden: true, displayTurnId: turnId }
22831
23104
  });
22832
23105
  this.auxiliaryState.deletePendingSteerRecord(localThreadId, pending.id, turnId);
22833
23106
  }
@@ -22915,6 +23188,27 @@ var ThreadService = class {
22915
23188
  );
22916
23189
  }
22917
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
+ }
22918
23212
 
22919
23213
  // src/routes/agent-runtimes.ts
22920
23214
  import fs15 from "fs/promises";
@@ -22987,6 +23281,17 @@ async function registerAgentRuntimeRoutes(app2) {
22987
23281
  const { provider: provider2 } = providerParamSchema.parse(request.params);
22988
23282
  return runtimeDto(app2, provider2);
22989
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
+ });
22990
23295
  app2.post("/api/agent-runtimes/:provider/restart", async (request) => {
22991
23296
  const { provider: provider2 } = providerParamSchema.parse(request.params);
22992
23297
  const runtime = app2.services.agentRuntimes.getOptional(provider2);
@@ -23393,9 +23698,11 @@ function parseProviderHostFileParams(params) {
23393
23698
  }
23394
23699
  async function registerSystemRoutes(app2) {
23395
23700
  app2.get("/healthz", async () => {
23701
+ const activeTurnCount = app2.services.database.sqlite.prepare("SELECT COUNT(*) AS count FROM threads WHERE status = 'running'").get();
23396
23702
  return {
23397
23703
  status: "ok",
23398
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
23704
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
23705
+ activeTurnCount: activeTurnCount.count
23399
23706
  };
23400
23707
  });
23401
23708
  app2.get("/readyz", async () => {
@@ -25017,7 +25324,16 @@ async function registerWorkspaceRoutes(app2) {
25017
25324
  await cloneRepository(body.gitUrl.trim(), targetPath);
25018
25325
  validated = await validateWorkspacePath(app2.services.config.workspaceRoot, targetPath);
25019
25326
  } else {
25020
- 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, {
25021
25337
  devHome: settings.devHome,
25022
25338
  createMissingLeaf: true
25023
25339
  });
@@ -27969,6 +28285,7 @@ var RelayTunnelClient = class {
27969
28285
  reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
27970
28286
  stopped = false;
27971
28287
  relayClientCleanup = /* @__PURE__ */ new Map();
28288
+ pendingActivity = /* @__PURE__ */ new Map();
27972
28289
  validateConfig() {
27973
28290
  if (!this.config.serverUrl || !this.config.agentToken) {
27974
28291
  throw new Error(
@@ -27983,7 +28300,10 @@ var RelayTunnelClient = class {
27983
28300
  if (this.socket) {
27984
28301
  return;
27985
28302
  }
27986
- 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
+ );
27987
28307
  url.searchParams.set("token", this.config.agentToken ?? "");
27988
28308
  url.searchParams.set("deviceToken", this.config.agentToken ?? "");
27989
28309
  const socket = new WebSocket(url);
@@ -27997,6 +28317,7 @@ var RelayTunnelClient = class {
27997
28317
  this.clearConnectTimeout();
27998
28318
  this.reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
27999
28319
  this.sendHeartbeat();
28320
+ this.flushPendingActivity();
28000
28321
  this.clearHeartbeat();
28001
28322
  this.heartbeatHandle = setInterval(() => {
28002
28323
  this.sendHeartbeat();
@@ -28021,6 +28342,29 @@ var RelayTunnelClient = class {
28021
28342
  this.socket?.close();
28022
28343
  this.socket = null;
28023
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
+ }
28024
28368
  sendHeartbeat() {
28025
28369
  const socket = this.socket;
28026
28370
  if (socket?.readyState !== WebSocket.OPEN) {
@@ -28040,9 +28384,12 @@ var RelayTunnelClient = class {
28040
28384
  }
28041
28385
  if (parsed.type !== "relay.request") {
28042
28386
  if (parsed.type === "relay.client.connected") {
28043
- const cleanup = this.handleClientConnected(parsed.clientId, (message) => {
28044
- this.sendClientMessage(parsed.clientId, message);
28045
- });
28387
+ const cleanup = this.handleClientConnected(
28388
+ parsed.clientId,
28389
+ (message) => {
28390
+ this.sendClientMessage(parsed.clientId, message);
28391
+ }
28392
+ );
28046
28393
  this.relayClientCleanup.set(parsed.clientId, cleanup);
28047
28394
  return;
28048
28395
  }
@@ -28052,9 +28399,13 @@ var RelayTunnelClient = class {
28052
28399
  return;
28053
28400
  }
28054
28401
  if (parsed.type === "relay.client.message") {
28055
- await this.handleClientMessage(parsed.clientId, parsed.payload, (message) => {
28056
- this.sendClientMessage(parsed.clientId, message);
28057
- });
28402
+ await this.handleClientMessage(
28403
+ parsed.clientId,
28404
+ parsed.payload,
28405
+ (message) => {
28406
+ this.sendClientMessage(parsed.clientId, message);
28407
+ }
28408
+ );
28058
28409
  return;
28059
28410
  }
28060
28411
  return;
@@ -28064,15 +28415,12 @@ var RelayTunnelClient = class {
28064
28415
  if (socket?.readyState !== WebSocket.OPEN) {
28065
28416
  return;
28066
28417
  }
28067
- this.sendEnvelope(
28068
- socket,
28069
- {
28070
- type: "relay.response",
28071
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
28072
- requestId: parsed.requestId,
28073
- payload: response
28074
- }
28075
- );
28418
+ this.sendEnvelope(socket, {
28419
+ type: "relay.response",
28420
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
28421
+ requestId: parsed.requestId,
28422
+ payload: response
28423
+ });
28076
28424
  }
28077
28425
  sendClientMessage(clientId, message) {
28078
28426
  const socket = this.socket;
@@ -28089,8 +28437,10 @@ var RelayTunnelClient = class {
28089
28437
  sendEnvelope(socket, message) {
28090
28438
  try {
28091
28439
  socket.send(JSON.stringify(message));
28440
+ return true;
28092
28441
  } catch {
28093
28442
  this.closeAndReconnect(socket);
28443
+ return false;
28094
28444
  }
28095
28445
  }
28096
28446
  closeAndReconnect(socket) {
@@ -28158,10 +28508,7 @@ var DEFAULT_WEBVIEW_CORS_ORIGINS = /* @__PURE__ */ new Set([
28158
28508
  "https://localhost",
28159
28509
  "https://appassets.androidplatform.net"
28160
28510
  ]);
28161
- var WEBVIEW_CORS_ALLOW_HEADERS = [
28162
- "authorization",
28163
- "content-type"
28164
- ].join(", ");
28511
+ var WEBVIEW_CORS_ALLOW_HEADERS = ["authorization", "content-type"].join(", ");
28165
28512
  var WEBVIEW_CORS_ALLOW_METHODS = [
28166
28513
  "GET",
28167
28514
  "POST",
@@ -28175,7 +28522,9 @@ function webViewCorsOrigins(env) {
28175
28522
  return null;
28176
28523
  }
28177
28524
  const configured = env.REMOTE_CODEX_WEBVIEW_CORS_ORIGINS?.split(",").map((origin) => origin.trim()).filter(Boolean);
28178
- return new Set(configured?.length ? configured : DEFAULT_WEBVIEW_CORS_ORIGINS);
28525
+ return new Set(
28526
+ configured?.length ? configured : DEFAULT_WEBVIEW_CORS_ORIGINS
28527
+ );
28179
28528
  }
28180
28529
  function applyWebViewCorsHeaders(reply, origin) {
28181
28530
  reply.header("access-control-allow-origin", origin);
@@ -28224,7 +28573,11 @@ function createServiceLifecycle() {
28224
28573
  });
28225
28574
  }
28226
28575
  const repoRoot = findRepoRoot();
28227
- const restartScript = path28.join(repoRoot, "scripts", "service-restart.mjs");
28576
+ const restartScript = path28.join(
28577
+ repoRoot,
28578
+ "scripts",
28579
+ "service-restart.mjs"
28580
+ );
28228
28581
  if (!fs26.existsSync(restartScript) || !fs26.existsSync(path28.join(repoRoot, "pnpm-workspace.yaml"))) {
28229
28582
  throw new HttpError(503, {
28230
28583
  code: "service_unavailable",
@@ -28284,7 +28637,9 @@ function buildApp(options = {}) {
28284
28637
  },
28285
28638
  disableRequestLogging: config.disableRequestLogging
28286
28639
  });
28287
- const allowedWebViewCorsOrigins = webViewCorsOrigins(options.env ?? process.env);
28640
+ const allowedWebViewCorsOrigins = webViewCorsOrigins(
28641
+ options.env ?? process.env
28642
+ );
28288
28643
  app2.addHook("onRequest", async (request, reply) => {
28289
28644
  if (!allowedWebViewCorsOrigins) {
28290
28645
  return;
@@ -28298,15 +28653,22 @@ function buildApp(options = {}) {
28298
28653
  return reply.code(204).send();
28299
28654
  }
28300
28655
  });
28301
- app2.register(multipart, {
28302
- limits: {
28303
- files: MAX_PROMPT_ATTACHMENTS2,
28304
- 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
+ }
28305
28663
  }
28306
- });
28664
+ );
28307
28665
  const backendPluginHost = new BackendPluginHost(app2);
28308
28666
  backendPluginHost.register(createTerminalPluginBackendContribution());
28309
- const relaySocketBridge = createRelaySocketBridge(app2, eventBus, backendPluginHost);
28667
+ const relaySocketBridge = createRelaySocketBridge(
28668
+ app2,
28669
+ eventBus,
28670
+ backendPluginHost
28671
+ );
28310
28672
  const relayTunnelClient = config.mode === "relay" ? options.relayTunnelClient ?? new RelayTunnelClient(
28311
28673
  config.relay,
28312
28674
  createRelayRequestHandler(app2),
@@ -28314,6 +28676,31 @@ function buildApp(options = {}) {
28314
28676
  relaySocketBridge.handleMessage
28315
28677
  ) : null;
28316
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;
28317
28704
  app2.decorate("services", {
28318
28705
  config,
28319
28706
  database,
@@ -28494,6 +28881,7 @@ function buildApp(options = {}) {
28494
28881
  });
28495
28882
  });
28496
28883
  app2.addHook("onClose", async () => {
28884
+ cleanupRelayActivity?.();
28497
28885
  await shellService.stop();
28498
28886
  relayTunnelClient?.stop();
28499
28887
  await Promise.all(agentRuntimes.all().map((runtime) => runtime.stop()));
@@ -28666,6 +29054,21 @@ if (fs27.existsSync(".env")) {
28666
29054
  }
28667
29055
  var app = buildApp();
28668
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"));
28669
29072
  app.listen({ host, port }).then(() => {
28670
29073
  app.log.info(`Supervisor API listening on http://${host}:${port}`);
28671
29074
  }).catch((error) => {