@agentconnect.md/daemon 1.0.0-rc.220 → 1.0.0-rc.221

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.
@@ -1,16 +1,16 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "./rolldown-runtime-CE-6LUnI.js";
2
2
  import { n as require_ms, t as require_src$2 } from "./src-DDtlVutj.js";
3
3
  import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
4
- import { a as agentChildEnv, i as selectAgent, n as findAgentFiles, o as loadConfig, r as loadAgents, s as persistDaemonId } from "./load-agents-nWFFmN5-.js";
4
+ import { a as agentChildEnv, i as selectAgent, n as findAgentFiles, o as loadConfig, r as loadAgents, s as persistDaemonId } from "./load-agents-C1m5Y9n_.js";
5
5
  import { l as resolveRoot, o as mcpSocketPath, u as statePath } from "./paths-DEcUO8lC.js";
6
- import { _ as resolveRuntimes, a as initGitInjection, c as simpleGit, d as MAX_FRAME_BYTES, f as buildEnvelope, g as cachedRuntimeVersions, h as cachedRuntimeNames, i as prepareWorkspace, m as encode, n as installedRuntimes, o as probeGitVersion, p as decodeEnvelope, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as normalizeGitUrl } from "./acp-host-CeJJZbtg.js";
6
+ import { _ as cachedRuntimeVersions, a as initGitInjection, c as simpleGit, d as MAX_FRAME_BYTES, f as buildEnvelope, g as cachedRuntimeNames, h as MEMORY_INDEX, i as prepareWorkspace, m as encode, n as installedRuntimes, o as probeGitVersion, p as decodeEnvelope, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as normalizeGitUrl, v as resolveRuntimes } from "./acp-host-zyZ_j0yY.js";
7
7
  import { i as GitCredentialCache, n as gitcredSocketPath, r as writeGitcredShim, t as GitCredServer } from "./gitcred-server-Dcsq6QJe.js";
8
8
  import { n as encodeFrame, t as decodeFrames } from "./ipc-2Xbxe7uu.js";
9
9
  import { URL as URL$1 } from "url";
10
10
  import * as tls from "tls";
11
11
  import * as net$2 from "net";
12
12
  import * as sp from "node:path";
13
- import { dirname, join, relative, resolve, sep } from "node:path";
13
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
14
14
  import * as http$6 from "http";
15
15
  import { Agent } from "https";
16
16
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, promises, readFileSync, rmSync, stat, unwatchFile, watch, watchFile, writeFileSync } from "node:fs";
@@ -5333,6 +5333,7 @@ function hostSpawnSig(a) {
5333
5333
  reasoningEffort: a.reasoningEffort,
5334
5334
  executionMode: a.executionMode,
5335
5335
  fastMode: a.fastMode,
5336
+ permissionMode: a.permissionMode,
5336
5337
  env: a.runtimeOverrides?.env
5337
5338
  });
5338
5339
  }
@@ -5395,7 +5396,8 @@ var LocalStore = class {
5395
5396
  key TEXT PRIMARY KEY, agentId TEXT, platform TEXT, channel TEXT, thread TEXT,
5396
5397
  acpSessionId TEXT, state TEXT, lastDeliveredTs TEXT, updatedAt INTEGER,
5397
5398
  usage TEXT, muted INTEGER, triggeredBy TEXT, title TEXT, modelOverride TEXT,
5398
- effortOverride TEXT, fastModeOverride INTEGER, outputModeOverride TEXT
5399
+ effortOverride TEXT, permissionModeOverride TEXT, fastModeOverride INTEGER,
5400
+ outputModeOverride TEXT
5399
5401
  );
5400
5402
  -- Platform id → human display name (Slack channel/user names, daemon-resolved
5401
5403
  -- and cached here so session read-back can label ids without a Slack call).
@@ -5442,10 +5444,11 @@ var LocalStore = class {
5442
5444
  }
5443
5445
  /** Add the `sessions.usage` (JSON token accounting), `sessions.muted` (`!stop`
5444
5446
  * thread mute), `sessions.triggeredBy`, `sessions.title`, `sessions.modelOverride`,
5445
- * `sessions.effortOverride` and `sessions.fastModeOverride` (the sticky per-session
5446
- * model / reasoning-effort / fast-mode choices) columns to a pre-existing DB. CREATE
5447
- * TABLE IF NOT EXISTS above adds them for fresh DBs but never alters an existing table,
5448
- * so upgrade in place. No-op once the columns are present. */
5447
+ * `sessions.effortOverride`, `sessions.permissionModeOverride` and
5448
+ * `sessions.fastModeOverride` (the sticky per-session model / reasoning-effort /
5449
+ * permission-mode / fast-mode choices) columns to a pre-existing DB. CREATE TABLE IF
5450
+ * NOT EXISTS above adds them for fresh DBs but never alters an existing table, so
5451
+ * upgrade in place. No-op once the columns are present. */
5449
5452
  migrateSessionUsage() {
5450
5453
  const cols = this.db.prepare("PRAGMA table_info(sessions)").all();
5451
5454
  if (!cols.some((c) => c.name === "usage")) this.db.exec("ALTER TABLE sessions ADD COLUMN usage TEXT");
@@ -5454,6 +5457,7 @@ var LocalStore = class {
5454
5457
  if (!cols.some((c) => c.name === "title")) this.db.exec("ALTER TABLE sessions ADD COLUMN title TEXT");
5455
5458
  if (!cols.some((c) => c.name === "modelOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN modelOverride TEXT");
5456
5459
  if (!cols.some((c) => c.name === "effortOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN effortOverride TEXT");
5460
+ if (!cols.some((c) => c.name === "permissionModeOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN permissionModeOverride TEXT");
5457
5461
  if (!cols.some((c) => c.name === "fastModeOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN fastModeOverride INTEGER");
5458
5462
  if (!cols.some((c) => c.name === "outputModeOverride")) this.db.exec("ALTER TABLE sessions ADD COLUMN outputModeOverride TEXT");
5459
5463
  }
@@ -5563,6 +5567,15 @@ var LocalStore = class {
5563
5567
  setEffortOverride(key, effort) {
5564
5568
  this.db.prepare("UPDATE sessions SET effortOverride = ? WHERE key = ?").run(effort, key);
5565
5569
  }
5570
+ /** The session-scoped permission-mode override (set via status bars), or undefined if
5571
+ * the session runs on the agent's default. Sticky across turns and restarts. */
5572
+ getPermissionModeOverride(key) {
5573
+ return this.db.prepare("SELECT permissionModeOverride FROM sessions WHERE key = ?").get(key)?.permissionModeOverride ?? void 0;
5574
+ }
5575
+ /** Persist the session-scoped permission-mode override. No-op on an unknown key. */
5576
+ setPermissionModeOverride(key, mode) {
5577
+ this.db.prepare("UPDATE sessions SET permissionModeOverride = ? WHERE key = ?").run(mode, key);
5578
+ }
5566
5579
  /** The session-scoped fast-mode override (set via the status-bar fast toggle), or
5567
5580
  * undefined if the session runs on the agent's default. Stored as 0/1; sticky across
5568
5581
  * turns and restarts, re-applied to the ACP session on each dispatch. */
@@ -5757,6 +5770,150 @@ function monotonicTs() {
5757
5770
  return String(last);
5758
5771
  }
5759
5772
  //#endregion
5773
+ //#region src/agents/memory.ts
5774
+ /**
5775
+ * Agent memory — a DIRECTORY at the agent's ROOT (`<agent-root>/memory/`), OUTSIDE
5776
+ * the workspace so it survives a workspace reset / re-clone and is never committed
5777
+ * into a github-workspace repo.
5778
+ *
5779
+ * Index-plus-topics shape (the structure Claude Code's auto-memory uses): a lean
5780
+ * `MEMORY.md` index — injected into the prompt every session, so it must stay small
5781
+ * — plus any number of `<topic>.md` files the agent reads on demand. The agent
5782
+ * maintains it via the `readMemory` / `writeMemory` / `listMemory` MCP tools; the
5783
+ * daemon serves the same directory to the CP for the console.
5784
+ *
5785
+ * SECURITY: topic paths are agent/console-supplied, so every path is contained to
5786
+ * the memory dir — absolute paths and `..` escapes are rejected, and the resolved
5787
+ * path is re-checked so a symlink can't smuggle a target out. Flat by design (one
5788
+ * level): a topic is a file directly under `memory/`, no nested dirs.
5789
+ */
5790
+ const MEMORY_DIRNAME = "memory";
5791
+ /** Max bytes of the index injected into the prompt each session — keeps a large
5792
+ * memory from swelling the context (mirrors Claude Code's 25 KB entrypoint cap). */
5793
+ const MAX_INDEX_INJECT_BYTES = 25e3;
5794
+ /** Hard cap on a single memory file. Injected/read every session, so an unbounded
5795
+ * file would swell the prompt and the wire; writes over this are rejected. */
5796
+ const MAX_MEMORY_FILE_BYTES = 256e3;
5797
+ /** Raised when a memory path escapes the memory dir. Surfaces as `BAD_PAYLOAD`. */
5798
+ var MemoryPathError = class extends Error {
5799
+ constructor(message) {
5800
+ super(message);
5801
+ this.name = "MemoryPathError";
5802
+ }
5803
+ };
5804
+ /** Raised when a write exceeds {@link MAX_MEMORY_FILE_BYTES}. `BAD_PAYLOAD`. */
5805
+ var MemoryTooLargeError = class extends Error {
5806
+ constructor(message) {
5807
+ super(message);
5808
+ this.name = "MemoryTooLargeError";
5809
+ }
5810
+ };
5811
+ /** Raised when an `ifMatchMtime` precondition fails (the file changed under the
5812
+ * writer — e.g. a console edit racing a newer agent write). Surfaces as CONFLICT. */
5813
+ var MemoryConflictError = class extends Error {
5814
+ constructor(message) {
5815
+ super(message);
5816
+ this.name = "MemoryConflictError";
5817
+ }
5818
+ };
5819
+ /** The memory directory for an agent, given its root dir (where agent.json lives). */
5820
+ function memoryDir(agentDir) {
5821
+ return join(agentDir, MEMORY_DIRNAME);
5822
+ }
5823
+ /** Resolve a memory-dir-relative path to an absolute one, contained to the dir.
5824
+ * Rejects absolute paths, `..` escapes, and any nested directory (flat only). */
5825
+ function resolveInMemoryDir(agentDir, relPath) {
5826
+ if (isAbsolute(relPath)) throw new MemoryPathError("absolute paths are not allowed");
5827
+ const dir = memoryDir(agentDir);
5828
+ const abs = resolve(dir, relPath);
5829
+ if (abs !== dir && !abs.startsWith(dir + sep)) throw new MemoryPathError("path escapes the memory dir");
5830
+ const rel = abs.slice(dir.length + 1);
5831
+ if (rel.includes(sep)) throw new MemoryPathError("memory is a flat directory (no subdirectories)");
5832
+ if (rel.length === 0) throw new MemoryPathError("a file name is required");
5833
+ return abs;
5834
+ }
5835
+ /** Seed `<agent-root>/memory/MEMORY.md` with a header if the dir/index is absent
5836
+ * (idempotent). Called at session start so a brand-new agent always has an index. */
5837
+ function ensureMemory(agentDir, agentName) {
5838
+ const dir = memoryDir(agentDir);
5839
+ mkdirSync(dir, { recursive: true });
5840
+ const index = join(dir, MEMORY_INDEX);
5841
+ if (!existsSync(index)) writeFileSync(index, `# ${agentName} memory\n\nYour long-term memory index. Keep it short — link out to topic files (e.g. \`[deploys](deploys.md)\`) for detail and read them on demand with the readMemory tool.\n`);
5842
+ }
5843
+ /** Read the memory index (`MEMORY.md`) for prompt injection, trimmed to the inject
5844
+ * cap so a large index can't blow up the prompt; '' when absent. */
5845
+ async function readIndex(agentDir) {
5846
+ const text = await readMemoryFile(agentDir, MEMORY_INDEX);
5847
+ if (Buffer.byteLength(text) <= 25e3) return text;
5848
+ return Buffer.from(text, "utf8").subarray(0, MAX_INDEX_INJECT_BYTES).toString("utf8") + "\n\n[…memory index truncated — trim MEMORY.md]";
5849
+ }
5850
+ /** Read a memory file's text; '' when it does not exist (never throws on ENOENT). */
5851
+ async function readMemoryFile(agentDir, relPath) {
5852
+ const abs = resolveInMemoryDir(agentDir, relPath);
5853
+ try {
5854
+ return await promises.readFile(abs, "utf8");
5855
+ } catch (err) {
5856
+ if (err.code === "ENOENT") return "";
5857
+ throw err;
5858
+ }
5859
+ }
5860
+ /** Overwrite a memory file with `content` (creating the dir if needed). Atomic
5861
+ * (tmp + rename) so a concurrent read never sees a partial file.
5862
+ *
5863
+ * - Rejects content over {@link MAX_MEMORY_FILE_BYTES} ({@link MemoryTooLargeError}).
5864
+ * - `ifMatchMtime` (optimistic concurrency): when given, the current file's mtime
5865
+ * must equal it, else {@link MemoryConflictError} — so a console edit can't
5866
+ * clobber a newer write. A brand-new file (no mtime) matches `ifMatchMtime`
5867
+ * only when the caller passes none (or the empty string). */
5868
+ async function writeMemoryFile(agentDir, relPath, content, ifMatchMtime) {
5869
+ if (Buffer.byteLength(content) > 256e3) throw new MemoryTooLargeError(`memory file exceeds the ${MAX_MEMORY_FILE_BYTES}-byte limit`);
5870
+ const abs = resolveInMemoryDir(agentDir, relPath);
5871
+ if (ifMatchMtime) {
5872
+ let current = null;
5873
+ try {
5874
+ current = (await promises.stat(abs)).mtime.toISOString();
5875
+ } catch (err) {
5876
+ if (err.code !== "ENOENT") throw err;
5877
+ }
5878
+ if (current !== ifMatchMtime) throw new MemoryConflictError("the memory file changed since it was read; reload and retry");
5879
+ }
5880
+ await promises.mkdir(memoryDir(agentDir), { recursive: true });
5881
+ const tmp = `${abs}.tmp`;
5882
+ await promises.writeFile(tmp, content, "utf8");
5883
+ await promises.rename(tmp, abs);
5884
+ const st = await promises.stat(abs);
5885
+ return {
5886
+ size: st.size,
5887
+ mtime: st.mtime.toISOString()
5888
+ };
5889
+ }
5890
+ /** List the files in the memory dir (flat; index + topics), sorted with the index
5891
+ * first. Empty when the dir does not exist. `.tmp` write artifacts are skipped. */
5892
+ async function listMemory(agentDir) {
5893
+ const dir = memoryDir(agentDir);
5894
+ let dirents;
5895
+ try {
5896
+ dirents = await promises.readdir(dir, { withFileTypes: true });
5897
+ } catch (err) {
5898
+ if (err.code === "ENOENT") return [];
5899
+ throw err;
5900
+ }
5901
+ const files = [];
5902
+ for (const d of dirents) {
5903
+ if (!d.isFile() || d.name.endsWith(".tmp")) continue;
5904
+ try {
5905
+ const st = await promises.stat(join(dir, d.name));
5906
+ files.push({
5907
+ name: d.name,
5908
+ size: st.size,
5909
+ mtime: st.mtime.toISOString()
5910
+ });
5911
+ } catch {}
5912
+ }
5913
+ files.sort((a, b) => a.name === "MEMORY.md" ? -1 : b.name === "MEMORY.md" ? 1 : a.name.localeCompare(b.name));
5914
+ return files;
5915
+ }
5916
+ //#endregion
5760
5917
  //#region src/session/attachment-block.ts
5761
5918
  /**
5762
5919
  * Turn one Slack attachment into the richest ACP content block the agent can
@@ -5833,6 +5990,7 @@ var SessionManager = class {
5833
5990
  async handle(agentId, msg) {
5834
5991
  const agent = this.deps.agentById(agentId);
5835
5992
  if (!agent) throw new Error(`unknown agent ${agentId}`);
5993
+ ensureMemory(agent.dir, agent.name);
5836
5994
  const { thread, ts: coordTs } = transcriptCoords(msg);
5837
5995
  const ts = msg.platform === "webchat" ? monotonicTs() : coordTs;
5838
5996
  const key = sessionKey(msg.platform, msg.channel, thread, agentId);
@@ -5925,6 +6083,13 @@ var SessionManager = class {
5925
6083
  text: `${head}\n${ctxText}`
5926
6084
  });
5927
6085
  }
6086
+ if (created) {
6087
+ const index = (await readIndex(agent.dir)).trim();
6088
+ if (index) blocks.push({
6089
+ type: "text",
6090
+ text: `# Your memory\nThis is your memory INDEX from earlier sessions. Read a linked topic file with the \`readMemory\` tool when relevant; revise memory with \`writeMemory\`.\n\n${index}`
6091
+ });
6092
+ }
5928
6093
  blocks.push({
5929
6094
  type: "text",
5930
6095
  text: msg.text
@@ -6093,8 +6258,34 @@ function guessMimeFromUrl(url) {
6093
6258
  * missing connection — the caller turns that into an MCP `isError` result.
6094
6259
  */
6095
6260
  async function executeTool(ctx, name, args, deps) {
6096
- const gw = deps.gatewayFor(ctx.integrationId);
6097
- if (!gw) throw new Error(`no live Slack connection for integration ${ctx.integrationId}`);
6261
+ if (name === "listMemory" || name === "readMemory" || name === "writeMemory") {
6262
+ const dir = deps.memoryDirFor(ctx.agentId);
6263
+ if (!dir) throw new Error(`unknown agent ${ctx.agentId}`);
6264
+ try {
6265
+ if (name === "listMemory") return { files: await listMemory(dir) };
6266
+ if (name === "readMemory") {
6267
+ const path = optionalString(args, "path") ?? "MEMORY.md";
6268
+ return {
6269
+ path,
6270
+ content: await readMemoryFile(dir, path)
6271
+ };
6272
+ }
6273
+ const path = optionalString(args, "path") ?? "MEMORY.md";
6274
+ const { size, mtime } = await writeMemoryFile(dir, path, requireStringAllowEmpty(args, "content"));
6275
+ return {
6276
+ ok: true,
6277
+ path,
6278
+ size,
6279
+ mtime
6280
+ };
6281
+ } catch (err) {
6282
+ if (err instanceof MemoryPathError) throw new Error(`invalid memory path: ${err.message}`);
6283
+ if (err instanceof MemoryTooLargeError) throw new Error(err.message);
6284
+ throw err;
6285
+ }
6286
+ }
6287
+ const gw = ctx.integrationId ? deps.gatewayFor(ctx.integrationId) : void 0;
6288
+ if (!gw) throw new Error(`no live Slack connection for integration ${ctx.integrationId ?? "(none)"}`);
6098
6289
  switch (name) {
6099
6290
  case "sendSlackMessage": {
6100
6291
  const text = requireString(args, "text");
@@ -6158,6 +6349,13 @@ function requireString(args, key) {
6158
6349
  if (typeof v !== "string" || v.length === 0) throw new Error(`missing required string argument: ${key}`);
6159
6350
  return v;
6160
6351
  }
6352
+ /** Like `requireString` but accepts '' — for `updateMemory`, where an empty string
6353
+ * is a valid value (clear the memory). */
6354
+ function requireStringAllowEmpty(args, key) {
6355
+ const v = args[key];
6356
+ if (typeof v !== "string") throw new Error(`missing required string argument: ${key}`);
6357
+ return v;
6358
+ }
6161
6359
  function optionalString(args, key) {
6162
6360
  const v = args[key];
6163
6361
  if (v === void 0 || v === null) return void 0;
@@ -6377,13 +6575,48 @@ const SLACK_TOOLS = [
6377
6575
  }, ["url"])
6378
6576
  }
6379
6577
  ];
6380
- SLACK_TOOLS.map((t) => t.name);
6381
6578
  /**
6382
- * The default MCP tool set for an agent, gated by its integrations. Today only
6383
- * Slack is supported, so an agent with no Slack integration gets no tools.
6579
+ * The agent's long-term memory tools — available to EVERY agent regardless of
6580
+ * integrations. Memory is a directory (`<agent-root>/memory/`) with a `MEMORY.md`
6581
+ * index (injected into the prompt each session) plus `<topic>.md` files the agent
6582
+ * reads on demand. Keep the index short; move detail into topic files.
6583
+ */
6584
+ const MEMORY_TOOLS = [
6585
+ {
6586
+ name: "listMemory",
6587
+ description: "List your memory files — the MEMORY.md index plus any topic files you have written. Use this to discover what you have stored before reading or updating it.",
6588
+ inputSchema: obj({})
6589
+ },
6590
+ {
6591
+ name: "readMemory",
6592
+ description: "Read one of your memory files. Omit `path` (or pass \"MEMORY.md\") to read the index; pass a topic file name (e.g. \"deploys.md\") to read that topic. The index is already shown to you at the start of each session — use this to pull the detail behind an index entry when it is relevant.",
6593
+ inputSchema: obj({ path: {
6594
+ type: "string",
6595
+ description: "Memory file name (e.g. \"deploys.md\"). Defaults to the MEMORY.md index."
6596
+ } })
6597
+ },
6598
+ {
6599
+ name: "writeMemory",
6600
+ description: "Save one of your memory files (whole-file overwrite). Omit `path` to write the MEMORY.md index; pass a topic file name (e.g. \"deploys.md\") for a topic. Keep the INDEX short — a scannable list that links to topic files (e.g. \"- [deploys](deploys.md) — how we ship\"); put the actual detail in the topic files. Use for durable facts across sessions: conventions, decisions, who to ask, things you had to re-learn. A flat directory — no subfolders in the path.",
6601
+ inputSchema: obj({
6602
+ path: {
6603
+ type: "string",
6604
+ description: "Memory file name (e.g. \"deploys.md\"). Defaults to the MEMORY.md index. No subdirectories."
6605
+ },
6606
+ content: {
6607
+ type: "string",
6608
+ description: "The full new file contents, in Markdown. Replaces the file."
6609
+ }
6610
+ }, ["content"])
6611
+ }
6612
+ ];
6613
+ [...SLACK_TOOLS, ...MEMORY_TOOLS].map((t) => t.name);
6614
+ /**
6615
+ * The default MCP tool set for an agent. The memory tools are always present
6616
+ * (every agent has memory); Slack tools are gated by a Slack integration.
6384
6617
  */
6385
6618
  function toolsForIntegrations(integrations) {
6386
- const tools = [];
6619
+ const tools = [...MEMORY_TOOLS];
6387
6620
  if (integrations.some((i) => i.platform === "slack")) tools.push(...SLACK_TOOLS);
6388
6621
  return tools;
6389
6622
  }
@@ -63012,6 +63245,7 @@ const STATUS_ACTION = {
63012
63245
  manage: "ac_manage",
63013
63246
  setModel: "ac_set_model",
63014
63247
  setEffort: "ac_set_effort",
63248
+ setPermissionMode: "ac_set_permission_mode",
63015
63249
  setFast: "ac_set_fast",
63016
63250
  setOutput: "ac_set_output",
63017
63251
  cancel: "ac_cancel",
@@ -63095,6 +63329,34 @@ function buildStatusModal(info, sessionKey, link) {
63095
63329
  text: `*Model:* ${info.model}`
63096
63330
  }
63097
63331
  });
63332
+ const permissionModes = info.permissionModes ?? [];
63333
+ if (permissionModes.length > 0) {
63334
+ const opts = info.permissionMode && !permissionModes.includes(info.permissionMode) ? [info.permissionMode, ...permissionModes] : permissionModes;
63335
+ const option = (m) => ({
63336
+ text: {
63337
+ type: "plain_text",
63338
+ text: clampTo(m, 75)
63339
+ },
63340
+ value: m
63341
+ });
63342
+ blocks.push({
63343
+ type: "section",
63344
+ text: {
63345
+ type: "mrkdwn",
63346
+ text: "*Permission mode*"
63347
+ },
63348
+ accessory: {
63349
+ type: "static_select",
63350
+ action_id: STATUS_ACTION.setPermissionMode,
63351
+ placeholder: {
63352
+ type: "plain_text",
63353
+ text: "Permission mode"
63354
+ },
63355
+ ...info.permissionMode ? { initial_option: option(info.permissionMode) } : {},
63356
+ options: opts.map(option)
63357
+ }
63358
+ });
63359
+ }
63098
63360
  const efforts = info.efforts ?? [];
63099
63361
  if (efforts.length > 0) {
63100
63362
  const opts = info.effort && !efforts.includes(info.effort) ? [info.effort, ...efforts] : efforts;
@@ -63459,6 +63721,7 @@ var SlackConnection = class {
63459
63721
  deps;
63460
63722
  app;
63461
63723
  queue;
63724
+ assistantDmThreads = /* @__PURE__ */ new Map();
63462
63725
  botUserId = "";
63463
63726
  /** The appToken this socket is keyed by (one socket per unique appToken). */
63464
63727
  appToken;
@@ -63499,8 +63762,8 @@ var SlackConnection = class {
63499
63762
  this.workspaceUrl = auth.url ?? "";
63500
63763
  log?.debug(`slack: auth.test ok → bot user ${this.botUserId} (bot_id ${this.botId || "n/a"})`);
63501
63764
  const deliver = (ev, kind) => {
63502
- const msg = normalizeSlackEvent(ev, { traceId: this.deps.newTraceId() });
63503
- log?.debug(`slack: inbound ${kind} ch=${msg.channel} user=${msg.sender.id} isBot=${msg.sender.isBot} isDm=${msg.isDm} mentions=[${msg.mentionedBots.join(",")}] text=${JSON.stringify(msg.text.slice(0, 80))}`);
63765
+ const msg = normalizeSlackEvent(this.withAssistantThread(ev), { traceId: this.deps.newTraceId() });
63766
+ log?.debug(`slack: inbound ${kind} ch=${msg.channel} thread=${msg.thread ?? "none"} user=${msg.sender.id} isBot=${msg.sender.isBot} isDm=${msg.isDm} mentions=[${msg.mentionedBots.join(",")}] text=${JSON.stringify(msg.text.slice(0, 80))}`);
63504
63767
  this.deps.onMessage(msg);
63505
63768
  };
63506
63769
  this.app.message(async ({ message }) => {
@@ -63516,6 +63779,10 @@ var SlackConnection = class {
63516
63779
  if (!ev.channel) return;
63517
63780
  deliver(ev, "app_mention");
63518
63781
  });
63782
+ this.app.event("assistant_thread_started", async ({ event }) => {
63783
+ const thread = this.rememberAssistantThread(event);
63784
+ if (thread) log?.debug(`slack: assistant thread started ch=${thread.channel} thread=${thread.threadTs}`);
63785
+ });
63519
63786
  this.app.event("member_joined_channel", async ({ event }) => {
63520
63787
  const ev = event;
63521
63788
  if (ev.user !== this.botUserId) return;
@@ -63562,6 +63829,16 @@ var SlackConnection = class {
63562
63829
  effort
63563
63830
  });
63564
63831
  });
63832
+ this.app.action(STATUS_ACTION.setPermissionMode, async ({ ack, action, body }) => {
63833
+ await ack();
63834
+ const sessionKey = body?.view?.private_metadata ?? action.block_id;
63835
+ const permissionMode = action.selected_option?.value;
63836
+ if (sessionKey && permissionMode) this.deps.onStatusAction?.({
63837
+ kind: "set-permission-mode",
63838
+ sessionKey,
63839
+ permissionMode
63840
+ });
63841
+ });
63565
63842
  this.app.action(STATUS_ACTION.setFast, async ({ ack, action, body }) => {
63566
63843
  await ack();
63567
63844
  const sessionKey = body?.view?.private_metadata ?? action.block_id;
@@ -63597,6 +63874,25 @@ var SlackConnection = class {
63597
63874
  await this.app.start();
63598
63875
  log?.debug("slack: app.start resolved → socket established");
63599
63876
  }
63877
+ rememberAssistantThread(ev) {
63878
+ const channel = ev.assistant_thread?.channel_id;
63879
+ const threadTs = ev.assistant_thread?.thread_ts;
63880
+ if (!channel || !threadTs) return null;
63881
+ this.assistantDmThreads.set(channel, threadTs);
63882
+ return {
63883
+ channel,
63884
+ threadTs
63885
+ };
63886
+ }
63887
+ withAssistantThread(ev) {
63888
+ if (ev.thread_ts || !ev.channel) return ev;
63889
+ if (ev.channel_type !== "im" && !ev.channel.startsWith("D")) return ev;
63890
+ const threadTs = this.assistantDmThreads.get(ev.channel);
63891
+ return threadTs ? {
63892
+ ...ev,
63893
+ thread_ts: threadTs
63894
+ } : ev;
63895
+ }
63600
63896
  async postMessage(channel, text, threadTs) {
63601
63897
  return this.queue.enqueue(async () => {
63602
63898
  return (await this.app.client.chat.postMessage({
@@ -64852,6 +65148,50 @@ function makeLogger(level = "info") {
64852
65148
  };
64853
65149
  }
64854
65150
  //#endregion
65151
+ //#region src/cp/wire-slice.ts
65152
+ /**
65153
+ * Pure helpers for slicing file bytes into a single WS frame — shared by the
65154
+ * workspace and memory readers so both honour the 256 KiB wire cap and never
65155
+ * split a UTF-8 character across slices.
65156
+ */
65157
+ /** Encoded-payload ceiling, leaving headroom under MAX_FRAME_BYTES for the
65158
+ * envelope (id/ts/type/corr + fencing ext, well under 4 KiB). */
65159
+ const REPLY_BUDGET$1 = MAX_FRAME_BYTES - 4096;
65160
+ /** The encoded size of the payload the wire will carry (JSON.stringify matches
65161
+ * the codec's `encode`), measured in bytes. */
65162
+ function encodedBytes$1(payload) {
65163
+ return Buffer.byteLength(JSON.stringify(payload));
65164
+ }
65165
+ /** Largest index ≤ `len` that lands on a UTF-8 character boundary — i.e. never
65166
+ * cuts a multi-byte sequence. If the slice's trailing char is incomplete, the
65167
+ * boundary is placed before that char (its bytes belong to the next slice). */
65168
+ function utf8Boundary$1(buf, len) {
65169
+ if (len >= buf.length) len = buf.length;
65170
+ if (len <= 0) return 0;
65171
+ let start = len - 1;
65172
+ while (start > 0 && ((buf[start] ?? 0) & 192) === 128) start--;
65173
+ const lead = buf[start] ?? 0;
65174
+ const seqLen = lead < 128 ? 1 : (lead & 224) === 192 ? 2 : (lead & 240) === 224 ? 3 : (lead & 248) === 240 ? 4 : 1;
65175
+ return start + seqLen <= len ? len : start;
65176
+ }
65177
+ /** Shrink `content` (a utf8 view of `slice[0..end]`) until its JSON-escaped size
65178
+ * fits `REPLY_BUDGET`, keeping the cut on a UTF-8 boundary. Returns the final
65179
+ * `{ end, content }`. Control-byte-heavy text escapes ~6×, so the raw byte limit
65180
+ * is not enough on its own. */
65181
+ function fitToBudget(slice, end) {
65182
+ let content = slice.toString("utf8", 0, end);
65183
+ while (end > 0 && encodedBytes$1(content) > REPLY_BUDGET$1) {
65184
+ const factor = REPLY_BUDGET$1 / encodedBytes$1(content);
65185
+ const shrunk = Math.max(1, Math.floor(end * factor * .9));
65186
+ end = utf8Boundary$1(slice, Math.min(shrunk, end - 1));
65187
+ content = slice.toString("utf8", 0, end);
65188
+ }
65189
+ return {
65190
+ end,
65191
+ content
65192
+ };
65193
+ }
65194
+ //#endregion
64855
65195
  //#region src/cp/workspace-reader.ts
64856
65196
  /**
64857
65197
  * `WorkspaceReader` — the read-only seam answering the CP's `workspace/list` and
@@ -64876,9 +65216,6 @@ function makeLogger(level = "info") {
64876
65216
  */
64877
65217
  /** Bytes sniffed from the head of a file for binary (NUL byte) detection. */
64878
65218
  const SNIFF_BYTES = 8192;
64879
- /** Encoded-payload ceiling, leaving headroom under MAX_FRAME_BYTES for the
64880
- * envelope (id/ts/type/corr + fencing ext, well under 4 KiB). */
64881
- const REPLY_BUDGET$1 = MAX_FRAME_BYTES - 4096;
64882
65219
  /** Path-containment / bad-request violation → `BAD_PAYLOAD` on the wire. */
64883
65220
  var WorkspaceViolationError = class extends Error {
64884
65221
  constructor(message) {
@@ -64886,29 +65223,12 @@ var WorkspaceViolationError = class extends Error {
64886
65223
  this.name = "WorkspaceViolationError";
64887
65224
  }
64888
65225
  };
64889
- function isErrno(err, code) {
65226
+ function isErrno$1(err, code) {
64890
65227
  return err?.code === code;
64891
65228
  }
64892
65229
  function under(root, p) {
64893
65230
  return p === root || p.startsWith(root + sp.sep);
64894
65231
  }
64895
- /** Largest index ≤ `len` that lands on a UTF-8 character boundary — i.e. never
64896
- * cuts a multi-byte sequence. If the slice's trailing char is incomplete, the
64897
- * boundary is placed before that char (its bytes belong to the next slice). */
64898
- function utf8Boundary$1(buf, len) {
64899
- if (len >= buf.length) len = buf.length;
64900
- if (len <= 0) return 0;
64901
- let start = len - 1;
64902
- while (start > 0 && ((buf[start] ?? 0) & 192) === 128) start--;
64903
- const lead = buf[start] ?? 0;
64904
- const seqLen = lead < 128 ? 1 : (lead & 224) === 192 ? 2 : (lead & 240) === 224 ? 3 : (lead & 248) === 240 ? 4 : 1;
64905
- return start + seqLen <= len ? len : start;
64906
- }
64907
- /** The encoded size of the payload the wire will carry (JSON.stringify matches
64908
- * the codec's `encode`), measured in bytes. */
64909
- function encodedBytes$1(payload) {
64910
- return Buffer.byteLength(JSON.stringify(payload));
64911
- }
64912
65232
  function createWorkspaceReader(workspaceRootByAgent) {
64913
65233
  function rootFor(agentId) {
64914
65234
  const root = workspaceRootByAgent(agentId);
@@ -64930,7 +65250,7 @@ function createWorkspaceReader(workspaceRootByAgent) {
64930
65250
  realRoot: await promises.realpath(root)
64931
65251
  };
64932
65252
  } catch (err) {
64933
- if (isErrno(err, "ENOENT")) return {
65253
+ if (isErrno$1(err, "ENOENT")) return {
64934
65254
  resolved,
64935
65255
  realRoot: null
64936
65256
  };
@@ -64959,14 +65279,14 @@ function createWorkspaceReader(workspaceRootByAgent) {
64959
65279
  try {
64960
65280
  target = await canonicalUnder(realRoot, resolved);
64961
65281
  } catch (err) {
64962
- if (isErrno(err, "ENOENT")) return notFound;
65282
+ if (isErrno$1(err, "ENOENT")) return notFound;
64963
65283
  throw err;
64964
65284
  }
64965
65285
  let dirents;
64966
65286
  try {
64967
65287
  dirents = await promises.readdir(target, { withFileTypes: true });
64968
65288
  } catch (err) {
64969
- if (isErrno(err, "ENOENT") || isErrno(err, "ENOTDIR")) return notFound;
65289
+ if (isErrno$1(err, "ENOENT") || isErrno$1(err, "ENOTDIR")) return notFound;
64970
65290
  throw err;
64971
65291
  }
64972
65292
  const entries = [];
@@ -65016,7 +65336,7 @@ function createWorkspaceReader(workspaceRootByAgent) {
65016
65336
  try {
65017
65337
  st = await promises.lstat(resolved);
65018
65338
  } catch (err) {
65019
- if (isErrno(err, "ENOENT")) return notFound;
65339
+ if (isErrno$1(err, "ENOENT")) return notFound;
65020
65340
  throw err;
65021
65341
  }
65022
65342
  if (!st.isFile()) throw new WorkspaceViolationError("not a regular file");
@@ -65046,13 +65366,9 @@ function createWorkspaceReader(workspaceRootByAgent) {
65046
65366
  slice = buf.subarray(0, bytesRead);
65047
65367
  }
65048
65368
  let end = utf8Boundary$1(slice, slice.length);
65049
- let content = slice.toString("utf8", 0, end);
65050
- while (end > 0 && encodedBytes$1(content) > REPLY_BUDGET$1) {
65051
- const factor = REPLY_BUDGET$1 / encodedBytes$1(content);
65052
- const shrunk = Math.max(1, Math.floor(end * factor * .9));
65053
- end = utf8Boundary$1(slice, Math.min(shrunk, end - 1));
65054
- content = slice.toString("utf8", 0, end);
65055
- }
65369
+ const fitted = fitToBudget(slice, end);
65370
+ end = fitted.end;
65371
+ const content = fitted.content;
65056
65372
  const nextOffset = req.offset + end;
65057
65373
  return {
65058
65374
  agentId: req.agentId,
@@ -65083,6 +65399,103 @@ function compareEntries(a, b) {
65083
65399
  return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
65084
65400
  }
65085
65401
  //#endregion
65402
+ //#region src/cp/memory-reader.ts
65403
+ /**
65404
+ * `MemoryReader` — the seam answering the CP's `memory/list`, `memory/read` and
65405
+ * `memory/write` REQs for an agent's memory directory (`<agent-root>/memory/`).
65406
+ * The files live only on the daemon (§1/§12); reads stream single byte slices,
65407
+ * writes replace one whole file.
65408
+ *
65409
+ * Path containment lives in `agents/memory.ts` (`resolveInMemoryDir`): a topic
65410
+ * path that escapes the memory dir throws `MemoryPathError` → `BAD_PAYLOAD`. An
65411
+ * unknown agent is also `BAD_PAYLOAD`; a not-yet-created file/dir is data
65412
+ * (`exists:false`), not an error. Frame-size safety mirrors the workspace reader:
65413
+ * the read slice is bounded by the *encoded* reply size and ends on a UTF-8
65414
+ * boundary. Never log memory contents.
65415
+ */
65416
+ /** Unknown-agent violation → `BAD_PAYLOAD` on the wire (path escapes surface as
65417
+ * MemoryPathError from agents/memory.ts, mapped the same way by the dispatcher). */
65418
+ var MemoryViolationError = class extends Error {
65419
+ constructor(message) {
65420
+ super(message);
65421
+ this.name = "MemoryViolationError";
65422
+ }
65423
+ };
65424
+ function isErrno(err, code) {
65425
+ return err?.code === code;
65426
+ }
65427
+ /** `agentDirByAgent` resolves an agent id → its root dir (which holds `memory/`),
65428
+ * or undefined for an unknown agent. */
65429
+ function createMemoryReader(agentDirByAgent) {
65430
+ function dirFor(agentId) {
65431
+ const dir = agentDirByAgent(agentId);
65432
+ if (!dir) throw new MemoryViolationError(`unknown agent "${agentId}"`);
65433
+ return dir;
65434
+ }
65435
+ return {
65436
+ async list(req) {
65437
+ const files = await listMemory(dirFor(req.agentId));
65438
+ return {
65439
+ agentId: req.agentId,
65440
+ exists: files.length > 0,
65441
+ entries: files
65442
+ };
65443
+ },
65444
+ async read(req) {
65445
+ const file = resolveInMemoryDir(dirFor(req.agentId), req.path);
65446
+ const notFound = {
65447
+ agentId: req.agentId,
65448
+ path: req.path,
65449
+ exists: false
65450
+ };
65451
+ let st;
65452
+ try {
65453
+ st = await promises.stat(file);
65454
+ } catch (err) {
65455
+ if (isErrno(err, "ENOENT")) return notFound;
65456
+ throw err;
65457
+ }
65458
+ if (!st.isFile()) return notFound;
65459
+ const size = st.size;
65460
+ const mtime = st.mtime.toISOString();
65461
+ const fh = await promises.open(file, "r");
65462
+ try {
65463
+ const want = Math.min(req.limit, Math.max(0, size - req.offset));
65464
+ let slice = Buffer.alloc(0);
65465
+ if (want > 0) {
65466
+ const buf = Buffer.alloc(want);
65467
+ const { bytesRead } = await fh.read(buf, 0, want, req.offset);
65468
+ slice = buf.subarray(0, bytesRead);
65469
+ }
65470
+ const { end, content } = fitToBudget(slice, utf8Boundary$1(slice, slice.length));
65471
+ const nextOffset = req.offset + end;
65472
+ return {
65473
+ agentId: req.agentId,
65474
+ path: req.path,
65475
+ exists: true,
65476
+ size,
65477
+ mtime,
65478
+ content,
65479
+ offset: req.offset,
65480
+ nextOffset,
65481
+ truncated: nextOffset < size
65482
+ };
65483
+ } finally {
65484
+ await fh.close();
65485
+ }
65486
+ },
65487
+ async write(req) {
65488
+ const { size, mtime } = await writeMemoryFile(dirFor(req.agentId), req.path, req.content, req.ifMatchMtime);
65489
+ return {
65490
+ agentId: req.agentId,
65491
+ path: req.path,
65492
+ size,
65493
+ mtime
65494
+ };
65495
+ }
65496
+ };
65497
+ }
65498
+ //#endregion
65086
65499
  //#region src/cp/correlator.ts
65087
65500
  /** Transport-free representation of a typed wire `error` REP. */
65088
65501
  var WireError = class extends Error {
@@ -65547,6 +65960,15 @@ var CpClient = class {
65547
65960
  case "workspace/gitpull":
65548
65961
  this.deps.workspaceGit.pull(frame.payload.agentId).then((result) => this.reply(frame, "workspace/gitpull/result", result)).catch((err) => this.workspaceError(frame.id, "workspace/gitpull", err));
65549
65962
  return;
65963
+ case "memory/list":
65964
+ this.deps.memoryReader.list(frame.payload).then((page) => this.reply(frame, "memory/list/page", page)).catch((err) => this.memoryError(frame.id, "memory/list", err));
65965
+ return;
65966
+ case "memory/read":
65967
+ this.deps.memoryReader.read(frame.payload).then((content) => this.reply(frame, "memory/read/content", content)).catch((err) => this.memoryError(frame.id, "memory/read", err));
65968
+ return;
65969
+ case "memory/write":
65970
+ this.deps.memoryReader.write(frame.payload).then((ok) => this.reply(frame, "memory/write/ok", ok)).catch((err) => this.memoryError(frame.id, "memory/write", err));
65971
+ return;
65550
65972
  case "webchat/message":
65551
65973
  if (!this.deps.onWebchatMessage) {
65552
65974
  this.sendError(frame.id, "INTERNAL", "webchat not supported", false);
@@ -65567,6 +65989,9 @@ var CpClient = class {
65567
65989
  case "webchat/set-effort":
65568
65990
  this.deps.onWebchatSetEffort?.(frame.payload);
65569
65991
  return;
65992
+ case "webchat/set-permission-mode":
65993
+ this.deps.onWebchatSetPermissionMode?.(frame.payload);
65994
+ return;
65570
65995
  case "webchat/set-fast":
65571
65996
  this.deps.onWebchatSetFast?.(frame.payload);
65572
65997
  return;
@@ -65590,6 +66015,18 @@ var CpClient = class {
65590
66015
  this.deps.log.warn(`cp: ${op} failed: ${err?.message}`);
65591
66016
  this.sendError(corr, "INTERNAL", `${op} failed`, false);
65592
66017
  }
66018
+ memoryError(corr, op, err) {
66019
+ if (err instanceof MemoryConflictError) {
66020
+ this.sendError(corr, "CONFLICT", `${op} failed: ${err.message}`, false);
66021
+ return;
66022
+ }
66023
+ if (err instanceof MemoryViolationError || err instanceof MemoryPathError || err instanceof MemoryTooLargeError) {
66024
+ this.sendError(corr, "BAD_PAYLOAD", `${op} failed: ${err.message}`, false);
66025
+ return;
66026
+ }
66027
+ this.deps.log.warn(`cp: ${op} failed: ${err?.message}`);
66028
+ this.sendError(corr, "INTERNAL", `${op} failed`, false);
66029
+ }
65593
66030
  reply(req, type, payload) {
65594
66031
  this.transport?.send(encode(buildEnvelope(type, payload, { corr: req.id })));
65595
66032
  }
@@ -66040,6 +66477,7 @@ function applySpecFields(raw, spec, opts) {
66040
66477
  if (spec.reasoningEffort !== void 0) raw.reasoningEffort = spec.reasoningEffort;
66041
66478
  if (spec.executionMode !== void 0) raw.executionMode = spec.executionMode;
66042
66479
  if (spec.fastMode !== void 0) raw.fastMode = spec.fastMode;
66480
+ if (spec.permissionMode !== void 0) raw.permissionMode = spec.permissionMode;
66043
66481
  if (spec.outputMode !== void 0) {
66044
66482
  const out = typeof raw.output === "object" && raw.output !== null ? raw.output : {};
66045
66483
  out.mode = spec.outputMode;
@@ -82267,6 +82705,7 @@ var Daemon = class Daemon {
82267
82705
  log: this.log,
82268
82706
  now: () => Date.now(),
82269
82707
  gatewayFor: (integrationId) => this.connByIntegration.get(integrationId),
82708
+ memoryDirFor: (agentId) => this.agents.get(agentId)?.dir,
82270
82709
  recordOutbound: (ctx, channel, thread, text, ts) => this.store.appendTranscript({
82271
82710
  channel,
82272
82711
  thread: thread ?? ctx.thread,
@@ -82285,11 +82724,11 @@ var Daemon = class Daemon {
82285
82724
  agentById: (id) => this.agents.get(id),
82286
82725
  mcpServersFor: ({ agent, channel, thread }) => {
82287
82726
  const tools = toolsForIntegrations(agent.integrations);
82727
+ if (tools.length === 0) return [];
82288
82728
  const integrationId = agent.integrations.find((i) => i.platform === "slack")?.id;
82289
- if (tools.length === 0 || !integrationId) return [];
82290
82729
  const token = this.mcp.register({
82291
82730
  agentId: agent.id,
82292
- integrationId,
82731
+ ...integrationId ? { integrationId } : {},
82293
82732
  channel,
82294
82733
  thread,
82295
82734
  tools
@@ -82631,6 +83070,7 @@ var Daemon = class Daemon {
82631
83070
  },
82632
83071
  configPrefs: {
82633
83072
  model: agent.runtimeOverrides?.model,
83073
+ permissionMode: agent.permissionMode,
82634
83074
  reasoningEffort: agent.reasoningEffort,
82635
83075
  fastMode: agent.fastMode,
82636
83076
  systemPrompt: agent.description
@@ -82653,6 +83093,10 @@ var Daemon = class Daemon {
82653
83093
  }
82654
83094
  this.seenMsgIds.add(msg.msgId);
82655
83095
  if (this.seenMsgIds.size > 2e3) this.seenMsgIds.clear();
83096
+ if (this.ownBotIdentities().has(msg.sender.id)) {
83097
+ this.log.debug(`routing: dropping own bot echo ${msg.msgId}`);
83098
+ return;
83099
+ }
82656
83100
  const command = parseCommand(msg.text);
82657
83101
  if (command) {
82658
83102
  this.handleCommand(command, msg);
@@ -82783,6 +83227,21 @@ var Daemon = class Daemon {
82783
83227
  }
82784
83228
  host.setSessionEffort(acpSessionId, effort).then(() => this.refreshStatusBarForKey(key)).catch((err) => this.log.warn(`set-effort failed: ${err.message}`));
82785
83229
  }
83230
+ /** Switch a session's permission/approval mode by its local key. Records the sticky
83231
+ * override and applies it live via ACP `mode` when the runtime advertises one. */
83232
+ setPermissionModeByKey(key, permissionMode) {
83233
+ const rec = this.store.getSession(key);
83234
+ if (!rec) return;
83235
+ this.store.setPermissionModeOverride(key, permissionMode);
83236
+ this.log.info(`session ${key} permission-mode override → "${permissionMode}"`);
83237
+ const acpSessionId = rec.acpSessionId;
83238
+ const host = this.hosts.get(rec.agentId);
83239
+ if (!acpSessionId || !host?.hasSession(acpSessionId)) {
83240
+ this.refreshStatusBarForKey(key);
83241
+ return;
83242
+ }
83243
+ host.setSessionPermissionMode(acpSessionId, permissionMode).then(() => this.refreshStatusBarForKey(key)).catch((err) => this.log.warn(`set-permission-mode failed: ${err.message}`));
83244
+ }
82786
83245
  /** Toggle a session's fast mode by its local key — the fast-mode counterpart of
82787
83246
  * {@link setModelByKey}. Records the sticky override and applies it live when the
82788
83247
  * current model offers a fast toggle. */
@@ -82823,6 +83282,10 @@ var Daemon = class Daemon {
82823
83282
  handleWebchatSetEffort(m) {
82824
83283
  this.setEffortByKey(this.webchatSessionKey(m.conversationId, m.agentId), m.effort);
82825
83284
  }
83285
+ /** Handle a webchat in-session permission-mode switch (C→D `webchat/set-permission-mode`). */
83286
+ handleWebchatSetPermissionMode(m) {
83287
+ this.setPermissionModeByKey(this.webchatSessionKey(m.conversationId, m.agentId), m.permissionMode);
83288
+ }
82826
83289
  /** Handle a webchat in-session fast-mode toggle (C→D `webchat/set-fast`). */
82827
83290
  handleWebchatSetFast(m) {
82828
83291
  this.setFastByKey(this.webchatSessionKey(m.conversationId, m.agentId), m.fastMode);
@@ -82846,6 +83309,8 @@ var Daemon = class Daemon {
82846
83309
  if (a.model) this.setModelByKey(a.sessionKey, a.model);
82847
83310
  } else if (a.kind === "set-effort") {
82848
83311
  if (a.effort) this.setEffortByKey(a.sessionKey, a.effort);
83312
+ } else if (a.kind === "set-permission-mode") {
83313
+ if (a.permissionMode) this.setPermissionModeByKey(a.sessionKey, a.permissionMode);
82849
83314
  } else if (a.kind === "set-fast") {
82850
83315
  if (a.fastMode !== void 0) this.setFastByKey(a.sessionKey, a.fastMode);
82851
83316
  } else if (a.kind === "set-output") {
@@ -83084,6 +83549,8 @@ var Daemon = class Daemon {
83084
83549
  const host = await this.ensureHostAsync(agentId);
83085
83550
  const override = this.store.getModelOverride(key);
83086
83551
  if (override) await host.setSessionModel(sessionId, override).catch((err) => this.log.debug(`model override "${override}" not applied: ${err.message}`));
83552
+ const permissionModeOverride = this.store.getPermissionModeOverride(key);
83553
+ if (permissionModeOverride) await host.setSessionPermissionMode(sessionId, permissionModeOverride).catch((err) => this.log.debug(`permission-mode override "${permissionModeOverride}" not applied: ${err.message}`));
83087
83554
  const effortOverride = this.store.getEffortOverride(key);
83088
83555
  if (effortOverride) await host.setSessionEffort(sessionId, effortOverride).catch((err) => this.log.debug(`effort override "${effortOverride}" not applied: ${err.message}`));
83089
83556
  const fastOverride = this.store.getFastModeOverride(key);
@@ -83347,12 +83814,15 @@ var Daemon = class Daemon {
83347
83814
  const host = this.hosts.get(agentId);
83348
83815
  const model = host?.modelOptions?.();
83349
83816
  const effort = host?.effortOptions?.();
83817
+ const permissionMode = host?.permissionModeOptions?.();
83350
83818
  const fast = host?.fastModeOption?.();
83351
83819
  const effortOverride = this.store.getEffortOverride(sessionKey);
83820
+ const permissionModeOverride = this.store.getPermissionModeOverride(sessionKey);
83352
83821
  const fastOverride = this.store.getFastModeOverride(sessionKey);
83353
83822
  return {
83354
83823
  model: model?.current ?? agent?.runtimeOverrides?.model,
83355
83824
  effort: effortOverride ?? effort?.current ?? agent?.reasoningEffort,
83825
+ permissionMode: permissionModeOverride ?? permissionMode?.current ?? agent?.permissionMode,
83356
83826
  fastMode: fastOverride ?? fast?.current ?? agent?.fastMode,
83357
83827
  contextUsed: usage.contextUsed,
83358
83828
  contextSize: usage.contextSize,
@@ -83369,6 +83839,7 @@ var Daemon = class Daemon {
83369
83839
  } : {},
83370
83840
  ...model?.models?.length ? { models: model.models } : {},
83371
83841
  ...effort?.efforts?.length ? { efforts: effort.efforts } : {},
83842
+ ...permissionMode?.modes?.length ? { permissionModes: permissionMode.modes } : {},
83372
83843
  ...fast ? { fastModeAvailable: true } : {},
83373
83844
  ...acpSessionId ? { sessionId: acpSessionId } : {}
83374
83845
  };
@@ -83995,10 +84466,12 @@ var Daemon = class Daemon {
83995
84466
  sessionRead: createSessionReader(this.store, (agentId) => this.replyConnFor(agentId)?.workspaceUrl),
83996
84467
  workspaceRead: createWorkspaceReader((id) => this.agents.get(id)?.workspace.path),
83997
84468
  workspaceGit: createWorkspaceGit((id) => this.agents.get(id)?.workspace.path),
84469
+ memoryReader: createMemoryReader((id) => this.agents.get(id)?.dir),
83998
84470
  onWebchatMessage: (m) => this.handleWebchatMessage(m),
83999
84471
  onWebchatClose: (m) => this.handleWebchatClose(m),
84000
84472
  onWebchatSetModel: (m) => this.handleWebchatSetModel(m),
84001
84473
  onWebchatSetEffort: (m) => this.handleWebchatSetEffort(m),
84474
+ onWebchatSetPermissionMode: (m) => this.handleWebchatSetPermissionMode(m),
84002
84475
  onWebchatSetFast: (m) => this.handleWebchatSetFast(m),
84003
84476
  onWebchatCancel: (m) => this.handleWebchatCancel(m),
84004
84477
  clock: systemClock,
@@ -84115,4 +84588,4 @@ async function runForeground(opts, deps = {}) {
84115
84588
  //#endregion
84116
84589
  export { ClientTransport as n, runForeground as t };
84117
84590
 
84118
- //# sourceMappingURL=run-foreground-DLxz75b4.js.map
84591
+ //# sourceMappingURL=run-foreground-mvTzCScX.js.map