@khorsheed/dsh-local-agent-kimi 0.2.0-rc.3 → 0.2.0

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
@@ -2080,13 +2080,16 @@ async function readKimiTranscript(sessionDir) {
2080
2080
  const sessionId = sessionDir.split("session_").pop() ?? sessionDir;
2081
2081
  const lines = [];
2082
2082
  const usageRecords = [];
2083
+ const closedSteps = /* @__PURE__ */ new Set();
2083
2084
  let model;
2084
2085
  let modelTurn;
2085
2086
  let lineModel;
2087
+ let lineConfig;
2086
2088
  const push = (line) => {
2087
2089
  lines.push({
2088
2090
  ...line,
2089
- ...lineModel === void 0 ? {} : { model: lineModel }
2091
+ ...lineModel === void 0 ? {} : { model: lineModel },
2092
+ ...line.kind !== "user" && lineConfig !== void 0 ? { requestConfig: { ...lineConfig } } : {}
2090
2093
  });
2091
2094
  };
2092
2095
  let turn = 0;
@@ -2100,7 +2103,8 @@ async function readKimiTranscript(sessionDir) {
2100
2103
  if (error.code === "ENOENT") return {
2101
2104
  sessionId,
2102
2105
  lines,
2103
- usageRecords
2106
+ usageRecords,
2107
+ closedSteps
2104
2108
  };
2105
2109
  throw error;
2106
2110
  }
@@ -2117,6 +2121,10 @@ async function readKimiTranscript(sessionDir) {
2117
2121
  if (typeof event.model === "string" && event.model !== "") {
2118
2122
  model = lineModel = event.model;
2119
2123
  modelTurn = turn || 1;
2124
+ if (lineConfig !== void 0) lineConfig = {
2125
+ ...lineConfig,
2126
+ model: event.model
2127
+ };
2120
2128
  }
2121
2129
  const usage = usageFromWire(event.usage);
2122
2130
  if (usage !== void 0) usageRecords.push({
@@ -2131,6 +2139,11 @@ async function readKimiTranscript(sessionDir) {
2131
2139
  model = event.model;
2132
2140
  modelTurn = turn || 1;
2133
2141
  }
2142
+ lineConfig = lineModel === void 0 ? void 0 : {
2143
+ model: lineModel,
2144
+ ...typeof event.thinkingEffort === "string" && event.thinkingEffort !== "" ? { reasoningEffort: event.thinkingEffort } : {},
2145
+ ...typeof event.maxTokens === "number" && Number.isFinite(event.maxTokens) ? { maxTokens: event.maxTokens } : {}
2146
+ };
2134
2147
  continue;
2135
2148
  }
2136
2149
  if (event.type === "turn.prompt") {
@@ -2175,17 +2188,21 @@ async function readKimiTranscript(sessionDir) {
2175
2188
  turn = Math.max(turn, loopTurn);
2176
2189
  turnSeen = true;
2177
2190
  }
2191
+ const wireStep = typeof loop.step === "number" ? loop.step : void 0;
2178
2192
  const part = loop.part;
2179
2193
  const lineTurn = turn === 0 ? 1 : turn;
2194
+ if (loop.type === "step.end" && wireStep !== void 0) closedSteps.add(`${lineTurn}:${wireStep}`);
2180
2195
  if (part?.type === "text" && part.text !== void 0 && part.text.trim() !== "") push({
2181
2196
  kind: "assistant",
2182
2197
  text: part.text,
2183
- turn: lineTurn
2198
+ turn: lineTurn,
2199
+ ...wireStep === void 0 ? {} : { wireStep }
2184
2200
  });
2185
2201
  else if (part?.type === "think" && part.think !== void 0 && part.think.trim() !== "") push({
2186
2202
  kind: "think",
2187
2203
  text: part.think,
2188
- turn: lineTurn
2204
+ turn: lineTurn,
2205
+ ...wireStep === void 0 ? {} : { wireStep }
2189
2206
  });
2190
2207
  else if (loop.type === "tool.call") {
2191
2208
  const call = loop;
@@ -2197,7 +2214,8 @@ async function readKimiTranscript(sessionDir) {
2197
2214
  id: typeof call.toolCallId === "string" ? call.toolCallId : typeof call.uuid === "string" ? call.uuid : `kimi-tool-${lineIndex}`,
2198
2215
  name,
2199
2216
  ...args === void 0 ? {} : { args },
2200
- turn: lineTurn
2217
+ turn: lineTurn,
2218
+ ...wireStep === void 0 ? {} : { wireStep }
2201
2219
  });
2202
2220
  if (typeof call.uuid === "string") callsByUuid.set(call.uuid, lineIndex);
2203
2221
  if (typeof call.toolCallId === "string") callsByToolCallId.set(call.toolCallId, lineIndex);
@@ -2230,6 +2248,7 @@ async function readKimiTranscript(sessionDir) {
2230
2248
  sessionId,
2231
2249
  lines,
2232
2250
  usageRecords,
2251
+ closedSteps,
2233
2252
  ...model === void 0 ? {} : { model },
2234
2253
  ...modelTurn === void 0 ? {} : { modelTurn }
2235
2254
  };
@@ -2238,7 +2257,9 @@ async function readKimiTranscript(sessionDir) {
2238
2257
  * Map a wire `usage.record` payload onto the shared usage contract. kimi
2239
2258
  * reports input (uncached), cache read, cache creation, and output; the
2240
2259
  * creation counter is its cache-write analog. Non-numeric or missing
2241
- * counters are dropped so the record stays usable when kimi omits a field.
2260
+ * counters are dropped so the record stays usable when kimi omits a field;
2261
+ * a reported cache-read counter is always written (zero included), so a
2262
+ * reader can tell "no hits" from "not reported".
2242
2263
  * @param raw - the event's `usage` value.
2243
2264
  * @returns the shared usage record, or undefined when nothing was reported.
2244
2265
  */
@@ -2258,7 +2279,7 @@ function usageFromWire(raw) {
2258
2279
  inputTokens: input ?? 0,
2259
2280
  outputTokens: output ?? 0
2260
2281
  };
2261
- if (cacheRead !== void 0 && cacheRead > 0) usage.cacheReadTokens = cacheRead;
2282
+ if (cacheRead !== void 0) usage.cacheReadTokens = cacheRead;
2262
2283
  if (cacheWrite !== void 0 && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
2263
2284
  return usage;
2264
2285
  }
@@ -2333,6 +2354,13 @@ function renderTranscript(transcript) {
2333
2354
  * call.
2334
2355
  * @module @khorsheed/dsh-local-agent-kimi/session-mirror
2335
2356
  */
2357
+ /**
2358
+ * The synthesized result for a call whose result the wire never recorded
2359
+ * (a settle-pass fold, or a live reservation kimi never logged). The
2360
+ * released-log contract resolves every advertised call before its step
2361
+ * closes, so an interrupted call folds with this error result.
2362
+ */
2363
+ const KIMI_INTERRUPTED_TOOL_RESULT = "(工具调用被中断,未能记录返回结果)";
2336
2364
  /** One user-role message event. */
2337
2365
  function userEvent(text) {
2338
2366
  return createUserMessage({
@@ -2354,41 +2382,42 @@ function assistantEvent(blocks, model) {
2354
2382
  });
2355
2383
  }
2356
2384
  /**
2357
- * The step the next fold pass would assign in one turn (the max existing
2358
- * step + 1). The token-granularity live
2359
- * driver reserves its stream merge key here LAZILY at the first delta, so
2360
- * tool cards folded before the stream started keep their chronological
2361
- * place below it instead of the stream squatting on step 1.
2385
+ * The step the next fold pass would assign in one turn: the smallest step
2386
+ * nothing holds (never written, never reserved at call time — callers skip
2387
+ * their own reservations on top). Filling the smallest free step keeps the
2388
+ * stored log dense: the released-log reader admits only dense, strictly
2389
+ * sequential steps, and a reservation released mid-round (its content rode an
2390
+ * earlier fold) leaves a gap only the next first-free write fills.
2362
2391
  * @param childSession - the run's child session.
2363
2392
  * @param turn - the round's turn number.
2364
- * @returns the next free step in the turn.
2393
+ * @returns the smallest free step in the turn.
2365
2394
  */
2366
2395
  function nextKimiSessionStep(childSession, turn) {
2367
- let next = 1;
2396
+ const taken = /* @__PURE__ */ new Set();
2368
2397
  for (const event of childSession.snapshotEvents()) {
2369
2398
  const data = event.data;
2370
- if (data.turn === turn && typeof data.step === "number") next = Math.max(next, data.step + 1);
2399
+ if (data.turn === turn && typeof data.step === "number") taken.add(data.step);
2371
2400
  }
2401
+ let next = 1;
2402
+ while (taken.has(next)) next += 1;
2372
2403
  return next;
2373
2404
  }
2374
2405
  /**
2375
- * Fold one non-tool transcript line into message blocks. Thinking maps to
2376
- * the native `reasoning` block so the standard conversation renders it as
2377
- * thinking rather than a `[思考]` text prefix; reply text stays text (content
2378
- * is never filtered — kimi's own injections remain visible). Tool lines are
2379
- * NOT folded here: they become native `tool/call`/`tool/result` event pairs
2380
- * (see the mirror loop), so the standard conversation renders them as tool
2381
- * cards instead of `[工具 X]` text.
2406
+ * The step a durable write takes: one past the highest step this turn has
2407
+ * opened in the log. The released-log reader admits steps only in order —
2408
+ * one past the last, never below it — so the coordinate is decided when the
2409
+ * step is written, never reserved ahead (a reservation filled later lands
2410
+ * below a higher step already written: a gap, then a step out of order).
2411
+ * Stream and tool reservations only key the live preview.
2382
2412
  */
2383
- function lineBlocks(line) {
2384
- if (line.kind === "think") return [{
2385
- type: "reasoning",
2386
- text: line.text
2387
- }];
2388
- return [{
2389
- type: "text",
2390
- text: line.text
2391
- }];
2413
+ function nextWrittenKimiStep(childSession, turn) {
2414
+ let last = 0;
2415
+ for (const event of childSession.snapshotEvents()) {
2416
+ if (event.type !== "step/start") continue;
2417
+ const data = event.data;
2418
+ if (data.turn === turn && typeof data.step === "number" && data.step > last) last = data.step;
2419
+ }
2420
+ return last + 1;
2392
2421
  }
2393
2422
  /** The run-progress delta text for one transcript line (any kind). */
2394
2423
  function kimiLineProgressText(line) {
@@ -2396,10 +2425,14 @@ function kimiLineProgressText(line) {
2396
2425
  return `${`[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}`}${line.result !== void 0 ? ` → ${line.result}` : ""}`;
2397
2426
  }
2398
2427
  /**
2399
- * Mirror one kimi session's transcript into the dsh subagent session: each
2400
- * user prompt becomes a `user/message`, each assistant reply (including
2401
- * folded thinking and tool activity) an `assistant/message`, then the events
2402
- * are appended to persistence so the child session is viewable.
2428
+ * Mirror one kimi session's transcript into a dsh subagent session: each
2429
+ * user prompt becomes a `user/message`, and each WIRE STEP (one LLM request:
2430
+ * its thinking, reply text and tool calls share the wire's own step number)
2431
+ * folds into one dsh step whose `assistant/message` carries the reasoning,
2432
+ * text and tool-call blocks together — the host session's own shape, so the
2433
+ * standard surfaces (conversation, Context Lens) read one request per LLM
2434
+ * call. Then the events are appended to persistence so the child session is
2435
+ * viewable.
2403
2436
  *
2404
2437
  * A resumed round (or a live poll) passes the already-mirrored
2405
2438
  * transcript-line count so only the delta is appended — re-mirroring earlier
@@ -2409,22 +2442,29 @@ function kimiLineProgressText(line) {
2409
2442
  * already in the child session, so a round mirrored across several live polls
2410
2443
  * does not restart step 1 per poll.
2411
2444
  *
2412
- * Every folded step is wrapped in the `step/start`–`step/end` boundary pair
2413
- * the live conversation assembler requires: without a boundary the step never
2414
- * enters the location index, the assistant message resolves to a turn-level
2415
- * location, and the real-time view renders nothing (only a full rebuild,
2416
- * which mints step drafts from the explicit coordinates, recovers it). A
2417
- * think/assistant line that completes an in-flight stream folds at the
2418
- * stream's reserved step instead (see {@link KimiMirrorStreams}), and a
2419
- * sequential fold never takes a step a stream reserved.
2445
+ * Every folded step is one ATOMIC `step/start`–`step/end` triple, written
2446
+ * only when its content lands: the released-log reader admits only dense,
2447
+ * strictly sequential steps (one open at a time — a second `step/start`
2448
+ * while another step is open fails the session's reload validation), and the
2449
+ * live conversation assembler needs the boundary to materialize the step at
2450
+ * all. A fold paired with in-flight streams lands at the earliest consumed
2451
+ * reservation (see {@link KimiMirrorStreams}), after `finalizeBefore` has
2452
+ * closed every stream reservation below it; a sequential fold never takes a
2453
+ * step a stream reserved. Mid-round, a wire step the wire has not CLOSED yet
2454
+ * (no `step.end`) holds the pass — its parts may still arrive — and so does
2455
+ * an unresulted tool call (the reader resolves every advertised call before
2456
+ * the step closes; the result merges into the call's line in place, so the
2457
+ * next pass sees it). The settle pass folds the remainder with a synthesized
2458
+ * interrupted result. Lines the wire never step-tagged (older kimi) fold one
2459
+ * by one, immediately, as before.
2420
2460
  *
2421
2461
  * Usage accounting is exactly-once ACROSS passes: each `usage.record` is
2422
- * tagged with the transcript position it occurred at, and a pass attaches the
2423
- * records in the half-open range `(fromLines, newTotal]` — the records that
2424
- * trail THIS pass's lines — summed onto the delta's final assistant message.
2425
- * (A record flushed only after its line was already mirrored belongs to no
2426
- * later pass and is dropped; kimi flushes content and usage together per
2427
- * request, so this window is a flush race, not the norm.)
2462
+ * tagged with the transcript position it occurred at, a pass owns the records
2463
+ * between the old offset and its own consumed end, and inside the window each
2464
+ * record rides the unit whose line span encloses it — one record per wire
2465
+ * step, landing on its own step's message. (A record raced PAST its content
2466
+ * encloses into the next unit instead: attribution off by one in a flush
2467
+ * race, totals still exact.)
2428
2468
  * @param ctx - host context carrying the session persistence service.
2429
2469
  * @param childSession - the dsh subagent session created for this delegation.
2430
2470
  * @param homeDir - the `kimi` harness's scoped home.
@@ -2432,7 +2472,9 @@ function kimiLineProgressText(line) {
2432
2472
  * recent session (a one-shot `kimi -p` run always creates a fresh one).
2433
2473
  * @param fromLines - transcript lines already mirrored into the child session
2434
2474
  * (0 for the first round); only lines after this offset are appended.
2435
- * @returns the new total and the newly mirrored lines' texts.
2475
+ * @returns the new mirror offset and the newly mirrored lines' texts. A held
2476
+ * unit (open wire step or unresulted tool) stops the offset before itself;
2477
+ * the next pass retries from there.
2436
2478
  */
2437
2479
  async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId, fromLines = 0, options) {
2438
2480
  let workspaces;
@@ -2485,110 +2527,153 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
2485
2527
  total: 0,
2486
2528
  texts: []
2487
2529
  };
2488
- const newTotal = transcript.lines.length;
2489
2530
  const delta = transcript.lines.slice(fromLines);
2490
2531
  const observedModel = options?.turn === void 0 || transcript.modelTurn === options.turn ? transcript.model : void 0;
2491
2532
  const toolCalls = roundToolCalls(transcript.lines, options?.turn);
2492
- const deltaUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= newTotal));
2493
- let lastAssistant = -1;
2494
- for (let index = 0; index < delta.length; index += 1) {
2495
- const line = delta[index];
2496
- if (line !== void 0 && line.kind !== "user" && line.kind !== "tool") lastAssistant = index;
2497
- }
2498
- const steps = /* @__PURE__ */ new Map();
2499
- const openCalls = /* @__PURE__ */ new Map();
2500
- const settledCalls = /* @__PURE__ */ new Set();
2533
+ const units = [];
2534
+ for (const line of delta) {
2535
+ if (line === void 0) continue;
2536
+ const last = units.at(-1);
2537
+ const head = last?.[0];
2538
+ if (last !== void 0 && head !== void 0 && line.kind !== "user" && head.kind !== "user" && line.wireStep !== void 0 && line.wireStep === head.wireStep && line.turn === head.turn) {
2539
+ last.push(line);
2540
+ continue;
2541
+ }
2542
+ units.push([line]);
2543
+ }
2544
+ let holdAt = -1;
2545
+ if (options?.settle !== true) for (let index = 0; index < units.length; index += 1) {
2546
+ const unit = units[index];
2547
+ const head = unit[0];
2548
+ if (head.kind !== "user" && head.wireStep !== void 0 && !transcript.closedSteps.has(`${head.turn}:${head.wireStep}`) || unit.some((line) => line.kind === "tool" && line.result === void 0)) {
2549
+ holdAt = index;
2550
+ break;
2551
+ }
2552
+ }
2553
+ const foldedUnits = holdAt < 0 ? units : units.slice(0, holdAt);
2554
+ const consumedTotal = fromLines + foldedUnits.reduce((n, unit) => n + unit.length, 0);
2555
+ const unitUsage = [];
2556
+ const claimed = [];
2557
+ let unitStart = fromLines;
2558
+ for (const unit of foldedUnits) {
2559
+ const start = unitStart;
2560
+ unitStart += unit.length;
2561
+ const head = unit[0];
2562
+ if (head.kind !== "user" && head.wireStep !== void 0) {
2563
+ claimed.push({
2564
+ from: start,
2565
+ to: unitStart
2566
+ });
2567
+ unitUsage.push(sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= start && record.line < unitStart)));
2568
+ } else unitUsage.push(void 0);
2569
+ }
2570
+ const unclaimedUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal && !claimed.some((span) => record.line >= span.from && record.line < span.to)));
2571
+ const deltaUsage = sumUsageRecords(transcript.usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal));
2572
+ const takenSteps = /* @__PURE__ */ new Map();
2501
2573
  for (const event of childSession.snapshotEvents()) {
2502
2574
  const data = event.data;
2503
- if (typeof data.turn === "number" && typeof data.step === "number") steps.set(data.turn, Math.max(steps.get(data.turn) ?? 0, data.step + 1));
2504
- if (event.type === "tool/call") {
2505
- const call = event.data;
2506
- openCalls.set(call.callId, {
2507
- turn: call.turn,
2508
- step: call.step,
2509
- seq: event.seq
2510
- });
2511
- } else if (event.type === "tool/result") {
2512
- const message = event.data.message;
2513
- const id = message?.toolCallId ?? message?.content?.[0]?.toolCallId;
2514
- if (id !== void 0) {
2515
- openCalls.delete(id);
2516
- settledCalls.add(id);
2517
- }
2575
+ if (typeof data.turn === "number" && typeof data.step === "number") {
2576
+ const taken = takenSteps.get(data.turn) ?? /* @__PURE__ */ new Set();
2577
+ taken.add(data.step);
2578
+ takenSteps.set(data.turn, taken);
2518
2579
  }
2519
2580
  }
2520
2581
  /**
2521
- * The next sequential fold step in one turn: the running counter, shifted
2522
- * past every step an in-flight stream reserved (a stream's snapshots and
2523
- * its completion fold live at the reserved coordinate — a sequential fold
2524
- * landing there would write a second step/start, which the assembler
2525
- * rejects).
2582
+ * The fold's coordinate: the smallest step of the turn that nothing holds
2583
+ * — neither written nor currently reserved. Filling the smallest free step
2584
+ * keeps the stored log dense even when a stream reservation below was
2585
+ * released mid-round (its content rode an earlier fold's step).
2526
2586
  */
2527
2587
  const nextFoldStep = (turn) => {
2528
- let step = steps.get(turn) ?? 1;
2529
- const reserved = options?.streams?.reservedSteps(turn);
2530
- if (reserved !== void 0 && reserved.length > 0) {
2531
- const skip = new Set(reserved);
2532
- while (skip.has(step)) step += 1;
2533
- }
2534
- steps.set(turn, step + 1);
2588
+ const taken = new Set(takenSteps.get(turn));
2589
+ for (const reserved of options?.streams?.reservedSteps(turn) ?? []) taken.add(reserved);
2590
+ let step = 1;
2591
+ while (taken.has(step)) step += 1;
2535
2592
  return step;
2536
2593
  };
2537
- /** Whether this pass attached its window's usage to a folded message. */
2538
- let usageAttached = false;
2594
+ /** Record a fold's coordinate chosen by a stream pairing. */
2595
+ const markTaken = (turn, step) => {
2596
+ const written = takenSteps.get(turn) ?? /* @__PURE__ */ new Set();
2597
+ written.add(step);
2598
+ takenSteps.set(turn, written);
2599
+ };
2600
+ /** The window's accounting that no fold carried (dropped units' sums). */
2601
+ let carryUsage;
2602
+ let lastContentUnit = -1;
2603
+ for (let index = foldedUnits.length - 1; index >= 0; index -= 1) if (foldedUnits[index][0]?.kind !== "user") {
2604
+ lastContentUnit = index;
2605
+ break;
2606
+ }
2539
2607
  const texts = [];
2540
- for (let index = 0; index < fromLines && index < transcript.lines.length; index += 1) {
2541
- const line = transcript.lines[index];
2542
- if (line === void 0 || line.kind !== "tool" || line.result === void 0) continue;
2543
- const call = openCalls.get(line.id);
2544
- if (call === void 0) continue;
2545
- openCalls.delete(line.id);
2546
- settledCalls.add(line.id);
2547
- childSession.append("tool/result", {
2548
- turn: call.turn,
2549
- step: call.step,
2550
- message: createToolResultMessage({
2551
- callId: line.id,
2552
- content: [{
2553
- type: "text",
2554
- text: line.result
2555
- }],
2556
- isError: false
2557
- })
2558
- }, {
2559
- surfaceOp: "append",
2560
- sourceEventSeqs: [call.seq]
2608
+ for (let unitIndex = 0; unitIndex < foldedUnits.length; unitIndex += 1) {
2609
+ const unit = foldedUnits[unitIndex];
2610
+ const head = unit[0];
2611
+ let usage = addTokenUsage(carryUsage, unitUsage[unitIndex]);
2612
+ carryUsage = void 0;
2613
+ if (unitIndex === lastContentUnit) usage = addTokenUsage(usage, unclaimedUsage);
2614
+ const turn = head.turn;
2615
+ if (head.kind === "user") {
2616
+ if (userAlreadyAppended(childSession, turn, head.text)) continue;
2617
+ childSession.append("user/message", userEvent(head.text), { surfaceOp: "append" });
2618
+ texts.push(head.text);
2619
+ carryUsage = usage;
2620
+ continue;
2621
+ }
2622
+ const toolLines = unit.filter((line) => line.kind === "tool");
2623
+ const reasoning = unit.filter((line) => line.kind === "think").map((line) => line.text).join("");
2624
+ const text = unit.filter((line) => line.kind === "assistant").map((line) => line.text).join("");
2625
+ const kinds = [...reasoning.trim() === "" ? [] : ["think"], ...text.trim() === "" ? [] : ["text"]];
2626
+ const pairing = kinds.length === 0 ? void 0 : options?.streams?.completeStreams(turn, kinds);
2627
+ const sealed = new Set(pairing?.sealed ?? []);
2628
+ const blocks = [];
2629
+ if (reasoning.trim() !== "" && !sealed.has("think")) blocks.push({
2630
+ type: "reasoning",
2631
+ text: reasoning
2561
2632
  });
2562
- texts.push(kimiLineProgressText(line));
2563
- }
2564
- if (delta.length === 0) {
2565
- if (texts.length > 0) await persistChildSession(ctx, childSession);
2566
- return {
2567
- total: newTotal,
2568
- texts,
2569
- ...deltaUsage !== void 0 ? { usage: deltaUsage } : {},
2570
- ...usageAttached ? { usageAttached: true } : {},
2571
- ...observedModel !== void 0 ? { model: observedModel } : {},
2572
- ...toolCalls !== void 0 ? { toolCalls } : {}
2573
- };
2574
- }
2575
- for (let index = 0; index < delta.length; index += 1) {
2576
- const line = delta[index];
2577
- if (line === void 0) continue;
2578
- const turn = line.turn;
2579
- if (line.kind === "user") {
2580
- if (userAlreadyAppended(childSession, turn, line.text)) continue;
2581
- steps.set(turn, 1);
2582
- childSession.append("user/message", userEvent(line.text), { surfaceOp: "append" });
2583
- texts.push(line.text);
2584
- } else if (line.kind === "tool") {
2585
- const reserved = options?.streams?.completeTool?.(line);
2586
- const step = reserved?.step ?? nextFoldStep(turn);
2587
- steps.set(turn, Math.max(steps.get(turn) ?? 1, step + 1));
2588
- if (reserved?.opened !== true) childSession.append("step/start", {
2589
- turn,
2590
- step
2591
- });
2633
+ if (text.trim() !== "" && !sealed.has("text")) blocks.push({
2634
+ type: "text",
2635
+ text
2636
+ });
2637
+ for (const line of toolLines) blocks.push({
2638
+ type: "tool-call",
2639
+ id: line.id,
2640
+ name: line.name,
2641
+ arguments: line.args ?? ""
2642
+ });
2643
+ for (const line of unit) texts.push(kimiLineProgressText(line));
2644
+ if (blocks.length === 0) {
2645
+ carryUsage = usage;
2646
+ continue;
2647
+ }
2648
+ for (const line of toolLines) options?.streams?.releaseTool?.(line);
2649
+ let step = pairing?.step ?? nextFoldStep(turn);
2650
+ options?.streams?.finalizeBefore?.(turn, step);
2651
+ let sequential = nextKimiSessionStep(childSession, turn);
2652
+ const reservedAfter = options?.streams?.reservedSteps(turn) ?? [];
2653
+ while (reservedAfter.includes(sequential)) sequential += 1;
2654
+ step = Math.min(step, sequential);
2655
+ markTaken(turn, step);
2656
+ step = nextWrittenKimiStep(childSession, turn);
2657
+ const requestConfig = unit.map((line) => line.kind !== "user" ? line.requestConfig : void 0).find((config) => config !== void 0);
2658
+ if (requestConfig?.model !== void 0) appendRequestHeaderIfChanged(childSession, {
2659
+ provider: "kimi-cli",
2660
+ model: requestConfig.model,
2661
+ ...requestConfig.reasoningEffort === void 0 ? {} : { reasoningEffort: requestConfig.reasoningEffort },
2662
+ ...requestConfig.maxTokens === void 0 ? {} : { maxTokens: requestConfig.maxTokens }
2663
+ });
2664
+ const model = [...unit].reverse().find((line) => line.model !== void 0)?.model;
2665
+ childSession.append("step/start", {
2666
+ turn,
2667
+ step
2668
+ });
2669
+ childSession.append("assistant/message", {
2670
+ turn,
2671
+ step,
2672
+ message: assistantEvent(blocks, model),
2673
+ stream: [],
2674
+ ...usage === void 0 ? {} : { usage }
2675
+ }, { surfaceOp: "append" });
2676
+ for (const line of toolLines) {
2592
2677
  const call = childSession.append("tool/call", {
2593
2678
  turn,
2594
2679
  step,
@@ -2596,75 +2681,35 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
2596
2681
  name: line.name,
2597
2682
  arguments: line.args ?? ""
2598
2683
  });
2599
- texts.push(kimiLineProgressText(line));
2600
- if (line.result !== void 0) childSession.append("tool/result", {
2684
+ childSession.append("tool/result", {
2601
2685
  turn,
2602
2686
  step,
2603
2687
  message: createToolResultMessage({
2604
2688
  callId: line.id,
2605
2689
  content: [{
2606
2690
  type: "text",
2607
- text: line.result
2691
+ text: line.result ?? "(工具调用被中断,未能记录返回结果)"
2608
2692
  }],
2609
- isError: false
2693
+ isError: line.result === void 0
2610
2694
  })
2611
2695
  }, {
2612
2696
  surfaceOp: "append",
2613
2697
  sourceEventSeqs: [call.seq]
2614
2698
  });
2615
- childSession.append("step/end", {
2616
- turn,
2617
- step
2618
- });
2619
- } else {
2620
- const completion = options?.streams?.completeStream(line);
2621
- if (completion !== void 0) {
2622
- steps.set(turn, Math.max(steps.get(turn) ?? 1, completion.step + 1));
2623
- if (!completion.opened) childSession.append("step/start", {
2624
- turn,
2625
- step: completion.step
2626
- });
2627
- if (index === lastAssistant && deltaUsage !== void 0) usageAttached = true;
2628
- childSession.append("assistant/message", {
2629
- turn,
2630
- step: completion.step,
2631
- message: assistantEvent(lineBlocks(line), line.model),
2632
- stream: [],
2633
- ...index === lastAssistant && deltaUsage !== void 0 ? { usage: deltaUsage } : {}
2634
- }, { surfaceOp: "append" });
2635
- childSession.append("step/end", {
2636
- turn,
2637
- step: completion.step
2638
- });
2639
- texts.push(kimiLineProgressText(line));
2640
- continue;
2641
- }
2642
- const step = nextFoldStep(turn);
2643
- childSession.append("step/start", {
2644
- turn,
2645
- step
2646
- });
2647
- if (index === lastAssistant && deltaUsage !== void 0) usageAttached = true;
2648
- childSession.append("assistant/message", {
2649
- turn,
2650
- step,
2651
- message: assistantEvent(lineBlocks(line), line.model),
2652
- stream: [],
2653
- ...index === lastAssistant && deltaUsage !== void 0 ? { usage: deltaUsage } : {}
2654
- }, { surfaceOp: "append" });
2655
- childSession.append("step/end", {
2656
- turn,
2657
- step
2658
- });
2659
- texts.push(kimiLineProgressText(line));
2660
2699
  }
2700
+ childSession.append("step/end", {
2701
+ turn,
2702
+ step
2703
+ });
2661
2704
  }
2662
2705
  await persistChildSession(ctx, childSession);
2706
+ const remainder = addTokenUsage(carryUsage, lastContentUnit === -1 ? unclaimedUsage : void 0);
2663
2707
  return {
2664
- total: newTotal,
2708
+ total: consumedTotal,
2665
2709
  texts,
2666
2710
  ...deltaUsage !== void 0 ? { usage: deltaUsage } : {},
2667
- ...usageAttached ? { usageAttached: true } : {},
2711
+ ...deltaUsage !== void 0 && remainder === void 0 ? { usageAttached: true } : {},
2712
+ ...remainder !== void 0 ? { usageUnattached: remainder } : {},
2668
2713
  ...observedModel !== void 0 ? { model: observedModel } : {},
2669
2714
  ...toolCalls !== void 0 ? { toolCalls } : {}
2670
2715
  };
@@ -2693,6 +2738,22 @@ function roundToolCalls(lines, turn) {
2693
2738
  };
2694
2739
  }
2695
2740
  /**
2741
+ * Write the fold's request/header when the wire's declared config differs from
2742
+ * the child log's last one (the host logs changed snapshots, not per-call
2743
+ * duplicates): first write is `initial`, a differing one `change`, an
2744
+ * identical one skipped.
2745
+ */
2746
+ function appendRequestHeaderIfChanged(childSession, config) {
2747
+ const serialized = JSON.stringify(config);
2748
+ let last;
2749
+ for (const event of childSession.snapshotEvents()) if (event.type === "request/header") last = JSON.stringify(event.data.header?.config);
2750
+ if (last === serialized) return;
2751
+ childSession.append("request/header", {
2752
+ header: { config },
2753
+ reason: last === void 0 ? "initial" : "change"
2754
+ });
2755
+ }
2756
+ /**
2696
2757
  * Whether the turn already carries a user/message with this exact text (the
2697
2758
  * live driver's round-start append). Scoped to the events at or after this
2698
2759
  * turn's turn/start so identical prompts across rounds stay independent.
@@ -2727,11 +2788,14 @@ function userAlreadyAppended(childSession, turn, text) {
2727
2788
  * throttled `mirrorKimiDelta` passes and the settle pass stays authoritative
2728
2789
  * (one fold, one offset, zero divergence between the two drivers). Every
2729
2790
  * transcript item folds 1:1 into the child session log.
2730
- * Live output also mirrors the in-flight item: chunk
2731
- * deltas accumulate per streaming item and append throttled snapshot
2732
- * assistant/messages at the item's reserved (turn, step) — the host folds
2733
- * repeated settles at one coordinate into one live-updating chat node — and
2734
- * the wire line completing the item folds at the same step, finalizing it.
2791
+ * Live output rides the `local-agent/stream` channel: chunk deltas
2792
+ * accumulate per streaming item and publish throttled updates at the item's
2793
+ * reserved (turn, step) — the member live node renders them as-is — while
2794
+ * the item's durable content lands as ONE atomic step/start→…→step/end
2795
+ * triple at that step, written by the completing wire line's fold or at
2796
+ * settle. Durable steps stay dense and strictly sequential (the released-log
2797
+ * reader validates exactly that on every cold open); snapshots never hold a
2798
+ * step open across appends.
2735
2799
  *
2736
2800
  * Lifecycle mirrors the dsh/codex live drivers' discipline: lazy spawn with an
2737
2801
  * initialize handshake (loadSession capability required — crash recovery is
@@ -3416,25 +3480,17 @@ var KimiAcpLiveDriver = class {
3416
3480
  /** The round's accumulated assistant text (the run output — chunks are the only source). */
3417
3481
  let roundText = "";
3418
3482
  /**
3419
- * The step ledger: every step a stream reservation consumed this round
3420
- * (in increasing order — permanently, whether the stream later completes
3421
- * or stays an orphan snapshot). The file fold skips these steps for its
3422
- * sequential folds, so a tool card folded mid-stream lands ABOVE the
3423
- * stream's step and the projection renders the round's chronological
3424
- * order.
3483
+ * The step ledger: the steps in-flight reservations currently hold this
3484
+ * round. A reservation's durable content lands at the fold that consumes
3485
+ * it (possibly carrying several reservations' kinds in one message — the
3486
+ * wire step's fold), which RELEASES the other consumed coordinates for
3487
+ * later folds; only a reservation kimi never completes (a plan stream, an
3488
+ * unwired tool notification) stays held until its settle-time finalize.
3489
+ * The file fold skips held steps for its sequential folds, so a tool card
3490
+ * folded mid-stream lands ABOVE the stream's step and the projection
3491
+ * renders the round's chronological order.
3425
3492
  */
3426
3493
  const reservedSteps = [];
3427
- /**
3428
- * The streaming items seen this round . kimi's ACP
3429
- * chunks carry no item id, so the stream key is synthetic per kind per
3430
- * round (the codex live driver's fallback shape): thought chunks share
3431
- * one stream, message chunks another. Deltas accumulate into throttled
3432
- * snapshot assistant/messages appended at the stream's reserved
3433
- * (turn, step) — the host folds repeated settles at one coordinate into
3434
- * one live-updating chat node. The wire line that completes the stream's
3435
- * kind folds at the same step and finalizes it; an entry whose completion
3436
- * never folds is force-finalized at settle.
3437
- */
3438
3494
  const streams = /* @__PURE__ */ new Map();
3439
3495
  /** The stream the latest delta accumulated into (a kind switch force-snapshots it). */
3440
3496
  let activeStream;
@@ -3462,7 +3518,23 @@ var KimiAcpLiveDriver = class {
3462
3518
  };
3463
3519
  /** A stream's synthetic key: kimi's ACP deltas carry no item id, so kind + turn pairs them. */
3464
3520
  let generation = 0;
3521
+ /**
3522
+ * One step reservation per generation, shared by the think and text
3523
+ * streams of the same response segment: the host's own live shape is
3524
+ * reasoning + text on ONE step, and a per-kind step would leave the
3525
+ * consumed reservation's stream frames pointing at a step that never
3526
+ * materializes (an empty trailing step breaks the host's answer
3527
+ * detection, folding the reply into the collapsed process block).
3528
+ */
3529
+ const generationSteps = /* @__PURE__ */ new Map();
3465
3530
  const seenToolCalls = /* @__PURE__ */ new Set();
3531
+ /**
3532
+ * ACP tool_call notifications' chronological reservations (call id →
3533
+ * step, plus the notification's display title). No durable event lands at
3534
+ * notification time: the native lifecycle (advertisement, call, result in
3535
+ * one closed step) is written by the wire line's fold, or at settle with
3536
+ * an interrupted placeholder when kimi never logged the call.
3537
+ */
3466
3538
  const toolSteps = /* @__PURE__ */ new Map();
3467
3539
  const streamKey = (kind) => `kimi-stream-${kind}-${turn}-${generation}`;
3468
3540
  /** The plan stream's key: a snapshot stream beside the think/text delta streams. */
@@ -3480,7 +3552,7 @@ var KimiAcpLiveDriver = class {
3480
3552
  const stream = streams.get(key);
3481
3553
  if (stream !== void 0) {
3482
3554
  stream.text += text;
3483
- appendStreamSnapshot(stream, false, false);
3555
+ publishStreamSnapshot(stream, false);
3484
3556
  }
3485
3557
  localAgent?.reportRunProgress(childSession.id, {
3486
3558
  kind: "delta",
@@ -3506,135 +3578,283 @@ var KimiAcpLiveDriver = class {
3506
3578
  const stream = streams.get(planKey);
3507
3579
  if (stream !== void 0 && stream.text !== plan) {
3508
3580
  stream.text = plan;
3509
- appendStreamSnapshot(stream, true, false);
3581
+ publishStreamSnapshot(stream, true);
3510
3582
  }
3511
3583
  };
3512
3584
  /**
3513
- * Append one snapshot of a streaming item at its reserved (turn, step).
3514
- * Batched per stream on a bounded deadline unless `force`; the forced
3515
- * final snapshot carries `interrupted` (a cancelled turn reads 已停止
3516
- * legitimately) and, when `withUsage`, the round's uncarried usage. An
3517
- * empty stream leaves no boundary at all.
3585
+ * The live-output channel. Snapshots are browser-only previews (never
3586
+ * `local-agent/stream` log events) — durable step content lands once per
3587
+ * stream, at finalization, as the fold's atomic triple.
3518
3588
  */
3519
3589
  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)}`));
3520
3590
  const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
3521
3591
  const pendingStreamArrival = /* @__PURE__ */ new Map();
3522
- const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
3523
- if (stream.text.trim() === "") return;
3592
+ /**
3593
+ * Publish one snapshot of a streaming item at its reserved (turn, step)
3594
+ * through the live channel. Batched per stream on a bounded deadline
3595
+ * unless `force`. Never writes durable content.
3596
+ */
3597
+ const publishStreamSnapshot = (stream, force) => {
3598
+ if (stream.text.trim() === "" || stream.sealed === true) return;
3524
3599
  const now = Date.now();
3525
3600
  if (!force) {
3526
3601
  if (!pendingStreamArrival.has(stream.step)) pendingStreamArrival.set(stream.step, now);
3527
- liveFlush.schedule(stream.step, () => appendStreamSnapshot(stream, true, interrupted, withUsage));
3602
+ liveFlush.schedule(stream.step, () => publishStreamSnapshot(stream, true));
3528
3603
  return;
3529
3604
  }
3530
3605
  liveFlush.cancel(stream.step);
3531
3606
  const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
3532
3607
  pendingStreamArrival.delete(stream.step);
3533
- if (streamPublisher !== void 0 && !final) {
3534
- if (!stream.opened) {
3535
- childSession.append("step/start", {
3536
- turn,
3537
- step: stream.step
3538
- });
3539
- stream.opened = true;
3608
+ streamPublisher?.preview(stream.step, stream.kind, stream.text, {
3609
+ receivedAt,
3610
+ itemId: stream.kind
3611
+ });
3612
+ stream.lastSnapshotLen = stream.text.length;
3613
+ };
3614
+ /**
3615
+ * Write the stream's durable content as ONE atomic
3616
+ * step/start→assistant/message→step/end triple at its reserved step and
3617
+ * seal the reservation (its completing wire line, when one exists, pairs
3618
+ * and drops). `interrupted` marks a non-completed round's partial (a
3619
+ * cancelled turn reads 已停止 legitimately); `withUsage` attaches the
3620
+ * round's uncarried usage.
3621
+ */
3622
+ /**
3623
+ * Write the durable content of every stream sharing one step as ONE atomic
3624
+ * step/start→assistant/message→step/end triple (a generation's think and
3625
+ * text merge into a single message — the host's own shape) and seal the
3626
+ * reservations. `interrupted` marks a non-completed round's partial (a
3627
+ * cancelled turn reads 已停止 legitimately); `withUsage` attaches the
3628
+ * round's uncarried usage.
3629
+ */
3630
+ const finalizeStep = (step, entries, interrupted, withUsage) => {
3631
+ liveFlush.cancel(step);
3632
+ pendingStreamArrival.delete(step);
3633
+ const reasoning = entries.filter(([, stream]) => stream.kind === "think").map(([, stream]) => stream.text).join("");
3634
+ const text = entries.filter(([, stream]) => stream.kind === "text").map(([, stream]) => stream.text).join("");
3635
+ const blocks = [];
3636
+ if (reasoning.trim() !== "") blocks.push({
3637
+ type: "reasoning",
3638
+ text: reasoning
3639
+ });
3640
+ if (text.trim() !== "") blocks.push({
3641
+ type: "text",
3642
+ text
3643
+ });
3644
+ if (blocks.length === 0) {
3645
+ streamPublisher?.finish(step);
3646
+ const keys = new Set(entries.map(([key]) => key));
3647
+ if (!new Set([...streams.entries()].filter(([key, stream]) => !keys.has(key) && stream.sealed !== true).map(([, stream]) => stream.step)).has(step)) {
3648
+ const index = reservedSteps.indexOf(step);
3649
+ if (index >= 0) reservedSteps.splice(index, 1);
3650
+ }
3651
+ for (const [key, stream] of entries) {
3652
+ stream.sealed = true;
3653
+ generationSteps.delete(stream.generation);
3654
+ if (activeStream === key) activeStream = void 0;
3540
3655
  }
3541
- streamPublisher.update(stream.step, stream.kind, stream.text, { receivedAt });
3542
- stream.lastSnapshotAt = now;
3543
- stream.lastSnapshotLen = stream.text.length;
3544
3656
  return;
3545
3657
  }
3546
- if (!stream.opened) {
3547
- childSession.append("step/start", {
3548
- turn,
3549
- step: stream.step
3550
- });
3551
- stream.opened = true;
3552
- }
3658
+ const durable = nextWrittenKimiStep(childSession, turn);
3659
+ childSession.append("step/start", {
3660
+ turn,
3661
+ step: durable
3662
+ });
3553
3663
  childSession.append("assistant/message", {
3554
3664
  turn,
3555
- step: stream.step,
3556
- message: assistantEvent([stream.kind === "think" ? {
3557
- type: "reasoning",
3558
- text: stream.text
3559
- } : {
3560
- type: "text",
3561
- text: stream.text
3562
- }], runtime?.modelConfiguration?.currentModel ?? roundModel),
3665
+ step: durable,
3666
+ message: assistantEvent(blocks, runtime?.modelConfiguration?.currentModel ?? roundModel),
3563
3667
  stream: [],
3564
3668
  ...withUsage && roundUsage !== void 0 ? { usage: roundUsage } : {},
3565
3669
  ...interrupted ? { interrupted: true } : {}
3566
3670
  }, { surfaceOp: "append" });
3567
3671
  if (withUsage) roundUsage = void 0;
3568
- stream.lastSnapshotAt = now;
3569
- stream.lastSnapshotLen = stream.text.length;
3672
+ streamPublisher?.finish(step);
3673
+ childSession.append("step/end", {
3674
+ turn,
3675
+ step: durable
3676
+ });
3677
+ for (const [key, stream] of entries) {
3678
+ stream.sealed = true;
3679
+ generationSteps.delete(stream.generation);
3680
+ if (activeStream === key) activeStream = void 0;
3681
+ }
3570
3682
  persist();
3571
3683
  };
3572
3684
  /**
3685
+ * Write the durable step for an ACP tool call kimi never logged to the
3686
+ * wire (a crash between the notification and the wire flush): the
3687
+ * notification's title stands in for the name and the result is the
3688
+ * shared interrupted placeholder — the native lifecycle in one step.
3689
+ */
3690
+ /** Whether this turn's log already carries the tool call (folded from the wire). */
3691
+ const toolLogged = (id) => childSession.snapshotEvents().some((event) => event.type === "tool/call" && event.data.turn === turn && event.data.callId === id);
3692
+ const finalizeOrphanTool = (id, entry) => {
3693
+ const name = entry.title ?? "tool";
3694
+ const step = nextWrittenKimiStep(childSession, turn);
3695
+ childSession.append("step/start", {
3696
+ turn,
3697
+ step
3698
+ });
3699
+ childSession.append("assistant/message", {
3700
+ turn,
3701
+ step,
3702
+ message: assistantEvent([{
3703
+ type: "tool-call",
3704
+ id,
3705
+ name,
3706
+ arguments: ""
3707
+ }], runtime?.modelConfiguration?.currentModel ?? roundModel),
3708
+ stream: []
3709
+ }, { surfaceOp: "append" });
3710
+ const call = childSession.append("tool/call", {
3711
+ turn,
3712
+ step,
3713
+ callId: id,
3714
+ name,
3715
+ arguments: ""
3716
+ });
3717
+ childSession.append("tool/result", {
3718
+ turn,
3719
+ step,
3720
+ message: createToolResultMessage({
3721
+ callId: id,
3722
+ content: [{
3723
+ type: "text",
3724
+ text: KIMI_INTERRUPTED_TOOL_RESULT
3725
+ }],
3726
+ isError: true
3727
+ })
3728
+ }, {
3729
+ surfaceOp: "append",
3730
+ sourceEventSeqs: [call.seq]
3731
+ });
3732
+ childSession.append("step/end", {
3733
+ turn,
3734
+ step
3735
+ });
3736
+ };
3737
+ /**
3738
+ * The fold's gap fill: a fold about to write at `step` first finalizes
3739
+ * every live stream reserved below it (ascending), so the stored log's
3740
+ * steps land dense and strictly sequential. Only streams can sit in the
3741
+ * gap: an unresulted tool line holds its fold instead (session-mirror).
3742
+ */
3743
+ const finalizeBefore = (targetTurn, step) => {
3744
+ if (targetTurn !== turn) return;
3745
+ const pending = [...streams.entries()].filter(([, stream]) => stream.sealed !== true && stream.step < step).sort((a, b) => a[1].step - b[1].step);
3746
+ const byStep = /* @__PURE__ */ new Map();
3747
+ for (const entry of pending) {
3748
+ const group = byStep.get(entry[1].step) ?? [];
3749
+ group.push(entry);
3750
+ byStep.set(entry[1].step, group);
3751
+ }
3752
+ for (const [at, entries] of byStep) finalizeStep(at, entries, false, false);
3753
+ };
3754
+ /**
3573
3755
  * Reserve the step one streaming item will occupy — past every folded
3574
3756
  * line (the session's own event ledger: the file fold writes events
3575
- * synchronously) and every earlier reservation. A kind switch forces one
3576
- * freshness snapshot of the current stream (skipped when its text has not
3577
- * grown since the last one); its completion still folds at its own
3578
- * reserved step when the wire line lands.
3757
+ * synchronously) and every earlier reservation. A kind switch publishes
3758
+ * one freshness snapshot of the current stream (skipped when its text has
3759
+ * not grown since the last one); its durable triple still lands at its
3760
+ * own reserved step when the wire line folds or the round settles.
3579
3761
  */
3580
3762
  const reserveStream = (key, kind) => {
3581
3763
  if (activeStream !== void 0 && activeStream !== key) {
3582
3764
  const previous = streams.get(activeStream);
3583
- if (previous !== void 0 && previous.text.length !== previous.lastSnapshotLen) appendStreamSnapshot(previous, true, false);
3765
+ if (previous !== void 0 && previous.text.length !== previous.lastSnapshotLen) publishStreamSnapshot(previous, true);
3584
3766
  activeStream = void 0;
3585
3767
  }
3586
3768
  let stream = streams.get(key);
3769
+ if (stream?.sealed === true) {
3770
+ streams.delete(key);
3771
+ stream = void 0;
3772
+ }
3587
3773
  if (stream === void 0) {
3588
- let step = nextKimiSessionStep(childSession, turn);
3589
- for (const reserved of reservedSteps) step = Math.max(step, reserved + 1);
3590
- reservedSteps.push(step);
3774
+ let step = key === planKey ? void 0 : generationSteps.get(generation);
3775
+ if (step === void 0) {
3776
+ step = nextKimiSessionStep(childSession, turn);
3777
+ while (reservedSteps.includes(step)) step += 1;
3778
+ reservedSteps.push(step);
3779
+ if (key !== planKey) generationSteps.set(generation, step);
3780
+ }
3591
3781
  stream = {
3592
3782
  kind,
3593
3783
  step,
3784
+ generation: key === planKey ? -1 : generation,
3594
3785
  text: "",
3595
- lastSnapshotAt: 0,
3596
- lastSnapshotLen: 0,
3597
- opened: false
3786
+ lastSnapshotLen: 0
3598
3787
  };
3599
3788
  streams.set(key, stream);
3600
3789
  }
3601
3790
  activeStream = key;
3602
3791
  };
3603
3792
  /**
3604
- * The fold-pass coordination surface : the file
3605
- * fold skips reserved steps for sequential folds and pairs a completed
3606
- * think/assistant line with its stream — scoped to THIS round's turn, so
3607
- * an earlier round's leftover lines never consume this round's stream.
3793
+ * The fold-pass coordination surface: the file
3794
+ * fold skips reserved steps for sequential folds, pairs one fold with the
3795
+ * round's in-flight streams of the fold's kinds (consuming both — a wire
3796
+ * step's think and text ride ONE durable step), and closes every
3797
+ * reservation below each fold step first (finalizeBefore) — scoped to
3798
+ * THIS round's turn, so an earlier round's leftover lines never consume
3799
+ * this round's stream.
3608
3800
  */
3609
3801
  const mirrorStreams = {
3610
3802
  reservedSteps: (t) => t === turn ? reservedSteps : [],
3611
- completeTool: (line) => {
3612
- if (line.turn !== turn) return void 0;
3613
- const step = toolSteps.get(line.id);
3614
- if (step === void 0) return void 0;
3803
+ finalizeBefore,
3804
+ releaseTool: (line) => {
3805
+ if (line.turn !== turn) return;
3806
+ const entry = toolSteps.get(line.id);
3807
+ if (entry === void 0) return;
3615
3808
  toolSteps.delete(line.id);
3616
- return {
3617
- step,
3618
- opened: true
3619
- };
3809
+ const index = reservedSteps.indexOf(entry.step);
3810
+ if (index >= 0) reservedSteps.splice(index, 1);
3620
3811
  },
3621
- completeStream: (line) => {
3622
- if (line.turn !== turn) return void 0;
3623
- const kind = line.kind === "think" ? "think" : "text";
3624
- const entry = [...streams.entries()].find(([key, stream]) => key !== planKey && stream.kind === kind);
3625
- if (entry === void 0) return void 0;
3626
- const [key, stream] = entry;
3627
- liveFlush.cancel(stream.step);
3628
- streamPublisher?.finish(stream.step);
3629
- streams.delete(key);
3630
- if (activeStream === key) activeStream = void 0;
3812
+ completeStreams: (t, kinds) => {
3813
+ if (t !== turn) return void 0;
3814
+ const picked = [];
3815
+ for (const kind of kinds) {
3816
+ const entry = [...streams.entries()].find(([key, stream]) => key !== planKey && stream.kind === kind);
3817
+ if (entry !== void 0) picked.push(entry);
3818
+ }
3819
+ if (picked.length === 0) return void 0;
3820
+ const sealed = [];
3821
+ for (const [key, stream] of picked) {
3822
+ streams.delete(key);
3823
+ generationSteps.delete(stream.generation);
3824
+ if (activeStream === key) activeStream = void 0;
3825
+ if (stream.sealed === true) {
3826
+ sealed.push(stream.kind);
3827
+ continue;
3828
+ }
3829
+ liveFlush.cancel(stream.step);
3830
+ }
3831
+ const open = picked.filter(([, stream]) => stream.sealed !== true);
3832
+ if (open.length === 0) return { sealed };
3833
+ const held = new Set([...streams.values()].filter((stream) => stream.sealed !== true).map((stream) => stream.step));
3834
+ for (const [, stream] of open) {
3835
+ if (held.has(stream.step)) continue;
3836
+ const index = reservedSteps.indexOf(stream.step);
3837
+ if (index >= 0) reservedSteps.splice(index, 1);
3838
+ }
3839
+ for (const step of new Set(open.map(([, stream]) => stream.step))) streamPublisher?.finish(step);
3840
+ const written = /* @__PURE__ */ new Set();
3841
+ for (const event of childSession.snapshotEvents()) {
3842
+ const data = event.data;
3843
+ if (event.type === "step/start" && data.turn === turn && typeof data.step === "number") written.add(data.step);
3844
+ }
3845
+ let free = nextKimiSessionStep(childSession, turn);
3846
+ while (reservedSteps.includes(free)) free += 1;
3847
+ const candidates = [...new Set(open.map(([, stream]) => stream.step))].filter((step) => !written.has(step));
3631
3848
  return {
3632
- step: stream.step,
3633
- opened: stream.opened
3849
+ step: Math.min(free, ...candidates),
3850
+ sealed
3634
3851
  };
3635
3852
  }
3636
3853
  };
3637
- const mirrorOptions = () => ({ streams: mirrorStreams });
3854
+ const mirrorOptions = (settle = false) => ({
3855
+ streams: mirrorStreams,
3856
+ ...settle ? { settle: true } : {}
3857
+ });
3638
3858
  /**
3639
3859
  * Observation accounting across mirror passes: a pass whose window
3640
3860
  * attached its usage to a folded message is already carried; anything else
@@ -3643,7 +3863,7 @@ var KimiAcpLiveDriver = class {
3643
3863
  * the latest.
3644
3864
  */
3645
3865
  const noteMirrorUsage = (delta) => {
3646
- if (delta.usage !== void 0 && delta.usageAttached !== true) roundUsage = addTokenUsage(roundUsage, delta.usage);
3866
+ if (delta.usageUnattached !== void 0) roundUsage = addTokenUsage(roundUsage, delta.usageUnattached);
3647
3867
  if (delta.model !== void 0) roundModel = delta.model;
3648
3868
  };
3649
3869
  /** Throttled push-triggered mirror pass; the file fold stays the only transcript source. */
@@ -3695,23 +3915,22 @@ var KimiAcpLiveDriver = class {
3695
3915
  const stream = streams.get(key);
3696
3916
  if (stream !== void 0) {
3697
3917
  stream.text += text;
3698
- appendStreamSnapshot(stream, false, false);
3918
+ publishStreamSnapshot(stream, false);
3699
3919
  }
3700
3920
  }
3701
3921
  } else if (kind === "tool_call") {
3702
3922
  const id = typeof update["toolCallId"] === "string" ? update["toolCallId"] : void 0;
3703
3923
  if (id === void 0 || !seenToolCalls.has(id)) {
3704
- if (id !== void 0) {
3924
+ if (id !== void 0 && toolLogged(id)) seenToolCalls.add(id);
3925
+ else if (id !== void 0) {
3705
3926
  seenToolCalls.add(id);
3706
3927
  let step = nextKimiSessionStep(childSession, turn);
3707
- for (const reserved of reservedSteps) step = Math.max(step, reserved + 1);
3928
+ while (reservedSteps.includes(step)) step += 1;
3708
3929
  reservedSteps.push(step);
3709
- toolSteps.set(id, step);
3710
- childSession.append("step/start", {
3711
- turn,
3712
- step
3930
+ toolSteps.set(id, {
3931
+ step,
3932
+ ...typeof update["title"] === "string" ? { title: update["title"] } : {}
3713
3933
  });
3714
- persist();
3715
3934
  }
3716
3935
  generation++;
3717
3936
  }
@@ -3781,7 +4000,7 @@ var KimiAcpLiveDriver = class {
3781
4000
  let stableReads = 0;
3782
4001
  const deadline = Date.now() + SETTLE_MIRROR_QUIESCE_MS;
3783
4002
  for (;;) {
3784
- const delta = await mirrorKimiDelta(this.ctx, childSession, spec.homeDir, sessionId, mirrorOptions());
4003
+ const delta = await mirrorKimiDelta(this.ctx, childSession, spec.homeDir, sessionId, mirrorOptions(true));
3785
4004
  noteMirrorUsage(delta);
3786
4005
  localAgent?.reportRunProgress(childSession.id, {
3787
4006
  kind: "mirror",
@@ -3816,25 +4035,28 @@ var KimiAcpLiveDriver = class {
3816
4035
  } catch (error) {
3817
4036
  this.ctx.logger.warn(`subagent-kimi: live settle mirror failed: ${thrown$1(error).message}`);
3818
4037
  }
3819
- if (streams.size > 0) {
3820
- const remaining = [...streams.values()];
3821
- for (const [position, stream] of remaining.entries()) {
3822
- appendStreamSnapshot(stream, true, settled.stopReason !== "completed", position === remaining.length - 1, true);
3823
- streamPublisher?.finish(stream.step);
3824
- if (stream.opened) childSession.append("step/end", {
3825
- turn,
3826
- step: stream.step
3827
- });
3828
- }
3829
- streams.clear();
3830
- activeStream = void 0;
3831
- persist();
3832
- }
3833
- for (const step of toolSteps.values()) childSession.append("step/end", {
3834
- turn,
3835
- step
4038
+ const pending = [...[...[...streams.entries()].filter(([, stream]) => stream.sealed !== true).reduce((byStep, entry) => {
4039
+ const group = byStep.get(entry[1].step) ?? [];
4040
+ group.push(entry);
4041
+ byStep.set(entry[1].step, group);
4042
+ return byStep;
4043
+ }, /* @__PURE__ */ new Map()).entries()].map(([step, entries]) => ({
4044
+ step,
4045
+ entries
4046
+ })), ...[...toolSteps.entries()].map(([toolId, tool]) => ({
4047
+ step: tool.step,
4048
+ toolId,
4049
+ tool
4050
+ }))].sort((a, b) => a.step - b.step);
4051
+ const lastStream = pending.map((entry, index) => "entries" in entry ? index : -1).filter((index) => index >= 0).at(-1);
4052
+ pending.forEach((entry, index) => {
4053
+ if ("entries" in entry) finalizeStep(entry.step, entry.entries, settled.stopReason !== "completed", index === lastStream);
4054
+ else if (!toolLogged(entry.toolId)) finalizeOrphanTool(entry.toolId, entry.tool);
3836
4055
  });
4056
+ streams.clear();
3837
4057
  toolSteps.clear();
4058
+ activeStream = void 0;
4059
+ if (pending.length > 0) persist();
3838
4060
  if (settled.stopReason === "completed") childSession.append("turn/end", {
3839
4061
  turn,
3840
4062
  reason: { kind: "completed" }
@@ -4433,7 +4655,10 @@ async function mirrorKimiAfterExit(spec, stderr) {
4433
4655
  try {
4434
4656
  const kimiSessionId = spec.resume?.cliSessionId ?? kimiSessionIdFromOutput(stderr);
4435
4657
  if (spec.resume === void 0) spec.onCliSessionId?.(kimiSessionId);
4436
- const delta = await mirrorKimiDelta(spec.ctx, spec.childSession, spec.homeDir, kimiSessionId, { turn: spec.resume?.turn ?? 1 });
4658
+ const delta = await mirrorKimiDelta(spec.ctx, spec.childSession, spec.homeDir, kimiSessionId, {
4659
+ turn: spec.resume?.turn ?? 1,
4660
+ settle: true
4661
+ });
4437
4662
  spec.ctx.get("localAgent")?.reportRunProgress(spec.childSession.id, {
4438
4663
  kind: "mirror",
4439
4664
  mirroredLines: delta.total
@@ -4599,29 +4824,6 @@ async function startKimiCliRun(request, spec) {
4599
4824
  onAbort
4600
4825
  }).then((settled) => {
4601
4826
  if (settled.stopReason === "completed") spec.onAuthSuccess?.();
4602
- if (spec.childSession !== void 0) {
4603
- if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
4604
- turn,
4605
- reason: { kind: "completed" }
4606
- });
4607
- else if (settled.stopReason === "aborted") spec.childSession.append("turn/end", {
4608
- turn,
4609
- reason: {
4610
- kind: "aborted",
4611
- reason: { kind: "parent" }
4612
- }
4613
- });
4614
- else spec.childSession.append("turn/end", {
4615
- turn,
4616
- reason: {
4617
- kind: "error",
4618
- error: {
4619
- message: "kimi -p exited before the run completed",
4620
- code: "UNKNOWN"
4621
- }
4622
- }
4623
- });
4624
- }
4625
4827
  return settled;
4626
4828
  });
4627
4829
  result.then(() => child.done).then(() => {
@@ -4630,14 +4832,41 @@ async function startKimiCliRun(request, spec) {
4630
4832
  if (KIMI_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
4631
4833
  }
4632
4834
  enqueueMirror(async () => {
4633
- const settled = await mirrorKimiAfterExit(spec, stderr);
4634
- addRoundUsage(settled?.usage);
4835
+ const settled = await result;
4836
+ const mirrored = await mirrorKimiAfterExit(spec, stderr);
4837
+ addRoundUsage(mirrored?.usage);
4838
+ if (spec.childSession !== void 0) {
4839
+ if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
4840
+ turn,
4841
+ reason: { kind: "completed" }
4842
+ });
4843
+ else if (settled.stopReason === "aborted") spec.childSession.append("turn/end", {
4844
+ turn,
4845
+ reason: {
4846
+ kind: "aborted",
4847
+ reason: { kind: "parent" }
4848
+ }
4849
+ });
4850
+ else spec.childSession.append("turn/end", {
4851
+ turn,
4852
+ reason: {
4853
+ kind: "error",
4854
+ error: {
4855
+ message: "kimi -p exited before the run completed",
4856
+ code: "UNKNOWN"
4857
+ }
4858
+ }
4859
+ });
4860
+ if (spec.ctx !== void 0) await persistChildSession(spec.ctx, spec.childSession).catch((error) => {
4861
+ spec.ctx?.logger.warn(`subagent-kimi: turn/end persistence failed: ${thrown(error).message}`);
4862
+ });
4863
+ }
4635
4864
  const cliVersion = await spec.cliVersion?.();
4636
4865
  spec.onRoundSettled?.({
4637
- ...settled?.model === void 0 ? {} : { observedModel: settled.model },
4866
+ ...mirrored?.model === void 0 ? {} : { observedModel: mirrored.model },
4638
4867
  ...cliVersion === void 0 ? {} : { cliVersion },
4639
4868
  ...roundUsage === void 0 ? {} : { usage: roundUsage },
4640
- ...settled?.toolCalls === void 0 ? {} : { toolCalls: settled.toolCalls }
4869
+ ...mirrored?.toolCalls === void 0 ? {} : { toolCalls: mirrored.toolCalls }
4641
4870
  });
4642
4871
  });
4643
4872
  }, () => {});
@@ -4790,6 +5019,47 @@ function dedupe(candidates) {
4790
5019
  return out;
4791
5020
  }
4792
5021
  /**
5022
+ * Scope the served directory to the surface asking for it (the same helper
5023
+ * lives in each provider's broker — fold into the family core when the
5024
+ * directory faces next evolve):
5025
+ *
5026
+ * - memberless (the card's add-model dialog, the invite picker): the
5027
+ * endpoint's served catalog only — the configuration/history extras the
5028
+ * broker's info extension adds for STATUS display (a config-defined model
5029
+ * the endpoint does not serve) must never become add candidates;
5030
+ * - a member whose harness has a curated list: the list IS the vocabulary,
5031
+ * in the list's own order (the same rule `choices` already follows) — a
5032
+ * curated value the endpoint no longer serves stays as a bare row (the
5033
+ * card shows it the same way), and the member's current effective model is
5034
+ * kept visible as a bare entry when it sits outside the list, so a pre-list
5035
+ * binding never renders blank;
5036
+ * - a member without a curated list: the legacy extended vocabulary.
5037
+ */
5038
+ function scopeServedDirectory(directory, opts) {
5039
+ if (opts.member && opts.curated.length === 0) return directory;
5040
+ const served = directory.entries.filter((entry) => entry.source === "native");
5041
+ if (!opts.member) return {
5042
+ ...directory,
5043
+ entries: served
5044
+ };
5045
+ const byValue = new Map(served.flatMap((entry) => entry.resolvedModel === void 0 ? [[entry.value, entry]] : [[entry.value, entry], [entry.resolvedModel, entry]]));
5046
+ const entries = opts.curated.map((value) => byValue.get(value) ?? {
5047
+ value,
5048
+ label: value,
5049
+ source: "configuration"
5050
+ });
5051
+ const current = opts.current?.trim();
5052
+ if (current !== void 0 && current !== "" && !entries.some((entry) => entry.value === current || entry.resolvedModel === current)) entries.push({
5053
+ value: current,
5054
+ label: current,
5055
+ source: "configuration"
5056
+ });
5057
+ return {
5058
+ ...directory,
5059
+ entries
5060
+ };
5061
+ }
5062
+ /**
4793
5063
  * The kimi harness's {@link LocalAgentModelBroker}. Also the provider-facing
4794
5064
  * member-model ledger: the provider records each delegation's start model
4795
5065
  * here and reads the session-level override for its exec-path resolution, so
@@ -4854,14 +5124,19 @@ var KimiModelBroker = class {
4854
5124
  }
4855
5125
  async modelDirectory(childSessionId, refresh = false) {
4856
5126
  if (refresh) await this.deps.catalog?.refresh(childSessionId);
4857
- return (await this.modelInfo(childSessionId)).directory ?? {
5127
+ const info = await this.modelInfo(childSessionId);
5128
+ return scopeServedDirectory(info.directory ?? {
4858
5129
  entries: [],
4859
5130
  complete: false,
4860
5131
  customInput: true,
4861
5132
  status: "unsupported",
4862
5133
  refreshing: false,
4863
5134
  revision: 0
4864
- };
5135
+ }, {
5136
+ member: childSessionId !== void 0,
5137
+ curated: this.deps.modelList?.(this.deps.homeDir(childSessionId)) ?? [],
5138
+ current: info.effective
5139
+ });
4865
5140
  }
4866
5141
  async *followModelDirectory(childSessionId, signal) {
4867
5142
  if (this.deps.catalog === void 0) {