remote-codex 0.11.31 → 0.11.33

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,74 @@ 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
+ subscriptionAuthKind = "unknown";
12506
+ subscriptionUsageObservedAt = null;
12376
12507
  clientApp;
12377
12508
  sdkLoadError = null;
12378
12509
  getStatus() {
12379
12510
  return { ...this.status };
12380
12511
  }
12512
+ async getSubscriptionUsage() {
12513
+ const observedAt = this.subscriptionUsageObservedAt ?? (/* @__PURE__ */ new Date()).toISOString();
12514
+ const windows = [...this.subscriptionUsageWindows.entries()].map(([id, window]) => ({
12515
+ id,
12516
+ durationMinutes: id === "five_hour" ? 300 : 10080,
12517
+ label: id === "five_hour" ? "5h" : "7d",
12518
+ usedPercent: window.usedPercent,
12519
+ resetsAt: window.resetsAt
12520
+ }));
12521
+ return {
12522
+ provider: "claude",
12523
+ authKind: this.subscriptionAuthKind,
12524
+ observedAt,
12525
+ stale: false,
12526
+ windows
12527
+ };
12528
+ }
12529
+ captureRateLimit(message) {
12530
+ if (message.type !== "rate_limit_event") {
12531
+ return;
12532
+ }
12533
+ const info = message.rate_limit_info;
12534
+ if (!info || info.rateLimitType !== "five_hour" && info.rateLimitType !== "seven_day" || typeof info.utilization !== "number" || !Number.isFinite(info.utilization)) {
12535
+ return;
12536
+ }
12537
+ this.subscriptionUsageWindows.set(info.rateLimitType, {
12538
+ usedPercent: Math.max(0, Math.min(100, info.utilization * 100)),
12539
+ resetsAt: typeof info.resetsAt === "number" ? new Date(info.resetsAt * 1e3).toISOString() : null
12540
+ });
12541
+ this.subscriptionAuthKind = "subscription";
12542
+ this.subscriptionUsageObservedAt = (/* @__PURE__ */ new Date()).toISOString();
12543
+ }
12544
+ async captureStructuredSubscriptionUsage(query) {
12545
+ const getUsage = query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
12546
+ if (!getUsage) {
12547
+ return;
12548
+ }
12549
+ try {
12550
+ const usage = await getUsage.call(query);
12551
+ const nextWindows = /* @__PURE__ */ new Map();
12552
+ const addWindow = (id, window) => {
12553
+ if (typeof window?.utilization !== "number" || !Number.isFinite(window.utilization)) {
12554
+ return;
12555
+ }
12556
+ nextWindows.set(id, {
12557
+ usedPercent: Math.max(0, Math.min(100, window.utilization)),
12558
+ resetsAt: typeof window.resets_at === "string" ? window.resets_at : null
12559
+ });
12560
+ };
12561
+ addWindow("five_hour", usage.rate_limits?.five_hour);
12562
+ addWindow("seven_day", usage.rate_limits?.seven_day);
12563
+ this.subscriptionUsageWindows.clear();
12564
+ for (const [id, window] of nextWindows) {
12565
+ this.subscriptionUsageWindows.set(id, window);
12566
+ }
12567
+ this.subscriptionAuthKind = usage.subscription_type ? "subscription" : usage.rate_limits_available ? "subscription" : "apiKey";
12568
+ this.subscriptionUsageObservedAt = (/* @__PURE__ */ new Date()).toISOString();
12569
+ } catch {
12570
+ }
12571
+ }
12381
12572
  updateToolboxItemsFromSystemInit(message) {
12382
12573
  this.managementSchema.toolboxItems = buildClaudeToolboxItems(
12383
12574
  normalizeClaudeSlashCommands(message.slash_commands)
@@ -12522,9 +12713,15 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12522
12713
  let providerSessionId = null;
12523
12714
  let model = input.model;
12524
12715
  const rawMessages = [];
12716
+ let capturedStructuredUsage = false;
12525
12717
  try {
12526
12718
  for await (const message of query) {
12527
12719
  rawMessages.push(message);
12720
+ if (!capturedStructuredUsage) {
12721
+ capturedStructuredUsage = true;
12722
+ await this.captureStructuredSubscriptionUsage(query);
12723
+ }
12724
+ this.captureRateLimit(message);
12528
12725
  if (message.type === "system" && message.subtype === "init") {
12529
12726
  this.updateToolboxItemsFromSystemInit(message);
12530
12727
  const sessionId = message.session_id;
@@ -12769,9 +12966,15 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12769
12966
  const rawMessages = [];
12770
12967
  let terminalStatus = null;
12771
12968
  let terminalError = null;
12969
+ let capturedStructuredUsage = false;
12772
12970
  try {
12773
12971
  for await (const message of state.query) {
12774
12972
  rawMessages.push(message);
12973
+ if (!capturedStructuredUsage) {
12974
+ capturedStructuredUsage = true;
12975
+ await this.captureStructuredSubscriptionUsage(state.query);
12976
+ }
12977
+ this.captureRateLimit(message);
12775
12978
  this.consumeMessage(state, message);
12776
12979
  const status = queryResultStatus(message);
12777
12980
  if (status) {
@@ -16074,6 +16277,7 @@ var ThreadAuxiliaryStateStore = class {
16074
16277
  clientRequestId: record.clientRequestId ?? null,
16075
16278
  turnId: record.turnId,
16076
16279
  prompt: record.displayPrompt,
16280
+ delivery: record.delivery === "continuation" ? "continuation" : "steer",
16077
16281
  createdAt: record.createdAt
16078
16282
  }));
16079
16283
  }
@@ -16090,6 +16294,11 @@ var ThreadAuxiliaryStateStore = class {
16090
16294
  hasPendingSteersForTurn(localThreadId, turnId) {
16091
16295
  return this.listPendingSteerRecordsForTurn(localThreadId, turnId).length > 0;
16092
16296
  }
16297
+ hasQueuedContinuationsForTurn(localThreadId, turnId) {
16298
+ return this.listPendingSteerRecordsForTurn(localThreadId, turnId).some(
16299
+ (record) => record.delivery === "continuation"
16300
+ );
16301
+ }
16093
16302
  deletePendingSteerRecord(localThreadId, id, turnId) {
16094
16303
  deleteThreadPendingSteerRecordById(this.db, id);
16095
16304
  this.callbacks.invalidateThreadDetailCache(localThreadId);
@@ -17889,6 +18098,7 @@ var ThreadRuntimeEventProjector = class {
17889
18098
  const turnItems = event.turn.items;
17890
18099
  updateThreadRecord(db, record.id, {
17891
18100
  providerTurnId: null,
18101
+ activeTurnCollaborationMode: null,
17892
18102
  status: event.turn.status === "failed" ? "failed" : event.turn.status === "interrupted" ? "interrupted" : "idle",
17893
18103
  lastError: event.turn.error?.message ?? null,
17894
18104
  lastTurnCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -17910,7 +18120,9 @@ var ThreadRuntimeEventProjector = class {
17910
18120
  }
17911
18121
  }
17912
18122
  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)) {
18123
+ if (event.turn.status === "completed" && !preservePendingSteers && callbacks.normalizeCollaborationMode(
18124
+ record.activeTurnCollaborationMode ?? record.collaborationMode
18125
+ ) === "plan" && turnItems.some((item) => item.kind === "plan") && !callbacks.hasPendingAskUserQuestion(record.id)) {
17914
18126
  callbacks.createPendingPlanDecisionRequest(record.id, turnId, true);
17915
18127
  } else {
17916
18128
  callbacks.dismissPlanDecisionTurn(record.id);
@@ -17942,6 +18154,7 @@ var ThreadRuntimeEventProjector = class {
17942
18154
  const turnId = liveState.displayTurnIdForRuntimeTurn(record.id, event.providerTurnId) ?? event.providerTurnId;
17943
18155
  updateThreadRecord(db, record.id, {
17944
18156
  providerTurnId: null,
18157
+ activeTurnCollaborationMode: null,
17945
18158
  status: "failed",
17946
18159
  lastError: event.error,
17947
18160
  lastTurnCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -19274,6 +19487,7 @@ var ThreadPromptTurnCoordinator = class {
19274
19487
  model: input.effectiveModel,
19275
19488
  reasoningEffort: input.normalizedReasoning,
19276
19489
  collaborationMode: input.collaborationMode,
19490
+ activeTurnCollaborationMode: input.collaborationMode,
19277
19491
  sandboxMode: input.sandboxMode
19278
19492
  };
19279
19493
  if (isAutoGeneratedTitle(record.title)) {
@@ -19396,7 +19610,8 @@ var ThreadPromptTurnCoordinator = class {
19396
19610
  turnId: steerTurnId,
19397
19611
  clientRequestId: input.clientRequestId,
19398
19612
  displayPrompt: input.displayPrompt,
19399
- submittedPrompt: input.prompt
19613
+ submittedPrompt: input.prompt,
19614
+ delivery: "steer"
19400
19615
  });
19401
19616
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19402
19617
  this.callbacks.emitThreadUpdated(localThreadId, {
@@ -19445,7 +19660,16 @@ var ThreadPromptTurnCoordinator = class {
19445
19660
  turnId: displayTurnId,
19446
19661
  clientRequestId: input.clientRequestId,
19447
19662
  displayPrompt: input.displayPrompt,
19448
- submittedPrompt: input.prompt
19663
+ submittedPrompt: input.prompt,
19664
+ delivery: "continuation",
19665
+ turnConfigJson: JSON.stringify({
19666
+ effectiveModel: input.effectiveModel,
19667
+ normalizedReasoning: input.normalizedReasoning,
19668
+ collaborationMode: input.collaborationMode,
19669
+ sandboxMode: input.sandboxMode,
19670
+ performanceMode: input.performanceMode,
19671
+ startNewTurn: input.collaborationMode !== (record.activeTurnCollaborationMode === "plan" ? "plan" : "default")
19672
+ })
19449
19673
  });
19450
19674
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19451
19675
  this.callbacks.emitThreadUpdated(localThreadId, {
@@ -19651,7 +19875,7 @@ var ThreadSessionCoordinator = class {
19651
19875
  if (!isRemoteThreadBootstrapError(error)) {
19652
19876
  throw error;
19653
19877
  }
19654
- return { status: "bootstrap_unavailable" };
19878
+ return { status: "bootstrap_unavailable", error };
19655
19879
  }
19656
19880
  const effectiveModel = input.resumeInput.model ?? input.currentModel ?? response.model ?? null;
19657
19881
  const resumedReasoning = this.providerRuntime.normalizeReasoningForModel(
@@ -19827,6 +20051,37 @@ var ThreadSessionLifecycleCoordinator = class {
19827
20051
  fastMode: record.fastMode
19828
20052
  });
19829
20053
  if (resumed.status === "bootstrap_unavailable") {
20054
+ if (!this.canRecreateUnmaterializedThread(record, resumed.error)) {
20055
+ return;
20056
+ }
20057
+ const workspace = getWorkspaceRecordById(this.db, record.workspaceId);
20058
+ const model = input.model ?? record.model;
20059
+ if (!workspace || !model) {
20060
+ return;
20061
+ }
20062
+ const recreated = await this.sessionCoordinator.startThreadSession({
20063
+ workspacePath: workspace.absPath,
20064
+ threadInput: {
20065
+ workspaceId: workspace.id,
20066
+ title: record.title,
20067
+ provider: record.provider,
20068
+ model,
20069
+ reasoningEffort: record.reasoningEffort,
20070
+ approvalMode: record.approvalMode ?? "yolo"
20071
+ },
20072
+ defaultTitle: record.title
20073
+ });
20074
+ updateThreadRecord(this.db, record.id, {
20075
+ ...buildThreadPatch(
20076
+ recreated.response.session,
20077
+ model,
20078
+ recreated.response.reasoningEffort ?? recreated.reasoningEffort
20079
+ ),
20080
+ providerSessionId: recreated.response.providerSessionId,
20081
+ sandboxMode: recreated.sandboxMode,
20082
+ isConnected: true
20083
+ });
20084
+ this.callbacks.invalidateThreadDetailCache(localThreadId);
19830
20085
  return;
19831
20086
  }
19832
20087
  updateThreadRecord(
@@ -19850,6 +20105,9 @@ var ThreadSessionLifecycleCoordinator = class {
19850
20105
  }
19851
20106
  this.callbacks.invalidateThreadDetailCache(localThreadId);
19852
20107
  }
20108
+ canRecreateUnmaterializedThread(record, error) {
20109
+ 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);
20110
+ }
19853
20111
  disconnectThread(localThreadId) {
19854
20112
  const record = getThreadRecordById(this.db, localThreadId);
19855
20113
  if (!record) {
@@ -20049,6 +20307,7 @@ var ThreadDeletionCoordinator = class {
20049
20307
  deleteThreadGoalRecordsByThreadId(this.db, localThreadId);
20050
20308
  deleteThreadHistoryItemRecordsByThreadId(this.db, localThreadId);
20051
20309
  deleteThreadPendingSteerRecordsByThreadId(this.db, localThreadId);
20310
+ deleteThreadPromptRequestRecordsByThreadId(this.db, localThreadId);
20052
20311
  deleteThreadTurnMetadataByThreadId(this.db, localThreadId);
20053
20312
  deleteThreadRecord(this.db, localThreadId);
20054
20313
  return { id: localThreadId };
@@ -21974,7 +22233,7 @@ var ThreadService = class {
21974
22233
  normalizeCollaborationMode,
21975
22234
  normalizeReasoningEffort: normalizeReasoningEffort2,
21976
22235
  normalizeThreadGoalStatusForThread: (goal, record) => this.goalCoordinator.normalizeThreadGoalStatusForThread(goal, record),
21977
- shouldPreservePendingSteersForCompletedTurn: (record, turnId) => !this.runtimeSupportsLiveRunningTurnInput(record.provider) && this.auxiliaryState.hasPendingSteersForTurn(record.id, turnId),
22236
+ shouldPreservePendingSteersForCompletedTurn: (record, turnId) => this.shouldPreserveCompletedPendingSteer(record.id, turnId),
21978
22237
  scheduleQueuedContinuationDrain: (localThreadId, turnId) => this.scheduleQueuedContinuationDrain(localThreadId, turnId),
21979
22238
  persistLiveHistoryItem: (localThreadId, turnId, item) => this.historyPersistence.persistLiveHistoryItem(localThreadId, turnId, item),
21980
22239
  persistFinalTurnOrderingHints: (localThreadId, turnId, items) => this.historyPersistence.persistFinalTurnOrderingHints(localThreadId, turnId, items),
@@ -22008,6 +22267,7 @@ var ThreadService = class {
22008
22267
  config;
22009
22268
  liveState = new ThreadLiveStateStore();
22010
22269
  queuedContinuationDrains = /* @__PURE__ */ new Set();
22270
+ promptRequestsInFlight = /* @__PURE__ */ new Map();
22011
22271
  detailAssembler;
22012
22272
  usageAccounting;
22013
22273
  requestCoordinator;
@@ -22326,6 +22586,57 @@ var ThreadService = class {
22326
22586
  return this.getThreadDetail(localThreadId);
22327
22587
  }
22328
22588
  async sendPrompt(localThreadId, input, options = {}) {
22589
+ const clientRequestId = input.clientRequestId?.trim();
22590
+ if (!clientRequestId) {
22591
+ return this.sendPromptOnce(localThreadId, input, options);
22592
+ }
22593
+ const requestKey = `${localThreadId}:${clientRequestId}`;
22594
+ const activeRequest = this.promptRequestsInFlight.get(requestKey);
22595
+ if (activeRequest) {
22596
+ return activeRequest;
22597
+ }
22598
+ const request = this.sendPromptIdempotently(
22599
+ localThreadId,
22600
+ { ...input, clientRequestId },
22601
+ options
22602
+ );
22603
+ this.promptRequestsInFlight.set(requestKey, request);
22604
+ try {
22605
+ return await request;
22606
+ } finally {
22607
+ if (this.promptRequestsInFlight.get(requestKey) === request) {
22608
+ this.promptRequestsInFlight.delete(requestKey);
22609
+ }
22610
+ }
22611
+ }
22612
+ async sendPromptIdempotently(localThreadId, input, options) {
22613
+ deleteExpiredThreadPromptRequestRecords(
22614
+ this.db,
22615
+ new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString()
22616
+ );
22617
+ const existing = getThreadPromptRequestRecord(
22618
+ this.db,
22619
+ localThreadId,
22620
+ input.clientRequestId
22621
+ );
22622
+ if (existing) {
22623
+ const record = this.requireThreadRecord(localThreadId);
22624
+ return this.toThreadDto(
22625
+ record,
22626
+ await this.listLoadedProviderSessionIds(record.provider)
22627
+ );
22628
+ }
22629
+ createThreadPromptRequestRecord(this.db, localThreadId, input.clientRequestId);
22630
+ try {
22631
+ const result = await this.sendPromptOnce(localThreadId, input, options);
22632
+ markThreadPromptRequestAccepted(this.db, localThreadId, input.clientRequestId);
22633
+ return result;
22634
+ } catch (error) {
22635
+ deleteThreadPromptRequestRecord(this.db, localThreadId, input.clientRequestId);
22636
+ throw error;
22637
+ }
22638
+ }
22639
+ async sendPromptOnce(localThreadId, input, options = {}) {
22329
22640
  let record = this.requireThreadRecord(localThreadId);
22330
22641
  await this.importCoordinator.assertImportedThreadReadyForPrompt({
22331
22642
  source: record.source,
@@ -22391,7 +22702,10 @@ var ThreadService = class {
22391
22702
  };
22392
22703
  const hasActiveProviderTurn = Boolean(record.providerTurnId) && (record.status === "running" || !turnConfig.supportsRunningTurnInput && !record.lastTurnCompletedAt) && record.status !== "failed" && record.status !== "interrupted";
22393
22704
  if (hasActiveProviderTurn && record.providerTurnId) {
22394
- if (!turnConfig.supportsRunningTurnInput) {
22705
+ const activeTurnCollaborationMode = normalizeCollaborationMode(
22706
+ record.activeTurnCollaborationMode ?? record.collaborationMode
22707
+ );
22708
+ if (!turnConfig.supportsRunningTurnInput || activeTurnCollaborationMode !== turnConfig.collaborationMode) {
22395
22709
  return this.promptTurnCoordinator.queueContinuationPromptTurn(localThreadId, {
22396
22710
  ...connectedRecord,
22397
22711
  providerTurnId: record.providerTurnId
@@ -22735,7 +23049,7 @@ var ThreadService = class {
22735
23049
  if (!record) {
22736
23050
  return false;
22737
23051
  }
22738
- return !this.runtimeSupportsLiveRunningTurnInput(record.provider) && this.auxiliaryState.hasPendingSteersForTurn(localThreadId, turnId);
23052
+ return this.auxiliaryState.hasQueuedContinuationsForTurn(localThreadId, turnId);
22739
23053
  }
22740
23054
  shouldPreserveMissingPendingSteer(localThreadId, turnId) {
22741
23055
  const record = getThreadRecordById(this.db, localThreadId);
@@ -22745,14 +23059,11 @@ var ThreadService = class {
22745
23059
  if (record.status === "failed" || record.status === "interrupted") {
22746
23060
  return false;
22747
23061
  }
22748
- if (this.runtimeSupportsLiveRunningTurnInput(record.provider)) {
22749
- return false;
22750
- }
22751
23062
  const activeDisplayTurnId = this.liveState.displayTurnIdForRuntimeTurn(
22752
23063
  localThreadId,
22753
23064
  record.providerTurnId
22754
23065
  );
22755
- return record.providerTurnId === turnId || activeDisplayTurnId === turnId;
23066
+ return (record.providerTurnId === turnId || activeDisplayTurnId === turnId) && this.auxiliaryState.hasQueuedContinuationsForTurn(localThreadId, turnId);
22756
23067
  }
22757
23068
  scheduleQueuedContinuationDrain(localThreadId, turnId) {
22758
23069
  const key = `${localThreadId}:${turnId}`;
@@ -22776,7 +23087,7 @@ var ThreadService = class {
22776
23087
  const pending = this.auxiliaryState.listPendingSteerRecordsForTurn(
22777
23088
  localThreadId,
22778
23089
  turnId
22779
- )[0];
23090
+ ).find((entry) => entry.delivery === "continuation");
22780
23091
  if (!pending) {
22781
23092
  return;
22782
23093
  }
@@ -22795,7 +23106,8 @@ var ThreadService = class {
22795
23106
  const developerInstructions = combineDeveloperInstructions([
22796
23107
  pluginDeveloperInstructions(this.pluginService)
22797
23108
  ]);
22798
- const turnConfig = await this.sessionCoordinator.resolvePromptTurnConfig({
23109
+ const queuedConfig = parseQueuedTurnConfig(pending.turnConfigJson);
23110
+ const turnConfig = queuedConfig ?? await this.sessionCoordinator.resolvePromptTurnConfig({
22799
23111
  provider: record.provider,
22800
23112
  currentModel: record.model,
22801
23113
  currentReasoningEffort: record.reasoningEffort,
@@ -22805,14 +23117,16 @@ var ThreadService = class {
22805
23117
  approvalMode: record.approvalMode ?? "yolo",
22806
23118
  promptInput: {}
22807
23119
  });
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
- });
23120
+ if (!queuedConfig?.startNewTurn) {
23121
+ const queuedUserItemId = `queued-continuation:${pending.id}:user`;
23122
+ this.historyPersistence.persistProjectedHistoryItem(localThreadId, turnId, {
23123
+ id: queuedUserItemId,
23124
+ kind: "userMessage",
23125
+ text: pending.displayPrompt,
23126
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
23127
+ sequence: this.liveState.recordTurnItemOrder(localThreadId, turnId, queuedUserItemId)
23128
+ });
23129
+ }
22816
23130
  await this.promptTurnCoordinator.startPromptTurn(localThreadId, {
22817
23131
  ...record,
22818
23132
  providerSessionId
@@ -22826,8 +23140,7 @@ var ThreadService = class {
22826
23140
  sandboxMode: turnConfig.sandboxMode,
22827
23141
  performanceMode: turnConfig.performanceMode,
22828
23142
  workspacePath: workspace.absPath,
22829
- hidden: true,
22830
- displayTurnId: turnId
23143
+ ...queuedConfig?.startNewTurn ? {} : { hidden: true, displayTurnId: turnId }
22831
23144
  });
22832
23145
  this.auxiliaryState.deletePendingSteerRecord(localThreadId, pending.id, turnId);
22833
23146
  }
@@ -22915,6 +23228,27 @@ var ThreadService = class {
22915
23228
  );
22916
23229
  }
22917
23230
  };
23231
+ function parseQueuedTurnConfig(value) {
23232
+ if (!value) {
23233
+ return null;
23234
+ }
23235
+ try {
23236
+ const parsed = JSON.parse(value);
23237
+ if (parsed.collaborationMode !== "default" && parsed.collaborationMode !== "plan" || parsed.performanceMode !== "fast" && parsed.performanceMode !== "standard" || typeof parsed.startNewTurn !== "boolean") {
23238
+ return null;
23239
+ }
23240
+ return {
23241
+ effectiveModel: typeof parsed.effectiveModel === "string" ? parsed.effectiveModel : null,
23242
+ normalizedReasoning: parsed.normalizedReasoning ?? null,
23243
+ collaborationMode: parsed.collaborationMode,
23244
+ sandboxMode: parsed.sandboxMode ?? "workspace-write",
23245
+ performanceMode: parsed.performanceMode,
23246
+ startNewTurn: parsed.startNewTurn
23247
+ };
23248
+ } catch {
23249
+ return null;
23250
+ }
23251
+ }
22918
23252
 
22919
23253
  // src/routes/agent-runtimes.ts
22920
23254
  import fs15 from "fs/promises";
@@ -22987,6 +23321,17 @@ async function registerAgentRuntimeRoutes(app2) {
22987
23321
  const { provider: provider2 } = providerParamSchema.parse(request.params);
22988
23322
  return runtimeDto(app2, provider2);
22989
23323
  });
23324
+ app2.get("/api/agent-runtimes/:provider/subscription-usage", async (request) => {
23325
+ const { provider: provider2 } = providerParamSchema.parse(request.params);
23326
+ const runtime = app2.services.agentRuntimes.getOptional(provider2);
23327
+ if (!runtime) {
23328
+ throw providerNotConfigured(provider2);
23329
+ }
23330
+ if (!runtime.getSubscriptionUsage) {
23331
+ return { usage: null };
23332
+ }
23333
+ return { usage: await runtime.getSubscriptionUsage() };
23334
+ });
22990
23335
  app2.post("/api/agent-runtimes/:provider/restart", async (request) => {
22991
23336
  const { provider: provider2 } = providerParamSchema.parse(request.params);
22992
23337
  const runtime = app2.services.agentRuntimes.getOptional(provider2);
@@ -23393,9 +23738,11 @@ function parseProviderHostFileParams(params) {
23393
23738
  }
23394
23739
  async function registerSystemRoutes(app2) {
23395
23740
  app2.get("/healthz", async () => {
23741
+ const activeTurnCount = app2.services.database.sqlite.prepare("SELECT COUNT(*) AS count FROM threads WHERE status = 'running'").get();
23396
23742
  return {
23397
23743
  status: "ok",
23398
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
23744
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
23745
+ activeTurnCount: activeTurnCount.count
23399
23746
  };
23400
23747
  });
23401
23748
  app2.get("/readyz", async () => {
@@ -25017,7 +25364,16 @@ async function registerWorkspaceRoutes(app2) {
25017
25364
  await cloneRepository(body.gitUrl.trim(), targetPath);
25018
25365
  validated = await validateWorkspacePath(app2.services.config.workspaceRoot, targetPath);
25019
25366
  } else {
25020
- validated = await validateWorkspacePath(app2.services.config.workspaceRoot, body.absPath, {
25367
+ const requestedPath = body.absPath.trim();
25368
+ const isWorkspaceName = !path22.isAbsolute(requestedPath) && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(requestedPath) && requestedPath !== "." && requestedPath !== "..";
25369
+ if (!path22.isAbsolute(requestedPath) && !isWorkspaceName) {
25370
+ throw new HttpError(400, {
25371
+ code: "bad_request",
25372
+ message: "Use a simple directory name, an absolute path, or a Git URL."
25373
+ });
25374
+ }
25375
+ const targetPath = isWorkspaceName ? path22.join(settings.devHome, requestedPath) : requestedPath;
25376
+ validated = await validateWorkspacePath(app2.services.config.workspaceRoot, targetPath, {
25021
25377
  devHome: settings.devHome,
25022
25378
  createMissingLeaf: true
25023
25379
  });
@@ -27969,6 +28325,7 @@ var RelayTunnelClient = class {
27969
28325
  reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
27970
28326
  stopped = false;
27971
28327
  relayClientCleanup = /* @__PURE__ */ new Map();
28328
+ pendingActivity = /* @__PURE__ */ new Map();
27972
28329
  validateConfig() {
27973
28330
  if (!this.config.serverUrl || !this.config.agentToken) {
27974
28331
  throw new Error(
@@ -27983,7 +28340,10 @@ var RelayTunnelClient = class {
27983
28340
  if (this.socket) {
27984
28341
  return;
27985
28342
  }
27986
- const url = new URL("/supervisor/tunnel", this.config.serverUrl ?? void 0);
28343
+ const url = new URL(
28344
+ "/supervisor/tunnel",
28345
+ this.config.serverUrl ?? void 0
28346
+ );
27987
28347
  url.searchParams.set("token", this.config.agentToken ?? "");
27988
28348
  url.searchParams.set("deviceToken", this.config.agentToken ?? "");
27989
28349
  const socket = new WebSocket(url);
@@ -27997,6 +28357,7 @@ var RelayTunnelClient = class {
27997
28357
  this.clearConnectTimeout();
27998
28358
  this.reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
27999
28359
  this.sendHeartbeat();
28360
+ this.flushPendingActivity();
28000
28361
  this.clearHeartbeat();
28001
28362
  this.heartbeatHandle = setInterval(() => {
28002
28363
  this.sendHeartbeat();
@@ -28021,6 +28382,29 @@ var RelayTunnelClient = class {
28021
28382
  this.socket?.close();
28022
28383
  this.socket = null;
28023
28384
  }
28385
+ sendActivity(payload) {
28386
+ const key = `${payload.threadId}\0${payload.turnId}`;
28387
+ const socket = this.socket;
28388
+ if (socket?.readyState !== WebSocket.OPEN) {
28389
+ this.pendingActivity.set(key, payload);
28390
+ return;
28391
+ }
28392
+ const sent = this.sendEnvelope(socket, {
28393
+ type: "relay.activity",
28394
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
28395
+ payload
28396
+ });
28397
+ if (sent) {
28398
+ this.pendingActivity.delete(key);
28399
+ } else {
28400
+ this.pendingActivity.set(key, payload);
28401
+ }
28402
+ }
28403
+ flushPendingActivity() {
28404
+ for (const payload of this.pendingActivity.values()) {
28405
+ this.sendActivity(payload);
28406
+ }
28407
+ }
28024
28408
  sendHeartbeat() {
28025
28409
  const socket = this.socket;
28026
28410
  if (socket?.readyState !== WebSocket.OPEN) {
@@ -28040,9 +28424,12 @@ var RelayTunnelClient = class {
28040
28424
  }
28041
28425
  if (parsed.type !== "relay.request") {
28042
28426
  if (parsed.type === "relay.client.connected") {
28043
- const cleanup = this.handleClientConnected(parsed.clientId, (message) => {
28044
- this.sendClientMessage(parsed.clientId, message);
28045
- });
28427
+ const cleanup = this.handleClientConnected(
28428
+ parsed.clientId,
28429
+ (message) => {
28430
+ this.sendClientMessage(parsed.clientId, message);
28431
+ }
28432
+ );
28046
28433
  this.relayClientCleanup.set(parsed.clientId, cleanup);
28047
28434
  return;
28048
28435
  }
@@ -28052,9 +28439,13 @@ var RelayTunnelClient = class {
28052
28439
  return;
28053
28440
  }
28054
28441
  if (parsed.type === "relay.client.message") {
28055
- await this.handleClientMessage(parsed.clientId, parsed.payload, (message) => {
28056
- this.sendClientMessage(parsed.clientId, message);
28057
- });
28442
+ await this.handleClientMessage(
28443
+ parsed.clientId,
28444
+ parsed.payload,
28445
+ (message) => {
28446
+ this.sendClientMessage(parsed.clientId, message);
28447
+ }
28448
+ );
28058
28449
  return;
28059
28450
  }
28060
28451
  return;
@@ -28064,15 +28455,12 @@ var RelayTunnelClient = class {
28064
28455
  if (socket?.readyState !== WebSocket.OPEN) {
28065
28456
  return;
28066
28457
  }
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
- );
28458
+ this.sendEnvelope(socket, {
28459
+ type: "relay.response",
28460
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
28461
+ requestId: parsed.requestId,
28462
+ payload: response
28463
+ });
28076
28464
  }
28077
28465
  sendClientMessage(clientId, message) {
28078
28466
  const socket = this.socket;
@@ -28089,8 +28477,10 @@ var RelayTunnelClient = class {
28089
28477
  sendEnvelope(socket, message) {
28090
28478
  try {
28091
28479
  socket.send(JSON.stringify(message));
28480
+ return true;
28092
28481
  } catch {
28093
28482
  this.closeAndReconnect(socket);
28483
+ return false;
28094
28484
  }
28095
28485
  }
28096
28486
  closeAndReconnect(socket) {
@@ -28158,10 +28548,7 @@ var DEFAULT_WEBVIEW_CORS_ORIGINS = /* @__PURE__ */ new Set([
28158
28548
  "https://localhost",
28159
28549
  "https://appassets.androidplatform.net"
28160
28550
  ]);
28161
- var WEBVIEW_CORS_ALLOW_HEADERS = [
28162
- "authorization",
28163
- "content-type"
28164
- ].join(", ");
28551
+ var WEBVIEW_CORS_ALLOW_HEADERS = ["authorization", "content-type"].join(", ");
28165
28552
  var WEBVIEW_CORS_ALLOW_METHODS = [
28166
28553
  "GET",
28167
28554
  "POST",
@@ -28175,7 +28562,9 @@ function webViewCorsOrigins(env) {
28175
28562
  return null;
28176
28563
  }
28177
28564
  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);
28565
+ return new Set(
28566
+ configured?.length ? configured : DEFAULT_WEBVIEW_CORS_ORIGINS
28567
+ );
28179
28568
  }
28180
28569
  function applyWebViewCorsHeaders(reply, origin) {
28181
28570
  reply.header("access-control-allow-origin", origin);
@@ -28224,7 +28613,11 @@ function createServiceLifecycle() {
28224
28613
  });
28225
28614
  }
28226
28615
  const repoRoot = findRepoRoot();
28227
- const restartScript = path28.join(repoRoot, "scripts", "service-restart.mjs");
28616
+ const restartScript = path28.join(
28617
+ repoRoot,
28618
+ "scripts",
28619
+ "service-restart.mjs"
28620
+ );
28228
28621
  if (!fs26.existsSync(restartScript) || !fs26.existsSync(path28.join(repoRoot, "pnpm-workspace.yaml"))) {
28229
28622
  throw new HttpError(503, {
28230
28623
  code: "service_unavailable",
@@ -28284,7 +28677,9 @@ function buildApp(options = {}) {
28284
28677
  },
28285
28678
  disableRequestLogging: config.disableRequestLogging
28286
28679
  });
28287
- const allowedWebViewCorsOrigins = webViewCorsOrigins(options.env ?? process.env);
28680
+ const allowedWebViewCorsOrigins = webViewCorsOrigins(
28681
+ options.env ?? process.env
28682
+ );
28288
28683
  app2.addHook("onRequest", async (request, reply) => {
28289
28684
  if (!allowedWebViewCorsOrigins) {
28290
28685
  return;
@@ -28298,15 +28693,22 @@ function buildApp(options = {}) {
28298
28693
  return reply.code(204).send();
28299
28694
  }
28300
28695
  });
28301
- app2.register(multipart, {
28302
- limits: {
28303
- files: MAX_PROMPT_ATTACHMENTS2,
28304
- fileSize: MAX_PROMPT_ATTACHMENT_BYTES2
28696
+ app2.register(
28697
+ multipart,
28698
+ {
28699
+ limits: {
28700
+ files: MAX_PROMPT_ATTACHMENTS2,
28701
+ fileSize: MAX_PROMPT_ATTACHMENT_BYTES2
28702
+ }
28305
28703
  }
28306
- });
28704
+ );
28307
28705
  const backendPluginHost = new BackendPluginHost(app2);
28308
28706
  backendPluginHost.register(createTerminalPluginBackendContribution());
28309
- const relaySocketBridge = createRelaySocketBridge(app2, eventBus, backendPluginHost);
28707
+ const relaySocketBridge = createRelaySocketBridge(
28708
+ app2,
28709
+ eventBus,
28710
+ backendPluginHost
28711
+ );
28310
28712
  const relayTunnelClient = config.mode === "relay" ? options.relayTunnelClient ?? new RelayTunnelClient(
28311
28713
  config.relay,
28312
28714
  createRelayRequestHandler(app2),
@@ -28314,6 +28716,31 @@ function buildApp(options = {}) {
28314
28716
  relaySocketBridge.handleMessage
28315
28717
  ) : null;
28316
28718
  relayTunnelClient?.validateConfig();
28719
+ const cleanupRelayActivity = relayTunnelClient ? eventBus.onThreadEvent((event) => {
28720
+ if (event.type === "thread.turn.started") {
28721
+ relayTunnelClient.sendActivity({
28722
+ kind: "turn_started",
28723
+ threadId: event.threadId,
28724
+ turnId: event.payload.turnId
28725
+ });
28726
+ return;
28727
+ }
28728
+ if (event.type === "thread.turn.completed") {
28729
+ relayTunnelClient.sendActivity({
28730
+ kind: "turn_terminal",
28731
+ threadId: event.threadId,
28732
+ turnId: event.payload.turnId
28733
+ });
28734
+ return;
28735
+ }
28736
+ if (event.type === "thread.turn.failed" && event.payload.willRetry !== true) {
28737
+ relayTunnelClient.sendActivity({
28738
+ kind: "turn_terminal",
28739
+ threadId: event.threadId,
28740
+ turnId: event.payload.turnId
28741
+ });
28742
+ }
28743
+ }) : null;
28317
28744
  app2.decorate("services", {
28318
28745
  config,
28319
28746
  database,
@@ -28494,6 +28921,7 @@ function buildApp(options = {}) {
28494
28921
  });
28495
28922
  });
28496
28923
  app2.addHook("onClose", async () => {
28924
+ cleanupRelayActivity?.();
28497
28925
  await shellService.stop();
28498
28926
  relayTunnelClient?.stop();
28499
28927
  await Promise.all(agentRuntimes.all().map((runtime) => runtime.stop()));
@@ -28666,6 +29094,21 @@ if (fs27.existsSync(".env")) {
28666
29094
  }
28667
29095
  var app = buildApp();
28668
29096
  var { host, port } = app.services.config;
29097
+ var closing = false;
29098
+ async function shutdown(signal) {
29099
+ if (closing) return;
29100
+ closing = true;
29101
+ app.log.info(`Supervisor API received ${signal}; closing cleanly.`);
29102
+ try {
29103
+ await app.close();
29104
+ process.exit(0);
29105
+ } catch (error) {
29106
+ app.log.error(error);
29107
+ process.exit(1);
29108
+ }
29109
+ }
29110
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
29111
+ process.once("SIGINT", () => void shutdown("SIGINT"));
28669
29112
  app.listen({ host, port }).then(() => {
28670
29113
  app.log.info(`Supervisor API listening on http://${host}:${port}`);
28671
29114
  }).catch((error) => {