@oar-sh/oar 0.9.18 → 0.9.20
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.
- package/.github/extensions/web-relay/polling/polling-loop.mjs +2 -26
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +13 -1
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +2 -26
- package/CHANGELOG.md +359 -0
- package/README.md +10 -10
- package/bin/oar.js +391 -97
- package/package.json +4 -2
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +60 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +27 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -2
- package/server/claude-worker/claude-session-process.mjs +3 -0
- package/server/claude-worker/claude-turn-publisher.mjs +163 -8
- package/server/claude-worker/claude-usage-totals.mjs +58 -0
- package/server/copilot-worker/copilot-plan-board.mjs +4 -7
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +86 -3
- package/server/copilot-worker/copilot-sdk-session-process.mjs +57 -4
- package/server/cursor-worker/cursor-turn-runner.mjs +42 -6
- package/server/db-schema.mjs +18 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -0
- package/server/grok-worker/grok-turn-runner.mjs +44 -6
- package/server/migrations/0001-add-structured-answer.mjs +5 -7
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +5 -7
- package/server/migrations/0003-push-subscriptions.mjs +5 -7
- package/server/migrations/0004-steer-settle-markers.mjs +5 -7
- package/server/migrations/0005-remote-relays.mjs +5 -7
- package/server/migrations/0006-workspace-root-settings.mjs +5 -7
- package/server/migrations/0007-claude-cloud.mjs +5 -7
- package/server/migrations/0008-message-pins.mjs +5 -7
- package/server/migrations/0009-subagent-timeline.mjs +5 -7
- package/server/migrations/0010-subagent-timeline-pruning.mjs +5 -7
- package/server/migrations/0011-relay-event-subscriptions.mjs +5 -7
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +5 -7
- package/server/migrations/0013-previews.mjs +5 -7
- package/server/migrations/0014-conversation-groups.mjs +5 -7
- package/server/migrations/0015-conversation-group-position.mjs +5 -7
- package/server/migrations/0016-share-access-log.mjs +5 -7
- package/server/migrations/0017-conversation-history-indexes.mjs +5 -7
- package/server/migrations/0018-background-task-rows.mjs +5 -7
- package/server/migrations/0019-relay-event-subscription-options.mjs +5 -7
- package/server/migrations/0020-conversation-may-create-sessions.mjs +5 -7
- package/server/migrations/0021-conversation-group-agent-marks.mjs +5 -7
- package/server/migrations/0022-relay-schedules.mjs +5 -7
- package/server/migrations/0023-relay-asks.mjs +5 -7
- package/server/migrations/0024-deepseek-usage-totals.mjs +5 -7
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +5 -7
- package/server/migrations/0026-deepseek-usage-served-models.mjs +5 -7
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +5 -7
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +5 -7
- package/server/migrations/0029-deepseek-balance-rises.mjs +5 -7
- package/server/migrations/0030-deepseek-usage-actuals.mjs +5 -7
- package/server/migrations/0031-queue-provider.mjs +5 -7
- package/server/migrations/0032-runtime-session-billing-switch.mjs +5 -7
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +5 -7
- package/server/migrations/0034-claude-api-usage-totals.mjs +5 -7
- package/server/migrations/0035-conversation-plan-only.mjs +87 -0
- package/server/migrations/0036-token-usage.mjs +267 -0
- package/server/public/app/about-view.mjs +87 -3
- package/server/public/app/api-client.js +2 -1
- package/server/public/app/bootstrap.js +55 -2
- package/server/public/app/composer-control-state.mjs +12 -2
- package/server/public/app/context-usage-view.mjs +16 -0
- package/server/public/app/conversation-view.js +17 -2
- package/server/public/app/inbox-ui.js +32 -0
- package/server/public/app/inbox-view.mjs +141 -5
- package/server/public/app/plan-board-shared.mjs +49 -0
- package/server/public/app/plan-usage-view.mjs +1 -1
- package/server/public/app/relay-board-inline.mjs +4 -1
- package/server/public/app/relay-board-view.js +38 -10
- package/server/public/app/relay-event-view.mjs +4 -0
- package/server/public/app/socket-handlers.js +10 -2
- package/server/public/index.html +14 -0
- package/server/public/sw.js +8 -0
- package/server/relay-heap-guard.mjs +351 -0
- package/server/relay-self-restart.mjs +116 -9
- package/server/relay-tools.md +5 -4
- package/server/repositories/message-repository.mjs +31 -1
- package/server/repositories/remote-relay-repository.mjs +10 -0
- package/server/repositories/session-repository.mjs +6 -1
- package/server/routes/messages-routes.mjs +120 -4
- package/server/routes/relay-ask-routes.mjs +138 -8
- package/server/routes/relay-board-routes.mjs +55 -40
- package/server/routes/sessions-routes.mjs +141 -31
- package/server/server-runtime.mjs +142 -11
- package/server/services/about-service.mjs +59 -0
- package/server/services/cache-rebuild-service.mjs +7 -0
- package/server/services/conversation-preferences-service.mjs +31 -0
- package/server/services/database-file.mjs +413 -0
- package/server/services/db-backup-service.mjs +809 -0
- package/server/services/deepseek-usage-import.mjs +6 -9
- package/server/services/delete-archive-service.mjs +3 -0
- package/server/services/earlier-database-service.mjs +69 -126
- package/server/services/oar-cli-helpers.mjs +214 -12
- package/server/services/oar-cli-menu.mjs +1 -0
- package/server/services/oar-state-migration-service.mjs +33 -13
- package/server/services/package-data-rescue.mjs +4 -2
- package/server/services/plan-usage-claude-api.mjs +14 -15
- package/server/services/plan-usage-contract.mjs +6 -2
- package/server/services/plan-usage-deepseek.mjs +1 -1
- package/server/services/plan-usage-tokens.mjs +248 -0
- package/server/services/push-dispatch-service.mjs +134 -9
- package/server/services/relay-cli-launcher-service.mjs +3 -0
- package/server/services/relay-event-service.mjs +30 -0
- package/server/services/relay-question-context.mjs +11 -0
- package/server/services/relay-restart-picture.mjs +67 -22
- package/server/services/remote-inbox-watch-service.mjs +204 -0
- package/server/services/remote-relay-caller-context.mjs +80 -7
- package/server/services/remote-relay-dispatcher.mjs +68 -8
- package/server/services/restart-resume-service.mjs +5 -2
- package/server/services/session-worker-launch-service.mjs +12 -3
- package/server/services/session-worker-stop-service.mjs +27 -6
- package/server/services/token-usage-service.mjs +430 -0
- package/server/services/update-install-service.mjs +13 -0
- package/server/services/usage-limit-pause-service.mjs +8 -1
- package/server/services/worker-scope.mjs +77 -0
- package/shared/plan-board-actions.mjs +130 -0
- package/shared/plan-only.mjs +43 -0
- package/shared/relay-asks.mjs +3 -0
- package/shared/relay-events.mjs +38 -4
- package/shared/remote-relay-contract.mjs +98 -21
- package/shared/turn-token-usage.mjs +144 -0
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from '../services/conversation-history-page.mjs';
|
|
18
18
|
import { MESSAGE_PIN_LIMIT, buildPinList } from '../services/message-pin-list.mjs';
|
|
19
19
|
import { buildHiddenMessageList } from '../services/hidden-message-list.mjs';
|
|
20
|
-
import { persistConversationPreferences, readMayCreateSessions } from '../services/conversation-preferences-service.mjs';
|
|
20
|
+
import { persistConversationPreferences, readMayCreateSessions, readPlanOnly } from '../services/conversation-preferences-service.mjs';
|
|
21
21
|
import { parseAutoCompactWindow } from '../../shared/auto-compact-window.mjs';
|
|
22
22
|
import {
|
|
23
23
|
claudeBackgroundCommandLimitSettingsFields,
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
DEEPSEEK_REASONING_EFFORTS,
|
|
58
58
|
DEFAULT_DEEPSEEK_MODEL,
|
|
59
59
|
DEFAULT_DEEPSEEK_REASONING_EFFORT,
|
|
60
|
+
deepseekModelLabel,
|
|
60
61
|
deepseekReasoningEffortFor,
|
|
61
62
|
} from '../../shared/deepseek-models.mjs';
|
|
62
63
|
import {
|
|
@@ -103,6 +104,7 @@ import {
|
|
|
103
104
|
resolveSessionGroupReference,
|
|
104
105
|
validateRemoteRelaySessionSettings,
|
|
105
106
|
} from '../../shared/remote-relay-contract.mjs';
|
|
107
|
+
import { PLAN_ONLY_REFUSAL, planOnlyRefusesMode } from '../../shared/plan-only.mjs';
|
|
106
108
|
import {
|
|
107
109
|
buildCloudSourceRecord,
|
|
108
110
|
buildConversationCloudPayload,
|
|
@@ -148,6 +150,7 @@ import {
|
|
|
148
150
|
import { parseRestartResumeUpdate } from '../services/restart-resume-service.mjs';
|
|
149
151
|
import { removeClaudeSessionLeftovers } from '../services/claude-session-leftovers.mjs';
|
|
150
152
|
import { deepseekStartingBalanceNote } from '../services/plan-usage-deepseek.mjs';
|
|
153
|
+
import { appendTokenSections } from '../services/plan-usage-tokens.mjs';
|
|
151
154
|
import { DEEPSEEK_USAGE_CHECKPOINTS_TABLE } from '../migrations/0024-deepseek-usage-totals.mjs';
|
|
152
155
|
import { claudeProjectDirSlug } from '../services/claude-session-root-service.mjs';
|
|
153
156
|
|
|
@@ -501,15 +504,34 @@ export function parseGrokSettingsUpdateRequest(body = {}) {
|
|
|
501
504
|
};
|
|
502
505
|
}
|
|
503
506
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
507
|
+
/**
|
|
508
|
+
* The lower-cased ids Copilot itself lists in `modelState`: those an earlier
|
|
509
|
+
* provider layer did not add. A layer tags every id it adds, so an id with
|
|
510
|
+
* tags but none of them Copilot's is another provider's ("claude-opus-5-5"
|
|
511
|
+
* from the Claude layer), not one Copilot serves too.
|
|
512
|
+
*/
|
|
513
|
+
function copilotListedModelIds(modelState = {}) {
|
|
514
|
+
const providersByModel = modelState?.providersByModel && typeof modelState.providersByModel === 'object'
|
|
515
|
+
? modelState.providersByModel
|
|
516
|
+
: {};
|
|
517
|
+
return new Set(
|
|
509
518
|
(Array.isArray(modelState?.models) ? modelState.models : [])
|
|
519
|
+
.filter((value) => {
|
|
520
|
+
const id = String(value || '').trim();
|
|
521
|
+
const providers = providersByModel[id] || providersByModel[id.toLowerCase()];
|
|
522
|
+
return !Array.isArray(providers) || providers.length === 0
|
|
523
|
+
|| providers.some((provider) => String(provider || '').trim().toLowerCase() === 'github-copilot');
|
|
524
|
+
})
|
|
510
525
|
.map((value) => String(value || '').trim().toLowerCase())
|
|
511
526
|
.filter((value) => value && value.toLowerCase() !== 'auto'),
|
|
512
527
|
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function buildModelCatalogWithClaudeProvider(modelState = {}, claudeSettings = {}) {
|
|
531
|
+
const configured = claudeSettings?.enabled === true;
|
|
532
|
+
const model = String(claudeSettings?.model || '').trim();
|
|
533
|
+
if (!configured || !model) return { ...modelState };
|
|
534
|
+
const baseModels = copilotListedModelIds(modelState);
|
|
513
535
|
const claudeModels = Array.isArray(claudeSettings?.models)
|
|
514
536
|
? claudeSettings.models.map((value) => String(value || '').trim()).filter(Boolean)
|
|
515
537
|
: [];
|
|
@@ -605,11 +627,7 @@ export function buildModelCatalogWithOpenAIProvider(modelState = {}, openAISetti
|
|
|
605
627
|
const configured = openAISettings?.enabled === true;
|
|
606
628
|
const model = String(openAISettings?.model || '').trim();
|
|
607
629
|
if (!configured || !model) return { ...modelState };
|
|
608
|
-
const baseModels =
|
|
609
|
-
(Array.isArray(modelState?.models) ? modelState.models : [])
|
|
610
|
-
.map((value) => String(value || '').trim().toLowerCase())
|
|
611
|
-
.filter((value) => value && value.toLowerCase() !== 'auto'),
|
|
612
|
-
);
|
|
630
|
+
const baseModels = copilotListedModelIds(modelState);
|
|
613
631
|
const openAIModels = Array.isArray(openAISettings?.models)
|
|
614
632
|
? openAISettings.models.map((value) => String(value || '').trim()).filter(Boolean)
|
|
615
633
|
: [];
|
|
@@ -659,11 +677,7 @@ export function buildModelCatalogWithCursorProvider(modelState = {}, cursorSetti
|
|
|
659
677
|
const configured = cursorSettings?.enabled === true;
|
|
660
678
|
const model = String(cursorSettings?.model || '').trim();
|
|
661
679
|
if (!configured || !model) return { ...modelState };
|
|
662
|
-
const baseModels =
|
|
663
|
-
(Array.isArray(modelState?.models) ? modelState.models : [])
|
|
664
|
-
.map((value) => String(value || '').trim().toLowerCase())
|
|
665
|
-
.filter((value) => value && value.toLowerCase() !== 'auto'),
|
|
666
|
-
);
|
|
680
|
+
const baseModels = copilotListedModelIds(modelState);
|
|
667
681
|
const cursorModelList = Array.isArray(cursorSettings?.models)
|
|
668
682
|
? cursorSettings.models.map((value) => String(value || '').trim()).filter(Boolean)
|
|
669
683
|
: [];
|
|
@@ -748,11 +762,7 @@ export function buildModelCatalogWithGrokProvider(modelState = {}, grokSettings
|
|
|
748
762
|
const configured = grokSettings?.enabled === true;
|
|
749
763
|
const model = String(grokSettings?.model || '').trim();
|
|
750
764
|
if (!configured || !model) return { ...modelState };
|
|
751
|
-
const baseModels =
|
|
752
|
-
(Array.isArray(modelState?.models) ? modelState.models : [])
|
|
753
|
-
.map((value) => String(value || '').trim().toLowerCase())
|
|
754
|
-
.filter((value) => value && value.toLowerCase() !== 'auto'),
|
|
755
|
-
);
|
|
765
|
+
const baseModels = copilotListedModelIds(modelState);
|
|
756
766
|
const grokModelList = Array.isArray(grokSettings?.models)
|
|
757
767
|
? grokSettings.models.map((value) => String(value || '').trim()).filter(Boolean)
|
|
758
768
|
: [];
|
|
@@ -2236,6 +2246,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2236
2246
|
// The once-per-episode notes for stopped self-started turns on the key
|
|
2237
2247
|
// (plan-usage-claude-api.mjs): a saved setting starts every chat afresh.
|
|
2238
2248
|
claudeApiStopLedger = null,
|
|
2249
|
+
// Tokens per turn and per day (services/token-usage-service.mjs): a
|
|
2250
|
+
// deleted chat drops its turn rows. Absent in older wiring and most tests.
|
|
2251
|
+
tokenUsageService = null,
|
|
2239
2252
|
reconcileUnstartedConversationProviders = async () => ({
|
|
2240
2253
|
updatedUnstartedConversations: 0,
|
|
2241
2254
|
skippedStartedConversations: 0,
|
|
@@ -2573,6 +2586,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2573
2586
|
if (findDeepSeekUsageCheckpointsTable.get(DEEPSEEK_USAGE_CHECKPOINTS_TABLE)) {
|
|
2574
2587
|
db.prepare(`DELETE FROM ${DEEPSEEK_USAGE_CHECKPOINTS_TABLE} WHERE conversation_id = ?`).run(conversationId);
|
|
2575
2588
|
}
|
|
2589
|
+
// Its turns' tokens go with it; the day sums behind Check Usage stay.
|
|
2590
|
+
tokenUsageService?.forgetConversation?.(conversationId);
|
|
2576
2591
|
// The share access log goes with it; the share rows follow the foreign key.
|
|
2577
2592
|
shares().forgetConversation(conversationId);
|
|
2578
2593
|
db.prepare(`DELETE FROM conversations WHERE id = ?`).run(conversationId);
|
|
@@ -3391,6 +3406,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3391
3406
|
origin: parseRemoteRelayOriginJson(r.origin_json),
|
|
3392
3407
|
// The owner let this agent-started session's agents start sessions.
|
|
3393
3408
|
mayCreateSessions: readMayCreateSessions(r),
|
|
3409
|
+
planOnly: readPlanOnly(r),
|
|
3394
3410
|
// Set on Claude Cloud conversations: repository, branch, session link.
|
|
3395
3411
|
cloud: buildConversationCloudPayload({
|
|
3396
3412
|
cloudSourceJson: r.cloud_source_json,
|
|
@@ -4151,6 +4167,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4151
4167
|
draftUpdatedByClientId: conv.draft_updated_by_client_id || null,
|
|
4152
4168
|
origin: parseRemoteRelayOriginJson(conv.origin_json),
|
|
4153
4169
|
mayCreateSessions: readMayCreateSessions(conv),
|
|
4170
|
+
planOnly: readPlanOnly(conv),
|
|
4154
4171
|
cloud: conversationCloudPayload(conv, runtimeSession),
|
|
4155
4172
|
group: conversationGroupPayload(conversationId),
|
|
4156
4173
|
groupPosition: conversationGroupPositionOf(conv),
|
|
@@ -4857,6 +4874,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4857
4874
|
// Set on conversations another relay's agent created ("via <relay>").
|
|
4858
4875
|
origin: parseRemoteRelayOriginJson(conv.origin_json),
|
|
4859
4876
|
mayCreateSessions: readMayCreateSessions(conv),
|
|
4877
|
+
planOnly: readPlanOnly(conv),
|
|
4860
4878
|
// Set on Claude Cloud conversations: repository, branch, session link.
|
|
4861
4879
|
cloud: conversationCloudPayload(conv, runtimeSession),
|
|
4862
4880
|
// The session group it is in, with the other members; null when none.
|
|
@@ -5079,6 +5097,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5079
5097
|
? { thinkingDisplay: parseThinkingDisplay(body.thinkingDisplay) }
|
|
5080
5098
|
: {}),
|
|
5081
5099
|
...(mentions('mayCreateSessions') ? { mayCreateSessions: body.mayCreateSessions === true } : {}),
|
|
5100
|
+
...(mentions('planOnly') ? { planOnly: body.planOnly === true } : {}),
|
|
5082
5101
|
updatedAt: now,
|
|
5083
5102
|
createIfMissing: !existing,
|
|
5084
5103
|
createTitle: 'Session',
|
|
@@ -5092,6 +5111,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5092
5111
|
thinkingEnabled: persisted.thinkingEnabled ?? null,
|
|
5093
5112
|
thinkingDisplay: persisted.thinkingDisplay ?? null,
|
|
5094
5113
|
mayCreateSessions: persisted.mayCreateSessions === true,
|
|
5114
|
+
planOnly: persisted.planOnly === true,
|
|
5095
5115
|
updatedAt: persisted.updatedAt,
|
|
5096
5116
|
senderClientId,
|
|
5097
5117
|
};
|
|
@@ -5121,6 +5141,26 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5121
5141
|
return res.status(503).json({ error: 'This setting is unavailable until the relay restarts (its storage is missing).' });
|
|
5122
5142
|
}
|
|
5123
5143
|
}
|
|
5144
|
+
// Plan-only: the owner's toggle here. Agents set and clear it through
|
|
5145
|
+
// update_session only (its dry run and Allow card), never through this
|
|
5146
|
+
// route, and they cannot change a plan-only session's mode here either.
|
|
5147
|
+
const remoteRequest = readRemoteRelayRequest(req, remoteRelayInbound).remote;
|
|
5148
|
+
if (Object.prototype.hasOwnProperty.call(req.body || {}, 'planOnly')) {
|
|
5149
|
+
if (remoteRequest) {
|
|
5150
|
+
return res.status(403).json({ error: 'Agents set plan-only through relay_tool update_session, not through this route.' });
|
|
5151
|
+
}
|
|
5152
|
+
if (typeof req.body.planOnly !== 'boolean') {
|
|
5153
|
+
return res.status(400).json({ error: 'planOnly must be true or false' });
|
|
5154
|
+
}
|
|
5155
|
+
if (!stmts.conversationsHavePlanOnly) {
|
|
5156
|
+
return res.status(503).json({ error: 'This setting is unavailable until the relay restarts (its storage is missing).' });
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
if (remoteRequest && readPlanOnly(existing)
|
|
5160
|
+
&& Object.prototype.hasOwnProperty.call(req.body || {}, 'preferredRelayMode')
|
|
5161
|
+
&& planOnlyRefusesMode(req.body.preferredRelayMode)) {
|
|
5162
|
+
return res.status(409).json(PLAN_ONLY_REFUSAL);
|
|
5163
|
+
}
|
|
5124
5164
|
|
|
5125
5165
|
const persisted = applyConversationPreferences({ conversationId, existing, body: req.body || {}, senderClientId });
|
|
5126
5166
|
return res.json({ ok: true, ...persisted });
|
|
@@ -5161,6 +5201,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5161
5201
|
thinking_enabled: parseThinkingEnabled(conv.thinking_enabled),
|
|
5162
5202
|
thinking_display: parseThinkingDisplay(conv.thinking_display),
|
|
5163
5203
|
cwd: String(workspace?.currentWorkspaceRootPath || workspace?.configuredWorkspaceRootPath || conv.configured_workspace_root_path || '').trim() || null,
|
|
5204
|
+
planOnly: readPlanOnly(conv),
|
|
5164
5205
|
};
|
|
5165
5206
|
}
|
|
5166
5207
|
|
|
@@ -5194,7 +5235,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5194
5235
|
* body }`. The plan holds what changes (`changed`, in the order of the
|
|
5195
5236
|
* contract's settings) and how.
|
|
5196
5237
|
*/
|
|
5197
|
-
function planSessionSettings(conversation, raw) {
|
|
5238
|
+
function planSessionSettings(conversation, raw, { remote = false } = {}) {
|
|
5198
5239
|
const conversationId = String(conversation.id);
|
|
5199
5240
|
const fail = (status, error, extra = {}) => ({ ok: false, status, body: { ok: false, error, ...extra } });
|
|
5200
5241
|
const settingsInput = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
|
|
@@ -5229,6 +5270,30 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5229
5270
|
change('mode');
|
|
5230
5271
|
}
|
|
5231
5272
|
}
|
|
5273
|
+
// Plan-only, read as it is after this call: one that clears the flag may
|
|
5274
|
+
// name another mode too, one that keeps or sets it may not. Checked before
|
|
5275
|
+
// anything else is planned, so a dry run refuses before any Allow card.
|
|
5276
|
+
if (wanted.planOnly !== undefined && !stmts.conversationsHavePlanOnly) {
|
|
5277
|
+
return fail(503, 'planOnly is unavailable until the relay restarts (its storage is missing). Nothing was changed.', { code: SESSION_SETTINGS_INVALID });
|
|
5278
|
+
}
|
|
5279
|
+
// planOnly:false counts only in the call that switches to another mode:
|
|
5280
|
+
// alone (or with plan) it is passed over, so a caller that fills every
|
|
5281
|
+
// property with false never drops the lock by accident.
|
|
5282
|
+
if (wanted.planOnly === false && !(wanted.mode !== undefined && wanted.mode !== 'plan')) {
|
|
5283
|
+
delete wanted.planOnly;
|
|
5284
|
+
if (current.planOnly === true) plan.notes.push('planOnly unchanged: clear it in the same call as the new mode.');
|
|
5285
|
+
}
|
|
5286
|
+
const planOnlyAfter = wanted.planOnly !== undefined ? wanted.planOnly === true : current.planOnly === true;
|
|
5287
|
+
if (remote && planOnlyAfter && wanted.mode !== undefined && planOnlyRefusesMode(wanted.mode)) {
|
|
5288
|
+
return fail(409, `${PLAN_ONLY_REFUSAL.error} Nothing was changed.`, { code: PLAN_ONLY_REFUSAL.code });
|
|
5289
|
+
}
|
|
5290
|
+
if (wanted.planOnly !== undefined && wanted.planOnly !== current.planOnly) {
|
|
5291
|
+
if (wanted.planOnly === true && (wanted.mode !== undefined ? wanted.mode : current.mode) !== 'plan') {
|
|
5292
|
+
return fail(400, `planOnly keeps a session in plan mode, and this one is in ${current.mode} mode: add mode: "plan" in the same call. Nothing was changed.`, { code: SESSION_SETTINGS_INVALID });
|
|
5293
|
+
}
|
|
5294
|
+
plan.preferences.planOnly = wanted.planOnly;
|
|
5295
|
+
change('planOnly');
|
|
5296
|
+
}
|
|
5232
5297
|
if (wanted.auto_compact_window !== undefined) {
|
|
5233
5298
|
const window = wanted.auto_compact_window === 'auto' ? null : parseAutoCompactWindow(wanted.auto_compact_window);
|
|
5234
5299
|
if (window !== parseAutoCompactWindow(conversation.auto_compact_window)) {
|
|
@@ -5415,7 +5480,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5415
5480
|
}
|
|
5416
5481
|
const conversationId = String(conversation.id);
|
|
5417
5482
|
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
5418
|
-
const planned = planSessionSettings(conversation, body.settings);
|
|
5483
|
+
const planned = planSessionSettings(conversation, body.settings, { remote: remoteRequest.remote === true });
|
|
5419
5484
|
if (!planned.ok) return res.status(planned.status).json(planned.body);
|
|
5420
5485
|
const { plan } = planned;
|
|
5421
5486
|
const createdBy = parseRemoteRelayOriginJson(conversation.origin_json);
|
|
@@ -6067,6 +6132,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6067
6132
|
stopOverrides: sessionWorkerStopOverrides,
|
|
6068
6133
|
});
|
|
6069
6134
|
if (!stopped.ok) {
|
|
6135
|
+
console.warn(`[delete] Not deleted ${id}: its worker did not stop (${stopped.error || 'unknown'}; remaining pids ${(stopped.remainingPids || []).join(',') || 'none'}).`);
|
|
6070
6136
|
return {
|
|
6071
6137
|
statusCode: 409,
|
|
6072
6138
|
body: {
|
|
@@ -6213,6 +6279,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6213
6279
|
stopOverrides: sessionWorkerStopOverrides,
|
|
6214
6280
|
});
|
|
6215
6281
|
if (!stopped.ok) {
|
|
6282
|
+
console.warn(`[archive] Not archived ${id}: its worker did not stop (${stopped.error || 'unknown'}; remaining pids ${(stopped.remainingPids || []).join(',') || 'none'}).`);
|
|
6216
6283
|
return {
|
|
6217
6284
|
statusCode: 409,
|
|
6218
6285
|
body: {
|
|
@@ -6600,6 +6667,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6600
6667
|
// lookup below knows exact base ids only and would fall back too.
|
|
6601
6668
|
let copilotBoundModel = '';
|
|
6602
6669
|
let copilotVariantEffort = '';
|
|
6670
|
+
let copilotRefusal = null;
|
|
6603
6671
|
if (
|
|
6604
6672
|
remoteRequest.remote
|
|
6605
6673
|
&& !useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
@@ -6625,20 +6693,23 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6625
6693
|
effortsFor: (base) => reasoningByModel[base] || reasoningByModel[String(base).toLowerCase()] || null,
|
|
6626
6694
|
});
|
|
6627
6695
|
// Another provider's model is answered by the checks below ("requires
|
|
6628
|
-
// the Claude provider"), not as an unknown one.
|
|
6696
|
+
// the Claude provider"), not as an unknown one. One that several other
|
|
6697
|
+
// providers list ("claude-opus-5-5": Claude and Cursor) passes those
|
|
6698
|
+
// checks and is refused as unknown after them.
|
|
6629
6699
|
const otherProviders = mergedModels.some((modelId) => String(modelId || '').trim().toLowerCase() === requestedBootstrapModel.toLowerCase());
|
|
6630
6700
|
if (copilotCheck.status === 'known' && copilotCheck.model && !isAutoModelId(copilotCheck.model)) {
|
|
6631
6701
|
copilotBoundModel = copilotCheck.base || copilotCheck.model;
|
|
6632
6702
|
copilotVariantEffort = copilotCheck.effort || '';
|
|
6633
6703
|
}
|
|
6634
|
-
if (copilotCheck.status === 'unknown'
|
|
6635
|
-
|
|
6704
|
+
if (copilotCheck.status === 'unknown') {
|
|
6705
|
+
copilotRefusal = {
|
|
6636
6706
|
ok: false,
|
|
6637
6707
|
error: unknownProviderModelText({ model: copilotCheck.model, provider: 'github', suggestions: copilotCheck.suggestions, models: copilotCheck.models }),
|
|
6638
6708
|
code: 'COPILOT_MODEL_UNAVAILABLE',
|
|
6639
6709
|
supportedModels: copilotCheck.models,
|
|
6640
6710
|
...(copilotCheck.suggestions.length ? { suggestedModels: copilotCheck.suggestions } : {}),
|
|
6641
|
-
}
|
|
6711
|
+
};
|
|
6712
|
+
if (!otherProviders) return res.status(400).json(copilotRefusal);
|
|
6642
6713
|
}
|
|
6643
6714
|
}
|
|
6644
6715
|
if (useOpenAIProvider && openAISettings?.configured !== true) {
|
|
@@ -6798,6 +6869,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6798
6869
|
});
|
|
6799
6870
|
}
|
|
6800
6871
|
}
|
|
6872
|
+
// No single other provider claims it: a Copilot session with a model
|
|
6873
|
+
// Copilot does not list would be refused at its first send.
|
|
6874
|
+
if (copilotRefusal) return res.status(400).json(copilotRefusal);
|
|
6801
6875
|
|
|
6802
6876
|
const bootstrapProviderType = useClaudeCloudProvider
|
|
6803
6877
|
? 'claude-cloud'
|
|
@@ -6815,10 +6889,27 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6815
6889
|
// new chat would be stored as the default one and reset the composer. An
|
|
6816
6890
|
// omitted or unknown mode still falls back to the default rather than 400,
|
|
6817
6891
|
// because a new chat is worth starting either way.
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6892
|
+
// Plan-only: a session that starts locked in plan mode. With no
|
|
6893
|
+
// mode named it starts in plan; a mode that is not plan is refused.
|
|
6894
|
+
const bootstrapPlanOnly = Object.prototype.hasOwnProperty.call(req.body || {}, 'planOnly') ? req.body.planOnly : undefined;
|
|
6895
|
+
if (bootstrapPlanOnly !== undefined && typeof bootstrapPlanOnly !== 'boolean') {
|
|
6896
|
+
return res.status(400).json({ ok: false, error: 'planOnly must be true or false', code: 'PLAN_ONLY_INVALID' });
|
|
6897
|
+
}
|
|
6898
|
+
const namedBootstrapMode = req.body?.relayMode ?? req.body?.preferredRelayMode;
|
|
6899
|
+
if (bootstrapPlanOnly === true) {
|
|
6900
|
+
if (!stmts.conversationsHavePlanOnly) {
|
|
6901
|
+
return res.status(503).json({ ok: false, error: 'planOnly is unavailable until the relay restarts (its storage is missing). No conversation was created.', code: 'PLAN_ONLY_UNAVAILABLE' });
|
|
6902
|
+
}
|
|
6903
|
+
if (String(namedBootstrapMode ?? '').trim() && String(namedBootstrapMode).trim().toLowerCase() !== 'plan') {
|
|
6904
|
+
return res.status(400).json({ ok: false, error: `planOnly keeps a session in plan mode; it cannot start in ${String(namedBootstrapMode).trim()} mode. No conversation was created.`, code: 'PLAN_ONLY_MODE' });
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
const bootstrapRelayMode = bootstrapPlanOnly === true
|
|
6908
|
+
? 'plan'
|
|
6909
|
+
: normalizeRelayModePreference(namedBootstrapMode, {
|
|
6910
|
+
supportedRelayModes: SUPPORTED_RELAY_MODES,
|
|
6911
|
+
fallbackMode: DEFAULT_RELAY_MODE,
|
|
6912
|
+
});
|
|
6822
6913
|
// The New Chat modal's reasoning choice is validated with the same rules the
|
|
6823
6914
|
// first send would apply, so the composer can restore it from the server
|
|
6824
6915
|
// instead of guessing from localStorage.
|
|
@@ -6896,6 +6987,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6896
6987
|
preferredRelayMode: bootstrapRelayMode,
|
|
6897
6988
|
preferredModel: selectedModel,
|
|
6898
6989
|
preferredReasoningEffort: bootstrapReasoning.effort,
|
|
6990
|
+
...(bootstrapPlanOnly === true ? { planOnly: true } : {}),
|
|
6899
6991
|
updatedAt: now,
|
|
6900
6992
|
});
|
|
6901
6993
|
return { boundRuntimeSession, preferences };
|
|
@@ -7014,6 +7106,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7014
7106
|
preferredRelayMode,
|
|
7015
7107
|
preferredModel,
|
|
7016
7108
|
preferredReasoningEffort,
|
|
7109
|
+
// Echoed so the calling relay can tell this relay knows the flag.
|
|
7110
|
+
planOnly: preferences?.planOnly === true,
|
|
7017
7111
|
configuredWorkspaceRootPath: workspaceRootState?.configuredWorkspaceRootPath || null,
|
|
7018
7112
|
configuredWorkspaceRootName: workspaceRootState?.configuredWorkspaceRootName || null,
|
|
7019
7113
|
currentWorkspaceRootPath: workspaceRootState?.currentWorkspaceRootPath || null,
|
|
@@ -9127,6 +9221,22 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
9127
9221
|
deepseekActuals: deepseekEnabled ? readDeepSeekActuals() : null,
|
|
9128
9222
|
claudeApiCard,
|
|
9129
9223
|
});
|
|
9224
|
+
// Every tab's Tokens sections, from the token booking.
|
|
9225
|
+
if (tokenUsageService) {
|
|
9226
|
+
appendTokenSections(report.providers, (options) => tokenUsageService.readPeriods(options), {
|
|
9227
|
+
// A tab names its models as its other rows do: the Claude API tab as
|
|
9228
|
+
// its spend rows (the Console's names), DeepSeek with its own labels,
|
|
9229
|
+
// the Claude tab with Claude's; the raw id where there is none.
|
|
9230
|
+
modelLabelFor: (provider) => {
|
|
9231
|
+
if (provider === 'claude-api' && typeof claudeApiPlanUsage?.modelLabel === 'function') {
|
|
9232
|
+
return (model) => claudeApiPlanUsage.modelLabel(model);
|
|
9233
|
+
}
|
|
9234
|
+
if (provider === 'deepseek') return (model) => deepseekModelLabel(model);
|
|
9235
|
+
if (provider === 'claude') return (model) => claudeAttributionModelLabel(model);
|
|
9236
|
+
return null;
|
|
9237
|
+
},
|
|
9238
|
+
});
|
|
9239
|
+
}
|
|
9130
9240
|
|
|
9131
9241
|
// Legacy top-level fields stay alongside `providers` so an older cached
|
|
9132
9242
|
// client shell keeps working through a PWA update.
|