@harmonyos-arkts/opencode-acp 0.0.4 → 0.0.7

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
 
@@ -19699,6 +19704,14 @@ var SessionManager = class {
19699
19704
  getChildren(parentId) {
19700
19705
  return [...this.children.get(parentId) ?? []];
19701
19706
  }
19707
+ /** All registered session IDs (parent + discovered children). */
19708
+ listSessionIds() {
19709
+ return [...this.sessions.keys()];
19710
+ }
19711
+ /** Top-level sessions only (no parentID) — used for global error broadcast. */
19712
+ listTopLevelSessionIds() {
19713
+ return [...this.sessions.values()].filter((s) => !s.parentID).map((s) => s.id);
19714
+ }
19702
19715
  // Model and mode management
19703
19716
  getModel(sessionId) {
19704
19717
  return this.get(sessionId).model;
@@ -20279,65 +20292,6 @@ function applyStructuredPatch(source, patch, options = {}) {
20279
20292
 
20280
20293
  // src/utils.ts
20281
20294
  var import_url = require("url");
20282
- function toToolKind(toolName) {
20283
- const tool = toolName.toLowerCase();
20284
- switch (tool) {
20285
- case "bash":
20286
- return "execute";
20287
- case "webfetch":
20288
- return "fetch";
20289
- case "edit":
20290
- case "patch":
20291
- case "write":
20292
- return "edit";
20293
- case "grep":
20294
- case "glob":
20295
- case "context7_resolve_library_id":
20296
- case "context7_get_library_docs":
20297
- return "search";
20298
- case "list":
20299
- case "read":
20300
- return "read";
20301
- default:
20302
- return "other";
20303
- }
20304
- }
20305
- function toLocations(toolName, input) {
20306
- const tool = toolName.toLowerCase();
20307
- switch (tool) {
20308
- case "read":
20309
- case "edit":
20310
- case "write":
20311
- return input["filePath"] ? [{ path: input["filePath"] }] : [];
20312
- case "glob":
20313
- case "grep":
20314
- return input["path"] ? [{ path: input["path"] }] : [];
20315
- case "list":
20316
- return input["path"] ? [{ path: input["path"] }] : [];
20317
- default:
20318
- return [];
20319
- }
20320
- }
20321
- function parseUri(uri) {
20322
- try {
20323
- if (uri.startsWith("file://")) {
20324
- const path = uri.slice(7);
20325
- const name = path.split("/").pop() || path;
20326
- return { type: "file", url: uri, filename: name, mime: "text/plain" };
20327
- }
20328
- if (uri.startsWith("zed://")) {
20329
- const url2 = new URL(uri);
20330
- const path = url2.searchParams.get("path");
20331
- if (path) {
20332
- const name = path.split("/").pop() || path;
20333
- return { type: "file", url: (0, import_url.pathToFileURL)(path).href, filename: name, mime: "text/plain" };
20334
- }
20335
- }
20336
- return { type: "text", text: uri };
20337
- } catch {
20338
- return { type: "text", text: uri };
20339
- }
20340
- }
20341
20295
 
20342
20296
  // src/logger.ts
20343
20297
  var import_fs = require("fs");
@@ -20403,13 +20357,18 @@ function sysLog(action, data) {
20403
20357
  write("system", action, data);
20404
20358
  }
20405
20359
  function sanitize(obj, depth = 0) {
20406
- if (!obj || depth > 5) return obj;
20360
+ if (!obj) return obj;
20361
+ if (depth > 5) return { __truncated__: true, __depth__: depth };
20407
20362
  const result = {};
20408
20363
  for (const [key, val] of Object.entries(obj)) {
20409
20364
  if (typeof val === "string" && val.length > 2e3) {
20410
20365
  result[key] = val.slice(0, 2e3) + `... [${val.length} chars total]`;
20411
20366
  } else if (val && typeof val === "object" && !Array.isArray(val)) {
20412
20367
  result[key] = sanitize(val, depth + 1);
20368
+ } else if (val && Array.isArray(val)) {
20369
+ result[key] = val.map(
20370
+ (item) => typeof item === "string" && item.length > 2e3 ? item.slice(0, 2e3) + `... [${item.length} chars total]` : item && typeof item === "object" ? sanitize(item, depth + 1) : item
20371
+ );
20413
20372
  } else {
20414
20373
  result[key] = val;
20415
20374
  }
@@ -20417,8 +20376,171 @@ function sanitize(obj, depth = 0) {
20417
20376
  return result;
20418
20377
  }
20419
20378
 
20379
+ // src/utils.ts
20380
+ function toToolKind(toolName) {
20381
+ const tool = toolName.toLowerCase();
20382
+ switch (tool) {
20383
+ case "bash":
20384
+ return "execute";
20385
+ case "webfetch":
20386
+ return "fetch";
20387
+ case "edit":
20388
+ case "patch":
20389
+ case "write":
20390
+ return "edit";
20391
+ case "grep":
20392
+ case "glob":
20393
+ case "context7_resolve_library_id":
20394
+ case "context7_get_library_docs":
20395
+ return "search";
20396
+ case "list":
20397
+ case "read":
20398
+ return "read";
20399
+ default:
20400
+ return "other";
20401
+ }
20402
+ }
20403
+ function toLocations(toolName, input) {
20404
+ const tool = toolName.toLowerCase();
20405
+ switch (tool) {
20406
+ case "read":
20407
+ case "edit":
20408
+ case "write":
20409
+ return input["filePath"] ? [{ path: input["filePath"] }] : [];
20410
+ case "glob":
20411
+ case "grep":
20412
+ return input["path"] ? [{ path: input["path"] }] : [];
20413
+ case "list":
20414
+ return input["path"] ? [{ path: input["path"] }] : [];
20415
+ default:
20416
+ return [];
20417
+ }
20418
+ }
20419
+ function parseUri(uri) {
20420
+ try {
20421
+ if (uri.startsWith("file://")) {
20422
+ const path = uri.slice(7);
20423
+ const name = path.split("/").pop() || path;
20424
+ return { type: "file", url: uri, filename: name, mime: "text/plain" };
20425
+ }
20426
+ if (uri.startsWith("zed://")) {
20427
+ const url2 = new URL(uri);
20428
+ const path = url2.searchParams.get("path");
20429
+ if (path) {
20430
+ const name = path.split("/").pop() || path;
20431
+ return { type: "file", url: (0, import_url.pathToFileURL)(path).href, filename: name, mime: "text/plain" };
20432
+ }
20433
+ }
20434
+ return { type: "text", text: uri };
20435
+ } catch {
20436
+ return { type: "text", text: uri };
20437
+ }
20438
+ }
20439
+ async function sendToClient(connection, params) {
20440
+ const updateType = params.update.sessionUpdate;
20441
+ const update = params.update;
20442
+ acpOut(`sessionUpdate.${updateType}`, {
20443
+ sessionId: params.sessionId.slice(0, 12),
20444
+ ...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" ? {
20445
+ used: update.used,
20446
+ size: update.size,
20447
+ cost: update.cost,
20448
+ _meta: update._meta,
20449
+ error: update._meta?.error?.message
20450
+ } : update._meta?.error ? { error: update._meta.error?.message ?? update._meta.error } : {}
20451
+ });
20452
+ return connection.sessionUpdate(params);
20453
+ }
20454
+
20455
+ // src/acp-error.ts
20456
+ function inferCode(err) {
20457
+ const name = typeof err.name === "string" ? err.name : "";
20458
+ if (name === "AI_LoadAPIKeyError" || /auth/i.test(name)) return "auth_required";
20459
+ if (/provider|api|model/i.test(name)) return "provider_error";
20460
+ return "turn_error";
20461
+ }
20462
+ function extractTurnError(raw) {
20463
+ return normalizeError(raw.responseError) ?? normalizeError(raw.messageError);
20464
+ }
20465
+ function normalizeError(err) {
20466
+ if (err == null || err === false) return void 0;
20467
+ if (typeof err === "string" && err.trim()) {
20468
+ return { code: "turn_error", message: err.trim(), display: "inline" };
20469
+ }
20470
+ if (err instanceof Error && err.message.trim()) {
20471
+ return {
20472
+ code: inferCode({ name: err.name }),
20473
+ message: err.message.trim(),
20474
+ name: err.name,
20475
+ cause: err.cause instanceof Error ? err.cause.message : void 0,
20476
+ display: "inline"
20477
+ };
20478
+ }
20479
+ if (typeof err === "object") {
20480
+ const o = err;
20481
+ const message = typeof o.message === "string" && o.message.trim() ? o.message.trim() : typeof o.error === "string" && o.error.trim() ? o.error.trim() : void 0;
20482
+ if (!message) return void 0;
20483
+ const cause = o.cause instanceof Error ? o.cause.message : typeof o.cause === "object" && o.cause !== null && typeof o.cause.message === "string" ? o.cause.message : void 0;
20484
+ return {
20485
+ code: inferCode(o),
20486
+ message,
20487
+ name: typeof o.name === "string" ? o.name : void 0,
20488
+ cause,
20489
+ display: "inline",
20490
+ details: o
20491
+ };
20492
+ }
20493
+ return void 0;
20494
+ }
20495
+ function subscriptionErrorFrom(err) {
20496
+ const normalized = normalizeError(err);
20497
+ if (normalized) {
20498
+ return { ...normalized, code: "subscription_error", display: "banner" };
20499
+ }
20500
+ const message = err instanceof Error ? err.message : "OpenCode event stream disconnected";
20501
+ return {
20502
+ code: "subscription_error",
20503
+ message: message.trim() || "OpenCode event stream disconnected",
20504
+ display: "banner"
20505
+ };
20506
+ }
20507
+ function stopReasonForTurn(error48, finish) {
20508
+ if (!error48) return "end_turn";
20509
+ if (finish === "refusal" || finish === "content_filter") return "refusal";
20510
+ if (/refus/i.test(error48.message) || error48.code === "auth_required") return "refusal";
20511
+ return "end_turn";
20512
+ }
20513
+ async function emitSessionError(connection, sessionId, payload, options) {
20514
+ const display = payload.display ?? "inline";
20515
+ const messageId = options?.messageId ?? payload.messageId ?? `err-${Date.now()}`;
20516
+ if (display !== "silent") {
20517
+ await sendToClient(connection, {
20518
+ sessionId,
20519
+ update: {
20520
+ sessionUpdate: "agent_message_chunk",
20521
+ messageId,
20522
+ content: { type: "text", text: payload.message },
20523
+ _meta: { error: payload }
20524
+ }
20525
+ }).catch(() => {
20526
+ });
20527
+ }
20528
+ if (display === "banner" || display === "inline") {
20529
+ await sendToClient(connection, {
20530
+ sessionId,
20531
+ update: {
20532
+ sessionUpdate: "session_info_update",
20533
+ _meta: { error: payload }
20534
+ }
20535
+ }).catch(() => {
20536
+ });
20537
+ }
20538
+ }
20539
+
20420
20540
  // src/event-handler.ts
20421
- var QUESTION_TIMEOUT_MS = 6e4;
20541
+ var SUBSCRIPTION_RETRY_MS = 3e3;
20542
+ var SUBSCRIPTION_ERROR_BROADCAST_MS = 3e4;
20543
+ var QUESTION_TIMEOUT_MS = 3e5;
20422
20544
  var permissionOptions = [
20423
20545
  { optionId: "once", kind: "allow_once", name: "Allow once" },
20424
20546
  { optionId: "always", kind: "allow_always", name: "Always allow" },
@@ -20438,60 +20560,69 @@ var EventHandler = class {
20438
20560
  partMetaIndex = /* @__PURE__ */ new Map();
20439
20561
  /** Track tool call counts per child session to detect completion. */
20440
20562
  childToolCounts = /* @__PURE__ */ new Map();
20441
- /** Cumulative code change stats per session, aggregated from session.diff SSE events. */
20442
- diffStats = /* @__PURE__ */ new Map();
20443
- /**
20444
- * Wrapper around connection.sessionUpdate that logs ALL outgoing traffic to the ACP client.
20445
- * Every call here corresponds to a real message sent over the wire.
20446
- */
20447
- async sendToClient(params) {
20448
- const updateType = params.update.sessionUpdate;
20449
- const update = params.update;
20450
- acpOut(`sessionUpdate.${updateType}`, {
20451
- sessionId: params.sessionId.slice(0, 12),
20452
- ...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 } : {}
20453
- });
20454
- return this.connection.sessionUpdate(params);
20455
- }
20456
- getDiffStats(sessionId) {
20457
- return this.diffStats.get(sessionId);
20458
- }
20459
- setDiffStats(sessionId, stats) {
20460
- if (this.diffStats.has(sessionId)) return;
20461
- this.diffStats.set(sessionId, stats);
20462
- }
20563
+ /** Cumulative file diff stats per session, aggregated from session.diff SSE events. */
20564
+ fileDiffStats = /* @__PURE__ */ new Map();
20565
+ /** AI code change stats per session, keyed by sessionId → filePath → { additions, deletions }. */
20566
+ aiCodeChangeStats = /* @__PURE__ */ new Map();
20567
+ lastSubscriptionErrorBroadcastAt = 0;
20568
+ turnTimeoutTracker;
20463
20569
  constructor(deps) {
20464
20570
  this.connection = deps.connection;
20465
20571
  this.sdk = deps.sdk;
20466
20572
  this.sessionManager = deps.sessionManager;
20573
+ this.turnTimeoutTracker = deps.turnTimeoutTracker;
20467
20574
  }
20468
20575
  start() {
20469
20576
  if (this.started) return;
20470
20577
  this.started = true;
20471
- this.runSubscription().catch((err) => {
20472
- if (this.abort.signal.aborted) return;
20473
- console.error("[event-handler] subscription failed:", err);
20474
- });
20578
+ this.turnTimeoutTracker?.start();
20579
+ void this.runSubscription();
20475
20580
  }
20476
20581
  stop() {
20477
20582
  this.abort.abort();
20583
+ this.turnTimeoutTracker?.stop();
20478
20584
  }
20479
20585
  async runSubscription() {
20480
20586
  while (true) {
20481
20587
  if (this.abort.signal.aborted) return;
20482
- const events = await this.sdk.global.event({
20483
- signal: this.abort.signal
20484
- });
20485
- for await (const event of events.stream) {
20486
- if (this.abort.signal.aborted) return;
20487
- const payload = event?.payload;
20488
- if (!payload) continue;
20489
- await this.handleEvent(payload).catch((err) => {
20490
- console.error("[event-handler] failed to handle event:", err);
20588
+ try {
20589
+ const events = await this.sdk.global.event({
20590
+ signal: this.abort.signal
20491
20591
  });
20592
+ for await (const event of events.stream) {
20593
+ if (this.abort.signal.aborted) return;
20594
+ const payload = event?.payload;
20595
+ if (!payload) continue;
20596
+ await this.handleEvent(payload).catch((err) => {
20597
+ console.error("[event-handler] failed to handle event:", err);
20598
+ });
20599
+ }
20600
+ } catch (err) {
20601
+ if (this.abort.signal.aborted) return;
20602
+ console.error("[event-handler] subscription failed:", err);
20603
+ await this.broadcastSubscriptionError(err);
20604
+ await new Promise((r) => setTimeout(r, SUBSCRIPTION_RETRY_MS));
20492
20605
  }
20493
20606
  }
20494
20607
  }
20608
+ /** Notify all top-level sessions that the OpenCode SSE stream dropped. */
20609
+ async broadcastSubscriptionError(err) {
20610
+ const now = Date.now();
20611
+ if (now - this.lastSubscriptionErrorBroadcastAt < SUBSCRIPTION_ERROR_BROADCAST_MS) {
20612
+ return;
20613
+ }
20614
+ this.lastSubscriptionErrorBroadcastAt = now;
20615
+ const payload = subscriptionErrorFrom(err);
20616
+ sysLog("subscription.error", { message: payload.message });
20617
+ const sessionIds = this.sessionManager.listTopLevelSessionIds();
20618
+ if (sessionIds.length === 0) {
20619
+ return;
20620
+ }
20621
+ for (const sessionId of sessionIds) {
20622
+ await emitSessionError(this.connection, sessionId, payload).catch(() => {
20623
+ });
20624
+ }
20625
+ }
20495
20626
  // ─── Resolve session for routing ────────────────────────────────
20496
20627
  /**
20497
20628
  * Resolve the sessionId to use for ACP routing.
@@ -20508,6 +20639,7 @@ var EventHandler = class {
20508
20639
  if (event.type === "server.heartbeat") {
20509
20640
  return;
20510
20641
  }
20642
+ this.turnTimeoutTracker?.noteEvent(event);
20511
20643
  switch (event.type) {
20512
20644
  case "session.created":
20513
20645
  case "session.updated": {
@@ -20602,15 +20734,26 @@ var EventHandler = class {
20602
20734
  }
20603
20735
  case "question.asked": {
20604
20736
  const q = event.properties;
20605
- const resolved = this.resolveSession(q.sessionID);
20606
- if (!resolved) return;
20737
+ ocEvent("question.asked", event);
20738
+ let resolved = this.resolveSession(q.sessionID);
20739
+ if (!resolved) {
20740
+ console.warn(`[event-handler] question.asked: session ${q.sessionID} not in manager, attempting auto-register`);
20741
+ try {
20742
+ const sessionResp = await this.sdk.session.get({ sessionID: q.sessionID, directory: "" });
20743
+ const fetchedSession = sessionResp.data;
20744
+ if (fetchedSession?.parentID) {
20745
+ this.sessionManager.registerDiscovered(fetchedSession.id, fetchedSession.parentID, fetchedSession.title);
20746
+ resolved = this.resolveSession(q.sessionID);
20747
+ } else if (fetchedSession) {
20748
+ console.warn(`[event-handler] question.asked: top-level session ${q.sessionID} not registered; was agent.newSession called?`);
20749
+ }
20750
+ } catch (err) {
20751
+ console.error(`[event-handler] question.asked: failed to fetch session ${q.sessionID}:`, err);
20752
+ }
20753
+ if (!resolved) return;
20754
+ }
20607
20755
  const directory = resolved.cwd;
20608
20756
  const sessionId = resolved.sessionId;
20609
- ocEvent("question.asked", {
20610
- id: q.id,
20611
- sessionID: q.sessionID,
20612
- questions: q.questions.length
20613
- });
20614
20757
  const prev = this.questionQueues.get(q.sessionID) ?? Promise.resolve();
20615
20758
  const next = prev.then(async () => {
20616
20759
  const extResult = await Promise.race([
@@ -20630,7 +20773,7 @@ var EventHandler = class {
20630
20773
  if (!extResult || !extResult.answers) {
20631
20774
  await this.sdk.question.reject({ requestID: q.id, directory }).catch(() => {
20632
20775
  });
20633
- ocEvent("question.rejected", { requestID: q.id });
20776
+ sysLog("question.rejected", { requestID: q.id });
20634
20777
  return;
20635
20778
  }
20636
20779
  const answers = extResult.answers;
@@ -20638,7 +20781,7 @@ var EventHandler = class {
20638
20781
  console.error("[event-handler] invalid answers shape from client:", JSON.stringify(answers));
20639
20782
  await this.sdk.question.reject({ requestID: q.id, directory }).catch(() => {
20640
20783
  });
20641
- ocEvent("question.rejected", {
20784
+ sysLog("question.rejected", {
20642
20785
  requestID: q.id,
20643
20786
  reason: "invalid_answers"
20644
20787
  });
@@ -20649,7 +20792,7 @@ var EventHandler = class {
20649
20792
  directory,
20650
20793
  answers
20651
20794
  });
20652
- ocEvent("question.reply", { requestID: q.id, answers });
20795
+ sysLog("question.reply", { requestID: q.id, answers });
20653
20796
  }).catch((err) => {
20654
20797
  console.error("[event-handler] question handling error:", err);
20655
20798
  }).finally(() => {
@@ -20673,7 +20816,7 @@ var EventHandler = class {
20673
20816
  await this.handleToolPart(resolved.sessionId, part);
20674
20817
  }
20675
20818
  if (part.type === "text" && typeof part.text === "string" && part.ignored === true) {
20676
- await this.sendToClient({
20819
+ await sendToClient(this.connection, {
20677
20820
  sessionId: resolved.sessionId,
20678
20821
  update: {
20679
20822
  sessionUpdate: "agent_message_chunk",
@@ -20694,7 +20837,7 @@ var EventHandler = class {
20694
20837
  const partMeta = this.partMetaIndex.get(props.partID);
20695
20838
  if (!partMeta) return;
20696
20839
  if (partMeta.type === "text" && props.field === "text" && partMeta.ignored !== true) {
20697
- await this.sendToClient({
20840
+ await sendToClient(this.connection, {
20698
20841
  sessionId,
20699
20842
  update: {
20700
20843
  sessionUpdate: "agent_message_chunk",
@@ -20706,7 +20849,7 @@ var EventHandler = class {
20706
20849
  return;
20707
20850
  }
20708
20851
  if (partMeta.type === "reasoning" && props.field === "text") {
20709
- await this.sendToClient({
20852
+ await sendToClient(this.connection, {
20710
20853
  sessionId,
20711
20854
  update: {
20712
20855
  sessionUpdate: "agent_thought_chunk",
@@ -20731,7 +20874,7 @@ var EventHandler = class {
20731
20874
  additions += d.additions;
20732
20875
  deletions += d.deletions;
20733
20876
  }
20734
- this.diffStats.set(resolved.sessionId, { additions, deletions, files: diff.length });
20877
+ this.fileDiffStats.set(resolved.sessionId, { additions, deletions, files: diff.length });
20735
20878
  return;
20736
20879
  }
20737
20880
  default: {
@@ -20747,7 +20890,7 @@ var EventHandler = class {
20747
20890
  * and _meta containing structured metadata about the subagent.
20748
20891
  */
20749
20892
  async announceChildSession(childSessionId, parentSessionId, title, meta3) {
20750
- await this.sendToClient({
20893
+ await sendToClient(this.connection, {
20751
20894
  sessionId: childSessionId,
20752
20895
  update: {
20753
20896
  sessionUpdate: "session_info_update",
@@ -20767,6 +20910,7 @@ var EventHandler = class {
20767
20910
  async handleToolPart(sessionId, part) {
20768
20911
  if (!this.toolStarts.has(part.callID)) {
20769
20912
  this.toolStarts.add(part.callID);
20913
+ this.turnTimeoutTracker?.onToolCallStart(sessionId);
20770
20914
  const session = this.sessionManager.tryGet(sessionId);
20771
20915
  if (session?.parentID) {
20772
20916
  const tracker = this.childToolCounts.get(sessionId);
@@ -20780,7 +20924,7 @@ var EventHandler = class {
20780
20924
  });
20781
20925
  }
20782
20926
  }
20783
- await this.sendToClient({
20927
+ await sendToClient(this.connection, {
20784
20928
  sessionId,
20785
20929
  update: {
20786
20930
  sessionUpdate: "tool_call",
@@ -20812,7 +20956,7 @@ var EventHandler = class {
20812
20956
  content: { type: "text", text: output }
20813
20957
  });
20814
20958
  }
20815
- await this.sendToClient({
20959
+ await sendToClient(this.connection, {
20816
20960
  sessionId,
20817
20961
  update: {
20818
20962
  sessionUpdate: "tool_call_update",
@@ -20829,15 +20973,18 @@ var EventHandler = class {
20829
20973
  return;
20830
20974
  }
20831
20975
  case "completed": {
20832
- this.toolStarts.delete(part.callID);
20976
+ if (this.toolStarts.delete(part.callID)) {
20977
+ this.turnTimeoutTracker?.onToolCallEnd(sessionId);
20978
+ }
20833
20979
  this.bashSnapshots.delete(part.callID);
20834
20980
  if (part.tool === "task" && part.state.metadata) {
20835
20981
  const childSessionId = typeof part.state.metadata["sessionId"] === "string" ? part.state.metadata["sessionId"] : void 0;
20836
20982
  if (childSessionId && this.sessionManager.tryGet(childSessionId)) {
20837
20983
  const tracker = this.childToolCounts.get(childSessionId);
20838
- await this.sendChildSessionCompleted(
20984
+ await this.sendChildSessionFinished(
20839
20985
  childSessionId,
20840
20986
  sessionId,
20987
+ "completed",
20841
20988
  tracker?.completed ?? 0,
20842
20989
  tracker ? Date.now() - tracker.startTime : 0
20843
20990
  ).catch(() => {
@@ -20865,12 +21012,13 @@ var EventHandler = class {
20865
21012
  const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "";
20866
21013
  const newText = typeof input["newString"] === "string" ? input["newString"] : typeof input["content"] === "string" ? input["content"] : "";
20867
21014
  content.push({ type: "diff", path: filePath, oldText, newText });
21015
+ this.accumulateAICodeChangeStats(sessionId, part);
20868
21016
  }
20869
21017
  if (part.tool === "todowrite") {
20870
21018
  try {
20871
21019
  const todos = JSON.parse(part.state.output);
20872
21020
  if (Array.isArray(todos)) {
20873
- await this.sendToClient({
21021
+ await sendToClient(this.connection, {
20874
21022
  sessionId,
20875
21023
  update: {
20876
21024
  sessionUpdate: "plan",
@@ -20886,7 +21034,7 @@ var EventHandler = class {
20886
21034
  } catch {
20887
21035
  }
20888
21036
  }
20889
- await this.sendToClient({
21037
+ await sendToClient(this.connection, {
20890
21038
  sessionId,
20891
21039
  update: {
20892
21040
  sessionUpdate: "tool_call_update",
@@ -20906,9 +21054,33 @@ var EventHandler = class {
20906
21054
  return;
20907
21055
  }
20908
21056
  case "error": {
20909
- this.toolStarts.delete(part.callID);
21057
+ if (this.toolStarts.delete(part.callID)) {
21058
+ this.turnTimeoutTracker?.onToolCallEnd(sessionId);
21059
+ }
20910
21060
  this.bashSnapshots.delete(part.callID);
20911
- await this.sendToClient({
21061
+ if (part.tool === "task" && part.state.metadata) {
21062
+ const childSessionId = typeof part.state.metadata["sessionId"] === "string" ? part.state.metadata["sessionId"] : void 0;
21063
+ if (childSessionId && this.sessionManager.tryGet(childSessionId)) {
21064
+ const tracker = this.childToolCounts.get(childSessionId);
21065
+ const errText = typeof part.state.error === "string" && part.state.error.trim() ? part.state.error.trim() : "Sub-agent task failed";
21066
+ const payload = {
21067
+ code: "session_error",
21068
+ message: errText,
21069
+ display: "inline"
21070
+ };
21071
+ await this.sendChildSessionFinished(
21072
+ childSessionId,
21073
+ sessionId,
21074
+ "failed",
21075
+ tracker?.completed ?? 0,
21076
+ tracker ? Date.now() - tracker.startTime : 0,
21077
+ payload
21078
+ ).catch(() => {
21079
+ });
21080
+ this.childToolCounts.delete(childSessionId);
21081
+ }
21082
+ }
21083
+ await sendToClient(this.connection, {
20912
21084
  sessionId,
20913
21085
  update: {
20914
21086
  sessionUpdate: "tool_call_update",
@@ -20935,21 +21107,20 @@ var EventHandler = class {
20935
21107
  }
20936
21108
  }
20937
21109
  /**
20938
- * Notify the ACP client that a child session has completed.
21110
+ * Notify the ACP client that a child session finished (success or failure).
20939
21111
  */
20940
- async sendChildSessionCompleted(childSessionId, parentSessionId, toolCallCount, durationMs) {
21112
+ async sendChildSessionFinished(childSessionId, parentSessionId, status, toolCallCount, durationMs, error48) {
20941
21113
  const child = this.sessionManager.tryGet(childSessionId);
20942
21114
  const title = child?.title ?? "Subagent";
20943
21115
  const agentMatch = title.match(/@(\w+)\s+subagent/);
20944
21116
  const agentType = agentMatch?.[1];
20945
21117
  const description = title.replace(/\s*\(@\w+\s+subagent\)\s*$/, "");
20946
- const childDiffStats = this.diffStats.get(childSessionId);
20947
- await this.sendToClient({
21118
+ await sendToClient(this.connection, {
20948
21119
  sessionId: childSessionId,
20949
21120
  update: {
20950
21121
  sessionUpdate: "session_info_update",
20951
21122
  title,
20952
- status: "completed",
21123
+ status,
20953
21124
  _meta: {
20954
21125
  parentSessionId,
20955
21126
  isSubagent: true,
@@ -20957,7 +21128,7 @@ var EventHandler = class {
20957
21128
  durationMs,
20958
21129
  ...agentType && { agentType },
20959
21130
  ...description && { description },
20960
- ...childDiffStats && { codeChange: childDiffStats }
21131
+ ...error48 && { error: error48 }
20961
21132
  }
20962
21133
  }
20963
21134
  });
@@ -20969,6 +21140,44 @@ var EventHandler = class {
20969
21140
  if (typeof output !== "string") return;
20970
21141
  return output;
20971
21142
  }
21143
+ // ─── Statistics Accessors ────────────────────────────────────────
21144
+ getFileDiffStats(sessionId) {
21145
+ return this.fileDiffStats.get(sessionId);
21146
+ }
21147
+ setFileDiffStats(sessionId, stats) {
21148
+ if (this.fileDiffStats.has(sessionId)) return;
21149
+ this.fileDiffStats.set(sessionId, stats);
21150
+ }
21151
+ /**
21152
+ * Accumulate AI code change stats from a completed tool part's metadata.
21153
+ * The harmony-code plugin injects `metadata.aiCodeChange` for edit/write tools.
21154
+ */
21155
+ accumulateAICodeChangeStats(sessionId, part) {
21156
+ if (part.state.status !== "completed") return;
21157
+ const meta3 = part.state.metadata ?? {};
21158
+ const aiCodeChange = meta3["aiCodeChange"];
21159
+ if (!aiCodeChange || aiCodeChange.additions === 0 && aiCodeChange.deletions === 0) return;
21160
+ const fileMap = this.aiCodeChangeStats.get(sessionId) ?? /* @__PURE__ */ new Map();
21161
+ const existing = fileMap.get(aiCodeChange.file) ?? { additions: 0, deletions: 0 };
21162
+ fileMap.set(aiCodeChange.file, {
21163
+ additions: existing.additions + aiCodeChange.additions,
21164
+ deletions: existing.deletions + aiCodeChange.deletions
21165
+ });
21166
+ if (!this.aiCodeChangeStats.has(sessionId)) {
21167
+ this.aiCodeChangeStats.set(sessionId, fileMap);
21168
+ }
21169
+ }
21170
+ getAICodeChangeStats(sessionId) {
21171
+ const fileMap = this.aiCodeChangeStats.get(sessionId);
21172
+ if (!fileMap || fileMap.size === 0) return void 0;
21173
+ let additions = 0;
21174
+ let deletions = 0;
21175
+ for (const stats of fileMap.values()) {
21176
+ additions += stats.additions;
21177
+ deletions += stats.deletions;
21178
+ }
21179
+ return { additions, deletions, files: fileMap.size };
21180
+ }
20972
21181
  };
20973
21182
  function simpleHash(str) {
20974
21183
  let hash2 = 0;
@@ -21060,6 +21269,140 @@ var AuthProviderManager = class {
21060
21269
  }
21061
21270
  };
21062
21271
 
21272
+ // src/turn-timeout-tracker.ts
21273
+ var TURN_IDLE_LOG_MS = 9e4;
21274
+ var TURN_IDLE_LOG_MS_WITH_TOOL = 10 * 6e4;
21275
+ var TURN_IDLE_CHECK_INTERVAL_MS = 3e4;
21276
+ function parsePositiveIntEnv(name, fallback) {
21277
+ const raw = process.env[name];
21278
+ if (!raw) return fallback;
21279
+ const n = Number.parseInt(raw, 10);
21280
+ return Number.isFinite(n) && n > 0 ? n : fallback;
21281
+ }
21282
+ function turnIdleLogThresholdMs(hasActiveTool) {
21283
+ const base = parsePositiveIntEnv("HARMONY_ACP_TURN_IDLE_LOG_MS", TURN_IDLE_LOG_MS);
21284
+ const withTool = parsePositiveIntEnv("HARMONY_ACP_TURN_IDLE_LOG_MS_WITH_TOOL", TURN_IDLE_LOG_MS_WITH_TOOL);
21285
+ return hasActiveTool ? withTool : base;
21286
+ }
21287
+ function sessionIdFromEvent(event) {
21288
+ const props = event.properties;
21289
+ if (!props) return void 0;
21290
+ switch (event.type) {
21291
+ case "message.part.updated": {
21292
+ const part = props.part;
21293
+ return typeof part?.sessionID === "string" ? part.sessionID : void 0;
21294
+ }
21295
+ case "message.part.delta":
21296
+ return typeof props.sessionID === "string" ? props.sessionID : void 0;
21297
+ case "session.diff":
21298
+ case "session.updated":
21299
+ case "session.created":
21300
+ return typeof props.sessionID === "string" ? props.sessionID : typeof props.info?.id === "string" ? props.info.id : void 0;
21301
+ case "permission.asked": {
21302
+ const permission = props;
21303
+ return typeof permission.sessionID === "string" ? permission.sessionID : void 0;
21304
+ }
21305
+ }
21306
+ return void 0;
21307
+ }
21308
+ var TurnTimeoutTracker = class {
21309
+ constructor(sessionManager) {
21310
+ this.sessionManager = sessionManager;
21311
+ }
21312
+ turns = /* @__PURE__ */ new Map();
21313
+ timer;
21314
+ start() {
21315
+ if (this.timer) return;
21316
+ const interval = parsePositiveIntEnv(
21317
+ "HARMONY_ACP_TURN_IDLE_CHECK_MS",
21318
+ TURN_IDLE_CHECK_INTERVAL_MS
21319
+ );
21320
+ this.timer = setInterval(() => this.checkIdleTurns(), interval);
21321
+ }
21322
+ stop() {
21323
+ if (this.timer) {
21324
+ clearInterval(this.timer);
21325
+ this.timer = void 0;
21326
+ }
21327
+ }
21328
+ rootSessionId(sessionId) {
21329
+ return this.sessionManager.findRootSession(sessionId)?.id ?? sessionId;
21330
+ }
21331
+ onPromptStart(sessionId, meta3) {
21332
+ const root = this.rootSessionId(sessionId);
21333
+ const now = Date.now();
21334
+ this.turns.set(root, {
21335
+ rootSessionId: root,
21336
+ promptStartedAt: now,
21337
+ lastActivityAt: now,
21338
+ activeToolCalls: 0,
21339
+ idleLogged: false,
21340
+ model: meta3?.model
21341
+ });
21342
+ sysLog("turn.prompt_start", { sessionId: root, model: meta3?.model });
21343
+ }
21344
+ onPromptEnd(sessionId) {
21345
+ const root = this.rootSessionId(sessionId);
21346
+ const state = this.turns.get(root);
21347
+ if (state) {
21348
+ sysLog("turn.prompt_end", {
21349
+ sessionId: root,
21350
+ durationMs: Date.now() - state.promptStartedAt
21351
+ });
21352
+ }
21353
+ this.turns.delete(root);
21354
+ }
21355
+ /** SSE / OpenCode activity for a session (child events bump the root turn). */
21356
+ noteActivity(sessionId) {
21357
+ const root = this.rootSessionId(sessionId);
21358
+ const state = this.turns.get(root);
21359
+ if (!state) return;
21360
+ state.lastActivityAt = Date.now();
21361
+ state.idleLogged = false;
21362
+ }
21363
+ onToolCallStart(sessionId) {
21364
+ const root = this.rootSessionId(sessionId);
21365
+ const state = this.turns.get(root);
21366
+ if (!state) return;
21367
+ state.activeToolCalls++;
21368
+ state.lastActivityAt = Date.now();
21369
+ state.idleLogged = false;
21370
+ }
21371
+ onToolCallEnd(sessionId) {
21372
+ const root = this.rootSessionId(sessionId);
21373
+ const state = this.turns.get(root);
21374
+ if (!state) return;
21375
+ state.activeToolCalls = Math.max(0, state.activeToolCalls - 1);
21376
+ state.lastActivityAt = Date.now();
21377
+ state.idleLogged = false;
21378
+ }
21379
+ noteEvent(event) {
21380
+ const sid = sessionIdFromEvent(event);
21381
+ if (sid) {
21382
+ this.noteActivity(sid);
21383
+ }
21384
+ }
21385
+ checkIdleTurns() {
21386
+ const now = Date.now();
21387
+ for (const state of this.turns.values()) {
21388
+ const idleMs = now - state.lastActivityAt;
21389
+ const thresholdMs = turnIdleLogThresholdMs(state.activeToolCalls > 0);
21390
+ if (idleMs < thresholdMs || state.idleLogged) {
21391
+ continue;
21392
+ }
21393
+ state.idleLogged = true;
21394
+ sysLog("turn.idle_timeout", {
21395
+ sessionId: state.rootSessionId,
21396
+ idleMs,
21397
+ thresholdMs,
21398
+ promptAgeMs: now - state.promptStartedAt,
21399
+ activeToolCalls: state.activeToolCalls,
21400
+ model: state.model
21401
+ });
21402
+ }
21403
+ }
21404
+ };
21405
+
21063
21406
  // src/agent.ts
21064
21407
  function isApiKeyError(err) {
21065
21408
  return err instanceof Error && err.name === "AI_LoadAPIKeyError";
@@ -21072,44 +21415,22 @@ var Agent = class {
21072
21415
  eventHandler;
21073
21416
  authProvider;
21074
21417
  mcpManager;
21075
- /**
21076
- * Wrapper around connection.sessionUpdate that logs what's sent to the ACP client.
21077
- */
21078
- async sendToClient(params) {
21079
- const updateType = params.update.sessionUpdate;
21080
- const update = params.update;
21081
- acpOut(`sessionUpdate.${updateType}`, {
21082
- sessionId: params.sessionId.slice(0, 12),
21083
- ...updateType === "agent_message_chunk" || updateType === "agent_thought_chunk" ? {
21084
- messageId: update.messageId?.slice(0, 12),
21085
- delta: update.content?.text?.slice(0, 200)
21086
- } : updateType === "tool_call" || updateType === "tool_call_update" ? {
21087
- toolCallId: update.toolCallId,
21088
- tool: update.title,
21089
- kind: update.kind,
21090
- status: update.status
21091
- } : updateType === "usage_update" ? {
21092
- used: update.used,
21093
- size: update.size,
21094
- cost: update.cost,
21095
- _meta: update._meta
21096
- } : {}
21097
- });
21098
- return this.connection.sessionUpdate(params);
21099
- }
21418
+ turnTimeoutTracker;
21100
21419
  constructor(config2) {
21101
21420
  this.config = config2;
21102
21421
  this.sdk = config2.sdk;
21103
21422
  this.sessionManager = new SessionManager(config2.sdk);
21104
21423
  this.authProvider = new AuthProviderManager(config2.sdk);
21105
21424
  this.mcpManager = new McpManager(config2.sdk);
21425
+ this.turnTimeoutTracker = new TurnTimeoutTracker(this.sessionManager);
21106
21426
  }
21107
21427
  init(connection) {
21108
21428
  this.connection = connection;
21109
21429
  this.eventHandler = new EventHandler({
21110
21430
  connection,
21111
21431
  sdk: this.sdk,
21112
- sessionManager: this.sessionManager
21432
+ sessionManager: this.sessionManager,
21433
+ turnTimeoutTracker: this.turnTimeoutTracker
21113
21434
  });
21114
21435
  this.eventHandler.start();
21115
21436
  }
@@ -21317,13 +21638,14 @@ var Agent = class {
21317
21638
  }
21318
21639
  }
21319
21640
  async loadSession(params) {
21641
+ acpIn("loadSession", { sessionId: params.sessionId, cwd: params.cwd });
21320
21642
  try {
21321
21643
  const directory = params.cwd;
21322
21644
  const sessionId = params.sessionId;
21323
21645
  const model = await this.defaultModel(directory);
21324
21646
  const { session } = await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model);
21325
21647
  if (session.summary) {
21326
- this.eventHandler.setDiffStats(sessionId, {
21648
+ this.eventHandler.setFileDiffStats(sessionId, {
21327
21649
  additions: session.summary.additions,
21328
21650
  deletions: session.summary.deletions,
21329
21651
  files: session.summary.files
@@ -21364,6 +21686,7 @@ var Agent = class {
21364
21686
  }
21365
21687
  }
21366
21688
  async listSessions(params) {
21689
+ acpIn("listSessions", { cwd: params.cwd, cursor: params.cursor });
21367
21690
  const limit = 100;
21368
21691
  const cursor = params.cursor ? Number(params.cursor) : void 0;
21369
21692
  const sessions = await this.sdk.session.list({ directory: params.cwd ?? void 0, roots: true }).then((x) => x.data ?? []);
@@ -21380,9 +21703,11 @@ var Agent = class {
21380
21703
  const next = filtered.length > limit && last ? String(last.time.updated) : void 0;
21381
21704
  const response = { sessions: entries };
21382
21705
  if (next) response.nextCursor = next;
21706
+ acpOut("listSessions.response", { count: entries.length, hasNext: !!next });
21383
21707
  return response;
21384
21708
  }
21385
21709
  async unstable_forkSession(params) {
21710
+ acpIn("forkSession", { sessionId: params.sessionId, cwd: params.cwd });
21386
21711
  try {
21387
21712
  const directory = params.cwd;
21388
21713
  const mcpServers = params.mcpServers ?? [];
@@ -21408,6 +21733,7 @@ var Agent = class {
21408
21733
  }
21409
21734
  }
21410
21735
  async unstable_resumeSession(params) {
21736
+ acpIn("resumeSession", { sessionId: params.sessionId, cwd: params.cwd });
21411
21737
  try {
21412
21738
  const directory = params.cwd;
21413
21739
  const sessionId = params.sessionId;
@@ -21415,7 +21741,7 @@ var Agent = class {
21415
21741
  const model = await this.defaultModel(directory);
21416
21742
  const { session } = await this.sessionManager.load(sessionId, directory, mcpServers, model);
21417
21743
  if (session.summary) {
21418
- this.eventHandler.setDiffStats(sessionId, {
21744
+ this.eventHandler.setFileDiffStats(sessionId, {
21419
21745
  additions: session.summary.additions,
21420
21746
  deletions: session.summary.deletions,
21421
21747
  files: session.summary.files
@@ -21447,92 +21773,114 @@ var Agent = class {
21447
21773
  const agent = session.modeId ?? await this.defaultAgent(directory);
21448
21774
  const parts = this.convertPromptParts(params.prompt);
21449
21775
  const cmd = this.parseCommand(parts);
21450
- ocCall("session.prompt", { sessionID, agent, model: `${model.providerID}/${model.modelID}` });
21451
- if (!cmd) {
21452
- const response2 = await this.sdk.session.prompt({
21776
+ const modelLabel = `${model.providerID}/${model.modelID}`;
21777
+ ocCall("session.prompt", { sessionID, agent, model: modelLabel });
21778
+ this.turnTimeoutTracker.onPromptStart(sessionID, { model: modelLabel });
21779
+ try {
21780
+ if (!cmd) {
21781
+ const response2 = await this.sdk.session.prompt({
21782
+ sessionID,
21783
+ model: {
21784
+ providerID: model.providerID,
21785
+ modelID: model.modelID
21786
+ },
21787
+ variant: this.sessionManager.getVariant(sessionID),
21788
+ parts,
21789
+ agent,
21790
+ directory
21791
+ });
21792
+ const rawErr2 = response2.error;
21793
+ ocCall("session.prompt.raw", {
21794
+ hasData: !!response2.data,
21795
+ hasError: !!rawErr2,
21796
+ errorName: rawErr2?.name,
21797
+ errorMessage: rawErr2?.message,
21798
+ errorCode: rawErr2?.code ?? rawErr2?.cause?.code,
21799
+ errorCauseName: rawErr2?.cause?.name,
21800
+ errorCauseMessage: rawErr2?.cause?.message
21801
+ });
21802
+ const msg2 = response2.data?.info;
21803
+ return await this.finishPromptTurn(sessionID, directory, { rawErr: rawErr2, msg: msg2 });
21804
+ }
21805
+ const command = await this.sdk.command.list({ directory }).then((x) => x.data?.find((c) => c.name === cmd.name));
21806
+ if (command) {
21807
+ const response2 = await this.sdk.session.command({
21808
+ sessionID,
21809
+ command: command.name,
21810
+ arguments: cmd.args,
21811
+ model: modelLabel,
21812
+ agent,
21813
+ directory
21814
+ });
21815
+ const rawErr2 = response2.error;
21816
+ const msg2 = response2.data?.info;
21817
+ return await this.finishPromptTurn(sessionID, directory, { rawErr: rawErr2, msg: msg2 });
21818
+ }
21819
+ if (cmd.name === "compact") {
21820
+ await this.sdk.session.summarize(
21821
+ {
21822
+ sessionID,
21823
+ directory,
21824
+ providerID: model.providerID,
21825
+ modelID: model.modelID
21826
+ },
21827
+ { throwOnError: true }
21828
+ ).catch(() => {
21829
+ });
21830
+ await this.sendUsageUpdate(sessionID, directory);
21831
+ return {
21832
+ stopReason: "end_turn",
21833
+ _meta: {}
21834
+ };
21835
+ }
21836
+ const response = await this.sdk.session.prompt({
21453
21837
  sessionID,
21454
- model: {
21455
- providerID: model.providerID,
21456
- modelID: model.modelID
21457
- },
21838
+ model: { providerID: model.providerID, modelID: model.modelID },
21458
21839
  variant: this.sessionManager.getVariant(sessionID),
21459
21840
  parts,
21460
21841
  agent,
21461
21842
  directory
21462
21843
  });
21463
- const msg2 = response2.data?.info;
21464
- ocCall("session.prompt.response", {
21465
- messageId: msg2?.id,
21466
- role: msg2?.role,
21467
- model: msg2 ? `${msg2.providerID}/${msg2.modelID}` : void 0,
21468
- tokens: msg2?.tokens,
21469
- cost: msg2?.cost,
21470
- finish: msg2?.finish,
21471
- error: msg2?.error
21472
- });
21473
- await this.logMessageContent(sessionID, msg2?.id, directory);
21474
- await this.sendUsageUpdate(sessionID, directory);
21475
- acpOut("prompt.response", {
21476
- stopReason: "end_turn",
21477
- usage: msg2 ? this.buildUsage(msg2) : void 0
21478
- });
21479
- return {
21480
- stopReason: "end_turn",
21481
- usage: msg2 ? this.buildUsage(msg2) : void 0,
21482
- _meta: {}
21483
- };
21484
- }
21485
- const command = await this.sdk.command.list({ directory }).then((x) => x.data?.find((c) => c.name === cmd.name));
21486
- if (command) {
21487
- const response2 = await this.sdk.session.command({
21488
- sessionID,
21489
- command: command.name,
21490
- arguments: cmd.args,
21491
- model: model.providerID + "/" + model.modelID,
21492
- agent,
21493
- directory
21494
- });
21495
- const msg2 = response2.data?.info;
21496
- await this.logMessageContent(sessionID, msg2?.id, directory);
21497
- await this.sendUsageUpdate(sessionID, directory);
21498
- return {
21499
- stopReason: "end_turn",
21500
- usage: msg2 ? this.buildUsage(msg2) : void 0,
21501
- _meta: {}
21502
- };
21503
- }
21504
- if (cmd.name === "compact") {
21505
- await this.sdk.session.summarize(
21506
- {
21507
- sessionID,
21508
- directory,
21509
- providerID: model.providerID,
21510
- modelID: model.modelID
21511
- },
21512
- { throwOnError: true }
21513
- ).catch(() => {
21514
- });
21515
- await this.sendUsageUpdate(sessionID, directory);
21516
- return {
21517
- stopReason: "end_turn",
21518
- _meta: {}
21519
- };
21844
+ const rawErr = response.error;
21845
+ const msg = response.data?.info;
21846
+ return await this.finishPromptTurn(sessionID, directory, { rawErr, msg });
21847
+ } finally {
21848
+ this.turnTimeoutTracker.onPromptEnd(sessionID);
21520
21849
  }
21521
- const response = await this.sdk.session.prompt({
21522
- sessionID,
21523
- model: { providerID: model.providerID, modelID: model.modelID },
21524
- variant: this.sessionManager.getVariant(sessionID),
21525
- parts,
21526
- agent,
21527
- directory
21850
+ }
21851
+ /**
21852
+ * Complete a prompt turn: emit streaming errors, usage, and PromptResponse._meta.error.
21853
+ */
21854
+ async finishPromptTurn(sessionID, directory, opts) {
21855
+ const turnError = extractTurnError({
21856
+ responseError: opts.rawErr,
21857
+ messageError: opts.msg?.error
21858
+ });
21859
+ ocCall("session.prompt.response", {
21860
+ messageId: opts.msg?.id,
21861
+ role: opts.msg?.role,
21862
+ model: opts.msg ? `${opts.msg.providerID}/${opts.msg.modelID}` : void 0,
21863
+ tokens: opts.msg?.tokens,
21864
+ cost: opts.msg?.cost,
21865
+ finish: opts.msg?.finish,
21866
+ error: turnError?.message
21867
+ });
21868
+ if (turnError) {
21869
+ await emitSessionError(this.connection, sessionID, turnError, { messageId: opts.msg?.id });
21870
+ }
21871
+ await this.logMessageContent(sessionID, opts.msg?.id, directory);
21872
+ await this.sendUsageUpdate(sessionID, directory, turnError);
21873
+ const stopReason = stopReasonForTurn(turnError, opts.msg?.finish);
21874
+ const usage = opts.msg ? this.buildUsage(opts.msg) : void 0;
21875
+ acpOut("prompt.response", {
21876
+ stopReason,
21877
+ usage,
21878
+ error: turnError?.message
21528
21879
  });
21529
- const msg = response.data?.info;
21530
- await this.logMessageContent(sessionID, msg?.id, directory);
21531
- await this.sendUsageUpdate(sessionID, directory);
21532
21880
  return {
21533
- stopReason: "end_turn",
21534
- usage: msg ? this.buildUsage(msg) : void 0,
21535
- _meta: {}
21881
+ stopReason,
21882
+ usage,
21883
+ _meta: turnError ? { error: turnError } : {}
21536
21884
  };
21537
21885
  }
21538
21886
  async cancel(params) {
@@ -21543,9 +21891,11 @@ var Agent = class {
21543
21891
  sessionID: params.sessionId,
21544
21892
  directory: session.cwd
21545
21893
  });
21894
+ this.turnTimeoutTracker.onPromptEnd(params.sessionId);
21546
21895
  }
21547
21896
  // ─── Configuration ────────────────────────────────────────────────
21548
21897
  async setSessionMode(params) {
21898
+ acpIn("setSessionMode", { sessionId: params.sessionId, modeId: params.modeId });
21549
21899
  const session = this.sessionManager.get(params.sessionId);
21550
21900
  const agents = await this.sdk.app.agents({ directory: session.cwd }).then((x) => x.data ?? []).catch(() => []);
21551
21901
  const availableModes = agents.filter((a) => a.mode !== "subagent" && !a.hidden);
@@ -21555,6 +21905,7 @@ var Agent = class {
21555
21905
  this.sessionManager.setMode(params.sessionId, params.modeId);
21556
21906
  }
21557
21907
  async unstable_setSessionModel(params) {
21908
+ acpIn("setSessionModel", { sessionId: params.sessionId, modelId: params.modelId });
21558
21909
  const session = this.sessionManager.get(params.sessionId);
21559
21910
  const providers = await this.sdk.config.providers({ directory: session.cwd }).then((x) => x.data?.providers ?? []).catch(() => []);
21560
21911
  const selection = parseModelSelection(params.modelId, providers);
@@ -21568,6 +21919,7 @@ var Agent = class {
21568
21919
  };
21569
21920
  }
21570
21921
  async setSessionConfigOption(params) {
21922
+ acpIn("setSessionConfigOption", { sessionId: params.sessionId, configId: params.configId, value: params.value });
21571
21923
  const session = this.sessionManager.get(params.sessionId);
21572
21924
  const providers = await this.sdk.config.providers({ directory: session.cwd }).then((x) => x.data?.providers ?? []).catch(() => []);
21573
21925
  const sortedProviders = [...providers].sort(
@@ -21653,7 +22005,7 @@ var Agent = class {
21653
22005
  if (!childMessages?.length) return;
21654
22006
  const title = `Subagent (${childSessionId.slice(0, 8)})`;
21655
22007
  this.sessionManager.registerDiscovered(childSessionId, parentSessionId, title);
21656
- await this.sendToClient({
22008
+ await sendToClient(this.connection, {
21657
22009
  sessionId: childSessionId,
21658
22010
  update: {
21659
22011
  sessionUpdate: "session_info_update",
@@ -21770,7 +22122,7 @@ var Agent = class {
21770
22122
  availableCommands.push({ name: "compact", description: "compact the session" });
21771
22123
  }
21772
22124
  setTimeout(() => {
21773
- this.sendToClient({
22125
+ sendToClient(this.connection, {
21774
22126
  sessionId: params.sessionId,
21775
22127
  update: {
21776
22128
  sessionUpdate: "available_commands_update",
@@ -21823,7 +22175,7 @@ var Agent = class {
21823
22175
  await this.replayToolPart(sessionId, part);
21824
22176
  } else if (part.type === "text" && part.text) {
21825
22177
  const audience = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : void 0;
21826
- await this.sendToClient({
22178
+ await sendToClient(this.connection, {
21827
22179
  sessionId,
21828
22180
  update: {
21829
22181
  sessionUpdate: messageChunk,
@@ -21841,7 +22193,7 @@ var Agent = class {
21841
22193
  const filename = part.filename ?? "file";
21842
22194
  const mime = part.mime || "application/octet-stream";
21843
22195
  if (url2.startsWith("file://")) {
21844
- await this.sendToClient({
22196
+ await sendToClient(this.connection, {
21845
22197
  sessionId,
21846
22198
  update: {
21847
22199
  sessionUpdate: messageChunk,
@@ -21856,7 +22208,7 @@ var Agent = class {
21856
22208
  const base64Data = base64Match?.[2] ?? "";
21857
22209
  const effectiveMime = dataMime || mime;
21858
22210
  if (effectiveMime.startsWith("image/")) {
21859
- await this.sendToClient({
22211
+ await sendToClient(this.connection, {
21860
22212
  sessionId,
21861
22213
  update: {
21862
22214
  sessionUpdate: messageChunk,
@@ -21878,7 +22230,7 @@ var Agent = class {
21878
22230
  mimeType: effectiveMime,
21879
22231
  text: Buffer.from(base64Data, "base64").toString("utf-8")
21880
22232
  } : { uri: fileUri, mimeType: effectiveMime, blob: base64Data };
21881
- await this.sendToClient({
22233
+ await sendToClient(this.connection, {
21882
22234
  sessionId,
21883
22235
  update: {
21884
22236
  sessionUpdate: messageChunk,
@@ -21890,7 +22242,7 @@ var Agent = class {
21890
22242
  }
21891
22243
  }
21892
22244
  } else if (part.type === "reasoning" && part.text) {
21893
- await this.sendToClient({
22245
+ await sendToClient(this.connection, {
21894
22246
  sessionId,
21895
22247
  update: {
21896
22248
  sessionUpdate: "agent_thought_chunk",
@@ -21903,7 +22255,7 @@ var Agent = class {
21903
22255
  }
21904
22256
  }
21905
22257
  async replayToolPart(sessionId, part) {
21906
- await this.sendToClient({
22258
+ await sendToClient(this.connection, {
21907
22259
  sessionId,
21908
22260
  update: {
21909
22261
  sessionUpdate: "tool_call",
@@ -21928,12 +22280,13 @@ var Agent = class {
21928
22280
  oldText: typeof input["oldString"] === "string" ? input["oldString"] : "",
21929
22281
  newText: typeof input["newString"] === "string" ? input["newString"] : typeof input["content"] === "string" ? input["content"] : ""
21930
22282
  });
22283
+ this.eventHandler.accumulateAICodeChangeStats(sessionId, part);
21931
22284
  }
21932
22285
  if (part.tool === "todowrite") {
21933
22286
  try {
21934
22287
  const todos = JSON.parse(part.state.output);
21935
22288
  if (Array.isArray(todos)) {
21936
- await this.sendToClient({
22289
+ await sendToClient(this.connection, {
21937
22290
  sessionId,
21938
22291
  update: {
21939
22292
  sessionUpdate: "plan",
@@ -21949,7 +22302,7 @@ var Agent = class {
21949
22302
  } catch {
21950
22303
  }
21951
22304
  }
21952
- await this.sendToClient({
22305
+ await sendToClient(this.connection, {
21953
22306
  sessionId,
21954
22307
  update: {
21955
22308
  sessionUpdate: "tool_call_update",
@@ -21966,7 +22319,7 @@ var Agent = class {
21966
22319
  break;
21967
22320
  }
21968
22321
  case "error":
21969
- await this.sendToClient({
22322
+ await sendToClient(this.connection, {
21970
22323
  sessionId,
21971
22324
  update: {
21972
22325
  sessionUpdate: "tool_call_update",
@@ -21983,7 +22336,7 @@ var Agent = class {
21983
22336
  break;
21984
22337
  }
21985
22338
  }
21986
- async sendUsageUpdate(sessionId, directory) {
22339
+ async sendUsageUpdate(sessionId, directory, turnError) {
21987
22340
  const messages = await this.sdk.session.messages({ sessionID: sessionId, directory }).then((x) => x.data).catch(() => void 0);
21988
22341
  if (!messages) return;
21989
22342
  const assistantMessages = messages.filter(
@@ -22002,19 +22355,6 @@ var Agent = class {
22002
22355
  childCost += childMessages.filter((m) => m.info.role === "assistant").reduce((sum, m) => sum + (m.info.cost ?? 0), 0);
22003
22356
  }
22004
22357
  }
22005
- const stats = this.eventHandler.getDiffStats(sessionId);
22006
- let childStats;
22007
- for (const childId of children) {
22008
- const cs = this.eventHandler.getDiffStats(childId);
22009
- if (cs) {
22010
- childStats = childStats ? {
22011
- additions: childStats.additions + cs.additions,
22012
- deletions: childStats.deletions + cs.deletions,
22013
- files: childStats.files + cs.files
22014
- } : { ...cs };
22015
- }
22016
- }
22017
- const totalStats = stats ?? childStats;
22018
22358
  const providers = await this.sdk.config.providers({ directory }).then((x) => x.data?.providers ?? []).catch(() => []);
22019
22359
  const provider = providers.find((p) => p.id === msg.providerID);
22020
22360
  const model = provider?.models[msg.modelID];
@@ -22025,13 +22365,12 @@ var Agent = class {
22025
22365
  _meta.childCost = childCost;
22026
22366
  _meta.childSessionCount = children.length;
22027
22367
  }
22028
- if (totalStats) {
22029
- _meta.codeChange = totalStats;
22368
+ this.aggregateFileDiffStats(sessionId, children, _meta);
22369
+ this.aggregateAICodeChangeStats(sessionId, children, _meta);
22370
+ if (turnError) {
22371
+ _meta.error = turnError;
22030
22372
  }
22031
- if (childStats) {
22032
- _meta.childCodeChange = childStats;
22033
- }
22034
- await this.sendToClient({
22373
+ await sendToClient(this.connection, {
22035
22374
  sessionId,
22036
22375
  update: {
22037
22376
  sessionUpdate: "usage_update",
@@ -22043,6 +22382,56 @@ var Agent = class {
22043
22382
  }).catch(() => {
22044
22383
  });
22045
22384
  }
22385
+ /**
22386
+ * Aggregate file diff stats (git-based) for parent + child sessions into _meta.
22387
+ * 主 session 的 session.diff 已包含子任务的变更数据,直接使用,不累加子 session。
22388
+ */
22389
+ aggregateFileDiffStats(sessionId, children, _meta) {
22390
+ const stats = this.eventHandler.getFileDiffStats(sessionId);
22391
+ let childFileDiffStats;
22392
+ for (const childId of children) {
22393
+ const cs = this.eventHandler.getFileDiffStats(childId);
22394
+ if (cs) {
22395
+ childFileDiffStats = childFileDiffStats ? {
22396
+ additions: childFileDiffStats.additions + cs.additions,
22397
+ deletions: childFileDiffStats.deletions + cs.deletions,
22398
+ files: childFileDiffStats.files + cs.files
22399
+ } : { ...cs };
22400
+ }
22401
+ }
22402
+ const totalFileDiffStats = stats ?? childFileDiffStats;
22403
+ if (totalFileDiffStats) {
22404
+ _meta.fileDiffStats = totalFileDiffStats;
22405
+ }
22406
+ if (childFileDiffStats) {
22407
+ _meta.childFileDiffStats = childFileDiffStats;
22408
+ }
22409
+ }
22410
+ /**
22411
+ * Aggregate AI code change stats (tool-based) for parent + child sessions into _meta.
22412
+ */
22413
+ aggregateAICodeChangeStats(sessionId, children, _meta) {
22414
+ const parentStats = this.eventHandler.getAICodeChangeStats(sessionId);
22415
+ let childAICodeChangeStats;
22416
+ for (const childId of children) {
22417
+ const cs = this.eventHandler.getAICodeChangeStats(childId);
22418
+ if (cs) {
22419
+ childAICodeChangeStats = childAICodeChangeStats ? {
22420
+ additions: childAICodeChangeStats.additions + cs.additions,
22421
+ deletions: childAICodeChangeStats.deletions + cs.deletions,
22422
+ files: childAICodeChangeStats.files + cs.files
22423
+ } : { ...cs };
22424
+ }
22425
+ }
22426
+ const totalAICodeChangeStats = parentStats ? childAICodeChangeStats ? {
22427
+ additions: parentStats.additions + childAICodeChangeStats.additions,
22428
+ deletions: parentStats.deletions + childAICodeChangeStats.deletions,
22429
+ files: parentStats.files + childAICodeChangeStats.files
22430
+ } : parentStats : childAICodeChangeStats;
22431
+ if (totalAICodeChangeStats) {
22432
+ _meta.aiCodeChange = totalAICodeChangeStats;
22433
+ }
22434
+ }
22046
22435
  convertPromptParts(parts) {
22047
22436
  const result = [];
22048
22437
  for (const part of parts) {
@@ -22124,13 +22513,15 @@ var Agent = class {
22124
22513
  }
22125
22514
  }
22126
22515
  buildUsage(msg) {
22516
+ const tokens = msg.tokens;
22517
+ if (!tokens) return void 0;
22127
22518
  return {
22128
- totalTokens: msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + (msg.tokens.cache?.read ?? 0) + (msg.tokens.cache?.write ?? 0),
22129
- inputTokens: msg.tokens.input,
22130
- outputTokens: msg.tokens.output,
22131
- thoughtTokens: msg.tokens.reasoning || void 0,
22132
- cachedReadTokens: msg.tokens.cache?.read || void 0,
22133
- cachedWriteTokens: msg.tokens.cache?.write || void 0
22519
+ totalTokens: tokens.input + tokens.output + tokens.reasoning + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0),
22520
+ inputTokens: tokens.input,
22521
+ outputTokens: tokens.output,
22522
+ thoughtTokens: tokens.reasoning || void 0,
22523
+ cachedReadTokens: tokens.cache?.read || void 0,
22524
+ cachedWriteTokens: tokens.cache?.write || void 0
22134
22525
  };
22135
22526
  }
22136
22527
  };
@@ -22168,6 +22559,91 @@ function parseModelSelection(modelId, providers) {
22168
22559
  }
22169
22560
 
22170
22561
  // src/index.ts
22562
+ var nativeFetch = (input, init) => {
22563
+ return new Promise((resolve, reject) => {
22564
+ let url2;
22565
+ let requestMethod;
22566
+ let requestHeaders;
22567
+ let requestBody = null;
22568
+ if (typeof input === "string") {
22569
+ url2 = new URL(input);
22570
+ } else if (input instanceof URL) {
22571
+ url2 = input;
22572
+ } else {
22573
+ url2 = new URL(input.url);
22574
+ requestMethod = input.method;
22575
+ requestHeaders = input.headers;
22576
+ requestBody = input.body;
22577
+ }
22578
+ const method = (init?.method ?? requestMethod ?? "GET").toUpperCase();
22579
+ const mergedHeaders = new Headers(init?.headers ?? requestHeaders ?? void 0);
22580
+ const headerObj = {};
22581
+ mergedHeaders.forEach((value, key) => {
22582
+ headerObj[key] = value;
22583
+ });
22584
+ const body = init?.body !== void 0 ? init.body : requestBody;
22585
+ const signal = init?.signal;
22586
+ if (signal?.aborted) {
22587
+ return reject(
22588
+ signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError")
22589
+ );
22590
+ }
22591
+ const mod = url2.protocol === "https:" ? https : http;
22592
+ const req = mod.request(
22593
+ url2,
22594
+ {
22595
+ method,
22596
+ headers: headerObj
22597
+ // Note: do NOT set `timeout` here. Leaving it unset means
22598
+ // node:http will not apply any inactivity-based abort.
22599
+ },
22600
+ (res) => {
22601
+ const respHeaders = new Headers();
22602
+ for (const [k, v] of Object.entries(res.headers)) {
22603
+ if (v == null) continue;
22604
+ if (Array.isArray(v)) {
22605
+ for (const item of v) respHeaders.append(k, item);
22606
+ } else {
22607
+ respHeaders.set(k, String(v));
22608
+ }
22609
+ }
22610
+ const webStream = import_node_stream.Readable.toWeb(res);
22611
+ resolve(
22612
+ new Response(webStream, {
22613
+ status: res.statusCode ?? 200,
22614
+ statusText: res.statusMessage ?? "",
22615
+ headers: respHeaders
22616
+ })
22617
+ );
22618
+ }
22619
+ );
22620
+ req.on("error", reject);
22621
+ if (signal) {
22622
+ signal.addEventListener(
22623
+ "abort",
22624
+ () => {
22625
+ req.destroy(
22626
+ signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError")
22627
+ );
22628
+ },
22629
+ { once: true }
22630
+ );
22631
+ }
22632
+ if (body == null) {
22633
+ req.end();
22634
+ } else if (typeof body === "string") {
22635
+ req.end(body);
22636
+ } else if (body instanceof Uint8Array || Buffer.isBuffer(body)) {
22637
+ req.end(body);
22638
+ } else if (body instanceof URLSearchParams) {
22639
+ req.end(body.toString());
22640
+ } else if (typeof body.pipeTo === "function") {
22641
+ import_node_stream.Readable.fromWeb(body).pipe(req);
22642
+ } else {
22643
+ req.end(String(body));
22644
+ }
22645
+ });
22646
+ };
22171
22647
  function parseArgs(args) {
22172
22648
  let server = "http://localhost:4096";
22173
22649
  let cwd = process.cwd();
@@ -22207,7 +22683,10 @@ async function main() {
22207
22683
  initLogger();
22208
22684
  if (log) setLogEnabled(true);
22209
22685
  sysLog("starting", { server, cwd });
22210
- const sdk = createOpencodeClient({ baseUrl: server });
22686
+ const sdk = createOpencodeClient({
22687
+ baseUrl: server,
22688
+ fetch: nativeFetch
22689
+ });
22211
22690
  try {
22212
22691
  await sdk.global.health();
22213
22692
  sysLog("server_connected");