@harmonyos-arkts/opencode-acp 0.0.6 → 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
@@ -19704,6 +19704,14 @@ var SessionManager = class {
19704
19704
  getChildren(parentId) {
19705
19705
  return [...this.children.get(parentId) ?? []];
19706
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
+ }
19707
19715
  // Model and mode management
19708
19716
  getModel(sessionId) {
19709
19717
  return this.get(sessionId).model;
@@ -20433,12 +20441,105 @@ async function sendToClient(connection, params) {
20433
20441
  const update = params.update;
20434
20442
  acpOut(`sessionUpdate.${updateType}`, {
20435
20443
  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 } : {}
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 } : {}
20437
20451
  });
20438
20452
  return connection.sessionUpdate(params);
20439
20453
  }
20440
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
+
20441
20540
  // src/event-handler.ts
20541
+ var SUBSCRIPTION_RETRY_MS = 3e3;
20542
+ var SUBSCRIPTION_ERROR_BROADCAST_MS = 3e4;
20442
20543
  var QUESTION_TIMEOUT_MS = 3e5;
20443
20544
  var permissionOptions = [
20444
20545
  { optionId: "once", kind: "allow_once", name: "Allow once" },
@@ -20463,38 +20564,65 @@ var EventHandler = class {
20463
20564
  fileDiffStats = /* @__PURE__ */ new Map();
20464
20565
  /** AI code change stats per session, keyed by sessionId → filePath → { additions, deletions }. */
20465
20566
  aiCodeChangeStats = /* @__PURE__ */ new Map();
20567
+ lastSubscriptionErrorBroadcastAt = 0;
20568
+ turnTimeoutTracker;
20466
20569
  constructor(deps) {
20467
20570
  this.connection = deps.connection;
20468
20571
  this.sdk = deps.sdk;
20469
20572
  this.sessionManager = deps.sessionManager;
20573
+ this.turnTimeoutTracker = deps.turnTimeoutTracker;
20470
20574
  }
20471
20575
  start() {
20472
20576
  if (this.started) return;
20473
20577
  this.started = true;
20474
- this.runSubscription().catch((err) => {
20475
- if (this.abort.signal.aborted) return;
20476
- console.error("[event-handler] subscription failed:", err);
20477
- });
20578
+ this.turnTimeoutTracker?.start();
20579
+ void this.runSubscription();
20478
20580
  }
20479
20581
  stop() {
20480
20582
  this.abort.abort();
20583
+ this.turnTimeoutTracker?.stop();
20481
20584
  }
20482
20585
  async runSubscription() {
20483
20586
  while (true) {
20484
20587
  if (this.abort.signal.aborted) return;
20485
- const events = await this.sdk.global.event({
20486
- signal: this.abort.signal
20487
- });
20488
- for await (const event of events.stream) {
20489
- if (this.abort.signal.aborted) return;
20490
- const payload = event?.payload;
20491
- if (!payload) continue;
20492
- await this.handleEvent(payload).catch((err) => {
20493
- console.error("[event-handler] failed to handle event:", err);
20588
+ try {
20589
+ const events = await this.sdk.global.event({
20590
+ signal: this.abort.signal
20494
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));
20495
20605
  }
20496
20606
  }
20497
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
+ }
20498
20626
  // ─── Resolve session for routing ────────────────────────────────
20499
20627
  /**
20500
20628
  * Resolve the sessionId to use for ACP routing.
@@ -20511,6 +20639,7 @@ var EventHandler = class {
20511
20639
  if (event.type === "server.heartbeat") {
20512
20640
  return;
20513
20641
  }
20642
+ this.turnTimeoutTracker?.noteEvent(event);
20514
20643
  switch (event.type) {
20515
20644
  case "session.created":
20516
20645
  case "session.updated": {
@@ -20781,6 +20910,7 @@ var EventHandler = class {
20781
20910
  async handleToolPart(sessionId, part) {
20782
20911
  if (!this.toolStarts.has(part.callID)) {
20783
20912
  this.toolStarts.add(part.callID);
20913
+ this.turnTimeoutTracker?.onToolCallStart(sessionId);
20784
20914
  const session = this.sessionManager.tryGet(sessionId);
20785
20915
  if (session?.parentID) {
20786
20916
  const tracker = this.childToolCounts.get(sessionId);
@@ -20843,15 +20973,18 @@ var EventHandler = class {
20843
20973
  return;
20844
20974
  }
20845
20975
  case "completed": {
20846
- this.toolStarts.delete(part.callID);
20976
+ if (this.toolStarts.delete(part.callID)) {
20977
+ this.turnTimeoutTracker?.onToolCallEnd(sessionId);
20978
+ }
20847
20979
  this.bashSnapshots.delete(part.callID);
20848
20980
  if (part.tool === "task" && part.state.metadata) {
20849
20981
  const childSessionId = typeof part.state.metadata["sessionId"] === "string" ? part.state.metadata["sessionId"] : void 0;
20850
20982
  if (childSessionId && this.sessionManager.tryGet(childSessionId)) {
20851
20983
  const tracker = this.childToolCounts.get(childSessionId);
20852
- await this.sendChildSessionCompleted(
20984
+ await this.sendChildSessionFinished(
20853
20985
  childSessionId,
20854
20986
  sessionId,
20987
+ "completed",
20855
20988
  tracker?.completed ?? 0,
20856
20989
  tracker ? Date.now() - tracker.startTime : 0
20857
20990
  ).catch(() => {
@@ -20921,8 +21054,32 @@ var EventHandler = class {
20921
21054
  return;
20922
21055
  }
20923
21056
  case "error": {
20924
- this.toolStarts.delete(part.callID);
21057
+ if (this.toolStarts.delete(part.callID)) {
21058
+ this.turnTimeoutTracker?.onToolCallEnd(sessionId);
21059
+ }
20925
21060
  this.bashSnapshots.delete(part.callID);
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
+ }
20926
21083
  await sendToClient(this.connection, {
20927
21084
  sessionId,
20928
21085
  update: {
@@ -20950,9 +21107,9 @@ var EventHandler = class {
20950
21107
  }
20951
21108
  }
20952
21109
  /**
20953
- * Notify the ACP client that a child session has completed.
21110
+ * Notify the ACP client that a child session finished (success or failure).
20954
21111
  */
20955
- async sendChildSessionCompleted(childSessionId, parentSessionId, toolCallCount, durationMs) {
21112
+ async sendChildSessionFinished(childSessionId, parentSessionId, status, toolCallCount, durationMs, error48) {
20956
21113
  const child = this.sessionManager.tryGet(childSessionId);
20957
21114
  const title = child?.title ?? "Subagent";
20958
21115
  const agentMatch = title.match(/@(\w+)\s+subagent/);
@@ -20963,14 +21120,15 @@ var EventHandler = class {
20963
21120
  update: {
20964
21121
  sessionUpdate: "session_info_update",
20965
21122
  title,
20966
- status: "completed",
21123
+ status,
20967
21124
  _meta: {
20968
21125
  parentSessionId,
20969
21126
  isSubagent: true,
20970
21127
  toolCallCount,
20971
21128
  durationMs,
20972
21129
  ...agentType && { agentType },
20973
- ...description && { description }
21130
+ ...description && { description },
21131
+ ...error48 && { error: error48 }
20974
21132
  }
20975
21133
  }
20976
21134
  });
@@ -21111,6 +21269,140 @@ var AuthProviderManager = class {
21111
21269
  }
21112
21270
  };
21113
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
+
21114
21406
  // src/agent.ts
21115
21407
  function isApiKeyError(err) {
21116
21408
  return err instanceof Error && err.name === "AI_LoadAPIKeyError";
@@ -21123,19 +21415,22 @@ var Agent = class {
21123
21415
  eventHandler;
21124
21416
  authProvider;
21125
21417
  mcpManager;
21418
+ turnTimeoutTracker;
21126
21419
  constructor(config2) {
21127
21420
  this.config = config2;
21128
21421
  this.sdk = config2.sdk;
21129
21422
  this.sessionManager = new SessionManager(config2.sdk);
21130
21423
  this.authProvider = new AuthProviderManager(config2.sdk);
21131
21424
  this.mcpManager = new McpManager(config2.sdk);
21425
+ this.turnTimeoutTracker = new TurnTimeoutTracker(this.sessionManager);
21132
21426
  }
21133
21427
  init(connection) {
21134
21428
  this.connection = connection;
21135
21429
  this.eventHandler = new EventHandler({
21136
21430
  connection,
21137
21431
  sdk: this.sdk,
21138
- sessionManager: this.sessionManager
21432
+ sessionManager: this.sessionManager,
21433
+ turnTimeoutTracker: this.turnTimeoutTracker
21139
21434
  });
21140
21435
  this.eventHandler.start();
21141
21436
  }
@@ -21478,102 +21773,114 @@ var Agent = class {
21478
21773
  const agent = session.modeId ?? await this.defaultAgent(directory);
21479
21774
  const parts = this.convertPromptParts(params.prompt);
21480
21775
  const cmd = this.parseCommand(parts);
21481
- ocCall("session.prompt", { sessionID, agent, model: `${model.providerID}/${model.modelID}` });
21482
- if (!cmd) {
21483
- 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({
21484
21837
  sessionID,
21485
- model: {
21486
- providerID: model.providerID,
21487
- modelID: model.modelID
21488
- },
21838
+ model: { providerID: model.providerID, modelID: model.modelID },
21489
21839
  variant: this.sessionManager.getVariant(sessionID),
21490
21840
  parts,
21491
21841
  agent,
21492
21842
  directory
21493
21843
  });
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
- });
21504
- const msg2 = response2.data?.info;
21505
- ocCall("session.prompt.response", {
21506
- messageId: msg2?.id,
21507
- role: msg2?.role,
21508
- model: msg2 ? `${msg2.providerID}/${msg2.modelID}` : void 0,
21509
- tokens: msg2?.tokens,
21510
- cost: msg2?.cost,
21511
- finish: msg2?.finish,
21512
- error: msg2?.error
21513
- });
21514
- await this.logMessageContent(sessionID, msg2?.id, directory);
21515
- await this.sendUsageUpdate(sessionID, directory);
21516
- acpOut("prompt.response", {
21517
- stopReason: "end_turn",
21518
- usage: msg2 ? this.buildUsage(msg2) : void 0
21519
- });
21520
- return {
21521
- stopReason: "end_turn",
21522
- usage: msg2 ? this.buildUsage(msg2) : void 0,
21523
- _meta: {}
21524
- };
21525
- }
21526
- const command = await this.sdk.command.list({ directory }).then((x) => x.data?.find((c) => c.name === cmd.name));
21527
- if (command) {
21528
- const response2 = await this.sdk.session.command({
21529
- sessionID,
21530
- command: command.name,
21531
- arguments: cmd.args,
21532
- model: model.providerID + "/" + model.modelID,
21533
- agent,
21534
- directory
21535
- });
21536
- const msg2 = response2.data?.info;
21537
- await this.logMessageContent(sessionID, msg2?.id, directory);
21538
- await this.sendUsageUpdate(sessionID, directory);
21539
- return {
21540
- stopReason: "end_turn",
21541
- usage: msg2 ? this.buildUsage(msg2) : void 0,
21542
- _meta: {}
21543
- };
21544
- }
21545
- if (cmd.name === "compact") {
21546
- await this.sdk.session.summarize(
21547
- {
21548
- sessionID,
21549
- directory,
21550
- providerID: model.providerID,
21551
- modelID: model.modelID
21552
- },
21553
- { throwOnError: true }
21554
- ).catch(() => {
21555
- });
21556
- await this.sendUsageUpdate(sessionID, directory);
21557
- return {
21558
- stopReason: "end_turn",
21559
- _meta: {}
21560
- };
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);
21561
21849
  }
21562
- const response = await this.sdk.session.prompt({
21563
- sessionID,
21564
- model: { providerID: model.providerID, modelID: model.modelID },
21565
- variant: this.sessionManager.getVariant(sessionID),
21566
- parts,
21567
- agent,
21568
- 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
21569
21879
  });
21570
- const msg = response.data?.info;
21571
- await this.logMessageContent(sessionID, msg?.id, directory);
21572
- await this.sendUsageUpdate(sessionID, directory);
21573
21880
  return {
21574
- stopReason: "end_turn",
21575
- usage: msg ? this.buildUsage(msg) : void 0,
21576
- _meta: {}
21881
+ stopReason,
21882
+ usage,
21883
+ _meta: turnError ? { error: turnError } : {}
21577
21884
  };
21578
21885
  }
21579
21886
  async cancel(params) {
@@ -21584,6 +21891,7 @@ var Agent = class {
21584
21891
  sessionID: params.sessionId,
21585
21892
  directory: session.cwd
21586
21893
  });
21894
+ this.turnTimeoutTracker.onPromptEnd(params.sessionId);
21587
21895
  }
21588
21896
  // ─── Configuration ────────────────────────────────────────────────
21589
21897
  async setSessionMode(params) {
@@ -22028,7 +22336,7 @@ var Agent = class {
22028
22336
  break;
22029
22337
  }
22030
22338
  }
22031
- async sendUsageUpdate(sessionId, directory) {
22339
+ async sendUsageUpdate(sessionId, directory, turnError) {
22032
22340
  const messages = await this.sdk.session.messages({ sessionID: sessionId, directory }).then((x) => x.data).catch(() => void 0);
22033
22341
  if (!messages) return;
22034
22342
  const assistantMessages = messages.filter(
@@ -22059,6 +22367,9 @@ var Agent = class {
22059
22367
  }
22060
22368
  this.aggregateFileDiffStats(sessionId, children, _meta);
22061
22369
  this.aggregateAICodeChangeStats(sessionId, children, _meta);
22370
+ if (turnError) {
22371
+ _meta.error = turnError;
22372
+ }
22062
22373
  await sendToClient(this.connection, {
22063
22374
  sessionId,
22064
22375
  update: {
@@ -22202,13 +22513,15 @@ var Agent = class {
22202
22513
  }
22203
22514
  }
22204
22515
  buildUsage(msg) {
22516
+ const tokens = msg.tokens;
22517
+ if (!tokens) return void 0;
22205
22518
  return {
22206
- totalTokens: msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + (msg.tokens.cache?.read ?? 0) + (msg.tokens.cache?.write ?? 0),
22207
- inputTokens: msg.tokens.input,
22208
- outputTokens: msg.tokens.output,
22209
- thoughtTokens: msg.tokens.reasoning || void 0,
22210
- cachedReadTokens: msg.tokens.cache?.read || void 0,
22211
- 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
22212
22525
  };
22213
22526
  }
22214
22527
  };