@oar-sh/oar 0.9.19 → 0.9.21
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 +529 -0
- package/README.md +14 -14
- package/bin/oar.js +392 -98
- 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 +258 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +22 -1
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +1 -0
- package/server/claude-worker/claude-session-process.mjs +10 -0
- package/server/claude-worker/claude-session-worker-main.mjs +8 -1
- 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 +131 -5
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -1
- package/server/cursor-worker/cursor-session-worker.mjs +12 -1
- package/server/cursor-worker/cursor-turn-runner.mjs +42 -6
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/db-schema.mjs +18 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -0
- package/server/grok-worker/grok-background-tasks.mjs +9 -0
- package/server/grok-worker/grok-session-worker.mjs +12 -1
- package/server/grok-worker/grok-turn-runner.mjs +47 -6
- package/server/grok-worker/grok-worker-steering.mjs +23 -0
- 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-ui.js +1 -1
- package/server/public/app/about-view.mjs +106 -3
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/api-client.js +6 -3
- package/server/public/app/ask-user-view.js +20 -6
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +611 -70
- package/server/public/app/chat-actions-menu-state.mjs +139 -4
- package/server/public/app/chat-header-meta.mjs +27 -0
- package/server/public/app/claude-billing-toggle-ui.js +2 -1
- package/server/public/app/claude-cloud-conversation-ui.js +2 -2
- package/server/public/app/composer-control-state.mjs +12 -2
- package/server/public/app/composer-settings-label.mjs +90 -0
- package/server/public/app/composer-settings-ui.js +442 -0
- package/server/public/app/context-menu.mjs +36 -5
- package/server/public/app/context-usage-view.mjs +16 -0
- package/server/public/app/conversation-deep-link.mjs +15 -11
- package/server/public/app/conversation-group-drag.mjs +12 -2
- package/server/public/app/conversation-group-order.mjs +9 -2
- package/server/public/app/conversation-group-state.mjs +47 -10
- package/server/public/app/conversation-origin-ui.js +17 -1
- package/server/public/app/conversation-view.js +20 -2
- package/server/public/app/cwd-menu-placement.mjs +18 -6
- package/server/public/app/cwd-picker.js +36 -6
- package/server/public/app/emoji-view.js +1 -1
- package/server/public/app/font-scaling.js +26 -0
- package/server/public/app/header-priority-actions.mjs +234 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +50 -2
- package/server/public/app/inbox-view.mjs +141 -5
- package/server/public/app/journal-view.js +311 -58
- package/server/public/app/phone-landscape-ui.js +225 -0
- package/server/public/app/phone-landscape.mjs +99 -0
- package/server/public/app/phone-typing-state.mjs +104 -0
- package/server/public/app/pinned-messages-view.mjs +5 -0
- package/server/public/app/plan-board-shared.mjs +49 -0
- package/server/public/app/plan-usage-view.mjs +1 -1
- package/server/public/app/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- 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-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +42 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/relay-schedule-view.mjs +32 -6
- package/server/public/app/relay-schedules-ui.js +69 -20
- package/server/public/app/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +250 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +61 -6
- package/server/public/app/settings-tabs.js +12 -0
- package/server/public/app/share-state.js +1 -1
- package/server/public/app/shared-conversations-settings.js +2 -2
- package/server/public/app/socket-handlers.js +15 -4
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/subagent-viewer.mjs +27 -2
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -0
- package/server/public/app/usage-limit-ui.js +63 -2
- package/server/public/app/viewer-history.mjs +13 -2
- package/server/public/index.html +2258 -1585
- 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 +5 -3
- package/server/repositories/message-repository.mjs +54 -1
- package/server/repositories/remote-relay-repository.mjs +10 -0
- package/server/repositories/session-repository.mjs +25 -1
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +144 -1
- package/server/routes/relay-ask-routes.mjs +138 -8
- package/server/routes/relay-board-routes.mjs +55 -40
- package/server/routes/sessions-routes.mjs +144 -14
- package/server/server-runtime.mjs +322 -51
- package/server/services/about-service.mjs +81 -53
- package/server/services/cache-rebuild-service.mjs +7 -0
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- 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 +101 -19
- package/server/services/restart-resume-service.mjs +5 -2
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-launch-service.mjs +12 -3
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-stop-service.mjs +27 -6
- package/server/services/session-worker-websocket-service.mjs +40 -16
- 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-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/server/services/worker-scope.mjs +77 -0
- package/shared/build-id.mjs +128 -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
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +23 -0
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -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,
|
|
@@ -43,6 +43,7 @@ import { readStoredClaudeContextUsage, resolveAutoCompactWindowStatus } from '..
|
|
|
43
43
|
import { buildContextUsageView } from '../services/context-usage-view.mjs';
|
|
44
44
|
import { cleanupGeneratedImagesForConversation as cleanupGeneratedImagesForConversationDefault } from '../services/generated-image-cleanup-service.mjs';
|
|
45
45
|
import { stopSessionWorkerProcesses } from '../services/session-worker-stop-service.mjs';
|
|
46
|
+
import { isWorkerOnOtherBuild } from '../services/worker-recycle-service.mjs';
|
|
46
47
|
import { createClaudeAuthService } from '../services/claude-auth-service.mjs';
|
|
47
48
|
import { createCliInstallService } from '../services/cli-install-service.mjs';
|
|
48
49
|
import { createGrokAuthService } from '../services/grok-auth-service.mjs';
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
DEEPSEEK_REASONING_EFFORTS,
|
|
58
59
|
DEFAULT_DEEPSEEK_MODEL,
|
|
59
60
|
DEFAULT_DEEPSEEK_REASONING_EFFORT,
|
|
61
|
+
deepseekModelLabel,
|
|
60
62
|
deepseekReasoningEffortFor,
|
|
61
63
|
} from '../../shared/deepseek-models.mjs';
|
|
62
64
|
import {
|
|
@@ -68,7 +70,7 @@ import { redactPreviewTokens } from '../services/preview-registry-service.mjs';
|
|
|
68
70
|
import { createSharePathRedactor } from '../services/share-path-redaction.mjs';
|
|
69
71
|
import { createSubagentTimelineService, TIMELINE_READ_DEFAULT_LIMIT } from '../services/subagent-timeline-service.mjs';
|
|
70
72
|
import { sanitizeSubagentRunId } from '../../shared/subagent-run-id.mjs';
|
|
71
|
-
import {
|
|
73
|
+
import { sessionLaunchMutex } from '../services/session-launch-mutex-service.mjs';
|
|
72
74
|
import { createFixedWindowRateLimiter } from '../services/rate-limit-service.mjs';
|
|
73
75
|
import { requestHostOf } from '../services/request-boundary.mjs';
|
|
74
76
|
import { parseRemoteRelayOriginJson } from '../repositories/remote-relay-repository.mjs';
|
|
@@ -103,6 +105,7 @@ import {
|
|
|
103
105
|
resolveSessionGroupReference,
|
|
104
106
|
validateRemoteRelaySessionSettings,
|
|
105
107
|
} from '../../shared/remote-relay-contract.mjs';
|
|
108
|
+
import { PLAN_ONLY_REFUSAL, planOnlyRefusesMode } from '../../shared/plan-only.mjs';
|
|
106
109
|
import {
|
|
107
110
|
buildCloudSourceRecord,
|
|
108
111
|
buildConversationCloudPayload,
|
|
@@ -148,6 +151,7 @@ import {
|
|
|
148
151
|
import { parseRestartResumeUpdate } from '../services/restart-resume-service.mjs';
|
|
149
152
|
import { removeClaudeSessionLeftovers } from '../services/claude-session-leftovers.mjs';
|
|
150
153
|
import { deepseekStartingBalanceNote } from '../services/plan-usage-deepseek.mjs';
|
|
154
|
+
import { appendTokenSections } from '../services/plan-usage-tokens.mjs';
|
|
151
155
|
import { DEEPSEEK_USAGE_CHECKPOINTS_TABLE } from '../migrations/0024-deepseek-usage-totals.mjs';
|
|
152
156
|
import { claudeProjectDirSlug } from '../services/claude-session-root-service.mjs';
|
|
153
157
|
|
|
@@ -1449,11 +1453,27 @@ export function buildSessionWorkerStatusPayload({
|
|
|
1449
1453
|
// What the last read of the process list did ({ at, durationMs, processes,
|
|
1450
1454
|
// error }); a slow box shows here before it shows as 502s.
|
|
1451
1455
|
processList = null,
|
|
1456
|
+
// The relay's own build id (runtimeState.buildId): each worker's is shown
|
|
1457
|
+
// next to it, so a worker still on the code before a deploy stands out.
|
|
1458
|
+
relayBuildId = null,
|
|
1459
|
+
// The worker recycle's status() (stale count, recycles in flight and done,
|
|
1460
|
+
// who waits on what), or null while the recycle is not wired.
|
|
1461
|
+
recycleStatus = null,
|
|
1452
1462
|
} = {}) {
|
|
1453
1463
|
const snapshot = supervisorSnapshot && typeof supervisorSnapshot === 'object'
|
|
1454
1464
|
? supervisorSnapshot
|
|
1455
1465
|
: {};
|
|
1456
|
-
const
|
|
1466
|
+
const relayBuild = typeof relayBuildId === 'string' && relayBuildId.trim() ? relayBuildId.trim() : null;
|
|
1467
|
+
const workers = (Array.isArray(snapshot.workers) ? snapshot.workers : []).map((worker) => (
|
|
1468
|
+
worker && typeof worker === 'object'
|
|
1469
|
+
? {
|
|
1470
|
+
...worker,
|
|
1471
|
+
buildId: typeof worker.buildId === 'string' && worker.buildId ? worker.buildId : null,
|
|
1472
|
+
startedAt: worker.startedAt || null,
|
|
1473
|
+
stale: isWorkerOnOtherBuild(worker, relayBuild),
|
|
1474
|
+
}
|
|
1475
|
+
: worker
|
|
1476
|
+
));
|
|
1457
1477
|
const onlineWorkerStatuses = new Set(SESSION_WORKER_ACTIVE_STATUSES);
|
|
1458
1478
|
const onlineCount = workers.reduce((count, worker) => {
|
|
1459
1479
|
const status = normalizeWorkerStatusText(worker?.status, 'new');
|
|
@@ -1558,6 +1578,8 @@ export function buildSessionWorkerStatusPayload({
|
|
|
1558
1578
|
onlineUnassignedProcessCount,
|
|
1559
1579
|
counts: snapshot.counts && typeof snapshot.counts === 'object' ? snapshot.counts : {},
|
|
1560
1580
|
workers,
|
|
1581
|
+
buildId: relayBuild,
|
|
1582
|
+
recycle: recycleStatus && typeof recycleStatus === 'object' ? recycleStatus : null,
|
|
1561
1583
|
pendingStarts: toSafeNonNegativeInt(snapshot.pendingStarts, 0),
|
|
1562
1584
|
lifecycle: Array.isArray(snapshot.lifecycle) ? snapshot.lifecycle : [],
|
|
1563
1585
|
integrity,
|
|
@@ -2243,6 +2265,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2243
2265
|
// The once-per-episode notes for stopped self-started turns on the key
|
|
2244
2266
|
// (plan-usage-claude-api.mjs): a saved setting starts every chat afresh.
|
|
2245
2267
|
claudeApiStopLedger = null,
|
|
2268
|
+
// Tokens per turn and per day (services/token-usage-service.mjs): a
|
|
2269
|
+
// deleted chat drops its turn rows. Absent in older wiring and most tests.
|
|
2270
|
+
tokenUsageService = null,
|
|
2246
2271
|
reconcileUnstartedConversationProviders = async () => ({
|
|
2247
2272
|
updatedUnstartedConversations: 0,
|
|
2248
2273
|
skippedStartedConversations: 0,
|
|
@@ -2580,6 +2605,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2580
2605
|
if (findDeepSeekUsageCheckpointsTable.get(DEEPSEEK_USAGE_CHECKPOINTS_TABLE)) {
|
|
2581
2606
|
db.prepare(`DELETE FROM ${DEEPSEEK_USAGE_CHECKPOINTS_TABLE} WHERE conversation_id = ?`).run(conversationId);
|
|
2582
2607
|
}
|
|
2608
|
+
// Its turns' tokens go with it; the day sums behind Check Usage stay.
|
|
2609
|
+
tokenUsageService?.forgetConversation?.(conversationId);
|
|
2583
2610
|
// The share access log goes with it; the share rows follow the foreign key.
|
|
2584
2611
|
shares().forgetConversation(conversationId);
|
|
2585
2612
|
db.prepare(`DELETE FROM conversations WHERE id = ?`).run(conversationId);
|
|
@@ -2600,9 +2627,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2600
2627
|
bucketTtlMs: 60_000,
|
|
2601
2628
|
maxBuckets: 4_096,
|
|
2602
2629
|
});
|
|
2603
|
-
//
|
|
2604
|
-
//
|
|
2605
|
-
|
|
2630
|
+
// Everything that reaches the session-worker launch path is serialized by
|
|
2631
|
+
// the relay-wide sessionLaunchMutex (services/session-launch-mutex-service.mjs),
|
|
2632
|
+
// keyed by sdkSessionId, which the worker recycle takes too; plus a
|
|
2633
|
+
// coalescer so a duplicated request replays one result.
|
|
2606
2634
|
const relaunchCoalescer = createRelaunchCoalescer({ windowMs: RELAUNCH_COALESCE_WINDOW_MS });
|
|
2607
2635
|
|
|
2608
2636
|
function getDeepSeekProviderSettings() {
|
|
@@ -3398,6 +3426,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3398
3426
|
origin: parseRemoteRelayOriginJson(r.origin_json),
|
|
3399
3427
|
// The owner let this agent-started session's agents start sessions.
|
|
3400
3428
|
mayCreateSessions: readMayCreateSessions(r),
|
|
3429
|
+
planOnly: readPlanOnly(r),
|
|
3401
3430
|
// Set on Claude Cloud conversations: repository, branch, session link.
|
|
3402
3431
|
cloud: buildConversationCloudPayload({
|
|
3403
3432
|
cloudSourceJson: r.cloud_source_json,
|
|
@@ -4158,6 +4187,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4158
4187
|
draftUpdatedByClientId: conv.draft_updated_by_client_id || null,
|
|
4159
4188
|
origin: parseRemoteRelayOriginJson(conv.origin_json),
|
|
4160
4189
|
mayCreateSessions: readMayCreateSessions(conv),
|
|
4190
|
+
planOnly: readPlanOnly(conv),
|
|
4161
4191
|
cloud: conversationCloudPayload(conv, runtimeSession),
|
|
4162
4192
|
group: conversationGroupPayload(conversationId),
|
|
4163
4193
|
groupPosition: conversationGroupPositionOf(conv),
|
|
@@ -4864,6 +4894,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4864
4894
|
// Set on conversations another relay's agent created ("via <relay>").
|
|
4865
4895
|
origin: parseRemoteRelayOriginJson(conv.origin_json),
|
|
4866
4896
|
mayCreateSessions: readMayCreateSessions(conv),
|
|
4897
|
+
planOnly: readPlanOnly(conv),
|
|
4867
4898
|
// Set on Claude Cloud conversations: repository, branch, session link.
|
|
4868
4899
|
cloud: conversationCloudPayload(conv, runtimeSession),
|
|
4869
4900
|
// The session group it is in, with the other members; null when none.
|
|
@@ -5086,6 +5117,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5086
5117
|
? { thinkingDisplay: parseThinkingDisplay(body.thinkingDisplay) }
|
|
5087
5118
|
: {}),
|
|
5088
5119
|
...(mentions('mayCreateSessions') ? { mayCreateSessions: body.mayCreateSessions === true } : {}),
|
|
5120
|
+
...(mentions('planOnly') ? { planOnly: body.planOnly === true } : {}),
|
|
5089
5121
|
updatedAt: now,
|
|
5090
5122
|
createIfMissing: !existing,
|
|
5091
5123
|
createTitle: 'Session',
|
|
@@ -5099,6 +5131,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5099
5131
|
thinkingEnabled: persisted.thinkingEnabled ?? null,
|
|
5100
5132
|
thinkingDisplay: persisted.thinkingDisplay ?? null,
|
|
5101
5133
|
mayCreateSessions: persisted.mayCreateSessions === true,
|
|
5134
|
+
planOnly: persisted.planOnly === true,
|
|
5102
5135
|
updatedAt: persisted.updatedAt,
|
|
5103
5136
|
senderClientId,
|
|
5104
5137
|
};
|
|
@@ -5128,6 +5161,26 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5128
5161
|
return res.status(503).json({ error: 'This setting is unavailable until the relay restarts (its storage is missing).' });
|
|
5129
5162
|
}
|
|
5130
5163
|
}
|
|
5164
|
+
// Plan-only: the owner's toggle here. Agents set and clear it through
|
|
5165
|
+
// update_session only (its dry run and Allow card), never through this
|
|
5166
|
+
// route, and they cannot change a plan-only session's mode here either.
|
|
5167
|
+
const remoteRequest = readRemoteRelayRequest(req, remoteRelayInbound).remote;
|
|
5168
|
+
if (Object.prototype.hasOwnProperty.call(req.body || {}, 'planOnly')) {
|
|
5169
|
+
if (remoteRequest) {
|
|
5170
|
+
return res.status(403).json({ error: 'Agents set plan-only through relay_tool update_session, not through this route.' });
|
|
5171
|
+
}
|
|
5172
|
+
if (typeof req.body.planOnly !== 'boolean') {
|
|
5173
|
+
return res.status(400).json({ error: 'planOnly must be true or false' });
|
|
5174
|
+
}
|
|
5175
|
+
if (!stmts.conversationsHavePlanOnly) {
|
|
5176
|
+
return res.status(503).json({ error: 'This setting is unavailable until the relay restarts (its storage is missing).' });
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
if (remoteRequest && readPlanOnly(existing)
|
|
5180
|
+
&& Object.prototype.hasOwnProperty.call(req.body || {}, 'preferredRelayMode')
|
|
5181
|
+
&& planOnlyRefusesMode(req.body.preferredRelayMode)) {
|
|
5182
|
+
return res.status(409).json(PLAN_ONLY_REFUSAL);
|
|
5183
|
+
}
|
|
5131
5184
|
|
|
5132
5185
|
const persisted = applyConversationPreferences({ conversationId, existing, body: req.body || {}, senderClientId });
|
|
5133
5186
|
return res.json({ ok: true, ...persisted });
|
|
@@ -5168,6 +5221,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5168
5221
|
thinking_enabled: parseThinkingEnabled(conv.thinking_enabled),
|
|
5169
5222
|
thinking_display: parseThinkingDisplay(conv.thinking_display),
|
|
5170
5223
|
cwd: String(workspace?.currentWorkspaceRootPath || workspace?.configuredWorkspaceRootPath || conv.configured_workspace_root_path || '').trim() || null,
|
|
5224
|
+
planOnly: readPlanOnly(conv),
|
|
5171
5225
|
};
|
|
5172
5226
|
}
|
|
5173
5227
|
|
|
@@ -5201,7 +5255,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5201
5255
|
* body }`. The plan holds what changes (`changed`, in the order of the
|
|
5202
5256
|
* contract's settings) and how.
|
|
5203
5257
|
*/
|
|
5204
|
-
function planSessionSettings(conversation, raw) {
|
|
5258
|
+
function planSessionSettings(conversation, raw, { remote = false } = {}) {
|
|
5205
5259
|
const conversationId = String(conversation.id);
|
|
5206
5260
|
const fail = (status, error, extra = {}) => ({ ok: false, status, body: { ok: false, error, ...extra } });
|
|
5207
5261
|
const settingsInput = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
|
|
@@ -5236,6 +5290,30 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5236
5290
|
change('mode');
|
|
5237
5291
|
}
|
|
5238
5292
|
}
|
|
5293
|
+
// Plan-only, read as it is after this call: one that clears the flag may
|
|
5294
|
+
// name another mode too, one that keeps or sets it may not. Checked before
|
|
5295
|
+
// anything else is planned, so a dry run refuses before any Allow card.
|
|
5296
|
+
if (wanted.planOnly !== undefined && !stmts.conversationsHavePlanOnly) {
|
|
5297
|
+
return fail(503, 'planOnly is unavailable until the relay restarts (its storage is missing). Nothing was changed.', { code: SESSION_SETTINGS_INVALID });
|
|
5298
|
+
}
|
|
5299
|
+
// planOnly:false counts only in the call that switches to another mode:
|
|
5300
|
+
// alone (or with plan) it is passed over, so a caller that fills every
|
|
5301
|
+
// property with false never drops the lock by accident.
|
|
5302
|
+
if (wanted.planOnly === false && !(wanted.mode !== undefined && wanted.mode !== 'plan')) {
|
|
5303
|
+
delete wanted.planOnly;
|
|
5304
|
+
if (current.planOnly === true) plan.notes.push('planOnly unchanged: clear it in the same call as the new mode.');
|
|
5305
|
+
}
|
|
5306
|
+
const planOnlyAfter = wanted.planOnly !== undefined ? wanted.planOnly === true : current.planOnly === true;
|
|
5307
|
+
if (remote && planOnlyAfter && wanted.mode !== undefined && planOnlyRefusesMode(wanted.mode)) {
|
|
5308
|
+
return fail(409, `${PLAN_ONLY_REFUSAL.error} Nothing was changed.`, { code: PLAN_ONLY_REFUSAL.code });
|
|
5309
|
+
}
|
|
5310
|
+
if (wanted.planOnly !== undefined && wanted.planOnly !== current.planOnly) {
|
|
5311
|
+
if (wanted.planOnly === true && (wanted.mode !== undefined ? wanted.mode : current.mode) !== 'plan') {
|
|
5312
|
+
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 });
|
|
5313
|
+
}
|
|
5314
|
+
plan.preferences.planOnly = wanted.planOnly;
|
|
5315
|
+
change('planOnly');
|
|
5316
|
+
}
|
|
5239
5317
|
if (wanted.auto_compact_window !== undefined) {
|
|
5240
5318
|
const window = wanted.auto_compact_window === 'auto' ? null : parseAutoCompactWindow(wanted.auto_compact_window);
|
|
5241
5319
|
if (window !== parseAutoCompactWindow(conversation.auto_compact_window)) {
|
|
@@ -5422,7 +5500,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5422
5500
|
}
|
|
5423
5501
|
const conversationId = String(conversation.id);
|
|
5424
5502
|
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
5425
|
-
const planned = planSessionSettings(conversation, body.settings);
|
|
5503
|
+
const planned = planSessionSettings(conversation, body.settings, { remote: remoteRequest.remote === true });
|
|
5426
5504
|
if (!planned.ok) return res.status(planned.status).json(planned.body);
|
|
5427
5505
|
const { plan } = planned;
|
|
5428
5506
|
const createdBy = parseRemoteRelayOriginJson(conversation.origin_json);
|
|
@@ -6041,7 +6119,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6041
6119
|
// Kill session stops the session's worker, and the delete counts no
|
|
6042
6120
|
// tasks for a conversation whose worker is gone.
|
|
6043
6121
|
message: activity.unstoppableTaskCount > 0
|
|
6044
|
-
? 'Background tasks are still running in this conversation, and some have no Stop button. Use Kill session in its
|
|
6122
|
+
? 'Background tasks are still running in this conversation, and some have no Stop button. Use Kill session in its ︙ menu, then delete it.'
|
|
6045
6123
|
: 'Background tasks are still running in this conversation. Stop them in the task panel, then delete it.',
|
|
6046
6124
|
},
|
|
6047
6125
|
};
|
|
@@ -6074,6 +6152,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6074
6152
|
stopOverrides: sessionWorkerStopOverrides,
|
|
6075
6153
|
});
|
|
6076
6154
|
if (!stopped.ok) {
|
|
6155
|
+
console.warn(`[delete] Not deleted ${id}: its worker did not stop (${stopped.error || 'unknown'}; remaining pids ${(stopped.remainingPids || []).join(',') || 'none'}).`);
|
|
6077
6156
|
return {
|
|
6078
6157
|
statusCode: 409,
|
|
6079
6158
|
body: {
|
|
@@ -6199,7 +6278,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6199
6278
|
error: 'conversation-active',
|
|
6200
6279
|
reason: 'background-tasks',
|
|
6201
6280
|
message: activity.unstoppableTaskCount > 0
|
|
6202
|
-
? 'Background tasks are still running in this conversation, and some have no Stop button. Use Kill session in its
|
|
6281
|
+
? 'Background tasks are still running in this conversation, and some have no Stop button. Use Kill session in its ︙ menu, then archive it.'
|
|
6203
6282
|
: 'Background tasks are still running in this conversation. Stop them in the task panel, then archive it.',
|
|
6204
6283
|
},
|
|
6205
6284
|
};
|
|
@@ -6220,6 +6299,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6220
6299
|
stopOverrides: sessionWorkerStopOverrides,
|
|
6221
6300
|
});
|
|
6222
6301
|
if (!stopped.ok) {
|
|
6302
|
+
console.warn(`[archive] Not archived ${id}: its worker did not stop (${stopped.error || 'unknown'}; remaining pids ${(stopped.remainingPids || []).join(',') || 'none'}).`);
|
|
6223
6303
|
return {
|
|
6224
6304
|
statusCode: 409,
|
|
6225
6305
|
body: {
|
|
@@ -6829,10 +6909,27 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6829
6909
|
// new chat would be stored as the default one and reset the composer. An
|
|
6830
6910
|
// omitted or unknown mode still falls back to the default rather than 400,
|
|
6831
6911
|
// because a new chat is worth starting either way.
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6912
|
+
// Plan-only: a session that starts locked in plan mode. With no
|
|
6913
|
+
// mode named it starts in plan; a mode that is not plan is refused.
|
|
6914
|
+
const bootstrapPlanOnly = Object.prototype.hasOwnProperty.call(req.body || {}, 'planOnly') ? req.body.planOnly : undefined;
|
|
6915
|
+
if (bootstrapPlanOnly !== undefined && typeof bootstrapPlanOnly !== 'boolean') {
|
|
6916
|
+
return res.status(400).json({ ok: false, error: 'planOnly must be true or false', code: 'PLAN_ONLY_INVALID' });
|
|
6917
|
+
}
|
|
6918
|
+
const namedBootstrapMode = req.body?.relayMode ?? req.body?.preferredRelayMode;
|
|
6919
|
+
if (bootstrapPlanOnly === true) {
|
|
6920
|
+
if (!stmts.conversationsHavePlanOnly) {
|
|
6921
|
+
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' });
|
|
6922
|
+
}
|
|
6923
|
+
if (String(namedBootstrapMode ?? '').trim() && String(namedBootstrapMode).trim().toLowerCase() !== 'plan') {
|
|
6924
|
+
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' });
|
|
6925
|
+
}
|
|
6926
|
+
}
|
|
6927
|
+
const bootstrapRelayMode = bootstrapPlanOnly === true
|
|
6928
|
+
? 'plan'
|
|
6929
|
+
: normalizeRelayModePreference(namedBootstrapMode, {
|
|
6930
|
+
supportedRelayModes: SUPPORTED_RELAY_MODES,
|
|
6931
|
+
fallbackMode: DEFAULT_RELAY_MODE,
|
|
6932
|
+
});
|
|
6836
6933
|
// The New Chat modal's reasoning choice is validated with the same rules the
|
|
6837
6934
|
// first send would apply, so the composer can restore it from the server
|
|
6838
6935
|
// instead of guessing from localStorage.
|
|
@@ -6910,6 +7007,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6910
7007
|
preferredRelayMode: bootstrapRelayMode,
|
|
6911
7008
|
preferredModel: selectedModel,
|
|
6912
7009
|
preferredReasoningEffort: bootstrapReasoning.effort,
|
|
7010
|
+
...(bootstrapPlanOnly === true ? { planOnly: true } : {}),
|
|
6913
7011
|
updatedAt: now,
|
|
6914
7012
|
});
|
|
6915
7013
|
return { boundRuntimeSession, preferences };
|
|
@@ -7028,6 +7126,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7028
7126
|
preferredRelayMode,
|
|
7029
7127
|
preferredModel,
|
|
7030
7128
|
preferredReasoningEffort,
|
|
7129
|
+
// Echoed so the calling relay can tell this relay knows the flag.
|
|
7130
|
+
planOnly: preferences?.planOnly === true,
|
|
7031
7131
|
configuredWorkspaceRootPath: workspaceRootState?.configuredWorkspaceRootPath || null,
|
|
7032
7132
|
configuredWorkspaceRootName: workspaceRootState?.configuredWorkspaceRootName || null,
|
|
7033
7133
|
currentWorkspaceRootPath: workspaceRootState?.currentWorkspaceRootPath || null,
|
|
@@ -7049,6 +7149,18 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7049
7149
|
}
|
|
7050
7150
|
});
|
|
7051
7151
|
|
|
7152
|
+
// The worker recycle's status, read at request time: the recycle is set on
|
|
7153
|
+
// runtimeState after the routes are registered, and a status that throws
|
|
7154
|
+
// must not take /api/status down with it.
|
|
7155
|
+
function readWorkerRecycleStatus() {
|
|
7156
|
+
if (typeof runtimeState.workerRecycle !== 'function') return null;
|
|
7157
|
+
try {
|
|
7158
|
+
return runtimeState.workerRecycle() || null;
|
|
7159
|
+
} catch {
|
|
7160
|
+
return null;
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
|
|
7052
7164
|
// GET /api/status — overall status
|
|
7053
7165
|
app.get('/api/status', auth, (req, res) => {
|
|
7054
7166
|
ensureSessionId(req, res);
|
|
@@ -7066,6 +7178,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7066
7178
|
supervisorSnapshot: sessionWorkerSupervisor?.snapshot?.({ pendingQuestionSessionIds }) || null,
|
|
7067
7179
|
queueRows: listSessionWorkerQueueRows.all(...SESSION_WORKER_STATUS_QUEUE_STATES),
|
|
7068
7180
|
processList: sessionWorkerProcessInspector?.getLastRead?.() || null,
|
|
7181
|
+
relayBuildId: runtimeState.buildId ?? null,
|
|
7182
|
+
recycleStatus: readWorkerRecycleStatus(),
|
|
7069
7183
|
});
|
|
7070
7184
|
const activeRuntimeSessionCount = featureFlags?.SESSION_WORKER_ROUTING_ENABLED === true
|
|
7071
7185
|
? Number(sessionWorkerStatus?.onlineBoundProcessCount || 0)
|
|
@@ -9141,6 +9255,22 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
9141
9255
|
deepseekActuals: deepseekEnabled ? readDeepSeekActuals() : null,
|
|
9142
9256
|
claudeApiCard,
|
|
9143
9257
|
});
|
|
9258
|
+
// Every tab's Tokens sections, from the token booking.
|
|
9259
|
+
if (tokenUsageService) {
|
|
9260
|
+
appendTokenSections(report.providers, (options) => tokenUsageService.readPeriods(options), {
|
|
9261
|
+
// A tab names its models as its other rows do: the Claude API tab as
|
|
9262
|
+
// its spend rows (the Console's names), DeepSeek with its own labels,
|
|
9263
|
+
// the Claude tab with Claude's; the raw id where there is none.
|
|
9264
|
+
modelLabelFor: (provider) => {
|
|
9265
|
+
if (provider === 'claude-api' && typeof claudeApiPlanUsage?.modelLabel === 'function') {
|
|
9266
|
+
return (model) => claudeApiPlanUsage.modelLabel(model);
|
|
9267
|
+
}
|
|
9268
|
+
if (provider === 'deepseek') return (model) => deepseekModelLabel(model);
|
|
9269
|
+
if (provider === 'claude') return (model) => claudeAttributionModelLabel(model);
|
|
9270
|
+
return null;
|
|
9271
|
+
},
|
|
9272
|
+
});
|
|
9273
|
+
}
|
|
9144
9274
|
|
|
9145
9275
|
// Legacy top-level fields stay alongside `providers` so an older cached
|
|
9146
9276
|
// client shell keeps working through a PWA update.
|