llm-cli-gateway 1.14.0 → 1.15.1

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/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, undefined, resolveIdleTimeout("gemini", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, undefined, geminiAsyncFrHandoff.flightRecorderEntry, geminiAsyncFrHandoff.extractUsage, true);
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, undefined, resolveIdleTimeout("grok", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, undefined, grokAsyncFrHandoff.flightRecorderEntry, grokAsyncFrHandoff.extractUsage, true);
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, undefined, resolveIdleTimeout("mistral", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, mistralEnv, undefined, mistralAsyncFrHandoff.flightRecorderEntry, mistralAsyncFrHandoff.extractUsage, true);
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, undefined, resolveIdleTimeout("codex", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, prepCleanup, codexAsyncFrHandoff.flightRecorderEntry, codexAsyncFrHandoff.extractUsage, true);
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
- return buildCliResponse("claude", parsed.text, optimizeResponse, corrId, effectiveSessionId, prep, durationMs, undefined, outputFormat, warnings);
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
- return buildCliResponse("claude", stdout, optimizeResponse, corrId, effectiveSessionId, prep, durationMs, undefined, outputFormat, warnings);
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
- }, 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, }) => {
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
- return buildCliResponse("codex", stdout, optimizeResponse, corrId, effectiveSessionId, prep, durationMs, undefined, outputFormat);
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
- }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, }) => {
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
- }, 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, }) => {
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
- }, 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, }) => {
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, undefined, effectiveIdleTimeout, outputFormat, forceRefresh, undefined, undefined, claudeAsyncFrHandoff.flightRecorderEntry, claudeAsyncFrHandoff.extractUsage, true, prep.stdinPayload);
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
- }, 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, }) => {
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
- }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, }) => {
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
- }, 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, }) => {
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
- }, 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, }) => {
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));