@centralu/darwin-arm64 0.1.0-beta.35 → 0.1.0-beta.36

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.
@@ -43055,7 +43055,7 @@ import { mkdirSync as mkdirSync19, writeSync as writeSync3 } from "node:fs";
43055
43055
 
43056
43056
  // packages/protocol/src/brand.ts
43057
43057
  var APP_NAME = "Centralu";
43058
- var APP_VERSION = "0.1.0-beta.35";
43058
+ var APP_VERSION = "0.1.0-beta.36";
43059
43059
  var APP_SLUG = "centralu";
43060
43060
  var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
43061
43061
  var DATA_DIR = ".centralu";
@@ -58787,6 +58787,13 @@ var RequestInput = external_exports.object({
58787
58787
  * when the tool does not say, or the request wrote nothing.
58788
58788
  */
58789
58789
  cacheTtl: external_exports.enum(["5m", "1h"]).optional(),
58790
+ /**
58791
+ * Of `cacheCreationTokens`, what the API said stays cached an hour and five minutes (Claude's `cache_creation` split, #1492):
58792
+ * the two are priced apart (docs/agent-host.md §6.1), so the weighting of writes needs them, not only which one the request had
58793
+ * most of. Absent when the tool did not split its writes; the part that is in neither is priced as a five-minute write.
58794
+ */
58795
+ cacheWrite1hTokens: external_exports.number().optional(),
58796
+ cacheWrite5mTokens: external_exports.number().optional(),
58790
58797
  /** The model that served this request, as the tool reported it (Claude's `message.model`); absent when it does not say (usage history, §6.2) */
58791
58798
  model: external_exports.string().optional()
58792
58799
  });
@@ -58813,7 +58820,13 @@ var CacheSessionStats = external_exports.object({
58813
58820
  cacheCreationTokens: external_exports.number(),
58814
58821
  rebuilds: external_exports.number(),
58815
58822
  /** The rebuilds by their likely cause (`RebuildCause`); absent from a host that predates it */
58816
- rebuildCauses: external_exports.record(external_exports.string(), external_exports.number()).optional()
58823
+ rebuildCauses: external_exports.record(external_exports.string(), external_exports.number()).optional(),
58824
+ /**
58825
+ * What the rebuilds wrote to the cache, in tokens (#1492): the sum of the cache writes of the requests counted as rebuilds, so the
58826
+ * steady writes every new message needs are not in it. For a tool that does not report writes (Codex) the input it did not read
58827
+ * from the cache stands in. Absent from a host that predates it, and from hours an earlier build stored.
58828
+ */
58829
+ rebuildTokens: external_exports.number().optional()
58817
58830
  });
58818
58831
  var CacheWindowStats = external_exports.object({
58819
58832
  /** `5h`, `today` */
@@ -58830,6 +58843,17 @@ var CacheWindowStats = external_exports.object({
58830
58843
  rebuilds: external_exports.number(),
58831
58844
  /** The rebuilds by their likely cause (`RebuildCause`); absent from a host that predates it */
58832
58845
  rebuildCauses: external_exports.record(external_exports.string(), external_exports.number()).optional(),
58846
+ /** What the rebuilds wrote to the cache, in tokens (`CacheSessionStats.rebuildTokens`) */
58847
+ rebuildTokens: external_exports.number().optional(),
58848
+ /**
58849
+ * What the cache cost in units of an uncached input token (#1492), for the share the writes were of it: the tokens read from the cache
58850
+ * times the price of a read, and the tokens written times the price of a write, each as a ratio to uncached input (Anthropic: read 0.1,
58851
+ * five-minute write 1.25, one-hour write 2; Codex by model, docs/agent-host.md §6.1). No money: the ratios, not the prices. For a tool
58852
+ * that does not report writes (Codex) the write units are those of the input it did not read from the cache. Absent from a host that
58853
+ * predates them.
58854
+ */
58855
+ cacheReadCost: external_exports.number().optional(),
58856
+ cacheWriteCost: external_exports.number().optional(),
58833
58857
  /**
58834
58858
  * The shortest and longest idle time before a rebuild put down to the cache expiring (`expired`, `probably_expired`),
58835
58859
  * in milliseconds; absent when there was none
@@ -59109,14 +59133,33 @@ function readNotifyPolicy(raw) {
59109
59133
  var MIN_AUTO_COMPACT_SHARE = 0.2;
59110
59134
  var MIN_IDLE_COMPACT_SHARE = 0.1;
59111
59135
  var MAX_AUTO_COMPACT_SHARE = 0.95;
59112
- var DEFAULT_AUTO_COMPACT = { controlled: true, share: 0.6, idleShare: 0.3 };
59136
+ var MIN_NUDGE_SHARE = 0.1;
59137
+ var MAX_NUDGE_SHARE = 0.9;
59138
+ var NUDGE_SECOND_GAP = 0.1;
59139
+ var NUDGE_MARGIN = 0.05;
59140
+ var DEFAULT_AUTO_COMPACT = { controlled: true, share: 0.6, idleShare: 0.3, nudge: true, nudgeFirst: 0.3, nudgeSecond: null };
59141
+ var hundredths = (n) => Math.round(n * 100) / 100;
59142
+ function nudgeShares(c) {
59143
+ const ceiling = hundredths(c.share - NUDGE_MARGIN);
59144
+ const second = hundredths(c.nudgeSecond ?? c.share - NUDGE_SECOND_GAP);
59145
+ return { first: c.nudgeFirst, second: Math.min(ceiling, Math.max(MIN_NUDGE_SHARE, second)) };
59146
+ }
59113
59147
  function readAutoCompact(raw) {
59114
59148
  const out = { ...DEFAULT_AUTO_COMPACT };
59115
59149
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return out;
59116
59150
  if ("controlled" in raw && typeof raw.controlled === "boolean") out.controlled = raw.controlled;
59117
59151
  if ("share" in raw && typeof raw.share === "number" && Number.isFinite(raw.share)) out.share = Math.min(MAX_AUTO_COMPACT_SHARE, Math.max(MIN_AUTO_COMPACT_SHARE, raw.share));
59118
59152
  if ("idleShare" in raw && typeof raw.idleShare === "number" && Number.isFinite(raw.idleShare)) out.idleShare = Math.min(MAX_AUTO_COMPACT_SHARE, Math.max(MIN_IDLE_COMPACT_SHARE, raw.idleShare));
59119
- return out;
59153
+ if ("nudge" in raw && typeof raw.nudge === "boolean") out.nudge = raw.nudge;
59154
+ if ("nudgeFirst" in raw && typeof raw.nudgeFirst === "number" && Number.isFinite(raw.nudgeFirst)) out.nudgeFirst = Math.min(MAX_NUDGE_SHARE, Math.max(MIN_NUDGE_SHARE, raw.nudgeFirst));
59155
+ if ("nudgeSecond" in raw && typeof raw.nudgeSecond === "number" && Number.isFinite(raw.nudgeSecond)) out.nudgeSecond = Math.min(MAX_NUDGE_SHARE, Math.max(MIN_NUDGE_SHARE, raw.nudgeSecond));
59156
+ return settledSecond(out);
59157
+ }
59158
+ function settledSecond(c) {
59159
+ if (c.nudgeSecond === null) return c;
59160
+ const held3 = Math.min(c.nudgeSecond, hundredths(c.share - NUDGE_MARGIN));
59161
+ const nudgeSecond = held3 > c.nudgeFirst ? held3 : null;
59162
+ return nudgeSecond === c.nudgeSecond ? c : { ...c, nudgeSecond };
59120
59163
  }
59121
59164
  var LineHeight = external_exports.enum(["compact", "normal", "relaxed"]);
59122
59165
  var UiPreferences = external_exports.object({
@@ -59234,7 +59277,11 @@ var UiPreferencesPatch = UiPreferences.partial().extend({
59234
59277
  autoCompact: external_exports.object({
59235
59278
  controlled: external_exports.boolean(),
59236
59279
  share: external_exports.number().min(MIN_AUTO_COMPACT_SHARE).max(MAX_AUTO_COMPACT_SHARE),
59237
- idleShare: external_exports.number().min(MIN_IDLE_COMPACT_SHARE).max(MAX_AUTO_COMPACT_SHARE)
59280
+ idleShare: external_exports.number().min(MIN_IDLE_COMPACT_SHARE).max(MAX_AUTO_COMPACT_SHARE),
59281
+ nudge: external_exports.boolean(),
59282
+ nudgeFirst: external_exports.number().min(MIN_NUDGE_SHARE).max(MAX_NUDGE_SHARE),
59283
+ /** null: back to the default, below `share` by `NUDGE_SECOND_GAP` */
59284
+ nudgeSecond: external_exports.number().min(MIN_NUDGE_SHARE).max(MAX_NUDGE_SHARE).nullable()
59238
59285
  }).partial().optional()
59239
59286
  });
59240
59287
  var DEFAULT_AGENT_DEPTH = 1;
@@ -59735,7 +59782,8 @@ var NormalizedEvent = external_exports.discriminatedUnion("type", [
59735
59782
  after: external_exports.number().optional(),
59736
59783
  /**
59737
59784
  * Who started it, when the tool says (Claude's `compact_metadata.trigger`): `manual` is a `/compact` (the person's or
59738
- * Centralu's, #1451), `auto` the tool's own at its threshold. Centralu does not take an `auto` one for its own.
59785
+ * Centralu's, #1451), `auto` the tool's own at its threshold. Centralu does not take an `auto` one for its own. Codex does
59786
+ * not say why; its adapter says `manual` for a compaction inside the compact turn the session started, `auto` otherwise (#1500).
59739
59787
  */
59740
59788
  trigger: external_exports.enum(["manual", "auto"]).optional()
59741
59789
  }),
@@ -65780,12 +65828,12 @@ var HostServer = class _HostServer {
65780
65828
  }
65781
65829
  const { method, params, id: id3 } = frame.data;
65782
65830
  if (bridgeOf !== null) {
65783
- const refusal = bridgeRefusal(bridgeOf, method, params);
65784
- if (refusal !== null) return this.sendError(ws2, id3, { code: "internal", message: refusal, retryable: false });
65831
+ const refusal2 = bridgeRefusal(bridgeOf, method, params);
65832
+ if (refusal2 !== null) return this.sendError(ws2, id3, { code: "internal", message: refusal2, retryable: false });
65785
65833
  }
65786
65834
  if (limited) {
65787
- const refusal = limitedRefusal(method, params);
65788
- if (refusal?.rights === "asks" && this.opts.onAsk) {
65835
+ const refusal2 = limitedRefusal(method, params);
65836
+ if (refusal2?.rights === "asks" && this.opts.onAsk) {
65789
65837
  try {
65790
65838
  const result = await this.opts.onAsk(method, params, { client: label }, leaving.signal);
65791
65839
  this.sendTo(ws2, JSON.stringify({ kind: "res", id: id3, ok: true, result }));
@@ -65795,9 +65843,9 @@ var HostServer = class _HostServer {
65795
65843
  }
65796
65844
  return;
65797
65845
  }
65798
- if (refusal !== null) {
65799
- console.error(`[agent-host] refused ${JSON.stringify(method)} to a connection holding the limited credential (${refusal.rights})`);
65800
- return this.sendError(ws2, id3, { code: "internal", message: refusal.message, retryable: false, data: { refused: "rights", rights: refusal.rights } });
65846
+ if (refusal2 !== null) {
65847
+ console.error(`[agent-host] refused ${JSON.stringify(method)} to a connection holding the limited credential (${refusal2.rights})`);
65848
+ return this.sendError(ws2, id3, { code: "internal", message: refusal2.message, retryable: false, data: { refused: "rights", rights: refusal2.rights } });
65801
65849
  }
65802
65850
  }
65803
65851
  try {
@@ -102663,8 +102711,8 @@ var InlineViews = class {
102663
102711
  if (this.deps.rt.list().find((a) => a.appId === v.ref.appId && a.projectId === v.ref.projectId)?.status === "failed") {
102664
102712
  refuse3("This app stopped after failing repeatedly. Restart it, then reopen this view");
102665
102713
  }
102666
- const refusal = await this.refusal(v.ref, v.uri);
102667
- if (refusal) refuse3(refusal);
102714
+ const refusal2 = await this.refusal(v.ref, v.uri);
102715
+ if (refusal2) refuse3(refusal2);
102668
102716
  if (!v.instanceId) {
102669
102717
  let instanceId;
102670
102718
  try {
@@ -102751,10 +102799,10 @@ var InlineViews = class {
102751
102799
  }
102752
102800
  if (this.disposed) return;
102753
102801
  const base2 = { type: "app_view", sessionId: c.sessionId, callId, appId: c.ref.appId, projectId: c.ref.projectId, tool: c.tool };
102754
- const refusal = await this.refusal(c.ref, ui2.uri);
102755
- if (refusal) {
102756
- this.log(`[apps] ${where2}: view rejected \u2014 ${refusal}`);
102757
- this.deps.emit({ ...base2, phase: "rejected", reason: refusal });
102802
+ const refusal2 = await this.refusal(c.ref, ui2.uri);
102803
+ if (refusal2) {
102804
+ this.log(`[apps] ${where2}: view rejected \u2014 ${refusal2}`);
102805
+ this.deps.emit({ ...base2, phase: "rejected", reason: refusal2 });
102758
102806
  return;
102759
102807
  }
102760
102808
  let instanceId;
@@ -107134,7 +107182,10 @@ var ORCHESTRATOR_TOOLS = [
107134
107182
  autoCompact: external_exports.object({
107135
107183
  controlled: external_exports.boolean().optional().describe("Whether Centralu sets when agents compact (default on); off, each agent's own settings decide."),
107136
107184
  share: external_exports.number().optional().describe("Share of the model's window to compact at, 0.2 to 0.95 (default 0.6)."),
107137
- idleShare: external_exports.number().optional().describe("Share over which a session at rest for 10 minutes is compacted by Centralu, 0.1 to 0.95 (default 0.3).")
107185
+ idleShare: external_exports.number().optional().describe("Share over which a session at rest for 10 minutes is compacted by Centralu, 0.1 to 0.95 (default 0.3)."),
107186
+ nudge: external_exports.boolean().optional().describe("Whether Centralu quietly tells a session to call compact_self as its context grows (default on)."),
107187
+ nudgeFirst: external_exports.number().optional().describe("Share of the window for the first notice, 0.1 to 0.9 (default 0.3)."),
107188
+ nudgeSecond: external_exports.number().nullable().optional().describe("Share for the second notice, 0.1 to 0.9 and 5+ points below share; null for the default, 10 points below share.")
107138
107189
  }).strict().optional().describe("When agents compact by themselves.")
107139
107190
  }).strict()
107140
107191
  },
@@ -109159,7 +109210,7 @@ ${machineLine(machine)}` };
109159
109210
  var HOUR_MS = 60 * 6e4;
109160
109211
  var BUCKET_COLUMNS = `hour, session_id AS sessionId, project_id AS projectId, tool, model, machine, requests, input,
109161
109212
  cache_read AS cacheRead, cache_write AS cacheWrite, cache_write_5m AS cacheWrite5m, cache_write_1h AS cacheWrite1h,
109162
- output, rebuilds, causes`;
109213
+ output, rebuilds, rebuild_tokens AS rebuildTokens, causes`;
109163
109214
  var UsageRecords = class {
109164
109215
  constructor(db) {
109165
109216
  this.db = db;
@@ -109215,14 +109266,14 @@ var UsageRecords = class {
109215
109266
  );
109216
109267
  const upsert = this.db.prepare(
109217
109268
  `INSERT INTO usage_buckets (hour, session_id, project_id, tool, model, machine, requests, input, cache_read, cache_write,
109218
- cache_write_5m, cache_write_1h, output, rebuilds, causes)
109269
+ cache_write_5m, cache_write_1h, output, rebuilds, rebuild_tokens, causes)
109219
109270
  VALUES (@hour, @sessionId, @projectId, @tool, @model, @machine, @requests, @input, @cacheRead, @cacheWrite,
109220
- @cacheWrite5m, @cacheWrite1h, @output, @rebuilds, @causes)
109271
+ @cacheWrite5m, @cacheWrite1h, @output, @rebuilds, @rebuildTokens, @causes)
109221
109272
  ON CONFLICT(hour, session_id, project_id, tool, model, machine) DO UPDATE SET
109222
109273
  requests = requests + excluded.requests, input = input + excluded.input, cache_read = cache_read + excluded.cache_read,
109223
109274
  cache_write = cache_write + excluded.cache_write, cache_write_5m = cache_write_5m + excluded.cache_write_5m,
109224
109275
  cache_write_1h = cache_write_1h + excluded.cache_write_1h, output = output + excluded.output,
109225
- rebuilds = rebuilds + excluded.rebuilds, causes = excluded.causes`
109276
+ rebuilds = rebuilds + excluded.rebuilds, rebuild_tokens = rebuild_tokens + excluded.rebuild_tokens, causes = excluded.causes`
109226
109277
  );
109227
109278
  this.db.transaction(() => {
109228
109279
  for (const b2 of buckets) {
@@ -113674,7 +113725,10 @@ var UPDATE_CARD = 'Turn on "Automatic updates"? From then on this computer check
113674
113725
  var FOLDED_GROUPS_MAX = 500;
113675
113726
  var SLEEP_CHOICES = 'a whole number of minutes, or "never"';
113676
113727
  function autoCompactText(c) {
113677
- return c.controlled ? `agents compact at ${Math.round(c.share * 100)}% of the model's window, set by Centralu; a session at rest is compacted over ${Math.round(c.idleShare * 100)}%` : "Centralu does not set when agents compact; each agent's own settings decide";
113728
+ if (!c.controlled) return "Centralu does not set when agents compact; each agent's own settings decide";
113729
+ const n = nudgeShares(c);
113730
+ const nudge = c.nudge ? `a session is told quietly to call compact_self at ${Math.round(n.first * 100)}% and ${Math.round(n.second * 100)}%` : "a session is not told to call compact_self";
113731
+ return `agents compact at ${Math.round(c.share * 100)}% of the model's window, set by Centralu; a session at rest is compacted over ${Math.round(c.idleShare * 100)}%; ${nudge}`;
113678
113732
  }
113679
113733
  var PreferencesFlow = class {
113680
113734
  constructor(host) {
@@ -115442,7 +115496,13 @@ var ProjectConsents = class {
115442
115496
 
115443
115497
  // packages/agent-host/src/sessions/ui-preferences.ts
115444
115498
  var UI_PREFS_KEY = "ui_preferences";
115445
- var IDLE_SHARE_KEY = "autoCompactIdleShare";
115499
+ var COMPACT_BESIDE = {
115500
+ idleShare: "autoCompactIdleShare",
115501
+ nudge: "autoCompactNudge",
115502
+ nudgeFirst: "autoCompactNudgeFirst",
115503
+ nudgeSecond: "autoCompactNudgeSecond"
115504
+ };
115505
+ var COMPACT_BESIDE_FIELDS = ["idleShare", "nudge", "nudgeFirst", "nudgeSecond"];
115446
115506
  var UiPreferenceStore = class {
115447
115507
  constructor(host) {
115448
115508
  this.host = host;
@@ -115479,8 +115539,9 @@ var UiPreferenceStore = class {
115479
115539
  }
115480
115540
  if (this.brokenRow() || "hubAuto" in stored && typeof stored.hubAuto !== "boolean") prefs.hubAuto = false;
115481
115541
  prefs.sessionTools = true;
115482
- const idle = stored[IDLE_SHARE_KEY];
115483
- if (typeof idle === "number" && Number.isFinite(idle)) prefs.autoCompact = readAutoCompact({ ...prefs.autoCompact ?? {}, idleShare: idle });
115542
+ const beside = {};
115543
+ for (const field of COMPACT_BESIDE_FIELDS) if (COMPACT_BESIDE[field] in stored) beside[field] = stored[COMPACT_BESIDE[field]];
115544
+ if (Object.keys(beside).length > 0) prefs.autoCompact = readAutoCompact({ ...prefs.autoCompact ?? {}, ...beside });
115484
115545
  return prefs;
115485
115546
  }
115486
115547
  /**
@@ -115495,21 +115556,37 @@ var UiPreferenceStore = class {
115495
115556
  const { notifyPolicy: switches, autoCompact: compact, sessionTools: _alwaysOn, ...rest } = patch;
115496
115557
  const base2 = current.notifyPolicy ?? readNotifyPolicy(this.loadWorkspace()?.notifyPolicy);
115497
115558
  const notifyPolicy = switches ? { ...base2, ...switches } : void 0;
115498
- const { idleShare, ...compactRest } = compact ?? {};
115499
- const merged = compact ? { ...readAutoCompact(current.autoCompact), ...compact } : void 0;
115500
- const autoCompact = Object.keys(compactRest).length > 0 && merged ? { controlled: merged.controlled, share: merged.share } : void 0;
115559
+ const before = readAutoCompact(current.autoCompact);
115560
+ const asked = compact ? { ...before, ...withoutUndefined(compact) } : void 0;
115561
+ if (compact && asked && asked.nudgeSecond !== null && (compact.nudgeSecond != null || compact.nudgeFirst !== void 0)) {
115562
+ const second = Math.round(asked.nudgeSecond * 100);
115563
+ if (asked.nudgeSecond > asked.share - NUDGE_MARGIN + 1e-9) {
115564
+ throw refusal(`The second reminder (${second}%) must be at least ${Math.round(NUDGE_MARGIN * 100)} points below the share agents compact at (${Math.round(asked.share * 100)}%): the tool's own compaction would come first.`);
115565
+ }
115566
+ if (asked.nudgeSecond <= asked.nudgeFirst + 1e-9) {
115567
+ throw refusal(`The second reminder (${second}%) must be above the first (${Math.round(asked.nudgeFirst * 100)}%).`);
115568
+ }
115569
+ }
115570
+ const merged = asked ? readAutoCompact(asked) : void 0;
115571
+ const compactKeys = Object.keys(withoutUndefined(compact ?? {}));
115572
+ const autoCompact = merged && compactKeys.some((k2) => !(k2 in COMPACT_BESIDE)) ? { controlled: merged.controlled, share: merged.share } : void 0;
115573
+ const besides = {};
115574
+ if (compact && merged) {
115575
+ for (const field of COMPACT_BESIDE_FIELDS) if (compact[field] !== void 0 || merged[field] !== before[field]) besides[COMPACT_BESIDE[field]] = merged[field];
115576
+ }
115501
115577
  const written = {
115502
115578
  ...rest,
115503
115579
  ...notifyPolicy ? { notifyPolicy } : {},
115504
115580
  ...autoCompact ? { autoCompact } : {},
115505
- ...idleShare !== void 0 && merged ? { [IDLE_SHARE_KEY]: merged.idleShare } : {}
115581
+ ...besides
115506
115582
  };
115507
115583
  const stored = this.storedUiPreferences();
115508
115584
  const damaged = this.brokenRow() || "hubAuto" in stored && typeof stored.hubAuto !== "boolean";
115509
115585
  const kept = damaged && patch.hubAuto === void 0 ? { hubAuto: false } : {};
115510
115586
  this.host.store.settings.setAppSetting(UI_PREFS_KEY, JSON.stringify({ ...stored, ...kept, ...written }));
115511
115587
  if (Object.keys(written).length) this.host.emit({ type: "prefs_changed" });
115512
- const { [IDLE_SHARE_KEY]: _beside, autoCompact: _two, ...shown3 } = written;
115588
+ const { autoCompact: _two, ...shown3 } = written;
115589
+ for (const key of Object.values(COMPACT_BESIDE)) delete shown3[key];
115513
115590
  return { ...current, ...shown3, ...merged ? { autoCompact: merged } : {} };
115514
115591
  }
115515
115592
  /** The stored record as written, every field kept; `{}` for none or for one that is not a JSON object */
@@ -115531,6 +115608,19 @@ var UiPreferenceStore = class {
115531
115608
  return raw !== null && !isObj(parseJson(raw));
115532
115609
  }
115533
115610
  };
115611
+ function refusal(message) {
115612
+ return rpcError(message, "refused", { retryable: false, data: { reason: "bad_value" } });
115613
+ }
115614
+ function withoutUndefined(o) {
115615
+ return {
115616
+ ...o.controlled !== void 0 ? { controlled: o.controlled } : {},
115617
+ ...o.share !== void 0 ? { share: o.share } : {},
115618
+ ...o.idleShare !== void 0 ? { idleShare: o.idleShare } : {},
115619
+ ...o.nudge !== void 0 ? { nudge: o.nudge } : {},
115620
+ ...o.nudgeFirst !== void 0 ? { nudgeFirst: o.nudgeFirst } : {},
115621
+ ...o.nudgeSecond !== void 0 ? { nudgeSecond: o.nudgeSecond } : {}
115622
+ };
115623
+ }
115534
115624
 
115535
115625
  // packages/agent-host/src/sessions/session-apps.ts
115536
115626
  var TOOL_LIST_WAIT_MS = 15e3;
@@ -119556,6 +119646,61 @@ var MailDelivery = class {
119556
119646
  }
119557
119647
  };
119558
119648
 
119649
+ // packages/agent-host/src/sessions/context-meter.ts
119650
+ var ESTIMATE_GAP_MS = 3e3;
119651
+ var ContextMeter = class {
119652
+ constructor(now = Date.now) {
119653
+ this.now = now;
119654
+ }
119655
+ now;
119656
+ sessions = /* @__PURE__ */ new Map();
119657
+ state(id3) {
119658
+ let s = this.sessions.get(id3);
119659
+ if (!s) this.sessions.set(id3, s = { working: false, estimated: false, readThisTurn: false, owed: false, at: 0 });
119660
+ return s;
119661
+ }
119662
+ /**
119663
+ * Whether a context reading, before it is applied, is the turn before's landing in a turn that has an estimate of its own: then it
119664
+ * must not become the session's figure. Sees the tool's readings only; an estimate is never stale.
119665
+ */
119666
+ stale(e) {
119667
+ if (e.type !== "context_update" || e.exactness === "estimate") return false;
119668
+ const s = this.state(e.sessionId);
119669
+ const late = s.owed;
119670
+ s.owed = false;
119671
+ if (late) return s.working && s.estimated;
119672
+ s.readThisTurn = true;
119673
+ return false;
119674
+ }
119675
+ /**
119676
+ * After an event is applied (`m` is the session as it now is): the estimate it brings, if any, as the event to apply.
119677
+ * `perRequest`: the tool reads the size with every request (Codex sends it right after the request's token count), so the
119678
+ * figure is exact without an estimate, which could only flicker in front of it (#1504 review)
119679
+ */
119680
+ observe(e, m, perRequest = false) {
119681
+ const s = this.state(m.id);
119682
+ if (e.type === "session_deleted") {
119683
+ this.sessions.delete(m.id);
119684
+ return null;
119685
+ }
119686
+ const working = inTurn(m.state);
119687
+ if (working && !s.working) {
119688
+ s.estimated = false;
119689
+ s.readThisTurn = false;
119690
+ }
119691
+ if (s.working && !working) s.owed = !s.readThisTurn;
119692
+ s.working = working;
119693
+ if (perRequest || e.type !== "usage_update" || !e.request || e.request.agent !== void 0) return null;
119694
+ if (!working || s.readThisTurn || !m.context || m.context.window <= 0) return null;
119695
+ const used = Math.min(m.context.window, e.request.inputTokens + e.request.cacheReadTokens + (e.request.cacheCreationTokens ?? 0));
119696
+ const now = this.now();
119697
+ if (used === m.context.used || now - s.at < ESTIMATE_GAP_MS) return null;
119698
+ s.at = now;
119699
+ s.estimated = true;
119700
+ return { type: "context_update", sessionId: m.id, used, window: m.context.window, exactness: "estimate" };
119701
+ }
119702
+ };
119703
+
119559
119704
  // packages/agent-host/src/sessions/app-messages.ts
119560
119705
  function appMessageFrame(app2, text2, via = "inline") {
119561
119706
  const body = text2.split(/\r\n|[\n\r\u0085\u2028\u2029]/).map((line) => `> ${line}`).join("\n");
@@ -119616,6 +119761,8 @@ var SessionEvents = class {
119616
119761
  this.host = host;
119617
119762
  }
119618
119763
  host;
119764
+ /** The context figure during a turn, from each request's size (#1492, context-meter.ts) */
119765
+ meter = new ContextMeter();
119619
119766
  /** Receive event -> update metadata -> persist message -> broadcast */
119620
119767
  onEvent(raw, from = null) {
119621
119768
  const e = raw.type === "files_touched" ? this.projectRelative(raw) : raw;
@@ -119636,6 +119783,10 @@ var SessionEvents = class {
119636
119783
  this.host.record.recordSubagentStep(e);
119637
119784
  return;
119638
119785
  }
119786
+ if (e.type === "context_update" && this.meter.stale(e)) {
119787
+ this.host.compactEvent(e, false);
119788
+ return;
119789
+ }
119639
119790
  if (e.type === "settings_changed" && e.by === "tool") {
119640
119791
  this.host.record.recordToolSettings(e);
119641
119792
  return;
@@ -119718,6 +119869,9 @@ var SessionEvents = class {
119718
119869
  if (endedTurn && e.sessionId) this.host.restartPolicy.applyDeferredRestart(e.sessionId);
119719
119870
  if (e.sessionId) this.host.mailEvent(e, endedTurn);
119720
119871
  if (e.sessionId) this.host.compactEvent(e, endedTurn);
119872
+ const meta3 = e.sessionId ? this.host.meta.get(e.sessionId) : void 0;
119873
+ const estimate = meta3 ? this.meter.observe(e, meta3, this.host.contextAtRequest(meta3.id)) : null;
119874
+ if (estimate) this.onEvent(estimate);
119721
119875
  if (e.sessionId && (endedTurn || e.type === "state_change" || e.type === "approval_resolved" || e.type === "question_resolved")) this.host.mailPoke(e.sessionId);
119722
119876
  if (e.type === "background_tasks" && e.sessionId) this.host.restartPolicy.applyDeferredRestart(e.sessionId);
119723
119877
  if (e.type === "background_tasks" && e.sessionId && this.host.children.pausedReports.has(e.sessionId)) this.host.reports.settlePausedReport(e.sessionId);
@@ -119765,7 +119919,7 @@ var SessionEvents = class {
119765
119919
  switch (e.type) {
119766
119920
  case "usage_update": {
119767
119921
  m.usage = e.tokens;
119768
- const counted = e.request && this.host.cacheLedger.record(m.tool, m.id, e.request);
119922
+ const counted = e.request && this.host.cacheLedger.record(m.tool, m.id, e.request, void 0, m.model);
119769
119923
  if (e.request && counted) this.host.usageRecorder.request({ sessionId: m.id, projectId: m.projectId, tool: m.tool, model: m.model ?? void 0 }, e.request, counted);
119770
119924
  break;
119771
119925
  }
@@ -119774,7 +119928,7 @@ var SessionEvents = class {
119774
119928
  break;
119775
119929
  case "context_update":
119776
119930
  m.context = nextContext(m.context, e);
119777
- this.host.modelWindows.note(m, e.window, e.model);
119931
+ if (e.exactness !== "estimate") this.host.modelWindows.note(m, e.window, e.model);
119778
119932
  break;
119779
119933
  case "conversation_reset":
119780
119934
  m.context = nextContext(m.context, e);
@@ -120004,6 +120158,8 @@ var SelfCompaction = class {
120004
120158
  /** The size of the last model request of the session's own conversation in the running turn: the size the turn grew to, in both tools */
120005
120159
  lastRequest = /* @__PURE__ */ new Map();
120006
120160
  timer = null;
120161
+ /** `stop()` was called: a late event of a turn that was running is not acted on (it would start the timer or send) */
120162
+ stopped = false;
120007
120163
  now() {
120008
120164
  return (this.host.now ?? Date.now)();
120009
120165
  }
@@ -120028,7 +120184,8 @@ var SelfCompaction = class {
120028
120184
  /** Hears every event of every session. Never throws into the caller's event */
120029
120185
  observe(e, endedTurn) {
120030
120186
  const id3 = e.sessionId;
120031
- if (!id3) return;
120187
+ if (!id3 || this.stopped) return;
120188
+ if (e.type === "context_update" && e.exactness === "estimate") return;
120032
120189
  if (e.type === "session_deleted") return this.forget(id3);
120033
120190
  const m = this.host.session(id3);
120034
120191
  if (!m) return;
@@ -120064,9 +120221,7 @@ var SelfCompaction = class {
120064
120221
  }
120065
120222
  const live2 = this.pending.get(id3);
120066
120223
  if (e.type === "context_update" && live2 && live2.endedAt !== null) return this.decide(id3, e.used, m.tool);
120067
- if ((e.type === "approval_resolved" || e.type === "question_resolved") && live2?.held && !busy && !this.cardOpen(id3)) {
120068
- return this.decide(id3, live2.size ?? m.context?.used ?? 0, m.tool);
120069
- }
120224
+ if (live2?.held && this.releaseHeld(id3, m)) return;
120070
120225
  if (!endedTurn) return;
120071
120226
  this.markRest(id3);
120072
120227
  if (this.read.has(id3)) this.late.delete(id3);
@@ -120094,6 +120249,24 @@ var SelfCompaction = class {
120094
120249
  if (grew !== null) this.decide(id3, grew, m.tool);
120095
120250
  else this.awaitReading(id3, live2);
120096
120251
  }
120252
+ /**
120253
+ * A request held for a card: judged now if the card is closed and the session at rest, dropped if it has no process any more.
120254
+ * Returns whether it was settled. **Not asked: background work.** The held path goes on `!busy && !cardOpen` even while the session
120255
+ * runs background work, as the rule for every request made in a turn does (the request does not ask whether it runs: waiting for
120256
+ * CI in the background is what it is for, docs/agent-host.md "Compacting at a break"); `sessionIdle`'s rule for the rest compaction is
120257
+ * stricter on purpose, so this is not to be made to match it.
120258
+ */
120259
+ releaseHeld(id3, m) {
120260
+ const req = this.pending.get(id3);
120261
+ if (!req?.held) return false;
120262
+ if (!this.host.running(id3)) {
120263
+ this.settle(id3);
120264
+ return true;
120265
+ }
120266
+ if (inTurn(m.state) || this.cardOpen(id3)) return false;
120267
+ this.decide(id3, req.size ?? m.context?.used ?? 0, m.tool);
120268
+ return true;
120269
+ }
120097
120270
  /** A request whose turn ended with no size: a timer decides it on the reading it has if the tool's own does not come */
120098
120271
  awaitReading(id3, req) {
120099
120272
  req.endedAt = this.now();
@@ -120170,6 +120343,7 @@ var SelfCompaction = class {
120170
120343
  const setting = this.host.setting();
120171
120344
  const sent = [];
120172
120345
  for (const m of this.host.sessions()) {
120346
+ if (this.pending.get(m.id)?.held) this.releaseHeld(m.id, m);
120173
120347
  if (!this.due(m, setting, now)) continue;
120174
120348
  this.fire(m.id, `at rest for ${IDLE_COMPACT_MINUTES} minutes, over ${Math.round(setting.idleShare * 100)}% of the window`, null, m.tool);
120175
120349
  if (this.compacting.has(m.id)) sent.push(m.id);
@@ -120198,10 +120372,12 @@ var SelfCompaction = class {
120198
120372
  /** Starts the sweep. A host that never calls it still serves `compact_self` */
120199
120373
  start() {
120200
120374
  if (this.timer) return;
120375
+ this.stopped = false;
120201
120376
  this.timer = setInterval(() => this.sweepSafely(), SWEEP_MS);
120202
120377
  this.timer.unref?.();
120203
120378
  }
120204
120379
  stop() {
120380
+ this.stopped = true;
120205
120381
  if (this.timer) clearInterval(this.timer);
120206
120382
  this.timer = null;
120207
120383
  for (const t of this.waits.values()) clearTimeout(t);
@@ -120225,6 +120401,102 @@ var SelfCompaction = class {
120225
120401
  }
120226
120402
  };
120227
120403
 
120404
+ // packages/agent-host/src/sessions/context-nudges.ts
120405
+ var ContextNudges = class {
120406
+ constructor(host) {
120407
+ this.host = host;
120408
+ }
120409
+ host;
120410
+ cycles = /* @__PURE__ */ new Map();
120411
+ stopped = false;
120412
+ /** Hears every event of every session. Never throws into the caller's event */
120413
+ observe(e) {
120414
+ const id3 = e.sessionId;
120415
+ if (!id3 || this.stopped) return;
120416
+ switch (e.type) {
120417
+ case "session_deleted":
120418
+ this.cycles.delete(id3);
120419
+ return;
120420
+ case "conversation_reset":
120421
+ this.cycles.delete(id3);
120422
+ return;
120423
+ case "compaction":
120424
+ if (!e.failed) this.cycles.delete(id3);
120425
+ return;
120426
+ case "usage_update":
120427
+ if (e.request && e.request.agent === void 0) this.requested(id3, e.request.inputTokens + e.request.cacheReadTokens + (e.request.cacheCreationTokens ?? 0));
120428
+ return;
120429
+ default:
120430
+ }
120431
+ }
120432
+ cycle(id3) {
120433
+ let c = this.cycles.get(id3);
120434
+ if (!c) this.cycles.set(id3, c = { told: /* @__PURE__ */ new Set(), waiting: null, size: 0, sending: false });
120435
+ return c;
120436
+ }
120437
+ requested(id3, size) {
120438
+ if (size < COMPACT_FLOOR_TOKENS) return;
120439
+ const setting = this.host.setting();
120440
+ const m = this.host.session(id3);
120441
+ if (!m || !setting.controlled || !setting.nudge || !this.host.hasCompactTool(id3)) return;
120442
+ const window2 = this.host.window(m) ?? m.context?.window ?? 0;
120443
+ if (window2 <= 0) return;
120444
+ const c = this.cycle(id3);
120445
+ c.size = size;
120446
+ const share = size / window2;
120447
+ const { first, second } = nudgeShares(setting);
120448
+ const crossed = ["first", "second"].filter((k2) => !c.told.has(k2) && (k2 === "first" ? first : second) < setting.share && (k2 === "first" ? first : second) <= share);
120449
+ if (crossed.length > 0) {
120450
+ for (const k2 of crossed) c.told.add(k2);
120451
+ c.waiting = crossed.includes("second") ? "second" : "first";
120452
+ }
120453
+ if (c.waiting) this.deliver(id3, c, m, window2, setting);
120454
+ }
120455
+ deliver(id3, c, m, window2, setting) {
120456
+ const which = c.waiting;
120457
+ if (!which || c.sending) return;
120458
+ const percent = Math.round(c.size / window2 * 100);
120459
+ const used = `${Math.round(c.size / 1e3)}k of ${Math.round(window2 / 1e3)}k tokens`;
120460
+ const text2 = which === "second" ? `[Centralu] Your context is now about ${percent}% full (${used}), and the tool compacts it by itself in the middle of work at ${Math.round(setting.share * 100)}%. Call compact_self when the current step is done, at the latest; do not stop what you are doing for it.` : `[Centralu] Your context is now about ${percent}% full (${used}). When this step of the work is done, call compact_self so the next step starts from a summary; do not stop what you are doing for it.`;
120461
+ const failed3 = (err) => {
120462
+ c.waiting = null;
120463
+ console.error(`[compact] ${id3.slice(0, 8)}: could not send the context notice: ${err instanceof Error ? err.message : String(err)}`);
120464
+ };
120465
+ const answered = (took2) => {
120466
+ if (!took2) return;
120467
+ c.waiting = null;
120468
+ this.host.notice(id3, `Centralu told it its context is ${percent}% full: call compact_self when this step is done`);
120469
+ };
120470
+ let took;
120471
+ try {
120472
+ took = this.host.nudge(id3, text2);
120473
+ } catch (err) {
120474
+ failed3(err);
120475
+ return;
120476
+ }
120477
+ if (typeof took === "boolean") {
120478
+ answered(took);
120479
+ return;
120480
+ }
120481
+ c.sending = true;
120482
+ void took.then(
120483
+ (ok) => {
120484
+ c.sending = false;
120485
+ if (!this.stopped && this.cycles.get(id3) === c) answered(ok);
120486
+ },
120487
+ (err) => {
120488
+ c.sending = false;
120489
+ if (!this.stopped && this.cycles.get(id3) === c) failed3(err);
120490
+ }
120491
+ );
120492
+ }
120493
+ /** The host is leaving: nothing is told after this, and nothing waits to be */
120494
+ stop() {
120495
+ this.stopped = true;
120496
+ this.cycles.clear();
120497
+ }
120498
+ };
120499
+
120228
120500
  // packages/agent-host/src/adapters/claude/index.ts
120229
120501
  import { tmpdir as tmpdir3 } from "node:os";
120230
120502
  import { mkdtemp as mkdtemp2, readdir as readdir9, rm as rm7, stat as stat8 } from "node:fs/promises";
@@ -122457,8 +122729,14 @@ var CLAUDE_KNOWN_TYPES = /* @__PURE__ */ new Set([
122457
122729
  ]);
122458
122730
  function ttlOf(split2) {
122459
122731
  const c = asObj(split2);
122460
- if (Number(c.ephemeral_1h_input_tokens ?? 0) > 0) return { cacheTtl: "1h" };
122461
- if (Number(c.ephemeral_5m_input_tokens ?? 0) > 0) return { cacheTtl: "5m" };
122732
+ const hour = Number(c.ephemeral_1h_input_tokens ?? 0);
122733
+ const five = Number(c.ephemeral_5m_input_tokens ?? 0);
122734
+ const amounts = {
122735
+ ...Number.isFinite(hour) && "ephemeral_1h_input_tokens" in c ? { cacheWrite1hTokens: Math.max(0, hour) } : {},
122736
+ ...Number.isFinite(five) && "ephemeral_5m_input_tokens" in c ? { cacheWrite5mTokens: Math.max(0, five) } : {}
122737
+ };
122738
+ if (hour > 0) return { cacheTtl: "1h", ...amounts };
122739
+ if (five > 0) return { cacheTtl: "5m", ...amounts };
122462
122740
  return {};
122463
122741
  }
122464
122742
  function readingOf(events2) {
@@ -122689,10 +122967,10 @@ var ClaudeStreamNormalizer = class {
122689
122967
  if (subagent) return events2;
122690
122968
  events2 = this.followRetry(type, events2);
122691
122969
  if (type === "result" && this.refusal !== null) {
122692
- const refusal = this.refusal;
122970
+ const refusal2 = this.refusal;
122693
122971
  this.refusal = null;
122694
122972
  const failed3 = events2.some((e) => e.type === "error");
122695
- events2 = failed3 ? events2.map((e) => e.type === "error" ? { ...e, error: { ...e.error, message: refusal } } : e) : [{ type: "notice", sessionId: this.sessionId, level: "warning", text: refusal, from: "Claude Code", label: "refusal" }, ...events2];
122973
+ events2 = failed3 ? events2.map((e) => e.type === "error" ? { ...e, error: { ...e.error, message: refusal2 } } : e) : [{ type: "notice", sessionId: this.sessionId, level: "warning", text: refusal2, from: "Claude Code", label: "refusal" }, ...events2];
122696
122974
  }
122697
122975
  if (msg.type === "stream_event") {
122698
122976
  const e = msg.event;
@@ -124154,6 +124432,12 @@ var ClaudeSession = class {
124154
124432
  send(text2, opts) {
124155
124433
  this.sendInput(text2, opts?.id !== void 0 && isUuid(opts.id) ? opts.id : randomUUID24(), void 0, opts?.origin, opts?.quiet);
124156
124434
  }
124435
+ /** A note read with the next message or at the running turn's next tool round, starting nothing (#1492): quiet input, stamped as Centralu's */
124436
+ nudge(text2) {
124437
+ if (this.closed) return false;
124438
+ this.send(text2, { origin: { kind: "channel", server: "centralu" }, quiet: true });
124439
+ return true;
124440
+ }
124157
124441
  /** The inputs the result being handled names (#1377); null when the CLI cannot name them, and outside a result */
124158
124442
  resultInputs = null;
124159
124443
  turnInputs() {
@@ -124456,6 +124740,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
124456
124740
  tool = "claude";
124457
124741
  /** `shouldQuery: false` (sdk.d.ts, measured on the Mac 2026-10-20: no turn, merged into the next message) */
124458
124742
  quietInput = true;
124743
+ cacheBilling = "anthropic";
124459
124744
  /** What Claude Code offers against what this adapter limits (#270) */
124460
124745
  surface = CLAUDE_SURFACE;
124461
124746
  launch;
@@ -126690,7 +126975,7 @@ var CodexSlashCommands = class {
126690
126975
  /** The request a slash command started, or null when the text is a message */
126691
126976
  run(text2) {
126692
126977
  if (text2.trim() === "/compact") {
126693
- this.host.setBlockingTurn(true);
126978
+ this.host.setBlockingTurn(true, true);
126694
126979
  return this.host.request("thread/compact/start", { threadId: this.host.threadId() }).catch((e) => {
126695
126980
  this.host.setBlockingTurn(false);
126696
126981
  this.host.flushPending();
@@ -127040,8 +127325,9 @@ var CodexSession = class {
127040
127325
  emit: (e) => this.emit(e),
127041
127326
  request: (method, params) => this.client.request(method, params),
127042
127327
  threadId: () => this.threadId,
127043
- setBlockingTurn: (on2) => {
127328
+ setBlockingTurn: (on2, compact) => {
127044
127329
  this.blockingTurn = on2;
127330
+ this.compactTurn = on2 && compact === true;
127045
127331
  },
127046
127332
  flushPending: () => this.flushPending()
127047
127333
  });
@@ -127144,6 +127430,8 @@ var CodexSession = class {
127144
127430
  inputGroups = /* @__PURE__ */ new Map();
127145
127431
  /** An unsteerable turn (compact/review) is running — we know because we are the ones who started it */
127146
127432
  blockingTurn = false;
127433
+ /** The unsteerable turn is a compaction we started (`/compact`), not a review: a compaction event in it is manual */
127434
+ compactTurn = false;
127147
127435
  /** Messages sent while the thread was still coming up, and those a Stop reclaimed (see `WakingSends`, #168) */
127148
127436
  sends = new WakingSends();
127149
127437
  /** Codex is reconnecting on its own (#168): the `retrying` activity and what it puts back (see `RetryActivity`) */
@@ -127290,6 +127578,7 @@ var CodexSession = class {
127290
127578
  }
127291
127579
  if (n.method === "turn/completed" && this.blockingTurn) {
127292
127580
  this.blockingTurn = false;
127581
+ this.compactTurn = false;
127293
127582
  this.flushPending();
127294
127583
  }
127295
127584
  if (n.method === "thread/settings/updated") {
@@ -127312,7 +127601,8 @@ var CodexSession = class {
127312
127601
  }
127313
127602
  /** A notification of this thread's own, as the events it maps to (`normalizeNotification`), each the way it goes out */
127314
127603
  emitNotification(n) {
127315
- for (const e of normalizeNotification(this.sessionId, n)) {
127604
+ for (const said of normalizeNotification(this.sessionId, n)) {
127605
+ const e = said.type === "compaction" ? { ...said, trigger: this.compactTurn ? "manual" : "auto" } : said;
127316
127606
  if (e.type === "error" && this.inputReceipts.size) this.interrupt(new Error(e.error.message));
127317
127607
  if (e.type === "message_image" && !e.data && e.path) {
127318
127608
  void imageEventFromDisk(this.sessionId, e.path).then((filled2) => this.emit(filled2));
@@ -127323,7 +127613,8 @@ var CodexSession = class {
127323
127613
  if (e.type === "usage_update") {
127324
127614
  const request = this.requests.take(n.params);
127325
127615
  if (request) {
127326
- this.emit({ ...e, request });
127616
+ const model = this.shown?.model ?? this.opts.model ?? this.threadSettings?.model ?? null;
127617
+ this.emit({ ...e, request: model ? { ...request, model } : request });
127327
127618
  continue;
127328
127619
  }
127329
127620
  }
@@ -127352,6 +127643,22 @@ var CodexSession = class {
127352
127643
  send(text2) {
127353
127644
  this.sendInput(text2);
127354
127645
  }
127646
+ /**
127647
+ * A note steered into the regular turn that is running, starting nothing (#1492). `turn/start` would do it only while a turn is
127648
+ * running: with none, it starts a turn of its own (Codex sends a request's token count just before `turn/completed`, so a note for
127649
+ * the last request of a turn went out after the turn had ended and ran as a turn nobody asked for, #1504 review), and behind a
127650
+ * compact or review turn it queues the note and runs it as a turn. `turn/steer` names the turn it is for (`expectedTurnId`) and
127651
+ * is refused by Codex when that turn is not the one running, so a note for a turn that has ended starts nothing. The answer says
127652
+ * whether it was taken: a refused note is not told, and waits for a later request of a running turn (`ContextNudges`).
127653
+ */
127654
+ nudge(text2) {
127655
+ const turnId = this.turns.turnId;
127656
+ if (this.closed || !this.started || !this.threadId || turnId === null || this.blockingTurn || this.pendingInputs.length > 0) return false;
127657
+ return this.client.request("turn/steer", { threadId: this.threadId, input: [{ type: "text", text: text2 }], expectedTurnId: turnId }).then(
127658
+ () => true,
127659
+ () => false
127660
+ );
127661
+ }
127355
127662
  sendInput(text2, receipt) {
127356
127663
  const nth = this.sends.issue();
127357
127664
  this.startsInFlight += 1;
@@ -127912,6 +128219,8 @@ var exec5 = promisify6(execFile7);
127912
128219
  var CODEX_VERBOSITIES = ["low", "medium", "high"];
127913
128220
  var CodexAdapter = class {
127914
128221
  tool = "codex";
128222
+ cacheBilling = "openai";
128223
+ contextAtRequest = true;
127915
128224
  /** What Codex offers against what this adapter limits (#270) */
127916
128225
  surface = CODEX_SURFACE;
127917
128226
  descriptor = {
@@ -129416,6 +129725,25 @@ var SessionSettings = class {
129416
129725
  }
129417
129726
  };
129418
129727
 
129728
+ // packages/agent-host/src/sessions/cache-cost.ts
129729
+ function gptGeneration(model) {
129730
+ const m = /gpt-(\d+)(?:\.(\d+))?/i.exec(model ?? "");
129731
+ return m ? Number(m[1]) + Number(m[2] ?? 0) / 100 : null;
129732
+ }
129733
+ function cachePrices(billing, model) {
129734
+ if (billing === "anthropic") return { read: 0.1, write5m: 1.25, write1h: 2 };
129735
+ const generation = gptGeneration(model);
129736
+ if (generation === null || generation >= 5.06) return { read: /(^|[^a-z])sol([^a-z]|$)/i.test(model ?? "") ? 0.05 : 0.1, write5m: 1.25, write1h: 1.25 };
129737
+ return { read: 0.1, write5m: 1, write1h: 1 };
129738
+ }
129739
+ function cacheCost(billing, model, t, reportsWrites) {
129740
+ const price = cachePrices(billing, model);
129741
+ const read = t.read * price.read;
129742
+ if (!reportsWrites) return { read, write: t.input * price.write5m };
129743
+ const hour = Math.min(t.write, Math.max(0, t.write1h ?? 0));
129744
+ return { read, write: hour * price.write1h + (t.write - hour) * price.write5m };
129745
+ }
129746
+
129419
129747
  // packages/agent-host/src/sessions/cache-ledger.ts
129420
129748
  var REBUILD_SHARE = 0.5;
129421
129749
  var REBUILD_MIN_TOKENS = 4096;
@@ -129428,12 +129756,21 @@ var TOP_SESSIONS = 3;
129428
129756
  var FIVE_HOURS_MS = 5 * 60 * 6e4;
129429
129757
  var HOUR_MS3 = 60 * 6e4;
129430
129758
  var NOTED = ["cold_start", "new_process", "model_change", "compaction", "thread_restart", "instructions", "launch_settings"];
129759
+ function paidTokens(r) {
129760
+ return r.cacheCreationTokens ?? r.inputTokens;
129761
+ }
129431
129762
  function isRebuild(r) {
129432
129763
  const total = r.inputTokens + r.cacheReadTokens + (r.cacheCreationTokens ?? 0);
129433
- const paid = r.cacheCreationTokens ?? r.inputTokens;
129764
+ const paid = paidTokens(r);
129434
129765
  return paid >= REBUILD_MIN_TOKENS && paid >= total * REBUILD_SHARE;
129435
129766
  }
129436
129767
  var CacheLedger = class {
129768
+ /** `billingOf`: whose price ratios a tool's model is billed by (`AgentAdapter.cacheBilling`) */
129769
+ constructor(now = Date.now(), billingOf = () => "anthropic") {
129770
+ this.billingOf = billingOf;
129771
+ this.startedAt = now;
129772
+ }
129773
+ billingOf;
129437
129774
  entries = /* @__PURE__ */ new Map();
129438
129775
  carried = /* @__PURE__ */ new Map();
129439
129776
  /** Tools that have reported a cache write figure at least once (Claude); the others report reads only */
@@ -129442,9 +129779,6 @@ var CacheLedger = class {
129442
129779
  /** What happened in a session since its conversation's last request (`note`), consumed by the next one */
129443
129780
  notes = /* @__PURE__ */ new Map();
129444
129781
  startedAt;
129445
- constructor(now = Date.now()) {
129446
- this.startedAt = now;
129447
- }
129448
129782
  /**
129449
129783
  * Something happened in a session that would make its next request rebuild the cache: the session was created, its
129450
129784
  * process started again, its model changed. Kept until that session's own conversation makes a request.
@@ -129480,14 +129814,22 @@ var CacheLedger = class {
129480
129814
  read: b2.cacheRead,
129481
129815
  write: b2.cacheWrite,
129482
129816
  rebuilds: b2.rebuilds,
129817
+ // A rebuild is at least `REBUILD_MIN_TOKENS`: fewer tokens than that for the rebuilds counted means some were stored before
129818
+ // the figure was (an older build kept adding rebuilds to an hour this one had written), and the sum is not known
129819
+ rebuildTokens: b2.rebuildTokens < b2.rebuilds * REBUILD_MIN_TOKENS ? null : b2.rebuildTokens,
129820
+ // The ratios depend on the tool and the model, which a bucket has one of, so its sums are weighted at once
129821
+ ...this.costOfBucket(b2),
129483
129822
  causes: b2.causes
129484
129823
  });
129485
129824
  if (b2.cacheWrite > 0) this.reportsWrites.add(b2.tool);
129486
129825
  this.startedAt = Math.min(this.startedAt, b2.hour);
129487
129826
  }
129488
129827
  }
129489
- /** Counts one request. Returns what it counted as, or undefined for a reading that was no request */
129490
- record(tool, sessionId, r, at2 = Date.now()) {
129828
+ /**
129829
+ * Counts one request. Returns what it counted as, or undefined for a reading that was no request. `model` is the one the session
129830
+ * runs when the request does not say (the Codex price of a read and a write depends on it)
129831
+ */
129832
+ record(tool, sessionId, r, at2 = Date.now(), model) {
129491
129833
  const write = r.cacheCreationTokens ?? 0;
129492
129834
  if (r.inputTokens + r.cacheReadTokens + write <= 0) return void 0;
129493
129835
  if (r.cacheCreationTokens !== void 0) this.reportsWrites.add(tool);
@@ -129497,7 +129839,8 @@ ${r.agent ?? ""}`;
129497
129839
  const notes = r.agent ? void 0 : this.notes.get(sessionId);
129498
129840
  if (!r.agent) this.notes.delete(sessionId);
129499
129841
  const rebuild = isRebuild(r);
129500
- const entry = { at: at2, sessionId, input: r.inputTokens, read: r.cacheReadTokens, write, rebuild };
129842
+ const cost = cacheCost(this.billingOf(tool), r.model ?? model, { input: r.inputTokens, read: r.cacheReadTokens, write, write1h: r.cacheWrite1hTokens ?? (r.cacheTtl === "1h" ? write : 0) }, r.cacheCreationTokens !== void 0);
129843
+ const entry = { at: at2, sessionId, input: r.inputTokens, read: r.cacheReadTokens, write, rebuild, rebuildTokens: rebuild ? paidTokens(r) : 0, readCost: cost.read, writeCost: cost.write };
129501
129844
  if (rebuild) Object.assign(entry, causeOf(r, lane, notes, at2));
129502
129845
  this.lanes.delete(key);
129503
129846
  const ttlMs = r.cacheTtl ? TTL_MS[r.cacheTtl] : lane?.ttlMs ?? null;
@@ -129512,6 +129855,12 @@ ${r.agent ?? ""}`;
129512
129855
  if (drop > 0) list.splice(0, drop);
129513
129856
  return { rebuild, ...entry.cause ? { cause: entry.cause } : {} };
129514
129857
  }
129858
+ /** A stored bucket's cache cost: the ratios belong to its tool and model, so its sums are weighted at once (`cacheCost`) */
129859
+ costOfBucket(b2) {
129860
+ const billing = this.billingOf(b2.tool);
129861
+ const { read, write } = cacheCost(billing, b2.model, { input: b2.input, read: b2.cacheRead, write: b2.cacheWrite, write1h: b2.cacheWrite1h }, billing === "anthropic" || b2.cacheWrite > 0);
129862
+ return { readCost: read, writeCost: write };
129863
+ }
129515
129864
  /**
129516
129865
  * The last five hours and today (the host's local midnight). `nameOf` names a session; undefined for one that is
129517
129866
  * gone.
@@ -129551,9 +129900,13 @@ function windowOf(list, carried, id3, label, since, writes, nameOf3) {
129551
129900
  cacheCreationTokens: 0,
129552
129901
  rebuilds: 0,
129553
129902
  rebuildCauses: {},
129903
+ rebuildTokens: 0,
129904
+ cacheReadCost: 0,
129905
+ cacheWriteCost: 0,
129554
129906
  sessions: []
129555
129907
  };
129556
129908
  const causes = w.rebuildCauses;
129909
+ const unknownTokens = /* @__PURE__ */ new Set();
129557
129910
  const bySession = /* @__PURE__ */ new Map();
129558
129911
  for (const e of list) {
129559
129912
  if (e.at < since) continue;
@@ -129563,16 +129916,20 @@ function windowOf(list, carried, id3, label, since, writes, nameOf3) {
129563
129916
  w.cacheCreationTokens += e.write;
129564
129917
  let s = bySession.get(e.sessionId);
129565
129918
  if (!s) {
129566
- s = { sessionId: e.sessionId, name: "", requests: 0, inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, rebuilds: 0, rebuildCauses: {} };
129919
+ s = { sessionId: e.sessionId, name: "", requests: 0, inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, rebuilds: 0, rebuildCauses: {}, rebuildTokens: 0 };
129567
129920
  bySession.set(e.sessionId, s);
129568
129921
  }
129569
129922
  s.requests++;
129570
129923
  s.inputTokens += e.input;
129571
129924
  s.cacheReadTokens += e.read;
129572
129925
  s.cacheCreationTokens += e.write;
129926
+ w.cacheReadCost = (w.cacheReadCost ?? 0) + e.readCost;
129927
+ w.cacheWriteCost = (w.cacheWriteCost ?? 0) + e.writeCost;
129573
129928
  if (!e.rebuild) continue;
129574
129929
  w.rebuilds++;
129575
129930
  s.rebuilds++;
129931
+ w.rebuildTokens = (w.rebuildTokens ?? 0) + e.rebuildTokens;
129932
+ s.rebuildTokens = (s.rebuildTokens ?? 0) + e.rebuildTokens;
129576
129933
  const cause2 = e.cause ?? "unknown";
129577
129934
  causes[cause2] = (causes[cause2] ?? 0) + 1;
129578
129935
  s.rebuildCauses[cause2] = (s.rebuildCauses[cause2] ?? 0) + 1;
@@ -129588,9 +129945,13 @@ function windowOf(list, carried, id3, label, since, writes, nameOf3) {
129588
129945
  w.cacheReadTokens += c.read;
129589
129946
  w.cacheCreationTokens += c.write;
129590
129947
  w.rebuilds += c.rebuilds;
129948
+ if (c.rebuildTokens === null) unknownTokens.add(c.sessionId);
129949
+ else w.rebuildTokens = (w.rebuildTokens ?? 0) + c.rebuildTokens;
129950
+ w.cacheReadCost = (w.cacheReadCost ?? 0) + c.readCost;
129951
+ w.cacheWriteCost = (w.cacheWriteCost ?? 0) + c.writeCost;
129591
129952
  let s = bySession.get(c.sessionId);
129592
129953
  if (!s) {
129593
- s = { sessionId: c.sessionId, name: "", requests: 0, inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, rebuilds: 0, rebuildCauses: {} };
129954
+ s = { sessionId: c.sessionId, name: "", requests: 0, inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, rebuilds: 0, rebuildCauses: {}, rebuildTokens: 0 };
129594
129955
  bySession.set(c.sessionId, s);
129595
129956
  }
129596
129957
  s.requests += c.requests;
@@ -129598,12 +129959,15 @@ function windowOf(list, carried, id3, label, since, writes, nameOf3) {
129598
129959
  s.cacheReadTokens += c.read;
129599
129960
  s.cacheCreationTokens += c.write;
129600
129961
  s.rebuilds += c.rebuilds;
129962
+ if (c.rebuildTokens !== null) s.rebuildTokens = (s.rebuildTokens ?? 0) + c.rebuildTokens;
129601
129963
  const ofSession = s.rebuildCauses ??= {};
129602
129964
  for (const [cause2, n] of Object.entries(c.causes)) {
129603
129965
  causes[cause2] = (causes[cause2] ?? 0) + n;
129604
129966
  ofSession[cause2] = (ofSession[cause2] ?? 0) + n;
129605
129967
  }
129606
129968
  }
129969
+ if (unknownTokens.size > 0) delete w.rebuildTokens;
129970
+ for (const id4 of unknownTokens) delete bySession.get(id4)?.rebuildTokens;
129607
129971
  const cost = (s) => writes ? s.cacheCreationTokens : s.inputTokens;
129608
129972
  w.sessions = [...bySession.values()].filter((s) => cost(s) > 0).sort((a, b2) => cost(b2) - cost(a)).slice(0, TOP_SESSIONS).map((s) => ({ ...s, name: nameOf3(s.sessionId) ?? "" }));
129609
129973
  return w;
@@ -129640,7 +130004,7 @@ var UsageRecorder = class {
129640
130004
  if (!b2) {
129641
130005
  this.buckets.set(
129642
130006
  id3,
129643
- b2 = { hour, sessionId: src.sessionId, projectId, tool: src.tool, model, machine: "", requests: 0, input: 0, cacheRead: 0, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0, rebuilds: 0, causes: {} }
130007
+ b2 = { hour, sessionId: src.sessionId, projectId, tool: src.tool, model, machine: "", requests: 0, input: 0, cacheRead: 0, cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0, rebuilds: 0, rebuildTokens: 0, causes: {} }
129644
130008
  );
129645
130009
  }
129646
130010
  const write = r.cacheCreationTokens ?? 0;
@@ -129648,11 +130012,12 @@ var UsageRecorder = class {
129648
130012
  b2.input += r.inputTokens;
129649
130013
  b2.cacheRead += r.cacheReadTokens;
129650
130014
  b2.cacheWrite += write;
129651
- if (r.cacheTtl === "5m") b2.cacheWrite5m += write;
129652
- if (r.cacheTtl === "1h") b2.cacheWrite1h += write;
130015
+ b2.cacheWrite1h += r.cacheWrite1hTokens ?? (r.cacheTtl === "1h" ? write : 0);
130016
+ b2.cacheWrite5m += r.cacheWrite5mTokens ?? (r.cacheTtl === "5m" ? write : 0);
129653
130017
  b2.output += r.outputTokens ?? 0;
129654
130018
  if (counted.rebuild) {
129655
130019
  b2.rebuilds++;
130020
+ b2.rebuildTokens += paidTokens(r);
129656
130021
  const cause2 = counted.cause ?? "unknown";
129657
130022
  b2.causes[cause2] = (b2.causes[cause2] ?? 0) + 1;
129658
130023
  }
@@ -131008,10 +131373,10 @@ var ReaderGate = class {
131008
131373
  return this.storedMinReader() ?? 0;
131009
131374
  }
131010
131375
  refuseIfTooNew() {
131011
- const refusal = this.tooNew(this.dbPath);
131012
- if (!refusal) return;
131376
+ const refusal2 = this.tooNew(this.dbPath);
131377
+ if (!refusal2) return;
131013
131378
  this.db.close();
131014
- throw refusal;
131379
+ throw refusal2;
131015
131380
  }
131016
131381
  /**
131017
131382
  * Why this host cannot open the store, or null. A record above this host refuses (#292). A record that is not a
@@ -133047,6 +133412,27 @@ var StepsV62 = class extends MigrationRange {
133047
133412
  CREATE INDEX IF NOT EXISTS idx_usage_buckets_session ON usage_buckets(session_id);
133048
133413
  `);
133049
133414
  }
133415
+ },
133416
+ {
133417
+ to: 63,
133418
+ breaksOlderReaders: false,
133419
+ /**
133420
+ * What the rebuilds wrote to the cache, per hour bucket (#1492): `usage_buckets.rebuild_tokens`, the sum of the cache writes
133421
+ * of the requests counted in `rebuilds` (for a tool that reports no writes, the input it did not read), so the usage panel can
133422
+ * say "7 rebuilds · 1.0M written" after a restart too. Hours stored before it have 0 with rebuilds counted: they are read as
133423
+ * "not known" rather than as nothing written.
133424
+ *
133425
+ * **Expand only (#292's rule).** A defaulted column. An older host names its columns in every read and insert: it neither
133426
+ * reads this one nor fails on it, and the row it adds or adds to keeps its sum (the upsert never touches the column), which
133427
+ * leaves that hour's rebuilds with fewer tokens than rebuilds, as an hour from before. `IF` because a store from a test's
133428
+ * partial schema has no such table.
133429
+ */
133430
+ run: () => {
133431
+ const cols = this.db.prepare(`PRAGMA table_info(usage_buckets)`).all();
133432
+ if (cols.length > 0 && !cols.some((c) => c.name === "rebuild_tokens")) {
133433
+ this.db.exec(`ALTER TABLE usage_buckets ADD COLUMN rebuild_tokens INTEGER NOT NULL DEFAULT 0`);
133434
+ }
133435
+ }
133050
133436
  }
133051
133437
  ];
133052
133438
  }
@@ -137148,6 +137534,18 @@ var SessionManager = class {
137148
137534
  send: (id3, text2) => this.events.sendCompact(id3, text2),
137149
137535
  notice: (id3, text2) => this.onEvent({ type: "notice", sessionId: id3, level: "info", from: "Centralu", label: "compaction", text: text2 })
137150
137536
  });
137537
+ this.contextNudges = new ContextNudges({
137538
+ session: (id3) => this.meta.get(id3),
137539
+ window: (m) => this.modelWindows.of(m),
137540
+ setting: () => autoCompactOf(this.uiPreferences()),
137541
+ // The two seats that have the tool (tool-seats.ts): an ordinary session and the orchestrator
137542
+ hasCompactTool: (id3) => {
137543
+ const profile = this.toolProfileOf(id3);
137544
+ return profile === "reader" || profile === "orchestrator";
137545
+ },
137546
+ nudge: (id3, text2) => this.handles.get(id3)?.nudge?.(text2) ?? false,
137547
+ notice: (id3, text2) => this.onEvent({ type: "notice", sessionId: id3, level: "info", from: "Centralu", label: "compaction", text: text2 })
137548
+ });
137151
137549
  this.orchestratorSlot = new OrchestratorSlot(this.parts());
137152
137550
  const rawEmit = this.emit;
137153
137551
  this.emit = (full) => {
@@ -137218,7 +137616,7 @@ var SessionManager = class {
137218
137616
  */
137219
137617
  running = /* @__PURE__ */ new Map();
137220
137618
  /** Every model request's cache figures over the last day, per tool (docs/agent-host.md §6.1); read by `usageFor` */
137221
- cacheLedger = new CacheLedger();
137619
+ cacheLedger = new CacheLedger(Date.now(), (tool) => this.adapters.get(tool)?.cacheBilling ?? "anthropic");
137222
137620
  /** The same requests summed per hour and kept, with the limit windows' readings (docs/agent-host.md §6.2) */
137223
137621
  usageRecorder;
137224
137622
  /** Each model's context window as the tools reported it, for where agents compact (#1376, auto-compact.ts) */
@@ -137281,6 +137679,8 @@ var SessionManager = class {
137281
137679
  updatesAsked;
137282
137680
  /** Compacting at a turn's end: the agent's `compact_self` and a session at rest over the lower share (#1451, self-compaction.ts) */
137283
137681
  selfCompaction;
137682
+ /** The quiet notices that tell a growing session to call `compact_self` (#1492, context-nudges.ts) */
137683
+ contextNudges;
137284
137684
  /** Whether a session of one project may reach another, and the consents remembered for it (project-consents.ts) */
137285
137685
  consents;
137286
137686
  /** The screen's preferences and workspace layout (ui-preferences.ts) */
@@ -137668,6 +138068,7 @@ var SessionManager = class {
137668
138068
  }
137669
138069
  stopSelfCompaction() {
137670
138070
  this.selfCompaction.stop();
138071
+ this.contextNudges.stop();
137671
138072
  }
137672
138073
  /** Starts compacting the sessions at rest over the lower share (#1451); the shutdown plan stops it before the sessions are let go of */
137673
138074
  startSelfCompaction() {
@@ -138531,6 +138932,11 @@ var SessionManager = class {
138531
138932
  } catch (err) {
138532
138933
  console.error(`[compact] ${e.sessionId?.slice(0, 8) ?? "?"}: ${errorMessage(err)}`);
138533
138934
  }
138935
+ try {
138936
+ self2.contextNudges.observe(e);
138937
+ } catch (err) {
138938
+ console.error(`[compact] ${e.sessionId?.slice(0, 8) ?? "?"}: ${errorMessage(err)}`);
138939
+ }
138534
138940
  },
138535
138941
  listAgents: (...a) => self2.delegationFlow.listAgents(...a),
138536
138942
  get agentRuns() {
@@ -138590,6 +138996,10 @@ var SessionManager = class {
138590
138996
  const tool = self2.meta.get(sessionId)?.tool;
138591
138997
  return tool !== void 0 && self2.adapters.get(tool)?.capabilities.midTurnInput === true;
138592
138998
  },
138999
+ contextAtRequest: (sessionId) => {
139000
+ const tool = self2.meta.get(sessionId)?.tool;
139001
+ return tool !== void 0 && self2.adapters.get(tool)?.contextAtRequest === true;
139002
+ },
138593
139003
  get children() {
138594
139004
  return self2.children;
138595
139005
  },
@@ -139176,6 +139586,7 @@ var SessionManager = class {
139176
139586
  async disposeAll() {
139177
139587
  this.usageRecorder.stop();
139178
139588
  this.selfCompaction.stop();
139589
+ this.contextNudges.stop();
139179
139590
  this.sweepingHandles = true;
139180
139591
  try {
139181
139592
  await this.disposeEverything();
@@ -139207,6 +139618,7 @@ var SessionManager = class {
139207
139618
  async detachAll() {
139208
139619
  this.usageRecorder.stop();
139209
139620
  this.selfCompaction.stop();
139621
+ this.contextNudges.stop();
139210
139622
  this.handingOver = true;
139211
139623
  this.leaveHost("Centralu is restarting");
139212
139624
  await Promise.allSettled([...[...this.handles.values()].map((h) => h.detach ? h.detach() : h.dispose()), ...this.merges.mergeChecks]);
@@ -139517,17 +139929,17 @@ var MailService = class {
139517
139929
  }
139518
139930
  }
139519
139931
  const reachable = [];
139520
- let refusal;
139932
+ let refusal2;
139521
139933
  for (const to of this.recipients(thread.id, caller)) {
139522
139934
  try {
139523
139935
  this.mayWriteOrRefuse(caller, to, thread);
139524
139936
  reachable.push(to);
139525
139937
  } catch (e) {
139526
139938
  if (!(e instanceof Error) || refusalReason(e) === void 0) throw e;
139527
- refusal ??= e;
139939
+ refusal2 ??= e;
139528
139940
  }
139529
139941
  }
139530
- if (refusal !== void 0 && reachable.length === 0) throw refusal;
139942
+ if (refusal2 !== void 0 && reachable.length === 0) throw refusal2;
139531
139943
  if (sender.kind === "person") this.records.putParticipantIfAbsent(thread.id, PERSON, "member", now);
139532
139944
  const message = this.insertPost(thread.id, caller, input.kind, body, {}, input, now, input.replyTo, reachable);
139533
139945
  let after = thread;
@@ -150187,7 +150599,7 @@ var handedToken = launch.tokenOnStdin ? await readHandedToken(process.stdin).cat
150187
150599
  console.error(`[agent-host] the launcher did not hand over the token: ${e.message}`);
150188
150600
  process.exit(1);
150189
150601
  }) : launch.token;
150190
- var BUILD = true ? "c20b51b" : "dev";
150602
+ var BUILD = true ? "50607f5" : "dev";
150191
150603
  var { values } = parseArgs({
150192
150604
  options: {
150193
150605
  port: { type: "string", default: "5175" },