@oar-sh/oar 0.9.19 → 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/skills/question-routing-hooks.mjs +2 -26
- package/CHANGELOG.md +317 -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 +1 -0
- 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 +14 -1
- package/server/relay-self-restart.mjs +83 -2
- package/server/relay-tools.md +4 -3
- 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 +87 -0
- package/server/routes/relay-ask-routes.mjs +138 -8
- package/server/routes/relay-board-routes.mjs +55 -40
- package/server/routes/sessions-routes.mjs +103 -7
- package/server/server-runtime.mjs +132 -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 +40 -5
- 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 +66 -11
- package/shared/turn-token-usage.mjs +144 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
import { persistConversationPreferences } from '../services/conversation-preferences-service.mjs';
|
|
3
|
+
import { persistConversationPreferences, readPlanOnly } from '../services/conversation-preferences-service.mjs';
|
|
4
4
|
import { fallbackReasoningEffort } from '../services/conversation-model-fallback.mjs';
|
|
5
|
+
import {
|
|
6
|
+
PLAN_BOARD_APPROVE_ONLY,
|
|
7
|
+
isPendingPlanBoard,
|
|
8
|
+
planBoardActionLabel,
|
|
9
|
+
planBoardFollowupMode,
|
|
10
|
+
planBoardFollowupText,
|
|
11
|
+
} from '../../shared/plan-board-actions.mjs';
|
|
5
12
|
|
|
6
13
|
function normalizeId(value) {
|
|
7
14
|
const text = String(value || '').trim();
|
|
@@ -20,34 +27,6 @@ function normalizeBoardType(value) {
|
|
|
20
27
|
return text.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'generic';
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
function labelForAction(actionId) {
|
|
24
|
-
const id = String(actionId || '').trim().toLowerCase();
|
|
25
|
-
if (id === 'autopilot') return 'Implement in autopilot';
|
|
26
|
-
if (id === 'autopilot_fleet') return 'Implement with autopilot fleet';
|
|
27
|
-
if (id === 'interactive') return 'Stop here and prompt myself';
|
|
28
|
-
if (id === 'exit_only') return 'Stop here';
|
|
29
|
-
return id.replace(/[_-]+/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()) || 'Select';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function defaultPromptForAction(actionId, board = null) {
|
|
33
|
-
const summary = String(board?.body || '').trim();
|
|
34
|
-
if (actionId === 'autopilot') {
|
|
35
|
-
return `Please implement the approved plan now.\n\nPlan summary:\n${summary}`;
|
|
36
|
-
}
|
|
37
|
-
if (actionId === 'autopilot_fleet') {
|
|
38
|
-
return `Please implement the approved plan using autopilot_fleet where useful.\n\nPlan summary:\n${summary}`;
|
|
39
|
-
}
|
|
40
|
-
if (actionId === 'interactive') {
|
|
41
|
-
return `I approved the plan. Let's proceed interactively with implementation.\n\nPlan summary:\n${summary}`;
|
|
42
|
-
}
|
|
43
|
-
return '';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function modeForAction(actionId, fallbackMode) {
|
|
47
|
-
if (actionId === 'autopilot' || actionId === 'autopilot_fleet') return 'autopilot';
|
|
48
|
-
if (actionId === 'interactive') return 'agent';
|
|
49
|
-
return fallbackMode;
|
|
50
|
-
}
|
|
51
30
|
|
|
52
31
|
function normalizeBoardActions(rawActions) {
|
|
53
32
|
const input = Array.isArray(rawActions) ? rawActions : [];
|
|
@@ -61,7 +40,7 @@ function normalizeBoardActions(rawActions) {
|
|
|
61
40
|
const id = String(item.id || item.actionId || item.value || '').trim().toLowerCase();
|
|
62
41
|
if (!id || seen.has(id)) continue;
|
|
63
42
|
seen.add(id);
|
|
64
|
-
const label = String(item.label || item.title ||
|
|
43
|
+
const label = String(item.label || item.title || planBoardActionLabel(id)).trim().slice(0, 120) || planBoardActionLabel(id);
|
|
65
44
|
const mode = normalizeId(item.mode);
|
|
66
45
|
const prompt = normalizeBoardText(item.prompt || '', '');
|
|
67
46
|
normalized.push({
|
|
@@ -99,6 +78,7 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
99
78
|
DEFAULT_MODEL,
|
|
100
79
|
formatRelayBoardRow,
|
|
101
80
|
pushDispatchService,
|
|
81
|
+
relayEventService,
|
|
102
82
|
} = deps;
|
|
103
83
|
|
|
104
84
|
/**
|
|
@@ -201,7 +181,12 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
201
181
|
const boardId = uuidv4();
|
|
202
182
|
const relayMode = normalizeRelayMode(mode || q.relay_mode) || DEFAULT_RELAY_MODE;
|
|
203
183
|
const normalizedActions = normalizeBoardActions(actions);
|
|
204
|
-
|
|
184
|
+
// A plan-only session's plan card recommends "Approve plan", whatever the
|
|
185
|
+
// worker or the Copilot CLI recommended.
|
|
186
|
+
const boardConversationId = normalizeId(conversationId) || q.conversation_id;
|
|
187
|
+
const planOnly = normalizedBoardType === 'plan_ready'
|
|
188
|
+
&& readPlanOnly(stmts.getConvAnyStatus?.get?.(boardConversationId));
|
|
189
|
+
const normalizedRecommended = planOnly ? PLAN_BOARD_APPROVE_ONLY : normalizeId(recommendedAction);
|
|
205
190
|
const boardContext = parseContext(context);
|
|
206
191
|
|
|
207
192
|
stmts.insertBoard.run(
|
|
@@ -240,23 +225,43 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
240
225
|
}
|
|
241
226
|
|
|
242
227
|
const board = formatRelayBoardRow(row);
|
|
228
|
+
const conversationRow = stmts.getConvAnyStatus?.get?.(String(board?.conversationId || '').trim()) || null;
|
|
229
|
+
if (conversationRow && String(conversationRow.status || '').trim() === 'deleted') {
|
|
230
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
231
|
+
}
|
|
243
232
|
const actions = Array.isArray(board?.actions) ? board.actions : [];
|
|
244
|
-
const
|
|
233
|
+
const chosen = actionId.toLowerCase();
|
|
234
|
+
// "Approve plan" is taken on any pending plan card, also one stored
|
|
235
|
+
// before the button existed.
|
|
236
|
+
const selected = actions.find((item) => String(item?.id || '').trim().toLowerCase() === chosen)
|
|
237
|
+
|| (chosen === PLAN_BOARD_APPROVE_ONLY && isPendingPlanBoard(board)
|
|
238
|
+
? { id: PLAN_BOARD_APPROVE_ONLY, label: planBoardActionLabel(PLAN_BOARD_APPROVE_ONLY), mode: planBoardFollowupMode(PLAN_BOARD_APPROVE_ONLY) }
|
|
239
|
+
: null);
|
|
245
240
|
if (!selected) return res.status(400).json({ error: 'Unknown board action' });
|
|
246
241
|
|
|
247
242
|
const now = new Date().toISOString();
|
|
248
|
-
const shouldDismiss =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
243
|
+
const shouldDismiss = chosen === 'exit_only';
|
|
244
|
+
const approveOnly = chosen === PLAN_BOARD_APPROVE_ONLY;
|
|
245
|
+
// Only a pending card is settled: a second press (here or in the Inbox)
|
|
246
|
+
// finds it settled already and queues nothing.
|
|
247
|
+
const settledRow = shouldDismiss
|
|
248
|
+
? stmts.dismissBoard.run(chosen, now, now, boardId)
|
|
249
|
+
: stmts.markBoardAction.run(chosen, now, now, boardId);
|
|
250
|
+
if (Number(settledRow?.changes) !== 1) {
|
|
251
|
+
const current = stmts.getBoard.get(boardId);
|
|
252
|
+
return res.status(409).json({ error: `Board already ${current?.status || 'settled'}`, board: formatRelayBoardRow(current) });
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
let queuedMessageId = null;
|
|
256
256
|
if (!shouldDismiss) {
|
|
257
|
-
|
|
257
|
+
// approve_only says exactly its line; the others may carry their own.
|
|
258
|
+
const followupPrompt = normalizeBoardText(approveOnly ? planBoardFollowupText(chosen) : (selected.prompt || planBoardFollowupText(chosen)), '');
|
|
258
259
|
if (followupPrompt) {
|
|
259
|
-
|
|
260
|
+
// approve_only runs this one turn in agent mode (plan mode blocks
|
|
261
|
+
// the writes its plan-only steps need); the session stays in plan.
|
|
262
|
+
const relayMode = normalizeRelayMode(approveOnly
|
|
263
|
+
? planBoardFollowupMode(chosen)
|
|
264
|
+
: (selected.mode || planBoardFollowupMode(chosen) || board.mode)) || DEFAULT_RELAY_MODE;
|
|
260
265
|
const convId = String(board?.conversationId || '').trim();
|
|
261
266
|
const runtimeSession = stmts.getRuntimeSessionByConversation?.get?.(convId) || null;
|
|
262
267
|
const latestModel = stmts.getLatestConversationModel?.get?.(convId)?.model || null;
|
|
@@ -323,11 +328,21 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
323
328
|
timestamp: now,
|
|
324
329
|
},
|
|
325
330
|
});
|
|
326
|
-
followChosenMode(convId, relayMode, now);
|
|
331
|
+
if (!approveOnly) followChosenMode(convId, relayMode, now);
|
|
327
332
|
}
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
const updated = formatRelayBoardRow(stmts.getBoard.get(boardId));
|
|
336
|
+
try {
|
|
337
|
+
relayEventService?.noteBoardSettled?.(String(updated?.conversationId || board?.conversationId || ''), {
|
|
338
|
+
boardId,
|
|
339
|
+
boardType: updated?.boardType || board?.boardType || null,
|
|
340
|
+
action: chosen,
|
|
341
|
+
actionLabel: String(selected.label || '').trim() || planBoardActionLabel(chosen),
|
|
342
|
+
title: updated?.title || board?.title || '',
|
|
343
|
+
queuedMessageId,
|
|
344
|
+
});
|
|
345
|
+
} catch {}
|
|
331
346
|
io.emit('relay_board_updated', { board: updated });
|
|
332
347
|
io.emit('relay_board_changed', { conversationId: updated?.conversationId || null, boardId });
|
|
333
348
|
return res.json({
|
|
@@ -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
|
|
|
@@ -2243,6 +2246,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2243
2246
|
// The once-per-episode notes for stopped self-started turns on the key
|
|
2244
2247
|
// (plan-usage-claude-api.mjs): a saved setting starts every chat afresh.
|
|
2245
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,
|
|
2246
2252
|
reconcileUnstartedConversationProviders = async () => ({
|
|
2247
2253
|
updatedUnstartedConversations: 0,
|
|
2248
2254
|
skippedStartedConversations: 0,
|
|
@@ -2580,6 +2586,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2580
2586
|
if (findDeepSeekUsageCheckpointsTable.get(DEEPSEEK_USAGE_CHECKPOINTS_TABLE)) {
|
|
2581
2587
|
db.prepare(`DELETE FROM ${DEEPSEEK_USAGE_CHECKPOINTS_TABLE} WHERE conversation_id = ?`).run(conversationId);
|
|
2582
2588
|
}
|
|
2589
|
+
// Its turns' tokens go with it; the day sums behind Check Usage stay.
|
|
2590
|
+
tokenUsageService?.forgetConversation?.(conversationId);
|
|
2583
2591
|
// The share access log goes with it; the share rows follow the foreign key.
|
|
2584
2592
|
shares().forgetConversation(conversationId);
|
|
2585
2593
|
db.prepare(`DELETE FROM conversations WHERE id = ?`).run(conversationId);
|
|
@@ -3398,6 +3406,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3398
3406
|
origin: parseRemoteRelayOriginJson(r.origin_json),
|
|
3399
3407
|
// The owner let this agent-started session's agents start sessions.
|
|
3400
3408
|
mayCreateSessions: readMayCreateSessions(r),
|
|
3409
|
+
planOnly: readPlanOnly(r),
|
|
3401
3410
|
// Set on Claude Cloud conversations: repository, branch, session link.
|
|
3402
3411
|
cloud: buildConversationCloudPayload({
|
|
3403
3412
|
cloudSourceJson: r.cloud_source_json,
|
|
@@ -4158,6 +4167,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4158
4167
|
draftUpdatedByClientId: conv.draft_updated_by_client_id || null,
|
|
4159
4168
|
origin: parseRemoteRelayOriginJson(conv.origin_json),
|
|
4160
4169
|
mayCreateSessions: readMayCreateSessions(conv),
|
|
4170
|
+
planOnly: readPlanOnly(conv),
|
|
4161
4171
|
cloud: conversationCloudPayload(conv, runtimeSession),
|
|
4162
4172
|
group: conversationGroupPayload(conversationId),
|
|
4163
4173
|
groupPosition: conversationGroupPositionOf(conv),
|
|
@@ -4864,6 +4874,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4864
4874
|
// Set on conversations another relay's agent created ("via <relay>").
|
|
4865
4875
|
origin: parseRemoteRelayOriginJson(conv.origin_json),
|
|
4866
4876
|
mayCreateSessions: readMayCreateSessions(conv),
|
|
4877
|
+
planOnly: readPlanOnly(conv),
|
|
4867
4878
|
// Set on Claude Cloud conversations: repository, branch, session link.
|
|
4868
4879
|
cloud: conversationCloudPayload(conv, runtimeSession),
|
|
4869
4880
|
// The session group it is in, with the other members; null when none.
|
|
@@ -5086,6 +5097,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5086
5097
|
? { thinkingDisplay: parseThinkingDisplay(body.thinkingDisplay) }
|
|
5087
5098
|
: {}),
|
|
5088
5099
|
...(mentions('mayCreateSessions') ? { mayCreateSessions: body.mayCreateSessions === true } : {}),
|
|
5100
|
+
...(mentions('planOnly') ? { planOnly: body.planOnly === true } : {}),
|
|
5089
5101
|
updatedAt: now,
|
|
5090
5102
|
createIfMissing: !existing,
|
|
5091
5103
|
createTitle: 'Session',
|
|
@@ -5099,6 +5111,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5099
5111
|
thinkingEnabled: persisted.thinkingEnabled ?? null,
|
|
5100
5112
|
thinkingDisplay: persisted.thinkingDisplay ?? null,
|
|
5101
5113
|
mayCreateSessions: persisted.mayCreateSessions === true,
|
|
5114
|
+
planOnly: persisted.planOnly === true,
|
|
5102
5115
|
updatedAt: persisted.updatedAt,
|
|
5103
5116
|
senderClientId,
|
|
5104
5117
|
};
|
|
@@ -5128,6 +5141,26 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5128
5141
|
return res.status(503).json({ error: 'This setting is unavailable until the relay restarts (its storage is missing).' });
|
|
5129
5142
|
}
|
|
5130
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
|
+
}
|
|
5131
5164
|
|
|
5132
5165
|
const persisted = applyConversationPreferences({ conversationId, existing, body: req.body || {}, senderClientId });
|
|
5133
5166
|
return res.json({ ok: true, ...persisted });
|
|
@@ -5168,6 +5201,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5168
5201
|
thinking_enabled: parseThinkingEnabled(conv.thinking_enabled),
|
|
5169
5202
|
thinking_display: parseThinkingDisplay(conv.thinking_display),
|
|
5170
5203
|
cwd: String(workspace?.currentWorkspaceRootPath || workspace?.configuredWorkspaceRootPath || conv.configured_workspace_root_path || '').trim() || null,
|
|
5204
|
+
planOnly: readPlanOnly(conv),
|
|
5171
5205
|
};
|
|
5172
5206
|
}
|
|
5173
5207
|
|
|
@@ -5201,7 +5235,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5201
5235
|
* body }`. The plan holds what changes (`changed`, in the order of the
|
|
5202
5236
|
* contract's settings) and how.
|
|
5203
5237
|
*/
|
|
5204
|
-
function planSessionSettings(conversation, raw) {
|
|
5238
|
+
function planSessionSettings(conversation, raw, { remote = false } = {}) {
|
|
5205
5239
|
const conversationId = String(conversation.id);
|
|
5206
5240
|
const fail = (status, error, extra = {}) => ({ ok: false, status, body: { ok: false, error, ...extra } });
|
|
5207
5241
|
const settingsInput = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
|
|
@@ -5236,6 +5270,30 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5236
5270
|
change('mode');
|
|
5237
5271
|
}
|
|
5238
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
|
+
}
|
|
5239
5297
|
if (wanted.auto_compact_window !== undefined) {
|
|
5240
5298
|
const window = wanted.auto_compact_window === 'auto' ? null : parseAutoCompactWindow(wanted.auto_compact_window);
|
|
5241
5299
|
if (window !== parseAutoCompactWindow(conversation.auto_compact_window)) {
|
|
@@ -5422,7 +5480,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5422
5480
|
}
|
|
5423
5481
|
const conversationId = String(conversation.id);
|
|
5424
5482
|
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
5425
|
-
const planned = planSessionSettings(conversation, body.settings);
|
|
5483
|
+
const planned = planSessionSettings(conversation, body.settings, { remote: remoteRequest.remote === true });
|
|
5426
5484
|
if (!planned.ok) return res.status(planned.status).json(planned.body);
|
|
5427
5485
|
const { plan } = planned;
|
|
5428
5486
|
const createdBy = parseRemoteRelayOriginJson(conversation.origin_json);
|
|
@@ -6074,6 +6132,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6074
6132
|
stopOverrides: sessionWorkerStopOverrides,
|
|
6075
6133
|
});
|
|
6076
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'}).`);
|
|
6077
6136
|
return {
|
|
6078
6137
|
statusCode: 409,
|
|
6079
6138
|
body: {
|
|
@@ -6220,6 +6279,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6220
6279
|
stopOverrides: sessionWorkerStopOverrides,
|
|
6221
6280
|
});
|
|
6222
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'}).`);
|
|
6223
6283
|
return {
|
|
6224
6284
|
statusCode: 409,
|
|
6225
6285
|
body: {
|
|
@@ -6829,10 +6889,27 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6829
6889
|
// new chat would be stored as the default one and reset the composer. An
|
|
6830
6890
|
// omitted or unknown mode still falls back to the default rather than 400,
|
|
6831
6891
|
// because a new chat is worth starting either way.
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
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
|
+
});
|
|
6836
6913
|
// The New Chat modal's reasoning choice is validated with the same rules the
|
|
6837
6914
|
// first send would apply, so the composer can restore it from the server
|
|
6838
6915
|
// instead of guessing from localStorage.
|
|
@@ -6910,6 +6987,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6910
6987
|
preferredRelayMode: bootstrapRelayMode,
|
|
6911
6988
|
preferredModel: selectedModel,
|
|
6912
6989
|
preferredReasoningEffort: bootstrapReasoning.effort,
|
|
6990
|
+
...(bootstrapPlanOnly === true ? { planOnly: true } : {}),
|
|
6913
6991
|
updatedAt: now,
|
|
6914
6992
|
});
|
|
6915
6993
|
return { boundRuntimeSession, preferences };
|
|
@@ -7028,6 +7106,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7028
7106
|
preferredRelayMode,
|
|
7029
7107
|
preferredModel,
|
|
7030
7108
|
preferredReasoningEffort,
|
|
7109
|
+
// Echoed so the calling relay can tell this relay knows the flag.
|
|
7110
|
+
planOnly: preferences?.planOnly === true,
|
|
7031
7111
|
configuredWorkspaceRootPath: workspaceRootState?.configuredWorkspaceRootPath || null,
|
|
7032
7112
|
configuredWorkspaceRootName: workspaceRootState?.configuredWorkspaceRootName || null,
|
|
7033
7113
|
currentWorkspaceRootPath: workspaceRootState?.currentWorkspaceRootPath || null,
|
|
@@ -9141,6 +9221,22 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
9141
9221
|
deepseekActuals: deepseekEnabled ? readDeepSeekActuals() : null,
|
|
9142
9222
|
claudeApiCard,
|
|
9143
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
|
+
}
|
|
9144
9240
|
|
|
9145
9241
|
// Legacy top-level fields stay alongside `providers` so an older cached
|
|
9146
9242
|
// client shell keeps working through a PWA update.
|