@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
|
@@ -28,6 +28,8 @@ import { createDetachedWorkerWatch } from './services/detached-worker-watch-serv
|
|
|
28
28
|
import { applyCdnNoStoreHeaders } from './services/file-serving.mjs';
|
|
29
29
|
import { pickLiveTurnRowId } from './services/live-turn-picker.mjs';
|
|
30
30
|
import { buildSteerSettleFailure, steerAgentLabelForProvider } from '../shared/steer-settle-failure.mjs';
|
|
31
|
+
import { PLAN_BOARD_APPROVE_ONLY, planBoardShownActions } from '../shared/plan-board-actions.mjs';
|
|
32
|
+
import { carryPlanOnlyToCompacted, readPlanOnly } from './services/conversation-preferences-service.mjs';
|
|
31
33
|
import { sanitizeRelayQuestionContext as sanitizeRelayQuestionContextWith } from './services/relay-question-context.mjs';
|
|
32
34
|
import { isRealPathWithinRoot } from './services/workspace-symlink-guard.mjs';
|
|
33
35
|
import { applySchema } from './db-schema.mjs';
|
|
@@ -74,6 +76,7 @@ import { createClaudeApiSettingsService } from './services/claude-api-settings-s
|
|
|
74
76
|
import { createClaudeBillingSwitchService, normalizeClaudeBilling } from './services/claude-billing-switch-service.mjs';
|
|
75
77
|
import { createClaudeBillingSwapService } from './services/claude-billing-swap-service.mjs';
|
|
76
78
|
import { createClaudeApiUsageService } from './services/claude-api-usage-service.mjs';
|
|
79
|
+
import { createTokenUsageService } from './services/token-usage-service.mjs';
|
|
77
80
|
import { createClaudeApiCapWatch, createClaudeApiPlanUsage, createClaudeApiStopNoteLedger } from './services/plan-usage-claude-api.mjs';
|
|
78
81
|
import { DEEPSEEK_DEFAULT_AUTO_COMPACT_WINDOW, DEFAULT_DEEPSEEK_MODEL, deepseekModelLabel } from '../shared/deepseek-models.mjs';
|
|
79
82
|
import { createClaudeCloudSessionService } from './services/claude-cloud-session-service.mjs';
|
|
@@ -101,6 +104,7 @@ import { createRelayEventService } from './services/relay-event-service.mjs';
|
|
|
101
104
|
import { createRelayScheduleService } from './services/relay-schedule-service.mjs';
|
|
102
105
|
import { createRelayAskService } from './services/relay-ask-service.mjs';
|
|
103
106
|
import { createRelayAskLinkService } from './services/relay-ask-link-service.mjs';
|
|
107
|
+
import { createRemoteInboxWatchService } from './services/remote-inbox-watch-service.mjs';
|
|
104
108
|
import { createRelayEventLinkService } from './services/relay-event-link-service.mjs';
|
|
105
109
|
import { buildClaudeAttributionSettings, normalizeClaudeAttributionMode, resolveClaudeAttributionMode, DEFAULT_CLAUDE_ATTRIBUTION_MODE } from '../shared/claude-attribution.mjs';
|
|
106
110
|
import {
|
|
@@ -142,7 +146,10 @@ import { createCursorSessionRootResolver } from './services/cursor-session-root-
|
|
|
142
146
|
import { createGitChangesService } from './services/git-changes-service.mjs';
|
|
143
147
|
import { createRelaySingletonGuard } from './services/relay-singleton-guard.mjs';
|
|
144
148
|
import { startRelayMemoryLog } from './relay-heap-guard.mjs';
|
|
145
|
-
import { adoptEarlierDatabase, lostPackageDatabaseWarning, packageDatabaseEvidence } from './services/earlier-database-service.mjs';
|
|
149
|
+
import { TAKEOVER_PROGRESS_FILE, adoptEarlierDatabase, lostPackageDatabaseWarning, packageDatabaseEvidence } from './services/earlier-database-service.mjs';
|
|
150
|
+
import { checkDatabaseFile, resolveDatabaseFile, settleDatabaseFile } from './services/database-file.mjs';
|
|
151
|
+
import { describeBackups, describeNextBackup, maybeBackupBeforeStart, writeDbLastRun } from './services/db-backup-service.mjs';
|
|
152
|
+
import { resolveStateLayout } from './services/oar-state-layout.mjs';
|
|
146
153
|
import { ensureDataDirs, resolveDataDir, resolveUploadDir, resolveUploadReadDirs, uploadPathInDirs } from './services/data-dir-resolution.mjs';
|
|
147
154
|
import { mergeStrayMigratedUploads } from './services/oar-state-migration-service.mjs';
|
|
148
155
|
import { createRelayRestartOrchestrator } from './services/relay-restart-orchestrator-service.mjs';
|
|
@@ -219,7 +226,8 @@ import {
|
|
|
219
226
|
resolveRelayRestartMaxWaitOverrideMs,
|
|
220
227
|
RELAY_RESTART_MAX_WAIT_SETTING_KEY,
|
|
221
228
|
} from './services/relay-restart-queue.mjs';
|
|
222
|
-
import { createRestartPictureCollector } from './services/relay-restart-picture.mjs';
|
|
229
|
+
import { createRestartPictureCollector, detectRelayHosting } from './services/relay-restart-picture.mjs';
|
|
230
|
+
import { createWorkerScope } from './services/worker-scope.mjs';
|
|
223
231
|
import { createStaleRecoveryClockGuard } from './services/stale-recovery-clock-guard.mjs';
|
|
224
232
|
import { createStaleTurnRecovery } from './services/stale-turn-recovery.mjs';
|
|
225
233
|
import {
|
|
@@ -320,7 +328,9 @@ const CONFIG_PATH = process.env.COPILOT_WEB_RELAY_CONFIG
|
|
|
320
328
|
// for why the config can carry it (a dev checkout pointing at a release
|
|
321
329
|
// install's data).
|
|
322
330
|
const DATA_DIR = resolveDataDir({ env: process.env, configPath: CONFIG_PATH, serverDir: __dirname });
|
|
323
|
-
|
|
331
|
+
// DB_PATH (oar.db, or the name of installs from before 0.9.20) is
|
|
332
|
+
// resolved under the lock, once an earlier database may have been brought
|
|
333
|
+
// over: see resolveDatabaseFile below.
|
|
324
334
|
const RELAY_LOCK_PATH = path.join(DATA_DIR, 'relay-server.lock');
|
|
325
335
|
// Uploads follow the data dir whenever the env var or config.json chose it.
|
|
326
336
|
const UPLOAD_DIR = resolveUploadDir({ dataDir: DATA_DIR, env: process.env, serverDir: __dirname });
|
|
@@ -1441,7 +1451,16 @@ async function refreshGrokProviderModels() {
|
|
|
1441
1451
|
}
|
|
1442
1452
|
}
|
|
1443
1453
|
|
|
1444
|
-
const DEFAULT_CONFIG = {
|
|
1454
|
+
const DEFAULT_CONFIG = {
|
|
1455
|
+
authToken: '',
|
|
1456
|
+
port: 3333,
|
|
1457
|
+
pollIntervalMs: 3000,
|
|
1458
|
+
conversationSessionMode: 'isolated',
|
|
1459
|
+
localhostOnly: true,
|
|
1460
|
+
// A copy of the database at the first start of each new version, the last
|
|
1461
|
+
// `keep` kept (db-backup-service.mjs); `dir` (absolute) moves the folder.
|
|
1462
|
+
dbBackups: { enabled: true, keep: 2 },
|
|
1463
|
+
};
|
|
1445
1464
|
// How long a turn may go without any sign of life from its worker before the
|
|
1446
1465
|
// relay assumes the worker died. This is an inactivity window, not a cap on turn
|
|
1447
1466
|
// length — see recoverStaleMessages.
|
|
@@ -1868,8 +1887,28 @@ try {
|
|
|
1868
1887
|
console.error(`[server] ${error?.message || String(error)}`);
|
|
1869
1888
|
process.exit(error?.code === 'ERELAYLOCKED' ? RELAY_STOP_EXIT_CODE : 1);
|
|
1870
1889
|
}
|
|
1871
|
-
// Before the database opens, under the lock
|
|
1872
|
-
//
|
|
1890
|
+
// Before the database opens, under the lock: both names there are settled
|
|
1891
|
+
// first (the earlier name's database, not newer, is set aside, else the
|
|
1892
|
+
// relay stops), then adoptEarlierDatabase, whose result also feeds
|
|
1893
|
+
// lostPackageDatabaseWarning.
|
|
1894
|
+
{
|
|
1895
|
+
let settled;
|
|
1896
|
+
try {
|
|
1897
|
+
settled = settleDatabaseFile({
|
|
1898
|
+
dataDir: DATA_DIR,
|
|
1899
|
+
Database,
|
|
1900
|
+
progressFile: path.join(DATA_DIR, TAKEOVER_PROGRESS_FILE),
|
|
1901
|
+
logger: console,
|
|
1902
|
+
});
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
settled = { status: 'refused', error: `[data] Not starting: the database files in ${DATA_DIR} could not be looked at (${error?.message || error}).` };
|
|
1905
|
+
}
|
|
1906
|
+
if (settled.status === 'refused') {
|
|
1907
|
+
console.error(settled.error);
|
|
1908
|
+
try { relaySingletonGuard.release(); } catch {}
|
|
1909
|
+
process.exit(RELAY_STOP_EXIT_CODE);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1873
1912
|
let earlierDatabase;
|
|
1874
1913
|
{
|
|
1875
1914
|
try {
|
|
@@ -1892,6 +1931,21 @@ let earlierDatabase;
|
|
|
1892
1931
|
process.exit(RELAY_STOP_EXIT_CODE);
|
|
1893
1932
|
}
|
|
1894
1933
|
}
|
|
1934
|
+
// Resolved after the adoption, which may have placed the database: `created`
|
|
1935
|
+
// only when neither name is there now.
|
|
1936
|
+
const databaseFile = resolveDatabaseFile(DATA_DIR);
|
|
1937
|
+
const DB_PATH = databaseFile.path;
|
|
1938
|
+
{
|
|
1939
|
+
// A dangling link, or an oar.db-wal/-shm without oar.db (a later rename to
|
|
1940
|
+
// oar.db would take its pages in), stops the start; other strays are said.
|
|
1941
|
+
const check = checkDatabaseFile(databaseFile);
|
|
1942
|
+
for (const warning of check.warnings) console.warn(warning);
|
|
1943
|
+
if (check.refusal) {
|
|
1944
|
+
console.error(check.refusal);
|
|
1945
|
+
try { relaySingletonGuard.release(); } catch {}
|
|
1946
|
+
process.exit(RELAY_STOP_EXIT_CODE);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1895
1949
|
|
|
1896
1950
|
const MAX_REQUEUE_RETRIES = Number.isFinite(Number(config.maxRequeueRetries))
|
|
1897
1951
|
? Math.max(1, Number(config.maxRequeueRetries))
|
|
@@ -2737,7 +2791,7 @@ const configuredConversationSessionMode =
|
|
|
2737
2791
|
updateModelCatalog({ models: [DEFAULT_MODEL], currentModel: DEFAULT_MODEL, defaultModel: DEFAULT_MODEL, source: 'bootstrap' });
|
|
2738
2792
|
|
|
2739
2793
|
// ─── SQLite Setup ─────────────────────────────────────────────────────────────
|
|
2740
|
-
const databaseCreatedAtStart =
|
|
2794
|
+
const databaseCreatedAtStart = databaseFile.created;
|
|
2741
2795
|
const db = new Database(DB_PATH);
|
|
2742
2796
|
if (process.platform !== 'win32') {
|
|
2743
2797
|
fs.chmodSync(DB_PATH, 0o600);
|
|
@@ -2750,11 +2804,39 @@ if (process.platform !== 'win32') {
|
|
|
2750
2804
|
}
|
|
2751
2805
|
}
|
|
2752
2806
|
|
|
2807
|
+
// Where the state root keeps backups (npm), or a checkout (server/backups).
|
|
2808
|
+
const STATE_LAYOUT = resolveStateLayout({ packageRoot: REPO_ROOT });
|
|
2809
|
+
// A copy of the database before this version's migrations, when another
|
|
2810
|
+
// version (on a checkout: another commit) ran on it last; before applySchema
|
|
2811
|
+
// and before reconcileUpdateAttempt, which removes the in-app update's
|
|
2812
|
+
// record that names the version before. Never stops the start.
|
|
2813
|
+
let dbBackupAtStart = null;
|
|
2814
|
+
try {
|
|
2815
|
+
dbBackupAtStart = await maybeBackupBeforeStart({
|
|
2816
|
+
db,
|
|
2817
|
+
Database,
|
|
2818
|
+
dbPath: DB_PATH,
|
|
2819
|
+
dataDir: DATA_DIR,
|
|
2820
|
+
databaseCreated: databaseCreatedAtStart,
|
|
2821
|
+
layout: STATE_LAYOUT,
|
|
2822
|
+
serverDir: __dirname,
|
|
2823
|
+
config,
|
|
2824
|
+
runningVersion: RUNNING_VERSION,
|
|
2825
|
+
runningCommit: BUILD_COMMIT,
|
|
2826
|
+
logger: console,
|
|
2827
|
+
});
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
console.warn(`[backup] failed: ${error?.message || error}; this start goes on without a backup of the version before.`);
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2753
2832
|
// Base DDL plus the column-backfill/upgrade migrations, extracted verbatim to
|
|
2754
2833
|
// db-schema.mjs so tests can build the exact production schema. Runs the
|
|
2755
2834
|
// CREATE TABLE IF NOT EXISTS block, the FTS setup, and every ALTER TABLE
|
|
2756
2835
|
// upgrade in the original order.
|
|
2757
2836
|
applySchema(db);
|
|
2837
|
+
// A database that had no app_settings table before applySchema (a new one):
|
|
2838
|
+
// the version that runs on it now.
|
|
2839
|
+
if (dbBackupAtStart?.deferredLastRun) writeDbLastRun(db, dbBackupAtStart.deferredLastRun);
|
|
2758
2840
|
|
|
2759
2841
|
// ─── Prepared Statements ──────────────────────────────────────────────────────
|
|
2760
2842
|
// Owns its own schema (plan-usage tables) and the derived Cursor spend
|
|
@@ -3245,11 +3327,16 @@ function buildSessionWorkerLaunchEnvForSession(targetSessionId) {
|
|
|
3245
3327
|
if (!copilotSdkEngine) return openAiEnv;
|
|
3246
3328
|
return applyCopilotSdkProviderEnvironment(openAiEnv, { enabled: true, model });
|
|
3247
3329
|
}
|
|
3330
|
+
// Under a systemd user service, session workers start in scopes of their
|
|
3331
|
+
// own (worker-scope.mjs); asked once, at the first worker start.
|
|
3332
|
+
const workerScope = createWorkerScope({ hosting: detectRelayHosting(), log: (message) => console.log(`${runtimeLogPrefix()}${message}`) });
|
|
3248
3333
|
const relayCliLauncherService = createRelayCliLauncherService({
|
|
3249
3334
|
cwd: (targetSessionId) => resolveLaunchWorkspaceRootForSession(targetSessionId),
|
|
3250
3335
|
env: sessionWorkerLaunchEnv,
|
|
3251
3336
|
log: (message) => console.log(`${runtimeLogPrefix()}${message}`),
|
|
3337
|
+
workerScope,
|
|
3252
3338
|
});
|
|
3339
|
+
|
|
3253
3340
|
async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true } = {}) {
|
|
3254
3341
|
const normalizedTargetSessionId = String(targetSessionId || '').trim();
|
|
3255
3342
|
if (!normalizedTargetSessionId) {
|
|
@@ -3281,6 +3368,7 @@ async function spawnSessionWorkerCli(targetSessionId, { allowProcessReuse = true
|
|
|
3281
3368
|
const resolvedWorkspaceRoot = resolveLaunchWorkspaceRootForSession(normalizedTargetSessionId);
|
|
3282
3369
|
const workerLaunchEnv = buildSessionWorkerLaunchEnvForSession(normalizedTargetSessionId);
|
|
3283
3370
|
const launched = await launchSessionCli({
|
|
3371
|
+
workerScope,
|
|
3284
3372
|
targetSessionId: normalizedTargetSessionId,
|
|
3285
3373
|
cwd: resolvedWorkspaceRoot,
|
|
3286
3374
|
env: workerLaunchEnv,
|
|
@@ -3359,6 +3447,7 @@ const updateCheckService = updateCheckKilled ? null : createUpdateCheckService({
|
|
|
3359
3447
|
});
|
|
3360
3448
|
const updateInstallService = createUpdateInstallService({
|
|
3361
3449
|
runningVersion: RUNNING_VERSION,
|
|
3450
|
+
describeNextBackup: () => describeNextBackup({ layout: STATE_LAYOUT, serverDir: __dirname, dataDir: DATA_DIR, dbPath: DB_PATH, config }),
|
|
3362
3451
|
installMethod: INSTALL_METHOD,
|
|
3363
3452
|
packageRoot: PACKAGE_ROOT,
|
|
3364
3453
|
readSetting: readAppSettingValue,
|
|
@@ -3676,6 +3765,7 @@ function collectRestartPicture({ action = 'restart' } = {}) {
|
|
|
3676
3765
|
restartPictureCollector = createRestartPictureCollector({
|
|
3677
3766
|
db,
|
|
3678
3767
|
backgroundWorkView,
|
|
3768
|
+
workerScopes: () => workerScope.inUse(),
|
|
3679
3769
|
listHeldTasks: () => backgroundTaskStore.listRestored().map(({ key, task }) => ({
|
|
3680
3770
|
conversationId: resolveConversationRecord({ conversationId: key, sdkSessionId: key })?.id || key,
|
|
3681
3771
|
description: task?.description || task?.taskType || '',
|
|
@@ -4055,19 +4145,28 @@ function parseBoardContext(rawContext) {
|
|
|
4055
4145
|
|
|
4056
4146
|
function formatRelayBoardRow(row) {
|
|
4057
4147
|
if (!row) return null;
|
|
4148
|
+
const boardType = String(row.board_type || '').trim().toLowerCase() || 'generic';
|
|
4149
|
+
const status = String(row.status || 'pending').trim().toLowerCase() || 'pending';
|
|
4150
|
+
const planOnly = readPlanOnly(stmts.getConvAnyStatus.get(row.conversation_id));
|
|
4058
4151
|
return {
|
|
4059
4152
|
id: row.id,
|
|
4060
4153
|
queueId: row.queue_id,
|
|
4061
4154
|
conversationId: row.conversation_id,
|
|
4062
4155
|
messageId: row.message_id,
|
|
4063
|
-
boardType
|
|
4156
|
+
boardType,
|
|
4064
4157
|
mode: normalizeRelayMode(row.relay_mode) || DEFAULT_RELAY_MODE,
|
|
4065
4158
|
title: String(row.title || '').trim(),
|
|
4066
4159
|
body: String(row.body || '').trim(),
|
|
4067
|
-
|
|
4068
|
-
|
|
4160
|
+
// A pending plan card stored before "Approve plan" existed shows it too.
|
|
4161
|
+
actions: planBoardShownActions({ boardType, status }, parseBoardActions(row.actions_json)),
|
|
4162
|
+
// On a plan-only session a pending plan card recommends "Approve plan".
|
|
4163
|
+
recommendedAction: (planOnly && status === 'pending' && boardType === 'plan_ready')
|
|
4164
|
+
? PLAN_BOARD_APPROVE_ONLY
|
|
4165
|
+
: (String(row.recommended_action || '').trim() || null),
|
|
4166
|
+
// The UI asks before an action that leaves plan mode on such a session.
|
|
4167
|
+
planOnly,
|
|
4069
4168
|
context: parseBoardContext(row.context_json),
|
|
4070
|
-
status
|
|
4169
|
+
status,
|
|
4071
4170
|
selectedAction: String(row.selected_action || '').trim() || null,
|
|
4072
4171
|
actedAt: row.acted_at || null,
|
|
4073
4172
|
createdAt: row.created_at,
|
|
@@ -5249,6 +5348,8 @@ function createCompactedConversation(sourceConversationId) {
|
|
|
5249
5348
|
// The new conversation stays in the old one's session group, and heads
|
|
5250
5349
|
// it if the old one did; the archived source stays a member.
|
|
5251
5350
|
conversationGroups.carryToCompacted(sourceConversationId, targetConversationId, now);
|
|
5351
|
+
// A plan-only conversation's lock goes along, with its saved mode.
|
|
5352
|
+
carryPlanOnlyToCompacted({ db, stmts, source, targetConversationId, updatedAt: now });
|
|
5252
5353
|
// A Claude chat keeps its billing (the subscription or the API key, a
|
|
5253
5354
|
// pending swap's included): the new chat's first worker launches on it,
|
|
5254
5355
|
// and it is never guessed again from a Claude model id. Other providers
|
|
@@ -5550,6 +5651,9 @@ const usageLimitPauseService = createUsageLimitPauseService({
|
|
|
5550
5651
|
// workers' reports (services/claude-api-usage-service.mjs); the Claude API
|
|
5551
5652
|
// card and its cap read it.
|
|
5552
5653
|
const claudeApiUsageService = createClaudeApiUsageService({ db, logger: console });
|
|
5654
|
+
// Tokens per turn and per day for every provider, booked from the
|
|
5655
|
+
// workers' reports; the Tokens sections in Check Usage and About read it.
|
|
5656
|
+
const tokenUsageService = createTokenUsageService({ db, logger: console });
|
|
5553
5657
|
// This month's spend on the key against the cap, and the Check Usage card
|
|
5554
5658
|
// (services/plan-usage-claude-api.mjs), over the same local days.
|
|
5555
5659
|
const claudeApiPlanUsage = createClaudeApiPlanUsage({
|
|
@@ -7362,6 +7466,16 @@ const relayAskLinkService = createRelayAskLinkService({
|
|
|
7362
7466
|
client: remoteRelayClient,
|
|
7363
7467
|
logger: console,
|
|
7364
7468
|
});
|
|
7469
|
+
// Paired relays' cards a turn waits on: the badge here stays current, and
|
|
7470
|
+
// one relay pushes a card its own relay reached no device for.
|
|
7471
|
+
const remoteInboxWatchService = createRemoteInboxWatchService({
|
|
7472
|
+
registry: remoteRelayRegistry,
|
|
7473
|
+
client: remoteRelayClient,
|
|
7474
|
+
push: pushDispatchService,
|
|
7475
|
+
hasSockets: () => io.of('/').sockets.size > 0,
|
|
7476
|
+
emit: (event) => io.emit(event),
|
|
7477
|
+
logger: console,
|
|
7478
|
+
});
|
|
7365
7479
|
const remoteRelayPairing = createRemoteRelayPairing({
|
|
7366
7480
|
registry: remoteRelayRegistry,
|
|
7367
7481
|
client: remoteRelayClient,
|
|
@@ -7647,6 +7761,7 @@ const sharedRouteDeps = {
|
|
|
7647
7761
|
usageLimitPauseService,
|
|
7648
7762
|
claudeBillingSwitchService,
|
|
7649
7763
|
claudeApiUsageService,
|
|
7764
|
+
tokenUsageService,
|
|
7650
7765
|
claudeApiClaimCheck,
|
|
7651
7766
|
onClaudeBillingFence: handleClaudeBillingFence,
|
|
7652
7767
|
claudeApiPlanUsage,
|
|
@@ -7719,11 +7834,13 @@ registerRelayAskRoutes(app, {
|
|
|
7719
7834
|
db,
|
|
7720
7835
|
stmts,
|
|
7721
7836
|
formatQuestionRow,
|
|
7837
|
+
formatRelayBoardRow,
|
|
7722
7838
|
relayAskService,
|
|
7723
7839
|
relayAskLinkService,
|
|
7724
7840
|
remoteRelayInbound,
|
|
7725
7841
|
remoteRelayRegistry,
|
|
7726
7842
|
remoteRelayClient,
|
|
7843
|
+
questionNotice: (questionId) => pushDispatchService.questionNotice(questionId),
|
|
7727
7844
|
});
|
|
7728
7845
|
registerRelayRestartRoutes(app, {
|
|
7729
7846
|
auth,
|
|
@@ -7758,6 +7875,7 @@ registerAboutRoutes(app, {
|
|
|
7758
7875
|
isRelayCliOnline: () => runtimeState.cliOnline === true,
|
|
7759
7876
|
listWorkers: () => sessionWorkerRegistry.listWorkers(),
|
|
7760
7877
|
getUpdateState: () => buildUpdateStatePayload(),
|
|
7878
|
+
describeBackups: () => describeBackups({ layout: STATE_LAYOUT, serverDir: __dirname, dataDir: DATA_DIR, config, sizes: false }),
|
|
7761
7879
|
}),
|
|
7762
7880
|
});
|
|
7763
7881
|
function markCliOffline(reason = 'offline', { clearOwner = true } = {}) {
|
|
@@ -8109,6 +8227,8 @@ relayEventLinkService.start();
|
|
|
8109
8227
|
relayScheduleService.start();
|
|
8110
8228
|
// Asks on paired relays still open there: look for their answers again.
|
|
8111
8229
|
relayAskLinkService.start();
|
|
8230
|
+
// Paired relays' Inboxes, every ~20 s.
|
|
8231
|
+
remoteInboxWatchService.start();
|
|
8112
8232
|
runtimeTimers.pendingWorkerPrime = setInterval(() => {
|
|
8113
8233
|
void primePendingSessionWorkers('maintenance');
|
|
8114
8234
|
detachedWorkerWatch.sweep().catch((error) => {
|
|
@@ -8431,6 +8551,7 @@ function shutdownRuntime(reason = 'unknown', { exitCode = 0 } = {}) {
|
|
|
8431
8551
|
try { relayEventLinkService.stop(); } catch {}
|
|
8432
8552
|
try { relayScheduleService.stop(); } catch {}
|
|
8433
8553
|
try { relayAskLinkService.stop(); } catch {}
|
|
8554
|
+
try { remoteInboxWatchService.stop(); } catch {}
|
|
8434
8555
|
tmuxInspectorSocketService.stop();
|
|
8435
8556
|
stopWorkspaceFileWatcher();
|
|
8436
8557
|
sshTunnelManager.stop();
|
|
@@ -273,6 +273,42 @@ export function collectUsageTotals(db) {
|
|
|
273
273
|
usage['claude-cloud'] = { costUsd: toAmount(row.cost), sessions: toCount(row.sessions) };
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
|
+
// Every provider's tokens per turn, all time (token_usage_days, migration
|
|
277
|
+
// 0036): fresh input, output, cache writes and cache reads apart, each
|
|
278
|
+
// null while no turn reported it, the first day, and whether days copied
|
|
279
|
+
// at the upgrade are in it. An own key's turns (Copilot) are kept apart
|
|
280
|
+
// from the plan's.
|
|
281
|
+
if (tableExists(db, 'token_usage_days')) {
|
|
282
|
+
const rows = db.prepare(`
|
|
283
|
+
SELECT provider, billing, SUM(input_tokens) AS input, SUM(output_tokens) AS output,
|
|
284
|
+
SUM(cache_write_tokens) AS cache_write, SUM(cache_read_tokens) AS cache_read, MIN(day) AS since,
|
|
285
|
+
MIN(CASE WHEN source = 'turns' THEN day END) AS first_turns_day,
|
|
286
|
+
MAX(CASE WHEN source = 'backfill' THEN 1 ELSE 0 END) AS backfilled
|
|
287
|
+
FROM token_usage_days
|
|
288
|
+
GROUP BY provider, billing
|
|
289
|
+
ORDER BY provider, billing
|
|
290
|
+
`).all();
|
|
291
|
+
const nullable = (value) => (value === null || value === undefined ? null : toAmount(value));
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
const tokens = {
|
|
294
|
+
inputTokens: nullable(row.input),
|
|
295
|
+
outputTokens: nullable(row.output),
|
|
296
|
+
cacheWriteTokens: nullable(row.cache_write),
|
|
297
|
+
cacheReadTokens: nullable(row.cache_read),
|
|
298
|
+
since: row.since || null,
|
|
299
|
+
// Days copied at the upgrade (0036) hold cache writes inside input;
|
|
300
|
+
// the first counted turn's day is where that stops.
|
|
301
|
+
backfilled: Number(row.backfilled) === 1,
|
|
302
|
+
firstTurnsDay: row.first_turns_day || null,
|
|
303
|
+
};
|
|
304
|
+
const entry = usage[row.provider] || (usage[row.provider] = {});
|
|
305
|
+
if (row.billing) {
|
|
306
|
+
(entry.ownKeyTokens || (entry.ownKeyTokens = [])).push({ billing: row.billing, ...tokens });
|
|
307
|
+
} else {
|
|
308
|
+
entry.tokens = tokens;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
276
312
|
return usage;
|
|
277
313
|
}
|
|
278
314
|
|
|
@@ -310,10 +346,12 @@ async function fileSize(fsImpl, filePath) {
|
|
|
310
346
|
* @param {() => object[]} [deps.listWorkers] the session worker registry's entries
|
|
311
347
|
* @param {() => object|null} [deps.getUpdateState] buildUpdateStatePayload()
|
|
312
348
|
* @param {object} [deps.sdkVersions] package version by provider id; read from node_modules when absent
|
|
349
|
+
* @param {() => object|null} [deps.describeBackups] the database backups (db-backup-service.mjs describeBackups)
|
|
313
350
|
*/
|
|
314
351
|
export function createAboutService({
|
|
315
352
|
db,
|
|
316
353
|
dbPath = null,
|
|
354
|
+
describeBackups = null,
|
|
317
355
|
version = '0.0.0',
|
|
318
356
|
commit = null,
|
|
319
357
|
installMethod = null,
|
|
@@ -451,14 +489,35 @@ export function createAboutService({
|
|
|
451
489
|
usage: totals.usage,
|
|
452
490
|
relay: {
|
|
453
491
|
uptimeSeconds,
|
|
492
|
+
// The full path: which of the two database names this relay runs on.
|
|
493
|
+
dbPath: dbPath || null,
|
|
454
494
|
dbBytes: dbFileBytes + walBytes,
|
|
455
495
|
dbFileBytes,
|
|
456
496
|
walBytes,
|
|
457
497
|
runningWorkers: workers.total,
|
|
458
498
|
},
|
|
459
499
|
update: describeUpdate(),
|
|
500
|
+
backups: readBackups(),
|
|
460
501
|
};
|
|
461
502
|
}
|
|
462
503
|
|
|
504
|
+
// The last backup and how many are kept, for one row; never the listing.
|
|
505
|
+
function readBackups() {
|
|
506
|
+
if (!describeBackups) return null;
|
|
507
|
+
try {
|
|
508
|
+
const backups = describeBackups();
|
|
509
|
+
return {
|
|
510
|
+
enabled: backups.enabled,
|
|
511
|
+
keep: backups.keep,
|
|
512
|
+
dir: backups.dir,
|
|
513
|
+
problem: backups.problem,
|
|
514
|
+
kept: backups.own.length,
|
|
515
|
+
lastResult: backups.lastResult,
|
|
516
|
+
};
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
463
522
|
return { describe };
|
|
464
523
|
}
|
|
@@ -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(`
|
|
@@ -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,
|