llm-cli-gateway 1.14.0 → 1.15.0
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 +225 -46
- package/dist/async-job-manager.js +9 -3
- package/dist/index.d.ts +101 -0
- package/dist/index.js +311 -26
- package/dist/session-manager.d.ts +20 -2
- package/dist/session-manager.js +28 -3
- package/dist/worktree-manager.d.ts +41 -0
- package/dist/worktree-manager.js +214 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { parseGeminiJson, parseGeminiStreamJson } from "./gemini-json-parser.js"
|
|
|
13
13
|
import { parseVibeMetaJson } from "./mistral-meta-json-parser.js";
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { createSessionManager } from "./session-manager.js";
|
|
16
|
+
import { createWorktree, createWorktreeSessionCleanupHook, } from "./worktree-manager.js";
|
|
16
17
|
import { ResourceProvider } from "./resources.js";
|
|
17
18
|
import { PerformanceMetrics } from "./metrics.js";
|
|
18
19
|
import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
|
|
@@ -246,6 +247,50 @@ export const MAX_TURNS_SCHEMA = z.number().int().positive().safe().max(10_000);
|
|
|
246
247
|
// upstream CLIs would reject. 1µUSD per request is fine-grained enough
|
|
247
248
|
// for any plausible budget-cap use.
|
|
248
249
|
export const MAX_PRICE_SCHEMA = z.number().positive().finite().min(1e-6).max(10_000);
|
|
250
|
+
/**
|
|
251
|
+
* Slice λ: shared worktree directive for all 10 `*_request` / `*_request_async`
|
|
252
|
+
* tools. `true` creates a fresh worktree under `<repoRoot>/.worktrees/<uuid>`
|
|
253
|
+
* branched from HEAD. `{ name?, ref? }` lets the caller supply a sanitized
|
|
254
|
+
* name and/or git ref (default ref: HEAD).
|
|
255
|
+
*
|
|
256
|
+
* Lifecycle is gateway-owned: the gateway pre-creates the worktree via
|
|
257
|
+
* `git worktree add`, then spawns the child CLI with `cwd: <worktree-path>`.
|
|
258
|
+
* No `-w` / `--worktree` flag is ever emitted to the underlying CLI. When
|
|
259
|
+
* the request carries a sessionId and the session already has a worktree,
|
|
260
|
+
* that worktree is reused. On session_delete or TTL eviction the gateway
|
|
261
|
+
* runs `git worktree remove --force`.
|
|
262
|
+
*
|
|
263
|
+
* Tool response: when a worktree was used, the successful response stdout
|
|
264
|
+
* is prefixed with `[gateway] worktree=<absolute-path>\n` so callers can
|
|
265
|
+
* parse/use the path without a schema change (slice λ §1.d).
|
|
266
|
+
*
|
|
267
|
+
* NOTE: callers should `.gitignore` the `.worktrees/` directory in their
|
|
268
|
+
* repo (the gateway does NOT auto-gitignore — see slice λ spec Q4).
|
|
269
|
+
*/
|
|
270
|
+
export const WORKTREE_SCHEMA = z
|
|
271
|
+
.union([
|
|
272
|
+
z.boolean(),
|
|
273
|
+
z
|
|
274
|
+
.object({
|
|
275
|
+
name: z.string().min(1).max(64).optional(),
|
|
276
|
+
ref: z.string().min(1).max(255).optional(),
|
|
277
|
+
})
|
|
278
|
+
.strict(),
|
|
279
|
+
])
|
|
280
|
+
.describe("Slice λ: run this request inside a dedicated git worktree owned by " +
|
|
281
|
+
"the gateway. `true` creates a fresh worktree at " +
|
|
282
|
+
"`<repoRoot>/.worktrees/<uuid>` branched from HEAD. " +
|
|
283
|
+
"`{ name?, ref? }` lets the caller supply a sanitized name and/or a " +
|
|
284
|
+
"git ref (default: HEAD). When the request carries a sessionId and " +
|
|
285
|
+
"the session already has a worktree, that worktree is reused. The " +
|
|
286
|
+
"gateway spawns the child CLI with `cwd: <worktree-path>` — no " +
|
|
287
|
+
"`-w`/`--worktree` flag is ever emitted to the underlying CLI. On " +
|
|
288
|
+
"session_delete or TTL eviction the gateway runs `git worktree " +
|
|
289
|
+
"remove --force`. Successful responses are prefixed with " +
|
|
290
|
+
"`[gateway] worktree=<absolute-path>\\n` so callers can use the " +
|
|
291
|
+
"path. NOTE: callers should `.gitignore` the `.worktrees/` " +
|
|
292
|
+
"directory in their repo (the gateway does NOT auto-gitignore — " +
|
|
293
|
+
"see slice λ spec Q4).");
|
|
249
294
|
// U22: Session-provider enum extended to five providers. The storage layer's
|
|
250
295
|
// CLI_TYPES already includes "mistral"; the MCP-tool layer mirrors that here so
|
|
251
296
|
// session_create / session_list / session_clear_all accept the fifth provider.
|
|
@@ -323,7 +368,17 @@ flightRecorderEntry, extractUsage,
|
|
|
323
368
|
* through both the direct-execute fallback (SYNC_DEADLINE_MS===0) and
|
|
324
369
|
* the AsyncJobManager spawn path, and participates in the dedup key.
|
|
325
370
|
*/
|
|
326
|
-
stdin
|
|
371
|
+
stdin,
|
|
372
|
+
/**
|
|
373
|
+
* Slice λ: optional working directory for the spawned child process,
|
|
374
|
+
* derived from a gateway-owned git worktree. Threaded to both the
|
|
375
|
+
* direct-execute fallback (`executeCli({ cwd })`) and the
|
|
376
|
+
* AsyncJobManager dedup-aware spawn path
|
|
377
|
+
* (`startJobWithDedup({ cwd })`). `cwd` also participates in the
|
|
378
|
+
* dedup key (see async-job-manager.buildRequestKey) so two requests
|
|
379
|
+
* with identical argv in different worktrees do not collide.
|
|
380
|
+
*/
|
|
381
|
+
cwd) {
|
|
327
382
|
// U26 fix: ownership of onComplete is a contract. Once this function returns
|
|
328
383
|
// OR throws, the caller MUST consider onComplete consumed — i.e. it has
|
|
329
384
|
// either been run, or the AsyncJobManager has taken ownership of it. The
|
|
@@ -358,6 +413,7 @@ stdin) {
|
|
|
358
413
|
logger: runtime.logger,
|
|
359
414
|
env: env ? { ...process.env, ...env } : undefined,
|
|
360
415
|
stdin,
|
|
416
|
+
cwd,
|
|
361
417
|
});
|
|
362
418
|
}
|
|
363
419
|
finally {
|
|
@@ -369,6 +425,7 @@ stdin) {
|
|
|
369
425
|
let outcome;
|
|
370
426
|
try {
|
|
371
427
|
outcome = runtime.asyncJobManager.startJobWithDedup(cli, args, corrId, {
|
|
428
|
+
cwd,
|
|
372
429
|
idleTimeoutMs,
|
|
373
430
|
outputFormat,
|
|
374
431
|
forceRefresh,
|
|
@@ -455,6 +512,73 @@ function buildDeferredToolResponse(deferred, sessionId) {
|
|
|
455
512
|
],
|
|
456
513
|
};
|
|
457
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Slice λ: resolve a request's worktree directive into a spawn cwd.
|
|
517
|
+
*
|
|
518
|
+
* - `worktreeOpt` is the Zod-validated input value (boolean |
|
|
519
|
+
* `{ name?, ref? }` | undefined).
|
|
520
|
+
* - When the request has a session AND the session already has a
|
|
521
|
+
* `metadata.worktreePath`, that path is reused (resume semantics).
|
|
522
|
+
* The reused path is returned without touching git; if the directory
|
|
523
|
+
* was externally removed between requests, the next CLI invocation
|
|
524
|
+
* will surface the error naturally.
|
|
525
|
+
* - When no reusable worktree exists, `createWorktree` runs; on success
|
|
526
|
+
* the new path is written to `session.metadata` (only when a session
|
|
527
|
+
* exists — request-scoped worktrees do NOT persist).
|
|
528
|
+
* - Returns `{}` when `worktreeOpt` is undefined/false (preserves
|
|
529
|
+
* pre-λ behaviour at non-worktree call sites).
|
|
530
|
+
* - Errors propagate as `WorktreeError`/`Error`; the caller wraps them
|
|
531
|
+
* in a `createErrorResponse` envelope. Do NOT swallow.
|
|
532
|
+
*
|
|
533
|
+
* Spec: docs/plans/slice-lambda.spec.md §"Implementation surface to
|
|
534
|
+
* verify" §5.
|
|
535
|
+
*/
|
|
536
|
+
export async function resolveWorktreeForRequest(worktreeOpt, sessionId, runtime) {
|
|
537
|
+
if (!worktreeOpt)
|
|
538
|
+
return {};
|
|
539
|
+
const sessionManager = runtime.sessionManager;
|
|
540
|
+
if (sessionId) {
|
|
541
|
+
const session = await Promise.resolve(sessionManager.getSession(sessionId));
|
|
542
|
+
const existingPath = session?.metadata?.worktreePath;
|
|
543
|
+
if (typeof existingPath === "string" && existingPath.length > 0) {
|
|
544
|
+
return { cwd: existingPath, worktreePath: existingPath };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const name = worktreeOpt === true ? undefined : worktreeOpt.name;
|
|
548
|
+
const ref = worktreeOpt === true ? undefined : worktreeOpt.ref;
|
|
549
|
+
const repoRoot = process.cwd();
|
|
550
|
+
const handle = await createWorktree({
|
|
551
|
+
repoRoot,
|
|
552
|
+
name,
|
|
553
|
+
ref,
|
|
554
|
+
logger: runtime.logger,
|
|
555
|
+
});
|
|
556
|
+
if (sessionId) {
|
|
557
|
+
await Promise.resolve(sessionManager.updateSessionMetadata(sessionId, {
|
|
558
|
+
worktreePath: handle.path,
|
|
559
|
+
worktreeName: handle.name,
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
return { cwd: handle.path, worktreePath: handle.path };
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Slice λ §1.d: response-envelope shape decision for `worktreePath`.
|
|
566
|
+
*
|
|
567
|
+
* We surface the worktree path inline as a stdout prefix
|
|
568
|
+
* (`[gateway] worktree=<absolute-path>\n`) rather than as a
|
|
569
|
+
* structuredContent field or JSON wrapper. Rationale:
|
|
570
|
+
* - zero schema change across all 10 tools and their downstream parsers
|
|
571
|
+
* - matches how other slice features (session warnings, cache_state
|
|
572
|
+
* aggregates) surface side-channel metadata today
|
|
573
|
+
* - callers that want the path can split on the first newline; callers
|
|
574
|
+
* that don't care see a single ignorable header line
|
|
575
|
+
*
|
|
576
|
+
* Use `formatWorktreePrefix(resolution.worktreePath)` once per tool, at
|
|
577
|
+
* the moment a successful response is constructed.
|
|
578
|
+
*/
|
|
579
|
+
export function formatWorktreePrefix(worktreePath) {
|
|
580
|
+
return worktreePath ? `[gateway] worktree=${worktreePath}\n` : "";
|
|
581
|
+
}
|
|
458
582
|
// Helper function for standardized error responses
|
|
459
583
|
function createErrorResponse(cli, code, stderr, correlationId, error) {
|
|
460
584
|
let errorMessage = `Error executing ${cli} CLI`;
|
|
@@ -1856,8 +1980,15 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
1856
1980
|
args.push(...sessionPlan.args);
|
|
1857
1981
|
const userProvidedSession = sessionPlan.resumed;
|
|
1858
1982
|
const effectiveSessionIdHint = sessionPlan.resumed ? params.sessionId : undefined;
|
|
1983
|
+
let worktreeResolution = {};
|
|
1984
|
+
try {
|
|
1985
|
+
worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionIdHint, runtime);
|
|
1986
|
+
}
|
|
1987
|
+
catch (err) {
|
|
1988
|
+
return createErrorResponse("gemini_request", 1, "", corrId, err);
|
|
1989
|
+
}
|
|
1859
1990
|
const geminiFrHandoff = buildAsyncFlightRecorderHandoff("gemini", prep, params.sessionId, params.outputFormat);
|
|
1860
|
-
const result = await awaitJobOrDefer("gemini", args, corrId, resolveIdleTimeout("gemini", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, geminiFrHandoff.flightRecorderEntry, geminiFrHandoff.extractUsage);
|
|
1991
|
+
const result = await awaitJobOrDefer("gemini", args, corrId, resolveIdleTimeout("gemini", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, geminiFrHandoff.flightRecorderEntry, geminiFrHandoff.extractUsage, worktreeResolution.cwd);
|
|
1861
1992
|
// Deferred — job still running, return async reference
|
|
1862
1993
|
if (isDeferredResponse(result)) {
|
|
1863
1994
|
return buildDeferredToolResponse(result, effectiveSessionIdHint);
|
|
@@ -1899,6 +2030,12 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
1899
2030
|
}
|
|
1900
2031
|
deps.logger.info(`[${corrId}] gemini_request completed successfully in ${durationMs}ms`);
|
|
1901
2032
|
const response = buildCliResponse("gemini", stdout, params.optimizeResponse ?? false, corrId, effectiveSessionId, prep, durationMs, userProvidedSession, params.outputFormat);
|
|
2033
|
+
if (worktreeResolution.worktreePath) {
|
|
2034
|
+
const first = response.content[0];
|
|
2035
|
+
if (first && first.type === "text") {
|
|
2036
|
+
first.text = formatWorktreePrefix(worktreeResolution.worktreePath) + first.text;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
1902
2039
|
const geminiUsage = extractUsageAndCost("gemini", stdout, params.outputFormat);
|
|
1903
2040
|
safeFlightComplete(corrId, {
|
|
1904
2041
|
response: stdout,
|
|
@@ -1986,6 +2123,13 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
1986
2123
|
}
|
|
1987
2124
|
await deps.sessionManager.updateSessionUsage(effectiveSessionId);
|
|
1988
2125
|
}
|
|
2126
|
+
let worktreeResolution = {};
|
|
2127
|
+
try {
|
|
2128
|
+
worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
|
|
2129
|
+
}
|
|
2130
|
+
catch (err) {
|
|
2131
|
+
return createErrorResponse("gemini_request_async", 1, "", corrId, err);
|
|
2132
|
+
}
|
|
1989
2133
|
// Start job only after all session I/O succeeds. U23: forward outputFormat
|
|
1990
2134
|
// so AsyncJobManager records it in the durable store (the manager also
|
|
1991
2135
|
// surfaces it in the snapshot).
|
|
@@ -1994,7 +2138,7 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
1994
2138
|
// Slice 1.5: pure async path — no upstream safeFlightStart, so the
|
|
1995
2139
|
// manager owns both logStart and logComplete for this corrId.
|
|
1996
2140
|
const geminiAsyncFrHandoff = buildAsyncFlightRecorderHandoff("gemini", prep, effectiveSessionId, params.outputFormat);
|
|
1997
|
-
const job = deps.asyncJobManager.startJob("gemini", args, corrId,
|
|
2141
|
+
const job = deps.asyncJobManager.startJob("gemini", args, corrId, worktreeResolution.cwd, resolveIdleTimeout("gemini", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, undefined, geminiAsyncFrHandoff.flightRecorderEntry, geminiAsyncFrHandoff.extractUsage, true);
|
|
1998
2142
|
deps.logger.info(`[${corrId}] gemini_request_async started job ${job.id}`);
|
|
1999
2143
|
const asyncResponse = {
|
|
2000
2144
|
success: true,
|
|
@@ -2007,6 +2151,9 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
2007
2151
|
if (prep.reviewIntegrity && prep.reviewIntegrity.violations.length > 0) {
|
|
2008
2152
|
asyncResponse.reviewIntegrity = prep.reviewIntegrity;
|
|
2009
2153
|
}
|
|
2154
|
+
if (worktreeResolution.worktreePath) {
|
|
2155
|
+
asyncResponse.worktreePath = worktreeResolution.worktreePath;
|
|
2156
|
+
}
|
|
2010
2157
|
return {
|
|
2011
2158
|
content: [
|
|
2012
2159
|
{
|
|
@@ -2071,8 +2218,15 @@ export async function handleGrokRequest(deps, params) {
|
|
|
2071
2218
|
createNewSession: params.createNewSession,
|
|
2072
2219
|
});
|
|
2073
2220
|
args.push(...sessionResult.resumeArgs);
|
|
2221
|
+
let worktreeResolution = {};
|
|
2222
|
+
try {
|
|
2223
|
+
worktreeResolution = await resolveWorktreeForRequest(params.worktree, sessionResult.effectiveSessionId, runtime);
|
|
2224
|
+
}
|
|
2225
|
+
catch (err) {
|
|
2226
|
+
return createErrorResponse("grok_request", 1, "", corrId, err);
|
|
2227
|
+
}
|
|
2074
2228
|
const grokFrHandoff = buildAsyncFlightRecorderHandoff("grok", prep, params.sessionId, params.outputFormat);
|
|
2075
|
-
const result = await awaitJobOrDefer("grok", args, corrId, resolveIdleTimeout("grok", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, grokFrHandoff.flightRecorderEntry, grokFrHandoff.extractUsage);
|
|
2229
|
+
const result = await awaitJobOrDefer("grok", args, corrId, resolveIdleTimeout("grok", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, grokFrHandoff.flightRecorderEntry, grokFrHandoff.extractUsage, undefined, worktreeResolution.cwd);
|
|
2076
2230
|
// Deferred — job still running, return async reference
|
|
2077
2231
|
if (isDeferredResponse(result)) {
|
|
2078
2232
|
return buildDeferredToolResponse(result, sessionResult.effectiveSessionId);
|
|
@@ -2116,6 +2270,12 @@ export async function handleGrokRequest(deps, params) {
|
|
|
2116
2270
|
}
|
|
2117
2271
|
deps.logger.info(`[${corrId}] grok_request completed successfully in ${durationMs}ms`);
|
|
2118
2272
|
const response = buildCliResponse("grok", stdout, params.optimizeResponse ?? false, corrId, effectiveSessionId, prep, durationMs, sessionResult.userProvidedSession, params.outputFormat);
|
|
2273
|
+
if (worktreeResolution.worktreePath) {
|
|
2274
|
+
const first = response.content[0];
|
|
2275
|
+
if (first && first.type === "text") {
|
|
2276
|
+
first.text = formatWorktreePrefix(worktreeResolution.worktreePath) + first.text;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2119
2279
|
safeFlightComplete(corrId, {
|
|
2120
2280
|
response: stdout,
|
|
2121
2281
|
durationMs,
|
|
@@ -2206,11 +2366,18 @@ export async function handleGrokRequestAsync(deps, params) {
|
|
|
2206
2366
|
const newSession = await deps.sessionManager.createSession("grok", "Grok Session", `${GATEWAY_SESSION_PREFIX}${randomUUID()}`);
|
|
2207
2367
|
effectiveSessionId = newSession.id;
|
|
2208
2368
|
}
|
|
2369
|
+
let worktreeResolution = {};
|
|
2370
|
+
try {
|
|
2371
|
+
worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
|
|
2372
|
+
}
|
|
2373
|
+
catch (err) {
|
|
2374
|
+
return createErrorResponse("grok_request_async", 1, "", corrId, err);
|
|
2375
|
+
}
|
|
2209
2376
|
// Start job only after all session I/O succeeds
|
|
2210
2377
|
assertUpstreamCliArgs("grok", args);
|
|
2211
2378
|
assertUpstreamCliEnv("grok", undefined);
|
|
2212
2379
|
const grokAsyncFrHandoff = buildAsyncFlightRecorderHandoff("grok", prep, effectiveSessionId, params.outputFormat);
|
|
2213
|
-
const job = deps.asyncJobManager.startJob("grok", args, corrId,
|
|
2380
|
+
const job = deps.asyncJobManager.startJob("grok", args, corrId, worktreeResolution.cwd, resolveIdleTimeout("grok", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, undefined, grokAsyncFrHandoff.flightRecorderEntry, grokAsyncFrHandoff.extractUsage, true);
|
|
2214
2381
|
deps.logger.info(`[${corrId}] grok_request_async started job ${job.id}`);
|
|
2215
2382
|
const asyncResponse = {
|
|
2216
2383
|
success: true,
|
|
@@ -2223,6 +2390,9 @@ export async function handleGrokRequestAsync(deps, params) {
|
|
|
2223
2390
|
if (prep.reviewIntegrity && prep.reviewIntegrity.violations.length > 0) {
|
|
2224
2391
|
asyncResponse.reviewIntegrity = prep.reviewIntegrity;
|
|
2225
2392
|
}
|
|
2393
|
+
if (worktreeResolution.worktreePath) {
|
|
2394
|
+
asyncResponse.worktreePath = worktreeResolution.worktreePath;
|
|
2395
|
+
}
|
|
2226
2396
|
return {
|
|
2227
2397
|
content: [
|
|
2228
2398
|
{
|
|
@@ -2283,8 +2453,15 @@ export async function handleMistralRequest(deps, params) {
|
|
|
2283
2453
|
createNewSession: params.createNewSession,
|
|
2284
2454
|
});
|
|
2285
2455
|
args.push(...sessionResult.resumeArgs);
|
|
2456
|
+
let worktreeResolution = {};
|
|
2457
|
+
try {
|
|
2458
|
+
worktreeResolution = await resolveWorktreeForRequest(params.worktree, sessionResult.effectiveSessionId, runtime);
|
|
2459
|
+
}
|
|
2460
|
+
catch (err) {
|
|
2461
|
+
return createErrorResponse("mistral_request", 1, "", corrId, err);
|
|
2462
|
+
}
|
|
2286
2463
|
const mistralFrHandoff = buildAsyncFlightRecorderHandoff("mistral", prep, params.sessionId, params.outputFormat);
|
|
2287
|
-
let result = await awaitJobOrDefer("mistral", args, corrId, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, mistralEnv, undefined, mistralFrHandoff.flightRecorderEntry, mistralFrHandoff.extractUsage);
|
|
2464
|
+
let result = await awaitJobOrDefer("mistral", args, corrId, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, mistralEnv, undefined, mistralFrHandoff.flightRecorderEntry, mistralFrHandoff.extractUsage, undefined, worktreeResolution.cwd);
|
|
2288
2465
|
if (isDeferredResponse(result)) {
|
|
2289
2466
|
return buildDeferredToolResponse(result, sessionResult.effectiveSessionId);
|
|
2290
2467
|
}
|
|
@@ -2296,7 +2473,7 @@ export async function handleMistralRequest(deps, params) {
|
|
|
2296
2473
|
const retryArgs = [...retryPrep.args, ...sessionResult.resumeArgs];
|
|
2297
2474
|
// Reuse the FR handoff built above — the retry preserves corrId,
|
|
2298
2475
|
// so the manager's logComplete still updates the original row.
|
|
2299
|
-
result = await awaitJobOrDefer("mistral", retryArgs, corrId, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, true, runtime, retryPrep.env, undefined, mistralFrHandoff.flightRecorderEntry, mistralFrHandoff.extractUsage);
|
|
2476
|
+
result = await awaitJobOrDefer("mistral", retryArgs, corrId, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, true, runtime, retryPrep.env, undefined, mistralFrHandoff.flightRecorderEntry, mistralFrHandoff.extractUsage, undefined, worktreeResolution.cwd);
|
|
2300
2477
|
if (isDeferredResponse(result)) {
|
|
2301
2478
|
return buildDeferredToolResponse(result, sessionResult.effectiveSessionId);
|
|
2302
2479
|
}
|
|
@@ -2342,6 +2519,12 @@ export async function handleMistralRequest(deps, params) {
|
|
|
2342
2519
|
}
|
|
2343
2520
|
deps.logger.info(`[${corrId}] mistral_request completed successfully in ${durationMs}ms`);
|
|
2344
2521
|
const response = buildCliResponse("mistral", stdout, params.optimizeResponse ?? false, corrId, effectiveSessionId, prep, durationMs, sessionResult.userProvidedSession, params.outputFormat);
|
|
2522
|
+
if (worktreeResolution.worktreePath) {
|
|
2523
|
+
const first = response.content[0];
|
|
2524
|
+
if (first && first.type === "text") {
|
|
2525
|
+
first.text = formatWorktreePrefix(worktreeResolution.worktreePath) + first.text;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2345
2528
|
safeFlightComplete(corrId, {
|
|
2346
2529
|
response: stdout,
|
|
2347
2530
|
durationMs,
|
|
@@ -2427,10 +2610,17 @@ export async function handleMistralRequestAsync(deps, params) {
|
|
|
2427
2610
|
const newSession = await deps.sessionManager.createSession("mistral", "Mistral Session", `${GATEWAY_SESSION_PREFIX}${randomUUID()}`);
|
|
2428
2611
|
effectiveSessionId = newSession.id;
|
|
2429
2612
|
}
|
|
2613
|
+
let worktreeResolution = {};
|
|
2614
|
+
try {
|
|
2615
|
+
worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
|
|
2616
|
+
}
|
|
2617
|
+
catch (err) {
|
|
2618
|
+
return createErrorResponse("mistral_request_async", 1, "", corrId, err);
|
|
2619
|
+
}
|
|
2430
2620
|
assertUpstreamCliArgs("mistral", args);
|
|
2431
2621
|
assertUpstreamCliEnv("mistral", mistralEnv);
|
|
2432
2622
|
const mistralAsyncFrHandoff = buildAsyncFlightRecorderHandoff("mistral", prep, effectiveSessionId, params.outputFormat);
|
|
2433
|
-
const job = deps.asyncJobManager.startJob("mistral", args, corrId,
|
|
2623
|
+
const job = deps.asyncJobManager.startJob("mistral", args, corrId, worktreeResolution.cwd, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, mistralEnv, undefined, mistralAsyncFrHandoff.flightRecorderEntry, mistralAsyncFrHandoff.extractUsage, true);
|
|
2434
2624
|
deps.logger.info(`[${corrId}] mistral_request_async started job ${job.id}`);
|
|
2435
2625
|
const asyncResponse = {
|
|
2436
2626
|
success: true,
|
|
@@ -2443,6 +2633,9 @@ export async function handleMistralRequestAsync(deps, params) {
|
|
|
2443
2633
|
if (prep.reviewIntegrity && prep.reviewIntegrity.violations.length > 0) {
|
|
2444
2634
|
asyncResponse.reviewIntegrity = prep.reviewIntegrity;
|
|
2445
2635
|
}
|
|
2636
|
+
if (worktreeResolution.worktreePath) {
|
|
2637
|
+
asyncResponse.worktreePath = worktreeResolution.worktreePath;
|
|
2638
|
+
}
|
|
2446
2639
|
return {
|
|
2447
2640
|
content: [
|
|
2448
2641
|
{
|
|
@@ -2529,6 +2722,17 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
2529
2722
|
const newSession = await deps.sessionManager.createSession("codex", "Codex Session");
|
|
2530
2723
|
effectiveSessionId = newSession.id;
|
|
2531
2724
|
}
|
|
2725
|
+
// Slice λ: resolve worktree directive after session I/O so resume reuse
|
|
2726
|
+
// can read metadata.worktreePath. A pre-startJob failure here means
|
|
2727
|
+
// prepCleanup is still owned locally; run it before returning.
|
|
2728
|
+
let worktreeResolution = {};
|
|
2729
|
+
try {
|
|
2730
|
+
worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionId, runtime);
|
|
2731
|
+
}
|
|
2732
|
+
catch (err) {
|
|
2733
|
+
runPrepCleanupLocally();
|
|
2734
|
+
return createErrorResponse("codex_request_async", 1, "", corrId, err);
|
|
2735
|
+
}
|
|
2532
2736
|
// Start job only after all session I/O succeeds. If startJob throws before
|
|
2533
2737
|
// registering the record, ownership stays here and we run it in the catch.
|
|
2534
2738
|
assertUpstreamCliArgs("codex", args);
|
|
@@ -2536,7 +2740,7 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
2536
2740
|
const codexAsyncFrHandoff = buildAsyncFlightRecorderHandoff("codex", prep, effectiveSessionId, params.outputFormat);
|
|
2537
2741
|
let job;
|
|
2538
2742
|
try {
|
|
2539
|
-
job = deps.asyncJobManager.startJob("codex", args, corrId,
|
|
2743
|
+
job = deps.asyncJobManager.startJob("codex", args, corrId, worktreeResolution.cwd, resolveIdleTimeout("codex", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, prepCleanup, codexAsyncFrHandoff.flightRecorderEntry, codexAsyncFrHandoff.extractUsage, true);
|
|
2540
2744
|
// Handoff succeeded: AsyncJobManager will fire prepCleanup on terminal
|
|
2541
2745
|
// status. Release our local ownership claim so the catch path doesn't
|
|
2542
2746
|
// double-fire.
|
|
@@ -2558,6 +2762,9 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
2558
2762
|
if (prep.reviewIntegrity && prep.reviewIntegrity.violations.length > 0) {
|
|
2559
2763
|
asyncResponse.reviewIntegrity = prep.reviewIntegrity;
|
|
2560
2764
|
}
|
|
2765
|
+
if (worktreeResolution.worktreePath) {
|
|
2766
|
+
asyncResponse.worktreePath = worktreeResolution.worktreePath;
|
|
2767
|
+
}
|
|
2561
2768
|
return {
|
|
2562
2769
|
content: [
|
|
2563
2770
|
{
|
|
@@ -2695,6 +2902,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
2695
2902
|
.array(z.string())
|
|
2696
2903
|
.optional()
|
|
2697
2904
|
.describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance."),
|
|
2905
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
2698
2906
|
approvalStrategy: z
|
|
2699
2907
|
.enum(["legacy", "mcp_managed"])
|
|
2700
2908
|
.default("legacy")
|
|
@@ -2725,7 +2933,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
2725
2933
|
.boolean()
|
|
2726
2934
|
.default(false)
|
|
2727
2935
|
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
2728
|
-
}, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
|
|
2936
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
|
|
2729
2937
|
const startTime = Date.now();
|
|
2730
2938
|
if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
|
|
2731
2939
|
return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
|
|
@@ -2823,10 +3031,19 @@ export function createGatewayServer(deps = {}) {
|
|
|
2823
3031
|
args.push("--session-id", effectiveSessionId);
|
|
2824
3032
|
await sessionManager.updateSessionUsage(effectiveSessionId);
|
|
2825
3033
|
}
|
|
3034
|
+
// Slice λ: resolve worktree directive into spawn cwd. Done after
|
|
3035
|
+
// session resolution so resume reuse can read metadata.worktreePath.
|
|
3036
|
+
let worktreeResolution = {};
|
|
3037
|
+
try {
|
|
3038
|
+
worktreeResolution = await resolveWorktreeForRequest(worktree, effectiveSessionId, runtime);
|
|
3039
|
+
}
|
|
3040
|
+
catch (err) {
|
|
3041
|
+
return createErrorResponse("claude_request", 1, "", corrId, err);
|
|
3042
|
+
}
|
|
2826
3043
|
// Idle timeout only for stream-json (text/json produce no output until done)
|
|
2827
3044
|
const effectiveIdleTimeout = outputFormat === "stream-json" ? resolveIdleTimeout("claude", idleTimeoutMs) : undefined;
|
|
2828
3045
|
const claudeSyncFrHandoff = buildAsyncFlightRecorderHandoff("claude", prep, effectiveSessionId, outputFormat);
|
|
2829
|
-
const result = await awaitJobOrDefer("claude", args, corrId, effectiveIdleTimeout, outputFormat, forceRefresh, runtime, undefined, undefined, claudeSyncFrHandoff.flightRecorderEntry, claudeSyncFrHandoff.extractUsage, prep.stdinPayload);
|
|
3046
|
+
const result = await awaitJobOrDefer("claude", args, corrId, effectiveIdleTimeout, outputFormat, forceRefresh, runtime, undefined, undefined, claudeSyncFrHandoff.flightRecorderEntry, claudeSyncFrHandoff.extractUsage, prep.stdinPayload, worktreeResolution.cwd);
|
|
2830
3047
|
// Deferred — job still running, return async reference
|
|
2831
3048
|
if (isDeferredResponse(result)) {
|
|
2832
3049
|
return buildDeferredToolResponse(result, effectiveSessionId);
|
|
@@ -2883,7 +3100,14 @@ export function createGatewayServer(deps = {}) {
|
|
|
2883
3100
|
exitCode: 0,
|
|
2884
3101
|
status: "completed",
|
|
2885
3102
|
}, runtime);
|
|
2886
|
-
|
|
3103
|
+
const streamResponse = buildCliResponse("claude", parsed.text, optimizeResponse, corrId, effectiveSessionId, prep, durationMs, undefined, outputFormat, warnings);
|
|
3104
|
+
if (worktreeResolution.worktreePath) {
|
|
3105
|
+
const first = streamResponse.content[0];
|
|
3106
|
+
if (first && first.type === "text") {
|
|
3107
|
+
first.text = formatWorktreePrefix(worktreeResolution.worktreePath) + first.text;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
return streamResponse;
|
|
2887
3111
|
}
|
|
2888
3112
|
safeFlightComplete(corrId, {
|
|
2889
3113
|
response: stdout,
|
|
@@ -2894,7 +3118,14 @@ export function createGatewayServer(deps = {}) {
|
|
|
2894
3118
|
exitCode: 0,
|
|
2895
3119
|
status: "completed",
|
|
2896
3120
|
}, runtime);
|
|
2897
|
-
|
|
3121
|
+
const nonStreamResponse = buildCliResponse("claude", stdout, optimizeResponse, corrId, effectiveSessionId, prep, durationMs, undefined, outputFormat, warnings);
|
|
3122
|
+
if (worktreeResolution.worktreePath) {
|
|
3123
|
+
const first = nonStreamResponse.content[0];
|
|
3124
|
+
if (first && first.type === "text") {
|
|
3125
|
+
first.text = formatWorktreePrefix(worktreeResolution.worktreePath) + first.text;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
return nonStreamResponse;
|
|
2898
3129
|
}
|
|
2899
3130
|
catch (error) {
|
|
2900
3131
|
const elapsedMs = Math.max(0, Date.now() - startTime);
|
|
@@ -3027,7 +3258,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
3027
3258
|
.array(z.string())
|
|
3028
3259
|
.optional()
|
|
3029
3260
|
.describe("Codex --add-dir <DIR>: additional writable workspace directories. Emitted once per entry on new sessions only; resume inherits the original session's writable-dir policy."),
|
|
3030
|
-
|
|
3261
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
3262
|
+
}, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, worktree, }) => {
|
|
3031
3263
|
const startTime = Date.now();
|
|
3032
3264
|
const prep = prepareCodexRequest({
|
|
3033
3265
|
prompt,
|
|
@@ -3079,9 +3311,20 @@ export function createGatewayServer(deps = {}) {
|
|
|
3079
3311
|
// execution, on terminal status for the job-backed path (sync
|
|
3080
3312
|
// completion or deferred). The outer finally MUST NOT clean again.
|
|
3081
3313
|
const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function" ? prep.cleanup : undefined;
|
|
3314
|
+
// Slice λ: resolve worktree directive into spawn cwd. Codex has no
|
|
3315
|
+
// in-handler session resolution prior to spawn (session lookup is
|
|
3316
|
+
// lazy via `codex exec resume`), so the user-supplied sessionId is
|
|
3317
|
+
// the only reuse key.
|
|
3318
|
+
let worktreeResolution = {};
|
|
3319
|
+
try {
|
|
3320
|
+
worktreeResolution = await resolveWorktreeForRequest(worktree, sessionId, runtime);
|
|
3321
|
+
}
|
|
3322
|
+
catch (err) {
|
|
3323
|
+
return createErrorResponse("codex_request", 1, "", corrId, err);
|
|
3324
|
+
}
|
|
3082
3325
|
try {
|
|
3083
3326
|
const codexSyncFrHandoff = buildAsyncFlightRecorderHandoff("codex", prep, sessionId, outputFormat);
|
|
3084
|
-
const result = await awaitJobOrDefer("codex", args, corrId, resolveIdleTimeout("codex", idleTimeoutMs), outputFormat, forceRefresh, runtime, undefined, prepCleanup, codexSyncFrHandoff.flightRecorderEntry, codexSyncFrHandoff.extractUsage);
|
|
3327
|
+
const result = await awaitJobOrDefer("codex", args, corrId, resolveIdleTimeout("codex", idleTimeoutMs), outputFormat, forceRefresh, runtime, undefined, prepCleanup, codexSyncFrHandoff.flightRecorderEntry, codexSyncFrHandoff.extractUsage, undefined, worktreeResolution.cwd);
|
|
3085
3328
|
// Deferred — job still running, return async reference. Cleanup
|
|
3086
3329
|
// ownership belongs to AsyncJobManager via onComplete.
|
|
3087
3330
|
if (isDeferredResponse(result)) {
|
|
@@ -3139,7 +3382,14 @@ export function createGatewayServer(deps = {}) {
|
|
|
3139
3382
|
cacheCreationTokens: codexUsage.cacheCreationTokens,
|
|
3140
3383
|
costUsd: codexUsage.costUsd,
|
|
3141
3384
|
}, runtime);
|
|
3142
|
-
|
|
3385
|
+
const codexResponse = buildCliResponse("codex", stdout, optimizeResponse, corrId, effectiveSessionId, prep, durationMs, undefined, outputFormat);
|
|
3386
|
+
if (worktreeResolution.worktreePath) {
|
|
3387
|
+
const first = codexResponse.content[0];
|
|
3388
|
+
if (first && first.type === "text") {
|
|
3389
|
+
first.text = formatWorktreePrefix(worktreeResolution.worktreePath) + first.text;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
return codexResponse;
|
|
3143
3393
|
}
|
|
3144
3394
|
catch (error) {
|
|
3145
3395
|
const elapsedMs = Math.max(0, Date.now() - startTime);
|
|
@@ -3329,7 +3579,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
3329
3579
|
.boolean()
|
|
3330
3580
|
.default(false)
|
|
3331
3581
|
.describe("Emit `--skip-trust` so Gemini trusts the workspace for this session and skips the interactive trust prompt (Phase 4 slice γ). Required for headless runs in fresh workspaces."),
|
|
3332
|
-
|
|
3582
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
3583
|
+
}, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, worktree, }) => {
|
|
3333
3584
|
return handleGeminiRequest({ sessionManager, logger, runtime }, {
|
|
3334
3585
|
prompt,
|
|
3335
3586
|
promptParts,
|
|
@@ -3354,6 +3605,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3354
3605
|
adminPolicyFiles,
|
|
3355
3606
|
attachments,
|
|
3356
3607
|
skipTrust,
|
|
3608
|
+
worktree,
|
|
3357
3609
|
});
|
|
3358
3610
|
});
|
|
3359
3611
|
//──────────────────────────────────────────────────────────────────────────────
|
|
@@ -3459,7 +3711,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
3459
3711
|
.array(z.string())
|
|
3460
3712
|
.optional()
|
|
3461
3713
|
.describe('Grok --deny <RULE>: permission deny rules. Each entry is emitted as its own --deny instance (per `grok --help`: "Repeat to add multiple rules").'),
|
|
3462
|
-
|
|
3714
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
3715
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, worktree, }) => {
|
|
3463
3716
|
return handleGrokRequest({ sessionManager, logger, runtime }, {
|
|
3464
3717
|
prompt,
|
|
3465
3718
|
promptParts,
|
|
@@ -3489,6 +3742,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3489
3742
|
systemPromptOverride,
|
|
3490
3743
|
allow,
|
|
3491
3744
|
deny,
|
|
3745
|
+
worktree,
|
|
3492
3746
|
});
|
|
3493
3747
|
});
|
|
3494
3748
|
//──────────────────────────────────────────────────────────────────────────────
|
|
@@ -3578,7 +3832,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
3578
3832
|
.array(z.string())
|
|
3579
3833
|
.optional()
|
|
3580
3834
|
.describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance (Vibe states this flag may be specified multiple times)."),
|
|
3581
|
-
|
|
3835
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
3836
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, workingDir, addDir, worktree, }) => {
|
|
3582
3837
|
return handleMistralRequest({ sessionManager, logger, runtime }, {
|
|
3583
3838
|
prompt,
|
|
3584
3839
|
promptParts,
|
|
@@ -3605,6 +3860,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3605
3860
|
maxPrice,
|
|
3606
3861
|
workingDir,
|
|
3607
3862
|
addDir,
|
|
3863
|
+
worktree,
|
|
3608
3864
|
});
|
|
3609
3865
|
});
|
|
3610
3866
|
//──────────────────────────────────────────────────────────────────────────────
|
|
@@ -3705,6 +3961,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3705
3961
|
.array(z.string())
|
|
3706
3962
|
.optional()
|
|
3707
3963
|
.describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance."),
|
|
3964
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
3708
3965
|
approvalStrategy: z
|
|
3709
3966
|
.enum(["legacy", "mcp_managed"])
|
|
3710
3967
|
.default("legacy")
|
|
@@ -3734,7 +3991,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3734
3991
|
.boolean()
|
|
3735
3992
|
.default(false)
|
|
3736
3993
|
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
3737
|
-
}, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
|
|
3994
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
|
|
3738
3995
|
if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
|
|
3739
3996
|
return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
|
|
3740
3997
|
}
|
|
@@ -3801,6 +4058,15 @@ export function createGatewayServer(deps = {}) {
|
|
|
3801
4058
|
sessionId: effectiveSessionId,
|
|
3802
4059
|
cli: "claude",
|
|
3803
4060
|
});
|
|
4061
|
+
// Slice λ: resolve worktree directive after session metadata is
|
|
4062
|
+
// settled so resume reuse can read metadata.worktreePath.
|
|
4063
|
+
let worktreeResolution = {};
|
|
4064
|
+
try {
|
|
4065
|
+
worktreeResolution = await resolveWorktreeForRequest(worktree, effectiveSessionId, runtime);
|
|
4066
|
+
}
|
|
4067
|
+
catch (err) {
|
|
4068
|
+
return createErrorResponse("claude_request_async", 1, "", corrId, err);
|
|
4069
|
+
}
|
|
3804
4070
|
// Idle timeout only for stream-json (text/json produce no output until done)
|
|
3805
4071
|
const effectiveIdleTimeout = outputFormat === "stream-json"
|
|
3806
4072
|
? resolveIdleTimeout("claude", idleTimeoutMs)
|
|
@@ -3808,7 +4074,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3808
4074
|
assertUpstreamCliArgs("claude", args);
|
|
3809
4075
|
assertUpstreamCliEnv("claude", undefined);
|
|
3810
4076
|
const claudeAsyncFrHandoff = buildAsyncFlightRecorderHandoff("claude", prep, effectiveSessionId, outputFormat);
|
|
3811
|
-
const job = asyncJobManager.startJob("claude", args, corrId,
|
|
4077
|
+
const job = asyncJobManager.startJob("claude", args, corrId, worktreeResolution.cwd, effectiveIdleTimeout, outputFormat, forceRefresh, undefined, undefined, claudeAsyncFrHandoff.flightRecorderEntry, claudeAsyncFrHandoff.extractUsage, true, prep.stdinPayload);
|
|
3812
4078
|
logger.info(`[${corrId}] claude_request_async started job ${job.id}, outputFormat=${outputFormat}`);
|
|
3813
4079
|
const asyncResponse = {
|
|
3814
4080
|
success: true,
|
|
@@ -3824,6 +4090,9 @@ export function createGatewayServer(deps = {}) {
|
|
|
3824
4090
|
if (prep.reviewIntegrity && prep.reviewIntegrity.violations.length > 0) {
|
|
3825
4091
|
asyncResponse.reviewIntegrity = prep.reviewIntegrity;
|
|
3826
4092
|
}
|
|
4093
|
+
if (worktreeResolution.worktreePath) {
|
|
4094
|
+
asyncResponse.worktreePath = worktreeResolution.worktreePath;
|
|
4095
|
+
}
|
|
3827
4096
|
// Rec #4: include any prep-time warnings (e.g.
|
|
3828
4097
|
// cacheable_prefix_uncached) alongside ttlWarning.
|
|
3829
4098
|
const mergedWarnings = [
|
|
@@ -3936,7 +4205,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
3936
4205
|
.array(z.string())
|
|
3937
4206
|
.optional()
|
|
3938
4207
|
.describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only."),
|
|
3939
|
-
|
|
4208
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
4209
|
+
}, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, worktree, }) => {
|
|
3940
4210
|
return handleCodexRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
|
|
3941
4211
|
prompt,
|
|
3942
4212
|
promptParts,
|
|
@@ -3967,6 +4237,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3967
4237
|
ignoreRules,
|
|
3968
4238
|
workingDir,
|
|
3969
4239
|
addDir,
|
|
4240
|
+
worktree,
|
|
3970
4241
|
});
|
|
3971
4242
|
});
|
|
3972
4243
|
server.tool("gemini_request_async", {
|
|
@@ -4038,7 +4309,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
4038
4309
|
.boolean()
|
|
4039
4310
|
.default(false)
|
|
4040
4311
|
.describe("Emit `--skip-trust` so Gemini trusts the workspace for this session and skips the interactive trust prompt (Phase 4 slice γ). Required for headless runs in fresh workspaces."),
|
|
4041
|
-
|
|
4312
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
4313
|
+
}, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, worktree, }) => {
|
|
4042
4314
|
return handleGeminiRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
|
|
4043
4315
|
prompt,
|
|
4044
4316
|
promptParts,
|
|
@@ -4062,6 +4334,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
4062
4334
|
adminPolicyFiles,
|
|
4063
4335
|
attachments,
|
|
4064
4336
|
skipTrust,
|
|
4337
|
+
worktree,
|
|
4065
4338
|
});
|
|
4066
4339
|
});
|
|
4067
4340
|
server.tool("grok_request_async", {
|
|
@@ -4163,7 +4436,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
4163
4436
|
.array(z.string())
|
|
4164
4437
|
.optional()
|
|
4165
4438
|
.describe("Grok --deny <RULE>: permission deny rules. Each entry → its own --deny instance."),
|
|
4166
|
-
|
|
4439
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
4440
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, worktree, }) => {
|
|
4167
4441
|
return handleGrokRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
|
|
4168
4442
|
prompt,
|
|
4169
4443
|
promptParts,
|
|
@@ -4192,6 +4466,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
4192
4466
|
systemPromptOverride,
|
|
4193
4467
|
allow,
|
|
4194
4468
|
deny,
|
|
4469
|
+
worktree,
|
|
4195
4470
|
});
|
|
4196
4471
|
});
|
|
4197
4472
|
server.tool("mistral_request_async", {
|
|
@@ -4277,7 +4552,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
4277
4552
|
.array(z.string())
|
|
4278
4553
|
.optional()
|
|
4279
4554
|
.describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance."),
|
|
4280
|
-
|
|
4555
|
+
worktree: WORKTREE_SCHEMA.optional(),
|
|
4556
|
+
}, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, workingDir, addDir, worktree, }) => {
|
|
4281
4557
|
return handleMistralRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
|
|
4282
4558
|
prompt,
|
|
4283
4559
|
promptParts,
|
|
@@ -4303,6 +4579,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
4303
4579
|
maxPrice,
|
|
4304
4580
|
workingDir,
|
|
4305
4581
|
addDir,
|
|
4582
|
+
worktree,
|
|
4306
4583
|
});
|
|
4307
4584
|
});
|
|
4308
4585
|
server.tool("llm_job_status", {
|
|
@@ -4818,6 +5095,12 @@ export function createGatewayServer(deps = {}) {
|
|
|
4818
5095
|
//──────────────────────────────────────────────────────────────────────────────
|
|
4819
5096
|
async function initializeSessionManager() {
|
|
4820
5097
|
const config = loadConfig();
|
|
5098
|
+
// Slice λ: file-backed sessions get a cleanup hook that tears down any
|
|
5099
|
+
// git worktrees recorded on session.metadata.worktreePath. PG-backed
|
|
5100
|
+
// sessions skip the hook (multi-tenant deployments don't necessarily
|
|
5101
|
+
// own a single filesystem); revisit if/when worktree support extends
|
|
5102
|
+
// there.
|
|
5103
|
+
const worktreeCleanupHook = createWorktreeSessionCleanupHook(logger);
|
|
4821
5104
|
if (config.database && config.redis) {
|
|
4822
5105
|
logger.info("Initializing PostgreSQL + Redis session manager");
|
|
4823
5106
|
const { createDatabaseConnection } = await import("./db.js");
|
|
@@ -4827,7 +5110,9 @@ async function initializeSessionManager() {
|
|
|
4827
5110
|
}
|
|
4828
5111
|
else {
|
|
4829
5112
|
logger.info("Initializing file-based session manager");
|
|
4830
|
-
sessionManager = await createSessionManager(config, undefined, logger
|
|
5113
|
+
sessionManager = await createSessionManager(config, undefined, logger, {
|
|
5114
|
+
cleanupHook: worktreeCleanupHook,
|
|
5115
|
+
});
|
|
4831
5116
|
logger.info("File-based session manager initialized");
|
|
4832
5117
|
}
|
|
4833
5118
|
resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger));
|