@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
@@ -45,6 +45,9 @@ import { registerClaudeCloudRoutes } from './routes/claude-cloud-routes.mjs';
45
45
  import { createPreviewScopeResolver, registerPreviewRoutes } from './routes/preview-routes.mjs';
46
46
  import { registerRemoteRelayRoutes } from './routes/remote-relay-routes.mjs';
47
47
  import { registerRelayEventRoutes } from './routes/relay-event-routes.mjs';
48
+ import { registerRelayScheduleRoutes } from './routes/relay-schedule-routes.mjs';
49
+ import { registerDeepSeekUsageImportRoutes } from './routes/deepseek-usage-import-routes.mjs';
50
+ import { registerRelayAskRoutes } from './routes/relay-ask-routes.mjs';
48
51
  import { createRemoteRelayRepository } from './repositories/remote-relay-repository.mjs';
49
52
  import { createConversationGroupRepository } from './repositories/conversation-group-repository.mjs';
50
53
  import { createShareRepository } from './repositories/share-repository.mjs';
@@ -62,6 +65,12 @@ import { createClaudeCloudCredentials } from '../shared/claude-cloud/credentials
62
65
  import { createClaudeCloudClient } from '../shared/claude-cloud/api-client.mjs';
63
66
  import { resolveClaudeCloudBaseUrl } from '../shared/claude-cloud/base-url.mjs';
64
67
  import { createClaudeCloudSettingsService } from './services/claude-cloud-settings-service.mjs';
68
+ import { createDeepSeekSettingsService } from './services/deepseek-settings-service.mjs';
69
+ import { createWorkerPostRedaction } from './services/worker-secret-redaction.mjs';
70
+ import { createDeepSeekBalanceService } from './services/deepseek-balance-service.mjs';
71
+ import { deepseekCardCurrency } from './services/plan-usage-deepseek.mjs';
72
+ import { createDeepSeekUsageImportService } from './services/deepseek-usage-import.mjs';
73
+ import { DEEPSEEK_DEFAULT_AUTO_COMPACT_WINDOW, DEFAULT_DEEPSEEK_MODEL, deepseekModelLabel } from '../shared/deepseek-models.mjs';
65
74
  import { createClaudeCloudSessionService } from './services/claude-cloud-session-service.mjs';
66
75
  import { createClaudeCloudRepoService, listRecentCloudSourcesFromStatements } from './services/claude-cloud-repo-service.mjs';
67
76
  import { createClaudeAccountUsageService } from './services/claude-account-usage-service.mjs';
@@ -84,6 +93,9 @@ import {
84
93
  } from './services/restart-resume-service.mjs';
85
94
  import { createBackgroundWorkView } from './services/background-work-view.mjs';
86
95
  import { createRelayEventService } from './services/relay-event-service.mjs';
96
+ import { createRelayScheduleService } from './services/relay-schedule-service.mjs';
97
+ import { createRelayAskService } from './services/relay-ask-service.mjs';
98
+ import { createRelayAskLinkService } from './services/relay-ask-link-service.mjs';
87
99
  import { createRelayEventLinkService } from './services/relay-event-link-service.mjs';
88
100
  import { buildClaudeAttributionSettings, normalizeClaudeAttributionMode, resolveClaudeAttributionMode, DEFAULT_CLAUDE_ATTRIBUTION_MODE } from '../shared/claude-attribution.mjs';
89
101
  import {
@@ -133,7 +145,7 @@ import { createRelayCliLauncherService } from './services/relay-cli-launcher-ser
133
145
  import { createSessionWorkerRegistry } from './services/session-worker-registry-service.mjs';
134
146
  import { createSessionWorkerSupervisor } from './services/session-worker-supervisor-service.mjs';
135
147
  import { createSessionWorkerProcessInspector } from './services/session-worker-process-service.mjs';
136
- import { applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
148
+ import { applyClaudeCloudProviderEnvironment, applyClaudeProviderEnvironment, applyCopilotSdkProviderEnvironment, applyCursorProviderEnvironment, applyDeepSeekProviderEnvironment, applyGrokProviderEnvironment, applyOpenAIProviderEnvironment, copilotSdkEngineUnavailableReason, killTmuxSession, launchSessionCli } from './services/session-worker-launch-service.mjs';
137
149
  import { createSshTunnelManager } from './services/ssh-tunnel-manager-service.mjs';
138
150
  import { createCloudflaredTunnelManager } from './services/cloudflared-tunnel-service.mjs';
139
151
  import {
@@ -2825,6 +2837,58 @@ const claudeCloudRepoService = createClaudeCloudRepoService({
2825
2837
  isEnabled: () => claudeCloudSettingsService.getSettings().enabled === true,
2826
2838
  listRecentCloudSources: () => listRecentCloudSourcesFromStatements(stmts),
2827
2839
  });
2840
+ // What a session worker posts loses every key a provider saved in Settings
2841
+ // (worker-secret-redaction.mjs). Built here, at the start, so it reads the
2842
+ // saved keys now, and the DeepSeek save below notes each key it writes: one
2843
+ // changed before any worker has posted is still redacted.
2844
+ const workerPostRedaction = createWorkerPostRedaction({ readSetting: readAppSettingValue });
2845
+ // DeepSeek: one API key per relay, its models and its endpoint. The key stays
2846
+ // in the settings table; GET routes answer `configured`, never the key.
2847
+ const deepseekSettingsService = createDeepSeekSettingsService({
2848
+ readSetting: readAppSettingValue,
2849
+ writeSetting: (key, value) => {
2850
+ stmts.upsertAppSetting.run(key, String(value), new Date().toISOString());
2851
+ workerPostRedaction.noteSavedSetting(key, value);
2852
+ },
2853
+ deleteSetting: (key) => stmts.deleteAppSetting.run(key),
2854
+ listProviderCandidates: () => stmts.listRuntimeSessionProviderCandidates?.all?.() || [],
2855
+ });
2856
+ function getDeepSeekProviderSettings() {
2857
+ return deepseekSettingsService.getSettings();
2858
+ }
2859
+ // The DeepSeek balance for Check Usage: read with the saved key only while
2860
+ // DeepSeek is on, kept for a minute.
2861
+ const deepseekBalanceService = createDeepSeekBalanceService({ getSettings: getDeepSeekProviderSettings });
2862
+ // DeepSeek's own usage export, imported in Settings (or with
2863
+ // `npm run deepseek:import`): the usage card shows its figures for the days
2864
+ // it covers, for the saved key only, and the estimate after.
2865
+ const deepseekUsageImportService = createDeepSeekUsageImportService({
2866
+ db,
2867
+ getApiKey: () => getDeepSeekProviderSettings()?.apiKey || '',
2868
+ });
2869
+ // Where DeepSeek workers keep their Claude Code state (sessions, transcripts,
2870
+ // settings): a directory of the relay's own, so no Claude login is in reach
2871
+ // of a CLI that talks to DeepSeek and their sessions stay out of Claude's.
2872
+ const DEEPSEEK_CLAUDE_CONFIG_DIR = path.join(DATA_DIR, 'deepseek-claude');
2873
+
2874
+ /**
2875
+ * What a DeepSeek session's commits and PRs say: the DeepSeek tab's mode
2876
+ * (OAR or off), the folder override on top, the DeepSeek model's name in the
2877
+ * trailer. A folder set to vanilla gets OAR's: Claude Code's own lines would
2878
+ * credit Claude for DeepSeek's work.
2879
+ */
2880
+ function deepseekAttributionForConversation(conv, modelId) {
2881
+ const root = String(conv?.runtime_workspace_root_path || conv?.configured_workspace_root_path || '').trim()
2882
+ || currentWorkspaceRootPath();
2883
+ const resolved = resolveClaudeAttributionMode({
2884
+ providerMode: getDeepSeekProviderSettings().attribution,
2885
+ folderMode: workspaceRootAttributionMode(root),
2886
+ });
2887
+ return buildClaudeAttributionSettings({
2888
+ mode: resolved === 'vanilla' ? 'oar' : resolved,
2889
+ modelLabel: deepseekModelLabel(modelId),
2890
+ });
2891
+ }
2828
2892
  // The account's live usage for the Check Usage modal. The Claude Cloud switch
2829
2893
  // is the consent to use the Claude CLI's login, so nothing is read while it
2830
2894
  // is off.
@@ -3053,7 +3117,7 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
3053
3117
  const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
3054
3118
  // Clear every provider's variables first (the appliers' kind-guarded deletes
3055
3119
  // keep the chained clears order-independent), then apply only the bound one.
3056
- const cleared = applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv))))));
3120
+ const cleared = applyDeepSeekProviderEnvironment(applyClaudeCloudProviderEnvironment(applyCopilotSdkProviderEnvironment(applyGrokProviderEnvironment(applyCursorProviderEnvironment(applyClaudeProviderEnvironment(applyOpenAIProviderEnvironment(sessionWorkerLaunchEnv)))))));
3057
3121
  // A Claude Cloud conversation gets its own worker and nothing else: it must
3058
3122
  // never reach the Copilot CLI fall-through at the end of this function. The
3059
3123
  // environment names the worker and the model, no credential.
@@ -3083,6 +3147,19 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
3083
3147
  dataDir: DATA_DIR,
3084
3148
  });
3085
3149
  }
3150
+ // A DeepSeek conversation gets its own worker and must never reach the
3151
+ // Copilot fall-through below. Without a key the applier throws, and the
3152
+ // conversation gets the launch note.
3153
+ if (providerType === 'deepseek') {
3154
+ const deepseekSettings = getDeepSeekProviderSettings();
3155
+ return applyDeepSeekProviderEnvironment(cleared, {
3156
+ enabled: true,
3157
+ model: String(runtimeSession?.provider_model || runtimeSession?.model || deepseekSettings.model).trim(),
3158
+ apiKey: deepseekSettings.apiKey,
3159
+ baseUrl: deepseekSettings.baseUrl,
3160
+ configDir: DEEPSEEK_CLAUDE_CONFIG_DIR,
3161
+ });
3162
+ }
3086
3163
  if (providerType === 'grok') {
3087
3164
  const grokSettings = getGrokProviderSettings();
3088
3165
  const grokModel = String(runtimeSession?.provider_model || runtimeSession?.model || grokSettings.model).trim();
@@ -3311,12 +3388,13 @@ async function stopSessionWorkerForProviderRebind(sdkSessionId) {
3311
3388
 
3312
3389
  async function reconcileUnstartedConversationProviders({ enabled, model, provider = 'openai' } = {}) {
3313
3390
  const normalizedProvider = String(provider || 'openai').trim().toLowerCase();
3314
- const managedProvider = ['claude', 'cursor', 'grok'].includes(normalizedProvider) ? normalizedProvider : 'openai';
3391
+ const managedProvider = ['claude', 'cursor', 'grok', 'deepseek'].includes(normalizedProvider) ? normalizedProvider : 'openai';
3315
3392
  const managedDefaultModel = {
3316
3393
  openai: DEFAULT_OPENAI_MODEL,
3317
3394
  claude: DEFAULT_CLAUDE_MODEL,
3318
3395
  cursor: DEFAULT_CURSOR_MODEL,
3319
3396
  grok: DEFAULT_GROK_MODEL,
3397
+ deepseek: DEFAULT_DEEPSEEK_MODEL,
3320
3398
  }[managedProvider];
3321
3399
  const providerType = enabled === true ? managedProvider : 'github';
3322
3400
  const providerModel = enabled === true
@@ -3390,7 +3468,9 @@ async function reconcileUnstartedConversationProviders({ enabled, model, provide
3390
3468
  ? getCursorProviderSettings().configured === true
3391
3469
  : (currentProvider === 'grok'
3392
3470
  ? getGrokProviderSettings().enabled === true
3393
- : (currentProvider !== 'openai' || getOpenAIProviderSettings().configured === true)));
3471
+ : (currentProvider === 'deepseek'
3472
+ ? getDeepSeekProviderSettings().configured === true
3473
+ : (currentProvider !== 'openai' || getOpenAIProviderSettings().configured === true))));
3394
3474
  if (providerWasUpdated && canRestorePreviousProvider) {
3395
3475
  stmts.updateRuntimeSessionProvider.run(
3396
3476
  currentProvider,
@@ -5128,6 +5208,11 @@ function createCompactedConversation(sourceConversationId) {
5128
5208
  targetRuntimeSession = ensureRuntimeSessionBinding(targetConversationId, null, now);
5129
5209
  });
5130
5210
  tx();
5211
+ // Its schedules go on in the new conversation, both the ones that fire
5212
+ // into it and the ones it set: the archived source would end them.
5213
+ try { relayScheduleService.carryToCompacted(sourceConversationId, targetConversationId); } catch {}
5214
+ // And so do its questions that don't block, asked in it or by it.
5215
+ try { relayAskService.carryToCompacted(sourceConversationId, targetConversationId); } catch {}
5131
5216
 
5132
5217
  return {
5133
5218
  sourceConversationId,
@@ -5458,6 +5543,37 @@ relayEventService = createRelayEventService({
5458
5543
  logger: console,
5459
5544
  });
5460
5545
 
5546
+ // ─── Relay schedules ──────────────────────────────────────────────────────────
5547
+ // Prompts agents scheduled with remote_relay's `schedule`: kept in the
5548
+ // database, their timers rebuilt at boot (services/relay-schedule-service.mjs).
5549
+ const relayScheduleService = createRelayScheduleService({
5550
+ db,
5551
+ stmts,
5552
+ emit: (event, payload) => io.emit(event, payload),
5553
+ // The scheduled message is an ordinary pending one, like a relay event's.
5554
+ wake: () => {
5555
+ void primePendingSessionWorkers('relay-schedule');
5556
+ sessionWorkerWebSocketService?.emitQueueChanged?.('relay-schedule');
5557
+ },
5558
+ // A firing into another session is a prompt of the conversation that set
5559
+ // it: the agents' loop guard counts it (and holds it once it wants the user).
5560
+ admitFiring: ({ createdBy }) => remoteRelayDispatcher.admitScheduledFiring({ createdBy }),
5561
+ logger: console,
5562
+ });
5563
+
5564
+ // ─── Questions that don't block ───────────────────────────────────────────────
5565
+ // remote_relay's `ask`: a card in the session and the Inbox; the answer
5566
+ // reaches the askers as a relay event (services/relay-ask-service.mjs).
5567
+ const relayAskService = createRelayAskService({
5568
+ db,
5569
+ emit: (event, payload) => io.emit(event, payload),
5570
+ relayEvents: relayEventService,
5571
+ // urgency "now" only.
5572
+ pushQuestion: (question) => pushDispatchService.notifyQuestion?.(question),
5573
+ logger: console,
5574
+ });
5575
+ if (!relayAskService.isAvailable()) console.warn(`[relay-asks] not available this boot: ${relayAskService.problem()}`);
5576
+
5461
5577
  /** A worker died: its tasks are gone, and whoever watches its sessions hears of it. */
5462
5578
  function noteWorkerDeathForEvents(sessionId, conversationIds, reason) {
5463
5579
  const ids = new Set((conversationIds || []).map((id) => String(id || '').trim()).filter(Boolean));
@@ -5943,6 +6059,14 @@ const readContextFromSessionEvents = contextSnapshotService.readContextFromSessi
5943
6059
  // Claude-provider sessions live under the Agent SDK's project layout rather than
5944
6060
  // ~/.copilot/session-state, so their browsable session folder resolves separately.
5945
6061
  const claudeSessionRootResolver = createClaudeSessionRootResolver({ fs, path });
6062
+ // DeepSeek chats run Claude Code with a config dir of their own: only that
6063
+ // root, never the host's ~/.claude.
6064
+ const deepseekSessionRootResolver = createClaudeSessionRootResolver({
6065
+ fs,
6066
+ path,
6067
+ env: { CLAUDE_CONFIG_DIR: DEEPSEEK_CLAUDE_CONFIG_DIR },
6068
+ homedir: () => '',
6069
+ });
5946
6070
  const cursorSessionRootResolver = createCursorSessionRootResolver({ fs, path, serverDir: __dirname, dataDir: DATA_DIR });
5947
6071
 
5948
6072
  // ─── Express + Socket.io Setup ────────────────────────────────────────────────
@@ -6330,8 +6454,12 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
6330
6454
  // without a new queue column or push channel. null = Auto.
6331
6455
  ...((() => {
6332
6456
  const conv = out.conversationId ? stmts.getConvAnyStatus.get(out.conversationId) : null;
6457
+ const deliveredProvider = String(out.providerType || '').trim().toLowerCase();
6333
6458
  return {
6334
- autoCompactWindow: parseAutoCompactWindow(conv?.auto_compact_window),
6459
+ // DeepSeek's window is 1M; its sessions compact at 400K unless the
6460
+ // conversation pins another window.
6461
+ autoCompactWindow: parseAutoCompactWindow(conv?.auto_compact_window)
6462
+ ?? (deliveredProvider === 'deepseek' ? DEEPSEEK_DEFAULT_AUTO_COMPACT_WINDOW : null),
6335
6463
  // Per-conversation thinking control, same delivery scoping. An
6336
6464
  // explicit null on thinkingEnabled means Host default — the worker
6337
6465
  // distinguishes it from an absent key (an older relay), so both
@@ -6343,7 +6471,14 @@ async function requestSessionWorkerSocketDelivery({ sessionId, pid, reason = 'wo
6343
6471
  // attribution. Always present on such a delivery, so the worker can
6344
6472
  // tell "left as it was" (absent) from "vanilla" (null).
6345
6473
  ...((() => {
6346
- const deliveredProvider = String(out.providerType || '').trim().toLowerCase();
6474
+ // A DeepSeek session runs Claude Code on the host like a Claude
6475
+ // one: the same two settings, with DeepSeek's attribution.
6476
+ if (deliveredProvider === 'deepseek') {
6477
+ return {
6478
+ attribution: deepseekAttributionForConversation(conv, out.providerModel || out.model),
6479
+ claudeBackgroundCommandLimitMs: claudeBackgroundCommandLimitMinutesToMs(getClaudeBackgroundCommandLimitMinutes()),
6480
+ };
6481
+ }
6347
6482
  if (deliveredProvider !== 'claude' && deliveredProvider !== 'claude-cloud') return {};
6348
6483
  return {
6349
6484
  attribution: claudeAttributionForConversation(conv, out.providerModel || out.model, {
@@ -6699,6 +6834,9 @@ app.use((req, _res, next) => {
6699
6834
  stripRequestPathPrefix(req, remotePath);
6700
6835
  next();
6701
6836
  });
6837
+ // What a session worker posts loses every key a provider saved in Settings
6838
+ // before any route stores, broadcasts or logs it (worker-secret-redaction.mjs).
6839
+ app.use(workerPostRedaction);
6702
6840
  function isApiRequestPath(requestPath) {
6703
6841
  return /^\/api(?:\/|$)/.test(String(requestPath || ''));
6704
6842
  }
@@ -6883,7 +7021,10 @@ const remoteRelayRegistry = createRemoteRelayRegistry({
6883
7021
  platform: process.platform,
6884
7022
  version: RUNNING_VERSION,
6885
7023
  emit: (event, payload) => io.emit(event, payload),
6886
- onRemove: (entry) => { void relayEventLinkService?.noteRelayRemoved(entry); },
7024
+ onRemove: (entry) => {
7025
+ void relayEventLinkService?.noteRelayRemoved(entry);
7026
+ try { relayAskService.noteRelayRemoved(entry?.id); } catch {}
7027
+ },
6887
7028
  logger: console,
6888
7029
  });
6889
7030
  relayEventLinkService = createRelayEventLinkService({
@@ -6892,6 +7033,13 @@ relayEventLinkService = createRelayEventLinkService({
6892
7033
  client: remoteRelayClient,
6893
7034
  logger: console,
6894
7035
  });
7036
+ // The answers to asks this relay's agents put on paired relays.
7037
+ const relayAskLinkService = createRelayAskLinkService({
7038
+ asks: relayAskService,
7039
+ registry: remoteRelayRegistry,
7040
+ client: remoteRelayClient,
7041
+ logger: console,
7042
+ });
6895
7043
  const remoteRelayPairing = createRemoteRelayPairing({
6896
7044
  registry: remoteRelayRegistry,
6897
7045
  client: remoteRelayClient,
@@ -6946,6 +7094,9 @@ const remoteRelayDispatcher = createRemoteRelayDispatcher({
6946
7094
  resolveConversationId: (id) => resolveConversationRecord({ conversationId: id, sdkSessionId: id })?.id || id,
6947
7095
  relayEvents: relayEventService,
6948
7096
  relayEventLink: relayEventLinkService,
7097
+ relaySchedules: relayScheduleService,
7098
+ relayAsks: relayAskService,
7099
+ relayAskLink: relayAskLinkService,
6949
7100
  logger: console,
6950
7101
  });
6951
7102
 
@@ -7020,6 +7171,10 @@ const sharedRouteDeps = {
7020
7171
  getClaudeCloudProviderSettings,
7021
7172
  claudeCloudSettingsService,
7022
7173
  claudeCloudSessionService,
7174
+ getDeepSeekProviderSettings,
7175
+ deepseekSettingsService,
7176
+ deepseekBalanceService,
7177
+ deepseekUsageImportService,
7023
7178
  claudeCloudRepoService,
7024
7179
  // The login's state for the workers' nudge route (never the token).
7025
7180
  claudeCloudLoginStatus: { describe: () => claudeCloudCredentials.describe() },
@@ -7144,6 +7299,8 @@ const sharedRouteDeps = {
7144
7299
  relayRestartOrchestrator,
7145
7300
  resolveSessionStateRoot,
7146
7301
  resolveClaudeSessionRoot: claudeSessionRootResolver.resolveClaudeSessionRoot,
7302
+ resolveDeepSeekSessionRoot: deepseekSessionRootResolver.resolveClaudeSessionRoot,
7303
+ deepseekClaudeConfigDir: DEEPSEEK_CLAUDE_CONFIG_DIR,
7147
7304
  resolveCursorSessionRoot: cursorSessionRootResolver.resolveCursorSessionRoot,
7148
7305
  getTurnCeilingMinutes,
7149
7306
  setTurnCeilingMinutes,
@@ -7165,6 +7322,8 @@ const sharedRouteDeps = {
7165
7322
  collectHostActivity,
7166
7323
  usageLimitPauseService,
7167
7324
  relayEventService,
7325
+ relayScheduleService,
7326
+ relayAskService,
7168
7327
  sessionActivityTracker,
7169
7328
  resolveConversationRecord,
7170
7329
  markSharedViewerPresence,
@@ -7210,6 +7369,30 @@ registerRelayEventRoutes(app, {
7210
7369
  relayEventService,
7211
7370
  remoteRelayInbound,
7212
7371
  });
7372
+ registerRelayScheduleRoutes(app, {
7373
+ auth,
7374
+ relayScheduleService,
7375
+ remoteRelayInbound,
7376
+ });
7377
+ registerDeepSeekUsageImportRoutes(app, {
7378
+ auth,
7379
+ deepseekUsageImportService,
7380
+ remoteRelayInbound,
7381
+ // The usage card's currency (the balance's), so the import's answer can
7382
+ // say whether the card uses the export's rows.
7383
+ getCardCurrency: async () => deepseekCardCurrency(await deepseekBalanceService.getBalance()),
7384
+ });
7385
+ registerRelayAskRoutes(app, {
7386
+ auth,
7387
+ db,
7388
+ stmts,
7389
+ formatQuestionRow,
7390
+ relayAskService,
7391
+ relayAskLinkService,
7392
+ remoteRelayInbound,
7393
+ remoteRelayRegistry,
7394
+ remoteRelayClient,
7395
+ });
7213
7396
  registerRelayRestartRoutes(app, {
7214
7397
  auth,
7215
7398
  // Read only: a GET neither fires the restart nor asks the question.
@@ -7480,10 +7663,12 @@ function ensureRuntimeSessionBinding(
7480
7663
  // default below and bind a GitHub (Copilot CLI) session.
7481
7664
  : (normalizedRequestedProviderType === 'claude-cloud'
7482
7665
  ? 'claude-cloud'
7483
- : (normalizedRequestedProviderType === 'github'
7484
- ? 'github'
7485
- : (openAISettings?.enabled ? 'openai' : 'github'))))));
7486
- const resolvedProviderModel = (resolvedProviderType === 'openai' || resolvedProviderType === 'claude' || resolvedProviderType === 'cursor' || resolvedProviderType === 'grok' || resolvedProviderType === 'claude-cloud')
7666
+ : (normalizedRequestedProviderType === 'deepseek'
7667
+ ? 'deepseek'
7668
+ : (normalizedRequestedProviderType === 'github'
7669
+ ? 'github'
7670
+ : (openAISettings?.enabled ? 'openai' : 'github')))))));
7671
+ const resolvedProviderModel = (resolvedProviderType === 'openai' || resolvedProviderType === 'claude' || resolvedProviderType === 'cursor' || resolvedProviderType === 'grok' || resolvedProviderType === 'claude-cloud' || resolvedProviderType === 'deepseek')
7487
7672
  ? (String(providerModel || normalizedModel || '').trim() || null)
7488
7673
  : null;
7489
7674
  try {
@@ -7572,6 +7757,11 @@ void primePendingSessionWorkers('startup');
7572
7757
  // heartbeat are swept from here on.
7573
7758
  relayEventService.start();
7574
7759
  relayEventLinkService.start();
7760
+ // Schedules get their timers back; one that came due while the relay was
7761
+ // down fires now, once.
7762
+ relayScheduleService.start();
7763
+ // Asks on paired relays still open there: look for their answers again.
7764
+ relayAskLinkService.start();
7575
7765
  runtimeTimers.pendingWorkerPrime = setInterval(() => {
7576
7766
  void primePendingSessionWorkers('maintenance');
7577
7767
  detachedWorkerWatch.sweep().catch((error) => {
@@ -7892,6 +8082,8 @@ function shutdownRuntime(reason = 'unknown', { exitCode = 0 } = {}) {
7892
8082
  try { relayEventService.noteRelayExit({ action: runtimeShutdownExitCode === RELAY_RESTART_EXIT_CODE ? 'restart' : 'shutdown', reason }); } catch {}
7893
8083
  try { relayEventService.stop(); } catch {}
7894
8084
  try { relayEventLinkService.stop(); } catch {}
8085
+ try { relayScheduleService.stop(); } catch {}
8086
+ try { relayAskLinkService.stop(); } catch {}
7895
8087
  tmuxInspectorSocketService.stop();
7896
8088
  stopWorkspaceFileWatcher();
7897
8089
  sshTunnelManager.stop();
@@ -30,6 +30,7 @@ export const ABOUT_PROVIDER_LABELS = Object.freeze({
30
30
  'claude-cloud': 'Claude Cloud',
31
31
  cursor: 'Cursor',
32
32
  grok: 'Grok',
33
+ deepseek: 'DeepSeek',
33
34
  });
34
35
 
35
36
  // The CLI each provider runs, keyed as the CLI install service keys them.
@@ -68,6 +68,9 @@ export function createCacheRebuildService({
68
68
  // Session groups: a purged head leaves its group, and a wipe is followed
69
69
  // by a repair of what the wiped rows headed. Absent in older wiring.
70
70
  conversationGroups = null,
71
+ // (conversationId, { deleted }) => void: a conversation this service
72
+ // archived or removed (its relay schedules end). Absent in older wiring.
73
+ onConversationGone = null,
71
74
  }) {
72
75
  const legacyConversationList = buildLegacyConversationList(db);
73
76
  const boundConversationList = buildBoundConversationList(db);
@@ -99,10 +102,19 @@ export function createCacheRebuildService({
99
102
  db.prepare(`DELETE FROM conversations WHERE id = ?`).run(sid);
100
103
  })();
101
104
  closePreviews(sid);
105
+ noteGone(sid, { deleted: true });
102
106
  deleteOrphanedUploads(orphanedUploads);
103
107
  return orphanedUploads;
104
108
  }
105
109
 
110
+ function noteGone(conversationId, options) {
111
+ try {
112
+ onConversationGone?.(conversationId, options);
113
+ } catch (error) {
114
+ console.warn(`[cache] the end of a conversation was not passed on: ${error?.message || error}`);
115
+ }
116
+ }
117
+
106
118
  function closePreviews(conversationId) {
107
119
  try {
108
120
  closeConversationPreviews?.(conversationId);
@@ -115,7 +127,9 @@ export function createCacheRebuildService({
115
127
  const sid = normalizeId(conversationId);
116
128
  if (!sid) return false;
117
129
  const result = db.prepare(`UPDATE conversations SET archived = 1, updated_at = ? WHERE id = ? AND status != 'deleted'`).run(new Date().toISOString(), sid);
118
- return Number(result?.changes || 0) > 0;
130
+ const archived = Number(result?.changes || 0) > 0;
131
+ if (archived) noteGone(sid, { deleted: false });
132
+ return archived;
119
133
  }
120
134
 
121
135
  function fullWipeCache() {
@@ -176,7 +190,10 @@ export function createCacheRebuildService({
176
190
  })();
177
191
 
178
192
  for (const conversationId of conversationIds) {
179
- if (!manualTitleConversationSet.has(conversationId)) closePreviews(conversationId);
193
+ if (!manualTitleConversationSet.has(conversationId)) {
194
+ closePreviews(conversationId);
195
+ noteGone(conversationId, { deleted: true });
196
+ }
180
197
  }
181
198
 
182
199
  deleteOrphanedUploads(uploadHashes);