@jpcasabiancai/cayenne 0.5.12 → 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 +341 -61
  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
  {
@@ -12363,7 +12471,7 @@ function asInterrupted(row) {
12363
12471
  )
12364
12472
  };
12365
12473
  }
12366
- var SESSION_FLAG_SETTINGS, SESSION_ASK_TOOLS, GUARD_FAILED, WRITE_TOOLS_MATCHER, CONTEXT_HOOK_TIMEOUT_MS, ASK_TOOL_REASON, CAYENNE_CHECK_MS, CAYENNE_CHECK_POLL_MS, CAYENNE_UNCONFIRMED, TOOL_FAILED, toResult, INTERRUPTED, STOPPED_TURN, ClaudeSession;
12474
+ var SESSION_FLAG_SETTINGS, SESSION_ASK_TOOLS, GUARD_FAILED, WRITE_TOOLS_MATCHER, CONTEXT_HOOK_TIMEOUT_MS, ASK_TOOL_REASON, CAYENNE_CHECK_MS, CAYENNE_CHECK_POLL_MS, CAYENNE_UNLISTED_POLL_MS, CAYENNE_UNCONFIRMED, TOOL_FAILED, toResult, INTERRUPTED, STOPPED_TURN, ClaudeSession;
12367
12475
  var init_session2 = __esm({
12368
12476
  "src/claude/session.ts"() {
12369
12477
  "use strict";
@@ -12386,6 +12494,7 @@ var init_session2 = __esm({
12386
12494
  ASK_TOOL_REASON = "This session was started from a task, so every shell command needs the member's approval.";
12387
12495
  CAYENNE_CHECK_MS = 1e4;
12388
12496
  CAYENNE_CHECK_POLL_MS = 100;
12497
+ CAYENNE_UNLISTED_POLL_MS = 1e3;
12389
12498
  CAYENNE_UNCONFIRMED = "Refused: the cayenne MCP server is not confirmed as the runner\u2019s own.";
12390
12499
  TOOL_FAILED = "Cayenne could not answer this call.";
12391
12500
  toResult = (a) => ({
@@ -12665,15 +12774,21 @@ var init_session2 = __esm({
12665
12774
  *
12666
12775
  * Any other answer stops the session. A `cayenne` with another `serverInfo` or config type is a
12667
12776
  * foreign server (`CAYENNE_NAME_TAKEN`); one never confirmed (connected without `serverInfo`, a
12668
- * status other than `pending`, still pending or unlisted at the deadline, a status call that
12669
- * fails) ends with `CAYENNE_UNCONFIRMED_EXIT`, the reason logged at warn. Until confirmed,
12670
- * `canUseTool` refuses every `cayenne` tool. A session already stopping or ended is left alone.
12777
+ * status other than `pending`, still pending at the deadline, a status call that fails) ends with
12778
+ * `CAYENNE_UNCONFIRMED_EXIT`, the reason logged at warn. Until confirmed, `canUseTool` refuses
12779
+ * every `cayenne` tool. A session already stopping or ended is left alone.
12780
+ *
12781
+ * An unlisted `cayenne` is waited for, not stopped: the CLI answers `[]` before it registers its
12782
+ * MCP servers, and on a slow network that took longer than the window (three chats killed
12783
+ * 2026-10-21/22). Nothing answers under the name meanwhile, so no tool of it can be called.
12671
12784
  */
12672
12785
  async confirmCayenne(q) {
12673
12786
  let deadline;
12787
+ let unlistedSince;
12788
+ let unlistedWarned = false;
12789
+ const warn = this.opts.log?.warn ?? ((obj, m) => logger.warn(obj, m));
12674
12790
  const end = (fatal, fields, msg) => {
12675
12791
  if (this.ending) return false;
12676
- const warn = this.opts.log?.warn ?? ((obj, m) => logger.warn(obj, m));
12677
12792
  warn({ providerSessionId: this.providerSessionId, ...fields }, msg);
12678
12793
  this.fatal = fatal;
12679
12794
  void this.stop();
@@ -12688,21 +12803,33 @@ var init_session2 = __esm({
12688
12803
  for (; ; ) {
12689
12804
  if (this.ending) return false;
12690
12805
  const statuses = await q.mcpServerStatus();
12691
- deadline ??= Date.now() + CAYENNE_CHECK_MS;
12692
12806
  const entry = statuses.find((s) => s.name === CAYENNE_SERVER);
12693
- const config2 = entry?.config;
12694
- const info = entry?.serverInfo;
12807
+ if (!entry) {
12808
+ unlistedSince ??= Date.now();
12809
+ if (!unlistedWarned && Date.now() - unlistedSince >= CAYENNE_CHECK_MS) {
12810
+ unlistedWarned = true;
12811
+ warn(
12812
+ { providerSessionId: this.providerSessionId },
12813
+ "cayenne MCP server not listed yet; its tools stay refused until it is"
12814
+ );
12815
+ }
12816
+ await new Promise((r) => setTimeout(r, CAYENNE_UNLISTED_POLL_MS));
12817
+ continue;
12818
+ }
12819
+ deadline ??= Date.now() + CAYENNE_CHECK_MS;
12820
+ const config2 = entry.config;
12821
+ const info = entry.serverInfo;
12695
12822
  if (config2 !== void 0 && config2.type !== "sdk" || info !== void 0 && (info.name !== CAYENNE_SERVER || info.version !== this.cayenneVersion)) {
12696
- return end(CAYENNE_NAME_TAKEN, { scope: entry?.scope }, CAYENNE_NAME_TAKEN);
12823
+ return end(CAYENNE_NAME_TAKEN, { scope: entry.scope }, CAYENNE_NAME_TAKEN);
12697
12824
  }
12698
- if (entry?.status === "connected") {
12825
+ if (entry.status === "connected") {
12699
12826
  return info !== void 0 || unconfirmed("connected without serverInfo", entry.scope);
12700
12827
  }
12701
- if (entry && entry.status !== "pending") {
12828
+ if (entry.status !== "pending") {
12702
12829
  return unconfirmed(`status ${entry.status}`, entry.scope);
12703
12830
  }
12704
12831
  if (Date.now() >= deadline) {
12705
- return unconfirmed(entry ? "still pending" : "not listed by mcpServerStatus");
12832
+ return unconfirmed("still pending", entry.scope);
12706
12833
  }
12707
12834
  await new Promise((r) => setTimeout(r, CAYENNE_CHECK_POLL_MS));
12708
12835
  }
@@ -14889,6 +15016,50 @@ var init_materialise = __esm({
14889
15016
  }
14890
15017
  });
14891
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
+
14892
15063
  // src/sessions/managed-git-guard.ts
14893
15064
  import { realpathSync as realpathSync15 } from "fs";
14894
15065
  import { isAbsolute as isAbsolute18, relative as relative11, resolve as resolve4, sep as sep15 } from "path";
@@ -15665,6 +15836,7 @@ var init_manager = __esm({
15665
15836
  init_registry();
15666
15837
  init_materialise();
15667
15838
  init_attachments2();
15839
+ init_autopilot_renewal();
15668
15840
  init_managed_git_guard();
15669
15841
  init_relay();
15670
15842
  init_repo_settings();
@@ -15729,6 +15901,8 @@ var init_manager = __esm({
15729
15901
  pendingBlock: null,
15730
15902
  manualPending: false,
15731
15903
  lastPrompt: null,
15904
+ autopilot: false,
15905
+ cause: null,
15732
15906
  todos: null
15733
15907
  });
15734
15908
  scopeOf2 = (v) => v === "private" || v === "workspace" ? v : null;
@@ -15885,7 +16059,9 @@ var init_manager = __esm({
15885
16059
  try {
15886
16060
  this.assertNotRemoving(cwd);
15887
16061
  } catch (err) {
15888
- throw this.giveBack(repo?.creds, err);
16062
+ const failure2 = this.giveBack(repo?.creds, err);
16063
+ await this.recordStartFailure(sessionId);
16064
+ throw failure2;
15889
16065
  }
15890
16066
  const started = this.markStarting(cwd, sessionId);
15891
16067
  const introspect = this.deps.introspect ?? ((dir, at) => introspectRepo(
@@ -15939,7 +16115,9 @@ var init_manager = __esm({
15939
16115
  );
15940
16116
  } catch (err) {
15941
16117
  this.discardSkills(sessionId);
15942
- throw this.giveBack(repo?.creds, err);
16118
+ const failure2 = this.giveBack(repo?.creds, err);
16119
+ await this.recordStartFailure(sessionId);
16120
+ throw failure2;
15943
16121
  } finally {
15944
16122
  started();
15945
16123
  }
@@ -16109,9 +16287,18 @@ var init_manager = __esm({
16109
16287
  repo: { ownerId, repositoryId: repository.id, creds }
16110
16288
  };
16111
16289
  } catch (err) {
16112
- throw this.giveBack(creds, err);
16290
+ const failure2 = this.giveBack(creds, err);
16291
+ await this.recordStartFailure(sessionId);
16292
+ throw failure2;
16113
16293
  }
16114
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
+ }
16115
16302
  /**
16116
16303
  * A repository chat's recorded worktree, proven as a resume needs it (§2.9): this runner's record,
16117
16304
  * still the same worktree, shaped `<root>/<owner>/<name>/.cayenne/worktrees/chat-<hex>` for this
@@ -16943,14 +17130,19 @@ var init_manager = __esm({
16943
17130
  * turns. Returns `{ scheduled: true }` at once and never waits for the renewal, so a
16944
17131
  * `session.permission.respond` or `session.interrupt` queued behind it in the session lane runs
16945
17132
  * while the renewal waits for the turn to end (§2.1). Progress goes out as `renewal` events.
16946
- * Refused when the effective threshold is below 100,000 tokens, and for a session that cannot be
16947
- * 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).
16948
17136
  */
16949
17137
  async requestRenewal(sessionId, createdBy) {
16950
17138
  const live = await this.ensureLive(sessionId, createdBy);
16951
17139
  this.assertCodexOwner(sessionId, createdBy);
16952
17140
  const r = live.renewal;
16953
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
+ }
16954
17146
  const machine = r.machine;
16955
17147
  if (machine && !machine.enabled) throw new Error(RENEWAL_NOT_AVAILABLE);
16956
17148
  if (r.busy) return { scheduled: true };
@@ -19275,8 +19467,9 @@ var init_manager = __esm({
19275
19467
  * failed turn, a pending permission), so they are never left waiting for a turn that won't come.
19276
19468
  */
19277
19469
  async turnEndRenewal(sessionId, live, r) {
19278
- const usage = await this.measure(sessionId, live);
19279
19470
  const rn = live.renewal;
19471
+ if (this.renewable(live)) rn.autopilot = await this.autopilotRunning(sessionId);
19472
+ const usage = await this.measure(sessionId, live);
19280
19473
  await this.noteScope(sessionId, live);
19281
19474
  if (rn.busy || live.stopping || live.dropping) return [];
19282
19475
  if (live.turns.owed > 0) return [];
@@ -19442,10 +19635,14 @@ var init_manager = __esm({
19442
19635
  async ensureMachine(sessionId, live, window) {
19443
19636
  const rn = live.renewal;
19444
19637
  if (rn.segment === null) return;
19445
- if (rn.machine && (rn.window === window || rn.machine.state !== "idle")) return;
19446
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
+ }
19447
19644
  rn.window = window;
19448
- rn.threshold = renewThreshold({ workspaceThreshold: rn.workspaceThreshold, window });
19645
+ rn.threshold = renewThreshold({ ...subject, window });
19449
19646
  rn.machine = new RenewalMachine({
19450
19647
  segment: rn.segment,
19451
19648
  threshold: rn.threshold,
@@ -19458,15 +19655,55 @@ var init_manager = __esm({
19458
19655
  if (rn.machine.enabled) {
19459
19656
  rn.machine.requestRenew("manual");
19460
19657
  } else {
19461
- await this.renewalEvent(sessionId, {
19462
- kind: "event",
19463
- phase: "failed",
19464
- segment: rn.segment,
19465
- error: RENEWAL_NOT_AVAILABLE
19466
- });
19658
+ await this.renewalEvent(
19659
+ sessionId,
19660
+ { kind: "event", phase: "failed", segment: rn.segment, error: RENEWAL_NOT_AVAILABLE },
19661
+ "manual"
19662
+ );
19467
19663
  }
19468
19664
  }
19469
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
+ }
19470
19707
  /** `workspace_settings.renewal_threshold_tokens`; the column default when it cannot be read. */
19471
19708
  async workspaceThreshold() {
19472
19709
  const { data, error } = await this.deps.db.from("workspace_settings").select("renewal_threshold_tokens").eq("workspace_id", this.deps.workspaceId).maybeSingle();
@@ -19500,15 +19737,37 @@ var init_manager = __esm({
19500
19737
  rn.busy = true;
19501
19738
  try {
19502
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;
19503
19753
  let prompt = null;
19504
19754
  for (const a of start2) {
19505
- if (a.kind === "event") await this.renewalEvent(sessionId, a);
19755
+ if (a.kind === "event") await this.renewalEvent(sessionId, a, reason);
19506
19756
  else if (a.kind === "runHandoffTurn") prompt = a.prompt;
19507
19757
  }
19508
19758
  if (prompt === null) return;
19509
19759
  const result = await this.runHandoffTurn(live, prompt);
19510
19760
  if (gone2()) return;
19511
- 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
+ );
19512
19771
  try {
19513
19772
  for (const a of machine.handoffCaptured(handoff)) {
19514
19773
  if (a.kind === "saveHandoff") {
@@ -19518,7 +19777,7 @@ var init_manager = __esm({
19518
19777
  () => this.deps.db.from("chat_segments").update({ handoff: a.handoff, handoff_source: a.handoffSource }).eq("session_id", sessionId).eq("idx", a.segment)
19519
19778
  );
19520
19779
  } else if (a.kind === "event") {
19521
- await this.renewalEvent(sessionId, a);
19780
+ await this.renewalEvent(sessionId, a, reason);
19522
19781
  } else if (a.kind === "startQuery") {
19523
19782
  next = await this.startRenewedQuery(sessionId, live);
19524
19783
  }
@@ -19527,10 +19786,13 @@ var init_manager = __esm({
19527
19786
  const providerSessionId = next?.providerSessionId;
19528
19787
  if (!next || !providerSessionId) throw new Error("the new query has no session id");
19529
19788
  const first = rn.queued[0] ?? null;
19789
+ if (run && !first && !await this.autopilotRunning(sessionId)) run = null;
19790
+ if (gone2()) return;
19530
19791
  const actions = machine.queryStarted({
19531
19792
  providerSessionId,
19532
19793
  queuedPrompt: first?.text ?? null,
19533
- lastMemberPrompt: rn.lastPrompt
19794
+ lastMemberPrompt: autopilot ? null : rn.lastPrompt,
19795
+ autopilotContinuation: run ? this.secrets.redact(autopilotContinuation(run)) : null
19534
19796
  });
19535
19797
  await this.rotateSegment(sessionId, live, n, handoff.text, actions, next, first);
19536
19798
  } catch (err) {
@@ -19598,12 +19860,12 @@ var init_manager = __esm({
19598
19860
  * before the memory pipeline) or the segment's last messages; plus the deterministic appendix.
19599
19861
  * Scrubbed (D50) before it is stored and before it becomes segment `n + 1`'s first input.
19600
19862
  */
19601
- async handoffFor(sessionId, live, n, result) {
19863
+ async handoffFor(sessionId, live, n, result, extra = []) {
19602
19864
  const fromModel = result.kind === "ok" && estimateTokens(result.text.trim()) >= MIN_HANDOFF_TOKENS;
19603
19865
  const messages = fromModel ? [] : await this.segmentMessages(sessionId, n);
19604
19866
  const ceiling = live.roots.find((root) => isAtOrBelow3(live.cwd, root)) ?? live.cwd;
19605
19867
  const git = await this.appendixGitState(live.cwd, ceiling).catch(() => null);
19606
- const appendix = buildAppendix(git, live.renewal.todos, [], []);
19868
+ const appendix = buildAppendix(git, live.renewal.todos, [], [], extra);
19607
19869
  const handoff = resolveHandoff({ result, digest: null, messages, appendix });
19608
19870
  return { ...handoff, text: this.secrets.redact(handoff.text) };
19609
19871
  }
@@ -19751,7 +20013,8 @@ var init_manager = __esm({
19751
20013
  case "event":
19752
20014
  await this.renewalEvent(
19753
20015
  sessionId,
19754
- 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
19755
20018
  );
19756
20019
  break;
19757
20020
  default:
@@ -20025,7 +20288,8 @@ ${text3}`;
20025
20288
  if (machine.state !== "failed" && live.renewal.machine === machine) live.renewal.machine = null;
20026
20289
  for (const a of actions) {
20027
20290
  if (a.kind === "clearHandoff") await this.clearHandoff(sessionId, a.segment);
20028
- 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);
20029
20293
  }
20030
20294
  if (live.renewal.scopeRenewalOwed && live.renewal.queued.length > 0) {
20031
20295
  try {
@@ -20076,16 +20340,23 @@ ${text3}`;
20076
20340
  throw new Error(`PROVIDER_ERROR: ${what} failed: ${error.message}`);
20077
20341
  }
20078
20342
  }
20079
- /** A machine `event` action as a `renewal` session event (protocol §3). */
20080
- 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) {
20081
20348
  await this.broadcast(sessionId, {
20082
20349
  type: "renewal",
20083
20350
  phase: a.phase,
20084
20351
  segment: a.segment,
20085
20352
  ...a.fromTokens === void 0 ? {} : { fromTokens: a.fromTokens },
20086
20353
  ...a.toTokens === void 0 ? {} : { toTokens: a.toTokens },
20087
- ...a.error === void 0 ? {} : { error: this.cap(a.error) }
20354
+ ...a.error === void 0 ? {} : { error: this.cap(a.error) },
20355
+ ...reason === void 0 ? {} : { reason }
20088
20356
  });
20357
+ if (a.phase === "done" && reason !== void 0) {
20358
+ await this.broadcast(sessionId, { type: "session.renewed", segment: a.segment, reason });
20359
+ }
20089
20360
  }
20090
20361
  /**
20091
20362
  * Expire a request the session settled itself, and say so when this write is what settled the
@@ -22404,7 +22675,9 @@ async function compose(cfg, lock, host, preflight) {
22404
22675
  publishWrites: makePublishWrites(db),
22405
22676
  poll: prPoll,
22406
22677
  gh: runGh
22407
- }
22678
+ },
22679
+ // Auto Pilot 03: an active run renews at 40% of the window and continues on its own.
22680
+ ...autopilotRunsReader(db, logger)
22408
22681
  });
22409
22682
  let ownerServed = liveRoster.serves(identity.userId);
22410
22683
  let ownerTransition = Promise.resolve();
@@ -22794,6 +23067,7 @@ var init_start = __esm({
22794
23067
  init_roster();
22795
23068
  init_registry();
22796
23069
  init_attachments2();
23070
+ init_autopilot_runs();
22797
23071
  init_handlers();
22798
23072
  init_manager();
22799
23073
  init_repo_sessions();
@@ -23163,7 +23437,9 @@ var isInside = (dir, path) => {
23163
23437
  };
23164
23438
  function isTemporary(path, tmpDir = tmpdir()) {
23165
23439
  const roots = [resolve(tmpDir), realOrSpelled(tmpDir)];
23166
- 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
+ );
23167
23443
  }
23168
23444
  async function resolveClaude(exec4, tmpDir) {
23169
23445
  const candidates = await whichAll(exec4, "claude");
@@ -23180,8 +23456,9 @@ Put the claude CLI itself on PATH, or run this from another terminal: https://cl
23180
23456
  "claude CLI not found on PATH. The runner spawns it to run sessions.\nInstall it first: https://claude.com/claude-code"
23181
23457
  );
23182
23458
  }
23183
- function buildPathEnv(nodePath, claudePath) {
23459
+ function buildPathEnv(nodePath, claudePath, codexPath) {
23184
23460
  const dirs = ["/usr/bin", "/bin", "/usr/sbin", "/sbin", dirname2(nodePath), dirname2(claudePath)];
23461
+ if (codexPath) dirs.push(dirname2(codexPath));
23185
23462
  return [...new Set(dirs)].join(":");
23186
23463
  }
23187
23464
  function readInstalledVersion(appDir, pkg) {
@@ -23203,6 +23480,9 @@ function readInstallRecord(cayenneHome2) {
23203
23480
  }
23204
23481
  async function installApp(deps) {
23205
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
+ );
23206
23486
  const appDir = join6(deps.cayenneHome, "app");
23207
23487
  mkdirSync3(appDir, { recursive: true, mode: 448 });
23208
23488
  const install = await deps.exec("npm", [
@@ -23227,7 +23507,7 @@ async function installApp(deps) {
23227
23507
  claudePath,
23228
23508
  cliPath,
23229
23509
  appDir,
23230
- pathEnv: buildPathEnv(nodePath, claudePath),
23510
+ pathEnv: buildPathEnv(nodePath, claudePath, codexPath),
23231
23511
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
23232
23512
  };
23233
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.12",
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",