@khorsheed/dsh-local-agent-claude-code 0.2.0-rc.3 → 0.2.1

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/lib/index.js CHANGED
@@ -192,6 +192,20 @@ function servedEfforts(effort) {
192
192
  return levels.length === 0 ? void 0 : levels;
193
193
  }
194
194
  /**
195
+ * The effort block's home in a served-model entry. Anthropic's current
196
+ * /v1/models shape nests it under `capabilities.effort` (verified against the
197
+ * openproxy relay 2026-10-22: `capabilities.effort.<level>.supported`); the
198
+ * flat top-level `effort` the parser was written against still wins when
199
+ * present, so either shape answers.
200
+ */
201
+ function servedEffortsOf(record) {
202
+ const direct = servedEfforts(record["effort"]);
203
+ if (direct !== void 0) return direct;
204
+ const capabilities = record["capabilities"];
205
+ if (typeof capabilities !== "object" || capabilities === null) return void 0;
206
+ return servedEfforts(capabilities["effort"]);
207
+ }
208
+ /**
195
209
  * `GET <baseUrl>/v1/models` with the resolved credential's auth header(s) —
196
210
  * the token-free endpoint probe shared by the `/claude-code check` verdict
197
211
  * and the model catalog. Anthropic's own API serves this route with
@@ -237,7 +251,7 @@ async function fetchEndpointModels(endpoint, fetchImpl, signal) {
237
251
  if (typeof entry !== "object" || entry === null) continue;
238
252
  const record = entry;
239
253
  if (typeof record.id !== "string" || record.id === "") continue;
240
- const efforts = servedEfforts(record.effort);
254
+ const efforts = servedEffortsOf(record);
241
255
  models.push({
242
256
  id: record.id,
243
257
  ...typeof record.display_name === "string" && record.display_name !== "" ? { displayName: record.display_name } : {},
@@ -528,8 +542,9 @@ var ClaudeControlRequests = class {
528
542
  * Fold: the resident stream speaks the exec path's exact event vocabulary
529
543
  * (`system`/`assistant`/`user`/`result`), so each turn folds through the
530
544
  * shared `ClaudeStreamParser` with the exec live mirror's hold-back rule (the
531
- * volatile last line waits for `result`, which carries the round's usage).
532
- * Every line folds 1:1. The always-enabled
545
+ * volatile last line waits for `result`, which carries the round's usage, and
546
+ * an unresulted tool line holds the fold until its result merges in place —
547
+ * the native lifecycle folds as one closed step). Every line folds 1:1. The always-enabled
533
548
  * `--include-partial-messages` adds incremental output on top: each `stream_event` partial accumulates into its kind's
534
549
  * stream (native message id + block index, with ordered legacy fallback),
535
550
  * which reserves a (turn, step) at its first delta and
@@ -570,11 +585,6 @@ const DEFAULT_LIVE_CHANNEL_RETRY_MS = 3e5;
570
585
  const RECLAIM_EOF_GRACE_MS = 1e3;
571
586
  /** Default minimum interval between one streaming kind's snapshot messages. */
572
587
  const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
573
- /**
574
- * The stream-json channel could not come up (spawn failure or the first
575
- * turn's init never arrived). The provider catches exactly this and falls
576
- * back to the exec one-shot; any other error is a real round failure.
577
- */
578
588
  var LiveChannelUnavailableError = class extends Error {
579
589
  constructor(message) {
580
590
  super(message);
@@ -927,6 +937,7 @@ var ClaudeLiveDriver = class {
927
937
  graceMs: DEFAULT_DISPOSE_GRACE_MS,
928
938
  env: delegationEnv({
929
939
  CLAUDE_CONFIG_DIR: spec.homeDir,
940
+ ...member === void 0 ? {} : { MCP_TOOL_TIMEOUT: MEMBER_TOOL_TIMEOUT_MS },
930
941
  ...spec.configuration === void 0 ? {} : { CLAUDE_CODE_EFFORT_LEVEL: spec.configuration.effort ?? "auto" },
931
942
  ...endpointAuthEnv(endpoint)
932
943
  })
@@ -1022,27 +1033,22 @@ var ClaudeLiveDriver = class {
1022
1033
  */
1023
1034
  let roundModel;
1024
1035
  let roundCliVersion;
1025
- /** Whether a folded line already carries the round's usage (no backfill event exists). */
1036
+ /** Whether a folded line already carried usage (per-call inline, or the round's total as the fallback — no backfill event exists). */
1026
1037
  let usageCarried = false;
1027
1038
  /**
1028
- * The step ledger: `reservedSteps` holds every step a stream reservation
1029
- * consumed (in increasing order — permanently, whether the stream later
1030
- * completes or stays an orphan snapshot), and a sequential line folds at
1031
- * its index shifted past every reservation before it. A tool completed
1032
- * mid-stream therefore folds ABOVE the stream's step and the projection
1033
- * renders the round's chronological order.
1034
- */
1035
- const reservedSteps = [];
1036
- /**
1037
- * The streaming kinds seen this round by stream key:
1038
- * deltas accumulate into throttled snapshot assistant/messages appended
1039
- * at the kind's reserved (turn, step) — the host folds repeated settles
1040
- * at one coordinate into one live-updating chat node, which is the only
1041
- * streaming channel left after 0.1.5 retired the durable per-chunk event.
1042
- * The kind's completed line folds at the same step and finalizes it; an
1043
- * entry whose completion never arrives is force-finalized at settle.
1039
+ * The streaming blocks seen this round by stream key: deltas accumulate
1040
+ * into throttled snapshots at the block's step once it may open in the
1041
+ * log (previewed live before that) — the host folds repeated settles at
1042
+ * one coordinate into one live-updating chat node. The block's completed
1043
+ * line folds at the same step and finalizes it; an entry whose
1044
+ * completion never arrives is force-finalized at settle. A tool that
1045
+ * completes mid-stream folds after the stream (chronological order).
1044
1046
  */
1045
1047
  const streams = /* @__PURE__ */ new Map();
1048
+ /** Provisional stream keys (live flush and preview identity), never log steps. */
1049
+ let streamKeys = 0;
1050
+ /** Line indexes already written (a line may fold ahead of one it chronologically precedes). */
1051
+ const folded = /* @__PURE__ */ new Set();
1046
1052
  /** The stream deltas currently accumulate into (kinds stream sequentially). */
1047
1053
  let activeStream;
1048
1054
  let persistQueue = Promise.resolve();
@@ -1072,6 +1078,34 @@ var ClaudeLiveDriver = class {
1072
1078
  };
1073
1079
  let messageKey;
1074
1080
  let legacyMessage = 0;
1081
+ const callUsage = /* @__PURE__ */ new Map();
1082
+ const openCalls = /* @__PURE__ */ new Set();
1083
+ let currentCall;
1084
+ const endCall = (usage) => {
1085
+ if (currentCall === void 0) return;
1086
+ if (typeof usage === "object" && usage !== null) {
1087
+ const merged = { ...callUsage.get(currentCall) };
1088
+ for (const [key, value] of Object.entries(usage)) if (value !== null && value !== void 0) merged[key] = value;
1089
+ callUsage.set(currentCall, merged);
1090
+ }
1091
+ openCalls.delete(currentCall);
1092
+ };
1093
+ /**
1094
+ * A line's usage when its call is known: `skip` on every line but the
1095
+ * call's last, the call's final usage (or, without stream events, the
1096
+ * line's own report) on the last; `null` for an id-less line.
1097
+ */
1098
+ const callLineUsage = (index) => {
1099
+ const id = parser.lines[index]?.messageId;
1100
+ if (id === void 0) return null;
1101
+ let last = -1;
1102
+ parser.lines.forEach((line, position) => {
1103
+ if (line.messageId === id) last = position;
1104
+ });
1105
+ if (index !== last) return { skip: true };
1106
+ const raw = callUsage.get(id);
1107
+ return { usage: raw !== void 0 ? usageFromClaude(raw) : parser.lines[index].usage };
1108
+ };
1075
1109
  const streamKey = (kind, index) => `${messageKey ?? `legacy-${turn}-${legacyMessage}`}:${typeof index === "number" ? index : kind}`;
1076
1110
  /**
1077
1111
  * Append one snapshot of a streaming kind at its reserved (turn, step).
@@ -1082,6 +1116,27 @@ var ClaudeLiveDriver = class {
1082
1116
  */
1083
1117
  const streamPublisher = localAgent?.liveStreams === void 0 ? void 0 : new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, (error) => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
1084
1118
  const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
1119
+ let lastStep = 0;
1120
+ let openStep;
1121
+ const openStreamStep = (stream) => {
1122
+ lastStep += 1;
1123
+ childSession.append("step/start", {
1124
+ turn,
1125
+ step: lastStep
1126
+ });
1127
+ stream.durable = lastStep;
1128
+ stream.opened = true;
1129
+ openStep = lastStep;
1130
+ };
1131
+ const closeOpenStep = () => {
1132
+ if (openStep === void 0) return;
1133
+ childSession.append("step/end", {
1134
+ turn,
1135
+ step: openStep
1136
+ });
1137
+ openStep = void 0;
1138
+ };
1139
+ const canOpenStream = (stream) => openStep === void 0 && mirrored >= stream.after && streams.keys().next().value === stream.itemId;
1085
1140
  const pendingStreamArrival = /* @__PURE__ */ new Map();
1086
1141
  const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
1087
1142
  if (stream.text.trim() === "") return;
@@ -1094,30 +1149,30 @@ var ClaudeLiveDriver = class {
1094
1149
  liveFlush.cancel(stream.step);
1095
1150
  const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
1096
1151
  pendingStreamArrival.delete(stream.step);
1097
- if (streamPublisher !== void 0 && !final) {
1098
- if (!stream.opened) {
1099
- childSession.append("step/start", {
1100
- turn,
1101
- step: stream.step
1152
+ if (!stream.opened) {
1153
+ if (canOpenStream(stream)) openStreamStep(stream);
1154
+ else if (!final) {
1155
+ streamPublisher?.preview(stream.step, stream.kind, stream.text, {
1156
+ itemId: stream.itemId,
1157
+ receivedAt
1102
1158
  });
1103
- stream.opened = true;
1159
+ return;
1160
+ } else {
1161
+ closeOpenStep();
1162
+ openStreamStep(stream);
1104
1163
  }
1105
- streamPublisher.update(stream.step, stream.kind, stream.text, { receivedAt });
1164
+ }
1165
+ if (streamPublisher !== void 0 && !final) {
1166
+ streamPublisher.update(stream.durable, stream.kind, stream.text, { receivedAt });
1106
1167
  stream.lastSnapshotAt = now;
1107
1168
  stream.lastSnapshotLen = stream.text.length;
1108
1169
  return;
1109
1170
  }
1110
- if (!stream.opened) {
1111
- childSession.append("step/start", {
1112
- turn,
1113
- step: stream.step
1114
- });
1115
- stream.opened = true;
1116
- }
1117
1171
  const usage = parser.usage;
1172
+ const totalFallback = withUsage && !usageCarried && usage !== void 0 && !parser.lines.some((line) => line.usage !== void 0);
1118
1173
  childSession.append("assistant/message", {
1119
1174
  turn,
1120
- step: stream.step,
1175
+ step: stream.durable,
1121
1176
  message: assistantEvent([stream.kind === "think" ? {
1122
1177
  type: "reasoning",
1123
1178
  text: stream.text
@@ -1126,19 +1181,26 @@ var ClaudeLiveDriver = class {
1126
1181
  text: stream.text
1127
1182
  }]),
1128
1183
  stream: [],
1129
- ...withUsage && !usageCarried && usage !== void 0 ? { usage } : {},
1184
+ ...totalFallback ? { usage } : {},
1130
1185
  ...interrupted ? { interrupted: true } : {}
1131
1186
  }, { surfaceOp: "append" });
1132
- if (withUsage && !usageCarried && usage !== void 0) usageCarried = true;
1187
+ if (totalFallback) usageCarried = true;
1133
1188
  stream.lastSnapshotAt = now;
1134
1189
  stream.lastSnapshotLen = stream.text.length;
1135
1190
  persist();
1136
1191
  };
1192
+ /** Close a stream's live presentation once its final text is in the log. */
1193
+ const finishStream = (stream) => {
1194
+ liveFlush.cancel(stream.step);
1195
+ streams.delete(stream.itemId);
1196
+ if (activeStream === stream.itemId) activeStream = void 0;
1197
+ streamPublisher?.finish(stream.step);
1198
+ if (stream.durable !== void 0) streamPublisher?.finish(stream.durable);
1199
+ };
1137
1200
  /**
1138
- * Reserve the step one streaming kind will occupy — past every completed
1139
- * line (folded or held back) and every earlier reservation. A different
1140
- * kind's deltas force one final snapshot of the current stream (its
1141
- * completion still folds at its own reserved step when it lands).
1201
+ * Track one new streaming block: its provisional key and how many lines
1202
+ * had completed when it began. A different block's deltas force one
1203
+ * freshness snapshot of the current stream.
1142
1204
  */
1143
1205
  const reserveStream = (itemId, kind) => {
1144
1206
  if (activeStream !== void 0 && activeStream !== itemId) {
@@ -1148,12 +1210,12 @@ var ClaudeLiveDriver = class {
1148
1210
  }
1149
1211
  let stream = streams.get(itemId);
1150
1212
  if (stream === void 0) {
1151
- const step = parser.lines.length + reservedSteps.length + 1;
1152
- reservedSteps.push(step);
1213
+ streamKeys += 1;
1153
1214
  stream = {
1154
1215
  itemId,
1155
1216
  kind,
1156
- step,
1217
+ step: streamKeys,
1218
+ after: parser.lines.length,
1157
1219
  text: "",
1158
1220
  lastSnapshotAt: 0,
1159
1221
  lastSnapshotLen: 0,
@@ -1163,60 +1225,90 @@ var ClaudeLiveDriver = class {
1163
1225
  }
1164
1226
  activeStream = itemId;
1165
1227
  };
1166
- /** The sequential fold step for `parser.lines[index]`: its position shifted past every reservation before it. */
1167
- const foldStep = (index) => {
1168
- let step = index + 1;
1169
- for (const reserved of reservedSteps) if (reserved <= step) step += 1;
1170
- else break;
1171
- return step;
1228
+ /** The stream a completed line finalizes, if it was streamed. */
1229
+ const streamOfLine = (line) => line === void 0 || line.kind === "tool" ? void 0 : (line.streamId === void 0 ? void 0 : streams.get(line.streamId)) ?? [...streams.values()].find((candidate) => candidate.kind === line.kind && (line.streamId === void 0 || candidate.itemId.startsWith("legacy-")));
1230
+ /** Write parser.lines[index] at the next step(s). */
1231
+ const foldLine = (index, lineUsage) => {
1232
+ const line = parser.lines[index];
1233
+ const perCall = callLineUsage(index);
1234
+ const effectiveUsage = perCall === null ? line.usage ?? lineUsage : "skip" in perCall ? void 0 : perCall.usage ?? lineUsage;
1235
+ if (effectiveUsage !== void 0) usageCarried = true;
1236
+ const stream = streamOfLine(line);
1237
+ if (stream !== void 0) {
1238
+ if (!stream.opened) {
1239
+ closeOpenStep();
1240
+ openStreamStep(stream);
1241
+ }
1242
+ const blocks = stream.kind === "think" ? [{
1243
+ type: "reasoning",
1244
+ text: line.kind === "tool" ? "" : line.text
1245
+ }] : [{
1246
+ type: "text",
1247
+ text: line.kind === "tool" ? "" : line.text
1248
+ }];
1249
+ childSession.append("assistant/message", {
1250
+ turn,
1251
+ step: stream.durable,
1252
+ message: assistantEvent(blocks),
1253
+ stream: [],
1254
+ ...effectiveUsage === void 0 ? {} : { usage: effectiveUsage }
1255
+ }, { surfaceOp: "append" });
1256
+ finishStream(stream);
1257
+ closeOpenStep();
1258
+ } else {
1259
+ closeOpenStep();
1260
+ lastStep += 1;
1261
+ const { usage: _snapshot, ...bare } = line;
1262
+ appendClaudeTranscriptLine(childSession, turn, lastStep, bare, effectiveUsage);
1263
+ }
1264
+ folded.add(index);
1265
+ localAgent?.reportRunProgress(childSession.id, {
1266
+ kind: "delta",
1267
+ text: claudeLineText(line)
1268
+ });
1172
1269
  };
1173
- /** Mirror folded lines [mirrored, upto); the last line is held back until `result`. */
1174
- const mirrorUpTo = (upto, withUsage) => {
1270
+ /**
1271
+ * Mirror the completed lines before `upto` in chronological order; the
1272
+ * last line is held back until `result`. A line waits behind every stream
1273
+ * that began before it completed (that stream's line folds first, or the
1274
+ * mirror stops while it still streams). A tool line whose result the
1275
+ * stream has not recorded yet HOLDS the fold mid-round: the fold writes
1276
+ * the native lifecycle as one closed step (the released-log reader
1277
+ * resolves every advertised call before step/end), and the result merges
1278
+ * into the call's line in place, so a later pass retries from here. The
1279
+ * settle flushes (`settle: true` — the turn's result event, or the final
1280
+ * flush after a cancel/failure) never hold: a result that will never
1281
+ * arrive folds as the interrupted placeholder.
1282
+ */
1283
+ const mirrorUpTo = (upto, withUsage, settle = false) => {
1175
1284
  let usageIndex = -1;
1176
- if (withUsage) {
1285
+ if (withUsage && !parser.lines.some((line) => line.usage !== void 0)) {
1177
1286
  for (let index = 0; index < parser.lines.length; index += 1) if (parser.lines[index]?.kind !== "tool") usageIndex = index;
1178
1287
  }
1179
- for (let index = mirrored; index < upto; index += 1) {
1180
- const line = parser.lines[index];
1181
- if (line === void 0) continue;
1288
+ const usageFor = (index) => {
1182
1289
  const lineUsage = withUsage && index === usageIndex ? parser.usage : void 0;
1183
1290
  if (lineUsage !== void 0) usageCarried = true;
1184
- const stream = line.kind === "tool" ? void 0 : (line.streamId === void 0 ? void 0 : streams.get(line.streamId)) ?? [...streams.values()].find((candidate) => candidate.kind === line.kind && (line.streamId === void 0 || candidate.itemId.startsWith("legacy-")));
1185
- const itemId = stream?.itemId;
1186
- if (itemId !== void 0 && stream !== void 0) {
1187
- liveFlush.cancel(stream.step);
1188
- streams.delete(itemId);
1189
- if (activeStream === itemId) activeStream = void 0;
1190
- if (!stream.opened) childSession.append("step/start", {
1191
- turn,
1192
- step: stream.step
1193
- });
1194
- const blocks = stream.kind === "think" ? [{
1195
- type: "reasoning",
1196
- text: line.kind === "tool" ? "" : line.text
1197
- }] : [{
1198
- type: "text",
1199
- text: line.kind === "tool" ? "" : line.text
1200
- }];
1201
- childSession.append("assistant/message", {
1202
- turn,
1203
- step: stream.step,
1204
- message: assistantEvent(blocks),
1205
- stream: [],
1206
- ...lineUsage === void 0 ? {} : { usage: lineUsage }
1207
- }, { surfaceOp: "append" });
1208
- streamPublisher?.finish(stream.step);
1209
- childSession.append("step/end", {
1210
- turn,
1211
- step: stream.step
1212
- });
1213
- } else appendClaudeTranscriptLine(childSession, turn, foldStep(index), line, lineUsage);
1214
- mirrored = index + 1;
1215
- localAgent?.reportRunProgress(childSession.id, {
1216
- kind: "delta",
1217
- text: claudeLineText(line)
1218
- });
1291
+ return lineUsage;
1292
+ };
1293
+ outer: for (let index = mirrored; index < upto; index += 1) {
1294
+ if (folded.has(index)) continue;
1295
+ const line = parser.lines[index];
1296
+ if (line === void 0) continue;
1297
+ if (!settle && line.kind === "tool" && line.result === void 0) break;
1298
+ if (!settle && line.messageId !== void 0 && openCalls.has(line.messageId)) break;
1299
+ const own = streamOfLine(line);
1300
+ const all = [...streams.values()];
1301
+ const before = own === void 0 ? all.filter((stream) => stream.after <= index) : all.slice(0, all.indexOf(own));
1302
+ for (const earlier of before) {
1303
+ const at = parser.lines.findIndex((candidate, position) => position > index && position < upto && !folded.has(position) && streamOfLine(candidate) === earlier);
1304
+ if (at < 0) break outer;
1305
+ const atCall = parser.lines[at]?.messageId;
1306
+ if (!settle && atCall !== void 0 && openCalls.has(atCall)) break outer;
1307
+ foldLine(at, usageFor(at));
1308
+ }
1309
+ foldLine(index, usageFor(index));
1219
1310
  }
1311
+ while (folded.has(mirrored)) mirrored += 1;
1220
1312
  if (upto > 0) persist();
1221
1313
  };
1222
1314
  let resolveResult;
@@ -1232,6 +1324,13 @@ var ClaudeLiveDriver = class {
1232
1324
  /** The round's event sink (installed on the runtime while it runs). */
1233
1325
  const onEvent = (event) => {
1234
1326
  const type = event["type"];
1327
+ if (roundSettled) {
1328
+ if (type === "result") {
1329
+ awaitingResult = false;
1330
+ resolveResult(event);
1331
+ }
1332
+ return;
1333
+ }
1235
1334
  if (type === "system" && typeof event["session_id"] === "string") {
1236
1335
  if (runtime !== void 0 && runtime.sessionId === void 0) {
1237
1336
  runtime.sessionId = event["session_id"];
@@ -1244,7 +1343,7 @@ var ClaudeLiveDriver = class {
1244
1343
  if (type === "result") {
1245
1344
  awaitingResult = false;
1246
1345
  parser.push(JSON.stringify(event) + "\n");
1247
- mirrorUpTo(parser.lines.length, true);
1346
+ mirrorUpTo(parser.lines.length, true, true);
1248
1347
  resolveResult(event);
1249
1348
  return;
1250
1349
  }
@@ -1253,6 +1352,17 @@ var ClaudeLiveDriver = class {
1253
1352
  if (inner?.["type"] === "message_start") {
1254
1353
  const message = inner["message"];
1255
1354
  messageKey = typeof message?.["id"] === "string" ? message["id"] : `legacy-${turn}-${++legacyMessage}`;
1355
+ currentCall = typeof message?.["id"] === "string" ? message["id"] : void 0;
1356
+ if (currentCall !== void 0) {
1357
+ openCalls.add(currentCall);
1358
+ const usage = message?.["usage"];
1359
+ if (typeof usage === "object" && usage !== null) callUsage.set(currentCall, { ...usage });
1360
+ }
1361
+ return;
1362
+ }
1363
+ if (inner?.["type"] === "message_delta" || inner?.["type"] === "message_stop") {
1364
+ endCall(inner["usage"]);
1365
+ mirrorUpTo(parser.lines.length - 1, false);
1256
1366
  return;
1257
1367
  }
1258
1368
  const delta = inner?.["delta"];
@@ -1328,6 +1438,11 @@ var ClaudeLiveDriver = class {
1328
1438
  throw new Error("subagent-claude: run cancelled locally");
1329
1439
  }
1330
1440
  childSession.append("turn/start", { turn });
1441
+ if (spec.configuration?.model !== void 0) appendClaudeRequestHeader(childSession, {
1442
+ provider: "claude-local",
1443
+ model: spec.configuration.model,
1444
+ reasoningEffort: spec.configuration.effort ?? "auto"
1445
+ });
1331
1446
  childSession.append("user/message", createUserMessage({
1332
1447
  content: [{
1333
1448
  type: "text",
@@ -1379,16 +1494,20 @@ var ClaudeLiveDriver = class {
1379
1494
  liveFlush.dispose();
1380
1495
  turnInFlight = false;
1381
1496
  if (turnOpened) {
1382
- if (mirrored < parser.lines.length) mirrorUpTo(parser.lines.length, true);
1497
+ if (mirrored < parser.lines.length) mirrorUpTo(parser.lines.length, true, true);
1498
+ while (mirrored < parser.lines.length && streams.size > 0) {
1499
+ const oldest = streams.values().next().value;
1500
+ appendStreamSnapshot(oldest, true, settled.stopReason !== "completed", false, true);
1501
+ finishStream(oldest);
1502
+ closeOpenStep();
1503
+ mirrorUpTo(parser.lines.length, true, true);
1504
+ }
1383
1505
  if (streams.size > 0) {
1384
1506
  const remaining = [...streams.values()];
1385
1507
  for (const [position, stream] of remaining.entries()) {
1386
1508
  appendStreamSnapshot(stream, true, settled.stopReason !== "completed", position === remaining.length - 1, true);
1387
- streamPublisher?.finish(stream.step);
1388
- if (stream.opened) childSession.append("step/end", {
1389
- turn,
1390
- step: stream.step
1391
- });
1509
+ finishStream(stream);
1510
+ closeOpenStep();
1392
1511
  }
1393
1512
  streams.clear();
1394
1513
  activeStream = void 0;
@@ -1460,24 +1579,6 @@ var ClaudeLiveDriver = class {
1460
1579
  });
1461
1580
  }
1462
1581
  };
1463
- //#endregion
1464
- //#region lib/types/claude-cli-provider.js
1465
- /**
1466
- * One-shot Claude Code CLI subagent lifecycle: spawn `claude -p "<task>"`
1467
- * through the subprocess seam under the harness's scoped home, capture its
1468
- * JSON result as the run output, and dispose to whole-tree quiescence.
1469
- * Mirrors the kimi/codex providers: every accepted run is a fresh process
1470
- * and a fresh claude session; there is no continuation across runs.
1471
- *
1472
- * The provider name `claude-local` avoids colliding with the official
1473
- * `subagent-claude-code` package's provider name `claude-code` — a
1474
- * composition that mounts both would fail loud with DUPLICATE_PROVIDER. The
1475
- * tool row in this bundle's patch takes the official model-facing name
1476
- * `subagent_claude_code` instead: the official preset row ships disabled,
1477
- * and the patch disables it too (a deliberate user re-enable conflicts
1478
- * loud, by design).
1479
- * @module @khorsheed/dsh-local-agent-claude-code/claude-cli-provider
1480
- */
1481
1582
  /** Default POSIX grace between subprocess termination tiers. */
1482
1583
  const DEFAULT_DISPOSE_GRACE_MS = 3e3;
1483
1584
  /**
@@ -1489,6 +1590,11 @@ const DEFAULT_DISPOSE_GRACE_MS = 3e3;
1489
1590
  * provider-controlled error strings only.
1490
1591
  */
1491
1592
  const CLAUDE_AUTH_FAILURE = /failed to authenticate|authentication_failed|oauth access token|invalid (x-api-key|api key|authentication token)|api error: 40[13]/i;
1593
+ /**
1594
+ * The MCP tool-call bound for a member's claude process (`MCP_TOOL_TIMEOUT`,
1595
+ * ms): a bridge call may wait up to 30 minutes for the human's answer.
1596
+ */
1597
+ const MEMBER_TOOL_TIMEOUT_MS = String(18e5);
1492
1598
  /** Register one member run with the channel; see {@link ClaudeMemberRunHandle}. */
1493
1599
  function registerClaudeMemberRun(ctx, childSessionId, parentSessionId) {
1494
1600
  const registry = ctx.localAgent;
@@ -1707,6 +1813,7 @@ var ClaudeCliProvider = class {
1707
1813
  cwd,
1708
1814
  env: delegationEnv({
1709
1815
  CLAUDE_CONFIG_DIR: homeDir,
1816
+ ...member === void 0 ? {} : { MCP_TOOL_TIMEOUT: MEMBER_TOOL_TIMEOUT_MS },
1710
1817
  ...endpointAuthEnv(endpoint)
1711
1818
  }),
1712
1819
  ...exec === void 0 ? {} : { exec },
@@ -1796,6 +1903,7 @@ var ClaudeCliProvider = class {
1796
1903
  cwd,
1797
1904
  env: delegationEnv({
1798
1905
  CLAUDE_CONFIG_DIR: homeDir,
1906
+ ...member === void 0 ? {} : { MCP_TOOL_TIMEOUT: MEMBER_TOOL_TIMEOUT_MS },
1799
1907
  ...endpointAuthEnv(endpoint)
1800
1908
  }),
1801
1909
  ...exec === void 0 ? {} : { exec },
@@ -1866,6 +1974,17 @@ function textTask(prompt) {
1866
1974
  if (texts.every((text) => text.trim().length === 0)) throw new Error("subagent-claude: the one-shot task must not be empty");
1867
1975
  return texts.join("\n");
1868
1976
  }
1977
+ /** Group the lines of one API call (consecutive lines sharing a message id).
1978
+ * Lines without an id stay singleton units (the pre-grouping shape). */
1979
+ function groupClaudeTranscriptLines(lines) {
1980
+ const groups = [];
1981
+ for (const line of lines) {
1982
+ const last = groups[groups.length - 1];
1983
+ if (line.messageId !== void 0 && last !== void 0 && last[0]?.messageId === line.messageId) last.push(line);
1984
+ else groups.push([line]);
1985
+ }
1986
+ return groups;
1987
+ }
1869
1988
  /**
1870
1989
  * Fold a tool-call counter into the settled shape: `{count, byName}` with the
1871
1990
  * per-name values summing to `count`. An empty map yields undefined — a
@@ -1978,11 +2097,17 @@ function foldClaudeStreamLine(state, raw) {
1978
2097
  }
1979
2098
  if (event.type !== "assistant" && event.type !== "user") return;
1980
2099
  const blocks = event.message?.content ?? [];
2100
+ const linesBefore = state.lines.length;
2101
+ const messageId = event.type === "assistant" && typeof event.message?.id === "string" ? event.message.id : void 0;
1981
2102
  for (const [blockIndex, block] of blocks.entries()) {
1982
2103
  if (typeof block !== "object" || block === null) continue;
1983
2104
  const record = block;
1984
- const identity = event.type === "assistant" && typeof event.message?.id === "string" ? { streamId: `${event.message.id}:${blockIndex}` } : {};
2105
+ const identity = messageId !== void 0 ? {
2106
+ streamId: `${messageId}:${blockIndex}`,
2107
+ messageId
2108
+ } : {};
1985
2109
  const kind = record["type"];
2110
+ if (kind === "text" && event.type === "user") continue;
1986
2111
  if (kind === "text" && typeof record["text"] === "string" && record["text"].trim() !== "") {
1987
2112
  state.lines.push({
1988
2113
  kind: "text",
@@ -2019,6 +2144,7 @@ function foldClaudeStreamLine(state, raw) {
2019
2144
  id: id ?? `claude-tool-${state.lines.length}`,
2020
2145
  name: "ApplyPatch",
2021
2146
  args: edit.args,
2147
+ ...messageId === void 0 ? {} : { messageId },
2022
2148
  ...edit.patch === void 0 ? {} : { result: edit.patch }
2023
2149
  });
2024
2150
  else {
@@ -2027,6 +2153,7 @@ function foldClaudeStreamLine(state, raw) {
2027
2153
  kind: "tool",
2028
2154
  id: id ?? `claude-tool-${state.lines.length}`,
2029
2155
  name: kind === "server_tool_use" ? SERVER_TOOL_CARD_NAMES[name] ?? name : name,
2156
+ ...messageId === void 0 ? {} : { messageId },
2030
2157
  ...detail === void 0 ? {} : { args: detail }
2031
2158
  });
2032
2159
  }
@@ -2038,6 +2165,22 @@ function foldClaudeStreamLine(state, raw) {
2038
2165
  if (resultText !== void 0 && resultText.trim() !== "") attachToolResult(state, record["tool_use_id"], resultText, false);
2039
2166
  }
2040
2167
  }
2168
+ if (event.type === "assistant" && event.message?.usage !== void 0 && state.lines.length > linesBefore) {
2169
+ const lastIndex = state.lines.length - 1;
2170
+ const last = state.lines[lastIndex];
2171
+ if (messageId !== void 0) {
2172
+ const previous = state.usageStamped.get(messageId);
2173
+ if (previous !== void 0 && previous !== lastIndex) {
2174
+ const { usage: _cleared, ...rest } = state.lines[previous];
2175
+ state.lines[previous] = rest;
2176
+ }
2177
+ }
2178
+ state.lines[lastIndex] = {
2179
+ ...last,
2180
+ usage: usageFromClaude(event.message.usage)
2181
+ };
2182
+ if (messageId !== void 0) state.usageStamped.set(messageId, lastIndex);
2183
+ }
2041
2184
  }
2042
2185
  /**
2043
2186
  * Merge a tool result into its call line: pair by tool_use_id when the stream
@@ -2077,6 +2220,7 @@ var ClaudeStreamParser = class {
2077
2220
  buffer = "";
2078
2221
  lines = [];
2079
2222
  callsById = /* @__PURE__ */ new Map();
2223
+ usageStamped = /* @__PURE__ */ new Map();
2080
2224
  text;
2081
2225
  usage;
2082
2226
  sessionId;
@@ -2113,6 +2257,7 @@ function parseClaudeStreamJson(output) {
2113
2257
  const state = {
2114
2258
  lines: [],
2115
2259
  callsById: /* @__PURE__ */ new Map(),
2260
+ usageStamped: /* @__PURE__ */ new Map(),
2116
2261
  toolCalls: /* @__PURE__ */ new Map(),
2117
2262
  text: void 0,
2118
2263
  usage: void 0,
@@ -2259,16 +2404,30 @@ function serverToolResultText(record) {
2259
2404
  return [url, body === void 0 ? void 0 : body.length <= WEB_FETCH_TEXT_LIMIT ? body : `${body.slice(0, WEB_FETCH_TEXT_LIMIT)}\n…`].filter((part) => part !== void 0 && part !== "").join("\n") || void 0;
2260
2405
  }
2261
2406
  }
2262
- /** Flatten a tool_result content payload (string or block array) to text. */
2407
+ /**
2408
+ * Flatten a tool_result content payload (string or block array) to text.
2409
+ * Beyond `text` blocks, ToolSearch answers with `tool_reference` blocks — one
2410
+ * per discovered tool — which list as the found tool names; dropping them
2411
+ * stranded the call (no result recorded) and it settled as a phantom
2412
+ * interrupted failure.
2413
+ */
2263
2414
  function toolResultText(content) {
2264
2415
  if (typeof content === "string") return content;
2265
2416
  if (!Array.isArray(content)) return void 0;
2266
- return content.filter((block) => typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string").map((block) => block.text ?? "").join("\n");
2417
+ return content.map((block) => {
2418
+ if (typeof block !== "object" || block === null) return void 0;
2419
+ const record = block;
2420
+ if (record.type === "text" && typeof record.text === "string") return record.text;
2421
+ if (record.type === "tool_reference" && typeof record.tool_name === "string") return record.tool_name;
2422
+ }).filter((text) => text !== void 0).join("\n");
2267
2423
  }
2268
2424
  /**
2269
2425
  * Map claude's usage payload onto the shared usage contract. Claude's
2270
2426
  * `input_tokens` is the uncached input; cache reads and cache creation are
2271
2427
  * reported separately, so each maps to its own bucket with no subtraction.
2428
+ * When the payload carries ANY cache field, `cacheReadTokens` is always
2429
+ * written (0 when the read counter is absent or zero) — a reader can then
2430
+ * tell "no hits" from "not reported".
2272
2431
  * @param usage - the raw claude usage object.
2273
2432
  * @returns the shared usage record.
2274
2433
  */
@@ -2290,7 +2449,7 @@ function usageFromClaude(usage) {
2290
2449
  inputTokens: input ?? 0,
2291
2450
  outputTokens: output ?? 0
2292
2451
  };
2293
- if (cacheRead !== void 0 && cacheRead > 0) usageRecord.cacheReadTokens = cacheRead;
2452
+ if (cacheRead !== void 0 || cacheWrite !== void 0) usageRecord.cacheReadTokens = cacheRead ?? 0;
2294
2453
  if (cacheWrite !== void 0 && cacheWrite > 0) usageRecord.cacheWriteTokens = cacheWrite;
2295
2454
  return usageRecord;
2296
2455
  }
@@ -2449,41 +2608,47 @@ async function startClaudeCliRun(request, spec) {
2449
2608
  onAbort
2450
2609
  }).then((settled) => {
2451
2610
  if (settled.stopReason === "completed") spec.onAuthSuccess?.();
2452
- if (spec.childSession !== void 0) {
2453
- if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
2611
+ return settled;
2612
+ });
2613
+ result.then(() => child.done).then(async () => {
2614
+ const drainedStdout = child.collected.stdout?.readFrom(0).text || output;
2615
+ if (spec.onAuthFailure !== void 0) {
2616
+ const drainedStderr = child.collected.stderr?.readFrom(0).text || stderr;
2617
+ const authDetail = `${parseClaudeStreamJson(drainedStdout).error ?? ""}\n${drainedStderr}`;
2618
+ if (CLAUDE_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
2619
+ }
2620
+ await mirrorClaudeAfterExit(spec, task, turn, drainedStdout, liveMirror);
2621
+ const closeTurn = async () => {
2622
+ if (spec.childSession === void 0) return;
2623
+ const childSession = spec.childSession;
2624
+ const settled = await result;
2625
+ if (settled.stopReason === "completed") childSession.append("turn/end", {
2454
2626
  turn,
2455
2627
  reason: { kind: "completed" }
2456
2628
  });
2457
- else if (settled.stopReason === "aborted") spec.childSession.append("turn/end", {
2629
+ else if (settled.stopReason === "aborted") childSession.append("turn/end", {
2458
2630
  turn,
2459
2631
  reason: {
2460
2632
  kind: "aborted",
2461
2633
  reason: { kind: "parent" }
2462
2634
  }
2463
2635
  });
2464
- else {
2465
- const parsed = parseClaudeStreamJson(output);
2466
- spec.childSession.append("turn/end", {
2467
- turn,
2468
- reason: {
2469
- kind: "error",
2470
- error: {
2471
- message: parsed.error ?? `claude -p exited with code ${String(exitCode)}`,
2472
- code: "UNKNOWN"
2473
- }
2636
+ else childSession.append("turn/end", {
2637
+ turn,
2638
+ reason: {
2639
+ kind: "error",
2640
+ error: {
2641
+ message: parseClaudeStreamJson(drainedStdout).error ?? `claude -p exited with code ${String(exitCode)}`,
2642
+ code: "UNKNOWN"
2474
2643
  }
2475
- });
2476
- }
2477
- }
2478
- return settled;
2479
- });
2480
- result.then(() => child.done).then(() => {
2481
- if (spec.onAuthFailure !== void 0) {
2482
- const drainedStderr = child.collected.stderr?.readFrom(0).text || stderr;
2483
- const authDetail = `${parseClaudeStreamJson(child.collected.stdout?.readFrom(0).text || output).error ?? ""}\n${drainedStderr}`;
2484
- if (CLAUDE_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
2485
- }
2486
- return mirrorClaudeAfterExit(spec, task, turn, output, liveMirror);
2644
+ }
2645
+ });
2646
+ if (spec.ctx !== void 0) await persistChildSession(spec.ctx, childSession).catch((error) => {
2647
+ spec.ctx?.logger.warn(`subagent-claude: turn/end persistence failed: ${thrown(error).message}`);
2648
+ });
2649
+ };
2650
+ if (liveMirror === void 0) await closeTurn();
2651
+ else await liveMirror.enqueue(closeTurn);
2487
2652
  }, () => {});
2488
2653
  return Promise.resolve(subprocessRunHandle({
2489
2654
  id: SessionId(randomUUID()),
@@ -2495,6 +2660,22 @@ async function startClaudeCliRun(request, spec) {
2495
2660
  }));
2496
2661
  }
2497
2662
  /** One assistant-role message event, attributed to the claude route. */
2663
+ /**
2664
+ * Write the round's request/header when the config differs from the child
2665
+ * log's last one (the host logs changed snapshots, not per-call duplicates):
2666
+ * the first write is `initial`, a differing one `change`, an identical one
2667
+ * skipped. Context Lens reads the child log's config from these headers.
2668
+ */
2669
+ function appendClaudeRequestHeader(childSession, config) {
2670
+ const serialized = JSON.stringify(config);
2671
+ let last;
2672
+ for (const event of childSession.snapshotEvents()) if (event.type === "request/header") last = JSON.stringify(event.data.header?.config);
2673
+ if (last === serialized) return;
2674
+ childSession.append("request/header", {
2675
+ header: { config },
2676
+ reason: last === void 0 ? "initial" : "change"
2677
+ });
2678
+ }
2498
2679
  function assistantEvent(blocks) {
2499
2680
  return createAssistantMessage({
2500
2681
  content: blocks,
@@ -2512,30 +2693,53 @@ function assistantEvent(blocks) {
2512
2693
  * the real-time view renders nothing (only a full rebuild, which mints step
2513
2694
  * drafts from the explicit coordinates, recovers it). Tool cards escape that
2514
2695
  * fate only because their definition does not consult the step location.
2696
+ *
2697
+ * A tool line folds the native lifecycle inside its one step: an assistant
2698
+ * message advertising the tool-call block, the `tool/call`, and its
2699
+ * `tool/result` — the released-log reader pairs call and result against the
2700
+ * advertisement and resolves every advertised call before the step closes, so
2701
+ * a line whose result the stream has not recorded yet must NOT reach this
2702
+ * fold mid-round (the callers hold there; the result merges into the call's
2703
+ * line in place, so a later pass sees it). A line still unresulted at settle
2704
+ * folds with the interrupted placeholder ({@link CLAUDE_INTERRUPTED_TOOL_RESULT}).
2515
2705
  */
2516
2706
  function appendClaudeTranscriptLine(childSession, turn, step, line, usage) {
2707
+ const lineUsage = line.usage ?? usage;
2517
2708
  childSession.append("step/start", {
2518
2709
  turn,
2519
2710
  step
2520
2711
  });
2521
2712
  if (line.kind === "tool") {
2713
+ const args = line.args ?? "";
2714
+ childSession.append("assistant/message", {
2715
+ turn,
2716
+ step,
2717
+ message: assistantEvent([{
2718
+ type: "tool-call",
2719
+ id: line.id,
2720
+ name: line.name,
2721
+ arguments: args
2722
+ }]),
2723
+ stream: [],
2724
+ ...lineUsage === void 0 ? {} : { usage: lineUsage }
2725
+ }, { surfaceOp: "append" });
2522
2726
  const call = childSession.append("tool/call", {
2523
2727
  turn,
2524
2728
  step,
2525
2729
  callId: line.id,
2526
2730
  name: line.name,
2527
- arguments: line.args ?? ""
2731
+ arguments: args
2528
2732
  });
2529
- if (line.result !== void 0) childSession.append("tool/result", {
2733
+ childSession.append("tool/result", {
2530
2734
  turn,
2531
2735
  step,
2532
2736
  message: createToolResultMessage({
2533
2737
  callId: line.id,
2534
2738
  content: [{
2535
2739
  type: "text",
2536
- text: line.result
2740
+ text: line.result ?? "(工具调用被中断,未能记录返回结果)"
2537
2741
  }],
2538
- isError: false
2742
+ isError: line.result === void 0
2539
2743
  })
2540
2744
  }, {
2541
2745
  surfaceOp: "append",
@@ -2559,24 +2763,82 @@ function appendClaudeTranscriptLine(childSession, turn, step, line, usage) {
2559
2763
  step,
2560
2764
  message: assistantEvent(blocks),
2561
2765
  stream: [],
2562
- ...usage === void 0 ? {} : { usage }
2766
+ ...lineUsage === void 0 ? {} : { usage: lineUsage }
2563
2767
  }, { surfaceOp: "append" });
2564
2768
  childSession.append("step/end", {
2565
2769
  turn,
2566
2770
  step
2567
2771
  });
2568
2772
  }
2569
- /**
2570
- * Fold one transcript line into the run's child session as one assistant step.
2571
- */
2572
- function appendClaudeLine(spec, turn, step, line, usage) {
2573
- if (spec.childSession === void 0) return;
2574
- appendClaudeTranscriptLine(spec.childSession, turn, step, line, usage);
2575
- }
2576
2773
  /** The delta-progress text for one transcript line. */
2577
2774
  function claudeLineText(line) {
2578
2775
  return line.kind === "tool" ? `[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}${line.result !== void 0 ? ` → ${line.result}` : ""}` : line.text;
2579
2776
  }
2777
+ /**
2778
+ * Fold one API call (a message-id group) into the child session as ONE
2779
+ * assistant step: a single assistant/message carrying the call's reasoning,
2780
+ * text and tool-call blocks (the host's own mixed-content shape), then each
2781
+ * call's native lifecycle, then step/end. The call's usage rides the message
2782
+ * once — the group's own per-call stamp wins over the round-total fallback.
2783
+ */
2784
+ function appendClaudeTranscriptGroup(childSession, turn, step, group, usage) {
2785
+ const lineUsage = group.map((line) => line.usage).find((value) => value !== void 0) ?? usage;
2786
+ const blocks = [];
2787
+ for (const line of group) if (line.kind === "think") blocks.push({
2788
+ type: "reasoning",
2789
+ text: line.text
2790
+ });
2791
+ else if (line.kind === "text") blocks.push({
2792
+ type: "text",
2793
+ text: line.text
2794
+ });
2795
+ else blocks.push({
2796
+ type: "tool-call",
2797
+ id: line.id,
2798
+ name: line.name,
2799
+ arguments: line.args ?? ""
2800
+ });
2801
+ childSession.append("step/start", {
2802
+ turn,
2803
+ step
2804
+ });
2805
+ childSession.append("assistant/message", {
2806
+ turn,
2807
+ step,
2808
+ message: assistantEvent(blocks),
2809
+ stream: [],
2810
+ ...lineUsage === void 0 ? {} : { usage: lineUsage }
2811
+ }, { surfaceOp: "append" });
2812
+ for (const line of group) {
2813
+ if (line.kind !== "tool") continue;
2814
+ const call = childSession.append("tool/call", {
2815
+ turn,
2816
+ step,
2817
+ callId: line.id,
2818
+ name: line.name,
2819
+ arguments: line.args ?? ""
2820
+ });
2821
+ childSession.append("tool/result", {
2822
+ turn,
2823
+ step,
2824
+ message: createToolResultMessage({
2825
+ callId: line.id,
2826
+ content: [{
2827
+ type: "text",
2828
+ text: line.result ?? "(工具调用被中断,未能记录返回结果)"
2829
+ }],
2830
+ isError: line.result === void 0
2831
+ })
2832
+ }, {
2833
+ surfaceOp: "append",
2834
+ sourceEventSeqs: [call.seq]
2835
+ });
2836
+ }
2837
+ childSession.append("step/end", {
2838
+ turn,
2839
+ step
2840
+ });
2841
+ }
2580
2842
  /** Create the per-run live mirror over the incremental parser. */
2581
2843
  function createClaudeLiveMirror(spec, task, turn) {
2582
2844
  const parser = new ClaudeStreamParser();
@@ -2584,6 +2846,7 @@ function createClaudeLiveMirror(spec, task, turn) {
2584
2846
  const ctx = spec.ctx;
2585
2847
  let mirrored = 0;
2586
2848
  let userMirrored = false;
2849
+ let headerWritten = false;
2587
2850
  let lastTodos;
2588
2851
  let todoSkewWarned = false;
2589
2852
  let queue = Promise.resolve();
@@ -2595,7 +2858,7 @@ function createClaudeLiveMirror(spec, task, turn) {
2595
2858
  return userMirrored;
2596
2859
  },
2597
2860
  enqueue(task) {
2598
- queue = queue.then(task);
2861
+ queue = queue.catch(() => {}).then(task);
2599
2862
  return queue;
2600
2863
  },
2601
2864
  push(chunk) {
@@ -2608,8 +2871,22 @@ function createClaudeLiveMirror(spec, task, turn) {
2608
2871
  todoSkewWarned = true;
2609
2872
  ctx.logger.warn("subagent-claude: a TodoWrite call missed the documented input shape; folded as a plain tool line");
2610
2873
  }
2611
- const upto = parser.completed ? parser.lines.length : parser.lines.length - 1;
2612
- if (upto <= mirrored) return;
2874
+ const groups = groupClaudeTranscriptLines(parser.lines);
2875
+ const groupStarts = [];
2876
+ let totalLines = 0;
2877
+ for (const group of groups) {
2878
+ groupStarts.push(totalLines);
2879
+ totalLines += group.length;
2880
+ }
2881
+ let uptoGroups = parser.completed ? groups.length : groups.length - 1;
2882
+ if (!parser.completed) for (let scan = 0; scan < uptoGroups; scan += 1) {
2883
+ if (groupStarts[scan] < mirrored) continue;
2884
+ if (groups[scan].some((line) => line.kind === "tool" && line.result === void 0)) {
2885
+ uptoGroups = scan;
2886
+ break;
2887
+ }
2888
+ }
2889
+ if ((groupStarts[uptoGroups] ?? totalLines) <= mirrored && userMirrored) return;
2613
2890
  mirror.enqueue(async () => {
2614
2891
  try {
2615
2892
  const localAgent = ctx.get("localAgent");
@@ -2627,17 +2904,27 @@ function createClaudeLiveMirror(spec, task, turn) {
2627
2904
  text: task
2628
2905
  });
2629
2906
  }
2907
+ if (!headerWritten) {
2908
+ const observedModel = parser.model ?? spec.model;
2909
+ if (observedModel !== void 0) {
2910
+ headerWritten = true;
2911
+ appendClaudeRequestHeader(childSession, {
2912
+ provider: "claude-local",
2913
+ model: observedModel,
2914
+ ...spec.controlled ? { reasoningEffort: spec.effort ?? "auto" } : {}
2915
+ });
2916
+ }
2917
+ }
2630
2918
  let carrier = -1;
2631
- if (parser.completed && parser.usage !== void 0) {
2632
- for (let scan = 0; scan < parser.lines.length; scan += 1) if (parser.lines[scan]?.kind !== "tool") carrier = scan;
2919
+ if (parser.completed && parser.usage !== void 0 && !parser.lines.some((line) => line.usage !== void 0)) {
2920
+ for (let scan = 0; scan < groups.length; scan += 1) if (groups[scan].some((line) => line.kind !== "tool")) carrier = scan;
2633
2921
  }
2634
- for (let index = mirrored; index < upto; index += 1) {
2635
- const line = parser.lines[index];
2636
- if (line === void 0) continue;
2637
- const usage = index === carrier ? parser.usage : void 0;
2638
- appendClaudeLine(spec, turn, index + 1, line, usage);
2639
- mirrored = index + 1;
2640
- localAgent?.reportRunProgress(childSession.id, {
2922
+ for (let scan = 0; scan < uptoGroups; scan += 1) {
2923
+ if (groupStarts[scan] < mirrored) continue;
2924
+ const group = groups[scan];
2925
+ appendClaudeTranscriptGroup(childSession, turn, scan + 1, group, scan === carrier ? parser.usage : void 0);
2926
+ mirrored = groupStarts[scan] + group.length;
2927
+ for (const line of group) localAgent?.reportRunProgress(childSession.id, {
2641
2928
  kind: "delta",
2642
2929
  text: claudeLineText(line)
2643
2930
  });
@@ -2687,18 +2974,28 @@ async function appendClaudeResponse(spec, task, turn, parsed, fromLines = 0, use
2687
2974
  text: task
2688
2975
  });
2689
2976
  }
2690
- let usageIndex = -1;
2691
- for (let index = 0; index < parsed.lines.length; index += 1) if (parsed.lines[index]?.kind !== "tool") usageIndex = index;
2692
- let step = fromLines + 1;
2693
- for (const line of parsed.lines.slice(fromLines)) {
2694
- appendClaudeLine(spec, turn, step, line, step - 1 === usageIndex ? parsed.usage : void 0);
2695
- localAgent?.reportRunProgress(childSession.id, {
2977
+ const groups = groupClaudeTranscriptLines(parsed.lines);
2978
+ const groupStarts = [];
2979
+ let totalLines = 0;
2980
+ for (const group of groups) {
2981
+ groupStarts.push(totalLines);
2982
+ totalLines += group.length;
2983
+ }
2984
+ let carrier = -1;
2985
+ if (!parsed.lines.some((line) => line.usage !== void 0)) {
2986
+ for (let scan = 0; scan < groups.length; scan += 1) if (groups[scan].some((line) => line.kind !== "tool")) carrier = scan;
2987
+ }
2988
+ for (let scan = 0; scan < groups.length; scan += 1) {
2989
+ if (groupStarts[scan] < fromLines) continue;
2990
+ const group = groups[scan];
2991
+ appendClaudeTranscriptGroup(childSession, turn, scan + 1, group, scan === carrier ? parsed.usage : void 0);
2992
+ for (const line of group) localAgent?.reportRunProgress(childSession.id, {
2696
2993
  kind: "delta",
2697
2994
  text: claudeLineText(line)
2698
2995
  });
2699
- step += 1;
2700
2996
  }
2701
- if (parsed.usage !== void 0 && usageIndex !== -1 && usageIndex < fromLines) spec.ctx.logger.warn(`subagent-claude: round ${turn} usage arrived after its carrier line was mirrored; the accounting is dropped`);
2997
+ const carrierLine = carrier === -1 ? -1 : groupStarts[carrier] ?? -1;
2998
+ if (parsed.usage !== void 0 && carrierLine !== -1 && carrierLine < fromLines) spec.ctx.logger.warn(`subagent-claude: round ${turn} usage arrived after its carrier line was mirrored; the accounting is dropped`);
2702
2999
  await persistChildSession(spec.ctx, childSession);
2703
3000
  localAgent?.reportRunProgress(childSession.id, {
2704
3001
  kind: "mirror",
@@ -3051,6 +3348,47 @@ var ClaudeScopedModelMemory = class {
3051
3348
  this.written = model;
3052
3349
  }
3053
3350
  };
3351
+ /**
3352
+ * Scope the served directory to the surface asking for it (the same helper
3353
+ * lives in each provider's broker — fold into the family core when the
3354
+ * directory faces next evolve):
3355
+ *
3356
+ * - memberless (the card's add-model dialog, the invite picker): the
3357
+ * endpoint's served catalog only — the configuration/history extras the
3358
+ * broker's info extension adds for STATUS display must never become add
3359
+ * candidates;
3360
+ * - a member whose harness has a curated list: the list IS the vocabulary,
3361
+ * in the list's own order (the same rule `choices` already follows) — a
3362
+ * curated value the endpoint no longer serves stays as a bare row (the
3363
+ * card shows it the same way), and the member's current effective model is
3364
+ * kept visible as a bare entry when it sits outside the list, so a pre-list
3365
+ * binding never renders blank;
3366
+ * - a member without a curated list: the legacy extended vocabulary.
3367
+ */
3368
+ function scopeServedDirectory(directory, opts) {
3369
+ if (opts.member && opts.curated.length === 0) return directory;
3370
+ const served = directory.entries.filter((entry) => entry.source === "native");
3371
+ if (!opts.member) return {
3372
+ ...directory,
3373
+ entries: served
3374
+ };
3375
+ const byValue = new Map(served.flatMap((entry) => entry.resolvedModel === void 0 ? [[entry.value, entry]] : [[entry.value, entry], [entry.resolvedModel, entry]]));
3376
+ const entries = opts.curated.map((value) => byValue.get(value) ?? {
3377
+ value,
3378
+ label: value,
3379
+ source: "configuration"
3380
+ });
3381
+ const current = opts.current?.trim();
3382
+ if (current !== void 0 && current !== "" && !entries.some((entry) => entry.value === current || entry.resolvedModel === current)) entries.push({
3383
+ value: current,
3384
+ label: current,
3385
+ source: "configuration"
3386
+ });
3387
+ return {
3388
+ ...directory,
3389
+ entries
3390
+ };
3391
+ }
3054
3392
  var ClaudeModelBroker = class {
3055
3393
  deps;
3056
3394
  constructor(deps) {
@@ -3101,14 +3439,20 @@ var ClaudeModelBroker = class {
3101
3439
  }
3102
3440
  async modelDirectory(childSessionId, refresh = false) {
3103
3441
  if (refresh) await this.deps.directory?.(childSessionId, true);
3104
- return (await this.modelInfo(childSessionId, childSessionId === void 0 ? void 0 : this.deps.localAgent.getDelegation(childSessionId)?.model)).directory ?? {
3442
+ const record = childSessionId === void 0 ? void 0 : this.deps.localAgent.getDelegation(childSessionId);
3443
+ const info = await this.modelInfo(childSessionId, record?.model);
3444
+ return scopeServedDirectory(info.directory ?? {
3105
3445
  entries: [],
3106
3446
  complete: false,
3107
3447
  customInput: true,
3108
3448
  status: "unsupported",
3109
3449
  refreshing: false,
3110
3450
  revision: 0
3111
- };
3451
+ }, {
3452
+ member: childSessionId !== void 0,
3453
+ curated: this.deps.modelList?.(record?.scope) ?? [],
3454
+ current: info.effective
3455
+ });
3112
3456
  }
3113
3457
  async *followModelDirectory(childSessionId, signal) {
3114
3458
  if (this.deps.followDirectory === void 0) {
@@ -3506,7 +3850,7 @@ const inject = [
3506
3850
  * behavior byte-identical.
3507
3851
  */
3508
3852
  const SETTINGS_FIELDS = {
3509
- live: z.boolean().default(false),
3853
+ live: z.boolean().default(true),
3510
3854
  liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
3511
3855
  model: z.string(),
3512
3856
  recentModels: z.array(z.string()).default([])
@@ -3556,7 +3900,7 @@ function apply(ctx, config) {
3556
3900
  const model = vol(config.model);
3557
3901
  const recentModels = vol(config.recentModels);
3558
3902
  return {
3559
- live: vol(config.live) ?? false,
3903
+ live: vol(config.live) ?? true,
3560
3904
  liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
3561
3905
  ...model === void 0 ? {} : { model },
3562
3906
  ...recentModels === void 0 ? {} : { recentModels }
@@ -3573,6 +3917,7 @@ function apply(ctx, config) {
3573
3917
  }
3574
3918
  const face = settingsFace(ctx, {
3575
3919
  namespace: CLAUDE_SETTINGS_NAMESPACE,
3920
+ harness: "claude-code",
3576
3921
  legacySchema: CLAUDE_SETTINGS_SCHEMA,
3577
3922
  legacyBase,
3578
3923
  read: readSettings