@jpcasabiancai/cayenne 0.5.13 → 0.5.14

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.
Files changed (2) hide show
  1. package/dist/cli.js +310 -49
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1586,7 +1586,7 @@ var init_documents = __esm({
1586
1586
 
1587
1587
  // ../../packages/shared/src/protocol/events.ts
1588
1588
  import { z as z14 } from "zod";
1589
- var SessionStatus, SessionEvent, SESSION_EVENT_NAME, sessionTopic, runnerTopic, terminalTopic, activityFeedTopic;
1589
+ var SessionStatus, RenewalReason, SessionEvent, SESSION_EVENT_NAME, sessionTopic, runnerTopic, terminalTopic, activityFeedTopic;
1590
1590
  var init_events = __esm({
1591
1591
  "../../packages/shared/src/protocol/events.ts"() {
1592
1592
  "use strict";
@@ -1600,6 +1600,13 @@ var init_events = __esm({
1600
1600
  "error",
1601
1601
  "archived"
1602
1602
  ]);
1603
+ RenewalReason = z14.enum([
1604
+ "autopilot_threshold",
1605
+ "threshold",
1606
+ "visibility",
1607
+ "manual",
1608
+ "compaction"
1609
+ ]);
1603
1610
  SessionEvent = z14.discriminatedUnion("type", [
1604
1611
  z14.object({
1605
1612
  type: z14.literal("session.status"),
@@ -1654,7 +1661,16 @@ var init_events = __esm({
1654
1661
  segment: z14.number().int().nonnegative(),
1655
1662
  fromTokens: z14.number().int().optional(),
1656
1663
  toTokens: z14.number().int().optional(),
1657
- error: z14.string().optional()
1664
+ error: z14.string().optional(),
1665
+ // Added 2026-10-23 (Auto Pilot 03, D62); absent from runners ≤ 0.5.13.
1666
+ reason: RenewalReason.optional()
1667
+ }),
1668
+ // Auto Pilot 03 (D62), protocol §3: sent once after `renewal { phase: 'done' }`, naming the new
1669
+ // segment `n + 1` and why it renewed.
1670
+ z14.object({
1671
+ type: z14.literal("session.renewed"),
1672
+ segment: z14.number().int().min(1),
1673
+ reason: RenewalReason
1658
1674
  })
1659
1675
  ]);
1660
1676
  SESSION_EVENT_NAME = "evt";
@@ -2594,7 +2610,7 @@ var init_registry = __esm({
2594
2610
  "use strict";
2595
2611
  REDACTED = "[REDACTED]";
2596
2612
  MIN_SECRET_LENGTH = 16;
2597
- GITHUB_TOKEN_SHAPE = /(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]{20,255}/;
2613
+ GITHUB_TOKEN_SHAPE = /(?:ghs_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]{20,})/;
2598
2614
  GITHUB_TOKEN_SHAPES = new RegExp(GITHUB_TOKEN_SHAPE.source, "g");
2599
2615
  secrets = createSecretRegistry();
2600
2616
  isError = (value) => value instanceof Error || types.isNativeError(value);
@@ -3469,7 +3485,7 @@ function statusSummary(porcelain) {
3469
3485
  if (lines.length > MAX_STATUS_PATHS) out.push(` \u2026 ${lines.length - MAX_STATUS_PATHS} more`);
3470
3486
  return out;
3471
3487
  }
3472
- function buildAppendix(git, todos, docs, claims) {
3488
+ function buildAppendix(git, todos, docs, claims, extra = []) {
3473
3489
  const out = [APPENDIX_HEADING];
3474
3490
  if (git) {
3475
3491
  out.push(`Branch: ${git.branch === null ? "(detached or none)" : field(git.branch, CAP_PATH)}`);
@@ -3504,7 +3520,9 @@ function buildAppendix(git, todos, docs, claims) {
3504
3520
  } else {
3505
3521
  out.push("Active claims: none");
3506
3522
  }
3507
- return truncateHead(out.join("\n"), MAX_APPENDIX_TOKENS);
3523
+ const tail = extra.map((section2) => `
3524
+ ${section2}`).join("");
3525
+ return `${truncateHead(out.join("\n"), MAX_APPENDIX_TOKENS - estimateTokens(tail))}${tail}`;
3508
3526
  }
3509
3527
  function captureHandoff(turn, clock, timeoutMs = HANDOFF_TIMEOUT_MS) {
3510
3528
  return new Promise((resolve7) => {
@@ -10052,7 +10070,7 @@ var init_token_client = __esm({
10052
10070
  "src/repos/token-client.ts"() {
10053
10071
  "use strict";
10054
10072
  init_src();
10055
- TOKEN_SHAPE = /^[A-Za-z0-9_]{20,255}$/;
10073
+ TOKEN_SHAPE = /^[A-Za-z0-9_.-]{20,}$/;
10056
10074
  DEFAULT_TIMEOUT_MS = 9e4;
10057
10075
  }
10058
10076
  });
@@ -10335,6 +10353,49 @@ var init_attachments2 = __esm({
10335
10353
  }
10336
10354
  });
10337
10355
 
10356
+ // src/sessions/autopilot-runs.ts
10357
+ function autopilotRunsReader(db, log = logger) {
10358
+ const running = (sessionId, columns) => db.from("autopilot_runs").select(columns).eq("session_id", sessionId).eq("status", "running").maybeSingle();
10359
+ const failed6 = (sessionId, err) => {
10360
+ const e = err;
10361
+ log.warn({ sessionId, code: e?.code, err: e?.message }, "autopilot_runs read failed");
10362
+ };
10363
+ return {
10364
+ isAutopilotRunning: async (sessionId) => {
10365
+ try {
10366
+ const { data, error } = await running(sessionId, "id");
10367
+ if (error) throw error;
10368
+ return data !== null;
10369
+ } catch (err) {
10370
+ failed6(sessionId, err);
10371
+ return false;
10372
+ }
10373
+ },
10374
+ /** Null too for a run `ownerId`, the session's owner, did not start. */
10375
+ autopilotRun: async (sessionId, ownerId) => {
10376
+ try {
10377
+ const { data, error } = await running(sessionId, "objective, done_when, owner_id");
10378
+ if (error) throw error;
10379
+ if (!data || data.owner_id !== ownerId) return null;
10380
+ return {
10381
+ objective: data.objective,
10382
+ doneWhen: data.done_when,
10383
+ unmetDoneWhen: data.done_when
10384
+ };
10385
+ } catch (err) {
10386
+ failed6(sessionId, err);
10387
+ return null;
10388
+ }
10389
+ }
10390
+ };
10391
+ }
10392
+ var init_autopilot_runs = __esm({
10393
+ "src/sessions/autopilot-runs.ts"() {
10394
+ "use strict";
10395
+ init_log();
10396
+ }
10397
+ });
10398
+
10338
10399
  // src/fs/list.ts
10339
10400
  import { readdirSync as readdirSync3, realpathSync as realpathSync12, statSync as statSync7 } from "fs";
10340
10401
  import { join as join22, relative as relative7, sep as sep10 } from "path";
@@ -10851,9 +10912,14 @@ var init_format = __esm({
10851
10912
  });
10852
10913
 
10853
10914
  // src/renewal/threshold.ts
10854
- function renewThreshold(session) {
10855
- const { workspaceThreshold, window } = session;
10856
- if (!Number.isFinite(workspaceThreshold) || !Number.isFinite(window) || window <= 0) return null;
10915
+ function renewThreshold(s) {
10916
+ const { window, workspaceThreshold } = s;
10917
+ if (window === null || !Number.isFinite(window) || window <= 0) return null;
10918
+ if (s.autopilot) {
10919
+ const t2 = Math.floor(AUTOPILOT_WINDOW_SHARE * window);
10920
+ return t2 > 0 ? t2 : null;
10921
+ }
10922
+ if (!Number.isFinite(workspaceThreshold)) return null;
10857
10923
  const t = Math.min(Math.floor(workspaceThreshold), Math.floor(WINDOW_SHARE * window));
10858
10924
  return t < MIN_RENEW_THRESHOLD ? null : t;
10859
10925
  }
@@ -10873,12 +10939,13 @@ function autoCompactWindowFor(window) {
10873
10939
  const above = Math.floor(WINDOW_SHARE * w) + AUTOCOMPACT_BUFFER + AUTOCOMPACT_MARGIN;
10874
10940
  return Math.min(w, Math.max(Math.floor(0.9 * w), above));
10875
10941
  }
10876
- var MIN_RENEW_THRESHOLD, WINDOW_SHARE, KNOWN_CONTEXT_WINDOWS, AUTOCOMPACT_BUFFER, AUTOCOMPACT_MARGIN;
10942
+ var MIN_RENEW_THRESHOLD, WINDOW_SHARE, AUTOPILOT_WINDOW_SHARE, KNOWN_CONTEXT_WINDOWS, AUTOCOMPACT_BUFFER, AUTOCOMPACT_MARGIN;
10877
10943
  var init_threshold = __esm({
10878
10944
  "src/renewal/threshold.ts"() {
10879
10945
  "use strict";
10880
10946
  MIN_RENEW_THRESHOLD = 1e5;
10881
10947
  WINDOW_SHARE = 0.75;
10948
+ AUTOPILOT_WINDOW_SHARE = 0.4;
10882
10949
  KNOWN_CONTEXT_WINDOWS = {
10883
10950
  "claude-opus-5-5": 1e6
10884
10951
  };
@@ -10888,13 +10955,26 @@ var init_threshold = __esm({
10888
10955
  });
10889
10956
 
10890
10957
  // src/renewal/rotation.ts
10958
+ function memberContinuation(lastMemberPrompt, mode) {
10959
+ if (lastMemberPrompt === null) return null;
10960
+ return { text: continuationText(lastMemberPrompt), auto: AUTO_CONTINUE_MODES.has(mode) };
10961
+ }
10891
10962
  function handoffBlock(segment, handoff) {
10892
- const frame = (body2) => `${HANDOFF_PREAMBLE}
10963
+ const frame = (body) => `${HANDOFF_PREAMBLE}
10893
10964
  <handoff segment="${segment}">
10894
- ${body2}
10965
+ ${body}
10895
10966
  </handoff>`;
10896
- const body = truncateHead(escapeAngles(handoff), HANDOFF_MAX_TOKENS - estimateTokens(frame("")));
10897
- return frame(body);
10967
+ const max = HANDOFF_MAX_TOKENS - estimateTokens(frame(""));
10968
+ const text3 = escapeAngles(handoff);
10969
+ const at = text3.lastIndexOf(`
10970
+
10971
+ ${APPENDIX_HEADING}
10972
+ `);
10973
+ const appendix = at < 0 ? "" : text3.slice(at);
10974
+ if (at < 0 || estimateTokens(appendix) > 2 * MAX_APPENDIX_TOKENS) {
10975
+ return frame(truncateHead(text3, max));
10976
+ }
10977
+ return frame(`${truncateHead(text3.slice(0, at), max - estimateTokens(appendix))}${appendix}`);
10898
10978
  }
10899
10979
  function continuationText(lastMemberPrompt) {
10900
10980
  const quoted3 = lastMemberPrompt.replace(/[«»]/g, '"');
@@ -10911,12 +10991,12 @@ function buildFirstInput(args) {
10911
10991
  ${args.queuedPrompt}`, send: "now" };
10912
10992
  }
10913
10993
  if (args.queuedPrompt !== null) return { text: block, send: "withNextPrompt" };
10914
- if (args.endedOnNudge && args.lastMemberPrompt !== null) {
10994
+ if (args.endedOnNudge && args.continuation !== null) {
10915
10995
  return {
10916
10996
  text: `${block}
10917
10997
 
10918
- ${continuationText(args.lastMemberPrompt)}`,
10919
- send: AUTO_CONTINUE_MODES.has(args.mode) ? "now" : "continue"
10998
+ ${args.continuation.text}`,
10999
+ send: args.continuation.auto ? "now" : "continue"
10920
11000
  };
10921
11001
  }
10922
11002
  return { text: block, send: "withNextPrompt" };
@@ -10946,6 +11026,8 @@ var init_rotation = __esm({
10946
11026
  cost = 0;
10947
11027
  handoff = null;
10948
11028
  reason = "renewal";
11029
+ why = "threshold";
11030
+ plainRetry = false;
10949
11031
  /** Why a pending `requestRenew` was made: `manual` unless the runner asked for itself. */
10950
11032
  requested = "manual";
10951
11033
  constructor(options) {
@@ -10962,6 +11044,27 @@ var init_rotation = __esm({
10962
11044
  get enabled() {
10963
11045
  return this.threshold !== null;
10964
11046
  }
11047
+ /**
11048
+ * What decided the current (or last) renewal. A plain retry after a failure keeps it; a
11049
+ * `requestRenew` that is due at that turn end names its own.
11050
+ */
11051
+ get cause() {
11052
+ return this.why;
11053
+ }
11054
+ /** The current renewal is the one retry of a failed one, not asked for again (`cause` kept). */
11055
+ get retrying() {
11056
+ return this.plainRetry;
11057
+ }
11058
+ /**
11059
+ * The threshold changed between turns (an Auto Pilot run started or stopped, Auto Pilot 03).
11060
+ * The next `midTurn` and `turnEnd` go by it, except while `nudged`: the threshold that nudged
11061
+ * stays until that turn end renews, since the model was already told the chat will. Returns
11062
+ * the threshold in effect.
11063
+ */
11064
+ setThreshold(threshold) {
11065
+ if (this.current !== "nudged") this.threshold = threshold;
11066
+ return this.threshold;
11067
+ }
10965
11068
  /** The mode changed (a `setMode` update or a mode command): the continuation follows it. */
10966
11069
  setMode(mode) {
10967
11070
  this.mode = mode;
@@ -11014,11 +11117,17 @@ var init_rotation = __esm({
11014
11117
  due = this.manual || this.current === "nudged" || input.tokens >= this.threshold;
11015
11118
  }
11016
11119
  if (!due) return [];
11120
+ this.plainRetry = this.current === "failed" && !this.manual;
11017
11121
  if (this.current === "failed") {
11018
11122
  this.retryArmed = false;
11123
+ if (this.manual) {
11124
+ this.reason = this.requested;
11125
+ this.why = this.requested === "manual" ? "manual" : "visibility";
11126
+ }
11019
11127
  } else {
11020
11128
  this.endedOnNudge = this.current === "nudged";
11021
11129
  this.reason = this.manual ? this.requested : "renewal";
11130
+ this.why = !this.manual ? "threshold" : this.requested === "manual" ? "manual" : "visibility";
11022
11131
  }
11023
11132
  this.manual = false;
11024
11133
  this.current = "handoff";
@@ -11068,9 +11177,8 @@ var init_rotation = __esm({
11068
11177
  segment: n,
11069
11178
  handoff: this.handoff.text,
11070
11179
  queuedPrompt: input.queuedPrompt,
11071
- lastMemberPrompt: input.lastMemberPrompt,
11072
- endedOnNudge: this.endedOnNudge,
11073
- mode: this.mode
11180
+ continuation: input.autopilotContinuation ? { text: input.autopilotContinuation, auto: true } : memberContinuation(input.lastMemberPrompt, this.mode),
11181
+ endedOnNudge: this.endedOnNudge
11074
11182
  });
11075
11183
  const actions = [
11076
11184
  {
@@ -14908,6 +15016,50 @@ var init_materialise = __esm({
14908
15016
  }
14909
15017
  });
14910
15018
 
15019
+ // src/sessions/autopilot-renewal.ts
15020
+ function autopilotAppendix(run) {
15021
+ const met = run.doneWhen.length - run.unmetDoneWhen.length;
15022
+ const out = [
15023
+ AUTOPILOT_HEADING,
15024
+ `Objective: ${field(run.objective, CAP_OBJECTIVE)}`,
15025
+ `Done-when items met: ${Math.max(0, met)} of ${run.doneWhen.length}`
15026
+ ];
15027
+ if (run.unmetDoneWhen.length) {
15028
+ out.push("Done when (not yet met):");
15029
+ for (const item of run.unmetDoneWhen) out.push(`- ${field(item, CAP_ITEM)}`);
15030
+ } else {
15031
+ out.push("Done when (not yet met): none");
15032
+ }
15033
+ const text3 = out.join("\n");
15034
+ if (text3.length <= MAX_AUTOPILOT_APPENDIX) return text3;
15035
+ const head = text3.slice(0, MAX_AUTOPILOT_APPENDIX - 2);
15036
+ return `${head.slice(0, head.lastIndexOf("\n"))}
15037
+ \u2026`;
15038
+ }
15039
+ function autopilotContinuation(run) {
15040
+ let clean3 = run.objective.replace(CONTROL, " ").replace(/\s+/g, " ").trim().replace(/[«»]/g, '"');
15041
+ if (clean3.length > CAP_OBJECTIVE) clean3 = `${clean3.slice(0, CAP_OBJECTIVE - 1)}\u2026`;
15042
+ const quoted3 = escapeAngles(clean3);
15043
+ return `Continue toward the objective the member set for Auto Pilot: \xAB${quoted3}\xBB. The handoff above is reference.`;
15044
+ }
15045
+ function renewalReason(p) {
15046
+ if (p.manual) return "manual";
15047
+ if (p.visibility) return "visibility";
15048
+ if (p.compacted) return "compaction";
15049
+ return p.autopilot ? "autopilot_threshold" : "threshold";
15050
+ }
15051
+ var AUTOPILOT_HEADING, MAX_AUTOPILOT_APPENDIX, CAP_OBJECTIVE, CAP_ITEM;
15052
+ var init_autopilot_renewal = __esm({
15053
+ "src/sessions/autopilot-renewal.ts"() {
15054
+ "use strict";
15055
+ init_handoff();
15056
+ AUTOPILOT_HEADING = "## Auto Pilot";
15057
+ MAX_AUTOPILOT_APPENDIX = 2400;
15058
+ CAP_OBJECTIVE = 800;
15059
+ CAP_ITEM = 240;
15060
+ }
15061
+ });
15062
+
14911
15063
  // src/sessions/managed-git-guard.ts
14912
15064
  import { realpathSync as realpathSync15 } from "fs";
14913
15065
  import { isAbsolute as isAbsolute18, relative as relative11, resolve as resolve4, sep as sep15 } from "path";
@@ -15684,6 +15836,7 @@ var init_manager = __esm({
15684
15836
  init_registry();
15685
15837
  init_materialise();
15686
15838
  init_attachments2();
15839
+ init_autopilot_renewal();
15687
15840
  init_managed_git_guard();
15688
15841
  init_relay();
15689
15842
  init_repo_settings();
@@ -15748,6 +15901,8 @@ var init_manager = __esm({
15748
15901
  pendingBlock: null,
15749
15902
  manualPending: false,
15750
15903
  lastPrompt: null,
15904
+ autopilot: false,
15905
+ cause: null,
15751
15906
  todos: null
15752
15907
  });
15753
15908
  scopeOf2 = (v) => v === "private" || v === "workspace" ? v : null;
@@ -15904,7 +16059,9 @@ var init_manager = __esm({
15904
16059
  try {
15905
16060
  this.assertNotRemoving(cwd);
15906
16061
  } catch (err) {
15907
- throw this.giveBack(repo?.creds, err);
16062
+ const failure2 = this.giveBack(repo?.creds, err);
16063
+ await this.recordStartFailure(sessionId);
16064
+ throw failure2;
15908
16065
  }
15909
16066
  const started = this.markStarting(cwd, sessionId);
15910
16067
  const introspect = this.deps.introspect ?? ((dir, at) => introspectRepo(
@@ -15958,7 +16115,9 @@ var init_manager = __esm({
15958
16115
  );
15959
16116
  } catch (err) {
15960
16117
  this.discardSkills(sessionId);
15961
- throw this.giveBack(repo?.creds, err);
16118
+ const failure2 = this.giveBack(repo?.creds, err);
16119
+ await this.recordStartFailure(sessionId);
16120
+ throw failure2;
15962
16121
  } finally {
15963
16122
  started();
15964
16123
  }
@@ -16128,9 +16287,18 @@ var init_manager = __esm({
16128
16287
  repo: { ownerId, repositoryId: repository.id, creds }
16129
16288
  };
16130
16289
  } catch (err) {
16131
- throw this.giveBack(creds, err);
16290
+ const failure2 = this.giveBack(creds, err);
16291
+ await this.recordStartFailure(sessionId);
16292
+ throw failure2;
16132
16293
  }
16133
16294
  }
16295
+ /** A failed first start has no live channel; persist its status without ending a live retry. */
16296
+ async recordStartFailure(sessionId) {
16297
+ if (this.live.has(sessionId)) return;
16298
+ await this.updateSession(sessionId, { status: "error" }).catch(
16299
+ (err) => this.deps.log.warn({ err, sessionId }, "failed startup status could not be recorded")
16300
+ );
16301
+ }
16134
16302
  /**
16135
16303
  * A repository chat's recorded worktree, proven as a resume needs it (§2.9): this runner's record,
16136
16304
  * still the same worktree, shaped `<root>/<owner>/<name>/.cayenne/worktrees/chat-<hex>` for this
@@ -16962,14 +17130,19 @@ var init_manager = __esm({
16962
17130
  * turns. Returns `{ scheduled: true }` at once and never waits for the renewal, so a
16963
17131
  * `session.permission.respond` or `session.interrupt` queued behind it in the session lane runs
16964
17132
  * while the renewal waits for the turn to end (§2.1). Progress goes out as `renewal` events.
16965
- * Refused when the effective threshold is below 100,000 tokens, and for a session that cannot be
16966
- * renewed (Codex: no hook can deny the handoff turn's tools; a backend without 0042).
17133
+ * Refused when no threshold applies (below 100,000 tokens outside an Auto Pilot run), and for a
17134
+ * session that cannot be renewed (Codex: no hook can deny the handoff turn's tools; a backend
17135
+ * without 0042).
16967
17136
  */
16968
17137
  async requestRenewal(sessionId, createdBy) {
16969
17138
  const live = await this.ensureLive(sessionId, createdBy);
16970
17139
  this.assertCodexOwner(sessionId, createdBy);
16971
17140
  const r = live.renewal;
16972
17141
  if (!this.renewable(live)) throw new Error(RENEWAL_NOT_AVAILABLE);
17142
+ if (r.machine && r.window !== null && !r.busy) {
17143
+ r.autopilot = await this.autopilotRunning(sessionId);
17144
+ await this.ensureMachine(sessionId, live, r.window);
17145
+ }
16973
17146
  const machine = r.machine;
16974
17147
  if (machine && !machine.enabled) throw new Error(RENEWAL_NOT_AVAILABLE);
16975
17148
  if (r.busy) return { scheduled: true };
@@ -19294,8 +19467,9 @@ var init_manager = __esm({
19294
19467
  * failed turn, a pending permission), so they are never left waiting for a turn that won't come.
19295
19468
  */
19296
19469
  async turnEndRenewal(sessionId, live, r) {
19297
- const usage = await this.measure(sessionId, live);
19298
19470
  const rn = live.renewal;
19471
+ if (this.renewable(live)) rn.autopilot = await this.autopilotRunning(sessionId);
19472
+ const usage = await this.measure(sessionId, live);
19299
19473
  await this.noteScope(sessionId, live);
19300
19474
  if (rn.busy || live.stopping || live.dropping) return [];
19301
19475
  if (live.turns.owed > 0) return [];
@@ -19461,10 +19635,14 @@ var init_manager = __esm({
19461
19635
  async ensureMachine(sessionId, live, window) {
19462
19636
  const rn = live.renewal;
19463
19637
  if (rn.segment === null) return;
19464
- if (rn.machine && (rn.window === window || rn.machine.state !== "idle")) return;
19465
19638
  rn.workspaceThreshold ??= await this.workspaceThreshold();
19639
+ const subject = { autopilot: rn.autopilot, workspaceThreshold: rn.workspaceThreshold };
19640
+ if (rn.machine && rn.window !== null && (rn.window === window || rn.machine.state !== "idle")) {
19641
+ rn.threshold = rn.machine.setThreshold(renewThreshold({ ...subject, window: rn.window }));
19642
+ return;
19643
+ }
19466
19644
  rn.window = window;
19467
- rn.threshold = renewThreshold({ workspaceThreshold: rn.workspaceThreshold, window });
19645
+ rn.threshold = renewThreshold({ ...subject, window });
19468
19646
  rn.machine = new RenewalMachine({
19469
19647
  segment: rn.segment,
19470
19648
  threshold: rn.threshold,
@@ -19477,15 +19655,55 @@ var init_manager = __esm({
19477
19655
  if (rn.machine.enabled) {
19478
19656
  rn.machine.requestRenew("manual");
19479
19657
  } else {
19480
- await this.renewalEvent(sessionId, {
19481
- kind: "event",
19482
- phase: "failed",
19483
- segment: rn.segment,
19484
- error: RENEWAL_NOT_AVAILABLE
19485
- });
19658
+ await this.renewalEvent(
19659
+ sessionId,
19660
+ { kind: "event", phase: "failed", segment: rn.segment, error: RENEWAL_NOT_AVAILABLE },
19661
+ "manual"
19662
+ );
19486
19663
  }
19487
19664
  }
19488
19665
  }
19666
+ /**
19667
+ * `ManagerDeps.isAutopilotRunning`; false when absent, when it rejects (logged), and when the
19668
+ * runner no longer serves the session's owner: a run's continuation is a turn no command is
19669
+ * behind, so it must not outlive the owner's place on the roster.
19670
+ */
19671
+ async autopilotRunning(sessionId) {
19672
+ const ask2 = this.deps.isAutopilotRunning;
19673
+ if (!ask2) return false;
19674
+ try {
19675
+ return await ask2(sessionId) === true && await this.servedOwner(sessionId) !== null;
19676
+ } catch (err) {
19677
+ this.deps.log.warn(
19678
+ { sessionId, err: err instanceof Error ? err.message : String(err) },
19679
+ "reading the Auto Pilot run failed; renewing as without one"
19680
+ );
19681
+ return false;
19682
+ }
19683
+ }
19684
+ /**
19685
+ * `ManagerDeps.autopilotRun`; null when absent, and when it rejects (logged with the error only,
19686
+ * never the run's text).
19687
+ */
19688
+ async autopilotBrief(sessionId) {
19689
+ const read2 = this.deps.autopilotRun;
19690
+ if (!read2) return null;
19691
+ try {
19692
+ const owner2 = await this.servedOwner(sessionId);
19693
+ return owner2 === null ? null : await read2(sessionId, owner2);
19694
+ } catch (err) {
19695
+ this.deps.log.warn(
19696
+ { sessionId, err: err instanceof Error ? err.message : String(err) },
19697
+ "reading the Auto Pilot run failed; renewing as without one"
19698
+ );
19699
+ return null;
19700
+ }
19701
+ }
19702
+ /** The session's owner while this runner serves them, else null. Throws when unreadable. */
19703
+ async servedOwner(sessionId) {
19704
+ const owner2 = await this.sessionOwner(sessionId);
19705
+ return this.serves(owner2) ? owner2 : null;
19706
+ }
19489
19707
  /** `workspace_settings.renewal_threshold_tokens`; the column default when it cannot be read. */
19490
19708
  async workspaceThreshold() {
19491
19709
  const { data, error } = await this.deps.db.from("workspace_settings").select("renewal_threshold_tokens").eq("workspace_id", this.deps.workspaceId).maybeSingle();
@@ -19519,15 +19737,37 @@ var init_manager = __esm({
19519
19737
  rn.busy = true;
19520
19738
  try {
19521
19739
  if (!machine || n === null) return;
19740
+ if (!machine.retrying || !rn.cause) {
19741
+ const cause = machine.cause;
19742
+ rn.cause = {
19743
+ autopilot: rn.autopilot,
19744
+ reason: renewalReason({
19745
+ autopilot: rn.autopilot,
19746
+ manual: cause === "manual",
19747
+ visibility: cause === "visibility",
19748
+ compacted: false
19749
+ })
19750
+ };
19751
+ }
19752
+ const reason = rn.cause.reason;
19522
19753
  let prompt = null;
19523
19754
  for (const a of start2) {
19524
- if (a.kind === "event") await this.renewalEvent(sessionId, a);
19755
+ if (a.kind === "event") await this.renewalEvent(sessionId, a, reason);
19525
19756
  else if (a.kind === "runHandoffTurn") prompt = a.prompt;
19526
19757
  }
19527
19758
  if (prompt === null) return;
19528
19759
  const result = await this.runHandoffTurn(live, prompt);
19529
19760
  if (gone2()) return;
19530
- const handoff = await this.handoffFor(sessionId, live, n, result);
19761
+ const autopilot = rn.cause.autopilot;
19762
+ let run = autopilot ? await this.autopilotBrief(sessionId) : null;
19763
+ if (gone2()) return;
19764
+ const handoff = await this.handoffFor(
19765
+ sessionId,
19766
+ live,
19767
+ n,
19768
+ result,
19769
+ run ? [autopilotAppendix(run)] : []
19770
+ );
19531
19771
  try {
19532
19772
  for (const a of machine.handoffCaptured(handoff)) {
19533
19773
  if (a.kind === "saveHandoff") {
@@ -19537,7 +19777,7 @@ var init_manager = __esm({
19537
19777
  () => this.deps.db.from("chat_segments").update({ handoff: a.handoff, handoff_source: a.handoffSource }).eq("session_id", sessionId).eq("idx", a.segment)
19538
19778
  );
19539
19779
  } else if (a.kind === "event") {
19540
- await this.renewalEvent(sessionId, a);
19780
+ await this.renewalEvent(sessionId, a, reason);
19541
19781
  } else if (a.kind === "startQuery") {
19542
19782
  next = await this.startRenewedQuery(sessionId, live);
19543
19783
  }
@@ -19546,10 +19786,13 @@ var init_manager = __esm({
19546
19786
  const providerSessionId = next?.providerSessionId;
19547
19787
  if (!next || !providerSessionId) throw new Error("the new query has no session id");
19548
19788
  const first = rn.queued[0] ?? null;
19789
+ if (run && !first && !await this.autopilotRunning(sessionId)) run = null;
19790
+ if (gone2()) return;
19549
19791
  const actions = machine.queryStarted({
19550
19792
  providerSessionId,
19551
19793
  queuedPrompt: first?.text ?? null,
19552
- lastMemberPrompt: rn.lastPrompt
19794
+ lastMemberPrompt: autopilot ? null : rn.lastPrompt,
19795
+ autopilotContinuation: run ? this.secrets.redact(autopilotContinuation(run)) : null
19553
19796
  });
19554
19797
  await this.rotateSegment(sessionId, live, n, handoff.text, actions, next, first);
19555
19798
  } catch (err) {
@@ -19617,12 +19860,12 @@ var init_manager = __esm({
19617
19860
  * before the memory pipeline) or the segment's last messages; plus the deterministic appendix.
19618
19861
  * Scrubbed (D50) before it is stored and before it becomes segment `n + 1`'s first input.
19619
19862
  */
19620
- async handoffFor(sessionId, live, n, result) {
19863
+ async handoffFor(sessionId, live, n, result, extra = []) {
19621
19864
  const fromModel = result.kind === "ok" && estimateTokens(result.text.trim()) >= MIN_HANDOFF_TOKENS;
19622
19865
  const messages = fromModel ? [] : await this.segmentMessages(sessionId, n);
19623
19866
  const ceiling = live.roots.find((root) => isAtOrBelow3(live.cwd, root)) ?? live.cwd;
19624
19867
  const git = await this.appendixGitState(live.cwd, ceiling).catch(() => null);
19625
- const appendix = buildAppendix(git, live.renewal.todos, [], []);
19868
+ const appendix = buildAppendix(git, live.renewal.todos, [], [], extra);
19626
19869
  const handoff = resolveHandoff({ result, digest: null, messages, appendix });
19627
19870
  return { ...handoff, text: this.secrets.redact(handoff.text) };
19628
19871
  }
@@ -19770,7 +20013,8 @@ var init_manager = __esm({
19770
20013
  case "event":
19771
20014
  await this.renewalEvent(
19772
20015
  sessionId,
19773
- a.phase === "done" ? { ...a, toTokens: await this.firstReading(next) } : a
20016
+ a.phase === "done" ? { ...a, toTokens: await this.firstReading(next) } : a,
20017
+ live.renewal.cause?.reason
19774
20018
  );
19775
20019
  break;
19776
20020
  default:
@@ -20044,7 +20288,8 @@ ${text3}`;
20044
20288
  if (machine.state !== "failed" && live.renewal.machine === machine) live.renewal.machine = null;
20045
20289
  for (const a of actions) {
20046
20290
  if (a.kind === "clearHandoff") await this.clearHandoff(sessionId, a.segment);
20047
- else if (a.kind === "event") await this.renewalEvent(sessionId, a);
20291
+ else if (a.kind === "event")
20292
+ await this.renewalEvent(sessionId, a, live.renewal.cause?.reason);
20048
20293
  }
20049
20294
  if (live.renewal.scopeRenewalOwed && live.renewal.queued.length > 0) {
20050
20295
  try {
@@ -20095,16 +20340,23 @@ ${text3}`;
20095
20340
  throw new Error(`PROVIDER_ERROR: ${what} failed: ${error.message}`);
20096
20341
  }
20097
20342
  }
20098
- /** A machine `event` action as a `renewal` session event (protocol §3). */
20099
- async renewalEvent(sessionId, a) {
20343
+ /**
20344
+ * A machine `event` action as a `renewal` session event (protocol §3), carrying the renewal's
20345
+ * `reason` when it is known; `done` is followed by one `session.renewed` naming it.
20346
+ */
20347
+ async renewalEvent(sessionId, a, reason) {
20100
20348
  await this.broadcast(sessionId, {
20101
20349
  type: "renewal",
20102
20350
  phase: a.phase,
20103
20351
  segment: a.segment,
20104
20352
  ...a.fromTokens === void 0 ? {} : { fromTokens: a.fromTokens },
20105
20353
  ...a.toTokens === void 0 ? {} : { toTokens: a.toTokens },
20106
- ...a.error === void 0 ? {} : { error: this.cap(a.error) }
20354
+ ...a.error === void 0 ? {} : { error: this.cap(a.error) },
20355
+ ...reason === void 0 ? {} : { reason }
20107
20356
  });
20357
+ if (a.phase === "done" && reason !== void 0) {
20358
+ await this.broadcast(sessionId, { type: "session.renewed", segment: a.segment, reason });
20359
+ }
20108
20360
  }
20109
20361
  /**
20110
20362
  * Expire a request the session settled itself, and say so when this write is what settled the
@@ -22423,7 +22675,9 @@ async function compose(cfg, lock, host, preflight) {
22423
22675
  publishWrites: makePublishWrites(db),
22424
22676
  poll: prPoll,
22425
22677
  gh: runGh
22426
- }
22678
+ },
22679
+ // Auto Pilot 03: an active run renews at 40% of the window and continues on its own.
22680
+ ...autopilotRunsReader(db, logger)
22427
22681
  });
22428
22682
  let ownerServed = liveRoster.serves(identity.userId);
22429
22683
  let ownerTransition = Promise.resolve();
@@ -22813,6 +23067,7 @@ var init_start = __esm({
22813
23067
  init_roster();
22814
23068
  init_registry();
22815
23069
  init_attachments2();
23070
+ init_autopilot_runs();
22816
23071
  init_handlers();
22817
23072
  init_manager();
22818
23073
  init_repo_sessions();
@@ -23182,7 +23437,9 @@ var isInside = (dir, path) => {
23182
23437
  };
23183
23438
  function isTemporary(path, tmpDir = tmpdir()) {
23184
23439
  const roots = [resolve(tmpDir), realOrSpelled(tmpDir)];
23185
- return [resolve(path), realOrSpelled(path)].some((p) => roots.some((root) => isInside(root, p)));
23440
+ return [resolve(path), realOrSpelled(path)].some(
23441
+ (p) => p.split(sep).includes("cmux-cli-shims") || roots.some((root) => isInside(root, p))
23442
+ );
23186
23443
  }
23187
23444
  async function resolveClaude(exec4, tmpDir) {
23188
23445
  const candidates = await whichAll(exec4, "claude");
@@ -23199,8 +23456,9 @@ Put the claude CLI itself on PATH, or run this from another terminal: https://cl
23199
23456
  "claude CLI not found on PATH. The runner spawns it to run sessions.\nInstall it first: https://claude.com/claude-code"
23200
23457
  );
23201
23458
  }
23202
- function buildPathEnv(nodePath, claudePath) {
23459
+ function buildPathEnv(nodePath, claudePath, codexPath) {
23203
23460
  const dirs = ["/usr/bin", "/bin", "/usr/sbin", "/sbin", dirname2(nodePath), dirname2(claudePath)];
23461
+ if (codexPath) dirs.push(dirname2(codexPath));
23204
23462
  return [...new Set(dirs)].join(":");
23205
23463
  }
23206
23464
  function readInstalledVersion(appDir, pkg) {
@@ -23222,6 +23480,9 @@ function readInstallRecord(cayenneHome2) {
23222
23480
  }
23223
23481
  async function installApp(deps) {
23224
23482
  const claudePath = await resolveClaude(deps.exec, deps.tmpDir ?? tmpdir());
23483
+ const codexPath = (await whichAll(deps.exec, "codex")).find(
23484
+ (path) => !isTemporary(path, deps.tmpDir ?? tmpdir())
23485
+ );
23225
23486
  const appDir = join6(deps.cayenneHome, "app");
23226
23487
  mkdirSync3(appDir, { recursive: true, mode: 448 });
23227
23488
  const install = await deps.exec("npm", [
@@ -23246,7 +23507,7 @@ async function installApp(deps) {
23246
23507
  claudePath,
23247
23508
  cliPath,
23248
23509
  appDir,
23249
- pathEnv: buildPathEnv(nodePath, claudePath),
23510
+ pathEnv: buildPathEnv(nodePath, claudePath, codexPath),
23250
23511
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
23251
23512
  };
23252
23513
  const binDir = join6(deps.cayenneHome, "bin");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpcasabiancai/cayenne",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
4
4
  "files": [
5
5
  "dist/cli.js"
6
6
  ],
@@ -33,7 +33,7 @@
33
33
  "test": "vitest run"
34
34
  },
35
35
  "dependencies": {
36
- "@anthropic-ai/claude-agent-sdk": "0.3.260",
36
+ "@anthropic-ai/claude-agent-sdk": "0.3.312",
37
37
  "@anthropic-ai/sdk": "0.123.0",
38
38
  "@modelcontextprotocol/sdk": "1.30.0",
39
39
  "@supabase/supabase-js": "2.115.0",