@agentconnect.md/daemon 1.0.0-rc.159 → 1.0.0-rc.160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13629,6 +13629,8 @@ const WebchatStatus = object({
13629
13629
  costAmount: number().optional(),
13630
13630
  costCurrency: string().optional(),
13631
13631
  models: array(string()).optional(),
13632
+ efforts: array(string()).optional(),
13633
+ fastModeAvailable: boolean().optional(),
13632
13634
  sessionId: string().optional()
13633
13635
  });
13634
13636
  //#endregion
@@ -13682,6 +13684,16 @@ const FRAME_SCHEMAS = {
13682
13684
  agentId: string().uuid(),
13683
13685
  model: string()
13684
13686
  }),
13687
+ "webchat/set-effort": object({
13688
+ conversationId: string().uuid(),
13689
+ agentId: string().uuid(),
13690
+ effort: string()
13691
+ }),
13692
+ "webchat/set-fast": object({
13693
+ conversationId: string().uuid(),
13694
+ agentId: string().uuid(),
13695
+ fastMode: boolean()
13696
+ }),
13685
13697
  "webchat/cancel": object({ conversationId: string().uuid() }),
13686
13698
  "route/assign": RouteAssign,
13687
13699
  "route/assign/ack": RouteAssignAck,
@@ -13760,6 +13772,8 @@ discriminatedUnion("type", [
13760
13772
  frame("webchat/done", FRAME_SCHEMAS["webchat/done"]),
13761
13773
  frame("webchat/close", FRAME_SCHEMAS["webchat/close"]),
13762
13774
  frame("webchat/set-model", FRAME_SCHEMAS["webchat/set-model"]),
13775
+ frame("webchat/set-effort", FRAME_SCHEMAS["webchat/set-effort"]),
13776
+ frame("webchat/set-fast", FRAME_SCHEMAS["webchat/set-fast"]),
13763
13777
  frame("webchat/cancel", FRAME_SCHEMAS["webchat/cancel"]),
13764
13778
  frame("route/assign", FRAME_SCHEMAS["route/assign"]),
13765
13779
  frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
@@ -18183,6 +18197,11 @@ function planConfigSelection(configOptions, category, desired) {
18183
18197
  value: desired
18184
18198
  };
18185
18199
  }
18200
+ /** Effort sentinel `'ultracode'` (matching Claude Code's own `ultracode` settings
18201
+ * key) meaning xhigh reasoning PLUS standing dynamic-workflow orchestration. It is
18202
+ * deliberately NOT a `thought_level` select value: the claude-acp runtime rejects
18203
+ * effort="ultracode" ("Invalid value"). See {@link claudeSessionMeta}. */
18204
+ const ULTRACODE_EFFORT = "ultracode";
18186
18205
  /** The session `_meta` sent to a Claude runtime on session/new and session/load
18187
18206
  * (`_meta.claudeCode.options` is spread into the SDK `query()` options layer), or
18188
18207
  * undefined off Claude runtimes. Two things ride on it:
@@ -18225,6 +18244,39 @@ function modelOptionsFrom(configOptions) {
18225
18244
  models
18226
18245
  };
18227
18246
  }
18247
+ /**
18248
+ * Extract the reasoning-effort selector from a session's `configOptions` (ACP
18249
+ * `category: "thought_level"`, `type: "select"`). Effort support is per-model: a runtime
18250
+ * only advertises this select for models that support effort (e.g. Opus/Sonnet, not
18251
+ * Haiku), so a missing/empty selector means the CURRENT model has no effort → returns
18252
+ * null and the picker is hidden. When the model DOES support effort, Claude runtimes get
18253
+ * the synthetic `max`/`ultracode` entries appended if not already offered (see
18254
+ * {@link ULTRACODE_EFFORT}) — never fabricated for a model that lacks effort entirely.
18255
+ */
18256
+ function effortOptionsFrom(configOptions, isClaudeRuntime) {
18257
+ const opt = configOptions?.find((o) => o.category === "thought_level" && o.type === "select");
18258
+ if (!opt || opt.type !== "select") return null;
18259
+ const advertised = opt.options.flatMap((o) => "group" in o ? o.options : [o]).map((o) => o.value);
18260
+ if (advertised.length === 0) return null;
18261
+ const efforts = [...advertised];
18262
+ if (isClaudeRuntime) {
18263
+ for (const extra of ["max", ULTRACODE_EFFORT]) if (!efforts.includes(extra)) efforts.push(extra);
18264
+ }
18265
+ return {
18266
+ current: opt.currentValue,
18267
+ efforts
18268
+ };
18269
+ }
18270
+ /**
18271
+ * Extract the fast-mode toggle from a session's `configOptions` (ACP `category:
18272
+ * "model_config"`, `type: "select"` with on/off values). Returns null when the current
18273
+ * model advertises no fast toggle.
18274
+ */
18275
+ function fastOptionFrom(configOptions) {
18276
+ const opt = configOptions?.find((o) => o.category === "model_config" && o.type === "select");
18277
+ if (!opt || opt.type !== "select") return null;
18278
+ return { current: opt.currentValue === "on" };
18279
+ }
18228
18280
  var AcpHost = class {
18229
18281
  runtime;
18230
18282
  opts;
@@ -18235,6 +18287,8 @@ var AcpHost = class {
18235
18287
  canLoad = false;
18236
18288
  promptCaps = {};
18237
18289
  lastModelOptions = null;
18290
+ lastEffortOptions = null;
18291
+ lastFastOption = null;
18238
18292
  sessionConfigs = /* @__PURE__ */ new Map();
18239
18293
  negotiatedProtocolVersion;
18240
18294
  constructor(runtime, opts) {
@@ -18299,8 +18353,12 @@ var AcpHost = class {
18299
18353
  promptSupports(kind) {
18300
18354
  return Boolean(this.promptCaps[kind]);
18301
18355
  }
18302
- async newSession(cwd, mcpServers = []) {
18303
- const _meta = claudeSessionMeta(this.opts.configPrefs?.reasoningEffort, this.isClaudeRuntime());
18356
+ /** Create a fresh ACP session. `effortOverride` (the sticky per-session effort choice,
18357
+ * when set) takes the place of the agent default in the Claude `_meta` — the only
18358
+ * channel `ultracode` can ride (the `thought_level` select rejects it); select-based
18359
+ * effort/model/fast overrides are layered on afterward by the daemon at turn start. */
18360
+ async newSession(cwd, mcpServers = [], effortOverride) {
18361
+ const _meta = claudeSessionMeta(effortOverride ?? this.opts.configPrefs?.reasoningEffort, this.isClaudeRuntime());
18304
18362
  const res = await this.conn.agent.request(methods.agent.session.new, {
18305
18363
  cwd,
18306
18364
  mcpServers,
@@ -18308,7 +18366,7 @@ var AcpHost = class {
18308
18366
  });
18309
18367
  this.live.add(res.sessionId);
18310
18368
  const configOptions = await this.applySessionConfig(res.sessionId, res.configOptions);
18311
- this.lastModelOptions = modelOptionsFrom(configOptions);
18369
+ this.refreshOptionCaches(configOptions);
18312
18370
  this.sessionConfigs.set(res.sessionId, configOptions);
18313
18371
  return res.sessionId;
18314
18372
  }
@@ -18350,23 +18408,38 @@ var AcpHost = class {
18350
18408
  }
18351
18409
  return options;
18352
18410
  }
18353
- /** The model selector advertised by the most recent session/new, or null if the
18354
- * agent exposed none. Used by the runtime probe to report available models. */
18411
+ /** Re-derive the model / effort / fast selector caches from a reconciled option set. */
18412
+ refreshOptionCaches(configOptions) {
18413
+ this.lastModelOptions = modelOptionsFrom(configOptions);
18414
+ this.lastEffortOptions = effortOptionsFrom(configOptions, this.isClaudeRuntime());
18415
+ this.lastFastOption = fastOptionFrom(configOptions);
18416
+ }
18417
+ /** The model selector advertised by the most recent session/new|load, or null if the
18418
+ * agent exposed none. Used by the runtime probe + status bar to report models. */
18355
18419
  modelOptions() {
18356
18420
  return this.lastModelOptions;
18357
18421
  }
18422
+ /** The reasoning-effort selector (incl. synthetic ultracode/max on Claude runtimes)
18423
+ * from the most recent session/new|load, or null if none is offered. */
18424
+ effortOptions() {
18425
+ return this.lastEffortOptions;
18426
+ }
18427
+ /** The fast-mode toggle from the most recent session/new|load, or null if the current
18428
+ * model offers no fast mode. */
18429
+ fastModeOption() {
18430
+ return this.lastFastOption;
18431
+ }
18358
18432
  /**
18359
- * Switch the model of an ALREADY-RUNNING session via ACP `session/set_config_option`
18360
- * (category `model`) — the mid-session counterpart of the model applied at
18361
- * new/load. Returns true iff the change was applied: false when the session isn't
18362
- * live here, the runtime advertises no model selector, the value isn't offered, or
18363
- * it's already selected. Refreshes the cached options + `lastModelOptions` on success.
18433
+ * Apply one select config option to an ALREADY-RUNNING session via ACP
18434
+ * `session/set_config_option`, refreshing the cached option set + selector caches on
18435
+ * success. Returns true iff applied: false when the session isn't live here, the
18436
+ * runtime advertises no such selector, the value isn't offered, or it's already set.
18364
18437
  */
18365
- async setSessionModel(sessionId, model) {
18438
+ async setSessionConfig(sessionId, category, value) {
18366
18439
  if (!this.live.has(sessionId)) return false;
18367
- const plan = planConfigSelection(this.sessionConfigs.get(sessionId), "model", model);
18440
+ const plan = planConfigSelection(this.sessionConfigs.get(sessionId), category, value);
18368
18441
  if ("skip" in plan) {
18369
- this.opts.log?.debug(`acp: setSessionModel "${model}" on ${sessionId} not applied — ${plan.skip}`);
18442
+ this.opts.log?.debug(`acp: set ${category}="${value}" on ${sessionId} not applied — ${plan.skip}`);
18370
18443
  return false;
18371
18444
  }
18372
18445
  const res = await this.conn.agent.request(methods.agent.session.setConfigOption, {
@@ -18375,10 +18448,28 @@ var AcpHost = class {
18375
18448
  value: plan.value
18376
18449
  });
18377
18450
  this.sessionConfigs.set(sessionId, res.configOptions);
18378
- this.lastModelOptions = modelOptionsFrom(res.configOptions);
18379
- this.opts.log?.info(`acp: session ${sessionId} model set to "${model}"`);
18451
+ this.refreshOptionCaches(res.configOptions);
18452
+ this.opts.log?.info(`acp: session ${sessionId} ${category} set to "${value}"`);
18380
18453
  return true;
18381
18454
  }
18455
+ /** Switch the model of an already-running session (ACP `model` select) — the
18456
+ * mid-session counterpart of the model applied at new/load. */
18457
+ async setSessionModel(sessionId, model) {
18458
+ return this.setSessionConfig(sessionId, "model", model);
18459
+ }
18460
+ /** Switch the reasoning effort of an already-running session (ACP `thought_level`
18461
+ * select). Returns false for `ultracode` — it isn't a select value and can only ride
18462
+ * session `_meta` at new/load, so the caller relies on the override being re-applied
18463
+ * when the session is next (re)created or resumed. */
18464
+ async setSessionEffort(sessionId, effort) {
18465
+ if (effort === "ultracode") return false;
18466
+ return this.setSessionConfig(sessionId, "thought_level", effort);
18467
+ }
18468
+ /** Toggle fast mode on an already-running session (ACP `model_config` on/off select).
18469
+ * False when the current model offers no fast toggle. */
18470
+ async setSessionFastMode(sessionId, on) {
18471
+ return this.setSessionConfig(sessionId, "model_config", on ? "on" : "off");
18472
+ }
18382
18473
  /** The ACP protocol version negotiated at initialize, or undefined before the
18383
18474
  * handshake completed. Used by the runtime probe to report ACP coverage. */
18384
18475
  acpProtocolVersion() {
@@ -18396,10 +18487,10 @@ var AcpHost = class {
18396
18487
  * restores its own history server-side; its replayed session/update stream is
18397
18488
  * suppressed so it isn't re-posted. Throws if the agent can't load the id
18398
18489
  * (caller falls back to newSession). */
18399
- async loadSession(sessionId, cwd) {
18490
+ async loadSession(sessionId, cwd, effortOverride) {
18400
18491
  this.loadingSessions.add(sessionId);
18401
18492
  try {
18402
- const _meta = claudeSessionMeta(this.opts.configPrefs?.reasoningEffort, this.isClaudeRuntime());
18493
+ const _meta = claudeSessionMeta(effortOverride ?? this.opts.configPrefs?.reasoningEffort, this.isClaudeRuntime());
18403
18494
  const res = await this.conn.agent.request(methods.agent.session.load, {
18404
18495
  sessionId,
18405
18496
  cwd,
@@ -18408,7 +18499,7 @@ var AcpHost = class {
18408
18499
  });
18409
18500
  this.live.add(sessionId);
18410
18501
  const configOptions = await this.applySessionConfig(sessionId, res.configOptions);
18411
- this.lastModelOptions = modelOptionsFrom(configOptions);
18502
+ this.refreshOptionCaches(configOptions);
18412
18503
  this.sessionConfigs.set(sessionId, configOptions);
18413
18504
  this.opts.log?.info(`acp: resumed session ${sessionId} via session/load`);
18414
18505
  } catch (err) {
@@ -24173,7 +24264,8 @@ var LocalStore = class {
24173
24264
  CREATE TABLE IF NOT EXISTS sessions (
24174
24265
  key TEXT PRIMARY KEY, agentId TEXT, platform TEXT, channel TEXT, thread TEXT,
24175
24266
  acpSessionId TEXT, state TEXT, lastDeliveredTs TEXT, updatedAt INTEGER,
24176
- usage TEXT, muted INTEGER, triggeredBy TEXT, title TEXT, modelOverride TEXT
24267
+ usage TEXT, muted INTEGER, triggeredBy TEXT, title TEXT, modelOverride TEXT,
24268
+ effortOverride TEXT, fastModeOverride INTEGER, outputModeOverride TEXT
24177
24269
  );
24178
24270
  -- Platform id → human display name (Slack channel/user names, daemon-resolved
24179
24271
  -- and cached here so session read-back can label ids without a Slack call).
@@ -24219,10 +24311,11 @@ var LocalStore = class {
24219
24311
  ON transcript (channel, thread, tool_call_id) WHERE tool_call_id IS NOT NULL`);
24220
24312
  }
24221
24313
  /** Add the `sessions.usage` (JSON token accounting), `sessions.muted` (`!stop`
24222
- * thread mute), `sessions.triggeredBy`, `sessions.title` and `sessions.modelOverride`
24223
- * (the sticky per-session model choice) columns to a pre-existing DB. CREATE TABLE IF
24224
- * NOT EXISTS above adds them for fresh DBs but never alters an existing table, so
24225
- * upgrade in place. No-op once the columns are present. */
24314
+ * thread mute), `sessions.triggeredBy`, `sessions.title`, `sessions.modelOverride`,
24315
+ * `sessions.effortOverride` and `sessions.fastModeOverride` (the sticky per-session
24316
+ * model / reasoning-effort / fast-mode choices) columns to a pre-existing DB. CREATE
24317
+ * TABLE IF NOT EXISTS above adds them for fresh DBs but never alters an existing table,
24318
+ * so upgrade in place. No-op once the columns are present. */
24226
24319
  migrateSessionUsage() {
24227
24320
  const cols = this.db.prepare("PRAGMA table_info(sessions)").all();
24228
24321
  if (!cols.some((c) => c.name === "usage")) this.db.exec("ALTER TABLE sessions ADD COLUMN usage TEXT");
@@ -24230,6 +24323,9 @@ var LocalStore = class {
24230
24323
  if (!cols.some((c) => c.name === "triggeredBy")) this.db.exec("ALTER TABLE sessions ADD COLUMN triggeredBy TEXT");
24231
24324
  if (!cols.some((c) => c.name === "title")) this.db.exec("ALTER TABLE sessions ADD COLUMN title TEXT");
24232
24325
  if (!cols.some((c) => c.name === "modelOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN modelOverride TEXT");
24326
+ if (!cols.some((c) => c.name === "effortOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN effortOverride TEXT");
24327
+ if (!cols.some((c) => c.name === "fastModeOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN fastModeOverride INTEGER");
24328
+ if (!cols.some((c) => c.name === "outputModeOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN outputModeOverride TEXT");
24233
24329
  }
24234
24330
  /**
24235
24331
  * Pre-`kind` transcript tables keyed on (channel, thread, ts) hold only conversational
@@ -24327,6 +24423,38 @@ var LocalStore = class {
24327
24423
  setModelOverride(key, model) {
24328
24424
  this.db.prepare("UPDATE sessions SET modelOverride = ? WHERE key = ?").run(model, key);
24329
24425
  }
24426
+ /** The session-scoped reasoning-effort override (set via the status-bar effort picker),
24427
+ * or undefined if the session runs on the agent's default. Sticky across turns and
24428
+ * restarts, re-applied to the ACP session on each dispatch. */
24429
+ getEffortOverride(key) {
24430
+ return this.db.prepare("SELECT effortOverride FROM sessions WHERE key = ?").get(key)?.effortOverride ?? void 0;
24431
+ }
24432
+ /** Persist the session-scoped reasoning-effort override. No-op on an unknown key. */
24433
+ setEffortOverride(key, effort) {
24434
+ this.db.prepare("UPDATE sessions SET effortOverride = ? WHERE key = ?").run(effort, key);
24435
+ }
24436
+ /** The session-scoped fast-mode override (set via the status-bar fast toggle), or
24437
+ * undefined if the session runs on the agent's default. Stored as 0/1; sticky across
24438
+ * turns and restarts, re-applied to the ACP session on each dispatch. */
24439
+ getFastModeOverride(key) {
24440
+ const row = this.db.prepare("SELECT fastModeOverride FROM sessions WHERE key = ?").get(key);
24441
+ return row?.fastModeOverride === null || row?.fastModeOverride === void 0 ? void 0 : row.fastModeOverride === 1;
24442
+ }
24443
+ /** Persist the session-scoped fast-mode override. No-op on an unknown key. */
24444
+ setFastModeOverride(key, fastMode) {
24445
+ this.db.prepare("UPDATE sessions SET fastModeOverride = ? WHERE key = ?").run(fastMode ? 1 : 0, key);
24446
+ }
24447
+ /** The session-scoped Slack output-mode override (set via the status-bar output picker),
24448
+ * or undefined if the session uses the agent's default. Daemon-side rendering verbosity
24449
+ * (low/medium/high) — NOT an ACP setting; picked up by the next turn's OutputConverger. */
24450
+ getOutputModeOverride(key) {
24451
+ const v = this.db.prepare("SELECT outputModeOverride FROM sessions WHERE key = ?").get(key)?.outputModeOverride;
24452
+ return v === "low" || v === "medium" || v === "high" ? v : void 0;
24453
+ }
24454
+ /** Persist the session-scoped Slack output-mode override. No-op on an unknown key. */
24455
+ setOutputModeOverride(key, mode) {
24456
+ this.db.prepare("UPDATE sessions SET outputModeOverride = ? WHERE key = ?").run(mode, key);
24457
+ }
24330
24458
  /** Current token accounting for a session (parsed from the `usage` JSON column),
24331
24459
  * or `{}` if none has been recorded / the JSON is unreadable. */
24332
24460
  getUsage(key) {
@@ -24589,6 +24717,7 @@ var SessionManager = class {
24589
24717
  });
24590
24718
  let rec = this.deps.store.getSession(key);
24591
24719
  const host = await this.deps.hostFor(agentId);
24720
+ const effortOverride = this.deps.store.getEffortOverride(key);
24592
24721
  if (!rec || !rec.acpSessionId) {
24593
24722
  const cwd = await prepareWorkspace(agent);
24594
24723
  const mcpServers = this.deps.mcpServersFor?.({
@@ -24596,7 +24725,7 @@ var SessionManager = class {
24596
24725
  channel: msg.channel,
24597
24726
  thread
24598
24727
  }) ?? [];
24599
- const acpSessionId = await host.newSession(cwd, mcpServers);
24728
+ const acpSessionId = await host.newSession(cwd, mcpServers, effortOverride);
24600
24729
  rec = {
24601
24730
  key,
24602
24731
  agentId,
@@ -24616,7 +24745,7 @@ var SessionManager = class {
24616
24745
  if (host.loadSupported?.()) {
24617
24746
  this.deps.store.setSessionState(key, "resuming", Date.now());
24618
24747
  try {
24619
- await host.loadSession(rec.acpSessionId, cwd);
24748
+ await host.loadSession(rec.acpSessionId, cwd, effortOverride);
24620
24749
  resumed = true;
24621
24750
  } catch {}
24622
24751
  }
@@ -24626,7 +24755,7 @@ var SessionManager = class {
24626
24755
  channel: msg.channel,
24627
24756
  thread
24628
24757
  }) ?? [];
24629
- const acpSessionId = await host.newSession(cwd, mcpServers);
24758
+ const acpSessionId = await host.newSession(cwd, mcpServers, effortOverride);
24630
24759
  rec = {
24631
24760
  ...rec,
24632
24761
  acpSessionId,
@@ -81400,25 +81529,20 @@ function compactCount(n) {
81400
81529
  if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
81401
81530
  return `${(n / 1e6).toFixed(1)}M`;
81402
81531
  }
81403
- /** Render the session status line as one compact `markdown` block — e.g.
81404
- * `:bar_chart: *opus-4.8* · xhigh · fast · ctx 120k/200k (60%) · 45.2k tok · $0.18`.
81405
- * Pure: unknown fields are dropped so a partial snapshot still yields a clean line, and
81406
- * an empty snapshot degrades to a bare `:bar_chart: —` placeholder. */
81532
+ /** Render the compact in-thread status line as one `markdown` block — e.g.
81533
+ * `:bar_chart: *opus-4.8* · fast · ctx 120k/200k (60%) · 45.2k tok`. Deliberately narrow:
81534
+ * effort and cost are omitted here (they live in the ⚙ modal) to keep the line short in a
81535
+ * Slack thread. Pure: unknown fields are dropped so a partial snapshot still yields a clean
81536
+ * line, and an empty snapshot degrades to a bare `:bar_chart: —` placeholder. */
81407
81537
  function renderStatusBar(info) {
81408
81538
  const parts = [];
81409
81539
  if (info.model) parts.push(`*${info.model}*`);
81410
- if (info.effort) parts.push(info.effort);
81411
81540
  if (info.fastMode) parts.push("fast");
81412
81541
  if (info.contextUsed !== void 0 && info.contextSize !== void 0 && info.contextSize > 0) {
81413
81542
  const pct = Math.round(info.contextUsed / info.contextSize * 100);
81414
81543
  parts.push(`ctx ${compactCount(info.contextUsed)}/${compactCount(info.contextSize)} (${pct}%)`);
81415
81544
  } else if (info.contextUsed !== void 0) parts.push(`ctx ${compactCount(info.contextUsed)}`);
81416
81545
  if (info.totalTokens !== void 0) parts.push(`${compactCount(info.totalTokens)} tok`);
81417
- if (info.costAmount !== void 0) {
81418
- const cur = info.costCurrency && info.costCurrency !== "USD" ? ` ${info.costCurrency}` : "";
81419
- const sym = !info.costCurrency || info.costCurrency === "USD" ? "$" : "";
81420
- parts.push(`${sym}${info.costAmount.toFixed(info.costAmount < 1 ? 3 : 2)}${cur}`);
81421
- }
81422
81546
  return `:bar_chart: ${parts.length ? parts.join(" · ") : "—"}`;
81423
81547
  }
81424
81548
  /** Slack action_ids for the interactive status bar + modal (matched by the connection's
@@ -81426,6 +81550,9 @@ function renderStatusBar(info) {
81426
81550
  const STATUS_ACTION = {
81427
81551
  manage: "ac_manage",
81428
81552
  setModel: "ac_set_model",
81553
+ setEffort: "ac_set_effort",
81554
+ setFast: "ac_set_fast",
81555
+ setOutput: "ac_set_output",
81429
81556
  cancel: "ac_cancel",
81430
81557
  view: "ac_view"
81431
81558
  };
@@ -81452,23 +81579,24 @@ function buildStatusBlocks(info, sessionKey, link) {
81452
81579
  action_id: STATUS_ACTION.manage,
81453
81580
  text: {
81454
81581
  type: "plain_text",
81455
- text: ":gear: Manage",
81582
+ text: ":gear:",
81456
81583
  emoji: true
81457
81584
  },
81458
81585
  value: sessionKey
81459
81586
  }
81460
81587
  }];
81461
81588
  }
81462
- /** One `Label: value` field line for the modal's usage detail (omitted when absent). */
81463
- function usageField(label, n) {
81464
- return n === void 0 ? void 0 : `*${label}:* ${n.toLocaleString("en-US")}`;
81589
+ /** Format a token count with thousands separators for the modal's usage grid. */
81590
+ function fmtCount(n) {
81591
+ return n === void 0 ? void 0 : n.toLocaleString("en-US");
81465
81592
  }
81466
81593
  /**
81467
81594
  * Build the controls modal opened from the ⚙ button (a snapshot — Slack modals don't
81468
81595
  * stream). `private_metadata` carries the session key so the modal's `block_actions`
81469
- * (model select / Cancel) resolve the session. Blocks: the model `static_select` (omitted
81470
- * when no models are advertised), a usage-detail section (input/output/cache/thought
81471
- * tokens · context · cost), and Cancel + View-session (URL, when `link`). Pure.
81596
+ * (model/effort/fast/output selects, Cancel) resolve the session. Blocks: the model /
81597
+ * effort / fast / output selectors (each omitted when not applicable), a labeled usage
81598
+ * grid (context · tokens · cost · per-type breakdown), and Cancel + View-session (URL,
81599
+ * when `link`). The modal has room, so each metric gets its own labeled cell. Pure.
81472
81600
  */
81473
81601
  function buildStatusModal(info, sessionKey, link) {
81474
81602
  const blocks = [];
@@ -81506,23 +81634,115 @@ function buildStatusModal(info, sessionKey, link) {
81506
81634
  text: `*Model:* ${info.model}`
81507
81635
  }
81508
81636
  });
81509
- const detail = [
81510
- renderStatusBar({
81511
- ...info,
81512
- model: void 0
81513
- }),
81514
- usageField("Input", info.inputTokens),
81515
- usageField("Output", info.outputTokens),
81516
- usageField("Thought", info.thoughtTokens),
81517
- usageField("Cache read", info.cachedReadTokens),
81518
- usageField("Cache write", info.cachedWriteTokens)
81637
+ const efforts = info.efforts ?? [];
81638
+ if (efforts.length > 0) {
81639
+ const opts = info.effort && !efforts.includes(info.effort) ? [info.effort, ...efforts] : efforts;
81640
+ const option = (e) => ({
81641
+ text: {
81642
+ type: "plain_text",
81643
+ text: clampTo(e, 75)
81644
+ },
81645
+ value: e
81646
+ });
81647
+ blocks.push({
81648
+ type: "section",
81649
+ text: {
81650
+ type: "mrkdwn",
81651
+ text: "*Effort*"
81652
+ },
81653
+ accessory: {
81654
+ type: "static_select",
81655
+ action_id: STATUS_ACTION.setEffort,
81656
+ placeholder: {
81657
+ type: "plain_text",
81658
+ text: "Effort"
81659
+ },
81660
+ ...info.effort ? { initial_option: option(info.effort) } : {},
81661
+ options: opts.map(option)
81662
+ }
81663
+ });
81664
+ }
81665
+ if (info.fastModeAvailable) {
81666
+ const fastOpt = (v) => ({
81667
+ text: {
81668
+ type: "plain_text",
81669
+ text: v === "on" ? "On" : "Off"
81670
+ },
81671
+ value: v
81672
+ });
81673
+ const current = info.fastMode ? "on" : "off";
81674
+ blocks.push({
81675
+ type: "section",
81676
+ text: {
81677
+ type: "mrkdwn",
81678
+ text: "*Fast mode*"
81679
+ },
81680
+ accessory: {
81681
+ type: "static_select",
81682
+ action_id: STATUS_ACTION.setFast,
81683
+ placeholder: {
81684
+ type: "plain_text",
81685
+ text: "Fast mode"
81686
+ },
81687
+ initial_option: fastOpt(current),
81688
+ options: [fastOpt("on"), fastOpt("off")]
81689
+ }
81690
+ });
81691
+ }
81692
+ if (info.outputMode) {
81693
+ const modeOpt = (v) => ({
81694
+ text: {
81695
+ type: "plain_text",
81696
+ text: v
81697
+ },
81698
+ value: v
81699
+ });
81700
+ blocks.push({
81701
+ type: "section",
81702
+ text: {
81703
+ type: "mrkdwn",
81704
+ text: "*Output*"
81705
+ },
81706
+ accessory: {
81707
+ type: "static_select",
81708
+ action_id: STATUS_ACTION.setOutput,
81709
+ placeholder: {
81710
+ type: "plain_text",
81711
+ text: "Output"
81712
+ },
81713
+ initial_option: modeOpt(info.outputMode),
81714
+ options: [
81715
+ modeOpt("low"),
81716
+ modeOpt("medium"),
81717
+ modeOpt("high")
81718
+ ]
81719
+ }
81720
+ });
81721
+ }
81722
+ const field = (label, value) => value === void 0 ? void 0 : {
81723
+ type: "mrkdwn",
81724
+ text: `*${label}*\n${value}`
81725
+ };
81726
+ let contextValue;
81727
+ if (info.contextUsed !== void 0) contextValue = info.contextSize && info.contextSize > 0 ? `${fmtCount(info.contextUsed)} / ${fmtCount(info.contextSize)} (${Math.round(info.contextUsed / info.contextSize * 100)}%)` : fmtCount(info.contextUsed);
81728
+ let costValue;
81729
+ if (info.costAmount !== void 0) {
81730
+ const cur = info.costCurrency && info.costCurrency !== "USD" ? ` ${info.costCurrency}` : "";
81731
+ costValue = `${!info.costCurrency || info.costCurrency === "USD" ? "$" : ""}${info.costAmount.toFixed(info.costAmount < 1 ? 3 : 2)}${cur}`;
81732
+ }
81733
+ const fields = [
81734
+ field("Context", contextValue),
81735
+ field("Total tokens", fmtCount(info.totalTokens)),
81736
+ field("Cost", costValue),
81737
+ field("Input", fmtCount(info.inputTokens)),
81738
+ field("Output", fmtCount(info.outputTokens)),
81739
+ field("Thought", fmtCount(info.thoughtTokens)),
81740
+ field("Cache read", fmtCount(info.cachedReadTokens)),
81741
+ field("Cache write", fmtCount(info.cachedWriteTokens))
81519
81742
  ].filter(Boolean);
81520
- blocks.push({
81521
- type: "context",
81522
- elements: [{
81523
- type: "mrkdwn",
81524
- text: detail.join(" · ")
81525
- }]
81743
+ if (fields.length) blocks.push({
81744
+ type: "section",
81745
+ fields
81526
81746
  });
81527
81747
  const actions = [{
81528
81748
  type: "button",
@@ -81857,6 +82077,36 @@ var SlackConnection = class {
81857
82077
  model
81858
82078
  });
81859
82079
  });
82080
+ this.app.action(STATUS_ACTION.setEffort, async ({ ack, action, body }) => {
82081
+ await ack();
82082
+ const sessionKey = body?.view?.private_metadata ?? action.block_id;
82083
+ const effort = action.selected_option?.value;
82084
+ if (sessionKey && effort) this.deps.onStatusAction?.({
82085
+ kind: "set-effort",
82086
+ sessionKey,
82087
+ effort
82088
+ });
82089
+ });
82090
+ this.app.action(STATUS_ACTION.setFast, async ({ ack, action, body }) => {
82091
+ await ack();
82092
+ const sessionKey = body?.view?.private_metadata ?? action.block_id;
82093
+ const value = action.selected_option?.value;
82094
+ if (sessionKey && value) this.deps.onStatusAction?.({
82095
+ kind: "set-fast",
82096
+ sessionKey,
82097
+ fastMode: value === "on"
82098
+ });
82099
+ });
82100
+ this.app.action(STATUS_ACTION.setOutput, async ({ ack, action, body }) => {
82101
+ await ack();
82102
+ const sessionKey = body?.view?.private_metadata ?? action.block_id;
82103
+ const mode = action.selected_option?.value;
82104
+ if (sessionKey && (mode === "low" || mode === "medium" || mode === "high")) this.deps.onStatusAction?.({
82105
+ kind: "set-output",
82106
+ sessionKey,
82107
+ outputMode: mode
82108
+ });
82109
+ });
81860
82110
  this.app.action(STATUS_ACTION.cancel, async ({ ack, action, body }) => {
81861
82111
  await ack();
81862
82112
  const sessionKey = body?.view?.private_metadata ?? action.value;
@@ -83771,6 +84021,12 @@ var CpClient = class {
83771
84021
  case "webchat/set-model":
83772
84022
  this.deps.onWebchatSetModel?.(frame.payload);
83773
84023
  return;
84024
+ case "webchat/set-effort":
84025
+ this.deps.onWebchatSetEffort?.(frame.payload);
84026
+ return;
84027
+ case "webchat/set-fast":
84028
+ this.deps.onWebchatSetFast?.(frame.payload);
84029
+ return;
83774
84030
  case "webchat/cancel":
83775
84031
  this.deps.onWebchatCancel?.(frame.payload);
83776
84032
  return;
@@ -85264,10 +85520,68 @@ var Daemon = class Daemon {
85264
85520
  if (!rec?.acpSessionId || !this.pending.has(rec.acpSessionId)) return;
85265
85521
  this.interruptTurn(rec.agentId, rec.acpSessionId, key, "cancel");
85266
85522
  }
85523
+ /** Switch a session's reasoning effort by its local key — the effort counterpart of
85524
+ * {@link setModelByKey}. Records the sticky override and, if the ACP session is warm,
85525
+ * applies it live via the `thought_level` select. `ultracode` can't ride the select
85526
+ * (setSessionEffort returns false); it's honored via session `_meta` when the session
85527
+ * is next (re)created or resumed, and the override still shows on the bar meanwhile. */
85528
+ setEffortByKey(key, effort) {
85529
+ const rec = this.store.getSession(key);
85530
+ if (!rec) return;
85531
+ this.store.setEffortOverride(key, effort);
85532
+ this.log.info(`session ${key} effort override → "${effort}"`);
85533
+ const acpSessionId = rec.acpSessionId;
85534
+ const host = this.hosts.get(rec.agentId);
85535
+ if (!acpSessionId || !host?.hasSession(acpSessionId)) {
85536
+ this.refreshStatusBarForKey(key);
85537
+ return;
85538
+ }
85539
+ host.setSessionEffort(acpSessionId, effort).then(() => this.refreshStatusBarForKey(key)).catch((err) => this.log.warn(`set-effort failed: ${err.message}`));
85540
+ }
85541
+ /** Toggle a session's fast mode by its local key — the fast-mode counterpart of
85542
+ * {@link setModelByKey}. Records the sticky override and applies it live when the
85543
+ * current model offers a fast toggle. */
85544
+ setFastByKey(key, fastMode) {
85545
+ const rec = this.store.getSession(key);
85546
+ if (!rec) return;
85547
+ this.store.setFastModeOverride(key, fastMode);
85548
+ this.log.info(`session ${key} fast-mode override → ${fastMode}`);
85549
+ const acpSessionId = rec.acpSessionId;
85550
+ const host = this.hosts.get(rec.agentId);
85551
+ if (!acpSessionId || !host?.hasSession(acpSessionId)) {
85552
+ this.refreshStatusBarForKey(key);
85553
+ return;
85554
+ }
85555
+ host.setSessionFastMode(acpSessionId, fastMode).then(() => this.refreshStatusBarForKey(key)).catch((err) => this.log.warn(`set-fast failed: ${err.message}`));
85556
+ }
85557
+ /** Re-emit the status bar for a session's in-flight turn (if any) so a config change
85558
+ * (model / effort / fast) is reflected. No-op when the session is idle. */
85559
+ refreshStatusBarForKey(key) {
85560
+ const rec = this.store.getSession(key);
85561
+ const p = rec?.acpSessionId ? this.pending.get(rec.acpSessionId) : void 0;
85562
+ if (p) this.emitStatusBar(p);
85563
+ }
85564
+ /** Set a session's Slack output verbosity by its local key. Purely daemon-side (no ACP):
85565
+ * the next turn's OutputConverger reads this override, so an in-flight turn keeps its
85566
+ * current verbosity and the change takes effect from the next turn. */
85567
+ setOutputModeByKey(key, mode) {
85568
+ if (!this.store.getSession(key)) return;
85569
+ this.store.setOutputModeOverride(key, mode);
85570
+ this.log.info(`session ${key} output-mode override → "${mode}"`);
85571
+ this.refreshStatusBarForKey(key);
85572
+ }
85267
85573
  /** Handle a webchat in-session model switch (C→D `webchat/set-model`). */
85268
85574
  handleWebchatSetModel(m) {
85269
85575
  this.setModelByKey(this.webchatSessionKey(m.conversationId, m.agentId), m.model);
85270
85576
  }
85577
+ /** Handle a webchat in-session effort switch (C→D `webchat/set-effort`). */
85578
+ handleWebchatSetEffort(m) {
85579
+ this.setEffortByKey(this.webchatSessionKey(m.conversationId, m.agentId), m.effort);
85580
+ }
85581
+ /** Handle a webchat in-session fast-mode toggle (C→D `webchat/set-fast`). */
85582
+ handleWebchatSetFast(m) {
85583
+ this.setFastByKey(this.webchatSessionKey(m.conversationId, m.agentId), m.fastMode);
85584
+ }
85271
85585
  /** Handle a webchat cancel (C→D `webchat/cancel`) — the status-bar "Cancel". Interrupts
85272
85586
  * the conversation's in-flight turn like `!cancel` (no mute; follow-ups still dispatch).
85273
85587
  * Resolved by conversationId (the frame carries no agentId). No-op when idle. */
@@ -85278,11 +85592,20 @@ var Daemon = class Daemon {
85278
85592
  }
85279
85593
  this.log.debug(`webchat cancel: no in-flight turn for conversation ${m.conversationId}`);
85280
85594
  }
85281
- /** Route a Slack status-bar Block Kit interaction (model select / Cancel button) to the
85282
- * shared key-based cores. `sessionKey` rides the block; no-op on an unknown key. */
85595
+ /** Route a Slack status-bar Block Kit interaction (model / effort / fast select or the
85596
+ * Cancel button) to the shared key-based cores. `sessionKey` rides the block; no-op on
85597
+ * an unknown key. */
85283
85598
  handleSlackStatusAction(a) {
85284
85599
  if (a.kind === "cancel") this.cancelSessionByKey(a.sessionKey);
85285
- else if (a.model) this.setModelByKey(a.sessionKey, a.model);
85600
+ else if (a.kind === "set-model") {
85601
+ if (a.model) this.setModelByKey(a.sessionKey, a.model);
85602
+ } else if (a.kind === "set-effort") {
85603
+ if (a.effort) this.setEffortByKey(a.sessionKey, a.effort);
85604
+ } else if (a.kind === "set-fast") {
85605
+ if (a.fastMode !== void 0) this.setFastByKey(a.sessionKey, a.fastMode);
85606
+ } else if (a.kind === "set-output") {
85607
+ if (a.outputMode) this.setOutputModeByKey(a.sessionKey, a.outputMode);
85608
+ }
85286
85609
  }
85287
85610
  /** All of our own connections' bot identities (both `U…` user ids and `B…`
85288
85611
  * bot_ids), so we recognize self-echoes regardless of which field Slack sends. */
@@ -85457,12 +85780,13 @@ var Daemon = class Daemon {
85457
85780
  this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
85458
85781
  return null;
85459
85782
  }
85460
- const conv = new OutputConverger(this.agents.get(agentId).output.mode);
85783
+ const agent = this.agents.get(agentId);
85461
85784
  const rec = new TranscriptRecorder();
85462
85785
  const replyConn = msg.headless || webchat ? void 0 : this.replyConnFor(agentId, integrationId);
85463
85786
  const wasRunning = this.hostStarts.has(agentId);
85464
85787
  const statusThread = msg.thread ?? msg.msgId;
85465
85788
  const key = sessionKey(msg.platform, msg.channel, statusThread, agentId);
85789
+ const conv = new OutputConverger(this.store.getOutputModeOverride(key) ?? agent.output.mode);
85466
85790
  replyConn?.setStatus(msg.channel, statusThread, wasRunning ? "is thinking…" : "is starting up…");
85467
85791
  let handled;
85468
85792
  try {
@@ -85506,6 +85830,10 @@ var Daemon = class Daemon {
85506
85830
  const host = await this.ensureHostAsync(agentId);
85507
85831
  const override = this.store.getModelOverride(key);
85508
85832
  if (override) await host.setSessionModel(sessionId, override).catch((err) => this.log.debug(`model override "${override}" not applied: ${err.message}`));
85833
+ const effortOverride = this.store.getEffortOverride(key);
85834
+ if (effortOverride) await host.setSessionEffort(sessionId, effortOverride).catch((err) => this.log.debug(`effort override "${effortOverride}" not applied: ${err.message}`));
85835
+ const fastOverride = this.store.getFastModeOverride(key);
85836
+ if (fastOverride !== void 0) await host.setSessionFastMode(sessionId, fastOverride).catch((err) => this.log.debug(`fast-mode override ${fastOverride} not applied: ${err.message}`));
85509
85837
  if (!wasRunning) replyConn?.setStatus(msg.channel, statusThread, "is thinking…");
85510
85838
  this.emitStatusBar(p);
85511
85839
  const { stopReason, usage } = await host.prompt(sessionId, blocks);
@@ -85685,11 +86013,16 @@ var Daemon = class Daemon {
85685
86013
  statusInfoFrom(agentId, sessionKey, acpSessionId, opts = {}) {
85686
86014
  const agent = this.agents.get(agentId);
85687
86015
  const usage = this.store.getUsage(sessionKey);
85688
- const model = this.hosts.get(agentId)?.modelOptions?.();
86016
+ const host = this.hosts.get(agentId);
86017
+ const model = host?.modelOptions?.();
86018
+ const effort = host?.effortOptions?.();
86019
+ const fast = host?.fastModeOption?.();
86020
+ const effortOverride = this.store.getEffortOverride(sessionKey);
86021
+ const fastOverride = this.store.getFastModeOverride(sessionKey);
85689
86022
  return {
85690
86023
  model: model?.current ?? agent?.runtimeOverrides?.model,
85691
- effort: agent?.reasoningEffort,
85692
- fastMode: agent?.fastMode,
86024
+ effort: effortOverride ?? effort?.current ?? agent?.reasoningEffort,
86025
+ fastMode: fastOverride ?? fast?.current ?? agent?.fastMode,
85693
86026
  contextUsed: usage.contextUsed,
85694
86027
  contextSize: usage.contextSize,
85695
86028
  totalTokens: usage.totalTokens,
@@ -85700,9 +86033,12 @@ var Daemon = class Daemon {
85700
86033
  outputTokens: usage.outputTokens,
85701
86034
  thoughtTokens: usage.thoughtTokens,
85702
86035
  cachedReadTokens: usage.cachedReadTokens,
85703
- cachedWriteTokens: usage.cachedWriteTokens
86036
+ cachedWriteTokens: usage.cachedWriteTokens,
86037
+ outputMode: this.store.getOutputModeOverride(sessionKey) ?? agent?.output?.mode
85704
86038
  } : {},
85705
86039
  ...model?.models?.length ? { models: model.models } : {},
86040
+ ...effort?.efforts?.length ? { efforts: effort.efforts } : {},
86041
+ ...fast ? { fastModeAvailable: true } : {},
85706
86042
  ...acpSessionId ? { sessionId: acpSessionId } : {}
85707
86043
  };
85708
86044
  }
@@ -86304,6 +86640,8 @@ var Daemon = class Daemon {
86304
86640
  onWebchatMessage: (m) => this.handleWebchatMessage(m),
86305
86641
  onWebchatClose: (m) => this.handleWebchatClose(m),
86306
86642
  onWebchatSetModel: (m) => this.handleWebchatSetModel(m),
86643
+ onWebchatSetEffort: (m) => this.handleWebchatSetEffort(m),
86644
+ onWebchatSetFast: (m) => this.handleWebchatSetFast(m),
86307
86645
  onWebchatCancel: (m) => this.handleWebchatCancel(m),
86308
86646
  clock: systemClock,
86309
86647
  connect: () => ClientTransport.dial(url),