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.
@@ -10,16 +10,16 @@
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
11
11
  <link rel="manifest" href="/site.webmanifest" />
12
12
  <title>Remote Codex</title>
13
- <script type="module" crossorigin src="/assets/index-BkwJP0KB.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-DIi8wTEY.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-Dfg_6BLf.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/ui-vendor-CuR8GHb0.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVQUpZ8C.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/terminal-vendor-C5bTa-Ka.js">
18
18
  <link rel="modulepreload" crossorigin href="/assets/markdown-vendor-RZk8L7-L.js">
19
- <link rel="modulepreload" crossorigin href="/assets/thread-ui-BOqbjoiB.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/thread-ui-B9eC2H4u.js">
20
20
  <link rel="stylesheet" crossorigin href="/assets/graph-vendor-C5ap-Sga.css">
21
21
  <link rel="stylesheet" crossorigin href="/assets/terminal-vendor-Beg8tuEN.css">
22
- <link rel="stylesheet" crossorigin href="/assets/index-CJFMmjP5.css">
22
+ <link rel="stylesheet" crossorigin href="/assets/index-CdjTdnJt.css">
23
23
  </head>
24
24
  <body class="bg-stone-950">
25
25
  <div id="root"></div>
@@ -851,9 +851,9 @@ What it starts:
851
851
  API http://SERVICE_API_HOST:SERVICE_API_PORT
852
852
 
853
853
  Environment:
854
- SERVICE_HOST Web listen host. Default 127.0.0.1.
854
+ SERVICE_HOST Web listen host. Default 0.0.0.0.
855
855
  SERVICE_PORT Web listen port. Default ${defaultServicePort}.
856
- SERVICE_API_HOST API listen host. Default 127.0.0.1.
856
+ SERVICE_API_HOST API listen host. Default 0.0.0.0.
857
857
  SERVICE_API_PORT API listen port. Default ${defaultApiPort}.
858
858
  REMOTE_CODEX_SERVICE_DIR Service state/log directory.
859
859
  LOG_LEVEL API log level. Default warn for service mode.
@@ -871,7 +871,10 @@ Supervisor configuration forwarded to the API:
871
871
  OPENCODE_COMMAND OpenCode executable. Default opencode.
872
872
 
873
873
  Example:
874
- SERVICE_HOST=127.0.0.1 SERVICE_PORT=4173 remote-codex start
874
+ remote-codex start
875
+
876
+ Restrict the service to this machine:
877
+ SERVICE_HOST=127.0.0.1 SERVICE_API_HOST=127.0.0.1 remote-codex start
875
878
 
876
879
  Expose over Tailscale after start:
877
880
  tailscale serve --bg http://127.0.0.1:${defaultServicePort}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.31",
3
+ "version": "0.11.33",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -582,4 +582,17 @@ export interface AgentRuntime extends EventEmitter {
582
582
  getGoal?(providerSessionId: string): Promise<AgentGoal | null>;
583
583
  setGoal?(input: SetAgentGoalInput): Promise<AgentGoal>;
584
584
  clearGoal?(providerSessionId: string): Promise<boolean>;
585
+ getSubscriptionUsage?(): Promise<{
586
+ provider: 'codex' | 'claude';
587
+ authKind: 'subscription' | 'apiKey' | 'unknown';
588
+ observedAt: string;
589
+ stale: boolean;
590
+ windows: Array<{
591
+ id: string;
592
+ durationMinutes: number | null;
593
+ label: string;
594
+ usedPercent: number;
595
+ resetsAt: string | null;
596
+ }>;
597
+ } | null>;
585
598
  }
@@ -15,6 +15,7 @@ interface Query extends AsyncIterable<SDKMessage> {
15
15
  interrupt(): Promise<void>;
16
16
  supportedModels(): Promise<any[]>;
17
17
  mcpServerStatus(): Promise<any[]>;
18
+ usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(): Promise<any>;
18
19
  }
19
20
 
20
21
  function wait(ms = 0) {
@@ -29,7 +30,7 @@ class FakeQuery implements Query {
29
30
 
30
31
  constructor(
31
32
  private readonly messages: SDKMessage[],
32
- private readonly options: { holdOpen?: boolean } = {},
33
+ private readonly options: { holdOpen?: boolean; usage?: Record<string, any> } = {},
33
34
  ) {}
34
35
 
35
36
  [Symbol.asyncIterator]() {
@@ -104,6 +105,12 @@ class FakeQuery implements Query {
104
105
  async getContextUsage(): Promise<any> {
105
106
  return {};
106
107
  }
108
+ async usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(): Promise<any> {
109
+ if (!this.options.usage) {
110
+ throw new Error('Structured usage unavailable in this fixture.');
111
+ }
112
+ return this.options.usage;
113
+ }
107
114
  async readFile(): Promise<any> {
108
115
  return {};
109
116
  }
@@ -208,6 +215,119 @@ function makeAdapter(
208
215
  }
209
216
 
210
217
  describe('ClaudeRuntimeAdapter', () => {
218
+ it('actively reads Claude subscription windows from the structured usage API', async () => {
219
+ const adapter = makeAdapter(() => new FakeQuery(
220
+ [systemInit(), result()],
221
+ {
222
+ usage: {
223
+ subscription_type: 'max',
224
+ rate_limits_available: true,
225
+ rate_limits: {
226
+ five_hour: {
227
+ utilization: 23,
228
+ resets_at: '2027-01-15T08:00:00.000Z',
229
+ },
230
+ seven_day: {
231
+ utilization: 84,
232
+ resets_at: '2027-01-20T08:00:00.000Z',
233
+ },
234
+ },
235
+ },
236
+ },
237
+ ));
238
+
239
+ await adapter.start();
240
+ await adapter.startSession({
241
+ cwd: '/tmp/workspace',
242
+ model: 'sonnet',
243
+ approvalMode: 'guarded',
244
+ sandboxMode: 'workspace-write',
245
+ });
246
+
247
+ await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
248
+ provider: 'claude',
249
+ authKind: 'subscription',
250
+ windows: [
251
+ {
252
+ id: 'five_hour',
253
+ label: '5h',
254
+ usedPercent: 23,
255
+ resetsAt: '2027-01-15T08:00:00.000Z',
256
+ },
257
+ {
258
+ id: 'seven_day',
259
+ label: '7d',
260
+ usedPercent: 84,
261
+ resetsAt: '2027-01-20T08:00:00.000Z',
262
+ },
263
+ ],
264
+ });
265
+ });
266
+
267
+ it('captures Claude subscription rate-limit windows from SDK events', async () => {
268
+ const adapter = makeAdapter(() => [
269
+ systemInit(),
270
+ {
271
+ type: 'rate_limit_event',
272
+ rate_limit_info: {
273
+ status: 'allowed',
274
+ rateLimitType: 'five_hour',
275
+ utilization: 0.23,
276
+ resetsAt: 1_800_000_000,
277
+ },
278
+ uuid: '00000000-0000-4000-8000-000000000003',
279
+ session_id: 'claude-session-1',
280
+ },
281
+ {
282
+ type: 'rate_limit_event',
283
+ rate_limit_info: {
284
+ status: 'allowed_warning',
285
+ rateLimitType: 'seven_day',
286
+ utilization: 0.84,
287
+ resetsAt: 1_800_086_400,
288
+ },
289
+ uuid: '00000000-0000-4000-8000-000000000004',
290
+ session_id: 'claude-session-1',
291
+ },
292
+ result(),
293
+ ]);
294
+
295
+ await adapter.start();
296
+ await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
297
+ provider: 'claude',
298
+ authKind: 'unknown',
299
+ windows: [],
300
+ });
301
+ await adapter.startSession({
302
+ cwd: '/tmp/workspace',
303
+ model: 'sonnet',
304
+ approvalMode: 'guarded',
305
+ sandboxMode: 'workspace-write',
306
+ });
307
+
308
+ await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
309
+ provider: 'claude',
310
+ authKind: 'subscription',
311
+ stale: false,
312
+ windows: [
313
+ {
314
+ id: 'five_hour',
315
+ durationMinutes: 300,
316
+ label: '5h',
317
+ usedPercent: 23,
318
+ resetsAt: new Date(1_800_000_000 * 1000).toISOString(),
319
+ },
320
+ {
321
+ id: 'seven_day',
322
+ durationMinutes: 10_080,
323
+ label: '7d',
324
+ usedPercent: 84,
325
+ resetsAt: new Date(1_800_086_400 * 1000).toISOString(),
326
+ },
327
+ ],
328
+ });
329
+ });
330
+
211
331
  it('passes the configured Claude executable to the SDK', async () => {
212
332
  const sdkOptions: Record<string, unknown>[] = [];
213
333
  const adapter = new ClaudeRuntimeAdapter({
@@ -99,6 +99,19 @@ interface Query extends AsyncIterable<SDKMessage> {
99
99
  interrupt(): Promise<void>;
100
100
  supportedModels(): Promise<ModelInfo[]>;
101
101
  mcpServerStatus(): Promise<McpServerStatus[]>;
102
+ usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET?(): Promise<SDKUsageResponse>;
103
+ }
104
+ interface SDKUsageWindow {
105
+ utilization: number | null;
106
+ resets_at: string | null;
107
+ }
108
+ interface SDKUsageResponse {
109
+ subscription_type: string | null;
110
+ rate_limits_available: boolean;
111
+ rate_limits: {
112
+ five_hour?: SDKUsageWindow | null;
113
+ seven_day?: SDKUsageWindow | null;
114
+ } | null;
102
115
  }
103
116
  interface SDKMessage {
104
117
  type: string;
@@ -119,6 +132,19 @@ interface SDKMessage {
119
132
  modelUsage?: unknown;
120
133
  errors?: string[];
121
134
  stop_reason?: string;
135
+ rate_limit_info?: SDKRateLimitInfo;
136
+ }
137
+ interface SDKRateLimitInfo {
138
+ status: 'allowed' | 'allowed_warning' | 'rejected';
139
+ resetsAt?: number;
140
+ rateLimitType?:
141
+ | 'five_hour'
142
+ | 'seven_day'
143
+ | 'seven_day_opus'
144
+ | 'seven_day_sonnet'
145
+ | 'seven_day_overage_included'
146
+ | 'overage';
147
+ utilization?: number;
122
148
  }
123
149
  interface SDKUserMessage {
124
150
  type: 'user';
@@ -1222,6 +1248,12 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1222
1248
  private readonly sessionModels = new Map<string, string | null>();
1223
1249
  private readonly sessionApprovalModes = new Map<string, StartAgentSessionInput['approvalMode']>();
1224
1250
  private readonly liveUserPrompts = new Map<string, Map<string, string>>();
1251
+ private readonly subscriptionUsageWindows = new Map<
1252
+ 'five_hour' | 'seven_day',
1253
+ { usedPercent: number; resetsAt: string | null }
1254
+ >();
1255
+ private subscriptionAuthKind: 'subscription' | 'apiKey' | 'unknown' = 'unknown';
1256
+ private subscriptionUsageObservedAt: string | null = null;
1225
1257
  private readonly clientApp: string;
1226
1258
  private sdkLoadError: string | null = null;
1227
1259
 
@@ -1242,6 +1274,93 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1242
1274
  return { ...this.status };
1243
1275
  }
1244
1276
 
1277
+ async getSubscriptionUsage() {
1278
+ const observedAt = this.subscriptionUsageObservedAt ?? new Date().toISOString();
1279
+ const windows = [...this.subscriptionUsageWindows.entries()].map(([id, window]) => ({
1280
+ id,
1281
+ durationMinutes: id === 'five_hour' ? 300 : 10_080,
1282
+ label: id === 'five_hour' ? '5h' : '7d',
1283
+ usedPercent: window.usedPercent,
1284
+ resetsAt: window.resetsAt,
1285
+ }));
1286
+ return {
1287
+ provider: 'claude' as const,
1288
+ authKind: this.subscriptionAuthKind,
1289
+ observedAt,
1290
+ stale: false,
1291
+ windows,
1292
+ };
1293
+ }
1294
+
1295
+ private captureRateLimit(message: SDKMessage) {
1296
+ if (message.type !== 'rate_limit_event') {
1297
+ return;
1298
+ }
1299
+ const info = message.rate_limit_info;
1300
+ if (
1301
+ !info
1302
+ || (info.rateLimitType !== 'five_hour' && info.rateLimitType !== 'seven_day')
1303
+ || typeof info.utilization !== 'number'
1304
+ || !Number.isFinite(info.utilization)
1305
+ ) {
1306
+ return;
1307
+ }
1308
+ this.subscriptionUsageWindows.set(info.rateLimitType, {
1309
+ usedPercent: Math.max(0, Math.min(100, info.utilization * 100)),
1310
+ resetsAt: typeof info.resetsAt === 'number'
1311
+ ? new Date(info.resetsAt * 1000).toISOString()
1312
+ : null,
1313
+ });
1314
+ this.subscriptionAuthKind = 'subscription';
1315
+ this.subscriptionUsageObservedAt = new Date().toISOString();
1316
+ }
1317
+
1318
+ private async captureStructuredSubscriptionUsage(query: Query) {
1319
+ const getUsage = query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
1320
+ if (!getUsage) {
1321
+ return;
1322
+ }
1323
+
1324
+ try {
1325
+ const usage = await getUsage.call(query);
1326
+ const nextWindows = new Map<
1327
+ 'five_hour' | 'seven_day',
1328
+ { usedPercent: number; resetsAt: string | null }
1329
+ >();
1330
+ const addWindow = (
1331
+ id: 'five_hour' | 'seven_day',
1332
+ window: SDKUsageWindow | null | undefined,
1333
+ ) => {
1334
+ if (
1335
+ typeof window?.utilization !== 'number'
1336
+ || !Number.isFinite(window.utilization)
1337
+ ) {
1338
+ return;
1339
+ }
1340
+ nextWindows.set(id, {
1341
+ usedPercent: Math.max(0, Math.min(100, window.utilization)),
1342
+ resetsAt: typeof window.resets_at === 'string' ? window.resets_at : null,
1343
+ });
1344
+ };
1345
+ addWindow('five_hour', usage.rate_limits?.five_hour);
1346
+ addWindow('seven_day', usage.rate_limits?.seven_day);
1347
+
1348
+ this.subscriptionUsageWindows.clear();
1349
+ for (const [id, window] of nextWindows) {
1350
+ this.subscriptionUsageWindows.set(id, window);
1351
+ }
1352
+ this.subscriptionAuthKind = usage.subscription_type
1353
+ ? 'subscription'
1354
+ : usage.rate_limits_available
1355
+ ? 'subscription'
1356
+ : 'apiKey';
1357
+ this.subscriptionUsageObservedAt = new Date().toISOString();
1358
+ } catch {
1359
+ // This SDK method is experimental. Rate-limit events remain the
1360
+ // compatibility fallback for Claude versions that do not expose it.
1361
+ }
1362
+ }
1363
+
1245
1364
  private updateToolboxItemsFromSystemInit(message: SDKMessage) {
1246
1365
  this.managementSchema.toolboxItems = buildClaudeToolboxItems(
1247
1366
  normalizeClaudeSlashCommands(message.slash_commands),
@@ -1405,9 +1524,15 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1405
1524
  let providerSessionId: string | null = null;
1406
1525
  let model: string | null = input.model;
1407
1526
  const rawMessages: SDKMessage[] = [];
1527
+ let capturedStructuredUsage = false;
1408
1528
  try {
1409
1529
  for await (const message of query) {
1410
1530
  rawMessages.push(message);
1531
+ if (!capturedStructuredUsage) {
1532
+ capturedStructuredUsage = true;
1533
+ await this.captureStructuredSubscriptionUsage(query);
1534
+ }
1535
+ this.captureRateLimit(message);
1411
1536
  if (message.type === 'system' && message.subtype === 'init') {
1412
1537
  this.updateToolboxItemsFromSystemInit(message);
1413
1538
  const sessionId = message.session_id;
@@ -1696,9 +1821,15 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
1696
1821
  const rawMessages: SDKMessage[] = [];
1697
1822
  let terminalStatus: AgentTurn['status'] | null = null;
1698
1823
  let terminalError: string | null = null;
1824
+ let capturedStructuredUsage = false;
1699
1825
  try {
1700
1826
  for await (const message of state.query) {
1701
1827
  rawMessages.push(message);
1828
+ if (!capturedStructuredUsage) {
1829
+ capturedStructuredUsage = true;
1830
+ await this.captureStructuredSubscriptionUsage(state.query);
1831
+ }
1832
+ this.captureRateLimit(message);
1702
1833
  this.consumeMessage(state, message);
1703
1834
  const status = queryResultStatus(message);
1704
1835
  if (status) {
@@ -320,6 +320,22 @@ export class CodexAppServerManager extends EventEmitter {
320
320
  return response.data.map(mapModel);
321
321
  }
322
322
 
323
+ async readAccount() {
324
+ await this.ensureReady();
325
+ return this.client!.request<{
326
+ account: { type: string } | null;
327
+ requiresOpenaiAuth: boolean;
328
+ }>('account/read', { refreshToken: false });
329
+ }
330
+
331
+ async readAccountRateLimits() {
332
+ await this.ensureReady();
333
+ return this.client!.request<{
334
+ rateLimits: unknown;
335
+ rateLimitsByLimitId?: Record<string, unknown> | null;
336
+ }>('account/rateLimits/read', null);
337
+ }
338
+
323
339
  async listThreads(): Promise<CodexThreadRecord[]> {
324
340
  await this.ensureReady();
325
341
  const response = await this.client!.request<{ data: any[] }>('thread/list', {
@@ -27,6 +27,22 @@ class FakeCodexManager extends EventEmitter {
27
27
  items: [],
28
28
  };
29
29
  }
30
+
31
+ async readAccount() {
32
+ return { account: { type: 'chatgpt' }, requiresOpenaiAuth: true };
33
+ }
34
+
35
+ async readAccountRateLimits() {
36
+ return {
37
+ rateLimits: {},
38
+ rateLimitsByLimitId: {
39
+ codex: {
40
+ primary: { usedPercent: 40, windowDurationMins: 300, resetsAt: 1_800_000_000 },
41
+ secondary: { usedPercent: 75, windowDurationMins: 10_080, resetsAt: 1_800_604_800 },
42
+ },
43
+ },
44
+ };
45
+ }
30
46
  }
31
47
 
32
48
  describe('CodexRuntimeAdapter', () => {
@@ -73,4 +89,32 @@ describe('CodexRuntimeAdapter', () => {
73
89
  { type: 'text', text: '.', text_elements: [] },
74
90
  ]);
75
91
  });
92
+
93
+ it('maps ChatGPT rate-limit windows without assuming fixed durations', async () => {
94
+ const adapter = new CodexRuntimeAdapter(new FakeCodexManager() as never);
95
+
96
+ await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
97
+ provider: 'codex',
98
+ authKind: 'subscription',
99
+ stale: false,
100
+ windows: [
101
+ { id: 'primary', label: '5h', durationMinutes: 300, usedPercent: 40 },
102
+ { id: 'secondary', label: '7d', durationMinutes: 10_080, usedPercent: 75 },
103
+ ],
104
+ });
105
+ });
106
+
107
+ it('hides subscription windows for API-key authentication', async () => {
108
+ const manager = new FakeCodexManager();
109
+ manager.readAccount = async () => ({
110
+ account: { type: 'apiKey' },
111
+ requiresOpenaiAuth: true,
112
+ });
113
+ const adapter = new CodexRuntimeAdapter(manager as never);
114
+
115
+ await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
116
+ authKind: 'apiKey',
117
+ windows: [],
118
+ });
119
+ });
76
120
  });
@@ -484,6 +484,20 @@ function mapCodexNotification(event: CodexServerEvent): AgentRuntimeEvent | null
484
484
  }
485
485
  }
486
486
 
487
+ function formatRateLimitWindowLabel(
488
+ durationMinutes: number | null,
489
+ fallback: string,
490
+ ) {
491
+ if (durationMinutes === null) return fallback;
492
+ if (durationMinutes % (60 * 24) === 0) {
493
+ return `${durationMinutes / (60 * 24)}d`;
494
+ }
495
+ if (durationMinutes % 60 === 0) {
496
+ return `${durationMinutes / 60}h`;
497
+ }
498
+ return `${durationMinutes}m`;
499
+ }
500
+
487
501
  function mapCodexRuntimeError(error: unknown): never {
488
502
  if (error instanceof AgentRuntimeError) {
489
503
  throw error;
@@ -617,6 +631,58 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
617
631
  return mapStatus(this.manager.getStatus());
618
632
  }
619
633
 
634
+ async getSubscriptionUsage() {
635
+ const account = await codexRuntimeCall(() => this.manager.readAccount());
636
+ if (account.account?.type === 'apiKey') {
637
+ return {
638
+ provider: 'codex' as const,
639
+ authKind: 'apiKey' as const,
640
+ observedAt: new Date().toISOString(),
641
+ stale: false,
642
+ windows: [],
643
+ };
644
+ }
645
+ if (account.account?.type !== 'chatgpt') {
646
+ return null;
647
+ }
648
+ const response = await codexRuntimeCall(() =>
649
+ this.manager.readAccountRateLimits(),
650
+ );
651
+ const buckets = response.rateLimitsByLimitId;
652
+ const snapshot =
653
+ (buckets && (buckets.codex ?? Object.values(buckets)[0])) ??
654
+ response.rateLimits;
655
+ const record = snapshot && typeof snapshot === 'object'
656
+ ? (snapshot as Record<string, unknown>)
657
+ : {};
658
+ const windows = ['primary', 'secondary'].flatMap((id) => {
659
+ const value = record[id];
660
+ if (!value || typeof value !== 'object') return [];
661
+ const window = value as Record<string, unknown>;
662
+ const usedPercent = Number(window.usedPercent);
663
+ if (!Number.isFinite(usedPercent)) return [];
664
+ const duration = Number(window.windowDurationMins);
665
+ const durationMinutes = Number.isFinite(duration) ? duration : null;
666
+ const resetsAt = Number(window.resetsAt);
667
+ return [{
668
+ id,
669
+ durationMinutes,
670
+ label: formatRateLimitWindowLabel(durationMinutes, id),
671
+ usedPercent: Math.max(0, Math.min(100, usedPercent)),
672
+ resetsAt: Number.isFinite(resetsAt)
673
+ ? new Date(resetsAt * 1000).toISOString()
674
+ : null,
675
+ }];
676
+ });
677
+ return {
678
+ provider: 'codex' as const,
679
+ authKind: 'subscription' as const,
680
+ observedAt: new Date().toISOString(),
681
+ stale: false,
682
+ windows,
683
+ };
684
+ }
685
+
620
686
  start() {
621
687
  return codexRuntimeCall(() => this.manager.start());
622
688
  }
@@ -0,0 +1,19 @@
1
+ ALTER TABLE threads ADD COLUMN active_turn_collaboration_mode TEXT;
2
+
3
+ ALTER TABLE thread_pending_steers
4
+ ADD COLUMN delivery TEXT NOT NULL DEFAULT 'steer';
5
+
6
+ ALTER TABLE thread_pending_steers
7
+ ADD COLUMN turn_config_json TEXT;
8
+
9
+ CREATE TABLE IF NOT EXISTS thread_prompt_requests (
10
+ id TEXT PRIMARY KEY,
11
+ thread_id TEXT NOT NULL,
12
+ client_request_id TEXT NOT NULL,
13
+ status TEXT NOT NULL,
14
+ created_at TEXT NOT NULL,
15
+ updated_at TEXT NOT NULL
16
+ );
17
+
18
+ CREATE UNIQUE INDEX IF NOT EXISTS thread_prompt_requests_thread_client_request_idx
19
+ ON thread_prompt_requests(thread_id, client_request_id);