@agentconnect.md/daemon 1.0.0-rc.293 → 1.0.0-rc.295

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
@@ -68493,8 +68493,8 @@ const [{ Command }, { fileURLToPath }, { runChat }, { runLogin }, { runForegroun
68493
68493
  import("./commander-B5QVEFzH.js"),
68494
68494
  import("node:url"),
68495
68495
  import("./chat-IRJd4_lj.js"),
68496
- import("./login-BAqikR3g.js"),
68497
- import("./run-foreground-B9nulgJU.js"),
68496
+ import("./login-COLOoyZB.js"),
68497
+ import("./run-foreground-Bqj9SR4V.js"),
68498
68498
  import("./service-6w0WKhdx.js"),
68499
68499
  import("./lock-CA9Bm8_s.js"),
68500
68500
  import("./paths-CcRUjFjy.js")
@@ -1,6 +1,6 @@
1
1
  import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
2
2
  import { _ as buildEnvelope, l as ConfigSchema, u as isFrame, v as decodeEnvelope, y as encode } from "./load-agents-8AzZXdVx.js";
3
- import { i as ClientTransport, t as runForeground } from "./run-foreground-yH77LpAv.js";
3
+ import { i as ClientTransport, t as runForeground } from "./run-foreground-Q1x-MDxZ.js";
4
4
  import { l as resolveRoot, t as configPath } from "./paths-DEcUO8lC.js";
5
5
  import { n as resolveController } from "./service-BQVw7wbz.js";
6
6
  import { dirname } from "node:path";
@@ -228,4 +228,4 @@ async function runLogin(opts, partial = {}) {
228
228
  //#endregion
229
229
  export { buildInstallOpts, persistCredentials, runLogin };
230
230
 
231
- //# sourceMappingURL=login-BAqikR3g.js.map
231
+ //# sourceMappingURL=login-COLOoyZB.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"login-BAqikR3g.js","names":["realRunForeground"],"sources":["../src/cp/auth-probe.ts","../src/cli/login.ts"],"sourcesContent":["/**\n * `probeAuth` — a one-shot CP auth check used by `agentconnect login`. It dials\n * the CP, sends ONLY the `auth` frame (no `register`), and resolves with the\n * authoritative daemonId from `auth/ok`, or a human-readable failure reason on\n * a `4401` close, a correlated `error`, a dial failure, or a timeout. It always\n * closes the socket and never leaves a connection open.\n */\nimport { buildEnvelope, decodeEnvelope, encode, isFrame } from '@agentconnect.md/protocol'\nimport { ClientTransport, type Transport } from '@agentconnect.md/connection'\nimport { CP_SUBPROTOCOL, CP_WS_PATH } from './client.js'\n\nexport interface ProbeResult {\n ok: boolean\n daemonId?: string\n reason?: string\n}\n\nexport interface ProbeOpts {\n url: string\n token: string\n daemonId?: string\n agentVersion?: string\n timeoutMs?: number\n /** Injectable for tests; defaults to dialing the real WebSocket. */\n connect?: () => Promise<Transport>\n}\n\nexport async function probeAuth(opts: ProbeOpts): Promise<ProbeResult> {\n const timeoutMs = opts.timeoutMs ?? 10_000\n const dial = opts.connect ?? (() => ClientTransport.dial(opts.url, { subprotocol: CP_SUBPROTOCOL, path: CP_WS_PATH }))\n\n let transport: Transport\n try {\n transport = await dial()\n } catch (err) {\n return { ok: false, reason: `cannot reach ${opts.url}: ${(err as Error).message}` }\n }\n\n const authPayload: Record<string, unknown> = {\n apiKey: opts.token,\n agentVersion: opts.agentVersion ?? 'probe'\n }\n if (opts.daemonId) authPayload.daemonId = opts.daemonId\n const frame = buildEnvelope('auth', authPayload)\n\n return await new Promise<ProbeResult>((resolve) => {\n let settled = false\n const done = (r: ProbeResult): void => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n try {\n transport.close(1000, 'probe done')\n } catch {\n // socket may already be closed\n }\n resolve(r)\n }\n\n const timer = setTimeout(\n () => done({ ok: false, reason: `timed out after ${timeoutMs}ms waiting for auth/ok` }),\n timeoutMs\n )\n\n transport.onMessage((text) => {\n const decoded = decodeEnvelope(text)\n if (!decoded.ok) return\n const f = decoded.frame\n if (f.corr !== frame.id) return\n if (f.type === 'error') {\n const e = f.payload as { code: string; message: string }\n done({ ok: false, reason: `${e.code}: ${e.message}` })\n return\n }\n if (isFrame('auth/ok')(f)) {\n const ok = f.payload as { daemonId: string }\n done({ ok: true, daemonId: ok.daemonId })\n }\n })\n\n transport.onClose((code) => {\n if (code === 4401) done({ ok: false, reason: 'authentication failed (4401) — check the daemon token' })\n else done({ ok: false, reason: `connection closed (${code}) before auth/ok` })\n })\n\n transport.send(encode(frame))\n })\n}\n","/**\n * `agentconnect login` — interactive onboarding. It tests the Control Plane auth\n * flow, persists credentials only on success, then offers to install the OS\n * service. If installed it starts the service (`up`) and returns; otherwise it\n * runs the daemon in the foreground (the same path as `agentconnect run`).\n *\n * `persistCredentials` is the pure config-merge half (token-only onboarding:\n * `daemonId` is omitted unless given — the first connect adopts it from the\n * token's `sub`). `runLogin` is the orchestrator, with injectable deps for tests.\n */\nimport { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigSchema } from '../config/config-schema.js'\nimport { resolveRoot, configPath } from '../paths.js'\nimport { DAEMON_VERSION } from '../version.js'\nimport { probeAuth, type ProbeResult } from '../cp/auth-probe.js'\nimport { resolveController, type InstallOpts } from '../service/index.js'\nimport { runForeground as realRunForeground } from './run-foreground.js'\n\nexport interface PersistCredsOpts {\n cpUrl: string\n cpKey: string\n daemonId?: string\n root?: string\n configPath?: string\n}\n\n/** Merge url/token into config.json (validated), returning the written path. */\nexport function persistCredentials(opts: PersistCredsOpts): string {\n const root = resolveRoot(opts.root)\n const file = opts.configPath ?? configPath(root)\n const raw: Record<string, unknown> = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { version: 1 }\n\n const cp = (raw.controlPlane as Record<string, unknown> | undefined) ?? {}\n raw.controlPlane = { ...cp, url: opts.cpUrl, key: opts.cpKey, enabled: true }\n if (opts.daemonId) raw.daemonId = opts.daemonId\n\n ConfigSchema.parse(raw) // fail before writing if the merge is invalid\n\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(raw, null, 2) + '\\n')\n return file\n}\n\nexport interface RunLoginOpts {\n cpUrl?: string\n cpKey?: string\n daemonId?: string\n root?: string\n configPath?: string\n /** The resolved CLI entry path. Must be supplied by the CLI entry point\n * (index.ts) so the correct bundle path is used rather than inferring it\n * from login.ts's own import.meta.url (which is the same bundle but the\n * `../` path calculation would be wrong). Falls back to import.meta.url\n * for the pure test path where login.ts is loaded standalone. */\n cliEntry?: string\n}\n\n/**\n * Build the InstallOpts for the OS service installer.\n *\n * Exported as a pure helper so it can be unit-tested independently of the\n * full `runLogin` orchestration. The caller (index.ts) passes `cliEntry` so\n * the correct bundle path always flows through; if omitted we fall back to\n * `import.meta.url` (correct only when login.ts is the entry, e.g. in tests).\n */\nexport function buildInstallOpts(opts: { root?: string; cliEntry?: string }): InstallOpts {\n return {\n execPath: process.execPath,\n cliEntry: opts.cliEntry ?? fileURLToPath(import.meta.url),\n includeRootEnv: resolveRoot(opts.root) !== resolveRoot(undefined)\n }\n}\n\nexport interface LoginDeps {\n probe: (o: { url: string; token: string }) => Promise<ProbeResult>\n installService: () => Promise<void>\n runForeground: () => Promise<void>\n out: NodeJS.WritableStream\n input: NodeJS.ReadableStream\n isTTY: boolean\n}\n\nfunction realDeps(opts: RunLoginOpts): LoginDeps {\n return {\n probe: (o) => probeAuth({ url: o.url, token: o.token, agentVersion: DAEMON_VERSION }),\n installService: async () => {\n const controller = resolveController({ root: opts.root })\n await controller.install(buildInstallOpts(opts))\n await controller.up()\n },\n runForeground: () =>\n realRunForeground({\n root: opts.root,\n overrides: { cpUrl: opts.cpUrl, cpKey: opts.cpKey, daemonId: opts.daemonId }\n }),\n out: process.stdout,\n input: process.stdin,\n isTTY: Boolean(process.stdin.isTTY)\n }\n}\n\n/** Read the next line from a readline interface via async iterator. */\nasync function nextLine(\n iter: AsyncIterableIterator<string>,\n prompt: string,\n out: NodeJS.WritableStream\n): Promise<string> {\n out.write(prompt)\n const { value, done } = await iter.next()\n if (done) throw new Error(`unexpected end of input waiting for: ${prompt}`)\n return (value as string).trim()\n}\n\nexport async function runLogin(opts: RunLoginOpts, partial: Partial<LoginDeps> = {}): Promise<void> {\n const deps: LoginDeps = { ...realDeps(opts), ...partial }\n const { out } = deps\n\n // ── Non-interactive: flags required; probe; persist. No prompts, no handoff. ──\n if (!deps.isTTY) {\n if (!opts.cpUrl) throw new Error('login requires --cp-url <url>')\n if (!opts.cpKey) throw new Error('login requires --cp-key <key>')\n const r = await deps.probe({ url: opts.cpUrl, token: opts.cpKey })\n if (!r.ok) throw new Error(`authentication failed: ${r.reason}`)\n persistCredentials({ ...opts, cpUrl: opts.cpUrl, cpKey: opts.cpKey })\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''} — credentials saved\\n`)\n return\n }\n\n // Both interactive handoffs read `<root>/config.json`: the installed service\n // runs `… run` (no `--config`), and the foreground `Daemon` loads by root. A\n // custom `--config` would persist credentials the daemon never reads, so the\n // install/run branch would look successful but start without them. Reject it;\n // `--root` is the knob that keeps persist + service + foreground in sync.\n if (opts.configPath) {\n throw new Error(\n 'interactive login does not support --config — use --root <dir> so the daemon and the installed service read the same config'\n )\n }\n\n // ── Interactive ──\n const rl = createInterface({ input: deps.input, output: out })\n const iter = rl[Symbol.asyncIterator]()\n let url = opts.cpUrl\n let token = opts.cpKey\n try {\n if (!url) url = await nextLine(iter, 'Control Plane URL: ', out)\n\n for (let attempt = 0; ; attempt++) {\n if (!token) token = await nextLine(iter, 'Daemon API key: ', out)\n out.write(`Testing connection to ${url}…\\n`)\n const r = await deps.probe({ url: url!, token: token! })\n if (r.ok) {\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''}\\n`)\n break\n }\n out.write(`✗ ${r.reason}\\n`)\n if (attempt >= 1) throw new Error('authentication failed')\n token = undefined // re-prompt on retry\n }\n\n persistCredentials({ ...opts, cpUrl: url!, cpKey: token! })\n\n const ans = (await nextLine(iter, 'Install AgentConnect as a background service? (y/N) ', out)).toLowerCase()\n if (ans === 'y' || ans === 'yes') {\n await deps.installService()\n out.write('Service installed and started. Manage it with `agentconnect up` / `down` / `status`.\\n')\n return\n }\n out.write('Starting in the foreground (Ctrl-C to stop)…\\n')\n } finally {\n rl.close()\n }\n\n await deps.runForeground()\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"login-COLOoyZB.js","names":["realRunForeground"],"sources":["../src/cp/auth-probe.ts","../src/cli/login.ts"],"sourcesContent":["/**\n * `probeAuth` — a one-shot CP auth check used by `agentconnect login`. It dials\n * the CP, sends ONLY the `auth` frame (no `register`), and resolves with the\n * authoritative daemonId from `auth/ok`, or a human-readable failure reason on\n * a `4401` close, a correlated `error`, a dial failure, or a timeout. It always\n * closes the socket and never leaves a connection open.\n */\nimport { buildEnvelope, decodeEnvelope, encode, isFrame } from '@agentconnect.md/protocol'\nimport { ClientTransport, type Transport } from '@agentconnect.md/connection'\nimport { CP_SUBPROTOCOL, CP_WS_PATH } from './client.js'\n\nexport interface ProbeResult {\n ok: boolean\n daemonId?: string\n reason?: string\n}\n\nexport interface ProbeOpts {\n url: string\n token: string\n daemonId?: string\n agentVersion?: string\n timeoutMs?: number\n /** Injectable for tests; defaults to dialing the real WebSocket. */\n connect?: () => Promise<Transport>\n}\n\nexport async function probeAuth(opts: ProbeOpts): Promise<ProbeResult> {\n const timeoutMs = opts.timeoutMs ?? 10_000\n const dial = opts.connect ?? (() => ClientTransport.dial(opts.url, { subprotocol: CP_SUBPROTOCOL, path: CP_WS_PATH }))\n\n let transport: Transport\n try {\n transport = await dial()\n } catch (err) {\n return { ok: false, reason: `cannot reach ${opts.url}: ${(err as Error).message}` }\n }\n\n const authPayload: Record<string, unknown> = {\n apiKey: opts.token,\n agentVersion: opts.agentVersion ?? 'probe'\n }\n if (opts.daemonId) authPayload.daemonId = opts.daemonId\n const frame = buildEnvelope('auth', authPayload)\n\n return await new Promise<ProbeResult>((resolve) => {\n let settled = false\n const done = (r: ProbeResult): void => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n try {\n transport.close(1000, 'probe done')\n } catch {\n // socket may already be closed\n }\n resolve(r)\n }\n\n const timer = setTimeout(\n () => done({ ok: false, reason: `timed out after ${timeoutMs}ms waiting for auth/ok` }),\n timeoutMs\n )\n\n transport.onMessage((text) => {\n const decoded = decodeEnvelope(text)\n if (!decoded.ok) return\n const f = decoded.frame\n if (f.corr !== frame.id) return\n if (f.type === 'error') {\n const e = f.payload as { code: string; message: string }\n done({ ok: false, reason: `${e.code}: ${e.message}` })\n return\n }\n if (isFrame('auth/ok')(f)) {\n const ok = f.payload as { daemonId: string }\n done({ ok: true, daemonId: ok.daemonId })\n }\n })\n\n transport.onClose((code) => {\n if (code === 4401) done({ ok: false, reason: 'authentication failed (4401) — check the daemon token' })\n else done({ ok: false, reason: `connection closed (${code}) before auth/ok` })\n })\n\n transport.send(encode(frame))\n })\n}\n","/**\n * `agentconnect login` — interactive onboarding. It tests the Control Plane auth\n * flow, persists credentials only on success, then offers to install the OS\n * service. If installed it starts the service (`up`) and returns; otherwise it\n * runs the daemon in the foreground (the same path as `agentconnect run`).\n *\n * `persistCredentials` is the pure config-merge half (token-only onboarding:\n * `daemonId` is omitted unless given — the first connect adopts it from the\n * token's `sub`). `runLogin` is the orchestrator, with injectable deps for tests.\n */\nimport { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigSchema } from '../config/config-schema.js'\nimport { resolveRoot, configPath } from '../paths.js'\nimport { DAEMON_VERSION } from '../version.js'\nimport { probeAuth, type ProbeResult } from '../cp/auth-probe.js'\nimport { resolveController, type InstallOpts } from '../service/index.js'\nimport { runForeground as realRunForeground } from './run-foreground.js'\n\nexport interface PersistCredsOpts {\n cpUrl: string\n cpKey: string\n daemonId?: string\n root?: string\n configPath?: string\n}\n\n/** Merge url/token into config.json (validated), returning the written path. */\nexport function persistCredentials(opts: PersistCredsOpts): string {\n const root = resolveRoot(opts.root)\n const file = opts.configPath ?? configPath(root)\n const raw: Record<string, unknown> = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { version: 1 }\n\n const cp = (raw.controlPlane as Record<string, unknown> | undefined) ?? {}\n raw.controlPlane = { ...cp, url: opts.cpUrl, key: opts.cpKey, enabled: true }\n if (opts.daemonId) raw.daemonId = opts.daemonId\n\n ConfigSchema.parse(raw) // fail before writing if the merge is invalid\n\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(raw, null, 2) + '\\n')\n return file\n}\n\nexport interface RunLoginOpts {\n cpUrl?: string\n cpKey?: string\n daemonId?: string\n root?: string\n configPath?: string\n /** The resolved CLI entry path. Must be supplied by the CLI entry point\n * (index.ts) so the correct bundle path is used rather than inferring it\n * from login.ts's own import.meta.url (which is the same bundle but the\n * `../` path calculation would be wrong). Falls back to import.meta.url\n * for the pure test path where login.ts is loaded standalone. */\n cliEntry?: string\n}\n\n/**\n * Build the InstallOpts for the OS service installer.\n *\n * Exported as a pure helper so it can be unit-tested independently of the\n * full `runLogin` orchestration. The caller (index.ts) passes `cliEntry` so\n * the correct bundle path always flows through; if omitted we fall back to\n * `import.meta.url` (correct only when login.ts is the entry, e.g. in tests).\n */\nexport function buildInstallOpts(opts: { root?: string; cliEntry?: string }): InstallOpts {\n return {\n execPath: process.execPath,\n cliEntry: opts.cliEntry ?? fileURLToPath(import.meta.url),\n includeRootEnv: resolveRoot(opts.root) !== resolveRoot(undefined)\n }\n}\n\nexport interface LoginDeps {\n probe: (o: { url: string; token: string }) => Promise<ProbeResult>\n installService: () => Promise<void>\n runForeground: () => Promise<void>\n out: NodeJS.WritableStream\n input: NodeJS.ReadableStream\n isTTY: boolean\n}\n\nfunction realDeps(opts: RunLoginOpts): LoginDeps {\n return {\n probe: (o) => probeAuth({ url: o.url, token: o.token, agentVersion: DAEMON_VERSION }),\n installService: async () => {\n const controller = resolveController({ root: opts.root })\n await controller.install(buildInstallOpts(opts))\n await controller.up()\n },\n runForeground: () =>\n realRunForeground({\n root: opts.root,\n overrides: { cpUrl: opts.cpUrl, cpKey: opts.cpKey, daemonId: opts.daemonId }\n }),\n out: process.stdout,\n input: process.stdin,\n isTTY: Boolean(process.stdin.isTTY)\n }\n}\n\n/** Read the next line from a readline interface via async iterator. */\nasync function nextLine(\n iter: AsyncIterableIterator<string>,\n prompt: string,\n out: NodeJS.WritableStream\n): Promise<string> {\n out.write(prompt)\n const { value, done } = await iter.next()\n if (done) throw new Error(`unexpected end of input waiting for: ${prompt}`)\n return (value as string).trim()\n}\n\nexport async function runLogin(opts: RunLoginOpts, partial: Partial<LoginDeps> = {}): Promise<void> {\n const deps: LoginDeps = { ...realDeps(opts), ...partial }\n const { out } = deps\n\n // ── Non-interactive: flags required; probe; persist. No prompts, no handoff. ──\n if (!deps.isTTY) {\n if (!opts.cpUrl) throw new Error('login requires --cp-url <url>')\n if (!opts.cpKey) throw new Error('login requires --cp-key <key>')\n const r = await deps.probe({ url: opts.cpUrl, token: opts.cpKey })\n if (!r.ok) throw new Error(`authentication failed: ${r.reason}`)\n persistCredentials({ ...opts, cpUrl: opts.cpUrl, cpKey: opts.cpKey })\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''} — credentials saved\\n`)\n return\n }\n\n // Both interactive handoffs read `<root>/config.json`: the installed service\n // runs `… run` (no `--config`), and the foreground `Daemon` loads by root. A\n // custom `--config` would persist credentials the daemon never reads, so the\n // install/run branch would look successful but start without them. Reject it;\n // `--root` is the knob that keeps persist + service + foreground in sync.\n if (opts.configPath) {\n throw new Error(\n 'interactive login does not support --config — use --root <dir> so the daemon and the installed service read the same config'\n )\n }\n\n // ── Interactive ──\n const rl = createInterface({ input: deps.input, output: out })\n const iter = rl[Symbol.asyncIterator]()\n let url = opts.cpUrl\n let token = opts.cpKey\n try {\n if (!url) url = await nextLine(iter, 'Control Plane URL: ', out)\n\n for (let attempt = 0; ; attempt++) {\n if (!token) token = await nextLine(iter, 'Daemon API key: ', out)\n out.write(`Testing connection to ${url}…\\n`)\n const r = await deps.probe({ url: url!, token: token! })\n if (r.ok) {\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''}\\n`)\n break\n }\n out.write(`✗ ${r.reason}\\n`)\n if (attempt >= 1) throw new Error('authentication failed')\n token = undefined // re-prompt on retry\n }\n\n persistCredentials({ ...opts, cpUrl: url!, cpKey: token! })\n\n const ans = (await nextLine(iter, 'Install AgentConnect as a background service? (y/N) ', out)).toLowerCase()\n if (ans === 'y' || ans === 'yes') {\n await deps.installService()\n out.write('Service installed and started. Manage it with `agentconnect up` / `down` / `status`.\\n')\n return\n }\n out.write('Starting in the foreground (Ctrl-C to stop)…\\n')\n } finally {\n rl.close()\n }\n\n await deps.runForeground()\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { t as runForeground } from "./run-foreground-Q1x-MDxZ.js";
2
+ export { runForeground };
@@ -67447,7 +67447,7 @@ function normalizeSlackEvent(event, ctx) {
67447
67447
  //#region src/slack/send-queue.ts
67448
67448
  /**
67449
67449
  * §9.1 SlackSendQueue: serialize outbound Slack Web API writes (chat.postMessage
67450
- * / chat.update / assistant.threads.setStatus) per connection and space them by a
67450
+ * / chat.update / assistant.threads.setStatus / assistant.threads.setTitle) per connection and space them by a
67451
67451
  * minimum interval so streamed edits don't trip Slack's Tier-3 rate limit
67452
67452
  * (chat.postMessage ~50 rpm). FIFO: preserves the order the converger emits.
67453
67453
  *
@@ -68816,6 +68816,23 @@ var SlackConnection = class {
68816
68816
  }
68817
68817
  });
68818
68818
  }
68819
+ /**
68820
+ * Best-effort assistant thread title (assistant.threads.setTitle). This API is
68821
+ * only valid for Slack app threads created by the Agents feature; the daemon
68822
+ * gates calls to DM sessions before reaching this boundary. Never throws into
68823
+ * dispatch, including when the shared send queue itself times out.
68824
+ */
68825
+ async setTitle(channel, threadTs, title) {
68826
+ try {
68827
+ await this.queue.enqueue(() => this.app.client.assistant.threads.setTitle({
68828
+ channel_id: channel,
68829
+ thread_ts: threadTs,
68830
+ title
68831
+ }));
68832
+ } catch (err) {
68833
+ this.deps.log?.debug(`slack: setTitle failed (ch=${channel} thread=${threadTs}): ${err.message}`);
68834
+ }
68835
+ }
68819
68836
  async stop() {
68820
68837
  await this.app.stop();
68821
68838
  }
@@ -197212,12 +197229,12 @@ function neutralizeDelimiters(body) {
197212
197229
  return body.split("\n").map((line) => line.trimStart().startsWith("----- ") ? `\\${line}` : line).join("\n");
197213
197230
  }
197214
197231
  /** Trailing instruction for github fires on a NUMBERED thread (issue/PR): the
197215
- * daemon streams the agent's reply back as a GitHub comment automatically, so
197216
- * the agent must NOT post its own — a manual `gh issue comment` would
197217
- * double-post. Only emitted when the poster will actually run (a thread number
197218
- * exists); push fires have no thread, so they never see this. */
197232
+ * daemon is the sole writer of the reply comment, so the agent must return its
197233
+ * answer rather than mutate comments/reviews through CLI, MCP, connector, or API.
197234
+ * Only emitted when the poster will actually run (a thread number exists); push
197235
+ * fires have no thread, so they never see this. */
197219
197236
  function githubReplyHint(c) {
197220
- return ["", `Your reply is posted back to ${`${c.repo ?? "this thread"}#${c.number}`} automatically as a GitHub comment (it streams as you write, so keep it a single self-contained reply). Do NOT run \`gh issue comment\` / \`gh pr comment\` yourself — that would double-post. You may still use \`gh\` for READ-only inspection (viewing the thread, diffs, files).`].join("\n");
197237
+ return ["", `Your final reply is posted back to ${`${c.repo ?? "this thread"}#${c.number}`} automatically as a GitHub comment. Keep it self-contained. The daemon exclusively owns that reply comment. Do NOT create, update, or delete GitHub issue/PR comments or reviews through any CLI (including \`gh\`), MCP/connector, API, or other tool — that would race or double-post with the automatic poster. Use GitHub tools only for READ-only inspection (viewing the thread, diffs, files), then return the content to publish in your final reply.`].join("\n");
197221
197238
  }
197222
197239
  /** The github-kind turn text: a trusted metadata header + the FENCED excerpt.
197223
197240
  * The title rides the header (relay-sanitized to one capped line) — it is
@@ -197312,16 +197329,202 @@ function buildHookMessage(msg, traceId) {
197312
197329
  //#endregion
197313
197330
  //#region src/github/poster.ts
197314
197331
  const DEFAULT_EDIT_INTERVAL_MS = 2e3;
197332
+ const DEFAULT_FINALIZE_TIMEOUT_MS = 15e3;
197315
197333
  /** GitHub caps comment bodies at 65536 chars — truncate with a marker well below. */
197316
197334
  const MAX_COMMENT_CHARS = 6e4;
197317
197335
  /** Consecutive API failures before the stream gives up for this turn. */
197318
197336
  const MAX_FAILURES = 3;
197337
+ const TRUNCATION_MARKER = "\n\n_(truncated — see the session transcript for the full reply)_";
197338
+ const LEGACY_BOUNDARIES = /* @__PURE__ */ new Set([
197339
+ "agent_thought_chunk",
197340
+ "tool_call",
197341
+ "tool_call_update",
197342
+ "plan"
197343
+ ]);
197344
+ const NO_FINAL_MESSAGE_ID = Symbol("no-final-message-id");
197345
+ const NO_COMMENTARY_MESSAGE_ID = Symbol("no-commentary-message-id");
197346
+ function record(value) {
197347
+ return value !== null && typeof value === "object" ? value : void 0;
197348
+ }
197349
+ /**
197350
+ * Select the one human-facing reply from an ACP turn.
197351
+ *
197352
+ * codex-acp ≥1.1.1 gives us the authoritative `_meta.codex.phase`; older Codex
197353
+ * builds still provide messageId, so the last logical message is the fallback.
197354
+ * Other ACP runtimes may provide neither — for them the last text run after a
197355
+ * thought/tool/plan boundary is the best runtime-agnostic approximation.
197356
+ */
197357
+ var GithubReplyCollector = class {
197358
+ messages = /* @__PURE__ */ new Map();
197359
+ nextOrder = 0;
197360
+ lastCommentarySeen = -1;
197361
+ legacyRuns = [];
197362
+ legacyCurrent;
197363
+ /** Consume one raw ACP update. Returns the complete in-progress final answer, if authoritative. */
197364
+ onUpdate(update) {
197365
+ const u = record(update);
197366
+ if (!u) return void 0;
197367
+ const kind = typeof u.sessionUpdate === "string" ? u.sessionUpdate : void 0;
197368
+ if (kind !== "agent_message_chunk") {
197369
+ if (kind && LEGACY_BOUNDARIES.has(kind)) this.commitLegacyBoundary();
197370
+ return;
197371
+ }
197372
+ const content = record(u.content);
197373
+ if (content?.type !== "text" || typeof content.text !== "string") return void 0;
197374
+ const text = content.text;
197375
+ const seen = this.nextOrder++;
197376
+ const codex = record(record(u._meta)?.codex);
197377
+ const rawPhase = typeof codex?.phase === "string" ? codex.phase : void 0;
197378
+ const phase = rawPhase === "commentary" || rawPhase === "final_answer" ? rawPhase : "unknown";
197379
+ const messageId = typeof u.messageId === "string" && u.messageId ? u.messageId : void 0;
197380
+ if (phase === "commentary" || phase === "final_answer") {
197381
+ if (phase === "commentary") this.lastCommentarySeen = seen;
197382
+ }
197383
+ if (messageId || phase !== "unknown") {
197384
+ const key = messageId ?? (phase === "final_answer" ? NO_FINAL_MESSAGE_ID : NO_COMMENTARY_MESSAGE_ID);
197385
+ let candidate = this.messages.get(key);
197386
+ if (!candidate) {
197387
+ const provisional = this.takeProvisionalText();
197388
+ candidate = {
197389
+ firstSeen: provisional?.firstSeen ?? seen,
197390
+ lastSeen: seen,
197391
+ phase: "unknown",
197392
+ text: provisional?.text ?? ""
197393
+ };
197394
+ }
197395
+ if (phase === "final_answer" || phase === "commentary" && candidate.phase === "unknown") candidate.phase = phase;
197396
+ candidate.text += text;
197397
+ candidate.lastSeen = seen;
197398
+ this.messages.set(key, candidate);
197399
+ } else if (text && isKnownNonReplyText(text)) {
197400
+ this.commitLegacyBoundary();
197401
+ this.legacyRuns.push({
197402
+ firstSeen: seen,
197403
+ lastSeen: seen,
197404
+ phase: "unknown",
197405
+ text
197406
+ });
197407
+ } else if (text) {
197408
+ this.legacyCurrent ??= {
197409
+ firstSeen: seen,
197410
+ lastSeen: seen,
197411
+ phase: "unknown",
197412
+ text: ""
197413
+ };
197414
+ this.legacyCurrent.text += text;
197415
+ this.legacyCurrent.lastSeen = seen;
197416
+ }
197417
+ return this.explicitFinalText();
197418
+ }
197419
+ finalText(completed = true) {
197420
+ const explicit = this.explicitFinalText();
197421
+ if (explicit || !completed) return explicit;
197422
+ const unknown = [
197423
+ ...this.messages.values(),
197424
+ ...this.legacyRuns,
197425
+ ...this.legacyCurrent ? [this.legacyCurrent] : []
197426
+ ].filter((candidate) => candidate.phase === "unknown" && candidate.text.trim() && !isKnownNonReplyText(candidate.text) && candidate.lastSeen > this.lastCommentarySeen).sort((a, b) => a.lastSeen - b.lastSeen).at(-1)?.text;
197427
+ return unknown?.trim() ? unknown : void 0;
197428
+ }
197429
+ explicitFinalText() {
197430
+ const text = [...this.messages.values()].filter((candidate) => candidate.phase === "final_answer" && candidate.text.trim()).sort((a, b) => a.firstSeen - b.firstSeen).map((candidate) => candidate.text).join("\n\n");
197431
+ return text.trim() ? text : void 0;
197432
+ }
197433
+ commitLegacyBoundary() {
197434
+ if (!this.legacyCurrent?.text.trim()) return;
197435
+ this.legacyRuns.push(this.legacyCurrent);
197436
+ this.legacyCurrent = void 0;
197437
+ }
197438
+ /** A phase/messageId can arrive one delta late; claim the immediately preceding
197439
+ * anonymous run so the public reply does not lose its prefix. Known adapter
197440
+ * chrome is deliberately left anonymous and therefore never folded into final. */
197441
+ takeProvisionalText() {
197442
+ const candidate = this.legacyCurrent;
197443
+ if (!candidate || !candidate.text.trim() || isKnownNonReplyText(candidate.text)) return void 0;
197444
+ this.legacyCurrent = void 0;
197445
+ return candidate;
197446
+ }
197447
+ };
197448
+ /** Adapter-generated status text that is neither commentary nor a user-facing
197449
+ * final. These older paths lack messageId/phase, so content is the only signal. */
197450
+ function isKnownNonReplyText(text) {
197451
+ const normalized = text.trim();
197452
+ return /^\*?context compacted\b/i.test(normalized) || /^(?:config )?warning:/i.test(normalized) || /^\*conversation interrupted\*$/i.test(normalized);
197453
+ }
197454
+ function escapeMarkdownText(raw) {
197455
+ return (raw.replace(/\s+/g, " ").trim().slice(0, 200) || "unknown agent").replace(/([\\`*_\[\]~<>])/g, "\\$1");
197456
+ }
197457
+ function markdownUrl(raw) {
197458
+ try {
197459
+ const url = new URL(raw);
197460
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
197461
+ const normalized = url.toString();
197462
+ if (normalized.length > 2048) return void 0;
197463
+ return `<${normalized.replace(/</g, "%3C").replace(/>/g, "%3E")}>`;
197464
+ } catch {
197465
+ return;
197466
+ }
197467
+ }
197468
+ function attributionFooter(attribution) {
197469
+ if (!attribution) return "";
197470
+ const name = escapeMarkdownText(attribution.agentName);
197471
+ const agentUrl = markdownUrl(attribution.agentUrl);
197472
+ const sessionUrl = markdownUrl(attribution.sessionUrl);
197473
+ const parts = [`Replied by ${agentUrl ? `[${name}](${agentUrl})` : name}`];
197474
+ if (sessionUrl) parts.push(`[View session](${sessionUrl})`);
197475
+ return `\n\n---\n_${parts.join(" · ")}_`;
197476
+ }
197477
+ /** Return the fenced-code block left open at the end of `text`, if any. */
197478
+ function unclosedMarkdownFence(text) {
197479
+ let open;
197480
+ for (const rawLine of text.split("\n")) {
197481
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
197482
+ const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
197483
+ if (!match) continue;
197484
+ const run = match[1];
197485
+ const marker = run[0];
197486
+ const rest = match[2];
197487
+ if (!open) {
197488
+ if (marker === "`" && rest.includes("`")) continue;
197489
+ open = {
197490
+ marker,
197491
+ length: run.length
197492
+ };
197493
+ } else if (marker === open.marker && run.length >= open.length && !rest.trim()) open = void 0;
197494
+ }
197495
+ return open;
197496
+ }
197497
+ function fenceCloser(text) {
197498
+ const open = unclosedMarkdownFence(text);
197499
+ if (!open) return "";
197500
+ return `${text.endsWith("\n") ? "" : "\n"}${open.marker.repeat(open.length)}`;
197501
+ }
197502
+ /** UTF-16-safe prefix: never leave a high surrogate separated from its pair. */
197503
+ function safePrefix(text, maxChars) {
197504
+ let end = Math.min(text.length, Math.max(0, maxChars));
197505
+ if (end > 0 && end < text.length && text.charCodeAt(end - 1) >= 55296 && text.charCodeAt(end - 1) <= 56319 && text.charCodeAt(end) >= 56320 && text.charCodeAt(end) <= 57343) end -= 1;
197506
+ return text.slice(0, end);
197507
+ }
197508
+ /** Fit a reply prefix into `budget`, closing a fence before marker/footer chrome. */
197509
+ function truncatedMarkdownPrefix(text, budget) {
197510
+ let contentBudget = Math.max(0, budget);
197511
+ for (;;) {
197512
+ const prefix = safePrefix(text, contentBudget);
197513
+ const closer = fenceCloser(prefix);
197514
+ if (prefix.length + closer.length <= budget) return prefix + closer;
197515
+ const nextBudget = Math.max(0, budget - closer.length);
197516
+ if (nextBudget >= contentBudget) return safePrefix(text, budget);
197517
+ contentBudget = nextBudget;
197518
+ }
197519
+ }
197319
197520
  var GithubCommentStream = class {
197320
197521
  deps;
197321
197522
  repo;
197322
197523
  issueNumber;
197323
197524
  buffer = "";
197324
- posted = "";
197525
+ /** Rendered body actually on GitHub. Comparing rendered output avoids no-op
197526
+ * PATCHes once an overlong reply has already filled the truncation budget. */
197527
+ postedBody = null;
197325
197528
  commentId = null;
197326
197529
  inFlight = Promise.resolve();
197327
197530
  busy = false;
@@ -197330,9 +197533,14 @@ var GithubCommentStream = class {
197330
197533
  lastSendAt = 0;
197331
197534
  failures = 0;
197332
197535
  finished = false;
197536
+ abandoned = false;
197537
+ finishPromise;
197538
+ abort = new AbortController();
197333
197539
  sched;
197334
197540
  editIntervalMs;
197335
- constructor(deps, repo, issueNumber) {
197541
+ finalizeTimeoutMs;
197542
+ footer;
197543
+ constructor(deps, repo, issueNumber, attribution) {
197336
197544
  this.deps = deps;
197337
197545
  this.repo = repo;
197338
197546
  this.issueNumber = issueNumber;
@@ -197342,30 +197550,72 @@ var GithubCommentStream = class {
197342
197550
  clearTimeout: (h) => clearTimeout(h)
197343
197551
  };
197344
197552
  this.editIntervalMs = deps.minEditIntervalMs ?? DEFAULT_EDIT_INTERVAL_MS;
197553
+ this.finalizeTimeoutMs = deps.finalizeTimeoutMs ?? DEFAULT_FINALIZE_TIMEOUT_MS;
197554
+ this.footer = attributionFooter(attribution);
197345
197555
  }
197346
197556
  /** Fold one agent-output chunk in; schedules the create/edit as needed. Never throws. */
197347
197557
  append(text) {
197348
- if (this.finished || this.failures >= MAX_FAILURES || !text) return;
197558
+ if (this.finished || this.abandoned || this.failures >= MAX_FAILURES || !text) return;
197349
197559
  this.buffer += text;
197350
197560
  this.schedule();
197351
197561
  }
197562
+ /** Replace the streamed final-answer body with its complete value (messageId-safe). */
197563
+ replace(text) {
197564
+ if (this.finished || this.abandoned || this.failures >= MAX_FAILURES || !text) return;
197565
+ this.buffer = text;
197566
+ this.schedule();
197567
+ }
197352
197568
  /**
197353
197569
  * Turn over: flush whatever is left as the final body. Resolves once the
197354
197570
  * last write settled (or was abandoned); never rejects. Idempotent.
197355
197571
  */
197356
- async finish() {
197357
- if (this.finished) return;
197572
+ finish(finalBody) {
197573
+ if (!this.finishPromise) this.finishPromise = this.finishOnce(finalBody);
197574
+ return this.finishPromise;
197575
+ }
197576
+ async finishOnce(finalBody) {
197577
+ if (finalBody !== void 0) this.buffer = finalBody;
197358
197578
  this.finished = true;
197359
197579
  if (this.timer !== null) {
197360
197580
  this.sched.clearTimeout(this.timer);
197361
197581
  this.timer = null;
197362
197582
  }
197363
- await this.inFlight;
197364
- if (this.buffer !== this.posted && this.buffer.trim() && this.failures < MAX_FAILURES) await this.send();
197365
- await this.inFlight;
197583
+ if (!this.buffer.trim() && !this.busy) return;
197584
+ let deadlineHandle;
197585
+ try {
197586
+ const deadline = new Promise((resolve) => {
197587
+ deadlineHandle = this.sched.setTimeout(() => {
197588
+ this.abandoned = true;
197589
+ this.abort.abort();
197590
+ try {
197591
+ this.deps.log.warn(`github poster: final flush timed out on ${this.repo}#${this.issueNumber}`);
197592
+ } catch {} finally {
197593
+ resolve();
197594
+ }
197595
+ }, this.finalizeTimeoutMs);
197596
+ });
197597
+ const drain = (async () => {
197598
+ await this.inFlight;
197599
+ if (this.abandoned) return;
197600
+ if ((this.commentId !== null || !this.renderedCurrent()) && this.buffer.trim() && this.failures < MAX_FAILURES) {
197601
+ this.inFlight = this.send();
197602
+ await this.inFlight;
197603
+ }
197604
+ })();
197605
+ await Promise.race([drain, deadline]);
197606
+ } catch (err) {
197607
+ try {
197608
+ this.deps.log.warn(`github poster: final flush failed on ${this.repo}#${this.issueNumber} (${String(err)})`);
197609
+ } catch {}
197610
+ } finally {
197611
+ if (deadlineHandle !== void 0) try {
197612
+ this.sched.clearTimeout(deadlineHandle);
197613
+ } catch {}
197614
+ }
197366
197615
  }
197367
197616
  schedule() {
197368
- if (this.busy || this.timer !== null) return;
197617
+ if (this.abandoned || this.busy || this.timer !== null) return;
197618
+ if (this.renderedCurrent()) return;
197369
197619
  if (!this.hasSent) {
197370
197620
  this.inFlight = this.send();
197371
197621
  return;
@@ -197379,32 +197629,41 @@ var GithubCommentStream = class {
197379
197629
  }
197380
197630
  /** One create-or-edit round; merges everything buffered at send time. */
197381
197631
  async send() {
197382
- if (this.busy) return;
197632
+ if (this.abandoned || this.busy) return;
197383
197633
  this.busy = true;
197384
197634
  const snapshot = this.buffer;
197385
- const body = this.render(snapshot);
197386
197635
  try {
197636
+ const body = this.render(snapshot);
197387
197637
  const token = await this.deps.token();
197638
+ if (this.abandoned) return;
197388
197639
  if (this.commentId === null) {
197389
197640
  const created = await this.request("POST", `/repos/${this.repo}/issues/${this.issueNumber}/comments`, token, body);
197390
197641
  this.commentId = created.id;
197391
197642
  this.deps.log.debug(`github poster: comment ${created.id} opened on ${this.repo}#${this.issueNumber}`);
197392
197643
  } else await this.request("PATCH", `/repos/${this.repo}/issues/comments/${this.commentId}`, token, body);
197393
- this.posted = snapshot;
197644
+ this.postedBody = body;
197394
197645
  this.failures = 0;
197395
197646
  } catch (err) {
197647
+ if (this.abandoned) return;
197396
197648
  this.failures += 1;
197397
197649
  this.deps.log.warn(`github poster: ${this.commentId === null ? "create" : "edit"} failed on ${this.repo}#${this.issueNumber} (${err.message?.slice(0, 200)})${this.failures >= MAX_FAILURES ? " — giving up for this turn" : ""}`);
197398
197650
  } finally {
197399
197651
  this.hasSent = true;
197400
197652
  this.lastSendAt = this.sched.now();
197401
197653
  this.busy = false;
197402
- if (!this.finished && this.buffer !== this.posted && this.failures < MAX_FAILURES) this.schedule();
197654
+ if (!this.finished && !this.renderedCurrent() && this.failures < MAX_FAILURES) this.schedule();
197403
197655
  }
197404
197656
  }
197405
197657
  render(text) {
197406
- if (text.length <= MAX_COMMENT_CHARS) return text;
197407
- return `${text.slice(0, MAX_COMMENT_CHARS)}\n\n_(truncated — see the session transcript for the full reply)_`;
197658
+ if (text.length + this.footer.length <= MAX_COMMENT_CHARS) {
197659
+ const closer = this.footer ? fenceCloser(text) : "";
197660
+ if (text.length + closer.length + this.footer.length <= MAX_COMMENT_CHARS) return text + closer + this.footer;
197661
+ }
197662
+ const suffix = TRUNCATION_MARKER + this.footer;
197663
+ return `${truncatedMarkdownPrefix(text, Math.max(0, MAX_COMMENT_CHARS - suffix.length))}${suffix}`;
197664
+ }
197665
+ renderedCurrent() {
197666
+ return this.postedBody !== null && this.render(this.buffer) === this.postedBody;
197408
197667
  }
197409
197668
  async request(method, path, token, body) {
197410
197669
  const res = await (this.deps.fetchImpl ?? fetch)(`${this.deps.baseUrl ?? "https://api.github.com"}${path}`, {
@@ -197415,6 +197674,7 @@ var GithubCommentStream = class {
197415
197674
  "content-type": "application/json",
197416
197675
  "x-github-api-version": "2022-11-28"
197417
197676
  },
197677
+ signal: this.abort.signal,
197418
197678
  body: JSON.stringify({ body })
197419
197679
  });
197420
197680
  if (!res.ok) throw new Error(`GitHub ${method} ${res.status}`);
@@ -217100,6 +217360,7 @@ var Daemon = class Daemon {
217100
217360
  rec,
217101
217361
  agentId,
217102
217362
  platform: msg.platform,
217363
+ isDm: msg.isDm,
217103
217364
  sessionKey: key,
217104
217365
  acpSessionId: sessionId,
217105
217366
  channel: msg.channel,
@@ -217116,7 +217377,7 @@ var Daemon = class Daemon {
217116
217377
  index: 0,
217117
217378
  replyText: ""
217118
217379
  } } : {},
217119
- ...githubReply ? { github: { stream: this.makeGithubReplyStream(agentId, githubReply) } } : {}
217380
+ ...githubReply ? { github: this.makeGithubReply(agentId, githubReply, sessionId) } : {}
217120
217381
  };
217121
217382
  this.pending.set(sessionId, p);
217122
217383
  if (callMeta) this.activeTurnCallMeta.set(key, callMeta);
@@ -217195,8 +217456,10 @@ var Daemon = class Daemon {
217195
217456
  this.clearIdle(p);
217196
217457
  this.releasePermissions(sessionId);
217197
217458
  this.releaseElicits(sessionId);
217198
- this.pending.delete(sessionId);
217199
217459
  if (callMeta) this.activeTurnCallMeta.delete(key);
217460
+ this.clearCancelBackstop(sessionId);
217461
+ if (p.github) await p.github.stream.finish(p.github.collector.finalText(finalPhase === "end")).catch((err) => this.log.warn(`github poster: final flush failed (${formatErr(err)})`));
217462
+ this.pending.delete(sessionId);
217200
217463
  this.store.setSessionState(key, "idle", this.clock.now());
217201
217464
  this.emitSessionMetadataSnapshot({
217202
217465
  sessionId,
@@ -217206,8 +217469,6 @@ var Daemon = class Daemon {
217206
217469
  channel: msg.channel,
217207
217470
  thread: statusThread
217208
217471
  });
217209
- this.clearCancelBackstop(sessionId);
217210
- if (p.github) p.github.stream.finish().catch((err) => this.log.warn(`github poster: final flush failed (${formatErr(err)})`));
217211
217472
  p.resolveDone();
217212
217473
  }
217213
217474
  return sessionId;
@@ -217263,6 +217524,7 @@ var Daemon = class Daemon {
217263
217524
  /**
217264
217525
  * Apply one converger action against the session's Slack connection:
217265
217526
  * - set-status → assistant.threads.setStatus (best-effort; '' clears)
217527
+ * - set-title → assistant.threads.setTitle (best-effort; app DMs only)
217266
217528
  * - post → a new thread message; ALSO recorded to the transcript as the
217267
217529
  * agent's reply text (sender = agentId), so other agents replaying the thread
217268
217530
  * see what it said. Keyed on `statusThread` (the §8.5 replay key).
@@ -217287,6 +217549,9 @@ var Daemon = class Daemon {
217287
217549
  case "set-status":
217288
217550
  if (p.statusThread) await conn.setStatus(p.channel, p.statusThread, action.text, action.loadingMessages);
217289
217551
  return;
217552
+ case "set-title":
217553
+ if (p.statusThread) await conn.setTitle(p.channel, p.statusThread, action.text);
217554
+ return;
217290
217555
  case "post": {
217291
217556
  const ts = await conn.postMessage(p.channel, action.text, p.thread);
217292
217557
  this.store.appendTranscript({
@@ -217489,14 +217754,14 @@ var Daemon = class Daemon {
217489
217754
  * the View-session link is always available. */
217490
217755
  sessionLink(acpSessionId) {
217491
217756
  const orgSeg = this.cpOrgSlug ? `/${encodeURIComponent(this.cpOrgSlug)}` : "";
217492
- return `${this.webAppBase()}${orgSeg}/sessions/${acpSessionId}`;
217757
+ return `${this.webAppBase()}${orgSeg}/sessions/${encodeURIComponent(acpSessionId)}`;
217493
217758
  }
217494
217759
  /** The console deep link to an agent: `<base>/<orgSlug>/agents/<agentId>` — the
217495
217760
  * shared-bot footer's "sent by <agent_name>" link (§10.1). Same org-seg rule as
217496
217761
  * {@link sessionLink}. */
217497
217762
  agentLink(agentId) {
217498
217763
  const orgSeg = this.cpOrgSlug ? `/${encodeURIComponent(this.cpOrgSlug)}` : "";
217499
- return `${this.webAppBase()}${orgSeg}/agents/${agentId}`;
217764
+ return `${this.webAppBase()}${orgSeg}/agents/${encodeURIComponent(agentId)}`;
217500
217765
  }
217501
217766
  /** The shared-bot footer context for a turn, or undefined when this isn't a shared
217502
217767
  * Slack channel (then the classic "sent by agentconnect.md" footer is used). A
@@ -217843,15 +218108,20 @@ var Daemon = class Daemon {
217843
218108
  channel: p.channel,
217844
218109
  thread: p.statusThread
217845
218110
  });
218111
+ const slackTitle = typeof update.title === "string" ? update.title.trim() : "";
218112
+ if (p.platform === "slack" && p.isDm && slackTitle) this.enqueueApply(p, {
218113
+ kind: "set-title",
218114
+ text: slackTitle
218115
+ });
217846
218116
  }
217847
218117
  if (p.webchat) this.emitWebchatUpdate(p, update);
217848
218118
  else {
217849
218119
  for (const action of p.conv.onUpdate(update)) this.enqueueApply(p, action);
217850
218120
  this.armIdle(p);
217851
218121
  }
217852
- if (p.github && update?.sessionUpdate === "agent_message_chunk" && update.content?.type === "text") {
217853
- const text = update.content.text ?? "";
217854
- if (text) p.github.stream.append(text);
218122
+ if (p.github) {
218123
+ const finalAnswer = p.github.collector.onUpdate(update);
218124
+ if (finalAnswer) p.github.stream.replace(finalAnswer);
217855
218125
  }
217856
218126
  for (const ev of p.rec.onUpdate(update)) this.recordEvent(p.agentId, p.channel, p.statusThread, ev);
217857
218127
  }
@@ -218538,16 +218808,26 @@ var Daemon = class Daemon {
218538
218808
  }
218539
218809
  return this.dispatch(agentId, msg, target?.integrationId, void 0, void 0, void 0, githubReply);
218540
218810
  }
218541
- /** Build the per-turn GitHub comment stream, tokened via the repo-targeted
218542
- * gitcred mint (issues/PR write, no contents — never enters agent env). */
218543
- makeGithubReplyStream(agentId, ref) {
218544
- return new GithubCommentStream({
218545
- token: async () => (await this.gitCreds.getPostToken(agentId, ref.repo)).token,
218546
- log: {
218547
- warn: (m) => this.log.warn(m),
218548
- debug: (m) => this.log.debug(m)
218549
- }
218550
- }, ref.repo, ref.number);
218811
+ /** Build the per-turn GitHub final-answer selector and comment stream, tokened
218812
+ * via the repo-targeted gitcred mint (issues/PR write, no contents — never
218813
+ * enters agent env). Attribution is fixed at turn creation so every edit has
218814
+ * the same agent/session footer. */
218815
+ makeGithubReply(agentId, ref, sessionId) {
218816
+ const agent = this.agents.get(agentId);
218817
+ return {
218818
+ collector: new GithubReplyCollector(),
218819
+ stream: new GithubCommentStream({
218820
+ token: async () => (await this.gitCreds.getPostToken(agentId, ref.repo)).token,
218821
+ log: {
218822
+ warn: (m) => this.log.warn(m),
218823
+ debug: (m) => this.log.debug(m)
218824
+ }
218825
+ }, ref.repo, ref.number, {
218826
+ agentName: agent?.name ?? agentId,
218827
+ agentUrl: this.agentLink(agentId),
218828
+ sessionUrl: this.sessionLink(sessionId)
218829
+ })
218830
+ };
218551
218831
  }
218552
218832
  /**
218553
218833
  * A cron fired for `agentId` (§8.6 bypass). The fire is stamped into D11
@@ -218835,4 +219115,4 @@ async function runForeground(opts, deps = {}) {
218835
219115
  //#endregion
218836
219116
  export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
218837
219117
 
218838
- //# sourceMappingURL=run-foreground-yH77LpAv.js.map
219118
+ //# sourceMappingURL=run-foreground-Q1x-MDxZ.js.map