@harmonyos-arkts/opencode-acp 0.0.3 → 0.0.6

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.cjs CHANGED
@@ -19586,6 +19586,11 @@ function createOpencodeClient(config2) {
19586
19586
  // node_modules/@opencode-ai/sdk/dist/v2/server.js
19587
19587
  var import_cross_spawn = __toESM(require_cross_spawn(), 1);
19588
19588
 
19589
+ // src/index.ts
19590
+ var http = __toESM(require("node:http"), 1);
19591
+ var https = __toESM(require("node:https"), 1);
19592
+ var import_node_stream = require("node:stream");
19593
+
19589
19594
  // src/agent.ts
19590
19595
  var import_url2 = require("url");
19591
19596
 
@@ -19631,9 +19636,11 @@ var SessionManager = class {
19631
19636
  /**
19632
19637
  * Load an existing session into the manager.
19633
19638
  * Called by agent.loadSession() / agent.unstable_resumeSession().
19639
+ * Returns the session state plus the raw Session object from OpenCode.
19634
19640
  */
19635
19641
  async load(sessionId, cwd, mcpServers, model) {
19636
- await this.sdk.session.get({ sessionID: sessionId, directory: cwd }, { throwOnError: true });
19642
+ const response = await this.sdk.session.get({ sessionID: sessionId, directory: cwd }, { throwOnError: true });
19643
+ const session = response.data;
19637
19644
  const state = {
19638
19645
  id: sessionId,
19639
19646
  cwd,
@@ -19642,7 +19649,7 @@ var SessionManager = class {
19642
19649
  model
19643
19650
  };
19644
19651
  this.sessions.set(sessionId, state);
19645
- return state;
19652
+ return { state, session };
19646
19653
  }
19647
19654
  /**
19648
19655
  * Auto-register a session discovered via SSE events.
@@ -20277,65 +20284,6 @@ function applyStructuredPatch(source, patch, options = {}) {
20277
20284
 
20278
20285
  // src/utils.ts
20279
20286
  var import_url = require("url");
20280
- function toToolKind(toolName) {
20281
- const tool = toolName.toLowerCase();
20282
- switch (tool) {
20283
- case "bash":
20284
- return "execute";
20285
- case "webfetch":
20286
- return "fetch";
20287
- case "edit":
20288
- case "patch":
20289
- case "write":
20290
- return "edit";
20291
- case "grep":
20292
- case "glob":
20293
- case "context7_resolve_library_id":
20294
- case "context7_get_library_docs":
20295
- return "search";
20296
- case "list":
20297
- case "read":
20298
- return "read";
20299
- default:
20300
- return "other";
20301
- }
20302
- }
20303
- function toLocations(toolName, input) {
20304
- const tool = toolName.toLowerCase();
20305
- switch (tool) {
20306
- case "read":
20307
- case "edit":
20308
- case "write":
20309
- return input["filePath"] ? [{ path: input["filePath"] }] : [];
20310
- case "glob":
20311
- case "grep":
20312
- return input["path"] ? [{ path: input["path"] }] : [];
20313
- case "list":
20314
- return input["path"] ? [{ path: input["path"] }] : [];
20315
- default:
20316
- return [];
20317
- }
20318
- }
20319
- function parseUri(uri) {
20320
- try {
20321
- if (uri.startsWith("file://")) {
20322
- const path = uri.slice(7);
20323
- const name = path.split("/").pop() || path;
20324
- return { type: "file", url: uri, filename: name, mime: "text/plain" };
20325
- }
20326
- if (uri.startsWith("zed://")) {
20327
- const url2 = new URL(uri);
20328
- const path = url2.searchParams.get("path");
20329
- if (path) {
20330
- const name = path.split("/").pop() || path;
20331
- return { type: "file", url: (0, import_url.pathToFileURL)(path).href, filename: name, mime: "text/plain" };
20332
- }
20333
- }
20334
- return { type: "text", text: uri };
20335
- } catch {
20336
- return { type: "text", text: uri };
20337
- }
20338
- }
20339
20287
 
20340
20288
  // src/logger.ts
20341
20289
  var import_fs = require("fs");
@@ -20401,49 +20349,97 @@ function sysLog(action, data) {
20401
20349
  write("system", action, data);
20402
20350
  }
20403
20351
  function sanitize(obj, depth = 0) {
20404
- if (!obj || depth > 2) return obj;
20352
+ if (!obj) return obj;
20353
+ if (depth > 5) return { __truncated__: true, __depth__: depth };
20405
20354
  const result = {};
20406
20355
  for (const [key, val] of Object.entries(obj)) {
20407
20356
  if (typeof val === "string" && val.length > 2e3) {
20408
20357
  result[key] = val.slice(0, 2e3) + `... [${val.length} chars total]`;
20409
20358
  } else if (val && typeof val === "object" && !Array.isArray(val)) {
20410
20359
  result[key] = sanitize(val, depth + 1);
20360
+ } else if (val && Array.isArray(val)) {
20361
+ result[key] = val.map(
20362
+ (item) => typeof item === "string" && item.length > 2e3 ? item.slice(0, 2e3) + `... [${item.length} chars total]` : item && typeof item === "object" ? sanitize(item, depth + 1) : item
20363
+ );
20411
20364
  } else {
20412
20365
  result[key] = val;
20413
20366
  }
20414
20367
  }
20415
20368
  return result;
20416
20369
  }
20417
- function acpAssembled(action, data) {
20418
- if (!enabled) return;
20419
- const entry = {
20420
- ts: (/* @__PURE__ */ new Date()).toISOString(),
20421
- cat: "acp.out",
20422
- action,
20423
- ...data && { data: sanitizeAssembled(data) }
20424
- };
20425
- try {
20426
- (0, import_fs.appendFileSync)(logFile, JSON.stringify(entry) + "\n");
20427
- } catch {
20370
+
20371
+ // src/utils.ts
20372
+ function toToolKind(toolName) {
20373
+ const tool = toolName.toLowerCase();
20374
+ switch (tool) {
20375
+ case "bash":
20376
+ return "execute";
20377
+ case "webfetch":
20378
+ return "fetch";
20379
+ case "edit":
20380
+ case "patch":
20381
+ case "write":
20382
+ return "edit";
20383
+ case "grep":
20384
+ case "glob":
20385
+ case "context7_resolve_library_id":
20386
+ case "context7_get_library_docs":
20387
+ return "search";
20388
+ case "list":
20389
+ case "read":
20390
+ return "read";
20391
+ default:
20392
+ return "other";
20428
20393
  }
20429
20394
  }
20430
- function sanitizeAssembled(obj, depth = 0) {
20431
- if (!obj || depth > 2) return obj;
20432
- const result = {};
20433
- for (const [key, val] of Object.entries(obj)) {
20434
- if (typeof val === "string" && val.length > 1e4) {
20435
- result[key] = val.slice(0, 1e4) + `... [${val.length} chars total]`;
20436
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
20437
- result[key] = sanitizeAssembled(val, depth + 1);
20438
- } else {
20439
- result[key] = val;
20395
+ function toLocations(toolName, input) {
20396
+ const tool = toolName.toLowerCase();
20397
+ switch (tool) {
20398
+ case "read":
20399
+ case "edit":
20400
+ case "write":
20401
+ return input["filePath"] ? [{ path: input["filePath"] }] : [];
20402
+ case "glob":
20403
+ case "grep":
20404
+ return input["path"] ? [{ path: input["path"] }] : [];
20405
+ case "list":
20406
+ return input["path"] ? [{ path: input["path"] }] : [];
20407
+ default:
20408
+ return [];
20409
+ }
20410
+ }
20411
+ function parseUri(uri) {
20412
+ try {
20413
+ if (uri.startsWith("file://")) {
20414
+ const path = uri.slice(7);
20415
+ const name = path.split("/").pop() || path;
20416
+ return { type: "file", url: uri, filename: name, mime: "text/plain" };
20440
20417
  }
20418
+ if (uri.startsWith("zed://")) {
20419
+ const url2 = new URL(uri);
20420
+ const path = url2.searchParams.get("path");
20421
+ if (path) {
20422
+ const name = path.split("/").pop() || path;
20423
+ return { type: "file", url: (0, import_url.pathToFileURL)(path).href, filename: name, mime: "text/plain" };
20424
+ }
20425
+ }
20426
+ return { type: "text", text: uri };
20427
+ } catch {
20428
+ return { type: "text", text: uri };
20441
20429
  }
20442
- return result;
20430
+ }
20431
+ async function sendToClient(connection, params) {
20432
+ const updateType = params.update.sessionUpdate;
20433
+ const update = params.update;
20434
+ acpOut(`sessionUpdate.${updateType}`, {
20435
+ sessionId: params.sessionId.slice(0, 12),
20436
+ ...updateType === "agent_message_chunk" || updateType === "agent_thought_chunk" ? { messageId: update.messageId?.slice(0, 12), delta: update.content?.text?.slice(0, 200) } : updateType === "tool_call" || updateType === "tool_call_update" ? { toolCallId: update.toolCallId, tool: update.title, kind: update.kind, status: update.status } : updateType === "session_info_update" ? { title: update.title, _meta: update._meta } : updateType === "usage_update" ? { used: update.used, size: update.size, cost: update.cost, _meta: update._meta } : {}
20437
+ });
20438
+ return connection.sessionUpdate(params);
20443
20439
  }
20444
20440
 
20445
20441
  // src/event-handler.ts
20446
- var QUESTION_TIMEOUT_MS = 6e4;
20442
+ var QUESTION_TIMEOUT_MS = 3e5;
20447
20443
  var permissionOptions = [
20448
20444
  { optionId: "once", kind: "allow_once", name: "Allow once" },
20449
20445
  { optionId: "always", kind: "allow_always", name: "Always allow" },
@@ -20459,131 +20455,14 @@ var EventHandler = class {
20459
20455
  permissionQueues = /* @__PURE__ */ new Map();
20460
20456
  questionQueues = /* @__PURE__ */ new Map();
20461
20457
  started = false;
20462
- messageBuffers = /* @__PURE__ */ new Map();
20463
- /** Index: sessionId Set<messageId> for O(1) flush lookup. */
20464
- sessionBufferIndex = /* @__PURE__ */ new Map();
20465
- messageMetaCache = /* @__PURE__ */ new Map();
20458
+ /** partID { type, ignored }, built from message.part.updated SSE events. */
20459
+ partMetaIndex = /* @__PURE__ */ new Map();
20466
20460
  /** Track tool call counts per child session to detect completion. */
20467
20461
  childToolCounts = /* @__PURE__ */ new Map();
20468
- /**
20469
- * Wrapper around connection.sessionUpdate that logs what's sent to the ACP client.
20470
- * This is the single point where we record all outgoing traffic.
20471
- */
20472
- async sendToClient(params) {
20473
- const updateType = params.update.sessionUpdate;
20474
- if (updateType !== "agent_message_chunk" && updateType !== "agent_thought_chunk") {
20475
- this.flushMessageBuffer(params.sessionId, `boundary:${updateType}`);
20476
- acpOut(`sessionUpdate.${updateType}`, {
20477
- sessionId: params.sessionId.slice(0, 12),
20478
- ...updateType === "tool_call" || updateType === "tool_call_update" ? {
20479
- toolCallId: params.update.toolCallId,
20480
- tool: params.update.title,
20481
- kind: params.update.kind,
20482
- status: params.update.status
20483
- } : updateType === "session_info_update" ? {
20484
- title: params.update.title,
20485
- _meta: params.update._meta
20486
- } : {}
20487
- });
20488
- }
20489
- return this.connection.sessionUpdate(params);
20490
- }
20491
- /**
20492
- * Accumulate a text/thought delta into the message buffer.
20493
- * The buffer is flushed as a single log entry when a boundary event occurs
20494
- * (e.g. tool_call starts, new message, session change).
20495
- */
20496
- accumulateDelta(messageId, sessionId, kind, delta) {
20497
- let buf = this.messageBuffers.get(messageId);
20498
- if (!buf) {
20499
- buf = { sessionId, text: "", thought: "" };
20500
- this.messageBuffers.set(messageId, buf);
20501
- const msgSet = this.sessionBufferIndex.get(sessionId);
20502
- if (msgSet) {
20503
- msgSet.add(messageId);
20504
- } else {
20505
- this.sessionBufferIndex.set(sessionId, /* @__PURE__ */ new Set([messageId]));
20506
- }
20507
- }
20508
- buf[kind] += delta;
20509
- }
20510
- /**
20511
- * Flush accumulated message content for a given session as assembled log entries.
20512
- * Called when a boundary event (tool_call, session change, etc.) signals the
20513
- * text/thought phase is complete.
20514
- */
20515
- flushMessageBuffer(sessionId, reason) {
20516
- const msgIds = this.sessionBufferIndex.get(sessionId);
20517
- if (!msgIds) return;
20518
- for (const messageId of msgIds) {
20519
- const buf = this.messageBuffers.get(messageId);
20520
- if (!buf) continue;
20521
- if (!buf.text && !buf.thought) {
20522
- this.messageBuffers.delete(messageId);
20523
- continue;
20524
- }
20525
- if (buf.text) {
20526
- acpAssembled("sessionUpdate.agent_message_complete", {
20527
- sessionId: sessionId.slice(0, 12),
20528
- messageId: messageId.slice(0, 12),
20529
- length: buf.text.length,
20530
- text: buf.text,
20531
- flushReason: reason
20532
- });
20533
- }
20534
- if (buf.thought) {
20535
- acpAssembled("sessionUpdate.agent_thought_complete", {
20536
- sessionId: sessionId.slice(0, 12),
20537
- messageId: messageId.slice(0, 12),
20538
- length: buf.thought.length,
20539
- text: buf.thought,
20540
- flushReason: reason
20541
- });
20542
- }
20543
- this.messageBuffers.delete(messageId);
20544
- }
20545
- this.sessionBufferIndex.delete(sessionId);
20546
- }
20547
- /** Flush all remaining buffers (e.g. on stop) */
20548
- flushAllBuffers() {
20549
- for (const sessionId of this.sessionBufferIndex.keys()) {
20550
- this.flushMessageBuffer(sessionId, "stop");
20551
- }
20552
- }
20553
- /**
20554
- * Get or cache message metadata (role + parts) for a message.
20555
- * Fetches from SDK only on first delta for a message, then serves from cache.
20556
- */
20557
- async getOrCacheMessageMeta(sessionID, messageID, cwd) {
20558
- const cached2 = this.messageMetaCache.get(messageID);
20559
- if (cached2) return cached2;
20560
- const message = await this.sdk.session.message({ sessionID, messageID, directory: cwd }, { throwOnError: true }).then((x) => x.data).catch(() => void 0);
20561
- if (!message) return void 0;
20562
- const parts = /* @__PURE__ */ new Map();
20563
- for (const p of message.parts) {
20564
- parts.set(p.id, { type: p.type, ignored: p.ignored });
20565
- }
20566
- const meta3 = { role: message.info.role, parts };
20567
- this.messageMetaCache.set(messageID, meta3);
20568
- if (this.messageMetaCache.size > 50) {
20569
- const oldest = this.messageMetaCache.keys().next().value;
20570
- if (oldest) this.messageMetaCache.delete(oldest);
20571
- }
20572
- return meta3;
20573
- }
20574
- /**
20575
- * Flush previous message buffer for a session when messageId changes.
20576
- */
20577
- flushPreviousBuffer(sessionId, currentMessageId) {
20578
- const msgIds = this.sessionBufferIndex.get(sessionId);
20579
- if (!msgIds) return;
20580
- for (const messageId of msgIds) {
20581
- if (messageId !== currentMessageId) {
20582
- this.flushMessageBuffer(sessionId, "new_message");
20583
- return;
20584
- }
20585
- }
20586
- }
20462
+ /** Cumulative file diff stats per session, aggregated from session.diff SSE events. */
20463
+ fileDiffStats = /* @__PURE__ */ new Map();
20464
+ /** AI code change stats per session, keyed by sessionId filePath → { additions, deletions }. */
20465
+ aiCodeChangeStats = /* @__PURE__ */ new Map();
20587
20466
  constructor(deps) {
20588
20467
  this.connection = deps.connection;
20589
20468
  this.sdk = deps.sdk;
@@ -20598,7 +20477,6 @@ var EventHandler = class {
20598
20477
  });
20599
20478
  }
20600
20479
  stop() {
20601
- this.flushAllBuffers();
20602
20480
  this.abort.abort();
20603
20481
  }
20604
20482
  async runSubscription() {
@@ -20630,9 +20508,13 @@ var EventHandler = class {
20630
20508
  }
20631
20509
  // ─── Event Handling ──────────────────────────────────────────────
20632
20510
  async handleEvent(event) {
20511
+ if (event.type === "server.heartbeat") {
20512
+ return;
20513
+ }
20633
20514
  switch (event.type) {
20634
20515
  case "session.created":
20635
20516
  case "session.updated": {
20517
+ ocEvent(event.type, event);
20636
20518
  const props = event.properties;
20637
20519
  const info = props.info ?? props;
20638
20520
  const parentID = info.parentID;
@@ -20642,7 +20524,6 @@ var EventHandler = class {
20642
20524
  const isNew = !this.sessionManager.tryGet(id);
20643
20525
  this.sessionManager.registerDiscovered(id, parentID, title);
20644
20526
  if (isNew) {
20645
- ocEvent("session.created.child", { id, parentID, title });
20646
20527
  const agentMatch = title?.match(/@(\w+)\s+subagent/);
20647
20528
  const agentType = agentMatch?.[1];
20648
20529
  const description = title?.replace(/\s*\(@\w+\s+subagent\)\s*$/, "") ?? title;
@@ -20654,6 +20535,7 @@ var EventHandler = class {
20654
20535
  return;
20655
20536
  }
20656
20537
  case "permission.asked": {
20538
+ ocEvent("permission.asked", event);
20657
20539
  const permission = event.properties;
20658
20540
  const resolved = this.resolveSession(permission.sessionID);
20659
20541
  if (!resolved) return;
@@ -20723,15 +20605,26 @@ var EventHandler = class {
20723
20605
  }
20724
20606
  case "question.asked": {
20725
20607
  const q = event.properties;
20726
- const resolved = this.resolveSession(q.sessionID);
20727
- if (!resolved) return;
20608
+ ocEvent("question.asked", event);
20609
+ let resolved = this.resolveSession(q.sessionID);
20610
+ if (!resolved) {
20611
+ console.warn(`[event-handler] question.asked: session ${q.sessionID} not in manager, attempting auto-register`);
20612
+ try {
20613
+ const sessionResp = await this.sdk.session.get({ sessionID: q.sessionID, directory: "" });
20614
+ const fetchedSession = sessionResp.data;
20615
+ if (fetchedSession?.parentID) {
20616
+ this.sessionManager.registerDiscovered(fetchedSession.id, fetchedSession.parentID, fetchedSession.title);
20617
+ resolved = this.resolveSession(q.sessionID);
20618
+ } else if (fetchedSession) {
20619
+ console.warn(`[event-handler] question.asked: top-level session ${q.sessionID} not registered; was agent.newSession called?`);
20620
+ }
20621
+ } catch (err) {
20622
+ console.error(`[event-handler] question.asked: failed to fetch session ${q.sessionID}:`, err);
20623
+ }
20624
+ if (!resolved) return;
20625
+ }
20728
20626
  const directory = resolved.cwd;
20729
20627
  const sessionId = resolved.sessionId;
20730
- ocEvent("question.asked", {
20731
- id: q.id,
20732
- sessionID: q.sessionID,
20733
- questions: q.questions.length
20734
- });
20735
20628
  const prev = this.questionQueues.get(q.sessionID) ?? Promise.resolve();
20736
20629
  const next = prev.then(async () => {
20737
20630
  const extResult = await Promise.race([
@@ -20751,7 +20644,7 @@ var EventHandler = class {
20751
20644
  if (!extResult || !extResult.answers) {
20752
20645
  await this.sdk.question.reject({ requestID: q.id, directory }).catch(() => {
20753
20646
  });
20754
- ocEvent("question.rejected", { requestID: q.id });
20647
+ sysLog("question.rejected", { requestID: q.id });
20755
20648
  return;
20756
20649
  }
20757
20650
  const answers = extResult.answers;
@@ -20759,7 +20652,7 @@ var EventHandler = class {
20759
20652
  console.error("[event-handler] invalid answers shape from client:", JSON.stringify(answers));
20760
20653
  await this.sdk.question.reject({ requestID: q.id, directory }).catch(() => {
20761
20654
  });
20762
- ocEvent("question.rejected", {
20655
+ sysLog("question.rejected", {
20763
20656
  requestID: q.id,
20764
20657
  reason: "invalid_answers"
20765
20658
  });
@@ -20770,7 +20663,7 @@ var EventHandler = class {
20770
20663
  directory,
20771
20664
  answers
20772
20665
  });
20773
- ocEvent("question.reply", { requestID: q.id, answers });
20666
+ sysLog("question.reply", { requestID: q.id, answers });
20774
20667
  }).catch((err) => {
20775
20668
  console.error("[event-handler] question handling error:", err);
20776
20669
  }).finally(() => {
@@ -20782,28 +20675,40 @@ var EventHandler = class {
20782
20675
  return;
20783
20676
  }
20784
20677
  case "message.part.updated": {
20678
+ ocEvent("message.part.updated", event);
20785
20679
  const props = event.properties;
20786
20680
  const part = props.part;
20681
+ if (part.id) {
20682
+ this.partMetaIndex.set(part.id, { type: part.type, ignored: part.ignored });
20683
+ }
20787
20684
  const resolved = this.resolveSession(part.sessionID);
20788
20685
  if (!resolved) return;
20789
20686
  if (part.type === "tool") {
20790
20687
  await this.handleToolPart(resolved.sessionId, part);
20791
20688
  }
20689
+ if (part.type === "text" && typeof part.text === "string" && part.ignored === true) {
20690
+ await sendToClient(this.connection, {
20691
+ sessionId: resolved.sessionId,
20692
+ update: {
20693
+ sessionUpdate: "agent_message_chunk",
20694
+ messageId: part.messageID,
20695
+ content: { type: "text", text: part.text }
20696
+ }
20697
+ }).catch(() => {
20698
+ });
20699
+ }
20792
20700
  return;
20793
20701
  }
20794
20702
  case "message.part.delta": {
20703
+ ocEvent("message.part.delta", event);
20795
20704
  const props = event.properties;
20796
20705
  const resolved = this.resolveSession(props.sessionID);
20797
20706
  if (!resolved) return;
20798
20707
  const sessionId = resolved.sessionId;
20799
- this.flushPreviousBuffer(sessionId, props.messageID);
20800
- const meta3 = await this.getOrCacheMessageMeta(props.sessionID, props.messageID, resolved.cwd);
20801
- if (!meta3 || meta3.role !== "assistant") return;
20802
- const partMeta = meta3.parts.get(props.partID);
20708
+ const partMeta = this.partMetaIndex.get(props.partID);
20803
20709
  if (!partMeta) return;
20804
20710
  if (partMeta.type === "text" && props.field === "text" && partMeta.ignored !== true) {
20805
- this.accumulateDelta(props.messageID, sessionId, "text", props.delta);
20806
- await this.sendToClient({
20711
+ await sendToClient(this.connection, {
20807
20712
  sessionId,
20808
20713
  update: {
20809
20714
  sessionUpdate: "agent_message_chunk",
@@ -20815,8 +20720,7 @@ var EventHandler = class {
20815
20720
  return;
20816
20721
  }
20817
20722
  if (partMeta.type === "reasoning" && props.field === "text") {
20818
- this.accumulateDelta(props.messageID, sessionId, "thought", props.delta);
20819
- await this.sendToClient({
20723
+ await sendToClient(this.connection, {
20820
20724
  sessionId,
20821
20725
  update: {
20822
20726
  sessionUpdate: "agent_thought_chunk",
@@ -20828,6 +20732,25 @@ var EventHandler = class {
20828
20732
  }
20829
20733
  return;
20830
20734
  }
20735
+ case "session.diff": {
20736
+ ocEvent("session.diff", event);
20737
+ const { sessionID, diff } = event.properties;
20738
+ const resolved = this.resolveSession(sessionID);
20739
+ if (!resolved) {
20740
+ return;
20741
+ }
20742
+ let additions = 0;
20743
+ let deletions = 0;
20744
+ for (const d of diff) {
20745
+ additions += d.additions;
20746
+ deletions += d.deletions;
20747
+ }
20748
+ this.fileDiffStats.set(resolved.sessionId, { additions, deletions, files: diff.length });
20749
+ return;
20750
+ }
20751
+ default: {
20752
+ return;
20753
+ }
20831
20754
  }
20832
20755
  }
20833
20756
  // ─── Child Session Announcement ──────────────────────────────────
@@ -20838,7 +20761,7 @@ var EventHandler = class {
20838
20761
  * and _meta containing structured metadata about the subagent.
20839
20762
  */
20840
20763
  async announceChildSession(childSessionId, parentSessionId, title, meta3) {
20841
- await this.sendToClient({
20764
+ await sendToClient(this.connection, {
20842
20765
  sessionId: childSessionId,
20843
20766
  update: {
20844
20767
  sessionUpdate: "session_info_update",
@@ -20871,7 +20794,7 @@ var EventHandler = class {
20871
20794
  });
20872
20795
  }
20873
20796
  }
20874
- await this.sendToClient({
20797
+ await sendToClient(this.connection, {
20875
20798
  sessionId,
20876
20799
  update: {
20877
20800
  sessionUpdate: "tool_call",
@@ -20903,7 +20826,7 @@ var EventHandler = class {
20903
20826
  content: { type: "text", text: output }
20904
20827
  });
20905
20828
  }
20906
- await this.sendToClient({
20829
+ await sendToClient(this.connection, {
20907
20830
  sessionId,
20908
20831
  update: {
20909
20832
  sessionUpdate: "tool_call_update",
@@ -20956,12 +20879,13 @@ var EventHandler = class {
20956
20879
  const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "";
20957
20880
  const newText = typeof input["newString"] === "string" ? input["newString"] : typeof input["content"] === "string" ? input["content"] : "";
20958
20881
  content.push({ type: "diff", path: filePath, oldText, newText });
20882
+ this.accumulateAICodeChangeStats(sessionId, part);
20959
20883
  }
20960
20884
  if (part.tool === "todowrite") {
20961
20885
  try {
20962
20886
  const todos = JSON.parse(part.state.output);
20963
20887
  if (Array.isArray(todos)) {
20964
- await this.sendToClient({
20888
+ await sendToClient(this.connection, {
20965
20889
  sessionId,
20966
20890
  update: {
20967
20891
  sessionUpdate: "plan",
@@ -20977,7 +20901,7 @@ var EventHandler = class {
20977
20901
  } catch {
20978
20902
  }
20979
20903
  }
20980
- await this.sendToClient({
20904
+ await sendToClient(this.connection, {
20981
20905
  sessionId,
20982
20906
  update: {
20983
20907
  sessionUpdate: "tool_call_update",
@@ -20999,7 +20923,7 @@ var EventHandler = class {
20999
20923
  case "error": {
21000
20924
  this.toolStarts.delete(part.callID);
21001
20925
  this.bashSnapshots.delete(part.callID);
21002
- await this.sendToClient({
20926
+ await sendToClient(this.connection, {
21003
20927
  sessionId,
21004
20928
  update: {
21005
20929
  sessionUpdate: "tool_call_update",
@@ -21034,7 +20958,7 @@ var EventHandler = class {
21034
20958
  const agentMatch = title.match(/@(\w+)\s+subagent/);
21035
20959
  const agentType = agentMatch?.[1];
21036
20960
  const description = title.replace(/\s*\(@\w+\s+subagent\)\s*$/, "");
21037
- await this.sendToClient({
20961
+ await sendToClient(this.connection, {
21038
20962
  sessionId: childSessionId,
21039
20963
  update: {
21040
20964
  sessionUpdate: "session_info_update",
@@ -21058,6 +20982,44 @@ var EventHandler = class {
21058
20982
  if (typeof output !== "string") return;
21059
20983
  return output;
21060
20984
  }
20985
+ // ─── Statistics Accessors ────────────────────────────────────────
20986
+ getFileDiffStats(sessionId) {
20987
+ return this.fileDiffStats.get(sessionId);
20988
+ }
20989
+ setFileDiffStats(sessionId, stats) {
20990
+ if (this.fileDiffStats.has(sessionId)) return;
20991
+ this.fileDiffStats.set(sessionId, stats);
20992
+ }
20993
+ /**
20994
+ * Accumulate AI code change stats from a completed tool part's metadata.
20995
+ * The harmony-code plugin injects `metadata.aiCodeChange` for edit/write tools.
20996
+ */
20997
+ accumulateAICodeChangeStats(sessionId, part) {
20998
+ if (part.state.status !== "completed") return;
20999
+ const meta3 = part.state.metadata ?? {};
21000
+ const aiCodeChange = meta3["aiCodeChange"];
21001
+ if (!aiCodeChange || aiCodeChange.additions === 0 && aiCodeChange.deletions === 0) return;
21002
+ const fileMap = this.aiCodeChangeStats.get(sessionId) ?? /* @__PURE__ */ new Map();
21003
+ const existing = fileMap.get(aiCodeChange.file) ?? { additions: 0, deletions: 0 };
21004
+ fileMap.set(aiCodeChange.file, {
21005
+ additions: existing.additions + aiCodeChange.additions,
21006
+ deletions: existing.deletions + aiCodeChange.deletions
21007
+ });
21008
+ if (!this.aiCodeChangeStats.has(sessionId)) {
21009
+ this.aiCodeChangeStats.set(sessionId, fileMap);
21010
+ }
21011
+ }
21012
+ getAICodeChangeStats(sessionId) {
21013
+ const fileMap = this.aiCodeChangeStats.get(sessionId);
21014
+ if (!fileMap || fileMap.size === 0) return void 0;
21015
+ let additions = 0;
21016
+ let deletions = 0;
21017
+ for (const stats of fileMap.values()) {
21018
+ additions += stats.additions;
21019
+ deletions += stats.deletions;
21020
+ }
21021
+ return { additions, deletions, files: fileMap.size };
21022
+ }
21061
21023
  };
21062
21024
  function simpleHash(str) {
21063
21025
  let hash2 = 0;
@@ -21068,6 +21030,49 @@ function simpleHash(str) {
21068
21030
  return hash2.toString(36);
21069
21031
  }
21070
21032
 
21033
+ // src/mcp-manager.ts
21034
+ var THROW = { throwOnError: true };
21035
+ var McpManager = class {
21036
+ constructor(sdk) {
21037
+ this.sdk = sdk;
21038
+ }
21039
+ async status(directory) {
21040
+ ocCall("mcp.status", { directory });
21041
+ const result = await this.sdk.mcp.status({ directory });
21042
+ return result.data ?? {};
21043
+ }
21044
+ async add(directory, name, config2) {
21045
+ ocCall("mcp.add", { directory, name });
21046
+ const result = await this.sdk.mcp.add({ directory, name, config: config2 }, THROW);
21047
+ return result.data ?? {};
21048
+ }
21049
+ async connect(directory, name) {
21050
+ ocCall("mcp.connect", { directory, name });
21051
+ await this.sdk.mcp.connect({ name, directory }, THROW);
21052
+ return { success: true };
21053
+ }
21054
+ async disconnect(directory, name) {
21055
+ ocCall("mcp.disconnect", { directory, name });
21056
+ await this.sdk.mcp.disconnect({ name, directory }, THROW);
21057
+ return { success: true };
21058
+ }
21059
+ async startAuth(directory, name) {
21060
+ ocCall("mcp.auth.start", { directory, name });
21061
+ const result = await this.sdk.mcp.auth.start({ name, directory }, THROW);
21062
+ return result.data ?? {};
21063
+ }
21064
+ async callbackAuth(directory, name, code) {
21065
+ ocCall("mcp.auth.callback", { directory, name });
21066
+ const result = await this.sdk.mcp.auth.callback({ name, directory, code }, THROW);
21067
+ return result.data ?? {};
21068
+ }
21069
+ async removeAuth(directory, name) {
21070
+ ocCall("mcp.auth.remove", { directory, name });
21071
+ await this.sdk.mcp.auth.remove({ name, directory }, THROW);
21072
+ return { success: true };
21073
+ }
21074
+ };
21075
+
21071
21076
  // src/auth-provider.ts
21072
21077
  var AuthProviderManager = class {
21073
21078
  sdk;
@@ -21117,29 +21122,13 @@ var Agent = class {
21117
21122
  sessionManager;
21118
21123
  eventHandler;
21119
21124
  authProvider;
21120
- /**
21121
- * Wrapper around connection.sessionUpdate that logs what's sent to the ACP client.
21122
- */
21123
- async sendToClient(params) {
21124
- const updateType = params.update.sessionUpdate;
21125
- if (updateType !== "agent_message_chunk" && updateType !== "agent_thought_chunk") {
21126
- acpOut(`sessionUpdate.${updateType}`, {
21127
- sessionId: params.sessionId.slice(0, 12),
21128
- ...updateType === "tool_call" || updateType === "tool_call_update" ? {
21129
- toolCallId: params.update.toolCallId,
21130
- tool: params.update.title,
21131
- kind: params.update.kind,
21132
- status: params.update.status
21133
- } : {}
21134
- });
21135
- }
21136
- return this.connection.sessionUpdate(params);
21137
- }
21125
+ mcpManager;
21138
21126
  constructor(config2) {
21139
21127
  this.config = config2;
21140
21128
  this.sdk = config2.sdk;
21141
21129
  this.sessionManager = new SessionManager(config2.sdk);
21142
21130
  this.authProvider = new AuthProviderManager(config2.sdk);
21131
+ this.mcpManager = new McpManager(config2.sdk);
21143
21132
  }
21144
21133
  init(connection) {
21145
21134
  this.connection = connection;
@@ -21189,10 +21178,13 @@ var Agent = class {
21189
21178
  // ─── Extension Methods ────────────────────────────────────────────
21190
21179
  async extMethod(method, params) {
21191
21180
  acpIn("extMethod", { method });
21192
- if (!method.startsWith("provider/")) {
21193
- throw new Error(`Unknown extMethod: ${method}`);
21181
+ if (method.startsWith("provider/")) {
21182
+ return this.handleProviderMethod(method, params);
21194
21183
  }
21195
- return this.handleProviderMethod(method, params);
21184
+ if (method.startsWith("mcp/")) {
21185
+ return this.handleMcpMethod(method, params);
21186
+ }
21187
+ throw new Error(`Unknown extMethod: ${method}`);
21196
21188
  }
21197
21189
  async handleProviderMethod(method, params) {
21198
21190
  const sessionId = params.sessionId;
@@ -21248,6 +21240,67 @@ var Agent = class {
21248
21240
  throw new Error(`Unknown provider extMethod: ${method}`);
21249
21241
  }
21250
21242
  }
21243
+ async handleMcpMethod(method, params) {
21244
+ const sessionId = params.sessionId;
21245
+ if (!sessionId) throw new Error("sessionId required");
21246
+ const directory = this.sessionManager.get(sessionId)?.cwd;
21247
+ if (!directory) throw new Error("Session not found");
21248
+ switch (method) {
21249
+ case "mcp/status": {
21250
+ const data = await this.mcpManager.status(directory);
21251
+ acpOut("extMethod.response", { method: "mcp/status" });
21252
+ return data;
21253
+ }
21254
+ case "mcp/add": {
21255
+ const name = params.name;
21256
+ const config2 = params.config;
21257
+ if (!name) throw new Error("Missing required parameter: name");
21258
+ if (!config2) throw new Error("Missing required parameter: config");
21259
+ const data = await this.mcpManager.add(directory, name, config2);
21260
+ acpOut("extMethod.response", { method: "mcp/add", name });
21261
+ return data;
21262
+ }
21263
+ case "mcp/connect": {
21264
+ const name = params.name;
21265
+ if (!name) throw new Error("Missing required parameter: name");
21266
+ const data = await this.mcpManager.connect(directory, name);
21267
+ acpOut("extMethod.response", { method: "mcp/connect", name });
21268
+ return data;
21269
+ }
21270
+ case "mcp/disconnect": {
21271
+ const name = params.name;
21272
+ if (!name) throw new Error("Missing required parameter: name");
21273
+ const data = await this.mcpManager.disconnect(directory, name);
21274
+ acpOut("extMethod.response", { method: "mcp/disconnect", name });
21275
+ return data;
21276
+ }
21277
+ case "mcp/auth/start": {
21278
+ const name = params.name;
21279
+ if (!name) throw new Error("Missing required parameter: name");
21280
+ const data = await this.mcpManager.startAuth(directory, name);
21281
+ acpOut("extMethod.response", { method: "mcp/auth/start", name });
21282
+ return data;
21283
+ }
21284
+ case "mcp/auth/callback": {
21285
+ const name = params.name;
21286
+ const code = params.code;
21287
+ if (!name) throw new Error("Missing required parameter: name");
21288
+ if (!code) throw new Error("Missing required parameter: code");
21289
+ const data = await this.mcpManager.callbackAuth(directory, name, code);
21290
+ acpOut("extMethod.response", { method: "mcp/auth/callback", name });
21291
+ return data;
21292
+ }
21293
+ case "mcp/auth/remove": {
21294
+ const name = params.name;
21295
+ if (!name) throw new Error("Missing required parameter: name");
21296
+ const data = await this.mcpManager.removeAuth(directory, name);
21297
+ acpOut("extMethod.response", { method: "mcp/auth/remove", name });
21298
+ return data;
21299
+ }
21300
+ default:
21301
+ throw new Error(`Unknown MCP method: ${method}`);
21302
+ }
21303
+ }
21251
21304
  async buildProviderMeta() {
21252
21305
  try {
21253
21306
  const data = await this.authProvider.listProviders(this.config.cwd);
@@ -21290,11 +21343,19 @@ var Agent = class {
21290
21343
  }
21291
21344
  }
21292
21345
  async loadSession(params) {
21346
+ acpIn("loadSession", { sessionId: params.sessionId, cwd: params.cwd });
21293
21347
  try {
21294
21348
  const directory = params.cwd;
21295
21349
  const sessionId = params.sessionId;
21296
21350
  const model = await this.defaultModel(directory);
21297
- await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model);
21351
+ const { session } = await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model);
21352
+ if (session.summary) {
21353
+ this.eventHandler.setFileDiffStats(sessionId, {
21354
+ additions: session.summary.additions,
21355
+ deletions: session.summary.deletions,
21356
+ files: session.summary.files
21357
+ });
21358
+ }
21298
21359
  const result = await this.loadSessionMode({
21299
21360
  cwd: directory,
21300
21361
  mcpServers: params.mcpServers,
@@ -21330,6 +21391,7 @@ var Agent = class {
21330
21391
  }
21331
21392
  }
21332
21393
  async listSessions(params) {
21394
+ acpIn("listSessions", { cwd: params.cwd, cursor: params.cursor });
21333
21395
  const limit = 100;
21334
21396
  const cursor = params.cursor ? Number(params.cursor) : void 0;
21335
21397
  const sessions = await this.sdk.session.list({ directory: params.cwd ?? void 0, roots: true }).then((x) => x.data ?? []);
@@ -21346,9 +21408,11 @@ var Agent = class {
21346
21408
  const next = filtered.length > limit && last ? String(last.time.updated) : void 0;
21347
21409
  const response = { sessions: entries };
21348
21410
  if (next) response.nextCursor = next;
21411
+ acpOut("listSessions.response", { count: entries.length, hasNext: !!next });
21349
21412
  return response;
21350
21413
  }
21351
21414
  async unstable_forkSession(params) {
21415
+ acpIn("forkSession", { sessionId: params.sessionId, cwd: params.cwd });
21352
21416
  try {
21353
21417
  const directory = params.cwd;
21354
21418
  const mcpServers = params.mcpServers ?? [];
@@ -21374,12 +21438,20 @@ var Agent = class {
21374
21438
  }
21375
21439
  }
21376
21440
  async unstable_resumeSession(params) {
21441
+ acpIn("resumeSession", { sessionId: params.sessionId, cwd: params.cwd });
21377
21442
  try {
21378
21443
  const directory = params.cwd;
21379
21444
  const sessionId = params.sessionId;
21380
21445
  const mcpServers = params.mcpServers ?? [];
21381
21446
  const model = await this.defaultModel(directory);
21382
- await this.sessionManager.load(sessionId, directory, mcpServers, model);
21447
+ const { session } = await this.sessionManager.load(sessionId, directory, mcpServers, model);
21448
+ if (session.summary) {
21449
+ this.eventHandler.setFileDiffStats(sessionId, {
21450
+ additions: session.summary.additions,
21451
+ deletions: session.summary.deletions,
21452
+ files: session.summary.files
21453
+ });
21454
+ }
21383
21455
  const result = await this.loadSessionMode({
21384
21456
  cwd: directory,
21385
21457
  mcpServers,
@@ -21419,6 +21491,16 @@ var Agent = class {
21419
21491
  agent,
21420
21492
  directory
21421
21493
  });
21494
+ const rawErr = response2.error;
21495
+ ocCall("session.prompt.raw", {
21496
+ hasData: !!response2.data,
21497
+ hasError: !!rawErr,
21498
+ errorName: rawErr?.name,
21499
+ errorMessage: rawErr?.message,
21500
+ errorCode: rawErr?.code ?? rawErr?.cause?.code,
21501
+ errorCauseName: rawErr?.cause?.name,
21502
+ errorCauseMessage: rawErr?.cause?.message
21503
+ });
21422
21504
  const msg2 = response2.data?.info;
21423
21505
  ocCall("session.prompt.response", {
21424
21506
  messageId: msg2?.id,
@@ -21505,6 +21587,7 @@ var Agent = class {
21505
21587
  }
21506
21588
  // ─── Configuration ────────────────────────────────────────────────
21507
21589
  async setSessionMode(params) {
21590
+ acpIn("setSessionMode", { sessionId: params.sessionId, modeId: params.modeId });
21508
21591
  const session = this.sessionManager.get(params.sessionId);
21509
21592
  const agents = await this.sdk.app.agents({ directory: session.cwd }).then((x) => x.data ?? []).catch(() => []);
21510
21593
  const availableModes = agents.filter((a) => a.mode !== "subagent" && !a.hidden);
@@ -21514,6 +21597,7 @@ var Agent = class {
21514
21597
  this.sessionManager.setMode(params.sessionId, params.modeId);
21515
21598
  }
21516
21599
  async unstable_setSessionModel(params) {
21600
+ acpIn("setSessionModel", { sessionId: params.sessionId, modelId: params.modelId });
21517
21601
  const session = this.sessionManager.get(params.sessionId);
21518
21602
  const providers = await this.sdk.config.providers({ directory: session.cwd }).then((x) => x.data?.providers ?? []).catch(() => []);
21519
21603
  const selection = parseModelSelection(params.modelId, providers);
@@ -21527,6 +21611,7 @@ var Agent = class {
21527
21611
  };
21528
21612
  }
21529
21613
  async setSessionConfigOption(params) {
21614
+ acpIn("setSessionConfigOption", { sessionId: params.sessionId, configId: params.configId, value: params.value });
21530
21615
  const session = this.sessionManager.get(params.sessionId);
21531
21616
  const providers = await this.sdk.config.providers({ directory: session.cwd }).then((x) => x.data?.providers ?? []).catch(() => []);
21532
21617
  const sortedProviders = [...providers].sort(
@@ -21612,7 +21697,7 @@ var Agent = class {
21612
21697
  if (!childMessages?.length) return;
21613
21698
  const title = `Subagent (${childSessionId.slice(0, 8)})`;
21614
21699
  this.sessionManager.registerDiscovered(childSessionId, parentSessionId, title);
21615
- await this.sendToClient({
21700
+ await sendToClient(this.connection, {
21616
21701
  sessionId: childSessionId,
21617
21702
  update: {
21618
21703
  sessionUpdate: "session_info_update",
@@ -21716,7 +21801,7 @@ var Agent = class {
21716
21801
  {}
21717
21802
  )
21718
21803
  };
21719
- await this.sdk.mcp.add({ directory: params.cwd, name: server.name, config: config2 }).catch(() => {
21804
+ await this.mcpManager.add(params.cwd, server.name, config2).catch(() => {
21720
21805
  });
21721
21806
  })
21722
21807
  );
@@ -21729,7 +21814,7 @@ var Agent = class {
21729
21814
  availableCommands.push({ name: "compact", description: "compact the session" });
21730
21815
  }
21731
21816
  setTimeout(() => {
21732
- this.sendToClient({
21817
+ sendToClient(this.connection, {
21733
21818
  sessionId: params.sessionId,
21734
21819
  update: {
21735
21820
  sessionUpdate: "available_commands_update",
@@ -21782,7 +21867,7 @@ var Agent = class {
21782
21867
  await this.replayToolPart(sessionId, part);
21783
21868
  } else if (part.type === "text" && part.text) {
21784
21869
  const audience = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : void 0;
21785
- await this.sendToClient({
21870
+ await sendToClient(this.connection, {
21786
21871
  sessionId,
21787
21872
  update: {
21788
21873
  sessionUpdate: messageChunk,
@@ -21800,7 +21885,7 @@ var Agent = class {
21800
21885
  const filename = part.filename ?? "file";
21801
21886
  const mime = part.mime || "application/octet-stream";
21802
21887
  if (url2.startsWith("file://")) {
21803
- await this.sendToClient({
21888
+ await sendToClient(this.connection, {
21804
21889
  sessionId,
21805
21890
  update: {
21806
21891
  sessionUpdate: messageChunk,
@@ -21815,7 +21900,7 @@ var Agent = class {
21815
21900
  const base64Data = base64Match?.[2] ?? "";
21816
21901
  const effectiveMime = dataMime || mime;
21817
21902
  if (effectiveMime.startsWith("image/")) {
21818
- await this.sendToClient({
21903
+ await sendToClient(this.connection, {
21819
21904
  sessionId,
21820
21905
  update: {
21821
21906
  sessionUpdate: messageChunk,
@@ -21837,7 +21922,7 @@ var Agent = class {
21837
21922
  mimeType: effectiveMime,
21838
21923
  text: Buffer.from(base64Data, "base64").toString("utf-8")
21839
21924
  } : { uri: fileUri, mimeType: effectiveMime, blob: base64Data };
21840
- await this.sendToClient({
21925
+ await sendToClient(this.connection, {
21841
21926
  sessionId,
21842
21927
  update: {
21843
21928
  sessionUpdate: messageChunk,
@@ -21849,7 +21934,7 @@ var Agent = class {
21849
21934
  }
21850
21935
  }
21851
21936
  } else if (part.type === "reasoning" && part.text) {
21852
- await this.sendToClient({
21937
+ await sendToClient(this.connection, {
21853
21938
  sessionId,
21854
21939
  update: {
21855
21940
  sessionUpdate: "agent_thought_chunk",
@@ -21862,7 +21947,7 @@ var Agent = class {
21862
21947
  }
21863
21948
  }
21864
21949
  async replayToolPart(sessionId, part) {
21865
- await this.sendToClient({
21950
+ await sendToClient(this.connection, {
21866
21951
  sessionId,
21867
21952
  update: {
21868
21953
  sessionUpdate: "tool_call",
@@ -21887,12 +21972,13 @@ var Agent = class {
21887
21972
  oldText: typeof input["oldString"] === "string" ? input["oldString"] : "",
21888
21973
  newText: typeof input["newString"] === "string" ? input["newString"] : typeof input["content"] === "string" ? input["content"] : ""
21889
21974
  });
21975
+ this.eventHandler.accumulateAICodeChangeStats(sessionId, part);
21890
21976
  }
21891
21977
  if (part.tool === "todowrite") {
21892
21978
  try {
21893
21979
  const todos = JSON.parse(part.state.output);
21894
21980
  if (Array.isArray(todos)) {
21895
- await this.sendToClient({
21981
+ await sendToClient(this.connection, {
21896
21982
  sessionId,
21897
21983
  update: {
21898
21984
  sessionUpdate: "plan",
@@ -21908,7 +21994,7 @@ var Agent = class {
21908
21994
  } catch {
21909
21995
  }
21910
21996
  }
21911
- await this.sendToClient({
21997
+ await sendToClient(this.connection, {
21912
21998
  sessionId,
21913
21999
  update: {
21914
22000
  sessionUpdate: "tool_call_update",
@@ -21925,7 +22011,7 @@ var Agent = class {
21925
22011
  break;
21926
22012
  }
21927
22013
  case "error":
21928
- await this.sendToClient({
22014
+ await sendToClient(this.connection, {
21929
22015
  sessionId,
21930
22016
  update: {
21931
22017
  sessionUpdate: "tool_call_update",
@@ -21965,24 +22051,76 @@ var Agent = class {
21965
22051
  const provider = providers.find((p) => p.id === msg.providerID);
21966
22052
  const model = provider?.models[msg.modelID];
21967
22053
  const size = model?.limit?.context ?? 0;
21968
- await this.sendToClient({
22054
+ const _meta = {};
22055
+ if (childCost > 0) {
22056
+ _meta.parentCost = totalCost;
22057
+ _meta.childCost = childCost;
22058
+ _meta.childSessionCount = children.length;
22059
+ }
22060
+ this.aggregateFileDiffStats(sessionId, children, _meta);
22061
+ this.aggregateAICodeChangeStats(sessionId, children, _meta);
22062
+ await sendToClient(this.connection, {
21969
22063
  sessionId,
21970
22064
  update: {
21971
22065
  sessionUpdate: "usage_update",
21972
22066
  used: msg.tokens.input + (msg.tokens.cache?.read ?? 0),
21973
22067
  size,
21974
22068
  cost: { amount: totalCost + childCost, currency: "USD" },
21975
- ...childCost > 0 && {
21976
- _meta: {
21977
- parentCost: totalCost,
21978
- childCost,
21979
- childSessionCount: children.length
21980
- }
21981
- }
22069
+ ...Object.keys(_meta).length > 0 && { _meta }
21982
22070
  }
21983
22071
  }).catch(() => {
21984
22072
  });
21985
22073
  }
22074
+ /**
22075
+ * Aggregate file diff stats (git-based) for parent + child sessions into _meta.
22076
+ * 主 session 的 session.diff 已包含子任务的变更数据,直接使用,不累加子 session。
22077
+ */
22078
+ aggregateFileDiffStats(sessionId, children, _meta) {
22079
+ const stats = this.eventHandler.getFileDiffStats(sessionId);
22080
+ let childFileDiffStats;
22081
+ for (const childId of children) {
22082
+ const cs = this.eventHandler.getFileDiffStats(childId);
22083
+ if (cs) {
22084
+ childFileDiffStats = childFileDiffStats ? {
22085
+ additions: childFileDiffStats.additions + cs.additions,
22086
+ deletions: childFileDiffStats.deletions + cs.deletions,
22087
+ files: childFileDiffStats.files + cs.files
22088
+ } : { ...cs };
22089
+ }
22090
+ }
22091
+ const totalFileDiffStats = stats ?? childFileDiffStats;
22092
+ if (totalFileDiffStats) {
22093
+ _meta.fileDiffStats = totalFileDiffStats;
22094
+ }
22095
+ if (childFileDiffStats) {
22096
+ _meta.childFileDiffStats = childFileDiffStats;
22097
+ }
22098
+ }
22099
+ /**
22100
+ * Aggregate AI code change stats (tool-based) for parent + child sessions into _meta.
22101
+ */
22102
+ aggregateAICodeChangeStats(sessionId, children, _meta) {
22103
+ const parentStats = this.eventHandler.getAICodeChangeStats(sessionId);
22104
+ let childAICodeChangeStats;
22105
+ for (const childId of children) {
22106
+ const cs = this.eventHandler.getAICodeChangeStats(childId);
22107
+ if (cs) {
22108
+ childAICodeChangeStats = childAICodeChangeStats ? {
22109
+ additions: childAICodeChangeStats.additions + cs.additions,
22110
+ deletions: childAICodeChangeStats.deletions + cs.deletions,
22111
+ files: childAICodeChangeStats.files + cs.files
22112
+ } : { ...cs };
22113
+ }
22114
+ }
22115
+ const totalAICodeChangeStats = parentStats ? childAICodeChangeStats ? {
22116
+ additions: parentStats.additions + childAICodeChangeStats.additions,
22117
+ deletions: parentStats.deletions + childAICodeChangeStats.deletions,
22118
+ files: parentStats.files + childAICodeChangeStats.files
22119
+ } : parentStats : childAICodeChangeStats;
22120
+ if (totalAICodeChangeStats) {
22121
+ _meta.aiCodeChange = totalAICodeChangeStats;
22122
+ }
22123
+ }
21986
22124
  convertPromptParts(parts) {
21987
22125
  const result = [];
21988
22126
  for (const part of parts) {
@@ -22045,33 +22183,13 @@ var Agent = class {
22045
22183
  return { name, args: rest.join(" ").trim() };
22046
22184
  }
22047
22185
  /**
22048
- * Fetch the complete message content after prompt returns and log it.
22049
- * This ensures the response text is always captured in logs, even if
22050
- * SSE delta events arrive out of sync or are missed.
22186
+ * Fetch the complete message content after prompt returns and log tool usage.
22051
22187
  */
22052
22188
  async logMessageContent(sessionId, messageId, directory) {
22053
22189
  if (!messageId) return;
22054
22190
  const message = await this.sdk.session.message({ sessionID: sessionId, messageID: messageId, directory }).then((x) => x.data).catch(() => void 0);
22055
22191
  if (!message) return;
22056
22192
  for (const part of message.parts) {
22057
- if (part.type === "text" && part.text) {
22058
- acpAssembled("sessionUpdate.agent_message_complete", {
22059
- sessionId: sessionId.slice(0, 12),
22060
- messageId: messageId.slice(0, 12),
22061
- length: part.text.length,
22062
- text: part.text,
22063
- flushReason: "prompt_complete"
22064
- });
22065
- }
22066
- if (part.type === "reasoning" && part.text) {
22067
- acpAssembled("sessionUpdate.agent_thought_complete", {
22068
- sessionId: sessionId.slice(0, 12),
22069
- messageId: messageId.slice(0, 12),
22070
- length: part.text.length,
22071
- text: part.text,
22072
- flushReason: "prompt_complete"
22073
- });
22074
- }
22075
22193
  if (part.type === "tool") {
22076
22194
  ocCall("session.prompt.tool", {
22077
22195
  sessionId: sessionId.slice(0, 12),
@@ -22128,6 +22246,91 @@ function parseModelSelection(modelId, providers) {
22128
22246
  }
22129
22247
 
22130
22248
  // src/index.ts
22249
+ var nativeFetch = (input, init) => {
22250
+ return new Promise((resolve, reject) => {
22251
+ let url2;
22252
+ let requestMethod;
22253
+ let requestHeaders;
22254
+ let requestBody = null;
22255
+ if (typeof input === "string") {
22256
+ url2 = new URL(input);
22257
+ } else if (input instanceof URL) {
22258
+ url2 = input;
22259
+ } else {
22260
+ url2 = new URL(input.url);
22261
+ requestMethod = input.method;
22262
+ requestHeaders = input.headers;
22263
+ requestBody = input.body;
22264
+ }
22265
+ const method = (init?.method ?? requestMethod ?? "GET").toUpperCase();
22266
+ const mergedHeaders = new Headers(init?.headers ?? requestHeaders ?? void 0);
22267
+ const headerObj = {};
22268
+ mergedHeaders.forEach((value, key) => {
22269
+ headerObj[key] = value;
22270
+ });
22271
+ const body = init?.body !== void 0 ? init.body : requestBody;
22272
+ const signal = init?.signal;
22273
+ if (signal?.aborted) {
22274
+ return reject(
22275
+ signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError")
22276
+ );
22277
+ }
22278
+ const mod = url2.protocol === "https:" ? https : http;
22279
+ const req = mod.request(
22280
+ url2,
22281
+ {
22282
+ method,
22283
+ headers: headerObj
22284
+ // Note: do NOT set `timeout` here. Leaving it unset means
22285
+ // node:http will not apply any inactivity-based abort.
22286
+ },
22287
+ (res) => {
22288
+ const respHeaders = new Headers();
22289
+ for (const [k, v] of Object.entries(res.headers)) {
22290
+ if (v == null) continue;
22291
+ if (Array.isArray(v)) {
22292
+ for (const item of v) respHeaders.append(k, item);
22293
+ } else {
22294
+ respHeaders.set(k, String(v));
22295
+ }
22296
+ }
22297
+ const webStream = import_node_stream.Readable.toWeb(res);
22298
+ resolve(
22299
+ new Response(webStream, {
22300
+ status: res.statusCode ?? 200,
22301
+ statusText: res.statusMessage ?? "",
22302
+ headers: respHeaders
22303
+ })
22304
+ );
22305
+ }
22306
+ );
22307
+ req.on("error", reject);
22308
+ if (signal) {
22309
+ signal.addEventListener(
22310
+ "abort",
22311
+ () => {
22312
+ req.destroy(
22313
+ signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError")
22314
+ );
22315
+ },
22316
+ { once: true }
22317
+ );
22318
+ }
22319
+ if (body == null) {
22320
+ req.end();
22321
+ } else if (typeof body === "string") {
22322
+ req.end(body);
22323
+ } else if (body instanceof Uint8Array || Buffer.isBuffer(body)) {
22324
+ req.end(body);
22325
+ } else if (body instanceof URLSearchParams) {
22326
+ req.end(body.toString());
22327
+ } else if (typeof body.pipeTo === "function") {
22328
+ import_node_stream.Readable.fromWeb(body).pipe(req);
22329
+ } else {
22330
+ req.end(String(body));
22331
+ }
22332
+ });
22333
+ };
22131
22334
  function parseArgs(args) {
22132
22335
  let server = "http://localhost:4096";
22133
22336
  let cwd = process.cwd();
@@ -22167,7 +22370,10 @@ async function main() {
22167
22370
  initLogger();
22168
22371
  if (log) setLogEnabled(true);
22169
22372
  sysLog("starting", { server, cwd });
22170
- const sdk = createOpencodeClient({ baseUrl: server });
22373
+ const sdk = createOpencodeClient({
22374
+ baseUrl: server,
22375
+ fetch: nativeFetch
22376
+ });
22171
22377
  try {
22172
22378
  await sdk.global.health();
22173
22379
  sysLog("server_connected");