@oar-sh/oar 0.9.6 → 0.9.7
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/CHANGELOG.md +172 -0
- package/README.md +176 -22
- package/package.json +1 -1
- package/server/README.md +174 -7
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +18 -2
- package/server/claude-worker/claude-session-process.mjs +33 -0
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/db-schema.mjs +17 -0
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +143 -4
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +344 -31
- package/server/public/app/claude-cloud-conversation-ui.js +110 -0
- package/server/public/app/claude-cloud-settings-ui.js +374 -0
- package/server/public/app/claude-cloud-ui.mjs +611 -0
- package/server/public/app/composer-control-state.mjs +11 -0
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +83 -4
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +698 -28
- package/server/public/app/math-dollars.mjs +89 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +36 -8
- package/server/public/app/relay-error-ctas.mjs +16 -2
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/settings-modal.js +33 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +51 -0
- package/server/public/index.html +381 -7
- package/server/relay-tools.md +13 -1
- package/server/repositories/remote-relay-repository.mjs +42 -1
- package/server/repositories/session-repository.mjs +47 -1
- package/server/routes/claude-cloud-routes.mjs +140 -0
- package/server/routes/git-routes.mjs +37 -0
- package/server/routes/messages-routes.mjs +120 -15
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +502 -72
- package/server/server-runtime.mjs +258 -25
- package/server/services/claude-account-usage-service.mjs +176 -0
- package/server/services/claude-cloud-repo-service.mjs +408 -0
- package/server/services/claude-cloud-session-service.mjs +326 -0
- package/server/services/claude-cloud-settings-service.mjs +269 -0
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/plan-usage-claude-cloud.mjs +211 -0
- package/server/services/plan-usage-claude.mjs +202 -40
- package/server/services/plan-usage-contract.mjs +39 -3
- package/server/services/plan-usage-service.mjs +82 -2
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/remote-relay-caller-context.mjs +49 -10
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +383 -35
- package/server/services/remote-relay-inbound.mjs +49 -2
- package/server/services/remote-relay-loopback.mjs +245 -0
- package/server/services/remote-relay-registry-service.mjs +52 -0
- package/server/services/session-worker-launch-service.mjs +100 -15
- package/server/services/session-worker-process-service.mjs +348 -129
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +56 -1
- package/server/services/usage-limit-pause-service.mjs +3 -1
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/claude-attribution.mjs +117 -0
- package/shared/claude-cloud/api-client.mjs +809 -0
- package/shared/claude-cloud/base-url.mjs +47 -0
- package/shared/claude-cloud/credentials.mjs +221 -0
- package/shared/claude-cloud/repo-url.mjs +111 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/remote-relay-contract.mjs +153 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Cloud session worker: a per-conversation Node process that speaks
|
|
3
|
+
// the same relay contracts as the Claude/Cursor/Grok workers, but runs no
|
|
4
|
+
// agent itself. The agent and its clone live in a Claude Code cloud session;
|
|
5
|
+
// this process sends the user's messages there and follows the session's
|
|
6
|
+
// event stream (claude-cloud-session-process.mjs).
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
loadTokenFromConfig,
|
|
12
|
+
resolveRelayServerUrl,
|
|
13
|
+
} from '../../shared/worker-runtime/config-loader.mjs';
|
|
14
|
+
import { createApiClient } from '../../shared/worker-runtime/api-client.mjs';
|
|
15
|
+
import { createWorkerWebSocketLink } from '../../shared/worker-runtime/worker-websocket-link.mjs';
|
|
16
|
+
import { createHeartbeatController } from '../../shared/worker-runtime/heartbeat.mjs';
|
|
17
|
+
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
18
|
+
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
19
|
+
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
20
|
+
import { createClaudeCloudCredentials } from '../../shared/claude-cloud/credentials.mjs';
|
|
21
|
+
import { createClaudeCloudClient } from '../../shared/claude-cloud/api-client.mjs';
|
|
22
|
+
import { resolveClaudeCloudBaseUrl } from '../../shared/claude-cloud/base-url.mjs';
|
|
23
|
+
import { createClaudeCloudSessionRunner } from './claude-cloud-session-process.mjs';
|
|
24
|
+
|
|
25
|
+
// Windows console launch: the launcher names the worker log, this copies
|
|
26
|
+
// stdout/stderr into it. A no-op wherever the launcher redirects instead.
|
|
27
|
+
installWorkerLogFile();
|
|
28
|
+
|
|
29
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const HEARTBEAT_MS = 10_000;
|
|
31
|
+
|
|
32
|
+
function parseSessionIdArg(argv = process.argv) {
|
|
33
|
+
const index = argv.indexOf('--session-id');
|
|
34
|
+
if (index !== -1 && argv[index + 1]) return String(argv[index + 1]).trim();
|
|
35
|
+
const inline = argv.find((arg) => String(arg || '').startsWith('--session-id='));
|
|
36
|
+
if (inline) return String(inline.split('=')[1] || '').trim();
|
|
37
|
+
return String(process.env.SESSION_ID || '').trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dbg(...parts) {
|
|
41
|
+
const timestamp = new Date().toISOString();
|
|
42
|
+
console.log(`[claude-cloud-worker ${timestamp}]`, ...parts);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main() {
|
|
46
|
+
const sdkSessionId = parseSessionIdArg();
|
|
47
|
+
if (!sdkSessionId) {
|
|
48
|
+
console.error('claude-cloud-session-worker: missing --session-id');
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
53
|
+
|| path.resolve(__dirname, '..', 'config.json');
|
|
54
|
+
const serverUrl = resolveRelayServerUrl({ configPath });
|
|
55
|
+
const token = loadTokenFromConfig(configPath);
|
|
56
|
+
const defaultModel = String(process.env.CLAUDE_CLOUD_RELAY_MODEL || '').trim();
|
|
57
|
+
|
|
58
|
+
const api = createApiClient({
|
|
59
|
+
serverUrl,
|
|
60
|
+
token,
|
|
61
|
+
getHeaders: () => ({
|
|
62
|
+
'X-Relay-Process-Pid': String(process.pid),
|
|
63
|
+
'X-Relay-Parent-Pid': String(process.ppid),
|
|
64
|
+
'X-Relay-Session-Id': sdkSessionId,
|
|
65
|
+
'X-Relay-Conversation-Id': sdkSessionId,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// The Claude CLI's login, read by the client's own module (the credentials
|
|
70
|
+
// file, or CLAUDE_CODE_OAUTH_TOKEN where the relay runs on one): this
|
|
71
|
+
// process never logs it. A login that has run out is nudged through the
|
|
72
|
+
// relay, which can run the CLI (`claude auth status` refreshes a login
|
|
73
|
+
// whose refresh token is still good); the file is read again afterwards.
|
|
74
|
+
// The base URL is the Anthropic API unless a test names a fake one on this
|
|
75
|
+
// machine (base-url.mjs takes nothing else).
|
|
76
|
+
const cloud = createClaudeCloudClient({
|
|
77
|
+
credentials: createClaudeCloudCredentials({
|
|
78
|
+
nudge: async () => {
|
|
79
|
+
const answer = await api('POST', '/api/claude-cloud/login-nudge', {});
|
|
80
|
+
dbg('login nudge', answer?.nudged ? 'ran' : 'skipped', answer?.expired ? 'still expired' : `expires ${answer?.expiresAt || '?'}`);
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
baseUrl: resolveClaudeCloudBaseUrl(process.env),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const controlPoller = createControlPoller({
|
|
87
|
+
api,
|
|
88
|
+
sdkSessionId,
|
|
89
|
+
abortAckNote: 'cloud turn interrupted',
|
|
90
|
+
dbg,
|
|
91
|
+
});
|
|
92
|
+
const runner = createClaudeCloudSessionRunner({
|
|
93
|
+
api,
|
|
94
|
+
cloud,
|
|
95
|
+
sdkSessionId,
|
|
96
|
+
defaultModel,
|
|
97
|
+
controlPoller,
|
|
98
|
+
dbg,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let heartbeatTimer = null;
|
|
102
|
+
const heartbeat = createHeartbeatController({
|
|
103
|
+
api,
|
|
104
|
+
pollMs: HEARTBEAT_MS,
|
|
105
|
+
getSessionReady: () => true,
|
|
106
|
+
getHeartbeatTimer: () => heartbeatTimer,
|
|
107
|
+
setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
|
|
108
|
+
getActiveQueueMessageId: () => runner.getActiveQueueMessageId(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const wsLink = createWorkerWebSocketLink({
|
|
112
|
+
serverUrl,
|
|
113
|
+
token,
|
|
114
|
+
dbg,
|
|
115
|
+
getSessionReady: () => true,
|
|
116
|
+
getSessionId: () => sdkSessionId,
|
|
117
|
+
getPid: () => process.pid,
|
|
118
|
+
onDeliver: async (pending, reason) => {
|
|
119
|
+
dbg('queue.deliver received', `reason=${reason}`, `msgId=${pending?.message?.id || 'none'}`);
|
|
120
|
+
try {
|
|
121
|
+
return await runner.handlePendingPayload(pending);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
dbg('turn handling failed', error?.message || String(error));
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const shutdown = async (signal) => {
|
|
130
|
+
dbg(`shutting down (${signal})`);
|
|
131
|
+
try { wsLink.stop(); } catch {}
|
|
132
|
+
try { heartbeat.stopHeartbeat(); } catch {}
|
|
133
|
+
try { controlPoller.stop(); } catch {}
|
|
134
|
+
// The cloud session is left as it is: a turn in flight keeps running
|
|
135
|
+
// there, and the next worker finds it through the stored sequence number.
|
|
136
|
+
try { await runner.dispose(); } catch {}
|
|
137
|
+
process.exit(0);
|
|
138
|
+
};
|
|
139
|
+
process.on('SIGTERM', () => { shutdown('SIGTERM'); });
|
|
140
|
+
process.on('SIGINT', () => { shutdown('SIGINT'); });
|
|
141
|
+
installWorkerCrashGuard({
|
|
142
|
+
api,
|
|
143
|
+
workerName: 'claude-cloud-session-worker',
|
|
144
|
+
getActiveQueueMessageIds: () => [runner.getActiveQueueAttempt()],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} model=${defaultModel || 'default'}`);
|
|
148
|
+
heartbeat.startHeartbeat();
|
|
149
|
+
wsLink.start();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
main().catch((error) => {
|
|
153
|
+
console.error('claude-cloud-session-worker fatal:', error?.stack || error?.message || error);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
});
|
|
@@ -62,6 +62,8 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
|
|
|
62
62
|
provider: choice('provider', REMOTE_RELAY_PROVIDERS),
|
|
63
63
|
model: text('model'),
|
|
64
64
|
cwd: text('cwd'),
|
|
65
|
+
repo: text('repo'),
|
|
66
|
+
branch: text('branch'),
|
|
65
67
|
mode: choice('mode', REMOTE_RELAY_MODES),
|
|
66
68
|
effort: text('effort'),
|
|
67
69
|
title: text('title'),
|
|
@@ -111,7 +111,7 @@ export function normalizeAutoCompactWindow(value) {
|
|
|
111
111
|
* spreads of `settings` in the options literal would silently clobber each
|
|
112
112
|
* other. Returns null when nothing needs to be set.
|
|
113
113
|
*/
|
|
114
|
-
export function claudeSpawnSettings({ ultracode = false, autoCompactWindow = null, thinkingEnabled = null } = {}) {
|
|
114
|
+
export function claudeSpawnSettings({ ultracode = false, autoCompactWindow = null, thinkingEnabled = null, attribution = null } = {}) {
|
|
115
115
|
const window = normalizeAutoCompactWindow(autoCompactWindow);
|
|
116
116
|
const thinking = strictThinkingEnabled(thinkingEnabled);
|
|
117
117
|
const settings = {
|
|
@@ -122,10 +122,25 @@ export function claudeSpawnSettings({ ultracode = false, autoCompactWindow = nul
|
|
|
122
122
|
// present; the omit path exists for direct callers that mean to say
|
|
123
123
|
// nothing.
|
|
124
124
|
...(thinking !== null ? { alwaysThinkingEnabled: thinking } : {}),
|
|
125
|
+
// What commits and PRs say (shared/claude-attribution.mjs). null is
|
|
126
|
+
// vanilla: the key is left out and the CLI uses its own.
|
|
127
|
+
...(attribution && typeof attribution === 'object' ? { attribution } : {}),
|
|
125
128
|
};
|
|
126
129
|
return Object.keys(settings).length ? settings : null;
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Live twin of the attribution spawn setting, for a value (OAR or off). A
|
|
134
|
+
* running CLI takes a new text through the flag layer (it arrives as a fresh
|
|
135
|
+
* system-reminder on the next user turn) but cannot UNSET a value it was
|
|
136
|
+
* spawned with — `{}`, `null` and `true` all leave the spawn-time text in
|
|
137
|
+
* force (wire-logged probe, 2026-10-01). Back to vanilla therefore means a
|
|
138
|
+
* fresh process without the key; the session runner does that.
|
|
139
|
+
*/
|
|
140
|
+
export function claudeAttributionFlagSettings(attribution) {
|
|
141
|
+
return { attribution: attribution && typeof attribution === 'object' ? attribution : { commit: '', pr: '', sessionUrl: false } };
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
export function permissionModeForRelayMode(relayMode) {
|
|
130
145
|
const mode = String(relayMode || 'agent').trim().toLowerCase();
|
|
131
146
|
if (mode === 'plan') return 'plan';
|
|
@@ -228,6 +243,7 @@ export function startClaudeSession({
|
|
|
228
243
|
reasoningEffort = '',
|
|
229
244
|
autoCompactWindow = null,
|
|
230
245
|
thinkingEnabled = null,
|
|
246
|
+
attribution = null,
|
|
231
247
|
thinkingDisplay = '',
|
|
232
248
|
abortController,
|
|
233
249
|
canUseTool,
|
|
@@ -248,7 +264,7 @@ export function startClaudeSession({
|
|
|
248
264
|
// only way to hand a fresh CLI the session-scoped ultracode flag, the
|
|
249
265
|
// auto-compact window and the thinking pin, and all three share one
|
|
250
266
|
// `settings` object.
|
|
251
|
-
const spawnSettings = claudeSpawnSettings({ ultracode, autoCompactWindow, thinkingEnabled });
|
|
267
|
+
const spawnSettings = claudeSpawnSettings({ ultracode, autoCompactWindow, thinkingEnabled, attribution });
|
|
252
268
|
// Relay tools need the worker's authenticated API helper; a caller that
|
|
253
269
|
// passes none (tests, probes) gets a session without them.
|
|
254
270
|
const relayMcpServer = typeof api === 'function'
|
|
@@ -21,12 +21,14 @@ import {
|
|
|
21
21
|
normalizeClaudeEffort,
|
|
22
22
|
claudeUltracodeFlagSettings,
|
|
23
23
|
claudeAutoCompactFlagSettings,
|
|
24
|
+
claudeAttributionFlagSettings,
|
|
24
25
|
claudeThinkingFlagSettings,
|
|
25
26
|
applyThinkingDisplay,
|
|
26
27
|
normalizeAutoCompactWindow,
|
|
27
28
|
permissionModeForRelayMode,
|
|
28
29
|
} from './claude-sdk-adapter.mjs';
|
|
29
30
|
import { parseThinkingDisplay, parseThinkingEnabled } from '../../shared/claude-thinking.mjs';
|
|
31
|
+
import { sameClaudeAttribution } from '../../shared/claude-attribution.mjs';
|
|
30
32
|
import { relocateClaudeTranscriptForCwd } from './claude-transcript-relocator.mjs';
|
|
31
33
|
import {
|
|
32
34
|
attemptFields,
|
|
@@ -448,6 +450,10 @@ export function createClaudeSessionRunner({
|
|
|
448
450
|
// per turn rather than captured, so a slider change picked up on the next
|
|
449
451
|
// delivery reaches a process that is already running.
|
|
450
452
|
getAutoCompactWindow = () => null,
|
|
453
|
+
// What this session's commits say (shared/claude-attribution.mjs); null
|
|
454
|
+
// leaves the CLI's own attribution. Piggybacked on deliveries like the
|
|
455
|
+
// window above.
|
|
456
|
+
getAttribution = () => null,
|
|
451
457
|
// The per-conversation thinking state ({enabled, display}); read per turn
|
|
452
458
|
// like the window, so a settings change reaches a running process on its
|
|
453
459
|
// next delivery.
|
|
@@ -3170,6 +3176,7 @@ export function createClaudeSessionRunner({
|
|
|
3170
3176
|
const thinkingState = getThinking() || {};
|
|
3171
3177
|
const thinkingEnabled = parseThinkingEnabled(thinkingState.enabled);
|
|
3172
3178
|
const thinkingDisplay = parseThinkingDisplay(thinkingState.display);
|
|
3179
|
+
const attribution = getAttribution() || null;
|
|
3173
3180
|
const abortController = new AbortController();
|
|
3174
3181
|
const processRef = {
|
|
3175
3182
|
turn: null,
|
|
@@ -3178,6 +3185,7 @@ export function createClaudeSessionRunner({
|
|
|
3178
3185
|
relayMode,
|
|
3179
3186
|
effort,
|
|
3180
3187
|
autoCompactWindow,
|
|
3188
|
+
attribution,
|
|
3181
3189
|
// Seeded from what the process is SPAWNED with, so the adapt diff is
|
|
3182
3190
|
// against reality rather than against a delivered setting the CLI may
|
|
3183
3191
|
// never have honored.
|
|
@@ -3335,6 +3343,7 @@ export function createClaudeSessionRunner({
|
|
|
3335
3343
|
autoCompactWindow,
|
|
3336
3344
|
thinkingEnabled,
|
|
3337
3345
|
thinkingDisplay,
|
|
3346
|
+
attribution,
|
|
3338
3347
|
abortController,
|
|
3339
3348
|
canUseTool,
|
|
3340
3349
|
pathToClaudeCodeExecutable,
|
|
@@ -3443,6 +3452,30 @@ export function createClaudeSessionRunner({
|
|
|
3443
3452
|
});
|
|
3444
3453
|
proc.autoCompactWindow = autoCompactWindow;
|
|
3445
3454
|
}
|
|
3455
|
+
// Attribution follows the window's pattern: a changed value (a settings
|
|
3456
|
+
// change, a model switch that renames the trailer) reaches the live CLI
|
|
3457
|
+
// through the flag layer, before the turn that may commit.
|
|
3458
|
+
const attribution = getAttribution() || null;
|
|
3459
|
+
if (!sameClaudeAttribution(attribution, proc.attribution)) {
|
|
3460
|
+
if (attribution === null) {
|
|
3461
|
+
// Back to the CLI's own attribution. A value the process was spawned
|
|
3462
|
+
// with cannot be unset through the flag layer (probe 2026-10-01), so
|
|
3463
|
+
// the honest way is a fresh process without the key — taken when
|
|
3464
|
+
// nothing lives in the old one; a busy process keeps the old text and
|
|
3465
|
+
// the next delivery tries again.
|
|
3466
|
+
if (!hasLiveWork() && !proc.liveTasks.size) {
|
|
3467
|
+
gracefulShutdown('attribution-vanilla');
|
|
3468
|
+
await drainClosingProcess(10_000);
|
|
3469
|
+
return null;
|
|
3470
|
+
}
|
|
3471
|
+
} else {
|
|
3472
|
+
expectSettingsReinit();
|
|
3473
|
+
await Promise.resolve(proc.turn.applyFlagSettings?.(claudeAttributionFlagSettings(attribution))).catch((error) => {
|
|
3474
|
+
dbg('applyFlagSettings attribution failed', error?.message || String(error));
|
|
3475
|
+
});
|
|
3476
|
+
proc.attribution = attribution;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3446
3479
|
// Thinking is NOT a symmetric copy of the window block: the probe
|
|
3447
3480
|
// (2026-08-26, docs/plans/claude-thinking-control.md) showed the two
|
|
3448
3481
|
// directions of the on/off axis are not equally reachable mid-session.
|
|
@@ -17,6 +17,7 @@ import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-fil
|
|
|
17
17
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
18
18
|
import { resolveDeliveredAutoCompactWindow } from '../../shared/auto-compact-window.mjs';
|
|
19
19
|
import { resolveDeliveredThinking } from '../../shared/claude-thinking.mjs';
|
|
20
|
+
import { resolveDeliveredAttribution } from '../../shared/claude-attribution.mjs';
|
|
20
21
|
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
21
22
|
import { createClaudeSessionRunner } from './claude-session-process.mjs';
|
|
22
23
|
import { createRunnerLinkBridge, failSettlingRowsOnShutdown } from './claude-worker-link-wiring.mjs';
|
|
@@ -66,6 +67,9 @@ async function main() {
|
|
|
66
67
|
// piggyback. enabled null = host default; display defaults to 'summarized'
|
|
67
68
|
// (today's behavior — visible thought bubbles).
|
|
68
69
|
let thinking = resolveDeliveredThinking(null, null);
|
|
70
|
+
// What this session's commits and PRs say; same piggyback. null = the
|
|
71
|
+
// CLI's own attribution (vanilla).
|
|
72
|
+
let attribution = null;
|
|
69
73
|
|
|
70
74
|
const api = createApiClient({
|
|
71
75
|
serverUrl,
|
|
@@ -111,6 +115,7 @@ async function main() {
|
|
|
111
115
|
getBackgroundTaskTimeoutMs: () => backgroundTaskTimeoutMs,
|
|
112
116
|
getAutoCompactWindow: () => autoCompactWindow,
|
|
113
117
|
getThinking: () => thinking,
|
|
118
|
+
getAttribution: () => attribution,
|
|
114
119
|
// A readiness flip is a hold starting or ending (question card,
|
|
115
120
|
// compaction, a turn-opening delivery): the steering snapshot changed
|
|
116
121
|
// with it, so it goes out now rather than on the next 10 s heartbeat.
|
|
@@ -176,6 +181,7 @@ async function main() {
|
|
|
176
181
|
// keep the last known state; an explicit null on thinkingEnabled is the
|
|
177
182
|
// user choosing Host default.
|
|
178
183
|
thinking = resolveDeliveredThinking(thinking, pending?.settings);
|
|
184
|
+
attribution = resolveDeliveredAttribution(attribution, pending?.settings);
|
|
179
185
|
try {
|
|
180
186
|
return await turnRunner.handlePendingPayload(pending);
|
|
181
187
|
} catch (error) {
|
package/server/db-schema.mjs
CHANGED
|
@@ -11,6 +11,8 @@ import { rebuildRecentWorkspaceRootsTable } from './migrations/0002-recent-works
|
|
|
11
11
|
import { ensurePushSubscriptionsTable } from './migrations/0003-push-subscriptions.mjs';
|
|
12
12
|
import { migrateSteerSettleMarkers } from './migrations/0004-steer-settle-markers.mjs';
|
|
13
13
|
import { migrateRemoteRelays } from './migrations/0005-remote-relays.mjs';
|
|
14
|
+
import { migrateWorkspaceRootSettings } from './migrations/0006-workspace-root-settings.mjs';
|
|
15
|
+
import { migrateClaudeCloud } from './migrations/0007-claude-cloud.mjs';
|
|
14
16
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
15
17
|
|
|
16
18
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -717,6 +719,21 @@ try {
|
|
|
717
719
|
} catch (error) {
|
|
718
720
|
console.warn(`[remote-relays] migration 0005 failed; retrying on next boot: ${error?.message || error}`);
|
|
719
721
|
}
|
|
722
|
+
// Per-folder settings (commit attribution override). Readers tolerate the
|
|
723
|
+
// table's absence, so a failure is logged and retried next boot like 0005.
|
|
724
|
+
try {
|
|
725
|
+
migrateWorkspaceRootSettings(db);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.warn(`[workspace-root-settings] migration 0006 failed; retrying on next boot: ${error?.message || error}`);
|
|
728
|
+
}
|
|
729
|
+
// Claude Cloud conversations: the cloud session binding and what it clones.
|
|
730
|
+
// Additive columns whose readers tolerate their absence, so a failure is
|
|
731
|
+
// logged and retried next boot like 0006.
|
|
732
|
+
try {
|
|
733
|
+
migrateClaudeCloud(db);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.warn(`[claude-cloud] migration 0007 failed; retrying on next boot: ${error?.message || error}`);
|
|
736
|
+
}
|
|
720
737
|
|
|
721
738
|
const runtimeSessionColumns = db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((c) => c.name);
|
|
722
739
|
if (runtimeSessionColumns.length) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: per-folder settings (`workspace_root_settings`).
|
|
5
|
+
*
|
|
6
|
+
* The first setting a repo folder can carry is the commit attribution mode
|
|
7
|
+
* of its Claude sessions (shared/claude-attribution.mjs): a repo that forbids
|
|
8
|
+
* co-author trailers, or wants its own, overrides the provider setting for
|
|
9
|
+
* every session that runs in it. Keyed like `recent_workspace_roots`
|
|
10
|
+
* (`normalizeWorkspaceRootKey`), but its own table: the MRU list is pruned.
|
|
11
|
+
*
|
|
12
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
13
|
+
*
|
|
14
|
+
* Usage: node server/migrations/0006-workspace-root-settings.mjs [path/to/copilot.db]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import process from 'process';
|
|
20
|
+
import Database from 'better-sqlite3';
|
|
21
|
+
|
|
22
|
+
function tableExists(db, name) {
|
|
23
|
+
return !!db
|
|
24
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
25
|
+
.get(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function migrateWorkspaceRootSettings(db) {
|
|
29
|
+
const existed = tableExists(db, 'workspace_root_settings');
|
|
30
|
+
db.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS workspace_root_settings (
|
|
32
|
+
path_key TEXT PRIMARY KEY,
|
|
33
|
+
path TEXT NOT NULL,
|
|
34
|
+
attribution_mode TEXT,
|
|
35
|
+
updated_at TEXT NOT NULL
|
|
36
|
+
)
|
|
37
|
+
`);
|
|
38
|
+
return { applied: !existed, tableCreated: !existed };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultDbPath() {
|
|
42
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function migrate(dbPath, options = {}) {
|
|
47
|
+
const db = new Database(dbPath, options);
|
|
48
|
+
try {
|
|
49
|
+
return migrateWorkspaceRootSettings(db);
|
|
50
|
+
} finally {
|
|
51
|
+
db.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
56
|
+
if (isMain) {
|
|
57
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
58
|
+
try {
|
|
59
|
+
const result = migrate(dbPath);
|
|
60
|
+
console.log(`Migration 0006 on ${dbPath}`);
|
|
61
|
+
console.log(result.applied ? ' workspace_root_settings table created' : ' skipped: nothing to change');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(`Migration 0006 failed: ${err?.message || err}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: storage for Claude Cloud conversations (provider `claude-cloud`).
|
|
5
|
+
*
|
|
6
|
+
* 1. `runtime_sessions.claude_cloud_session_id` — the cloud session a
|
|
7
|
+
* conversation runs in, reported by its worker after the first message
|
|
8
|
+
* (the per-provider native id, next to `claude_native_session_id`,
|
|
9
|
+
* `cursor_agent_id` and `grok_native_session_id`).
|
|
10
|
+
* 2. `runtime_sessions.claude_cloud_last_sequence` — the last event sequence
|
|
11
|
+
* the worker has handled: where it resumes the event stream after a restart.
|
|
12
|
+
* 3. `runtime_sessions.claude_cloud_cost_usd` — what the session has cost so
|
|
13
|
+
* far, as the cloud reports it.
|
|
14
|
+
* 4. `conversations.cloud_source_json` — what the session clones and where it
|
|
15
|
+
* is shown: `{ repoUrl, branch, environmentId, sessionUrl,
|
|
16
|
+
* pushedBranches: [{ branch, at }] }`.
|
|
17
|
+
*
|
|
18
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
19
|
+
*
|
|
20
|
+
* Usage: node server/migrations/0007-claude-cloud.mjs [path/to/copilot.db]
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
import process from 'process';
|
|
26
|
+
import Database from 'better-sqlite3';
|
|
27
|
+
|
|
28
|
+
const COLUMNS = Object.freeze([
|
|
29
|
+
Object.freeze({ table: 'runtime_sessions', name: 'claude_cloud_session_id', type: 'TEXT' }),
|
|
30
|
+
Object.freeze({ table: 'runtime_sessions', name: 'claude_cloud_last_sequence', type: 'TEXT' }),
|
|
31
|
+
Object.freeze({ table: 'runtime_sessions', name: 'claude_cloud_cost_usd', type: 'REAL' }),
|
|
32
|
+
Object.freeze({ table: 'conversations', name: 'cloud_source_json', type: 'TEXT' }),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function tableExists(db, name) {
|
|
36
|
+
return !!db
|
|
37
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
38
|
+
.get(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function columnNames(db, table) {
|
|
42
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => String(column.name)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function migrateClaudeCloud(db) {
|
|
46
|
+
let columnsAdded = 0;
|
|
47
|
+
for (const column of COLUMNS) {
|
|
48
|
+
if (!tableExists(db, column.table)) continue;
|
|
49
|
+
if (columnNames(db, column.table).has(column.name)) continue;
|
|
50
|
+
db.exec(`ALTER TABLE ${column.table} ADD COLUMN ${column.name} ${column.type}`);
|
|
51
|
+
columnsAdded += 1;
|
|
52
|
+
}
|
|
53
|
+
return { applied: columnsAdded > 0, columnsAdded };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function defaultDbPath() {
|
|
57
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function migrate(dbPath, options = {}) {
|
|
62
|
+
const db = new Database(dbPath, options);
|
|
63
|
+
try {
|
|
64
|
+
return migrateClaudeCloud(db);
|
|
65
|
+
} finally {
|
|
66
|
+
db.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
71
|
+
if (isMain) {
|
|
72
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
73
|
+
try {
|
|
74
|
+
const result = migrate(dbPath);
|
|
75
|
+
console.log(`Migration 0007 on ${dbPath}`);
|
|
76
|
+
console.log(result.applied ? ` claude cloud columns added: ${result.columnsAdded}` : ' skipped: nothing to change');
|
|
77
|
+
process.exit(0);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(`Migration 0007 failed: ${err?.message || err}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|