@oar-sh/oar 0.9.15 → 0.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
  2. package/CHANGELOG.md +305 -0
  3. package/README.md +12 -11
  4. package/package.json +2 -1
  5. package/server/claude-worker/claude-attachments.mjs +7 -4
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
  7. package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
  8. package/server/claude-worker/claude-session-process.mjs +131 -33
  9. package/server/claude-worker/claude-session-worker-main.mjs +266 -0
  10. package/server/claude-worker/claude-session-worker.mjs +2 -253
  11. package/server/claude-worker/claude-turn-failure.mjs +8 -0
  12. package/server/claude-worker/claude-turn-publisher.mjs +282 -13
  13. package/server/claude-worker/claude-usage-totals.mjs +108 -0
  14. package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
  15. package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
  16. package/server/copilot-worker/copilot-sdk-session-process.mjs +73 -7
  17. package/server/db-schema.mjs +115 -0
  18. package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
  19. package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
  20. package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
  21. package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
  22. package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
  23. package/server/migrations/0022-relay-schedules.mjs +142 -0
  24. package/server/migrations/0023-relay-asks.mjs +141 -0
  25. package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
  26. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
  27. package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
  28. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
  29. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
  30. package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
  31. package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
  32. package/server/public/app/api-client.js +53 -0
  33. package/server/public/app/ask-user-view.js +3 -0
  34. package/server/public/app/bootstrap.js +162 -16
  35. package/server/public/app/context-tier-options.mjs +4 -0
  36. package/server/public/app/context-usage-view.mjs +17 -0
  37. package/server/public/app/conversation-group-order.mjs +35 -0
  38. package/server/public/app/conversation-provider-indicator.mjs +7 -0
  39. package/server/public/app/conversation-view.js +2 -0
  40. package/server/public/app/deepseek-settings-ui.js +342 -0
  41. package/server/public/app/inbox-ui.js +367 -0
  42. package/server/public/app/inbox-view.mjs +370 -0
  43. package/server/public/app/journal-view.js +54 -5
  44. package/server/public/app/model-selector-options.mjs +11 -0
  45. package/server/public/app/model-vision-support.mjs +2 -0
  46. package/server/public/app/no-conversation-state.mjs +1 -1
  47. package/server/public/app/plan-usage-view.mjs +165 -13
  48. package/server/public/app/provider-readiness.mjs +4 -1
  49. package/server/public/app/relay-error-ctas.mjs +8 -0
  50. package/server/public/app/relay-event-subscriptions-ui.js +4 -2
  51. package/server/public/app/relay-event-view.mjs +33 -7
  52. package/server/public/app/relay-schedule-view.mjs +119 -0
  53. package/server/public/app/relay-schedules-ui.js +172 -0
  54. package/server/public/app/settings-modal.js +11 -0
  55. package/server/public/app/settings-tabs.js +1 -1
  56. package/server/public/app/socket-handlers.js +37 -3
  57. package/server/public/app/store.js +18 -5
  58. package/server/public/index.html +567 -35
  59. package/server/relay-tools.md +18 -3
  60. package/server/repositories/conversation-group-repository.mjs +110 -17
  61. package/server/repositories/remote-relay-repository.mjs +20 -2
  62. package/server/repositories/session-repository.mjs +7 -1
  63. package/server/routes/ask-user-routes.mjs +57 -3
  64. package/server/routes/cache-routes.mjs +6 -0
  65. package/server/routes/conversation-group-routes.mjs +19 -3
  66. package/server/routes/deepseek-usage-import-routes.mjs +72 -0
  67. package/server/routes/messages-routes.mjs +195 -18
  68. package/server/routes/relay-ask-routes.mjs +387 -0
  69. package/server/routes/relay-event-routes.mjs +3 -1
  70. package/server/routes/relay-schedule-routes.mjs +57 -0
  71. package/server/routes/sessions-routes.mjs +932 -97
  72. package/server/server-runtime.mjs +203 -11
  73. package/server/services/about-service.mjs +1 -0
  74. package/server/services/cache-rebuild-service.mjs +19 -2
  75. package/server/services/claude-session-leftovers.mjs +226 -0
  76. package/server/services/conversation-preferences-service.mjs +17 -0
  77. package/server/services/deepseek-balance-service.mjs +102 -0
  78. package/server/services/deepseek-settings-service.mjs +380 -0
  79. package/server/services/deepseek-usage-import.mjs +471 -0
  80. package/server/services/model-variant-catalog-service.mjs +1 -0
  81. package/server/services/plan-usage-contract.mjs +100 -5
  82. package/server/services/plan-usage-deepseek.mjs +931 -0
  83. package/server/services/plan-usage-service.mjs +431 -0
  84. package/server/services/provider-model-selection.mjs +1 -0
  85. package/server/services/provider-reasoning-effort.mjs +15 -2
  86. package/server/services/relay-ask-link-service.mjs +174 -0
  87. package/server/services/relay-ask-service.mjs +604 -0
  88. package/server/services/relay-event-service.mjs +589 -73
  89. package/server/services/relay-schedule-service.mjs +596 -0
  90. package/server/services/remote-relay-caller-context.mjs +89 -20
  91. package/server/services/remote-relay-dispatcher.mjs +958 -31
  92. package/server/services/session-worker-launch-service.mjs +233 -82
  93. package/server/services/worker-secret-redaction.mjs +318 -0
  94. package/shared/context-window-fallbacks.mjs +3 -0
  95. package/shared/deepseek-api.mjs +137 -0
  96. package/shared/deepseek-models.mjs +114 -0
  97. package/shared/deepseek-pricing.mjs +146 -0
  98. package/shared/host-claude-session-env.mjs +101 -0
  99. package/shared/model-id.mjs +7 -0
  100. package/shared/preview-tool-core.mjs +1 -1
  101. package/shared/provider-routing.mjs +2 -2
  102. package/shared/relay-asks.mjs +145 -0
  103. package/shared/relay-events.mjs +268 -18
  104. package/shared/relay-schedules.mjs +128 -0
  105. package/shared/remote-relay-contract.mjs +554 -26
  106. package/shared/remote-relay-tool-core.mjs +16 -5
  107. package/shared/steer-settle-failure.mjs +1 -0
  108. package/shared/worker-runtime/direct-openai-image.mjs +128 -0
  109. package/shared/worker-runtime/worker-log-file.mjs +60 -0
@@ -13,7 +13,9 @@ import {
13
13
  REMOTE_RELAY_MODES,
14
14
  REMOTE_RELAY_PROVIDERS,
15
15
  REMOTE_RELAY_SESSION_SCOPES,
16
+ REMOTE_RELAY_THINKING_DISPLAYS,
16
17
  } from '../../shared/remote-relay-contract.mjs';
18
+ import { RELAY_ASK_URGENCIES } from '../../shared/relay-asks.mjs';
17
19
  import {
18
20
  REMOTE_RELAY_CALL_FAILED,
19
21
  REMOTE_RELAY_TOOL_DESCRIPTION,
@@ -67,7 +69,21 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
67
69
  mode: choice('mode', REMOTE_RELAY_MODES),
68
70
  effort: text('effort'),
69
71
  title: text('title'),
72
+ // The one exception to the parity with the JSON schema: Claude may send
73
+ // null for update_session's group (out of the group), as "none" does on
74
+ // the providers whose strict schemas take a string only.
75
+ group: z.string().nullable().optional().describe(FIELDS.group.description),
76
+ auto_compact_window: number('auto_compact_window'),
77
+ thinking_enabled: z.boolean().optional().describe(FIELDS.thinking_enabled.description),
78
+ thinking_display: choice('thinking_display', REMOTE_RELAY_THINKING_DISPLAYS),
70
79
  question_id: text('question_id'),
80
+ // ask: the contract's validator refuses a question or detail that is too
81
+ // long, or more than eight options, with a message the model can act on.
82
+ question: text('question'),
83
+ options: z.array(z.string()).optional().describe(FIELDS.options.description),
84
+ recommendation: text('recommendation'),
85
+ detail: text('detail'),
86
+ urgency: choice('urgency', RELAY_ASK_URGENCIES),
71
87
  answer: text('answer'),
72
88
  choices: z.array(z.string()).optional().describe(FIELDS.choices.description),
73
89
  if_busy: choice('if_busy', REMOTE_RELAY_IF_BUSY),
@@ -78,18 +94,37 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
78
94
  match: text('match'),
79
95
  stalled_minutes: number('stalled_minutes'),
80
96
  heartbeat_minutes: number('heartbeat_minutes'),
97
+ digest_minutes: number('digest_minutes'),
98
+ // Loose and every field optional: the contract's validator checks the
99
+ // times and the zone and says what is wrong; an empty one is no option.
100
+ quiet_hours: z.looseObject({
101
+ from: z.string().optional().describe(FIELDS.quiet_hours.properties.from.description),
102
+ to: z.string().optional().describe(FIELDS.quiet_hours.properties.to.description),
103
+ tz: z.string().optional().describe(FIELDS.quiet_hours.properties.tz.description),
104
+ }).optional().describe(FIELDS.quiet_hours.description),
105
+ // A list, or the comma-separated text the contract takes as well.
106
+ exclude: z.union([z.array(z.string()), z.string()]).optional().describe(FIELDS.exclude.description),
81
107
  subscription_id: text('subscription_id'),
82
108
  watcher: text('watcher'),
83
109
  from: text('from'),
110
+ at: text('at'),
111
+ every_minutes: number('every_minutes'),
112
+ schedule_id: text('schedule_id'),
84
113
  task_id: text('task_id'),
85
114
  lines: number('lines'),
86
115
  };
87
116
 
117
+ // What the tool registers: the shape as a loose object. A raw shape becomes a
118
+ // strict-stripping object in the SDK's MCP layer, which would drop a key the
119
+ // schema lacks before the contract's validator could refuse it (create_session)
120
+ // or name it in `ignored` (the other actions). The loose object lets it through.
121
+ export const REMOTE_RELAY_TOOL_ZOD_SCHEMA = z.looseObject(REMOTE_RELAY_TOOL_ZOD_SHAPE);
122
+
88
123
  export function createRemoteRelayToolDefinition({ api, getConversationId = () => '', dbg = () => {} } = {}) {
89
124
  return {
90
125
  name: REMOTE_RELAY_TOOL_NAME,
91
126
  description: REMOTE_RELAY_TOOL_DESCRIPTION,
92
- inputSchema: REMOTE_RELAY_TOOL_ZOD_SHAPE,
127
+ inputSchema: REMOTE_RELAY_TOOL_ZOD_SCHEMA,
93
128
  // `extra.signal` is the MCP request's: the SDK aborts it when the CLI
94
129
  // cancels the call (a Stop interrupting the turn) or the process's MCP
95
130
  // transport closes, and the pending relay request goes with it.
@@ -6,6 +6,9 @@ import { MEDIA_EMBED_INSTRUCTION_TEXT } from '../../shared/media-embed-instructi
6
6
  import { REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS } from '../../shared/remote-relay-tool-core.mjs';
7
7
  import { createPreviewToolDefinition } from './claude-preview-tool.mjs';
8
8
  import { createRemoteRelayToolDefinition } from './claude-remote-relay-tool.mjs';
9
+ import { CLAUDE_LIVE_FLAG_SETTINGS_CODE, CLAUDE_LIVE_FLAG_SETTINGS_GUIDANCE } from './claude-turn-failure.mjs';
10
+
11
+ export { CLAUDE_LIVE_FLAG_SETTINGS_CODE };
9
12
 
10
13
  // The name the in-process MCP server is registered under; it prefixes every
11
14
  // tool it carries on the wire (`mcp__relay__preview`, `mcp__relay__remote_relay`).
@@ -229,14 +232,25 @@ export function createRelayMcpServer({ api, getConversationId, dbg, remoteRelayT
229
232
  * emitted, which also closes the control transport (canUseTool, context
230
233
  * usage), so the session process keeps this open while background tasks or
231
234
  * queued continuations still need the process alive.
235
+ *
236
+ * `held`: nothing is sent until `release()` (the messages pushed meanwhile
237
+ * wait in order). `fail(error)` instead makes the stream throw it, which the
238
+ * SDK answers by aborting the query; nothing held is ever sent, and neither
239
+ * is it when the stream ends while still held.
232
240
  */
233
- function createPushableUserMessageStream() {
241
+ function createPushableUserMessageStream({ held = false } = {}) {
234
242
  const queued = [];
235
243
  let wake = null;
236
244
  let ended = false;
245
+ let holding = held;
246
+ let failure = null;
237
247
  async function* stream() {
238
248
  for (;;) {
239
- while (queued.length) yield queued.shift();
249
+ if (failure) throw failure;
250
+ if (!holding) {
251
+ while (queued.length && !failure) yield queued.shift();
252
+ if (failure) continue;
253
+ }
240
254
  if (ended) return;
241
255
  await new Promise((resolve) => { wake = resolve; });
242
256
  wake = null;
@@ -257,9 +271,80 @@ function createPushableUserMessageStream() {
257
271
  ended = true;
258
272
  wake?.();
259
273
  },
274
+ release() {
275
+ holding = false;
276
+ wake?.();
277
+ },
278
+ fail(error) {
279
+ failure = error;
280
+ wake?.();
281
+ },
260
282
  };
261
283
  }
262
284
 
285
+ /** Every string in a settings object, for taking them out of a text. */
286
+ function settingsStrings(value, found = []) {
287
+ if (typeof value === 'string') {
288
+ if (value) found.push(value);
289
+ } else if (value && typeof value === 'object') {
290
+ for (const item of Object.values(value)) settingsStrings(item, found);
291
+ }
292
+ return found;
293
+ }
294
+
295
+ /**
296
+ * The blanked keys of a backend's live flag settings (top-level and `env`
297
+ * values that are ''), for the spawn's `settings` too: an empty value holds
298
+ * no secret, and the CLI acts on some settings at startup, before the live
299
+ * ones land (it runs a folder's apiKeyHelper then; CLI 2.1.304,
300
+ * scripts/deepseek-cli-proof.mjs). Null when there is none.
301
+ */
302
+ function liveSettingsBlanks(live) {
303
+ if (!live) return null;
304
+ const blanks = {};
305
+ for (const [key, value] of Object.entries(live)) {
306
+ if (value === '') blanks[key] = '';
307
+ }
308
+ const env = Object.entries(live.env && typeof live.env === 'object' ? live.env : {}).filter(([, value]) => value === '');
309
+ if (env.length) blanks.env = Object.fromEntries(env);
310
+ return Object.keys(blanks).length ? blanks : null;
311
+ }
312
+
313
+ /** The spawn's `settings` with the live settings' blanks on top. */
314
+ function withLiveSettingsBlanks(spawnSettings, live) {
315
+ const blanks = liveSettingsBlanks(live);
316
+ if (!blanks) return spawnSettings;
317
+ const settings = { ...(spawnSettings || {}), ...blanks };
318
+ if (spawnSettings?.env || blanks.env) settings.env = { ...(spawnSettings?.env || {}), ...(blanks.env || {}) };
319
+ return settings;
320
+ }
321
+
322
+ /**
323
+ * How long the CLI may take to answer a backend's live flag settings (ms).
324
+ * It answers in well under a second (CLI 2.1.304); one that never does fails
325
+ * the turn like a refusal, and the held message is never sent.
326
+ */
327
+ export const LIVE_FLAG_SETTINGS_TIMEOUT_MS = 60_000;
328
+
329
+ /**
330
+ * The Error a spawn fails with when the CLI refused its live flag settings.
331
+ * The message names no value; `detail` is the CLI's own words with every
332
+ * value of the settings taken out.
333
+ */
334
+ function liveFlagSettingsError(cause, settings, label = '') {
335
+ const error = new Error(
336
+ `Claude Code did not take the ${label ? `${label} ` : ''}connection settings before the first message, so nothing was sent.`,
337
+ );
338
+ error.code = CLAUDE_LIVE_FLAG_SETTINGS_CODE;
339
+ error.guidance = CLAUDE_LIVE_FLAG_SETTINGS_GUIDANCE;
340
+ let detail = String(cause?.message || cause || '');
341
+ for (const value of settingsStrings(settings).sort((a, b) => b.length - a.length)) {
342
+ detail = detail.split(value).join('[redacted]');
343
+ }
344
+ error.detail = detail.slice(0, 200);
345
+ return error;
346
+ }
347
+
263
348
  /**
264
349
  * The only module that imports `@anthropic-ai/claude-agent-sdk`.
265
350
  *
@@ -293,13 +378,19 @@ export function startClaudeSession({
293
378
  // Whether this process carries `remote_relay` (the worker's registration
294
379
  // decision). Fixed for the process: the tool list is part of the spawn.
295
380
  remoteRelayTool = false,
381
+ // Another endpoint for the CLI (null = Anthropic through the host's Claude
382
+ // login): it builds the CLI's environment and maps model and effort into
383
+ // its terms (../deepseek-worker/deepseek-claude-backend.mjs).
384
+ backend = null,
385
+ liveFlagSettingsTimeoutMs = LIVE_FLAG_SETTINGS_TIMEOUT_MS,
296
386
  queryImpl = query,
297
387
  dbg = () => {},
298
388
  } = {}) {
299
389
  const normalizedModel = String(model || '').trim().toLowerCase();
300
- const effort = normalizeClaudeEffort(reasoningEffort);
390
+ const effort = backend ? '' : normalizeClaudeEffort(reasoningEffort);
301
391
  const ultracode = effort === CLAUDE_ULTRACODE_EFFORT;
302
- const effortOption = ultracode ? 'xhigh' : effort;
392
+ const effortOption = backend ? backend.effortOption(reasoningEffort) : (ultracode ? 'xhigh' : effort);
393
+ const cliModel = backend ? backend.cliModel(model) : String(model || '').trim();
303
394
  // Spawn-time twin of the flag-settings helpers: the settings layer is the
304
395
  // only way to hand a fresh CLI the session-scoped ultracode flag, the
305
396
  // auto-compact window and the thinking pin, and all three share one
@@ -310,17 +401,40 @@ export function startClaudeSession({
310
401
  const relayMcpServer = typeof api === 'function'
311
402
  ? createRelayMcpServer({ api, getConversationId, dbg, remoteRelayTool: remoteRelayTool === true })
312
403
  : null;
313
- const env = claudeSpawnEnv({ backgroundCommandLimitMs });
404
+ // A backend's CLI always gets an environment of its own: the worker's
405
+ // carries the backend's secret and maybe the host's Anthropic settings.
406
+ // The model goes in as well: the backend pins Claude Code's model aliases to
407
+ // it, so a sub-agent asked for as "opus" runs on the session's model. The
408
+ // Bash limits go on top: the backend drops a host session's Bash cap, not
409
+ // the relay's own.
410
+ const backendEnv = backend ? backend.spawnEnv(process.env, { effort: reasoningEffort, model }) : null;
411
+ const limitEnv = claudeSpawnEnv({ backgroundCommandLimitMs, baseEnv: backendEnv || process.env });
412
+ const env = limitEnv || backendEnv;
413
+ // A backend's pins that may not go on the command line (a key): `settings`
414
+ // below is the CLI's `--settings` argument, which other users of the host
415
+ // can read. These go into the same flag layer over the control channel
416
+ // (stdin), and no user message is sent before the CLI has taken them: the
417
+ // first API request of the process comes after (measured on CLI 2.1.304,
418
+ // the CLI proofs in scripts/). The layer outranks the user's, the
419
+ // project's and the local settings and holds through their reloads and
420
+ // through later applyFlagSettings calls of other keys. A refusal ends the
421
+ // process before anything is sent (CLAUDE_LIVE_FLAG_SETTINGS_CODE), and so
422
+ // does no answer within liveFlagSettingsTimeoutMs. Their blanks go on the
423
+ // command line as well, for what the CLI does at startup. They are built
424
+ // from the CLI's environment: a pin holds the value the CLI starts with.
425
+ const liveSettings = typeof backend?.liveFlagSettings === 'function' ? backend.liveFlagSettings(env || process.env) : null;
426
+ const live = liveSettings && typeof liveSettings === 'object' && Object.keys(liveSettings).length ? liveSettings : null;
427
+ const settings = withLiveSettingsBlanks(spawnSettings, live);
314
428
  const options = {
315
429
  cwd,
316
430
  permissionMode: permissionModeForRelayMode(relayMode),
317
- systemPrompt: systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs: env ? backgroundCommandLimitMs : null }),
431
+ systemPrompt: systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs: limitEnv ? backgroundCommandLimitMs : null }),
318
432
  includePartialMessages: true,
319
433
  forwardSubagentText: true,
320
434
  ...(effortOption ? { effort: effortOption } : {}),
321
- ...(spawnSettings ? { settings: spawnSettings } : {}),
435
+ ...(settings ? { settings } : {}),
322
436
  ...(env ? { env } : {}),
323
- ...(normalizedModel && normalizedModel !== 'auto' ? { model: String(model).trim() } : {}),
437
+ ...(normalizedModel && normalizedModel !== 'auto' && cliModel ? { model: cliModel } : {}),
324
438
  ...(String(resume || '').trim() ? { resume: String(resume).trim() } : {}),
325
439
  ...(abortController ? { abortController } : {}),
326
440
  ...(typeof canUseTool === 'function' ? { canUseTool } : {}),
@@ -329,8 +443,52 @@ export function startClaudeSession({
329
443
  ? { pathToClaudeCodeExecutable: String(pathToClaudeCodeExecutable).trim() }
330
444
  : {}),
331
445
  };
332
- const { stream, push, end } = createPushableUserMessageStream();
446
+ const { stream, push, end, release, fail } = createPushableUserMessageStream({ held: Boolean(live) });
333
447
  const turn = queryImpl({ prompt: stream, options });
448
+ if (live) {
449
+ // The wait ends with the answer, the timeout or the session's abort,
450
+ // whichever comes first; each one clears the others.
451
+ const signal = abortController?.signal || null;
452
+ let timer = null;
453
+ let onAbort = null;
454
+ const settled = () => {
455
+ clearTimeout(timer);
456
+ timer = null;
457
+ if (onAbort) signal?.removeEventListener('abort', onAbort);
458
+ onAbort = null;
459
+ };
460
+ const unanswered = new Promise((resolve, reject) => {
461
+ const waitMs = Math.max(0, Number(liveFlagSettingsTimeoutMs) || 0);
462
+ timer = setTimeout(() => {
463
+ timer = null;
464
+ reject(new Error(`no answer within ${Math.ceil(waitMs / 1000)} s`));
465
+ }, waitMs);
466
+ timer.unref?.();
467
+ if (signal) {
468
+ onAbort = settled;
469
+ if (signal.aborted) settled();
470
+ else signal.addEventListener('abort', onAbort, { once: true });
471
+ }
472
+ });
473
+ turn.liveFlagSettingsApplied = Promise.race([
474
+ Promise.resolve().then(() => {
475
+ if (typeof turn.applyFlagSettings !== 'function') throw new Error('applyFlagSettings is not available');
476
+ return turn.applyFlagSettings(live);
477
+ }),
478
+ unanswered,
479
+ ])
480
+ .finally(settled)
481
+ .then(() => {
482
+ release();
483
+ return true;
484
+ }, (error) => {
485
+ const refused = liveFlagSettingsError(error, live, backend?.label || '');
486
+ turn.liveFlagSettingsError = refused;
487
+ dbg('live flag settings refused', refused.detail);
488
+ fail(refused);
489
+ return false;
490
+ });
491
+ }
334
492
  applyThinkingDisplay(turn, thinkingDisplay, { dbg });
335
493
  turn.pushUserMessage = push;
336
494
  // The session process MUST call this on every teardown path or the CLI