vibe-coding-master 0.7.15 → 0.7.16
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/README.md +18 -9
- package/dist/backend/runtime/session-registry.js +7 -0
- package/dist/backend/server.js +1 -4
- package/dist/backend/services/ccr-integration-service.js +70 -11
- package/dist/backend/services/claude-transcript-service.js +10 -10
- package/dist/backend/services/runtime-coordinator-service.js +37 -42
- package/dist/backend/services/session-service.js +49 -13
- package/package.json +1 -1
- package/dist/backend/adapters/claude-settings-adapter.js +0 -79
package/README.md
CHANGED
|
@@ -254,11 +254,17 @@ DevContainer, make the second endpoint reachable from the container. In the VCM
|
|
|
254
254
|
The key is stored in global VCM state (`~/.vcm/settings.json`) with owner-only
|
|
255
255
|
permissions and is never returned by the settings API. It is used for CCR
|
|
256
256
|
checks, model discovery, and the GPT-only `apiKeyHelper`. GPT sessions receive a
|
|
257
|
-
child-only `--settings` override
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
child-only `--settings` override and use the isolated Claude configuration root
|
|
258
|
+
`~/.vcm/claude/ccr`. VCM never edits `~/.claude/settings.json`. Native Claude
|
|
259
|
+
sessions keep their normal configuration and account authentication; VCM only
|
|
260
|
+
removes inherited environment variables that clearly point at the local CCR
|
|
261
|
+
gateway from the native child process. Configure CCR without enabling its
|
|
262
|
+
global Claude Code or Claude App takeover if those clients should remain on
|
|
263
|
+
Anthropic.
|
|
264
|
+
|
|
265
|
+
Resume keeps the provider recorded by the existing Session. Use Restart when
|
|
266
|
+
switching between a native Claude model and `GPT-5.6 Sol (CCR)`. If CCR is
|
|
267
|
+
disabled, unreachable, rejects the key, or does not expose
|
|
262
268
|
`Codex API/gpt-5.6-sol`, VCM blocks the new Start, Resume, or Restart and does
|
|
263
269
|
not fall back to another model.
|
|
264
270
|
|
|
@@ -324,7 +330,9 @@ Conversation translation is controlled from the sidebar `Translation` section.
|
|
|
324
330
|
|
|
325
331
|
VCM uses a task-scoped Translator role and Claude transcript JSONL files, not
|
|
326
332
|
raw terminal text. Translation memory and completed file translations remain
|
|
327
|
-
project-level durable data.
|
|
333
|
+
project-level durable data. When translation is enabled and the active task's
|
|
334
|
+
Harness is initialized, the backend automatically starts a fresh Translator for
|
|
335
|
+
the task or resumes its saved Session.
|
|
328
336
|
|
|
329
337
|
Common controls:
|
|
330
338
|
|
|
@@ -428,9 +436,10 @@ Use it to:
|
|
|
428
436
|
- merge task harness commits back to the connected repository branch when
|
|
429
437
|
appropriate
|
|
430
438
|
|
|
431
|
-
Harness Engineer is task-scoped and runs from the active task worktree.
|
|
432
|
-
|
|
433
|
-
the project harness
|
|
439
|
+
Harness Engineer is task-scoped and runs from the active task worktree. The
|
|
440
|
+
backend automatically starts a fresh Harness Engineer for each active task or
|
|
441
|
+
resumes its saved Session. Durable memory is versioned with the project harness
|
|
442
|
+
files.
|
|
434
443
|
|
|
435
444
|
### Auto Memory
|
|
436
445
|
|
|
@@ -2,6 +2,13 @@ export function createSessionRegistry() {
|
|
|
2
2
|
const sessions = new Map();
|
|
3
3
|
return {
|
|
4
4
|
upsert(session) {
|
|
5
|
+
for (const [sessionId, candidate] of sessions) {
|
|
6
|
+
if (sessionId !== session.id
|
|
7
|
+
&& candidate.taskSlug === session.taskSlug
|
|
8
|
+
&& candidate.role === session.role) {
|
|
9
|
+
sessions.delete(sessionId);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
5
12
|
sessions.set(session.id, session);
|
|
6
13
|
},
|
|
7
14
|
get(sessionId) {
|
package/dist/backend/server.js
CHANGED
|
@@ -5,7 +5,6 @@ import fastifyStatic from "@fastify/static";
|
|
|
5
5
|
import { createArtifactService } from "./services/artifact-service.js";
|
|
6
6
|
import { createClaudeAdapter } from "./adapters/claude-adapter.js";
|
|
7
7
|
import { createCcrGatewayAdapter } from "./adapters/ccr-gateway-adapter.js";
|
|
8
|
-
import { createClaudeSettingsAdapter } from "./adapters/claude-settings-adapter.js";
|
|
9
8
|
import { createCommandRunner } from "./adapters/command-runner.js";
|
|
10
9
|
import { createCommandDispatcher } from "./services/command-dispatcher.js";
|
|
11
10
|
import { createClaudeHookService } from "./services/claude-hook-service.js";
|
|
@@ -209,11 +208,9 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
209
208
|
const git = createGitAdapter(runner);
|
|
210
209
|
const claude = createClaudeAdapter(runner);
|
|
211
210
|
const appSettings = createAppSettingsService({ fs });
|
|
212
|
-
const claudeSettings = createClaudeSettingsAdapter({ fs });
|
|
213
211
|
const ccrIntegration = createCcrIntegrationService({
|
|
214
212
|
settings: appSettings,
|
|
215
|
-
gateway: createCcrGatewayAdapter()
|
|
216
|
-
restoreNativeClaudeSettings: () => claudeSettings.restoreNativeSettings()
|
|
213
|
+
gateway: createCcrGatewayAdapter()
|
|
217
214
|
});
|
|
218
215
|
const runtime = createNodePtyTerminalRuntime({ fs });
|
|
219
216
|
const registry = createSessionRegistry();
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { CCR_GATEWAY_BASE_URL, CCR_GPT_MODEL_ID, createSessionModelOptions, isCcrSessionModel } from "../../shared/types/session.js";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { VcmError } from "../errors.js";
|
|
5
|
+
import { resolveVcmDataDir } from "../vcm-data-dir.js";
|
|
4
6
|
const DEFAULT_CACHE_TTL_MS = 10_000;
|
|
5
7
|
const DEFAULT_API_KEY_HELPER_PATH = fileURLToPath(new URL("../../../scripts/ccr-api-key-helper.mjs", import.meta.url));
|
|
6
8
|
export function createCcrIntegrationService(deps) {
|
|
7
9
|
const now = deps.now ?? (() => new Date());
|
|
8
10
|
const cacheTtlMs = deps.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
11
|
+
const baseEnv = deps.baseEnv ?? process.env;
|
|
12
|
+
const configDir = deps.configDir ?? path.join(resolveVcmDataDir(baseEnv), "claude", "ccr");
|
|
9
13
|
let cachedProbe;
|
|
10
14
|
let inFlight;
|
|
11
15
|
async function probe(force = false) {
|
|
@@ -67,9 +71,6 @@ export function createCcrIntegrationService(deps) {
|
|
|
67
71
|
return {
|
|
68
72
|
async initialize() {
|
|
69
73
|
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
70
|
-
if (settings.apiKey) {
|
|
71
|
-
await deps.restoreNativeClaudeSettings?.();
|
|
72
|
-
}
|
|
73
74
|
if (settings.enabled) {
|
|
74
75
|
await probe(true);
|
|
75
76
|
}
|
|
@@ -102,9 +103,6 @@ export function createCcrIntegrationService(deps) {
|
|
|
102
103
|
enabled: nextEnabled,
|
|
103
104
|
apiKey: nextApiKey
|
|
104
105
|
});
|
|
105
|
-
if (nextApiKey) {
|
|
106
|
-
await deps.restoreNativeClaudeSettings?.();
|
|
107
|
-
}
|
|
108
106
|
if (clearApiKey || !nextEnabled) {
|
|
109
107
|
cachedProbe = undefined;
|
|
110
108
|
return buildStatus();
|
|
@@ -130,7 +128,7 @@ export function createCcrIntegrationService(deps) {
|
|
|
130
128
|
},
|
|
131
129
|
async getLaunchEnvironment(model) {
|
|
132
130
|
if (!isCcrSessionModel(model)) {
|
|
133
|
-
return
|
|
131
|
+
return buildNativeLaunchEnvironment(baseEnv);
|
|
134
132
|
}
|
|
135
133
|
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
136
134
|
if (!settings.enabled) {
|
|
@@ -164,15 +162,12 @@ export function createCcrIntegrationService(deps) {
|
|
|
164
162
|
CODEXL_CLAUDE_CODE_MODEL: CCR_GPT_MODEL_ID,
|
|
165
163
|
ANTHROPIC_SMALL_FAST_MODEL: CCR_GPT_MODEL_ID,
|
|
166
164
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
165
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
167
166
|
NO_PROXY: noProxy,
|
|
168
167
|
no_proxy: noProxy
|
|
169
168
|
};
|
|
170
169
|
},
|
|
171
170
|
async getLaunchSettingsOverride(model) {
|
|
172
|
-
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
173
|
-
if (settings.apiKey) {
|
|
174
|
-
await deps.restoreNativeClaudeSettings?.();
|
|
175
|
-
}
|
|
176
171
|
if (!isCcrSessionModel(model)) {
|
|
177
172
|
return undefined;
|
|
178
173
|
}
|
|
@@ -182,6 +177,70 @@ export function createCcrIntegrationService(deps) {
|
|
|
182
177
|
}
|
|
183
178
|
};
|
|
184
179
|
}
|
|
180
|
+
const CCR_BASE_URL_KEYS = [
|
|
181
|
+
"ANTHROPIC_BASE_URL",
|
|
182
|
+
"ANTHROPIC_API_BASE_URL",
|
|
183
|
+
"CLAUDE_AGENT_API_BASE_URL"
|
|
184
|
+
];
|
|
185
|
+
const CCR_MODEL_KEYS = [
|
|
186
|
+
"ANTHROPIC_MODEL",
|
|
187
|
+
"ANTHROPIC_SMALL_FAST_MODEL"
|
|
188
|
+
];
|
|
189
|
+
const CCR_ONLY_ENV_KEYS = [
|
|
190
|
+
"CCR_CLAUDE_CODE_MODEL",
|
|
191
|
+
"CODEXL_CLAUDE_CODE_MODEL",
|
|
192
|
+
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
|
|
193
|
+
];
|
|
194
|
+
export function buildNativeLaunchEnvironment(baseEnv) {
|
|
195
|
+
const environment = {};
|
|
196
|
+
let inheritedCcrTakeover = false;
|
|
197
|
+
if (baseEnv.CLAUDE_CONFIG_DIR?.trim()) {
|
|
198
|
+
environment.CLAUDE_CONFIG_DIR = baseEnv.CLAUDE_CONFIG_DIR.trim();
|
|
199
|
+
}
|
|
200
|
+
for (const key of CCR_BASE_URL_KEYS) {
|
|
201
|
+
if (isCcrGatewayUrl(baseEnv[key])) {
|
|
202
|
+
environment[key] = undefined;
|
|
203
|
+
inheritedCcrTakeover = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const key of CCR_MODEL_KEYS) {
|
|
207
|
+
if (isCcrModelName(baseEnv[key])) {
|
|
208
|
+
environment[key] = undefined;
|
|
209
|
+
inheritedCcrTakeover = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const key of CCR_ONLY_ENV_KEYS) {
|
|
213
|
+
if (baseEnv[key] !== undefined) {
|
|
214
|
+
environment[key] = undefined;
|
|
215
|
+
inheritedCcrTakeover = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (inheritedCcrTakeover) {
|
|
219
|
+
environment.ANTHROPIC_AUTH_TOKEN = undefined;
|
|
220
|
+
environment.ANTHROPIC_API_KEY = undefined;
|
|
221
|
+
}
|
|
222
|
+
return environment;
|
|
223
|
+
}
|
|
224
|
+
function isCcrGatewayUrl(value) {
|
|
225
|
+
if (!value) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const url = new URL(value);
|
|
230
|
+
return url.port === "3456"
|
|
231
|
+
&& ["127.0.0.1", "localhost", "host.docker.internal"].includes(url.hostname);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function isCcrModelName(value) {
|
|
238
|
+
if (!value) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
return value === CCR_GPT_MODEL_ID
|
|
242
|
+
|| value.startsWith("anthropic/claude-ccr-h");
|
|
243
|
+
}
|
|
185
244
|
function createStatus(input) {
|
|
186
245
|
const reason = input.error
|
|
187
246
|
?? (input.enabled ? "CCR GPT-5.6 Sol is unavailable." : "Enable CCR GPT models in Settings.");
|
|
@@ -208,14 +208,14 @@ export function resolveExistingClaudeTranscriptPath(session) {
|
|
|
208
208
|
if (sessionPath) {
|
|
209
209
|
return sessionPath;
|
|
210
210
|
}
|
|
211
|
-
const cwdPath = existingFile(claudeTranscriptPath(session.cwd, session.claudeSessionId));
|
|
211
|
+
const cwdPath = existingFile(claudeTranscriptPath(session.cwd, session.claudeSessionId, session.claudeConfigDir));
|
|
212
212
|
if (cwdPath) {
|
|
213
213
|
return cwdPath;
|
|
214
214
|
}
|
|
215
|
-
return findClaudeTranscriptPathBySessionId(session.claudeSessionId);
|
|
215
|
+
return findClaudeTranscriptPathBySessionId(session.claudeSessionId, session.claudeConfigDir);
|
|
216
216
|
}
|
|
217
|
-
export function findClaudeTranscriptPathBySessionId(claudeSessionId) {
|
|
218
|
-
const root = claudeProjectsRoot();
|
|
217
|
+
export function findClaudeTranscriptPathBySessionId(claudeSessionId, configDir) {
|
|
218
|
+
const root = claudeProjectsRoot(configDir);
|
|
219
219
|
let projectDirs;
|
|
220
220
|
try {
|
|
221
221
|
projectDirs = readdirSync(root, { withFileTypes: true });
|
|
@@ -253,17 +253,17 @@ function existingFile(candidate) {
|
|
|
253
253
|
return undefined;
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
-
export function claudeProjectsRoot() {
|
|
257
|
-
return join(homedir(), ".claude", "projects");
|
|
256
|
+
export function claudeProjectsRoot(configDir) {
|
|
257
|
+
return join(configDir ?? join(homedir(), ".claude"), "projects");
|
|
258
258
|
}
|
|
259
259
|
export function projectHash(projectDir) {
|
|
260
260
|
return projectDir.replace(/[\/\s]+/g, "-");
|
|
261
261
|
}
|
|
262
|
-
export function projectsTranscriptDir(projectDir) {
|
|
263
|
-
return join(claudeProjectsRoot(), projectHash(projectDir));
|
|
262
|
+
export function projectsTranscriptDir(projectDir, configDir) {
|
|
263
|
+
return join(claudeProjectsRoot(configDir), projectHash(projectDir));
|
|
264
264
|
}
|
|
265
|
-
export function claudeTranscriptPath(projectDir, claudeSessionId) {
|
|
266
|
-
return join(projectsTranscriptDir(projectDir), `${claudeSessionId}.jsonl`);
|
|
265
|
+
export function claudeTranscriptPath(projectDir, claudeSessionId, configDir) {
|
|
266
|
+
return join(projectsTranscriptDir(projectDir, configDir), `${claudeSessionId}.jsonl`);
|
|
267
267
|
}
|
|
268
268
|
export function parseAssistantContent(line) {
|
|
269
269
|
let obj;
|
|
@@ -34,6 +34,40 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
function reconcileProject(repoRoot, input = {}) {
|
|
38
|
+
return withRepoLock(repoRoot, async () => {
|
|
39
|
+
const [activeTask, gatewayStatus] = await Promise.all([
|
|
40
|
+
resolveActiveTask(repoRoot, input.taskSlug),
|
|
41
|
+
deps.gatewayService.getStatus().catch(() => null)
|
|
42
|
+
]);
|
|
43
|
+
const preferences = await deps.appSettings.getPreferences();
|
|
44
|
+
if (!activeTask) {
|
|
45
|
+
return { activeTask: null, gatewayStatus };
|
|
46
|
+
}
|
|
47
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
|
|
48
|
+
const stateRoot = await deps.getStateRoot(repoRoot);
|
|
49
|
+
await deps.turnReconciler.reconcileTask(repoRoot, activeTask, stateRoot);
|
|
50
|
+
const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
|
|
51
|
+
.then((status) => status.initialized)
|
|
52
|
+
.catch(() => false);
|
|
53
|
+
await Promise.all([
|
|
54
|
+
reconcileHarnessEngineer(repoRoot, activeTask),
|
|
55
|
+
reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized)
|
|
56
|
+
]);
|
|
57
|
+
if (preferences.translationEnabled && harnessInitialized) {
|
|
58
|
+
await startConversationTranslationListeners(repoRoot, activeTask);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
|
|
64
|
+
const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
|
|
65
|
+
if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
|
|
66
|
+
await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
|
|
67
|
+
}
|
|
68
|
+
return { activeTask, gatewayStatus };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
37
71
|
return {
|
|
38
72
|
start() {
|
|
39
73
|
if (reconcileTimer !== undefined) {
|
|
@@ -51,53 +85,14 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
51
85
|
clearTimer(reconcileTimer);
|
|
52
86
|
reconcileTimer = undefined;
|
|
53
87
|
},
|
|
54
|
-
reconcileProject
|
|
55
|
-
return withRepoLock(repoRoot, async () => {
|
|
56
|
-
const [activeTask, gatewayStatus] = await Promise.all([
|
|
57
|
-
resolveActiveTask(repoRoot, input.taskSlug),
|
|
58
|
-
deps.gatewayService.getStatus().catch(() => null)
|
|
59
|
-
]);
|
|
60
|
-
const preferences = await deps.appSettings.getPreferences();
|
|
61
|
-
if (!activeTask) {
|
|
62
|
-
return { activeTask: null, gatewayStatus };
|
|
63
|
-
}
|
|
64
|
-
const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
|
|
65
|
-
const stateRoot = await deps.getStateRoot(repoRoot);
|
|
66
|
-
await deps.turnReconciler.reconcileTask(repoRoot, activeTask, stateRoot);
|
|
67
|
-
const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
|
|
68
|
-
.then((status) => status.initialized)
|
|
69
|
-
.catch(() => false);
|
|
70
|
-
await Promise.all([
|
|
71
|
-
reconcileHarnessEngineer(repoRoot, activeTask),
|
|
72
|
-
reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized)
|
|
73
|
-
]);
|
|
74
|
-
if (preferences.translationEnabled && harnessInitialized) {
|
|
75
|
-
await startConversationTranslationListeners(repoRoot, activeTask);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
|
|
79
|
-
}
|
|
80
|
-
await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
|
|
81
|
-
const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
|
|
82
|
-
if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
|
|
83
|
-
await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
|
|
84
|
-
}
|
|
85
|
-
return { activeTask, gatewayStatus };
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
+
reconcileProject
|
|
88
89
|
};
|
|
89
90
|
async function reconcileCurrentProject() {
|
|
90
91
|
const project = await deps.projectService.getCurrentProject();
|
|
91
92
|
if (!project) {
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
|
-
await
|
|
95
|
-
const activeTask = await resolveActiveTask(project.repoRoot);
|
|
96
|
-
if (activeTask) {
|
|
97
|
-
await deps.turnReconciler.reconcileTask(project.repoRoot, activeTask, await deps.getStateRoot(project.repoRoot));
|
|
98
|
-
}
|
|
99
|
-
return { activeTask, gatewayStatus: null };
|
|
100
|
-
});
|
|
95
|
+
await reconcileProject(project.repoRoot);
|
|
101
96
|
}
|
|
102
97
|
async function resolveActiveTask(repoRoot, requestedTaskSlug) {
|
|
103
98
|
const tasks = await deps.taskService.listTasks(repoRoot);
|
|
@@ -136,7 +131,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
136
131
|
});
|
|
137
132
|
}
|
|
138
133
|
function shouldAutoEnsureTaskToolSession(session) {
|
|
139
|
-
return
|
|
134
|
+
return !session || session.status === "running" || Boolean(session.claudeSessionId);
|
|
140
135
|
}
|
|
141
136
|
async function ensureTaskToolRoleSession(repoRoot, taskSlug, role, input) {
|
|
142
137
|
const existing = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
|
|
@@ -55,6 +55,9 @@ export function createSessionService(deps) {
|
|
|
55
55
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
56
56
|
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
57
57
|
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
|
|
58
|
+
if (launchMode === "resume" && persisted) {
|
|
59
|
+
assertResumeProviderCompatible(persisted, model);
|
|
60
|
+
}
|
|
58
61
|
const [modelEnvironment, modelSettingsOverride] = await Promise.all([
|
|
59
62
|
getModelLaunchEnvironment(model),
|
|
60
63
|
getModelLaunchSettingsOverride(model)
|
|
@@ -74,7 +77,7 @@ export function createSessionService(deps) {
|
|
|
74
77
|
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
75
78
|
? persisted.transcriptPath
|
|
76
79
|
: resumeClaudeSessionId
|
|
77
|
-
? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId)
|
|
80
|
+
? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
78
81
|
: undefined;
|
|
79
82
|
const startCommand = {
|
|
80
83
|
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
|
|
@@ -113,6 +116,7 @@ export function createSessionService(deps) {
|
|
|
113
116
|
model,
|
|
114
117
|
effort,
|
|
115
118
|
cwd: startCommand.cwd,
|
|
119
|
+
claudeConfigDir: readClaudeConfigDir(modelEnvironment),
|
|
116
120
|
terminalBackend: "node-pty",
|
|
117
121
|
pid: runtimeSession.pid,
|
|
118
122
|
roleCommandPath: isDispatchableRole(role)
|
|
@@ -163,6 +167,9 @@ export function createSessionService(deps) {
|
|
|
163
167
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
164
168
|
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
165
169
|
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
|
|
170
|
+
if (launchMode === "resume" && persisted) {
|
|
171
|
+
assertResumeProviderCompatible(persisted, model);
|
|
172
|
+
}
|
|
166
173
|
const [modelEnvironment, modelSettingsOverride] = await Promise.all([
|
|
167
174
|
getModelLaunchEnvironment(model),
|
|
168
175
|
getModelLaunchSettingsOverride(model)
|
|
@@ -194,7 +201,7 @@ export function createSessionService(deps) {
|
|
|
194
201
|
const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
|
|
195
202
|
const claudeSessionId = resumeClaudeSessionId ?? "";
|
|
196
203
|
const transcriptPath = resumeClaudeSessionId
|
|
197
|
-
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
|
|
204
|
+
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
198
205
|
: undefined;
|
|
199
206
|
const startCommand = {
|
|
200
207
|
...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
|
|
@@ -236,6 +243,7 @@ export function createSessionService(deps) {
|
|
|
236
243
|
model,
|
|
237
244
|
effort,
|
|
238
245
|
cwd: sessionCwd,
|
|
246
|
+
claudeConfigDir: readClaudeConfigDir(modelEnvironment),
|
|
239
247
|
terminalBackend: "node-pty",
|
|
240
248
|
pid: runtimeSession.pid,
|
|
241
249
|
startedAt: runtimeSession.startedAt,
|
|
@@ -276,6 +284,9 @@ export function createSessionService(deps) {
|
|
|
276
284
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
277
285
|
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
278
286
|
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
|
|
287
|
+
if (launchMode === "resume" && persisted) {
|
|
288
|
+
assertResumeProviderCompatible(persisted, model);
|
|
289
|
+
}
|
|
279
290
|
const [modelEnvironment, modelSettingsOverride] = await Promise.all([
|
|
280
291
|
getModelLaunchEnvironment(model),
|
|
281
292
|
getModelLaunchSettingsOverride(model)
|
|
@@ -303,7 +314,7 @@ export function createSessionService(deps) {
|
|
|
303
314
|
const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
|
|
304
315
|
const claudeSessionId = resumeClaudeSessionId ?? "";
|
|
305
316
|
const transcriptPath = resumeClaudeSessionId
|
|
306
|
-
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
|
|
317
|
+
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
307
318
|
: undefined;
|
|
308
319
|
const startCommand = {
|
|
309
320
|
...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
|
|
@@ -345,6 +356,7 @@ export function createSessionService(deps) {
|
|
|
345
356
|
model,
|
|
346
357
|
effort,
|
|
347
358
|
cwd: sessionCwd,
|
|
359
|
+
claudeConfigDir: readClaudeConfigDir(modelEnvironment),
|
|
348
360
|
terminalBackend: "node-pty",
|
|
349
361
|
pid: runtimeSession.pid,
|
|
350
362
|
startedAt: runtimeSession.startedAt,
|
|
@@ -489,6 +501,7 @@ export function createSessionService(deps) {
|
|
|
489
501
|
const permissionMode = normalizeClaudePermissionMode(session.permissionMode);
|
|
490
502
|
const model = normalizeClaudeModel(session.model);
|
|
491
503
|
const effort = normalizeClaudeEffort(session.effort);
|
|
504
|
+
assertResumeProviderCompatible(session, model);
|
|
492
505
|
const [modelEnvironment, modelSettingsOverride] = await Promise.all([
|
|
493
506
|
getModelLaunchEnvironment(model),
|
|
494
507
|
getModelLaunchSettingsOverride(model)
|
|
@@ -546,13 +559,14 @@ export function createSessionService(deps) {
|
|
|
546
559
|
permissionMode,
|
|
547
560
|
model,
|
|
548
561
|
effort,
|
|
562
|
+
claudeConfigDir: readClaudeConfigDir(modelEnvironment),
|
|
549
563
|
pid: runtimeSession.pid,
|
|
550
564
|
startedAt: runtimeSession.startedAt,
|
|
551
565
|
updatedAt: timestamp,
|
|
552
566
|
lastOutputAt: runtimeSession.lastOutputAt,
|
|
553
567
|
exitCode: runtimeSession.exitCode,
|
|
554
568
|
transcriptPath: session.claudeSessionId
|
|
555
|
-
? claudeTranscriptPath(repoRoot, session.claudeSessionId)
|
|
569
|
+
? claudeTranscriptPath(repoRoot, session.claudeSessionId, session.claudeConfigDir)
|
|
556
570
|
: session.transcriptPath
|
|
557
571
|
};
|
|
558
572
|
deps.registry.upsert(normalizeProjectScopedRecordForPersistence(resumed));
|
|
@@ -560,16 +574,16 @@ export function createSessionService(deps) {
|
|
|
560
574
|
return migrateRunningProjectToolSessionCwd(repoRoot, resumed, targetCwd);
|
|
561
575
|
}
|
|
562
576
|
async function getModelLaunchEnvironment(model) {
|
|
563
|
-
if (!isCcrSessionModel(model)) {
|
|
564
|
-
return {};
|
|
565
|
-
}
|
|
566
577
|
if (!deps.ccrIntegration) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
578
|
+
if (isCcrSessionModel(model)) {
|
|
579
|
+
throw new VcmError({
|
|
580
|
+
code: "CCR_UNAVAILABLE",
|
|
581
|
+
message: "CCR integration is not available in this VCM runtime.",
|
|
582
|
+
statusCode: 409,
|
|
583
|
+
hint: "Enable and configure CCR GPT models before starting this session."
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return {};
|
|
573
587
|
}
|
|
574
588
|
return deps.ccrIntegration.getLaunchEnvironment(model);
|
|
575
589
|
}
|
|
@@ -1490,6 +1504,28 @@ function normalizeClaudeEffort(value) {
|
|
|
1490
1504
|
}
|
|
1491
1505
|
return "default";
|
|
1492
1506
|
}
|
|
1507
|
+
function assertResumeProviderCompatible(session, requestedModel) {
|
|
1508
|
+
const persistedModel = normalizeClaudeModel(session.model);
|
|
1509
|
+
if (isCcrSessionModel(persistedModel) !== isCcrSessionModel(requestedModel)) {
|
|
1510
|
+
throw new VcmError({
|
|
1511
|
+
code: "SESSION_PROVIDER_SWITCH_REQUIRES_RESTART",
|
|
1512
|
+
message: `Cannot resume ${session.role} with a different model provider.`,
|
|
1513
|
+
statusCode: 409,
|
|
1514
|
+
hint: "Use Restart to switch between native Claude and CCR models."
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
if (isCcrSessionModel(requestedModel) && !session.claudeConfigDir) {
|
|
1518
|
+
throw new VcmError({
|
|
1519
|
+
code: "CCR_SESSION_CONFIG_MISSING",
|
|
1520
|
+
message: `${session.role} was created before isolated CCR session storage was enabled.`,
|
|
1521
|
+
statusCode: 409,
|
|
1522
|
+
hint: "Restart this role once to create an isolated CCR session."
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function readClaudeConfigDir(environment) {
|
|
1527
|
+
return environment.CLAUDE_CONFIG_DIR?.trim() || undefined;
|
|
1528
|
+
}
|
|
1493
1529
|
function formatClaudeCdCommand(targetCwd) {
|
|
1494
1530
|
// Claude Code's `/cd` slash command takes the literal remainder of the line as
|
|
1495
1531
|
// the path, so the target must NOT be wrapped in quotes (quotes are taken as part
|
package/package.json
CHANGED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
const CCR_ENV_KEYS = [
|
|
4
|
-
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
|
|
5
|
-
"ANTHROPIC_BASE_URL",
|
|
6
|
-
"ANTHROPIC_API_BASE_URL",
|
|
7
|
-
"CLAUDE_AGENT_API_BASE_URL",
|
|
8
|
-
"ANTHROPIC_MODEL",
|
|
9
|
-
"CCR_CLAUDE_CODE_MODEL",
|
|
10
|
-
"CODEXL_CLAUDE_CODE_MODEL",
|
|
11
|
-
"ANTHROPIC_SMALL_FAST_MODEL"
|
|
12
|
-
];
|
|
13
|
-
export function createClaudeSettingsAdapter(deps) {
|
|
14
|
-
const settingsPath = deps.settingsPath ?? resolveClaudeSettingsPath(deps.env ?? process.env);
|
|
15
|
-
return {
|
|
16
|
-
async restoreNativeSettings() {
|
|
17
|
-
if (!(await deps.fs.pathExists(settingsPath))) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
const current = await deps.fs.readJson(settingsPath);
|
|
21
|
-
const restored = restoreNativeClaudeSettings(current);
|
|
22
|
-
if (!restored.changed) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
await deps.fs.writeJsonAtomic(settingsPath, restored.settings);
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
export function restoreNativeClaudeSettings(value) {
|
|
31
|
-
const settings = isObject(value) ? structuredClone(value) : {};
|
|
32
|
-
const helperIsCcr = typeof settings.apiKeyHelper === "string"
|
|
33
|
-
&& settings.apiKeyHelper.includes(".claude-code-router");
|
|
34
|
-
const env = isObject(settings.env) ? settings.env : undefined;
|
|
35
|
-
const envUsesCcr = env
|
|
36
|
-
? ["ANTHROPIC_BASE_URL", "ANTHROPIC_API_BASE_URL", "CLAUDE_AGENT_API_BASE_URL"]
|
|
37
|
-
.some((key) => isCcrGatewayUrl(env[key]))
|
|
38
|
-
: false;
|
|
39
|
-
if (!helperIsCcr && !envUsesCcr) {
|
|
40
|
-
return { changed: false, settings };
|
|
41
|
-
}
|
|
42
|
-
let changed = false;
|
|
43
|
-
if (helperIsCcr) {
|
|
44
|
-
delete settings.apiKeyHelper;
|
|
45
|
-
changed = true;
|
|
46
|
-
}
|
|
47
|
-
if (env) {
|
|
48
|
-
for (const key of CCR_ENV_KEYS) {
|
|
49
|
-
if (key in env) {
|
|
50
|
-
delete env[key];
|
|
51
|
-
changed = true;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (Object.keys(env).length === 0) {
|
|
55
|
-
delete settings.env;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return { changed, settings };
|
|
59
|
-
}
|
|
60
|
-
function resolveClaudeSettingsPath(env) {
|
|
61
|
-
const configDir = env.CLAUDE_CONFIG_DIR?.trim();
|
|
62
|
-
return path.join(configDir ? path.resolve(configDir) : path.join(homedir(), ".claude"), "settings.json");
|
|
63
|
-
}
|
|
64
|
-
function isCcrGatewayUrl(value) {
|
|
65
|
-
if (typeof value !== "string") {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
const url = new URL(value);
|
|
70
|
-
return url.port === "3456"
|
|
71
|
-
&& ["127.0.0.1", "localhost", "host.docker.internal"].includes(url.hostname);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function isObject(value) {
|
|
78
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
-
}
|