@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
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
+
import { normalizeBuildId, readGitCommit, resolveBuildCommit } from '../../shared/build-id.mjs';
|
|
7
|
+
import { isWorkerOnOtherBuild } from './worker-recycle-service.mjs';
|
|
8
|
+
|
|
9
|
+
// The commit readers moved to shared/ so the workers can state their build id
|
|
10
|
+
// too; they are re-exported here so the routes and tests importing them keep
|
|
11
|
+
// their import.
|
|
12
|
+
export { readGitCommit, resolveBuildCommit };
|
|
13
|
+
|
|
6
14
|
// What the About OAR modal shows: the running build, the providers that are
|
|
7
15
|
// on, what this relay has stored over its life, and how it is doing now.
|
|
8
16
|
//
|
|
@@ -64,58 +72,6 @@ function readFileText(fsImpl, filePath) {
|
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
const COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The commit a git checkout has checked out, read from its .git files rather
|
|
71
|
-
* than from a `git` process: HEAD, then the loose ref, then packed-refs. A
|
|
72
|
-
* linked worktree's .git is a file naming its git dir, whose refs live in the
|
|
73
|
-
* common dir. Anything unexpected reads as null. `pathImpl` is the host's path
|
|
74
|
-
* module; tests pass path.posix and path.win32.
|
|
75
|
-
*/
|
|
76
|
-
export function readGitCommit(root, { fsImpl = fs, pathImpl = path } = {}) {
|
|
77
|
-
if (!root) return null;
|
|
78
|
-
const dotGit = pathImpl.join(root, '.git');
|
|
79
|
-
let gitDir = dotGit;
|
|
80
|
-
const pointer = readFileText(fsImpl, dotGit);
|
|
81
|
-
if (pointer !== null) {
|
|
82
|
-
const match = pointer.match(/^gitdir:\s*(.+)$/m);
|
|
83
|
-
if (!match) return null;
|
|
84
|
-
gitDir = pathImpl.resolve(root, match[1].trim());
|
|
85
|
-
}
|
|
86
|
-
const head = readFileText(fsImpl, pathImpl.join(gitDir, 'HEAD'));
|
|
87
|
-
if (head === null) return null;
|
|
88
|
-
const headText = head.trim();
|
|
89
|
-
if (COMMIT_PATTERN.test(headText)) return headText.toLowerCase();
|
|
90
|
-
const refMatch = headText.match(/^ref:\s*(\S+)$/);
|
|
91
|
-
if (!refMatch) return null;
|
|
92
|
-
const ref = refMatch[1];
|
|
93
|
-
const commonDirText = readFileText(fsImpl, pathImpl.join(gitDir, 'commondir'));
|
|
94
|
-
const commonDir = commonDirText ? pathImpl.resolve(gitDir, commonDirText.trim()) : gitDir;
|
|
95
|
-
for (const dir of new Set([gitDir, commonDir])) {
|
|
96
|
-
const loose = readFileText(fsImpl, pathImpl.join(dir, ...ref.split('/')));
|
|
97
|
-
if (loose && COMMIT_PATTERN.test(loose.trim())) return loose.trim().toLowerCase();
|
|
98
|
-
}
|
|
99
|
-
const packed = readFileText(fsImpl, pathImpl.join(commonDir, 'packed-refs')) || '';
|
|
100
|
-
for (const line of packed.split(/\r?\n/)) {
|
|
101
|
-
const [hash, name] = line.trim().split(/\s+/);
|
|
102
|
-
if (name === ref && COMMIT_PATTERN.test(hash || '')) return hash.toLowerCase();
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The commit this build came from: the checkout's HEAD, else a `gitHead` stamp in package.json. */
|
|
108
|
-
export function resolveBuildCommit(packageRoot, { fsImpl = fs, pathImpl = path } = {}) {
|
|
109
|
-
const fromGit = readGitCommit(packageRoot, { fsImpl, pathImpl });
|
|
110
|
-
if (fromGit) return fromGit;
|
|
111
|
-
try {
|
|
112
|
-
const stamp = String(JSON.parse(readFileText(fsImpl, pathImpl.join(packageRoot, 'package.json')) || '{}')?.gitHead || '').trim();
|
|
113
|
-
return COMMIT_PATTERN.test(stamp) ? stamp.toLowerCase() : null;
|
|
114
|
-
} catch {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
75
|
export function readInstalledPackageVersion(packageRoot, packageName, { fsImpl = fs } = {}) {
|
|
120
76
|
if (!packageRoot || !packageName) return null;
|
|
121
77
|
try {
|
|
@@ -273,6 +229,42 @@ export function collectUsageTotals(db) {
|
|
|
273
229
|
usage['claude-cloud'] = { costUsd: toAmount(row.cost), sessions: toCount(row.sessions) };
|
|
274
230
|
}
|
|
275
231
|
}
|
|
232
|
+
// Every provider's tokens per turn, all time (token_usage_days, migration
|
|
233
|
+
// 0036): fresh input, output, cache writes and cache reads apart, each
|
|
234
|
+
// null while no turn reported it, the first day, and whether days copied
|
|
235
|
+
// at the upgrade are in it. An own key's turns (Copilot) are kept apart
|
|
236
|
+
// from the plan's.
|
|
237
|
+
if (tableExists(db, 'token_usage_days')) {
|
|
238
|
+
const rows = db.prepare(`
|
|
239
|
+
SELECT provider, billing, SUM(input_tokens) AS input, SUM(output_tokens) AS output,
|
|
240
|
+
SUM(cache_write_tokens) AS cache_write, SUM(cache_read_tokens) AS cache_read, MIN(day) AS since,
|
|
241
|
+
MIN(CASE WHEN source = 'turns' THEN day END) AS first_turns_day,
|
|
242
|
+
MAX(CASE WHEN source = 'backfill' THEN 1 ELSE 0 END) AS backfilled
|
|
243
|
+
FROM token_usage_days
|
|
244
|
+
GROUP BY provider, billing
|
|
245
|
+
ORDER BY provider, billing
|
|
246
|
+
`).all();
|
|
247
|
+
const nullable = (value) => (value === null || value === undefined ? null : toAmount(value));
|
|
248
|
+
for (const row of rows) {
|
|
249
|
+
const tokens = {
|
|
250
|
+
inputTokens: nullable(row.input),
|
|
251
|
+
outputTokens: nullable(row.output),
|
|
252
|
+
cacheWriteTokens: nullable(row.cache_write),
|
|
253
|
+
cacheReadTokens: nullable(row.cache_read),
|
|
254
|
+
since: row.since || null,
|
|
255
|
+
// Days copied at the upgrade (0036) hold cache writes inside input;
|
|
256
|
+
// the first counted turn's day is where that stops.
|
|
257
|
+
backfilled: Number(row.backfilled) === 1,
|
|
258
|
+
firstTurnsDay: row.first_turns_day || null,
|
|
259
|
+
};
|
|
260
|
+
const entry = usage[row.provider] || (usage[row.provider] = {});
|
|
261
|
+
if (row.billing) {
|
|
262
|
+
(entry.ownKeyTokens || (entry.ownKeyTokens = [])).push({ billing: row.billing, ...tokens });
|
|
263
|
+
} else {
|
|
264
|
+
entry.tokens = tokens;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
276
268
|
return usage;
|
|
277
269
|
}
|
|
278
270
|
|
|
@@ -302,6 +294,7 @@ async function fileSize(fsImpl, filePath) {
|
|
|
302
294
|
* @param {string|null} [deps.dbPath] the database file; its -wal file is counted too
|
|
303
295
|
* @param {string} deps.version
|
|
304
296
|
* @param {string|null} [deps.commit]
|
|
297
|
+
* @param {string|null} [deps.buildId] the relay's build id (shared/build-id.mjs), to tell the workers on other code
|
|
305
298
|
* @param {string} [deps.installMethod]
|
|
306
299
|
* @param {string} deps.startedAt ISO time this relay process started
|
|
307
300
|
* @param {() => object} [deps.getProviderSettings] `{ github: { enabled, engine }, claude: { enabled }, … }`
|
|
@@ -310,12 +303,15 @@ async function fileSize(fsImpl, filePath) {
|
|
|
310
303
|
* @param {() => object[]} [deps.listWorkers] the session worker registry's entries
|
|
311
304
|
* @param {() => object|null} [deps.getUpdateState] buildUpdateStatePayload()
|
|
312
305
|
* @param {object} [deps.sdkVersions] package version by provider id; read from node_modules when absent
|
|
306
|
+
* @param {() => object|null} [deps.describeBackups] the database backups (db-backup-service.mjs describeBackups)
|
|
313
307
|
*/
|
|
314
308
|
export function createAboutService({
|
|
315
309
|
db,
|
|
316
310
|
dbPath = null,
|
|
311
|
+
describeBackups = null,
|
|
317
312
|
version = '0.0.0',
|
|
318
313
|
commit = null,
|
|
314
|
+
buildId = null,
|
|
319
315
|
installMethod = null,
|
|
320
316
|
startedAt,
|
|
321
317
|
packageRoot = null,
|
|
@@ -359,20 +355,30 @@ export function createAboutService({
|
|
|
359
355
|
return totalsInFlight;
|
|
360
356
|
}
|
|
361
357
|
|
|
358
|
+
// The running workers, by provider and by build: after a deploy the
|
|
359
|
+
// workers keep the code they started with until the recycle replaces them,
|
|
360
|
+
// and About shows how many still do. A relay that does not know its own
|
|
361
|
+
// build id cannot tell, so both build counts are null then.
|
|
362
362
|
function runningWorkersByProvider() {
|
|
363
363
|
const counts = new Map();
|
|
364
364
|
let total = 0;
|
|
365
|
+
const relayBuildId = normalizeBuildId(buildId);
|
|
366
|
+
let onOtherBuild = 0;
|
|
365
367
|
for (const worker of listWorkers() || []) {
|
|
366
368
|
const status = String(worker?.status || '').trim().toLowerCase();
|
|
367
369
|
if (!ACTIVE_WORKER_STATUSES.has(status) || !isPidAlive(worker?.pid)) continue;
|
|
368
370
|
total += 1;
|
|
371
|
+
if (isWorkerOnOtherBuild(worker, relayBuildId)) onOtherBuild += 1;
|
|
369
372
|
const conversationId = String(worker?.conversationId || '').trim();
|
|
370
373
|
const provider = conversationId
|
|
371
374
|
? normalizeProviderId(providerForConversation.get(conversationId)?.provider_type)
|
|
372
375
|
: 'unassigned';
|
|
373
376
|
counts.set(provider, (counts.get(provider) || 0) + 1);
|
|
374
377
|
}
|
|
375
|
-
|
|
378
|
+
const builds = relayBuildId
|
|
379
|
+
? { total, onThisBuild: total - onOtherBuild, onOtherBuild }
|
|
380
|
+
: { total, onThisBuild: null, onOtherBuild: null };
|
|
381
|
+
return { total, counts, builds };
|
|
376
382
|
}
|
|
377
383
|
|
|
378
384
|
function describeProviders(workerCounts) {
|
|
@@ -436,6 +442,7 @@ export function createAboutService({
|
|
|
436
442
|
startedAt: startedAt || null,
|
|
437
443
|
uptimeSeconds,
|
|
438
444
|
providers: describeProviders(workers.counts),
|
|
445
|
+
workers: workers.builds,
|
|
439
446
|
stats: {
|
|
440
447
|
conversations: totals.conversations.total,
|
|
441
448
|
archivedConversations: totals.conversations.archived,
|
|
@@ -451,14 +458,35 @@ export function createAboutService({
|
|
|
451
458
|
usage: totals.usage,
|
|
452
459
|
relay: {
|
|
453
460
|
uptimeSeconds,
|
|
461
|
+
// The full path: which of the two database names this relay runs on.
|
|
462
|
+
dbPath: dbPath || null,
|
|
454
463
|
dbBytes: dbFileBytes + walBytes,
|
|
455
464
|
dbFileBytes,
|
|
456
465
|
walBytes,
|
|
457
466
|
runningWorkers: workers.total,
|
|
458
467
|
},
|
|
459
468
|
update: describeUpdate(),
|
|
469
|
+
backups: readBackups(),
|
|
460
470
|
};
|
|
461
471
|
}
|
|
462
472
|
|
|
473
|
+
// The last backup and how many are kept, for one row; never the listing.
|
|
474
|
+
function readBackups() {
|
|
475
|
+
if (!describeBackups) return null;
|
|
476
|
+
try {
|
|
477
|
+
const backups = describeBackups();
|
|
478
|
+
return {
|
|
479
|
+
enabled: backups.enabled,
|
|
480
|
+
keep: backups.keep,
|
|
481
|
+
dir: backups.dir,
|
|
482
|
+
problem: backups.problem,
|
|
483
|
+
kept: backups.own.length,
|
|
484
|
+
lastResult: backups.lastResult,
|
|
485
|
+
};
|
|
486
|
+
} catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
463
491
|
return { describe };
|
|
464
492
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import { forgetTurnTokenRows } from './token-usage-service.mjs';
|
|
4
|
+
|
|
3
5
|
function normalizeId(value) {
|
|
4
6
|
return String(value || '').trim();
|
|
5
7
|
}
|
|
@@ -99,6 +101,8 @@ export function createCacheRebuildService({
|
|
|
99
101
|
db.prepare(`DELETE FROM queue WHERE conversation_id = ?`).run(sid);
|
|
100
102
|
db.prepare(`DELETE FROM messages WHERE conversation_id = ?`).run(sid);
|
|
101
103
|
db.prepare(`DELETE FROM runtime_sessions WHERE conversation_id = ?`).run(sid);
|
|
104
|
+
// Its turns' tokens go too; the day sums stay.
|
|
105
|
+
forgetTurnTokenRows(db, [sid]);
|
|
102
106
|
db.prepare(`DELETE FROM conversations WHERE id = ?`).run(sid);
|
|
103
107
|
})();
|
|
104
108
|
closePreviews(sid);
|
|
@@ -164,6 +168,9 @@ export function createCacheRebuildService({
|
|
|
164
168
|
db.prepare(`DELETE FROM queue`).run();
|
|
165
169
|
db.prepare(`DELETE FROM messages`).run();
|
|
166
170
|
db.prepare(`DELETE FROM runtime_sessions`).run();
|
|
171
|
+
// The turn tokens of the chats removed for good; a hand-titled chat
|
|
172
|
+
// stays, and keeps them.
|
|
173
|
+
forgetTurnTokenRows(db, db.prepare(`SELECT id FROM conversations WHERE status = 'deleted' OR title_source != 'manual'`).all().map((row) => row.id));
|
|
167
174
|
db.prepare(`DELETE FROM conversations WHERE status = 'deleted' OR title_source != 'manual'`).run();
|
|
168
175
|
for (const conversationId of manualTitleConversations) {
|
|
169
176
|
db.prepare(`
|
|
@@ -28,29 +28,16 @@
|
|
|
28
28
|
* the fence lets them through, and the waiting state ages out.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { workerReleaseHold } from './worker-replacement.mjs';
|
|
32
|
+
|
|
31
33
|
export const CLAUDE_BILLING_SWAP_WAIT_TTL_MS = 15_000;
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
|
-
* What keeps a worker from being replaced now
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* from the rest of it.
|
|
36
|
+
* What keeps a worker from being replaced now (worker-replacement.mjs, which
|
|
37
|
+
* the worker recycle shares). Kept under its old name for the importers that
|
|
38
|
+
* know it by that.
|
|
38
39
|
*/
|
|
39
|
-
export
|
|
40
|
-
if (!worker) return null;
|
|
41
|
-
const steering = worker.steering || null;
|
|
42
|
-
if (steering && typeof steering.releasable === 'boolean') {
|
|
43
|
-
return steering.releasable ? null : (steering.releaseHold || 'turn');
|
|
44
|
-
}
|
|
45
|
-
if (steering) {
|
|
46
|
-
if (steering.holdReason === 'question') return 'question';
|
|
47
|
-
if (steering.holdReason === 'compaction') return 'compaction';
|
|
48
|
-
if (steering.turnActive) return 'turn';
|
|
49
|
-
if (steering.autoCompactWindow?.backgroundWork) return 'background';
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return String(worker.status || '') === 'processing' ? 'turn' : null;
|
|
53
|
-
}
|
|
40
|
+
export const claudeWorkerReleaseHold = workerReleaseHold;
|
|
54
41
|
|
|
55
42
|
export function createClaudeBillingSwapService({
|
|
56
43
|
// sid → the registry's entry for the session's worker, or null.
|
|
@@ -208,7 +195,7 @@ export function createClaudeBillingSwapService({
|
|
|
208
195
|
running = true;
|
|
209
196
|
noteProbeFailure(sid, state, fence, error);
|
|
210
197
|
}
|
|
211
|
-
const hold = running ? 'turn' :
|
|
198
|
+
const hold = running ? 'turn' : workerReleaseHold(worker);
|
|
212
199
|
if (hold) {
|
|
213
200
|
setPending(sid, state, {
|
|
214
201
|
to: fence.rowBilling,
|
|
@@ -157,12 +157,9 @@ export function buildClaudeCloudDelivery({ conversation = null, runtimeSession =
|
|
|
157
157
|
if (!isClaudeCloudProviderType(runtimeSession?.provider_type)) return null;
|
|
158
158
|
const source = parseCloudSourceJson(conversation?.cloud_source_json);
|
|
159
159
|
if (!source) return null;
|
|
160
|
-
const sessionId = toText(runtimeSession?.claude_cloud_session_id);
|
|
161
|
-
const lastSequence = toText(runtimeSession?.claude_cloud_last_sequence);
|
|
162
160
|
const fallback = toText(fallbackEnvironmentId);
|
|
163
161
|
return {
|
|
164
|
-
|
|
165
|
-
lastSequence: SEQUENCE_PATTERN.test(lastSequence) ? lastSequence : null,
|
|
162
|
+
...readStoredClaudeCloudBinding(runtimeSession),
|
|
166
163
|
repoUrl: source.repoUrl,
|
|
167
164
|
branch: source.branch,
|
|
168
165
|
environmentId: source.environmentId || (isSafeClaudeCloudId(fallback) ? fallback : null),
|
|
@@ -170,6 +167,20 @@ export function buildClaudeCloudDelivery({ conversation = null, runtimeSession =
|
|
|
170
167
|
};
|
|
171
168
|
}
|
|
172
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The cloud session a runtime session row is bound to and the cursor its
|
|
172
|
+
* worker reads on from: `{ sessionId, lastSequence }`, each null when absent
|
|
173
|
+
* or not in the shape the worker may put into a request path.
|
|
174
|
+
*/
|
|
175
|
+
export function readStoredClaudeCloudBinding(runtimeSession = null) {
|
|
176
|
+
const sessionId = toText(runtimeSession?.claude_cloud_session_id);
|
|
177
|
+
const lastSequence = toText(runtimeSession?.claude_cloud_last_sequence);
|
|
178
|
+
return {
|
|
179
|
+
sessionId: isSafeClaudeCloudId(sessionId) ? sessionId : null,
|
|
180
|
+
lastSequence: SEQUENCE_PATTERN.test(lastSequence) ? lastSequence : null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
173
184
|
/** The larger of two sequence numbers (strings of digits), so a late report never moves the cursor back. */
|
|
174
185
|
function laterSequence(stored, reported) {
|
|
175
186
|
const a = SEQUENCE_PATTERN.test(toText(stored)) ? toText(stored) : null;
|
|
@@ -264,6 +275,31 @@ export function createClaudeCloudSessionService({
|
|
|
264
275
|
return { ok: true, cloud };
|
|
265
276
|
}
|
|
266
277
|
|
|
278
|
+
/**
|
|
279
|
+
* GET /api/claude-cloud-session: the binding a starting worker attaches to
|
|
280
|
+
* before any delivery (so it follows the turns the session starts by
|
|
281
|
+
* itself, as the worker it replaces did). `id` is the conversation id or
|
|
282
|
+
* the runtime session's sdk session id, which is all a worker knows of its
|
|
283
|
+
* conversation. A known conversation that has no cloud session (none yet,
|
|
284
|
+
* or another provider) answers `sessionId: null`; only an unknown id is
|
|
285
|
+
* 404. Returns `{ ok, statusCode, error }` or `{ ok: true, sessionId,
|
|
286
|
+
* lastSequence }`.
|
|
287
|
+
*/
|
|
288
|
+
function describeStoredBinding(id = '') {
|
|
289
|
+
const key = toText(id);
|
|
290
|
+
if (!key) return { ok: false, statusCode: 400, error: 'Missing conversationId' };
|
|
291
|
+
const runtimeSession = stmts.getRuntimeSessionByConversation?.get?.(key)
|
|
292
|
+
|| stmts.getRuntimeSessionBySdkSessionId?.get?.(key)
|
|
293
|
+
|| null;
|
|
294
|
+
if (!runtimeSession && !stmts.getConvAnyStatus?.get?.(key)) {
|
|
295
|
+
return { ok: false, statusCode: 404, error: 'Conversation not found' };
|
|
296
|
+
}
|
|
297
|
+
if (!runtimeSession || !isClaudeCloudProviderType(runtimeSession.provider_type)) {
|
|
298
|
+
return { ok: true, sessionId: null, lastSequence: null };
|
|
299
|
+
}
|
|
300
|
+
return { ok: true, ...readStoredClaudeCloudBinding(runtimeSession) };
|
|
301
|
+
}
|
|
302
|
+
|
|
267
303
|
/**
|
|
268
304
|
* Archives the cloud session of a conversation that is being deleted or
|
|
269
305
|
* archived here. Best effort: it returns at once (true when an archive was
|
|
@@ -322,5 +358,11 @@ export function createClaudeCloudSessionService({
|
|
|
322
358
|
return true;
|
|
323
359
|
}
|
|
324
360
|
|
|
325
|
-
return {
|
|
361
|
+
return {
|
|
362
|
+
cloudPayloadFor,
|
|
363
|
+
recordWorkerReport,
|
|
364
|
+
describeStoredBinding,
|
|
365
|
+
archiveSessionInBackground,
|
|
366
|
+
unarchiveSessionInBackground,
|
|
367
|
+
};
|
|
326
368
|
}
|
|
@@ -44,6 +44,9 @@ export function persistConversationPreferences({
|
|
|
44
44
|
// The owner's "agents in this conversation may start sessions" (a boolean;
|
|
45
45
|
// `undefined` leaves it alone). The route decides who may write it.
|
|
46
46
|
mayCreateSessions = undefined,
|
|
47
|
+
// Plan-only: agents cannot take the session out of plan mode (a boolean;
|
|
48
|
+
// `undefined` leaves it alone). The routes decide who may write it.
|
|
49
|
+
planOnly = undefined,
|
|
47
50
|
updatedAt = new Date().toISOString(),
|
|
48
51
|
createIfMissing = false,
|
|
49
52
|
createTitle = 'Session',
|
|
@@ -60,6 +63,7 @@ export function persistConversationPreferences({
|
|
|
60
63
|
const writesThinkingDisplay = thinkingDisplay !== undefined;
|
|
61
64
|
const display = writesThinkingDisplay ? parseThinkingDisplay(thinkingDisplay) : null;
|
|
62
65
|
const writesMayCreateSessions = mayCreateSessions !== undefined;
|
|
66
|
+
const writesPlanOnly = planOnly !== undefined;
|
|
63
67
|
if (!db || !stmts || !convId || !mode) {
|
|
64
68
|
return {
|
|
65
69
|
ok: false,
|
|
@@ -71,6 +75,7 @@ export function persistConversationPreferences({
|
|
|
71
75
|
thinkingEnabled: DEFAULT_THINKING_ENABLED,
|
|
72
76
|
thinkingDisplay: DEFAULT_THINKING_DISPLAY,
|
|
73
77
|
mayCreateSessions: false,
|
|
78
|
+
planOnly: false,
|
|
74
79
|
updatedAt,
|
|
75
80
|
};
|
|
76
81
|
}
|
|
@@ -108,6 +113,10 @@ export function persistConversationPreferences({
|
|
|
108
113
|
extraSets.push('may_create_sessions = ?');
|
|
109
114
|
extraValues.push(mayCreateSessions === true ? 1 : 0);
|
|
110
115
|
}
|
|
116
|
+
if (writesPlanOnly) {
|
|
117
|
+
extraSets.push('plan_only = ?');
|
|
118
|
+
extraValues.push(planOnly === true ? 1 : 0);
|
|
119
|
+
}
|
|
111
120
|
try {
|
|
112
121
|
if (extraSets.length) {
|
|
113
122
|
db.prepare(`
|
|
@@ -148,6 +157,9 @@ export function persistConversationPreferences({
|
|
|
148
157
|
mayCreateSessions: writesMayCreateSessions
|
|
149
158
|
? mayCreateSessions === true
|
|
150
159
|
: readMayCreateSessions(existing),
|
|
160
|
+
planOnly: writesPlanOnly
|
|
161
|
+
? planOnly === true
|
|
162
|
+
: readPlanOnly(existing),
|
|
151
163
|
updatedAt,
|
|
152
164
|
};
|
|
153
165
|
});
|
|
@@ -159,6 +171,25 @@ export function readMayCreateSessions(row) {
|
|
|
159
171
|
return Number(row?.may_create_sessions) === 1;
|
|
160
172
|
}
|
|
161
173
|
|
|
174
|
+
/** Whether a conversations row is plan-only (migration 0035; NULL is not). */
|
|
175
|
+
export function readPlanOnly(row) {
|
|
176
|
+
return Number(row?.plan_only) === 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A compaction's new conversation keeps a plan-only source's lock and the
|
|
181
|
+
* mode it is saved in, so the work goes on as locked as it was. Nothing for
|
|
182
|
+
* any other source. Returns whether it carried the lock.
|
|
183
|
+
*/
|
|
184
|
+
export function carryPlanOnlyToCompacted({ db, stmts, source, targetConversationId, updatedAt = new Date().toISOString() } = {}) {
|
|
185
|
+
if (!db || !stmts?.conversationsHavePlanOnly || !readPlanOnly(source)) return false;
|
|
186
|
+
const target = String(targetConversationId || '').trim();
|
|
187
|
+
if (!target) return false;
|
|
188
|
+
const mode = String(source?.preferred_relay_mode || '').trim() || 'plan';
|
|
189
|
+
db.prepare(`UPDATE conversations SET plan_only = 1, preferred_relay_mode = ?, updated_at = ? WHERE id = ?`).run(mode, updatedAt, target);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
162
193
|
export function persistConversationModelPreference({
|
|
163
194
|
db,
|
|
164
195
|
stmts,
|