@agentconnect.md/daemon 1.0.0-rc.294 → 1.0.0-rc.296

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-BvZ3PrjS.js"),
68497
- import("./run-foreground-BcZug4FD.js"),
68496
+ import("./login-vU_lQeMG.js"),
68497
+ import("./run-foreground-K-ATxgOm.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-DWgt3Xvd.js";
3
+ import { i as ClientTransport, t as runForeground } from "./run-foreground-DasVEmdw.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-BvZ3PrjS.js.map
231
+ //# sourceMappingURL=login-vU_lQeMG.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"login-BvZ3PrjS.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-vU_lQeMG.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":";;;;;;;;;;;;;;;;;AA2BA,eAAsB,UAAU,MAAuC;CACrE,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,OAAO,KAAK,kBAAkB,gBAAgB,KAAK,KAAK,KAAK;EAAE,aAAA;EAA6B,MAAA;CAAiB,CAAC;CAEpH,IAAI;CACJ,IAAI;EACF,YAAY,MAAM,KAAK;CACzB,SAAS,KAAK;EACZ,OAAO;GAAE,IAAI;GAAO,QAAQ,gBAAgB,KAAK,IAAI,IAAK,IAAc;EAAU;CACpF;CAEA,MAAM,cAAuC;EAC3C,QAAQ,KAAK;EACb,cAAc,KAAK,gBAAgB;CACrC;CACA,IAAI,KAAK,UAAU,YAAY,WAAW,KAAK;CAC/C,MAAM,QAAQ,cAAc,QAAQ,WAAW;CAE/C,OAAO,MAAM,IAAI,SAAsB,YAAY;EACjD,IAAI,UAAU;EACd,MAAM,QAAQ,MAAyB;GACrC,IAAI,SAAS;GACb,UAAU;GACV,aAAa,KAAK;GAClB,IAAI;IACF,UAAU,MAAM,KAAM,YAAY;GACpC,QAAQ,CAER;GACA,QAAQ,CAAC;EACX;EAEA,MAAM,QAAQ,iBACN,KAAK;GAAE,IAAI;GAAO,QAAQ,mBAAmB,UAAU;EAAwB,CAAC,GACtF,SACF;EAEA,UAAU,WAAW,SAAS;GAC5B,MAAM,UAAU,eAAe,IAAI;GACnC,IAAI,CAAC,QAAQ,IAAI;GACjB,MAAM,IAAI,QAAQ;GAClB,IAAI,EAAE,SAAS,MAAM,IAAI;GACzB,IAAI,EAAE,SAAS,SAAS;IACtB,MAAM,IAAI,EAAE;IACZ,KAAK;KAAE,IAAI;KAAO,QAAQ,GAAG,EAAE,KAAK,IAAI,EAAE;IAAU,CAAC;IACrD;GACF;GACA,IAAI,QAAQ,SAAS,CAAC,CAAC,CAAC,GAAG;IACzB,MAAM,KAAK,EAAE;IACb,KAAK;KAAE,IAAI;KAAM,UAAU,GAAG;IAAS,CAAC;GAC1C;EACF,CAAC;EAED,UAAU,SAAS,SAAS;GAC1B,IAAI,SAAS,MAAM,KAAK;IAAE,IAAI;IAAO,QAAQ;GAAwD,CAAC;QACjG,KAAK;IAAE,IAAI;IAAO,QAAQ,sBAAsB,KAAK;GAAkB,CAAC;EAC/E,CAAC;EAED,UAAU,KAAK,OAAO,KAAK,CAAC;CAC9B,CAAC;AACH;;;;;;;;;;;;;;ACzDA,SAAgB,mBAAmB,MAAgC;CACjE,MAAM,OAAO,YAAY,KAAK,IAAI;CAClC,MAAM,OAAO,KAAK,cAAc,WAAW,IAAI;CAC/C,MAAM,MAA+B,WAAW,IAAI,IAAI,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE;CAG9G,IAAI,eAAe;EAAE,GADT,IAAI,gBAAwD,CAAC;EAC7C,KAAK,KAAK;EAAO,KAAK,KAAK;EAAO,SAAS;CAAK;CAC5E,IAAI,KAAK,UAAU,IAAI,WAAW,KAAK;CAEvC,aAAa,MAAM,GAAG;CAEtB,UAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CAC5C,cAAc,MAAM,KAAK,UAAU,KAAK,MAAM,CAAC,IAAI,IAAI;CACvD,OAAO;AACT;;;;;;;;;AAwBA,SAAgB,iBAAiB,MAAyD;CACxF,OAAO;EACL,UAAU,QAAQ;EAClB,UAAU,KAAK,YAAY,cAAc,OAAO,KAAK,GAAG;EACxD,gBAAgB,YAAY,KAAK,IAAI,MAAM,YAAY,KAAA,CAAS;CAClE;AACF;AAWA,SAAS,SAAS,MAA+B;CAC/C,OAAO;EACL,QAAQ,MAAM,UAAU;GAAE,KAAK,EAAE;GAAK,OAAO,EAAE;GAAO,cAAc;EAAe,CAAC;EACpF,gBAAgB,YAAY;GAC1B,MAAM,aAAa,kBAAkB,EAAE,MAAM,KAAK,KAAK,CAAC;GACxD,MAAM,WAAW,QAAQ,iBAAiB,IAAI,CAAC;GAC/C,MAAM,WAAW,GAAG;EACtB;EACA,qBACEA,cAAkB;GAChB,MAAM,KAAK;GACX,WAAW;IAAE,OAAO,KAAK;IAAO,OAAO,KAAK;IAAO,UAAU,KAAK;GAAS;EAC7E,CAAC;EACH,KAAK,QAAQ;EACb,OAAO,QAAQ;EACf,OAAO,QAAQ,QAAQ,MAAM,KAAK;CACpC;AACF;;AAGA,eAAe,SACb,MACA,QACA,KACiB;CACjB,IAAI,MAAM,MAAM;CAChB,MAAM,EAAE,OAAO,SAAS,MAAM,KAAK,KAAK;CACxC,IAAI,MAAM,MAAM,IAAI,MAAM,wCAAwC,QAAQ;CAC1E,OAAQ,MAAiB,KAAK;AAChC;AAEA,eAAsB,SAAS,MAAoB,UAA8B,CAAC,GAAkB;CAClG,MAAM,OAAkB;EAAE,GAAG,SAAS,IAAI;EAAG,GAAG;CAAQ;CACxD,MAAM,EAAE,QAAQ;CAGhB,IAAI,CAAC,KAAK,OAAO;EACf,IAAI,CAAC,KAAK,OAAO,MAAM,IAAI,MAAM,+BAA+B;EAChE,IAAI,CAAC,KAAK,OAAO,MAAM,IAAI,MAAM,+BAA+B;EAChE,MAAM,IAAI,MAAM,KAAK,MAAM;GAAE,KAAK,KAAK;GAAO,OAAO,KAAK;EAAM,CAAC;EACjE,IAAI,CAAC,EAAE,IAAI,MAAM,IAAI,MAAM,0BAA0B,EAAE,QAAQ;EAC/D,mBAAmB;GAAE,GAAG;GAAM,OAAO,KAAK;GAAO,OAAO,KAAK;EAAM,CAAC;EACpE,IAAI,MAAM,kBAAkB,EAAE,WAAW,YAAY,EAAE,SAAS,KAAK,GAAG,uBAAuB;EAC/F;CACF;CAOA,IAAI,KAAK,YACP,MAAM,IAAI,MACR,6HACF;CAIF,MAAM,KAAK,gBAAgB;EAAE,OAAO,KAAK;EAAO,QAAQ;CAAI,CAAC;CAC7D,MAAM,OAAO,GAAG,OAAO,cAAc,CAAC;CACtC,IAAI,MAAM,KAAK;CACf,IAAI,QAAQ,KAAK;CACjB,IAAI;EACF,IAAI,CAAC,KAAK,MAAM,MAAM,SAAS,MAAM,uBAAuB,GAAG;EAE/D,KAAK,IAAI,UAAU,IAAK,WAAW;GACjC,IAAI,CAAC,OAAO,QAAQ,MAAM,SAAS,MAAM,oBAAoB,GAAG;GAChE,IAAI,MAAM,yBAAyB,IAAI,IAAI;GAC3C,MAAM,IAAI,MAAM,KAAK,MAAM;IAAO;IAAa;GAAO,CAAC;GACvD,IAAI,EAAE,IAAI;IACR,IAAI,MAAM,kBAAkB,EAAE,WAAW,YAAY,EAAE,SAAS,KAAK,GAAG,GAAG;IAC3E;GACF;GACA,IAAI,MAAM,KAAK,EAAE,OAAO,GAAG;GAC3B,IAAI,WAAW,GAAG,MAAM,IAAI,MAAM,uBAAuB;GACzD,QAAQ,KAAA;EACV;EAEA,mBAAmB;GAAE,GAAG;GAAM,OAAO;GAAM,OAAO;EAAO,CAAC;EAE1D,MAAM,OAAO,MAAM,SAAS,MAAM,wDAAwD,GAAG,EAAA,CAAG,YAAY;EAC5G,IAAI,QAAQ,OAAO,QAAQ,OAAO;GAChC,MAAM,KAAK,eAAe;GAC1B,IAAI,MAAM,wFAAwF;GAClG;EACF;EACA,IAAI,MAAM,gDAAgD;CAC5D,UAAU;EACR,GAAG,MAAM;CACX;CAEA,MAAM,KAAK,cAAc;AAC3B"}
@@ -7034,12 +7034,14 @@ var SessionManager = class {
7034
7034
  const effortOverride = this.deps.store.getEffortOverride(key);
7035
7035
  const memoryIndex = (await this.deps.memory.injectAtSessionStart({ agentId })).trim();
7036
7036
  const memoryAppend = memoryIndex ? `# 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${memoryIndex}` : "";
7037
+ const channelName = this.deps.store.getDisplayNames([msg.channel]).get(msg.channel);
7037
7038
  const agentMeta = [
7038
7039
  "# Agent",
7039
7040
  `- Name: ${agent.name}`,
7040
7041
  `- ID: ${agent.id}`,
7041
7042
  `- Source: ${msg.platform}`,
7042
7043
  `- Channel: ${msg.channel}`,
7044
+ ...channelName ? [`- Channel name: ${channelName}`] : [],
7043
7045
  ...agent.description ? ["", agent.description] : []
7044
7046
  ].join("\n");
7045
7047
  const sessionContext = [agentMeta, memoryAppend].filter(Boolean).join("\n\n");
@@ -197229,12 +197231,12 @@ function neutralizeDelimiters(body) {
197229
197231
  return body.split("\n").map((line) => line.trimStart().startsWith("----- ") ? `\\${line}` : line).join("\n");
197230
197232
  }
197231
197233
  /** Trailing instruction for github fires on a NUMBERED thread (issue/PR): the
197232
- * daemon streams the agent's reply back as a GitHub comment automatically, so
197233
- * the agent must NOT post its own — a manual `gh issue comment` would
197234
- * double-post. Only emitted when the poster will actually run (a thread number
197235
- * exists); push fires have no thread, so they never see this. */
197234
+ * daemon is the sole writer of the reply comment, so the agent must return its
197235
+ * answer rather than mutate comments/reviews through CLI, MCP, connector, or API.
197236
+ * Only emitted when the poster will actually run (a thread number exists); push
197237
+ * fires have no thread, so they never see this. */
197236
197238
  function githubReplyHint(c) {
197237
- 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");
197239
+ 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");
197238
197240
  }
197239
197241
  /** The github-kind turn text: a trusted metadata header + the FENCED excerpt.
197240
197242
  * The title rides the header (relay-sanitized to one capped line) — it is
@@ -197329,16 +197331,202 @@ function buildHookMessage(msg, traceId) {
197329
197331
  //#endregion
197330
197332
  //#region src/github/poster.ts
197331
197333
  const DEFAULT_EDIT_INTERVAL_MS = 2e3;
197334
+ const DEFAULT_FINALIZE_TIMEOUT_MS = 15e3;
197332
197335
  /** GitHub caps comment bodies at 65536 chars — truncate with a marker well below. */
197333
197336
  const MAX_COMMENT_CHARS = 6e4;
197334
197337
  /** Consecutive API failures before the stream gives up for this turn. */
197335
197338
  const MAX_FAILURES = 3;
197339
+ const TRUNCATION_MARKER = "\n\n_(truncated — see the session transcript for the full reply)_";
197340
+ const LEGACY_BOUNDARIES = /* @__PURE__ */ new Set([
197341
+ "agent_thought_chunk",
197342
+ "tool_call",
197343
+ "tool_call_update",
197344
+ "plan"
197345
+ ]);
197346
+ const NO_FINAL_MESSAGE_ID = Symbol("no-final-message-id");
197347
+ const NO_COMMENTARY_MESSAGE_ID = Symbol("no-commentary-message-id");
197348
+ function record(value) {
197349
+ return value !== null && typeof value === "object" ? value : void 0;
197350
+ }
197351
+ /**
197352
+ * Select the one human-facing reply from an ACP turn.
197353
+ *
197354
+ * codex-acp ≥1.1.1 gives us the authoritative `_meta.codex.phase`; older Codex
197355
+ * builds still provide messageId, so the last logical message is the fallback.
197356
+ * Other ACP runtimes may provide neither — for them the last text run after a
197357
+ * thought/tool/plan boundary is the best runtime-agnostic approximation.
197358
+ */
197359
+ var GithubReplyCollector = class {
197360
+ messages = /* @__PURE__ */ new Map();
197361
+ nextOrder = 0;
197362
+ lastCommentarySeen = -1;
197363
+ legacyRuns = [];
197364
+ legacyCurrent;
197365
+ /** Consume one raw ACP update. Returns the complete in-progress final answer, if authoritative. */
197366
+ onUpdate(update) {
197367
+ const u = record(update);
197368
+ if (!u) return void 0;
197369
+ const kind = typeof u.sessionUpdate === "string" ? u.sessionUpdate : void 0;
197370
+ if (kind !== "agent_message_chunk") {
197371
+ if (kind && LEGACY_BOUNDARIES.has(kind)) this.commitLegacyBoundary();
197372
+ return;
197373
+ }
197374
+ const content = record(u.content);
197375
+ if (content?.type !== "text" || typeof content.text !== "string") return void 0;
197376
+ const text = content.text;
197377
+ const seen = this.nextOrder++;
197378
+ const codex = record(record(u._meta)?.codex);
197379
+ const rawPhase = typeof codex?.phase === "string" ? codex.phase : void 0;
197380
+ const phase = rawPhase === "commentary" || rawPhase === "final_answer" ? rawPhase : "unknown";
197381
+ const messageId = typeof u.messageId === "string" && u.messageId ? u.messageId : void 0;
197382
+ if (phase === "commentary" || phase === "final_answer") {
197383
+ if (phase === "commentary") this.lastCommentarySeen = seen;
197384
+ }
197385
+ if (messageId || phase !== "unknown") {
197386
+ const key = messageId ?? (phase === "final_answer" ? NO_FINAL_MESSAGE_ID : NO_COMMENTARY_MESSAGE_ID);
197387
+ let candidate = this.messages.get(key);
197388
+ if (!candidate) {
197389
+ const provisional = this.takeProvisionalText();
197390
+ candidate = {
197391
+ firstSeen: provisional?.firstSeen ?? seen,
197392
+ lastSeen: seen,
197393
+ phase: "unknown",
197394
+ text: provisional?.text ?? ""
197395
+ };
197396
+ }
197397
+ if (phase === "final_answer" || phase === "commentary" && candidate.phase === "unknown") candidate.phase = phase;
197398
+ candidate.text += text;
197399
+ candidate.lastSeen = seen;
197400
+ this.messages.set(key, candidate);
197401
+ } else if (text && isKnownNonReplyText(text)) {
197402
+ this.commitLegacyBoundary();
197403
+ this.legacyRuns.push({
197404
+ firstSeen: seen,
197405
+ lastSeen: seen,
197406
+ phase: "unknown",
197407
+ text
197408
+ });
197409
+ } else if (text) {
197410
+ this.legacyCurrent ??= {
197411
+ firstSeen: seen,
197412
+ lastSeen: seen,
197413
+ phase: "unknown",
197414
+ text: ""
197415
+ };
197416
+ this.legacyCurrent.text += text;
197417
+ this.legacyCurrent.lastSeen = seen;
197418
+ }
197419
+ return this.explicitFinalText();
197420
+ }
197421
+ finalText(completed = true) {
197422
+ const explicit = this.explicitFinalText();
197423
+ if (explicit || !completed) return explicit;
197424
+ const unknown = [
197425
+ ...this.messages.values(),
197426
+ ...this.legacyRuns,
197427
+ ...this.legacyCurrent ? [this.legacyCurrent] : []
197428
+ ].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;
197429
+ return unknown?.trim() ? unknown : void 0;
197430
+ }
197431
+ explicitFinalText() {
197432
+ 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");
197433
+ return text.trim() ? text : void 0;
197434
+ }
197435
+ commitLegacyBoundary() {
197436
+ if (!this.legacyCurrent?.text.trim()) return;
197437
+ this.legacyRuns.push(this.legacyCurrent);
197438
+ this.legacyCurrent = void 0;
197439
+ }
197440
+ /** A phase/messageId can arrive one delta late; claim the immediately preceding
197441
+ * anonymous run so the public reply does not lose its prefix. Known adapter
197442
+ * chrome is deliberately left anonymous and therefore never folded into final. */
197443
+ takeProvisionalText() {
197444
+ const candidate = this.legacyCurrent;
197445
+ if (!candidate || !candidate.text.trim() || isKnownNonReplyText(candidate.text)) return void 0;
197446
+ this.legacyCurrent = void 0;
197447
+ return candidate;
197448
+ }
197449
+ };
197450
+ /** Adapter-generated status text that is neither commentary nor a user-facing
197451
+ * final. These older paths lack messageId/phase, so content is the only signal. */
197452
+ function isKnownNonReplyText(text) {
197453
+ const normalized = text.trim();
197454
+ return /^\*?context compacted\b/i.test(normalized) || /^(?:config )?warning:/i.test(normalized) || /^\*conversation interrupted\*$/i.test(normalized);
197455
+ }
197456
+ function escapeMarkdownText(raw) {
197457
+ return (raw.replace(/\s+/g, " ").trim().slice(0, 200) || "unknown agent").replace(/([\\`*_\[\]~<>])/g, "\\$1");
197458
+ }
197459
+ function markdownUrl(raw) {
197460
+ try {
197461
+ const url = new URL(raw);
197462
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
197463
+ const normalized = url.toString();
197464
+ if (normalized.length > 2048) return void 0;
197465
+ return `<${normalized.replace(/</g, "%3C").replace(/>/g, "%3E")}>`;
197466
+ } catch {
197467
+ return;
197468
+ }
197469
+ }
197470
+ function attributionFooter(attribution) {
197471
+ if (!attribution) return "";
197472
+ const name = escapeMarkdownText(attribution.agentName);
197473
+ const agentUrl = markdownUrl(attribution.agentUrl);
197474
+ const sessionUrl = markdownUrl(attribution.sessionUrl);
197475
+ const parts = [`🛠️ Generated by ${agentUrl ? `[${name}](${agentUrl})` : name}`];
197476
+ if (sessionUrl) parts.push(`[View session](${sessionUrl})`);
197477
+ return `\n\n${parts.join(" · ")}`;
197478
+ }
197479
+ /** Return the fenced-code block left open at the end of `text`, if any. */
197480
+ function unclosedMarkdownFence(text) {
197481
+ let open;
197482
+ for (const rawLine of text.split("\n")) {
197483
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
197484
+ const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
197485
+ if (!match) continue;
197486
+ const run = match[1];
197487
+ const marker = run[0];
197488
+ const rest = match[2];
197489
+ if (!open) {
197490
+ if (marker === "`" && rest.includes("`")) continue;
197491
+ open = {
197492
+ marker,
197493
+ length: run.length
197494
+ };
197495
+ } else if (marker === open.marker && run.length >= open.length && !rest.trim()) open = void 0;
197496
+ }
197497
+ return open;
197498
+ }
197499
+ function fenceCloser(text) {
197500
+ const open = unclosedMarkdownFence(text);
197501
+ if (!open) return "";
197502
+ return `${text.endsWith("\n") ? "" : "\n"}${open.marker.repeat(open.length)}`;
197503
+ }
197504
+ /** UTF-16-safe prefix: never leave a high surrogate separated from its pair. */
197505
+ function safePrefix(text, maxChars) {
197506
+ let end = Math.min(text.length, Math.max(0, maxChars));
197507
+ 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;
197508
+ return text.slice(0, end);
197509
+ }
197510
+ /** Fit a reply prefix into `budget`, closing a fence before marker/footer chrome. */
197511
+ function truncatedMarkdownPrefix(text, budget) {
197512
+ let contentBudget = Math.max(0, budget);
197513
+ for (;;) {
197514
+ const prefix = safePrefix(text, contentBudget);
197515
+ const closer = fenceCloser(prefix);
197516
+ if (prefix.length + closer.length <= budget) return prefix + closer;
197517
+ const nextBudget = Math.max(0, budget - closer.length);
197518
+ if (nextBudget >= contentBudget) return safePrefix(text, budget);
197519
+ contentBudget = nextBudget;
197520
+ }
197521
+ }
197336
197522
  var GithubCommentStream = class {
197337
197523
  deps;
197338
197524
  repo;
197339
197525
  issueNumber;
197340
197526
  buffer = "";
197341
- posted = "";
197527
+ /** Rendered body actually on GitHub. Comparing rendered output avoids no-op
197528
+ * PATCHes once an overlong reply has already filled the truncation budget. */
197529
+ postedBody = null;
197342
197530
  commentId = null;
197343
197531
  inFlight = Promise.resolve();
197344
197532
  busy = false;
@@ -197347,9 +197535,14 @@ var GithubCommentStream = class {
197347
197535
  lastSendAt = 0;
197348
197536
  failures = 0;
197349
197537
  finished = false;
197538
+ abandoned = false;
197539
+ finishPromise;
197540
+ abort = new AbortController();
197350
197541
  sched;
197351
197542
  editIntervalMs;
197352
- constructor(deps, repo, issueNumber) {
197543
+ finalizeTimeoutMs;
197544
+ footer;
197545
+ constructor(deps, repo, issueNumber, attribution) {
197353
197546
  this.deps = deps;
197354
197547
  this.repo = repo;
197355
197548
  this.issueNumber = issueNumber;
@@ -197359,30 +197552,72 @@ var GithubCommentStream = class {
197359
197552
  clearTimeout: (h) => clearTimeout(h)
197360
197553
  };
197361
197554
  this.editIntervalMs = deps.minEditIntervalMs ?? DEFAULT_EDIT_INTERVAL_MS;
197555
+ this.finalizeTimeoutMs = deps.finalizeTimeoutMs ?? DEFAULT_FINALIZE_TIMEOUT_MS;
197556
+ this.footer = attributionFooter(attribution);
197362
197557
  }
197363
197558
  /** Fold one agent-output chunk in; schedules the create/edit as needed. Never throws. */
197364
197559
  append(text) {
197365
- if (this.finished || this.failures >= MAX_FAILURES || !text) return;
197560
+ if (this.finished || this.abandoned || this.failures >= MAX_FAILURES || !text) return;
197366
197561
  this.buffer += text;
197367
197562
  this.schedule();
197368
197563
  }
197564
+ /** Replace the streamed final-answer body with its complete value (messageId-safe). */
197565
+ replace(text) {
197566
+ if (this.finished || this.abandoned || this.failures >= MAX_FAILURES || !text) return;
197567
+ this.buffer = text;
197568
+ this.schedule();
197569
+ }
197369
197570
  /**
197370
197571
  * Turn over: flush whatever is left as the final body. Resolves once the
197371
197572
  * last write settled (or was abandoned); never rejects. Idempotent.
197372
197573
  */
197373
- async finish() {
197374
- if (this.finished) return;
197574
+ finish(finalBody) {
197575
+ if (!this.finishPromise) this.finishPromise = this.finishOnce(finalBody);
197576
+ return this.finishPromise;
197577
+ }
197578
+ async finishOnce(finalBody) {
197579
+ if (finalBody !== void 0) this.buffer = finalBody;
197375
197580
  this.finished = true;
197376
197581
  if (this.timer !== null) {
197377
197582
  this.sched.clearTimeout(this.timer);
197378
197583
  this.timer = null;
197379
197584
  }
197380
- await this.inFlight;
197381
- if (this.buffer !== this.posted && this.buffer.trim() && this.failures < MAX_FAILURES) await this.send();
197382
- await this.inFlight;
197585
+ if (!this.buffer.trim() && !this.busy) return;
197586
+ let deadlineHandle;
197587
+ try {
197588
+ const deadline = new Promise((resolve) => {
197589
+ deadlineHandle = this.sched.setTimeout(() => {
197590
+ this.abandoned = true;
197591
+ this.abort.abort();
197592
+ try {
197593
+ this.deps.log.warn(`github poster: final flush timed out on ${this.repo}#${this.issueNumber}`);
197594
+ } catch {} finally {
197595
+ resolve();
197596
+ }
197597
+ }, this.finalizeTimeoutMs);
197598
+ });
197599
+ const drain = (async () => {
197600
+ await this.inFlight;
197601
+ if (this.abandoned) return;
197602
+ if ((this.commentId !== null || !this.renderedCurrent()) && this.buffer.trim() && this.failures < MAX_FAILURES) {
197603
+ this.inFlight = this.send();
197604
+ await this.inFlight;
197605
+ }
197606
+ })();
197607
+ await Promise.race([drain, deadline]);
197608
+ } catch (err) {
197609
+ try {
197610
+ this.deps.log.warn(`github poster: final flush failed on ${this.repo}#${this.issueNumber} (${String(err)})`);
197611
+ } catch {}
197612
+ } finally {
197613
+ if (deadlineHandle !== void 0) try {
197614
+ this.sched.clearTimeout(deadlineHandle);
197615
+ } catch {}
197616
+ }
197383
197617
  }
197384
197618
  schedule() {
197385
- if (this.busy || this.timer !== null) return;
197619
+ if (this.abandoned || this.busy || this.timer !== null) return;
197620
+ if (this.renderedCurrent()) return;
197386
197621
  if (!this.hasSent) {
197387
197622
  this.inFlight = this.send();
197388
197623
  return;
@@ -197396,32 +197631,41 @@ var GithubCommentStream = class {
197396
197631
  }
197397
197632
  /** One create-or-edit round; merges everything buffered at send time. */
197398
197633
  async send() {
197399
- if (this.busy) return;
197634
+ if (this.abandoned || this.busy) return;
197400
197635
  this.busy = true;
197401
197636
  const snapshot = this.buffer;
197402
- const body = this.render(snapshot);
197403
197637
  try {
197638
+ const body = this.render(snapshot);
197404
197639
  const token = await this.deps.token();
197640
+ if (this.abandoned) return;
197405
197641
  if (this.commentId === null) {
197406
197642
  const created = await this.request("POST", `/repos/${this.repo}/issues/${this.issueNumber}/comments`, token, body);
197407
197643
  this.commentId = created.id;
197408
197644
  this.deps.log.debug(`github poster: comment ${created.id} opened on ${this.repo}#${this.issueNumber}`);
197409
197645
  } else await this.request("PATCH", `/repos/${this.repo}/issues/comments/${this.commentId}`, token, body);
197410
- this.posted = snapshot;
197646
+ this.postedBody = body;
197411
197647
  this.failures = 0;
197412
197648
  } catch (err) {
197649
+ if (this.abandoned) return;
197413
197650
  this.failures += 1;
197414
197651
  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" : ""}`);
197415
197652
  } finally {
197416
197653
  this.hasSent = true;
197417
197654
  this.lastSendAt = this.sched.now();
197418
197655
  this.busy = false;
197419
- if (!this.finished && this.buffer !== this.posted && this.failures < MAX_FAILURES) this.schedule();
197656
+ if (!this.finished && !this.renderedCurrent() && this.failures < MAX_FAILURES) this.schedule();
197420
197657
  }
197421
197658
  }
197422
197659
  render(text) {
197423
- if (text.length <= MAX_COMMENT_CHARS) return text;
197424
- return `${text.slice(0, MAX_COMMENT_CHARS)}\n\n_(truncated — see the session transcript for the full reply)_`;
197660
+ if (text.length + this.footer.length <= MAX_COMMENT_CHARS) {
197661
+ const closer = this.footer ? fenceCloser(text) : "";
197662
+ if (text.length + closer.length + this.footer.length <= MAX_COMMENT_CHARS) return text + closer + this.footer;
197663
+ }
197664
+ const suffix = TRUNCATION_MARKER + this.footer;
197665
+ return `${truncatedMarkdownPrefix(text, Math.max(0, MAX_COMMENT_CHARS - suffix.length))}${suffix}`;
197666
+ }
197667
+ renderedCurrent() {
197668
+ return this.postedBody !== null && this.render(this.buffer) === this.postedBody;
197425
197669
  }
197426
197670
  async request(method, path, token, body) {
197427
197671
  const res = await (this.deps.fetchImpl ?? fetch)(`${this.deps.baseUrl ?? "https://api.github.com"}${path}`, {
@@ -197432,6 +197676,7 @@ var GithubCommentStream = class {
197432
197676
  "content-type": "application/json",
197433
197677
  "x-github-api-version": "2022-11-28"
197434
197678
  },
197679
+ signal: this.abort.signal,
197435
197680
  body: JSON.stringify({ body })
197436
197681
  });
197437
197682
  if (!res.ok) throw new Error(`GitHub ${method} ${res.status}`);
@@ -217134,7 +217379,7 @@ var Daemon = class Daemon {
217134
217379
  index: 0,
217135
217380
  replyText: ""
217136
217381
  } } : {},
217137
- ...githubReply ? { github: { stream: this.makeGithubReplyStream(agentId, githubReply) } } : {}
217382
+ ...githubReply ? { github: this.makeGithubReply(agentId, githubReply, sessionId) } : {}
217138
217383
  };
217139
217384
  this.pending.set(sessionId, p);
217140
217385
  if (callMeta) this.activeTurnCallMeta.set(key, callMeta);
@@ -217213,8 +217458,10 @@ var Daemon = class Daemon {
217213
217458
  this.clearIdle(p);
217214
217459
  this.releasePermissions(sessionId);
217215
217460
  this.releaseElicits(sessionId);
217216
- this.pending.delete(sessionId);
217217
217461
  if (callMeta) this.activeTurnCallMeta.delete(key);
217462
+ this.clearCancelBackstop(sessionId);
217463
+ 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)})`));
217464
+ this.pending.delete(sessionId);
217218
217465
  this.store.setSessionState(key, "idle", this.clock.now());
217219
217466
  this.emitSessionMetadataSnapshot({
217220
217467
  sessionId,
@@ -217224,8 +217471,6 @@ var Daemon = class Daemon {
217224
217471
  channel: msg.channel,
217225
217472
  thread: statusThread
217226
217473
  });
217227
- this.clearCancelBackstop(sessionId);
217228
- if (p.github) p.github.stream.finish().catch((err) => this.log.warn(`github poster: final flush failed (${formatErr(err)})`));
217229
217474
  p.resolveDone();
217230
217475
  }
217231
217476
  return sessionId;
@@ -217511,14 +217756,14 @@ var Daemon = class Daemon {
217511
217756
  * the View-session link is always available. */
217512
217757
  sessionLink(acpSessionId) {
217513
217758
  const orgSeg = this.cpOrgSlug ? `/${encodeURIComponent(this.cpOrgSlug)}` : "";
217514
- return `${this.webAppBase()}${orgSeg}/sessions/${acpSessionId}`;
217759
+ return `${this.webAppBase()}${orgSeg}/sessions/${encodeURIComponent(acpSessionId)}`;
217515
217760
  }
217516
217761
  /** The console deep link to an agent: `<base>/<orgSlug>/agents/<agentId>` — the
217517
217762
  * shared-bot footer's "sent by <agent_name>" link (§10.1). Same org-seg rule as
217518
217763
  * {@link sessionLink}. */
217519
217764
  agentLink(agentId) {
217520
217765
  const orgSeg = this.cpOrgSlug ? `/${encodeURIComponent(this.cpOrgSlug)}` : "";
217521
- return `${this.webAppBase()}${orgSeg}/agents/${agentId}`;
217766
+ return `${this.webAppBase()}${orgSeg}/agents/${encodeURIComponent(agentId)}`;
217522
217767
  }
217523
217768
  /** The shared-bot footer context for a turn, or undefined when this isn't a shared
217524
217769
  * Slack channel (then the classic "sent by agentconnect.md" footer is used). A
@@ -217876,9 +218121,9 @@ var Daemon = class Daemon {
217876
218121
  for (const action of p.conv.onUpdate(update)) this.enqueueApply(p, action);
217877
218122
  this.armIdle(p);
217878
218123
  }
217879
- if (p.github && update?.sessionUpdate === "agent_message_chunk" && update.content?.type === "text") {
217880
- const text = update.content.text ?? "";
217881
- if (text) p.github.stream.append(text);
218124
+ if (p.github) {
218125
+ const finalAnswer = p.github.collector.onUpdate(update);
218126
+ if (finalAnswer) p.github.stream.replace(finalAnswer);
217882
218127
  }
217883
218128
  for (const ev of p.rec.onUpdate(update)) this.recordEvent(p.agentId, p.channel, p.statusThread, ev);
217884
218129
  }
@@ -218565,16 +218810,26 @@ var Daemon = class Daemon {
218565
218810
  }
218566
218811
  return this.dispatch(agentId, msg, target?.integrationId, void 0, void 0, void 0, githubReply);
218567
218812
  }
218568
- /** Build the per-turn GitHub comment stream, tokened via the repo-targeted
218569
- * gitcred mint (issues/PR write, no contents — never enters agent env). */
218570
- makeGithubReplyStream(agentId, ref) {
218571
- return new GithubCommentStream({
218572
- token: async () => (await this.gitCreds.getPostToken(agentId, ref.repo)).token,
218573
- log: {
218574
- warn: (m) => this.log.warn(m),
218575
- debug: (m) => this.log.debug(m)
218576
- }
218577
- }, ref.repo, ref.number);
218813
+ /** Build the per-turn GitHub final-answer selector and comment stream, tokened
218814
+ * via the repo-targeted gitcred mint (issues/PR write, no contents — never
218815
+ * enters agent env). Attribution is fixed at turn creation so every edit has
218816
+ * the same agent/session footer. */
218817
+ makeGithubReply(agentId, ref, sessionId) {
218818
+ const agent = this.agents.get(agentId);
218819
+ return {
218820
+ collector: new GithubReplyCollector(),
218821
+ stream: new GithubCommentStream({
218822
+ token: async () => (await this.gitCreds.getPostToken(agentId, ref.repo)).token,
218823
+ log: {
218824
+ warn: (m) => this.log.warn(m),
218825
+ debug: (m) => this.log.debug(m)
218826
+ }
218827
+ }, ref.repo, ref.number, {
218828
+ agentName: agent?.name ?? agentId,
218829
+ agentUrl: this.agentLink(agentId),
218830
+ sessionUrl: this.sessionLink(sessionId)
218831
+ })
218832
+ };
218578
218833
  }
218579
218834
  /**
218580
218835
  * A cron fired for `agentId` (§8.6 bypass). The fire is stamped into D11
@@ -218862,4 +219117,4 @@ async function runForeground(opts, deps = {}) {
218862
219117
  //#endregion
218863
219118
  export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
218864
219119
 
218865
- //# sourceMappingURL=run-foreground-DWgt3Xvd.js.map
219120
+ //# sourceMappingURL=run-foreground-DasVEmdw.js.map