dsh-loop-engine 1.0.0-rc6 → 1.0.0-rc8

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.
Files changed (36) hide show
  1. package/README.md +43 -3
  2. package/README.zh.md +4 -3
  3. package/lib/client.js +82 -19
  4. package/lib/index.js +2249 -460
  5. package/lib/invariant.js +4 -3
  6. package/lib/types/client/LoopEngineBadge.d.ts +1 -1
  7. package/lib/types/client/LoopEngineComposerSelect.d.ts +1 -1
  8. package/lib/types/client/LoopEngineSection.d.ts +1 -1
  9. package/lib/types/client/index.d.ts +1 -1
  10. package/lib/types/client/locales.d.ts +4 -0
  11. package/lib/types/client/store.d.ts +2 -1
  12. package/lib/types/engine-claude/agent.d.ts +2 -0
  13. package/lib/types/engine-claude/loop.d.ts +24 -2
  14. package/lib/types/engine-claude/mapping.d.ts +3 -3
  15. package/lib/types/engine-codex/agent.d.ts +2 -0
  16. package/lib/types/engine-codex/appserver/mapping.d.ts +4 -4
  17. package/lib/types/engine-codex/loop.d.ts +24 -2
  18. package/lib/types/engine-kimi/acp/client.d.ts +76 -0
  19. package/lib/types/engine-kimi/acp/mapping.d.ts +44 -0
  20. package/lib/types/engine-kimi/acp/types.d.ts +95 -0
  21. package/lib/types/engine-kimi/agent.d.ts +123 -0
  22. package/lib/types/engine-kimi/commands.d.ts +40 -0
  23. package/lib/types/engine-kimi/loop.d.ts +108 -0
  24. package/lib/types/engine-kimi/mapping.d.ts +71 -0
  25. package/lib/types/engine-kimi/permission.d.ts +28 -0
  26. package/lib/types/engine-kimi/process.d.ts +61 -0
  27. package/lib/types/engine-kimi/skills.d.ts +57 -0
  28. package/lib/types/engine-kimi/types.d.ts +23 -0
  29. package/lib/types/engine-pi/agent.d.ts +2 -0
  30. package/lib/types/engine-pi/loop.d.ts +24 -2
  31. package/lib/types/index.d.ts +20 -2
  32. package/lib/types/patch-manager.d.ts +9 -0
  33. package/lib/types/preset.d.ts +73 -0
  34. package/lib/types/provider-route.d.ts +35 -0
  35. package/lib/types/settings.d.ts +6 -6
  36. package/package.json +24 -24
package/lib/index.js CHANGED
@@ -46,11 +46,10 @@ var __callDispose = (stack, error, hasError) => {
46
46
 
47
47
  // src/index.ts
48
48
  import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
49
- import { mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
50
- import { randomUUID } from "node:crypto";
51
- import { dirname as dirname4, join as join8 } from "node:path";
52
- import z5 from "@deepseek-ai/schemastery";
53
- import { installSettingsSection } from "@deepseek-ai/dsh-settings";
49
+ import { mkdir as mkdir2, readFile as readFile6, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
50
+ import { randomUUID as randomUUID2 } from "node:crypto";
51
+ import { dirname as dirname6, join as join11 } from "node:path";
52
+ import z6 from "@deepseek-ai/schemastery";
54
53
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
55
54
 
56
55
  // src/engine-claude/loop.ts
@@ -58,7 +57,7 @@ import { Service } from "@deepseek-ai/cordis";
58
57
  import z from "@deepseek-ai/schemastery";
59
58
  import { emitAgentEvent } from "@deepseek-ai/dsh-agent";
60
59
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
61
- import { SessionPreparation } from "@deepseek-ai/dsh-session";
60
+ import { interruptedTurnClosers, SessionLogOffset, SessionPreparation } from "@deepseek-ai/dsh-session";
62
61
 
63
62
  // src/engine-claude/agent.ts
64
63
  import { Inbox, agentEvents } from "@deepseek-ai/dsh-agent";
@@ -69,7 +68,7 @@ import { query as officialQuery } from "@anthropic-ai/claude-agent-sdk";
69
68
 
70
69
  // src/engine-claude/mapping.ts
71
70
  import {
72
- CallId,
71
+ ToolCallId,
73
72
  createToolResultMessage
74
73
  } from "@deepseek-ai/dsh-llm";
75
74
  function stringifyToolInput(input) {
@@ -88,7 +87,7 @@ function mapAssistantMessage(message) {
88
87
  content.push({ type: "text", text: block.text });
89
88
  break;
90
89
  case "tool_use": {
91
- const callId = CallId(block.id);
90
+ const callId = ToolCallId(block.id);
92
91
  content.push({
93
92
  type: "tool-call",
94
93
  id: callId,
@@ -123,7 +122,7 @@ function mapToolResults(message) {
123
122
  for (const block of content) {
124
123
  if (block.type !== "tool_result") continue;
125
124
  results.push(createToolResultMessage({
126
- callId: CallId(block.tool_use_id),
125
+ callId: ToolCallId(block.tool_use_id),
127
126
  content: toolResultContent(block.content),
128
127
  isError: block.is_error === true
129
128
  }));
@@ -166,7 +165,7 @@ function mapStreamEvent(event, toolCalls) {
166
165
  return [{ type: "block-start", index: event.index, blockType: "reasoning" }];
167
166
  }
168
167
  if (block.type === "tool_use") {
169
- toolCalls.set(event.index, { callId: CallId(block.id), name: block.name });
168
+ toolCalls.set(event.index, { callId: ToolCallId(block.id), name: block.name });
170
169
  return [{ type: "block-start", index: event.index, blockType: "tool-call" }];
171
170
  }
172
171
  return [];
@@ -184,7 +183,7 @@ function mapStreamEvent(event, toolCalls) {
184
183
  return [{
185
184
  type: "tool-call-delta",
186
185
  index: event.index,
187
- id: call?.callId ?? CallId(`call-${event.index}`),
186
+ id: call?.callId ?? ToolCallId(`call-${event.index}`),
188
187
  ...call === void 0 ? {} : { name: call.name },
189
188
  argumentsDelta: delta.partial_json
190
189
  }];
@@ -552,7 +551,7 @@ var ClaudeCodeAgent = class {
552
551
  this.dispatch.emit("agent/inbox/claimed", { message, turn });
553
552
  }
554
553
  });
555
- const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
554
+ const lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
556
555
  this.phase = { kind: "idle", lastTurn };
557
556
  this.scope = createScope(loopCtx, this);
558
557
  this.ctx = this.scope.ctx.extend({ agent: this });
@@ -756,7 +755,7 @@ var ClaudeCodeAgent = class {
756
755
  */
757
756
  queryPermission() {
758
757
  if (this.config.permissionMode !== void 0) return { permissionMode: this.config.permissionMode };
759
- const permission = resolveSessionPermission(this.session.events);
758
+ const permission = resolveSessionPermission(this.session.snapshotEvents());
760
759
  if (permission.kind === "bypass") return { permissionMode: "bypassPermissions" };
761
760
  if (permission.kind === "ask") {
762
761
  const approval = this.loopCtx.get("approval");
@@ -1162,7 +1161,7 @@ var ClaudeCodeLoop = class extends Service {
1162
1161
  * fuses caller cancellation with lifecycle teardown for setup awaits.
1163
1162
  */
1164
1163
  /* jscpd:ignore-start -- ownership/transaction machinery mirrors the default agent-loop factory; depending on agent-loop is forbidden. */
1165
- prepare(ownerCtx, id, options, session, callerSignal) {
1164
+ prepare(ownerCtx, id, options, session, callerSignal, handle) {
1166
1165
  ownerCtx.fiber.assertActive();
1167
1166
  if (!this.ownership.isActive()) throw new Error("agent loop is not active");
1168
1167
  if (callerSignal?.aborted) {
@@ -1196,11 +1195,15 @@ var ClaudeCodeLoop = class extends Service {
1196
1195
  }
1197
1196
  } finally {
1198
1197
  try {
1199
- detachAgent?.();
1200
- detachSession?.();
1198
+ await handle?.close();
1201
1199
  } finally {
1202
- untrack();
1203
- if (!ownerTriggered) await unfollowOwner();
1200
+ try {
1201
+ detachAgent?.();
1202
+ detachSession?.();
1203
+ } finally {
1204
+ untrack();
1205
+ if (!ownerTriggered) await unfollowOwner();
1206
+ }
1204
1207
  }
1205
1208
  }
1206
1209
  })();
@@ -1250,18 +1253,27 @@ var ClaudeCodeLoop = class extends Service {
1250
1253
  }
1251
1254
  }
1252
1255
  /** Prepare one Agent around an acquired Session, run setup, and publish it. */
1253
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
1256
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored) {
1254
1257
  var _stack = [];
1255
1258
  try {
1256
1259
  const ownedPreparation = __using(_stack, preparation);
1257
1260
  const session = ownedPreparation.session;
1258
- const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
1261
+ let prepared;
1262
+ try {
1263
+ prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle);
1264
+ } catch (error) {
1265
+ await stored?.handle.close().catch(() => {
1266
+ });
1267
+ throw error;
1268
+ }
1259
1269
  try {
1260
1270
  const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
1261
1271
  setupCommit?.commit();
1272
+ await this.appendUnstoredSuffix(stored, session);
1262
1273
  return prepared.publish(source);
1263
1274
  } catch (error) {
1264
- await prepared.dispose();
1275
+ await prepared.dispose().catch(() => {
1276
+ });
1265
1277
  throw error;
1266
1278
  }
1267
1279
  } catch (_) {
@@ -1272,7 +1284,8 @@ var ClaudeCodeLoop = class extends Service {
1272
1284
  }
1273
1285
  /**
1274
1286
  * Create an agent and session under one caller-supplied identity, owned by
1275
- * the accessing fiber.
1287
+ * the accessing fiber. When a persistence backend is mounted, the session's
1288
+ * durable identity is stored before publication.
1276
1289
  * @param ownerCtx - caller context that structurally owns the lifecycle.
1277
1290
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
1278
1291
  * @returns the published handle.
@@ -1280,20 +1293,73 @@ var ClaudeCodeLoop = class extends Service {
1280
1293
  async createAgent(ownerCtx, options) {
1281
1294
  const preparation = SessionPreparation.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
1282
1295
  ...options.seed === void 0 ? {} : { seed: options.seed },
1283
- ...options.meta === void 0 ? {} : { meta: options.meta }
1296
+ ...options.meta === void 0 ? {} : { meta: options.meta },
1297
+ ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
1284
1298
  }));
1285
- const published = this.setupAndPublish(
1286
- ownerCtx,
1287
- options.sessionId,
1288
- preparation,
1289
- options.agentOptions ?? {},
1290
- options.setup,
1291
- options.signal,
1292
- "startup"
1293
- );
1299
+ const published = (async () => {
1300
+ let stored;
1301
+ try {
1302
+ stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
1303
+ () => this.createStoredSession(preparation.session, options.signal),
1304
+ options.signal,
1305
+ options.sessionId,
1306
+ (abandoned) => {
1307
+ void abandoned?.handle.close().catch(() => {
1308
+ });
1309
+ }
1310
+ );
1311
+ } catch (error) {
1312
+ preparation[Symbol.dispose]();
1313
+ throw error;
1314
+ }
1315
+ return this.setupAndPublish(
1316
+ ownerCtx,
1317
+ options.sessionId,
1318
+ preparation,
1319
+ options.agentOptions ?? {},
1320
+ options.setup,
1321
+ options.signal,
1322
+ "startup",
1323
+ stored
1324
+ );
1325
+ })();
1294
1326
  this.ownership.trackWrapper(published);
1295
1327
  return published;
1296
1328
  }
1329
+ /**
1330
+ * Take a fresh session's write ownership when persistence is mounted.
1331
+ * Nothing is appended here: the constructor seed (which never re-emits
1332
+ * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
1333
+ * publication commit point, so a failed or cancelled setup closes an
1334
+ * unmaterialized handle and leaves no stored residue — the same id can be
1335
+ * created again.
1336
+ * @param session - the unpublished session to store.
1337
+ * @param signal - optional cancellation forwarded to the backend create.
1338
+ * @returns the owned handle and stored cursor, or `undefined` without a backend.
1339
+ */
1340
+ async createStoredSession(session, signal) {
1341
+ const persistence = this.runtime.ctx.get("sessionPersistence");
1342
+ if (persistence === void 0) return void 0;
1343
+ const handle = await persistence.create(session.header, {
1344
+ inheritedEventCount: session.inheritedEventCount,
1345
+ ...signal === void 0 ? {} : { signal }
1346
+ });
1347
+ return { handle, storedCount: 0 };
1348
+ }
1349
+ /**
1350
+ * Durably store the session events appended since the last stored cursor.
1351
+ * Pre-publication appends (constructor seed markers, setup-window events)
1352
+ * never re-emit through `session/event`, so publication must flush them
1353
+ * through the handle before live events start routing into it.
1354
+ * @param stored - the session's owned handle and stored cursor, if any.
1355
+ * @param session - the unpublished session whose suffix is stored.
1356
+ */
1357
+ async appendUnstoredSuffix(stored, session) {
1358
+ if (stored === void 0) return;
1359
+ const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
1360
+ if (suffix.length > 0) await stored.handle.append(suffix);
1361
+ stored.storedCount += suffix.length;
1362
+ }
1297
1363
  /**
1298
1364
  * Resume an owned agent from the configured persistence service.
1299
1365
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
@@ -1307,11 +1373,10 @@ var ClaudeCodeLoop = class extends Service {
1307
1373
  }
1308
1374
  return this.resumeWith(ownerCtx, persistence, options);
1309
1375
  }
1310
- /** Resume through an explicit persistence handle. */
1311
- async resumeWith(ownerCtx, persistence, options) {
1376
+ /** Resume through an explicit persistence service. */
1377
+ resumeWith(ownerCtx, persistence, options) {
1312
1378
  const id = options.resumeSessionId;
1313
- let preparation;
1314
- try {
1379
+ const published = (async () => {
1315
1380
  const ownerAbort = new AbortController();
1316
1381
  const unfollowOwner = ownerCtx.effect(() => () => {
1317
1382
  ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
@@ -1321,32 +1386,56 @@ var ClaudeCodeLoop = class extends Service {
1321
1386
  ownerAbort.signal,
1322
1387
  this.ownership.signal
1323
1388
  ]);
1389
+ let handle;
1390
+ let stored;
1391
+ let preparation;
1324
1392
  try {
1325
- preparation = await raceAbortCall(
1326
- () => persistence.prepare(id, fused),
1327
- fused,
1393
+ try {
1394
+ handle = await raceAbortCall(
1395
+ () => persistence.open(id, "write", { signal: fused }),
1396
+ fused,
1397
+ id,
1398
+ (abandoned) => {
1399
+ void abandoned.close();
1400
+ }
1401
+ );
1402
+ const persisted = await handle.read(0, void 0, { signal: fused });
1403
+ fused.throwIfAborted();
1404
+ const closers = interruptedTurnClosers(persisted);
1405
+ if (closers.length > 0) await handle.append(closers);
1406
+ preparation = SessionPreparation.create(this.runtime.ctx.sessions.prepare(id, {
1407
+ seed: [...persisted, ...closers],
1408
+ meta: structuredClone(handle.header),
1409
+ inheritedEventCount: handle.inheritedEventCount,
1410
+ seedSource: "persistence"
1411
+ }));
1412
+ stored = { handle, storedCount: persisted.length + closers.length };
1413
+ await this.appendUnstoredSuffix(stored, preparation.session);
1414
+ } finally {
1415
+ await unfollowOwner();
1416
+ }
1417
+ ownerCtx.fiber.assertActive();
1418
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
1419
+ const owned = stored;
1420
+ handle = void 0;
1421
+ return await this.setupAndPublish(
1422
+ ownerCtx,
1328
1423
  id,
1329
- (abandoned) => {
1330
- abandoned[Symbol.dispose]();
1331
- }
1424
+ preparation,
1425
+ options.agentOptions ?? {},
1426
+ options.setup,
1427
+ options.signal,
1428
+ "resume",
1429
+ owned
1332
1430
  );
1333
1431
  } finally {
1334
- await unfollowOwner();
1432
+ preparation?.[Symbol.dispose]();
1433
+ await handle?.close().catch(() => {
1434
+ });
1335
1435
  }
1336
- ownerCtx.fiber.assertActive();
1337
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
1338
- return await this.setupAndPublish(
1339
- ownerCtx,
1340
- id,
1341
- preparation,
1342
- options.agentOptions ?? {},
1343
- options.setup,
1344
- options.signal,
1345
- "resume"
1346
- );
1347
- } finally {
1348
- preparation?.[Symbol.dispose]();
1349
- }
1436
+ })();
1437
+ this.ownership.trackWrapper(published);
1438
+ return published;
1350
1439
  }
1351
1440
  };
1352
1441
 
@@ -1354,7 +1443,7 @@ var ClaudeCodeLoop = class extends Service {
1354
1443
  import { Service as Service2 } from "@deepseek-ai/cordis";
1355
1444
  import z2 from "@deepseek-ai/schemastery";
1356
1445
  import { emitAgentEvent as emitAgentEvent2 } from "@deepseek-ai/dsh-agent";
1357
- import { SessionPreparation as SessionPreparation2 } from "@deepseek-ai/dsh-session";
1446
+ import { interruptedTurnClosers as interruptedTurnClosers2, SessionLogOffset as SessionLogOffset2, SessionPreparation as SessionPreparation2 } from "@deepseek-ai/dsh-session";
1358
1447
 
1359
1448
  // src/engine-codex/agent.ts
1360
1449
  import { Inbox as Inbox2, agentEvents as agentEvents2 } from "@deepseek-ai/dsh-agent";
@@ -1441,7 +1530,7 @@ var AppServerClient = class _AppServerClient {
1441
1530
  clientInfo: {
1442
1531
  name: "dsh-loop-engine",
1443
1532
  title: null,
1444
- version: "0.1.1-rc.2"
1533
+ version: "1.0.0-rc8"
1445
1534
  },
1446
1535
  capabilities: { experimentalApi: true, requestAttestation: false }
1447
1536
  };
@@ -1478,8 +1567,8 @@ var AppServerClient = class _AppServerClient {
1478
1567
  }
1479
1568
  const id = this.reqId++;
1480
1569
  const msg = { jsonrpc: "2.0", id, method, params };
1481
- return new Promise((resolve4, reject) => {
1482
- this.pending.set(id, { resolve: resolve4, reject });
1570
+ return new Promise((resolve5, reject) => {
1571
+ this.pending.set(id, { resolve: resolve5, reject });
1483
1572
  this.process.stdin.write(JSON.stringify(msg) + "\n");
1484
1573
  });
1485
1574
  }
@@ -1530,7 +1619,7 @@ var AppServerThread = class _AppServerThread {
1530
1619
  const { signal, params } = options;
1531
1620
  const queue = [];
1532
1621
  const earlyNotifications = [];
1533
- let resolve4;
1622
+ let resolve5;
1534
1623
  let done = false;
1535
1624
  let turnError;
1536
1625
  let turnId;
@@ -1604,7 +1693,7 @@ var AppServerThread = class _AppServerThread {
1604
1693
  }
1605
1694
  if (event) {
1606
1695
  queue.push(event);
1607
- resolve4?.();
1696
+ resolve5?.();
1608
1697
  }
1609
1698
  };
1610
1699
  this.client.onNotification(notificationHandler);
@@ -1630,7 +1719,7 @@ var AppServerThread = class _AppServerThread {
1630
1719
  void this.client.turnInterrupt({ threadId: this.threadId, turnId }).catch(() => {
1631
1720
  });
1632
1721
  }
1633
- resolve4?.();
1722
+ resolve5?.();
1634
1723
  };
1635
1724
  signal?.addEventListener("abort", abortHandler, { once: true });
1636
1725
  try {
@@ -1640,9 +1729,9 @@ var AppServerThread = class _AppServerThread {
1640
1729
  yield queue.shift();
1641
1730
  } else {
1642
1731
  await new Promise((r) => {
1643
- resolve4 = r;
1732
+ resolve5 = r;
1644
1733
  });
1645
- resolve4 = void 0;
1734
+ resolve5 = void 0;
1646
1735
  }
1647
1736
  }
1648
1737
  if (turnError) throw turnError;
@@ -1656,7 +1745,7 @@ function noopNotificationHandler() {
1656
1745
  }
1657
1746
 
1658
1747
  // src/engine-codex/appserver/mapping.ts
1659
- import { CallId as CallId2, createToolResultMessage as createToolResultMessage2 } from "@deepseek-ai/dsh-llm";
1748
+ import { ToolCallId as ToolCallId2, createToolResultMessage as createToolResultMessage2 } from "@deepseek-ai/dsh-llm";
1660
1749
  function mapUsage2(usage) {
1661
1750
  return {
1662
1751
  inputTokens: usage.inputTokens,
@@ -1668,12 +1757,12 @@ function mapUsage2(usage) {
1668
1757
  function mapCommandExecution(item) {
1669
1758
  return {
1670
1759
  call: {
1671
- callId: CallId2(item.id),
1760
+ callId: ToolCallId2(item.id),
1672
1761
  name: "command_execution",
1673
1762
  arguments: JSON.stringify({ command: item.command ?? "" })
1674
1763
  },
1675
1764
  result: createToolResultMessage2({
1676
- callId: CallId2(item.id),
1765
+ callId: ToolCallId2(item.id),
1677
1766
  content: [{ type: "text", text: item.aggregatedOutput ?? "" }],
1678
1767
  isError: (item.exitCode ?? 0) !== 0 || item.status === "failed"
1679
1768
  })
@@ -1682,12 +1771,12 @@ function mapCommandExecution(item) {
1682
1771
  function mapFileChange(item) {
1683
1772
  return {
1684
1773
  call: {
1685
- callId: CallId2(item.id),
1774
+ callId: ToolCallId2(item.id),
1686
1775
  name: "apply_patch",
1687
1776
  arguments: JSON.stringify(item.changes ?? [])
1688
1777
  },
1689
1778
  result: createToolResultMessage2({
1690
- callId: CallId2(item.id),
1779
+ callId: ToolCallId2(item.id),
1691
1780
  content: [{ type: "text", text: `patch ${item.status ?? "completed"}` }],
1692
1781
  isError: item.status === "failed"
1693
1782
  })
@@ -1698,12 +1787,12 @@ function mapMcpToolCall(item) {
1698
1787
  const isError = item.error !== void 0 && item.error !== null;
1699
1788
  return {
1700
1789
  call: {
1701
- callId: CallId2(item.id),
1790
+ callId: ToolCallId2(item.id),
1702
1791
  name: name2,
1703
1792
  arguments: JSON.stringify(item.arguments ?? {})
1704
1793
  },
1705
1794
  result: createToolResultMessage2({
1706
- callId: CallId2(item.id),
1795
+ callId: ToolCallId2(item.id),
1707
1796
  content: isError ? [{ type: "text", text: item.error?.message ?? "tool call failed" }] : [{ type: "text", text: JSON.stringify(item.result?.content ?? []) }],
1708
1797
  isError
1709
1798
  })
@@ -1732,7 +1821,7 @@ var CodexAgent = class {
1732
1821
  this.dispatch.emit("agent/inbox/claimed", { message, turn });
1733
1822
  }
1734
1823
  });
1735
- const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
1824
+ const lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
1736
1825
  this.phase = { kind: "idle", lastTurn };
1737
1826
  this.scope = createScope2(loopCtx, this);
1738
1827
  this.ctx = this.scope.ctx.extend({ agent: this });
@@ -1945,7 +2034,7 @@ var CodexAgent = class {
1945
2034
  * @returns the permission fields of the query spec.
1946
2035
  */
1947
2036
  queryPermission() {
1948
- const fold = resolveSessionPermission2(this.session.events);
2037
+ const fold = resolveSessionPermission2(this.session.snapshotEvents());
1949
2038
  return {
1950
2039
  sandboxMode: this.config.sandboxMode ?? fold.sandboxMode,
1951
2040
  approvalPolicy: this.config.approvalPolicy ?? fold.approvalPolicy
@@ -2298,7 +2387,7 @@ var CodexLoop = class extends Service2 {
2298
2387
  * fuses caller cancellation with lifecycle teardown for setup awaits.
2299
2388
  */
2300
2389
  /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
2301
- prepare(ownerCtx, id, options, session, callerSignal) {
2390
+ prepare(ownerCtx, id, options, session, callerSignal, handle) {
2302
2391
  ownerCtx.fiber.assertActive();
2303
2392
  if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2304
2393
  if (callerSignal?.aborted) {
@@ -2332,11 +2421,15 @@ var CodexLoop = class extends Service2 {
2332
2421
  }
2333
2422
  } finally {
2334
2423
  try {
2335
- detachAgent?.();
2336
- detachSession?.();
2424
+ await handle?.close();
2337
2425
  } finally {
2338
- untrack();
2339
- if (!ownerTriggered) await unfollowOwner();
2426
+ try {
2427
+ detachAgent?.();
2428
+ detachSession?.();
2429
+ } finally {
2430
+ untrack();
2431
+ if (!ownerTriggered) await unfollowOwner();
2432
+ }
2340
2433
  }
2341
2434
  }
2342
2435
  })();
@@ -2386,18 +2479,27 @@ var CodexLoop = class extends Service2 {
2386
2479
  }
2387
2480
  }
2388
2481
  /** Prepare one Agent around an acquired Session, run setup, and publish it. */
2389
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
2482
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored) {
2390
2483
  var _stack = [];
2391
2484
  try {
2392
2485
  const ownedPreparation = __using(_stack, preparation);
2393
2486
  const session = ownedPreparation.session;
2394
- const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
2487
+ let prepared;
2488
+ try {
2489
+ prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle);
2490
+ } catch (error) {
2491
+ await stored?.handle.close().catch(() => {
2492
+ });
2493
+ throw error;
2494
+ }
2395
2495
  try {
2396
2496
  const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
2397
2497
  setupCommit?.commit();
2498
+ await this.appendUnstoredSuffix(stored, session);
2398
2499
  return prepared.publish(source);
2399
2500
  } catch (error) {
2400
- await prepared.dispose();
2501
+ await prepared.dispose().catch(() => {
2502
+ });
2401
2503
  throw error;
2402
2504
  }
2403
2505
  } catch (_) {
@@ -2408,7 +2510,8 @@ var CodexLoop = class extends Service2 {
2408
2510
  }
2409
2511
  /**
2410
2512
  * Create an agent and session under one caller-supplied identity, owned by
2411
- * the accessing fiber.
2513
+ * the accessing fiber. When a persistence backend is mounted, the session's
2514
+ * durable identity is stored before publication.
2412
2515
  * @param ownerCtx - caller context that structurally owns the lifecycle.
2413
2516
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
2414
2517
  * @returns the published handle.
@@ -2416,20 +2519,73 @@ var CodexLoop = class extends Service2 {
2416
2519
  async createAgent(ownerCtx, options) {
2417
2520
  const preparation = SessionPreparation2.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
2418
2521
  ...options.seed === void 0 ? {} : { seed: options.seed },
2419
- ...options.meta === void 0 ? {} : { meta: options.meta }
2522
+ ...options.meta === void 0 ? {} : { meta: options.meta },
2523
+ ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
2420
2524
  }));
2421
- const published = this.setupAndPublish(
2422
- ownerCtx,
2423
- options.sessionId,
2424
- preparation,
2425
- options.agentOptions ?? {},
2426
- options.setup,
2427
- options.signal,
2428
- "startup"
2429
- );
2525
+ const published = (async () => {
2526
+ let stored;
2527
+ try {
2528
+ stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
2529
+ () => this.createStoredSession(preparation.session, options.signal),
2530
+ options.signal,
2531
+ options.sessionId,
2532
+ (abandoned) => {
2533
+ void abandoned?.handle.close().catch(() => {
2534
+ });
2535
+ }
2536
+ );
2537
+ } catch (error) {
2538
+ preparation[Symbol.dispose]();
2539
+ throw error;
2540
+ }
2541
+ return this.setupAndPublish(
2542
+ ownerCtx,
2543
+ options.sessionId,
2544
+ preparation,
2545
+ options.agentOptions ?? {},
2546
+ options.setup,
2547
+ options.signal,
2548
+ "startup",
2549
+ stored
2550
+ );
2551
+ })();
2430
2552
  this.ownership.trackWrapper(published);
2431
2553
  return published;
2432
2554
  }
2555
+ /**
2556
+ * Take a fresh session's write ownership when persistence is mounted.
2557
+ * Nothing is appended here: the constructor seed (which never re-emits
2558
+ * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
2559
+ * publication commit point, so a failed or cancelled setup closes an
2560
+ * unmaterialized handle and leaves no stored residue — the same id can be
2561
+ * created again.
2562
+ * @param session - the unpublished session to store.
2563
+ * @param signal - optional cancellation forwarded to the backend create.
2564
+ * @returns the owned handle and stored cursor, or `undefined` without a backend.
2565
+ */
2566
+ async createStoredSession(session, signal) {
2567
+ const persistence = this.runtime.ctx.get("sessionPersistence");
2568
+ if (persistence === void 0) return void 0;
2569
+ const handle = await persistence.create(session.header, {
2570
+ inheritedEventCount: session.inheritedEventCount,
2571
+ ...signal === void 0 ? {} : { signal }
2572
+ });
2573
+ return { handle, storedCount: 0 };
2574
+ }
2575
+ /**
2576
+ * Durably store the session events appended since the last stored cursor.
2577
+ * Pre-publication appends (constructor seed markers, setup-window events)
2578
+ * never re-emit through `session/event`, so publication must flush them
2579
+ * through the handle before live events start routing into it.
2580
+ * @param stored - the session's owned handle and stored cursor, if any.
2581
+ * @param session - the unpublished session whose suffix is stored.
2582
+ */
2583
+ async appendUnstoredSuffix(stored, session) {
2584
+ if (stored === void 0) return;
2585
+ const suffix = session.snapshotEvents(SessionLogOffset2(stored.storedCount));
2586
+ if (suffix.length > 0) await stored.handle.append(suffix);
2587
+ stored.storedCount += suffix.length;
2588
+ }
2433
2589
  /**
2434
2590
  * Resume an owned agent from the configured persistence service.
2435
2591
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
@@ -2443,11 +2599,10 @@ var CodexLoop = class extends Service2 {
2443
2599
  }
2444
2600
  return this.resumeWith(ownerCtx, persistence, options);
2445
2601
  }
2446
- /** Resume through an explicit persistence handle. */
2447
- async resumeWith(ownerCtx, persistence, options) {
2602
+ /** Resume through an explicit persistence service. */
2603
+ resumeWith(ownerCtx, persistence, options) {
2448
2604
  const id = options.resumeSessionId;
2449
- let preparation;
2450
- try {
2605
+ const published = (async () => {
2451
2606
  const ownerAbort = new AbortController();
2452
2607
  const unfollowOwner = ownerCtx.effect(() => () => {
2453
2608
  ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
@@ -2457,32 +2612,56 @@ var CodexLoop = class extends Service2 {
2457
2612
  ownerAbort.signal,
2458
2613
  this.ownership.signal
2459
2614
  ]);
2615
+ let handle;
2616
+ let stored;
2617
+ let preparation;
2460
2618
  try {
2461
- preparation = await raceAbortCall(
2462
- () => persistence.prepare(id, fused),
2463
- fused,
2619
+ try {
2620
+ handle = await raceAbortCall(
2621
+ () => persistence.open(id, "write", { signal: fused }),
2622
+ fused,
2623
+ id,
2624
+ (abandoned) => {
2625
+ void abandoned.close();
2626
+ }
2627
+ );
2628
+ const persisted = await handle.read(0, void 0, { signal: fused });
2629
+ fused.throwIfAborted();
2630
+ const closers = interruptedTurnClosers2(persisted);
2631
+ if (closers.length > 0) await handle.append(closers);
2632
+ preparation = SessionPreparation2.create(this.runtime.ctx.sessions.prepare(id, {
2633
+ seed: [...persisted, ...closers],
2634
+ meta: structuredClone(handle.header),
2635
+ inheritedEventCount: handle.inheritedEventCount,
2636
+ seedSource: "persistence"
2637
+ }));
2638
+ stored = { handle, storedCount: persisted.length + closers.length };
2639
+ await this.appendUnstoredSuffix(stored, preparation.session);
2640
+ } finally {
2641
+ await unfollowOwner();
2642
+ }
2643
+ ownerCtx.fiber.assertActive();
2644
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2645
+ const owned = stored;
2646
+ handle = void 0;
2647
+ return await this.setupAndPublish(
2648
+ ownerCtx,
2464
2649
  id,
2465
- (abandoned) => {
2466
- abandoned[Symbol.dispose]();
2467
- }
2650
+ preparation,
2651
+ options.agentOptions ?? {},
2652
+ options.setup,
2653
+ options.signal,
2654
+ "resume",
2655
+ owned
2468
2656
  );
2469
2657
  } finally {
2470
- await unfollowOwner();
2658
+ preparation?.[Symbol.dispose]();
2659
+ await handle?.close().catch(() => {
2660
+ });
2471
2661
  }
2472
- ownerCtx.fiber.assertActive();
2473
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2474
- return await this.setupAndPublish(
2475
- ownerCtx,
2476
- id,
2477
- preparation,
2478
- options.agentOptions ?? {},
2479
- options.setup,
2480
- options.signal,
2481
- "resume"
2482
- );
2483
- } finally {
2484
- preparation?.[Symbol.dispose]();
2485
- }
2662
+ })();
2663
+ this.ownership.trackWrapper(published);
2664
+ return published;
2486
2665
  }
2487
2666
  };
2488
2667
 
@@ -2493,11 +2672,11 @@ import { fileURLToPath } from "node:url";
2493
2672
  import { Service as Service3 } from "@deepseek-ai/cordis";
2494
2673
  import z3 from "@deepseek-ai/schemastery";
2495
2674
  import { emitAgentEvent as emitAgentEvent3 } from "@deepseek-ai/dsh-agent";
2496
- import { SessionPreparation as SessionPreparation3 } from "@deepseek-ai/dsh-session";
2675
+ import { interruptedTurnClosers as interruptedTurnClosers3, SessionLogOffset as SessionLogOffset3, SessionPreparation as SessionPreparation3 } from "@deepseek-ai/dsh-session";
2497
2676
 
2498
2677
  // src/engine-pi/agent.ts
2499
2678
  import { Inbox as Inbox3, agentEvents as agentEvents3 } from "@deepseek-ai/dsh-agent";
2500
- import { CallId as CallId4, LlmError as LlmError3, createAssistantMessage as createAssistantMessage3, createUserMessage as createUserMessage3, errorChain as errorChain3 } from "@deepseek-ai/dsh-llm";
2679
+ import { ToolCallId as ToolCallId4, LlmError as LlmError3, createAssistantMessage as createAssistantMessage3, createUserMessage as createUserMessage3, errorChain as errorChain3 } from "@deepseek-ai/dsh-llm";
2501
2680
  import { createScope as createScope3 } from "@deepseek-ai/dsh-scope";
2502
2681
  import { canonicalHeader as canonicalHeader3 } from "@deepseek-ai/dsh-session";
2503
2682
 
@@ -2591,8 +2770,8 @@ var PiRpcClient = class _PiRpcClient {
2591
2770
  * absent falls back to the plain node child spawn.
2592
2771
  * @returns the connected client.
2593
2772
  */
2594
- static create(spec, spawn3) {
2595
- const process2 = spawn3 === void 0 ? defaultSpawn(spec) : spawn3(spec);
2773
+ static create(spec, spawn4) {
2774
+ const process2 = spawn4 === void 0 ? defaultSpawn(spec) : spawn4(spec);
2596
2775
  return new _PiRpcClient(process2);
2597
2776
  }
2598
2777
  /** Register the event dispatch handler. */
@@ -2637,8 +2816,8 @@ var PiRpcClient = class _PiRpcClient {
2637
2816
  if (this.disposed) throw new Error("pi RPC client is disposed");
2638
2817
  const id = command.id ?? this.reqId++;
2639
2818
  const wire = { ...command, id };
2640
- return new Promise((resolve4, reject) => {
2641
- this.pending.set(id, { resolve: resolve4, reject });
2819
+ return new Promise((resolve5, reject) => {
2820
+ this.pending.set(id, { resolve: resolve5, reject });
2642
2821
  this.process.stdin.write(`${JSON.stringify(wire)}
2643
2822
  `);
2644
2823
  });
@@ -2655,8 +2834,8 @@ var PiRpcClient = class _PiRpcClient {
2655
2834
  continue;
2656
2835
  }
2657
2836
  if (this.disposed) return;
2658
- await new Promise((resolve4) => {
2659
- this.eventWake = resolve4;
2837
+ await new Promise((resolve5) => {
2838
+ this.eventWake = resolve5;
2660
2839
  });
2661
2840
  this.eventWake = void 0;
2662
2841
  }
@@ -2717,7 +2896,7 @@ var PiRpcClient = class _PiRpcClient {
2717
2896
 
2718
2897
  // src/engine-pi/rpc/mapping.ts
2719
2898
  import {
2720
- CallId as CallId3,
2899
+ ToolCallId as ToolCallId3,
2721
2900
  createToolResultMessage as createToolResultMessage3
2722
2901
  } from "@deepseek-ai/dsh-llm";
2723
2902
  function mapUsage3(usage) {
@@ -2748,7 +2927,7 @@ function resultText(content) {
2748
2927
  }
2749
2928
  function mapToolResult(ev) {
2750
2929
  return createToolResultMessage3({
2751
- callId: CallId3(ev.toolCallId),
2930
+ callId: ToolCallId3(ev.toolCallId),
2752
2931
  content: [{ type: "text", text: resultText(ev.result) || "(no content)" }],
2753
2932
  isError: ev.isError
2754
2933
  });
@@ -2763,13 +2942,13 @@ function specsEqual(a, b) {
2763
2942
  return a.cwd === b.cwd && a.env === b.env && a.argv.length === b.argv.length && a.argv.every((value, index) => value === b.argv[index]);
2764
2943
  }
2765
2944
  var PiAgent = class {
2766
- constructor(loopCtx, id, options, session, config, spawn3, bin) {
2945
+ constructor(loopCtx, id, options, session, config, spawn4, bin) {
2767
2946
  this.loopCtx = loopCtx;
2768
2947
  this.id = id;
2769
2948
  this.options = options;
2770
2949
  this.session = session;
2771
2950
  this.config = config;
2772
- this.spawn = spawn3;
2951
+ this.spawn = spawn4;
2773
2952
  this.bin = bin;
2774
2953
  this.dispatch = agentEvents3(loopCtx, this);
2775
2954
  this.inbox = new Inbox3(session, {
@@ -2783,7 +2962,7 @@ var PiAgent = class {
2783
2962
  this.dispatch.emit("agent/inbox/claimed", { message, turn });
2784
2963
  }
2785
2964
  });
2786
- const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
2965
+ const lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
2787
2966
  this.phase = { kind: "idle", lastTurn };
2788
2967
  this.scope = createScope3(loopCtx, this);
2789
2968
  this.ctx = this.scope.ctx.extend({ agent: this });
@@ -3004,7 +3183,7 @@ var PiAgent = class {
3004
3183
  * @returns the permission fields of the query spec.
3005
3184
  */
3006
3185
  queryPermission() {
3007
- const fold = resolveSessionPermission3(this.session.events);
3186
+ const fold = resolveSessionPermission3(this.session.snapshotEvents());
3008
3187
  const sandboxMode = this.config.sandboxMode ?? fold.sandboxMode;
3009
3188
  return {
3010
3189
  sandboxMode,
@@ -3215,7 +3394,7 @@ var PiAgent = class {
3215
3394
  this.session.append("tool/call", {
3216
3395
  turn,
3217
3396
  step,
3218
- callId: CallId4(callId),
3397
+ callId: ToolCallId4(callId),
3219
3398
  name: name2,
3220
3399
  arguments: argumentsValue
3221
3400
  });
@@ -3316,8 +3495,8 @@ var PiAgent = class {
3316
3495
  held = { content: contentOf(event.message), refs: [...chunkSeqs] };
3317
3496
  chunkSeqs.length = 0;
3318
3497
  }
3319
- for (const toolResult of event.toolResults ?? []) {
3320
- this.appendToolResult(turn, step, toolResult);
3498
+ for (const toolResult2 of event.toolResults ?? []) {
3499
+ this.appendToolResult(turn, step, toolResult2);
3321
3500
  }
3322
3501
  flushHeld(lastUsage);
3323
3502
  finished = true;
@@ -3350,15 +3529,15 @@ var PiAgent = class {
3350
3529
  }
3351
3530
  }
3352
3531
  /** Append one Pi tool result to the durable log as a `tool/result` message. */
3353
- appendToolResult(turn, step, toolResult) {
3354
- const text = typeof toolResult.content === "string" ? toolResult.content : toolResult.content.map((block) => block.type === "text" ? block.text : "").filter((segment) => segment !== "").join("\n\n");
3532
+ appendToolResult(turn, step, toolResult2) {
3533
+ const text = typeof toolResult2.content === "string" ? toolResult2.content : toolResult2.content.map((block) => block.type === "text" ? block.text : "").filter((segment) => segment !== "").join("\n\n");
3355
3534
  this.session.append("tool/result", {
3356
3535
  turn,
3357
3536
  step,
3358
3537
  message: mapToolResult({
3359
- toolCallId: toolResult.toolCallId,
3538
+ toolCallId: toolResult2.toolCallId,
3360
3539
  result: { content: [{ type: "text", text }] },
3361
- isError: toolResult.isError === true
3540
+ isError: toolResult2.isError === true
3362
3541
  })
3363
3542
  }, { surfaceOp: "append" });
3364
3543
  }
@@ -3452,7 +3631,7 @@ var PiLoop = class extends Service3 {
3452
3631
  * fuses caller cancellation with lifecycle teardown for setup awaits.
3453
3632
  */
3454
3633
  /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
3455
- prepare(ownerCtx, id, options, session, callerSignal) {
3634
+ prepare(ownerCtx, id, options, session, callerSignal, handle) {
3456
3635
  ownerCtx.fiber.assertActive();
3457
3636
  if (!this.ownership.isActive()) throw new Error("agent loop is not active");
3458
3637
  if (callerSignal?.aborted) {
@@ -3486,11 +3665,15 @@ var PiLoop = class extends Service3 {
3486
3665
  }
3487
3666
  } finally {
3488
3667
  try {
3489
- detachAgent?.();
3490
- detachSession?.();
3668
+ await handle?.close();
3491
3669
  } finally {
3492
- untrack();
3493
- if (!ownerTriggered) await unfollowOwner();
3670
+ try {
3671
+ detachAgent?.();
3672
+ detachSession?.();
3673
+ } finally {
3674
+ untrack();
3675
+ if (!ownerTriggered) await unfollowOwner();
3676
+ }
3494
3677
  }
3495
3678
  }
3496
3679
  })();
@@ -3540,18 +3723,27 @@ var PiLoop = class extends Service3 {
3540
3723
  }
3541
3724
  }
3542
3725
  /** Prepare one Agent around an acquired Session, run setup, and publish it. */
3543
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
3726
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored) {
3544
3727
  var _stack = [];
3545
3728
  try {
3546
3729
  const ownedPreparation = __using(_stack, preparation);
3547
3730
  const session = ownedPreparation.session;
3548
- const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
3731
+ let prepared;
3732
+ try {
3733
+ prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle);
3734
+ } catch (error) {
3735
+ await stored?.handle.close().catch(() => {
3736
+ });
3737
+ throw error;
3738
+ }
3549
3739
  try {
3550
3740
  const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
3551
3741
  setupCommit?.commit();
3742
+ await this.appendUnstoredSuffix(stored, session);
3552
3743
  return prepared.publish(source);
3553
3744
  } catch (error) {
3554
- await prepared.dispose();
3745
+ await prepared.dispose().catch(() => {
3746
+ });
3555
3747
  throw error;
3556
3748
  }
3557
3749
  } catch (_) {
@@ -3562,7 +3754,8 @@ var PiLoop = class extends Service3 {
3562
3754
  }
3563
3755
  /**
3564
3756
  * Create an agent and session under one caller-supplied identity, owned by
3565
- * the accessing fiber.
3757
+ * the accessing fiber. When a persistence backend is mounted, the session's
3758
+ * durable identity is stored before publication.
3566
3759
  * @param ownerCtx - caller context that structurally owns the lifecycle.
3567
3760
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
3568
3761
  * @returns the published handle.
@@ -3570,20 +3763,73 @@ var PiLoop = class extends Service3 {
3570
3763
  async createAgent(ownerCtx, options) {
3571
3764
  const preparation = SessionPreparation3.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
3572
3765
  ...options.seed === void 0 ? {} : { seed: options.seed },
3573
- ...options.meta === void 0 ? {} : { meta: options.meta }
3766
+ ...options.meta === void 0 ? {} : { meta: options.meta },
3767
+ ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
3574
3768
  }));
3575
- const published = this.setupAndPublish(
3576
- ownerCtx,
3577
- options.sessionId,
3578
- preparation,
3579
- options.agentOptions ?? {},
3580
- options.setup,
3581
- options.signal,
3582
- "startup"
3583
- );
3769
+ const published = (async () => {
3770
+ let stored;
3771
+ try {
3772
+ stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
3773
+ () => this.createStoredSession(preparation.session, options.signal),
3774
+ options.signal,
3775
+ options.sessionId,
3776
+ (abandoned) => {
3777
+ void abandoned?.handle.close().catch(() => {
3778
+ });
3779
+ }
3780
+ );
3781
+ } catch (error) {
3782
+ preparation[Symbol.dispose]();
3783
+ throw error;
3784
+ }
3785
+ return this.setupAndPublish(
3786
+ ownerCtx,
3787
+ options.sessionId,
3788
+ preparation,
3789
+ options.agentOptions ?? {},
3790
+ options.setup,
3791
+ options.signal,
3792
+ "startup",
3793
+ stored
3794
+ );
3795
+ })();
3584
3796
  this.ownership.trackWrapper(published);
3585
3797
  return published;
3586
3798
  }
3799
+ /**
3800
+ * Take a fresh session's write ownership when persistence is mounted.
3801
+ * Nothing is appended here: the constructor seed (which never re-emits
3802
+ * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
3803
+ * publication commit point, so a failed or cancelled setup closes an
3804
+ * unmaterialized handle and leaves no stored residue — the same id can be
3805
+ * created again.
3806
+ * @param session - the unpublished session to store.
3807
+ * @param signal - optional cancellation forwarded to the backend create.
3808
+ * @returns the owned handle and stored cursor, or `undefined` without a backend.
3809
+ */
3810
+ async createStoredSession(session, signal) {
3811
+ const persistence = this.runtime.ctx.get("sessionPersistence");
3812
+ if (persistence === void 0) return void 0;
3813
+ const handle = await persistence.create(session.header, {
3814
+ inheritedEventCount: session.inheritedEventCount,
3815
+ ...signal === void 0 ? {} : { signal }
3816
+ });
3817
+ return { handle, storedCount: 0 };
3818
+ }
3819
+ /**
3820
+ * Durably store the session events appended since the last stored cursor.
3821
+ * Pre-publication appends (constructor seed markers, setup-window events)
3822
+ * never re-emit through `session/event`, so publication must flush them
3823
+ * through the handle before live events start routing into it.
3824
+ * @param stored - the session's owned handle and stored cursor, if any.
3825
+ * @param session - the unpublished session whose suffix is stored.
3826
+ */
3827
+ async appendUnstoredSuffix(stored, session) {
3828
+ if (stored === void 0) return;
3829
+ const suffix = session.snapshotEvents(SessionLogOffset3(stored.storedCount));
3830
+ if (suffix.length > 0) await stored.handle.append(suffix);
3831
+ stored.storedCount += suffix.length;
3832
+ }
3587
3833
  /**
3588
3834
  * Resume an owned agent from the configured persistence service.
3589
3835
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
@@ -3597,11 +3843,10 @@ var PiLoop = class extends Service3 {
3597
3843
  }
3598
3844
  return this.resumeWith(ownerCtx, persistence, options);
3599
3845
  }
3600
- /** Resume through an explicit persistence handle. */
3601
- async resumeWith(ownerCtx, persistence, options) {
3846
+ /** Resume through an explicit persistence service. */
3847
+ resumeWith(ownerCtx, persistence, options) {
3602
3848
  const id = options.resumeSessionId;
3603
- let preparation;
3604
- try {
3849
+ const published = (async () => {
3605
3850
  const ownerAbort = new AbortController();
3606
3851
  const unfollowOwner = ownerCtx.effect(() => () => {
3607
3852
  ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
@@ -3611,203 +3856,1181 @@ var PiLoop = class extends Service3 {
3611
3856
  ownerAbort.signal,
3612
3857
  this.ownership.signal
3613
3858
  ]);
3859
+ let handle;
3860
+ let stored;
3861
+ let preparation;
3614
3862
  try {
3615
- preparation = await raceAbortCall(
3616
- () => persistence.prepare(id, fused),
3617
- fused,
3863
+ try {
3864
+ handle = await raceAbortCall(
3865
+ () => persistence.open(id, "write", { signal: fused }),
3866
+ fused,
3867
+ id,
3868
+ (abandoned) => {
3869
+ void abandoned.close();
3870
+ }
3871
+ );
3872
+ const persisted = await handle.read(0, void 0, { signal: fused });
3873
+ fused.throwIfAborted();
3874
+ const closers = interruptedTurnClosers3(persisted);
3875
+ if (closers.length > 0) await handle.append(closers);
3876
+ preparation = SessionPreparation3.create(this.runtime.ctx.sessions.prepare(id, {
3877
+ seed: [...persisted, ...closers],
3878
+ meta: structuredClone(handle.header),
3879
+ inheritedEventCount: handle.inheritedEventCount,
3880
+ seedSource: "persistence"
3881
+ }));
3882
+ stored = { handle, storedCount: persisted.length + closers.length };
3883
+ await this.appendUnstoredSuffix(stored, preparation.session);
3884
+ } finally {
3885
+ await unfollowOwner();
3886
+ }
3887
+ ownerCtx.fiber.assertActive();
3888
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
3889
+ const owned = stored;
3890
+ handle = void 0;
3891
+ return await this.setupAndPublish(
3892
+ ownerCtx,
3618
3893
  id,
3619
- (abandoned) => {
3620
- abandoned[Symbol.dispose]();
3621
- }
3894
+ preparation,
3895
+ options.agentOptions ?? {},
3896
+ options.setup,
3897
+ options.signal,
3898
+ "resume",
3899
+ owned
3622
3900
  );
3623
3901
  } finally {
3624
- await unfollowOwner();
3902
+ preparation?.[Symbol.dispose]();
3903
+ await handle?.close().catch(() => {
3904
+ });
3625
3905
  }
3626
- ownerCtx.fiber.assertActive();
3627
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
3628
- return await this.setupAndPublish(
3629
- ownerCtx,
3630
- id,
3631
- preparation,
3632
- options.agentOptions ?? {},
3633
- options.setup,
3634
- options.signal,
3635
- "resume"
3636
- );
3637
- } finally {
3638
- preparation?.[Symbol.dispose]();
3639
- }
3906
+ })();
3907
+ this.ownership.trackWrapper(published);
3908
+ return published;
3640
3909
  }
3641
3910
  };
3642
3911
 
3643
- // src/settings.ts
3912
+ // src/engine-kimi/loop.ts
3913
+ import { Service as Service4 } from "@deepseek-ai/cordis";
3644
3914
  import z4 from "@deepseek-ai/schemastery";
3645
- import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3915
+ import { emitAgentEvent as emitAgentEvent4 } from "@deepseek-ai/dsh-agent";
3916
+ import { interruptedTurnClosers as interruptedTurnClosers4, SessionLogOffset as SessionLogOffset4, SessionPreparation as SessionPreparation4 } from "@deepseek-ai/dsh-session";
3646
3917
 
3647
- // src/namespace.ts
3648
- var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
3918
+ // src/engine-kimi/agent.ts
3919
+ import { Inbox as Inbox4, agentEvents as agentEvents4 } from "@deepseek-ai/dsh-agent";
3920
+ import { ToolCallId as ToolCallId6, LlmError as LlmError4, createAssistantMessage as createAssistantMessage4, createUserMessage as createUserMessage4, errorChain as errorChain4 } from "@deepseek-ai/dsh-llm";
3921
+ import { createScope as createScope4 } from "@deepseek-ai/dsh-scope";
3922
+ import { canonicalHeader as canonicalHeader4 } from "@deepseek-ai/dsh-session";
3649
3923
 
3650
- // src/settings.ts
3651
- var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi"];
3652
- var LOOP_ENGINE_SETTINGS_SCHEMA = z4.object({
3653
- engine: z4.union([z4.const("in-process"), z4.const("claude-code"), z4.const("codex"), z4.const("pi")]).default("in-process"),
3654
- showInComposer: z4.boolean().default(true)
3655
- });
3656
- function loopEngineSettingsNamespace() {
3657
- return settingsNamespace(LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL);
3924
+ // src/engine-kimi/process.ts
3925
+ import { existsSync } from "node:fs";
3926
+ import { homedir } from "node:os";
3927
+ import { join as join3 } from "node:path";
3928
+ function kimiHomeDir() {
3929
+ const envHome = process.env.KIMI_CODE_HOME;
3930
+ return envHome !== void 0 && envHome !== "" ? envHome : join3(homedir(), ".kimi-code");
3658
3931
  }
3659
-
3660
- // src/patch-manager.ts
3661
- var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
3662
- var MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
3663
- var END_MARKER_LINE = `${MANAGED_BLOCK_END}
3664
- `;
3665
- function renderManagedBlock(engine) {
3666
- if (engine === "in-process") return "";
3667
- return [
3668
- `${MANAGED_BLOCK_BEGIN}${engine} --`,
3669
- "- id: agent-loop",
3670
- " disabled: true",
3671
- END_MARKER_LINE
3672
- ].join("\n");
3932
+ function kimiBinResolver(configBin) {
3933
+ if (configBin !== void 0 && configBin !== "") return configBin;
3934
+ const executable = process.platform === "win32" ? "kimi.exe" : "kimi";
3935
+ const candidate = join3(kimiHomeDir(), "bin", executable);
3936
+ return existsSync(candidate) ? candidate : "kimi";
3673
3937
  }
3674
- var BEGIN_MARKER_RE = /^# -- dsh-loop-engine managed block: (\S+) --$/m;
3675
- function currentEngineOf(text) {
3676
- const engine = BEGIN_MARKER_RE.exec(text)?.[1];
3677
- return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : "in-process";
3938
+ function kimiAcpArgv(bin) {
3939
+ return [bin, "acp"];
3678
3940
  }
3679
- function managedSpan(text) {
3680
- const begin = text.indexOf(MANAGED_BLOCK_BEGIN);
3681
- if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
3682
- const afterBegin = begin + MANAGED_BLOCK_BEGIN.length;
3683
- const endAt = text.indexOf(MANAGED_BLOCK_END, afterBegin);
3684
- const spanEnd = endAt === -1 ? text.length : endAt + END_MARKER_LINE.length;
3685
- const before = text.slice(0, begin);
3686
- const blankBefore = before.endsWith("\n\n");
3941
+ function kimiSubprocessSpec(spec, graceMs) {
3687
3942
  return {
3688
- head: blankBefore ? before.slice(0, -1) : before,
3689
- tail: text.slice(spanEnd),
3690
- present: true,
3691
- blankBefore
3943
+ argv: [...spec.argv],
3944
+ cwd: spec.cwd,
3945
+ stdio: { stdin: "pipe", stdout: "pipe", stderr: "pipe" },
3946
+ graceMs,
3947
+ env: spec.env,
3948
+ ...spec.signal === void 0 ? {} : { signal: spec.signal }
3692
3949
  };
3693
3950
  }
3694
- function ensureTrailingNewline(text) {
3695
- return text.endsWith("\n") ? text : `${text}
3696
- `;
3697
- }
3698
- function hasRootEntry(text) {
3699
- return /^(?:- |\[)/m.test(text);
3700
- }
3701
- function dropSeedPlaceholder(text) {
3702
- return text.replace(/^\[\]\n/m, "");
3703
- }
3704
- function seedEmptyArray(text) {
3705
- const head = text.replace(/\n+$/, "");
3706
- return head === "" ? "[]\n" : `${head}
3707
- []
3708
- `;
3709
- }
3710
- function applyManagedBlock(text, engine) {
3711
- const block = renderManagedBlock(engine);
3712
- const span = managedSpan(text);
3713
- let result;
3714
- if (!span.present) {
3715
- if (block === "") {
3716
- result = text;
3717
- } else {
3718
- const base = ensureTrailingNewline(text);
3719
- result = `${base}
3720
- ${block}`;
3721
- }
3722
- } else if (block === "") {
3723
- result = span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
3724
- } else {
3725
- result = `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
3951
+ function fromSubprocess2(handle) {
3952
+ const { stdin, stdout, stderr } = handle;
3953
+ if (stdin === void 0 || stdout === void 0 || stderr === void 0) {
3954
+ throw new Error("agent-loop-kimi: spawned child must pipe stdin/stdout/stderr");
3726
3955
  }
3727
- if (block !== "") return dropSeedPlaceholder(result);
3728
- if (text.trim() === "") return result;
3729
- return hasRootEntry(result) ? result : seedEmptyArray(result);
3956
+ return {
3957
+ stdin,
3958
+ stdout,
3959
+ stderr,
3960
+ done: handle.done,
3961
+ terminate: () => handle.terminate()
3962
+ };
3730
3963
  }
3731
3964
 
3732
- // src/commands.ts
3733
- import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
3734
- import { homedir } from "node:os";
3735
- import { join as join3 } from "node:path";
3736
- import { createUserMessage as createUserMessage4 } from "@deepseek-ai/dsh-llm";
3737
- var COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
3738
- function forwardClaudeCodeCommand(name2) {
3739
- return (invocation) => {
3740
- invocation.agent.followup(createUserMessage4({
3741
- content: [{ type: "text", text: `/${name2}${invocation.rawInput}` }],
3742
- source: { kind: "user" }
3743
- }));
3744
- return { kind: "success" };
3745
- };
3965
+ // src/engine-kimi/acp/client.ts
3966
+ import { spawn as spawn3 } from "node:child_process";
3967
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
3968
+
3969
+ // src/engine-kimi/acp/types.ts
3970
+ function isUpdateFrame(frame2) {
3971
+ return frame2.method === "session/update" && typeof frame2.params === "object" && frame2.params !== null && typeof frame2.params.update === "object" && frame2.params.update !== null;
3746
3972
  }
3747
- function builtin(name2, description) {
3748
- return { name: name2, description, handler: forwardClaudeCodeCommand(name2) };
3973
+ function isPermissionRequestFrame(frame2) {
3974
+ return frame2.method === "session/request_permission" && typeof frame2.id === "number";
3749
3975
  }
3750
- var CLAUDE_CODE_COMMANDS = [
3751
- builtin("help", "Show help about Claude Code commands"),
3752
- builtin("compact", "Compact the conversation to reduce context usage"),
3753
- builtin("clear", "Clear the conversation and start fresh"),
3754
- builtin("review", "Review recent changes (git diff)"),
3755
- builtin("explain", "Explain the selected code"),
3756
- builtin("fix", "Fix issues in the code"),
3757
- builtin("tests", "Add tests for the selected code")
3758
- ];
3759
- function discoverUserSlashCommands() {
3760
- let entries;
3761
- try {
3762
- entries = readdirSync(userCommandsDir(), { encoding: "utf8" });
3763
- } catch {
3764
- return [];
3976
+
3977
+ // src/engine-kimi/acp/client.ts
3978
+ function defaultSpawn2(spec) {
3979
+ const child = spawn3(spec.argv[0], spec.argv.slice(1), {
3980
+ cwd: spec.cwd,
3981
+ env: spec.env,
3982
+ stdio: ["pipe", "pipe", "pipe"]
3983
+ });
3984
+ return fromNativeChild(child);
3985
+ }
3986
+ function fromNativeChild(child) {
3987
+ const done = Promise.withResolvers();
3988
+ child.once("exit", () => done.resolve(void 0));
3989
+ child.once("error", done.reject);
3990
+ return {
3991
+ stdin: child.stdin,
3992
+ stdout: child.stdout,
3993
+ stderr: child.stderr,
3994
+ done: done.promise,
3995
+ terminate: () => child.kill()
3996
+ };
3997
+ }
3998
+ var AcpClient = class _AcpClient {
3999
+ /** Mount a client over an already-spawned `kimi acp` process. */
4000
+ constructor(process2) {
4001
+ this.process = process2;
4002
+ this.process.stdout.on("data", (chunk) => this.feed(chunk));
4003
+ this.process.stderr.on("data", () => {
4004
+ });
4005
+ this.process.done.then(() => {
4006
+ this.sealed = true;
4007
+ const error = new Error("kimi acp process exited unexpectedly");
4008
+ for (const { reject } of this.pending.values()) reject(error);
4009
+ this.pending.clear();
4010
+ this.updateWake?.();
4011
+ }, () => {
4012
+ this.sealed = true;
4013
+ this.updateWake?.();
4014
+ });
3765
4015
  }
3766
- const definitions = [];
3767
- const seen = new Set(CLAUDE_CODE_COMMANDS.map((command) => command.name));
3768
- for (const entry of entries.sort()) {
3769
- if (!entry.endsWith(".md")) continue;
3770
- const name2 = entry.slice(0, -".md".length);
3771
- if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
3772
- const path = join3(userCommandsDir(), entry);
3773
- let raw;
4016
+ process;
4017
+ pending = /* @__PURE__ */ new Map();
4018
+ updateBuffer = [];
4019
+ updateWake;
4020
+ updateHandler;
4021
+ permissionHandler;
4022
+ sealed = false;
4023
+ nextId = 1;
4024
+ decoder = new StringDecoder2("utf8");
4025
+ buffer = "";
4026
+ /** Whether this client was sealed or its process exited. */
4027
+ get closed() {
4028
+ return this.sealed;
4029
+ }
4030
+ /**
4031
+ * Create a client, spawning the `kimi acp` child through the supplied
4032
+ * capability (or the default node spawn when none is given).
4033
+ * @param spec - the `kimi acp` argv/cwd/env the child should run with.
4034
+ * @param spawn - optional process-spawn capability (the subprocess seam).
4035
+ * @returns the connected client.
4036
+ */
4037
+ static create(spec, spawn4) {
4038
+ const process2 = spawn4 === void 0 ? defaultSpawn2(spec) : spawn4(spec);
4039
+ return new _AcpClient(process2);
4040
+ }
4041
+ /** Register the event dispatch handler. */
4042
+ onUpdate(handler) {
4043
+ this.updateHandler = handler;
4044
+ }
4045
+ /** Register the permission-approval handler (reverse-RPC answers). */
4046
+ onPermission(handler) {
4047
+ this.permissionHandler = handler;
4048
+ }
4049
+ /** Send one request and await the correlated response. */
4050
+ request(method, params) {
4051
+ if (this.sealed) return Promise.reject(new Error("kimi acp client is sealed"));
4052
+ const id = this.nextId++;
4053
+ return new Promise((resolve5, reject) => {
4054
+ this.pending.set(id, { resolve: resolve5, reject });
4055
+ this.process.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
4056
+ `);
4057
+ });
4058
+ }
4059
+ /** Send a notification (no correlated response awaited). */
4060
+ notify(method, params) {
4061
+ if (this.sealed) return;
4062
+ this.process.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
4063
+ `);
4064
+ }
4065
+ /** Open the protocol handshake. */
4066
+ initialize() {
4067
+ return this.request("initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "dsh-loop-engine", version: "1.0.0" } });
4068
+ }
4069
+ /** Start a fresh ACP session and resolve to its session id. */
4070
+ async newSession(cwd) {
4071
+ const result = await this.request("session/new", { cwd, mcpServers: [] });
4072
+ const sessionId = result?.sessionId;
4073
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
4074
+ throw new Error("kimi acp session/new returned no session id");
4075
+ }
4076
+ return sessionId;
4077
+ }
4078
+ /** Prompt the agent in a session and resolve when the turn completes. */
4079
+ prompt(sessionId, text) {
4080
+ return this.request("session/prompt", { sessionId, prompt: [{ type: "text", text }] });
4081
+ }
4082
+ /** Cancel the active turn in a session (fire-and-forget). */
4083
+ cancel(sessionId) {
4084
+ this.request("session/cancel", { sessionId }).catch(() => void 0);
4085
+ }
4086
+ /** Answer a pending `session/request_permission`. */
4087
+ respondPermission(id, approved) {
4088
+ this.process.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, result: { approved } })}
4089
+ `);
4090
+ }
4091
+ /** Consume every buffered update as an async generator. */
4092
+ async *updates() {
4093
+ while (true) {
4094
+ if (this.updateBuffer.length > 0) {
4095
+ yield this.updateBuffer.shift();
4096
+ continue;
4097
+ }
4098
+ if (this.sealed) return;
4099
+ await new Promise((resolve5) => {
4100
+ this.updateWake = resolve5;
4101
+ });
4102
+ this.updateWake = void 0;
4103
+ }
4104
+ }
4105
+ /** Seal the client and request child termination. */
4106
+ dispose() {
4107
+ if (this.sealed) return;
4108
+ this.sealed = true;
4109
+ this.process.terminate();
4110
+ const error = new Error("kimi acp client is sealed");
4111
+ for (const { reject } of this.pending.values()) reject(error);
4112
+ this.pending.clear();
4113
+ this.updateWake?.();
4114
+ }
4115
+ feed(chunk) {
4116
+ if (this.sealed) return;
4117
+ const text = typeof chunk === "string" ? this.buffer + chunk : this.buffer + this.decoder.write(chunk);
4118
+ this.buffer = text;
4119
+ while (true) {
4120
+ const newline = this.buffer.indexOf("\n");
4121
+ if (newline === -1) break;
4122
+ let line = this.buffer.slice(0, newline);
4123
+ this.buffer = this.buffer.slice(newline + 1);
4124
+ if (line.endsWith("\r")) line = line.slice(0, -1);
4125
+ this.dispatch(line);
4126
+ }
4127
+ }
4128
+ /** Dispatch one parsed line: a response, an update notification, or a reverse-RPC request. */
4129
+ dispatch(line) {
4130
+ if (line.trim().length === 0) return;
4131
+ let frame2;
3774
4132
  try {
3775
- raw = readFileSync2(path, "utf8");
4133
+ frame2 = JSON.parse(line);
3776
4134
  } catch {
3777
- continue;
4135
+ return;
4136
+ }
4137
+ if (isPermissionRequestFrame(frame2)) {
4138
+ void this.handlePermission(frame2.id, frame2);
4139
+ return;
4140
+ }
4141
+ if (typeof frame2.id === "number" && frame2.method === void 0) {
4142
+ this.settle(frame2);
4143
+ return;
4144
+ }
4145
+ if (isUpdateFrame(frame2)) {
4146
+ const update = frame2.params.update;
4147
+ this.updateHandler?.(update);
4148
+ this.updateBuffer.push(update);
4149
+ this.updateWake?.();
4150
+ return;
4151
+ }
4152
+ if (typeof frame2.id === "number" && typeof frame2.method === "string") {
4153
+ this.process.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: frame2.id, error: { code: -32601, message: "Method not found" } })}
4154
+ `);
3778
4155
  }
3779
- const description = commandDescription(raw);
3780
- if (description === void 0) continue;
3781
- seen.add(name2);
3782
- definitions.push({ name: name2, description, handler: forwardClaudeCodeCommand(name2) });
3783
4156
  }
3784
- return definitions;
4157
+ settle(frame2) {
4158
+ const id = frame2.id;
4159
+ const pending = this.pending.get(id);
4160
+ if (pending === void 0) return;
4161
+ this.pending.delete(id);
4162
+ if (frame2.error !== void 0) {
4163
+ pending.reject(new Error(frame2.error.message ?? "kimi acp request failed"));
4164
+ } else {
4165
+ pending.resolve(frame2.result);
4166
+ }
4167
+ }
4168
+ async handlePermission(id, frame2) {
4169
+ const approved = this.permissionHandler === void 0 ? false : await this.permissionHandler(frame2);
4170
+ this.respondPermission(id, approved);
4171
+ }
4172
+ };
4173
+
4174
+ // src/engine-kimi/acp/mapping.ts
4175
+ import { ToolCallId as ToolCallId5, createToolResultMessage as createToolResultMessage4 } from "@deepseek-ai/dsh-llm";
4176
+ function isTextChunk(update) {
4177
+ return update.sessionUpdate === "agent_message_chunk";
3785
4178
  }
3786
- function userCommandsDir() {
3787
- return join3(homedir(), ".claude", "commands");
4179
+ function isThoughtChunk(update) {
4180
+ return update.sessionUpdate === "agent_thought_chunk";
3788
4181
  }
3789
- function commandDescription(raw) {
3790
- const trimmed = raw.trim();
3791
- if (trimmed.length === 0) return void 0;
3792
- let body = trimmed;
3793
- if (trimmed.startsWith("---\n")) {
3794
- const closing = trimmed.indexOf("\n---");
3795
- if (closing <= 0) return void 0;
3796
- for (const line of trimmed.slice(4, closing).split("\n")) {
3797
- const colon = line.indexOf(":");
3798
- if (colon < 0 || line.slice(0, colon).trim() !== "description") continue;
3799
- const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
3800
- if (value.length > 0) return value;
4182
+ function isToolCall(update) {
4183
+ return update.sessionUpdate === "tool_call";
4184
+ }
4185
+ function isToolCallUpdate(update) {
4186
+ return update.sessionUpdate === "tool_call_update";
4187
+ }
4188
+ function chunkDelta(update) {
4189
+ const content = update.content;
4190
+ if (content === void 0) return "";
4191
+ const text = content.text;
4192
+ return typeof text === "string" ? text : "";
4193
+ }
4194
+ function toolCallIdOf(update) {
4195
+ return update.toolCallId;
4196
+ }
4197
+ function toolCallName(update) {
4198
+ return update.title;
4199
+ }
4200
+ function isToolSettledStatus(status) {
4201
+ return status !== "pending" && status !== "queued" && status !== "running" && status !== "in_progress";
4202
+ }
4203
+ function isToolErrorStatus(status) {
4204
+ return status === "failed" || status === "error" || status === "denied";
4205
+ }
4206
+ function toolContentText(update) {
4207
+ const blocks = update.content ?? [];
4208
+ return blocks.map((block) => block.type === "content" && block.content.type === "text" ? block.content.text : "").join("");
4209
+ }
4210
+ function toolResult(callId, text, isError) {
4211
+ return createToolResultMessage4({
4212
+ callId: ToolCallId5(callId),
4213
+ content: [{ type: "text", text: text.length > 0 ? text : "(no content)" }],
4214
+ isError
4215
+ });
4216
+ }
4217
+
4218
+ // src/engine-kimi/permission.ts
4219
+ function resolveToolApproval(events) {
4220
+ return sessionApprovalPolicy(events) !== "ask";
4221
+ }
4222
+
4223
+ // src/engine-kimi/agent.ts
4224
+ var PROVIDER4 = "kimi";
4225
+ var NATIVE_MODEL_LABEL4 = "kimi-native";
4226
+ var KimiAgent = class {
4227
+ constructor(loopCtx, id, options, session, config, spawn4, bin) {
4228
+ this.loopCtx = loopCtx;
4229
+ this.id = id;
4230
+ this.options = options;
4231
+ this.session = session;
4232
+ this.config = config;
4233
+ this.spawn = spawn4;
4234
+ this.bin = bin;
4235
+ this.dispatch = agentEvents4(loopCtx, this);
4236
+ this.inbox = new Inbox4(session, {
4237
+ inserted: (message) => {
4238
+ this.dispatch.emit("agent/inbox/inserted", { message });
4239
+ },
4240
+ discarded: (message) => {
4241
+ this.dispatch.emit("agent/inbox/discarded", { message });
4242
+ },
4243
+ claimed: (message, turn) => {
4244
+ this.dispatch.emit("agent/inbox/claimed", { message, turn });
4245
+ }
4246
+ });
4247
+ const lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
4248
+ this.phase = { kind: "idle", lastTurn };
4249
+ this.scope = createScope4(loopCtx, this);
4250
+ this.ctx = this.scope.ctx.extend({ agent: this });
4251
+ this.scope.ctx.effect(() => () => {
4252
+ this.acp?.dispose();
4253
+ this.acp = void 0;
4254
+ }, "kimi.acpClient()");
4255
+ }
4256
+ loopCtx;
4257
+ id;
4258
+ options;
4259
+ session;
4260
+ config;
4261
+ spawn;
4262
+ bin;
4263
+ inbox;
4264
+ phase;
4265
+ activityDone = Promise.resolve();
4266
+ /** The agent-scoped registration boundary; the lifecycle owner unwinds it after the driver exits. */
4267
+ scope;
4268
+ ctx;
4269
+ /** Fused dispatcher, built once in the constructor so hot-path dispatches never allocate. */
4270
+ dispatch;
4271
+ /** Whether this loop instance has appended its initial/resume request anchor. */
4272
+ requestHeaderLogged = false;
4273
+ /** Lazily created ACP client, reused across steps and released on scope teardown. */
4274
+ acp;
4275
+ /** The spawn spec the cached client was built from; a change forces a respawn. */
4276
+ lastSpec;
4277
+ get status() {
4278
+ return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
4279
+ }
4280
+ /** Commit a phase and publish its externally visible status transition. */
4281
+ setPhase(next) {
4282
+ const previousStatus = this.status;
4283
+ this.phase = next;
4284
+ const status = this.status;
4285
+ if (status !== previousStatus) {
4286
+ this.dispatch.emit("agent/status", { status });
3801
4287
  }
3802
- body = trimmed.slice(closing + 4);
3803
4288
  }
3804
- for (const line of body.split("\n")) {
3805
- const candidate = line.trim();
3806
- if (candidate.length === 0 || candidate.startsWith("#")) continue;
3807
- return candidate.length > 120 ? `${candidate.slice(0, 119)}\u2026` : candidate;
4289
+ send(message, target, wakeup) {
4290
+ const wakingAfterAbort = wakeup && this.phase.kind !== "idle" && this.phase.abort.signal.aborted;
4291
+ const resolvedTarget = wakingAfterAbort ? "next-turn" : target;
4292
+ this.inbox.splice(resolvedTarget, Infinity, 0, [message]);
4293
+ if (wakeup) this.wakeDriver(wakingAfterAbort);
3808
4294
  }
3809
- return void 0;
3810
- }
4295
+ /**
4296
+ * Queue a message for the next turn and wake the driver.
4297
+ * @param input - the user message to deliver.
4298
+ */
4299
+ followup(input) {
4300
+ this.send(input, "next-turn", true);
4301
+ }
4302
+ /**
4303
+ * Queue a message for the running step and wake the driver.
4304
+ * @param input - the user message to deliver.
4305
+ */
4306
+ steer(input) {
4307
+ this.send(input, "next-step", true);
4308
+ }
4309
+ /**
4310
+ * Queue a message for the running step without waking the driver.
4311
+ * @param input - the user message to deliver.
4312
+ */
4313
+ inject(input) {
4314
+ this.send(input, "next-step", false);
4315
+ }
4316
+ cancel(cause, options = {}) {
4317
+ if (!options.keepInbox) {
4318
+ this.inbox.clear();
4319
+ if (this.phase.kind !== "idle") this.phase.wakeRequested = false;
4320
+ }
4321
+ if (this.phase.kind !== "idle") this.phase.abort.abort(cause);
4322
+ }
4323
+ /**
4324
+ * Run a maintenance job while the agent is idle.
4325
+ * @param job - the maintenance operation, receiving the phase abort signal.
4326
+ * @returns the maintenance result.
4327
+ */
4328
+ runMaintenance(job) {
4329
+ if (this.phase.kind !== "idle") throw new Error(`agent "${this.id}" already has active work`);
4330
+ const done = Promise.withResolvers();
4331
+ const maintenance = {
4332
+ kind: "maintenance",
4333
+ abort: new AbortController(),
4334
+ lastTurn: this.phase.lastTurn,
4335
+ wakeRequested: false
4336
+ };
4337
+ this.setPhase(maintenance);
4338
+ this.activityDone = done.promise;
4339
+ return (async () => {
4340
+ try {
4341
+ return await job(maintenance.abort.signal);
4342
+ } finally {
4343
+ this.setPhase({ kind: "idle", lastTurn: maintenance.lastTurn });
4344
+ if (maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver();
4345
+ done.resolve();
4346
+ }
4347
+ })();
4348
+ }
4349
+ /**
4350
+ * Start one driver, or latch its wake behind maintenance or an aborted
4351
+ * activity. A wake sent while idle always opens its turn boundary, even
4352
+ * when its message was cleared; only a latched replay is suppressed when
4353
+ * the queue no longer holds the wake.
4354
+ * @param wakeAfterAbort - the {@link send} classification, captured before
4355
+ * the inbox insertion so a reentrant cancel cannot reclassify it.
4356
+ */
4357
+ wakeDriver(wakeAfterAbort = false) {
4358
+ if (this.phase.kind !== "idle") {
4359
+ const reason = this.phase.abort.signal.reason;
4360
+ if (reason?.kind !== "disposed" && (this.phase.kind === "maintenance" || wakeAfterAbort)) {
4361
+ this.phase.wakeRequested = true;
4362
+ }
4363
+ return;
4364
+ }
4365
+ const driver = Promise.withResolvers();
4366
+ this.activityDone = driver.promise;
4367
+ this.setPhase({
4368
+ kind: "running",
4369
+ abort: new AbortController(),
4370
+ turn: this.phase.lastTurn,
4371
+ step: 0,
4372
+ wakeRequested: false
4373
+ });
4374
+ this.loopCtx.agents.withInitiator(this, () => this.kick()).then(driver.resolve, driver.reject);
4375
+ }
4376
+ async whenIdle() {
4377
+ let activity;
4378
+ do {
4379
+ await (activity = this.activityDone);
4380
+ } while (activity !== this.activityDone);
4381
+ }
4382
+ /** Report one failure at its live boundary, then preserve it for driver containment. */
4383
+ throwError(error) {
4384
+ const turn = this.phase.kind === "running" ? this.phase.turn : this.phase.lastTurn;
4385
+ const step = this.phase.kind === "running" ? this.phase.step : 0;
4386
+ this.dispatch.emit("agent/error", { turn, step, error });
4387
+ throw error;
4388
+ }
4389
+ async kick() {
4390
+ try {
4391
+ while (await this.turn()) {
4392
+ }
4393
+ } catch (_error) {
4394
+ } finally {
4395
+ if (this.phase.kind === "running") {
4396
+ const { turn, wakeRequested } = this.phase;
4397
+ this.setPhase({ kind: "idle", lastTurn: turn });
4398
+ if (wakeRequested && this.inbox.hasPending) this.wakeDriver();
4399
+ }
4400
+ }
4401
+ }
4402
+ async preStep(target, position) {
4403
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": pre-step outside running phase`);
4404
+ const signal = this.phase.abort.signal;
4405
+ const claimed = this.inbox.claim(target, position.turn);
4406
+ const decision = await this.dispatch.waterfall(
4407
+ "agent/pre-step",
4408
+ { messages: claimed, ...position, signal },
4409
+ () => Promise.resolve({ kind: "enter", messages: claimed })
4410
+ );
4411
+ signal.throwIfAborted();
4412
+ if (decision.kind === "reject") return decision;
4413
+ const injected = await this.injectSkills(decision.messages, signal);
4414
+ signal.throwIfAborted();
4415
+ return injected !== decision.messages ? { kind: "enter", messages: [...injected] } : { ...decision };
4416
+ }
4417
+ /**
4418
+ * Scan the step's user messages for `/name` skill gestures, load each
4419
+ * matching skill, and inject the rendered skill content into the message
4420
+ * batch. This mirrors what dsh-tool-skill does for the in-process engine.
4421
+ * @param messages - the current step's message batch.
4422
+ * @param signal - cancellation signal (aborted loads are silently dropped).
4423
+ * @returns the original batch when no skill was invoked, or an extended
4424
+ * batch with injected skill-content messages appended.
4425
+ */
4426
+ async injectSkills(messages, signal) {
4427
+ const names = invokedSkillNames(messages);
4428
+ if (names.length === 0) return messages;
4429
+ const skills = this.loopCtx.get("skills");
4430
+ if (skills === void 0) return messages;
4431
+ const cwd = this.session.header.cwd;
4432
+ const injections = [];
4433
+ for (const name2 of names) {
4434
+ if (!isSkillName(name2)) continue;
4435
+ let skill;
4436
+ try {
4437
+ skill = await skills.get(name2, { signal, scope: this, ...cwd === void 0 ? {} : { cwd } });
4438
+ } catch {
4439
+ continue;
4440
+ }
4441
+ if (skill === void 0 || !skill.invocation.userInvocable) continue;
4442
+ if (signal.aborted) return messages;
4443
+ injections.push(createUserMessage4({
4444
+ content: [{ type: "text", text: renderSkillContent(skill) }],
4445
+ source: { kind: "skill-invocation", name: name2, form: "instructions" }
4446
+ }));
4447
+ }
4448
+ return injections.length > 0 ? [...messages, ...injections] : messages;
4449
+ }
4450
+ /** Open one turn before claiming its first proposed step. */
4451
+ async turn() {
4452
+ if (this.phase.kind !== "running") {
4453
+ this.throwError(new Error(`agent "${this.id}": turn without driver reservation`));
4454
+ }
4455
+ const phase = this.phase;
4456
+ const { signal } = phase.abort;
4457
+ signal.throwIfAborted();
4458
+ const turn = phase.turn + 1;
4459
+ try {
4460
+ this.session.append("turn/start", { turn });
4461
+ } catch (error) {
4462
+ this.throwError(error);
4463
+ }
4464
+ phase.turn = turn;
4465
+ let turnEnds = null;
4466
+ let target = "next-turn";
4467
+ try {
4468
+ while (true) {
4469
+ signal.throwIfAborted();
4470
+ const step = phase.step + 1;
4471
+ const decision = await this.preStep(target, { turn, step });
4472
+ if (decision.kind === "reject") {
4473
+ turnEnds = { kind: "blocked" };
4474
+ return false;
4475
+ }
4476
+ if (turnEnds && decision.messages.length === 0) break;
4477
+ if (phase.step === 0 && decision.messages.length === 0) {
4478
+ turnEnds = { kind: "completed" };
4479
+ return false;
4480
+ }
4481
+ signal.throwIfAborted();
4482
+ this.session.append("step/start", { turn, step });
4483
+ phase.step = step;
4484
+ try {
4485
+ for (const message of decision.messages) {
4486
+ this.session.append("user/message", message, { surfaceOp: "append" });
4487
+ }
4488
+ const stepEnd = await this.step();
4489
+ if (turnEnds === null) turnEnds = stepEnd;
4490
+ } finally {
4491
+ this.session.append("step/end", { turn, step });
4492
+ }
4493
+ signal.throwIfAborted();
4494
+ if (turnEnds && this.inbox.nextStep.length === 0) {
4495
+ await this.dispatch.serial("agent/turn-stopping", { turn, signal });
4496
+ signal.throwIfAborted();
4497
+ }
4498
+ if (turnEnds && this.inbox.nextStep.length === 0) break;
4499
+ target = "next-step";
4500
+ }
4501
+ } catch (error) {
4502
+ if (signal.aborted) {
4503
+ turnEnds = { kind: "aborted", reason: signal.reason };
4504
+ throw error;
4505
+ }
4506
+ turnEnds = {
4507
+ kind: "error",
4508
+ error: error instanceof LlmError4 ? error.failure : { message: errorChain4(error), code: "UNKNOWN" }
4509
+ };
4510
+ this.throwError(error);
4511
+ } finally {
4512
+ try {
4513
+ this.session.append("turn/end", { turn, reason: turnEnds });
4514
+ } catch (error) {
4515
+ this.throwError(error);
4516
+ }
4517
+ }
4518
+ if (!this.inbox.hasPending) return false;
4519
+ phase.abort = new AbortController();
4520
+ phase.wakeRequested = false;
4521
+ phase.step = 0;
4522
+ return true;
4523
+ }
4524
+ /** Model label recorded in the request header for one lifecycle. */
4525
+ modelLabel() {
4526
+ return this.config.model ?? NATIVE_MODEL_LABEL4;
4527
+ }
4528
+ /** Append the request header snapshot once per loop instance. */
4529
+ assertRequestHeader() {
4530
+ if (this.requestHeaderLogged) return;
4531
+ const header = canonicalHeader4({
4532
+ config: { provider: PROVIDER4, model: this.modelLabel() }
4533
+ });
4534
+ const baseline = this.session.requestHeader();
4535
+ this.session.append("request/header", {
4536
+ header,
4537
+ reason: baseline === void 0 ? "initial" : "resume"
4538
+ });
4539
+ this.requestHeaderLogged = true;
4540
+ }
4541
+ /** Whether two spawn specs describe the same `kimi acp` child. */
4542
+ specsEqual(a, b) {
4543
+ if (a === void 0) return false;
4544
+ return a.cwd === b.cwd && a.env === b.env && a.argv.length === b.argv.length && a.argv.every((value, index) => value === b.argv[index]);
4545
+ }
4546
+ /** Return the cached ACP client, respawning when the spec or process changed. */
4547
+ async acpClient(cwd) {
4548
+ const spec = this.spawnSpec(cwd);
4549
+ if (this.acp !== void 0 && !this.acp.closed && this.specsEqual(this.lastSpec, spec)) return this.acp;
4550
+ this.acp?.dispose();
4551
+ const client = AcpClient.create(spec, this.spawn);
4552
+ this.acp = client;
4553
+ this.lastSpec = spec;
4554
+ try {
4555
+ await client.initialize();
4556
+ } catch (error) {
4557
+ this.acp = void 0;
4558
+ this.lastSpec = void 0;
4559
+ client.dispose();
4560
+ throw error;
4561
+ }
4562
+ return client;
4563
+ }
4564
+ /** Build the `kimi acp` argv/cwd/env for the persistent child. */
4565
+ spawnSpec(cwd) {
4566
+ return {
4567
+ argv: kimiAcpArgv(this.bin),
4568
+ cwd,
4569
+ env: this.config.env
4570
+ };
4571
+ }
4572
+ /** Run one `kimi acp` step for the current session history and map the streamed updates. */
4573
+ async step() {
4574
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
4575
+ const { turn, step, abort: { signal } } = this.phase;
4576
+ signal.throwIfAborted();
4577
+ this.blocks = [];
4578
+ this.emittedToolCalls = /* @__PURE__ */ new Set();
4579
+ this.toolText = /* @__PURE__ */ new Map();
4580
+ const cwd = this.session.header.cwd;
4581
+ if (cwd === void 0 || cwd.length === 0) {
4582
+ throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
4583
+ }
4584
+ const history = this.session.deriveMessages();
4585
+ const prompt = serializeHistory(history);
4586
+ if (prompt.length === 0) {
4587
+ throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
4588
+ }
4589
+ this.assertRequestHeader();
4590
+ signal.throwIfAborted();
4591
+ const client = await this.acpClient(cwd);
4592
+ signal.throwIfAborted();
4593
+ client.onPermission(() => resolveToolApproval(this.session.snapshotEvents()));
4594
+ const acpSessionId = await client.newSession(cwd);
4595
+ signal.throwIfAborted();
4596
+ client.onUpdate((update) => this.applyUpdate(turn, step, update));
4597
+ const cancel = () => {
4598
+ client.cancel(acpSessionId);
4599
+ };
4600
+ signal.addEventListener("abort", cancel, { once: true });
4601
+ try {
4602
+ await raceAbort(client.prompt(acpSessionId, prompt), signal, this.id);
4603
+ } finally {
4604
+ signal.removeEventListener("abort", cancel);
4605
+ }
4606
+ this.flushAssistant(turn, step);
4607
+ if (this.blocks.length === 0 && this.emittedToolCalls.size === 0) {
4608
+ throw new LlmError4(
4609
+ `agent "${this.id}": kimi query produced no assistant output`,
4610
+ "KIMI_NO_RESULT"
4611
+ );
4612
+ }
4613
+ return { kind: "completed" };
4614
+ }
4615
+ /** Per-step accumulation state for streamed assistant blocks and tool calls. */
4616
+ blocks = [];
4617
+ emittedToolCalls = /* @__PURE__ */ new Set();
4618
+ toolText = /* @__PURE__ */ new Map();
4619
+ blockRef(type) {
4620
+ return this.blocks.find((block) => block.type === type);
4621
+ }
4622
+ ensureBlock(type) {
4623
+ const existing = this.blockRef(type);
4624
+ if (existing !== void 0) return existing;
4625
+ const index = this.blocks.length;
4626
+ const block = { index, type, text: "", refs: [] };
4627
+ this.blocks.push(block);
4628
+ return block;
4629
+ }
4630
+ /** Append one streamed update's durable effect for the current step. */
4631
+ applyUpdate(turn, step, update) {
4632
+ if (isThoughtChunk(update)) {
4633
+ const delta = chunkDelta(update);
4634
+ if (delta === "") return;
4635
+ const block = this.ensureBlock("reasoning");
4636
+ const started = block.refs.length === 0;
4637
+ if (started) block.refs.push(this.appendChunk(turn, step, { type: "block-start", index: block.index, blockType: "reasoning" }));
4638
+ block.refs.push(this.appendChunk(turn, step, { type: "reasoning-delta", index: block.index, text: delta }));
4639
+ block.text += delta;
4640
+ return;
4641
+ }
4642
+ if (isTextChunk(update)) {
4643
+ const delta = chunkDelta(update);
4644
+ if (delta === "") return;
4645
+ const block = this.ensureBlock("text");
4646
+ const started = block.refs.length === 0;
4647
+ if (started) block.refs.push(this.appendChunk(turn, step, { type: "block-start", index: block.index, blockType: "text" }));
4648
+ block.refs.push(this.appendChunk(turn, step, { type: "text-delta", index: block.index, text: delta }));
4649
+ block.text += delta;
4650
+ return;
4651
+ }
4652
+ if (isToolCall(update)) {
4653
+ const callId = toolCallIdOf(update);
4654
+ if (callId === "" || this.emittedToolCalls.has(callId)) return;
4655
+ this.emittedToolCalls.add(callId);
4656
+ const name2 = toolCallName(update);
4657
+ this.session.append("tool/call", { turn, step, callId: ToolCallId6(callId), name: name2, arguments: "{}" });
4658
+ this.toolText.set(callId, "");
4659
+ return;
4660
+ }
4661
+ if (isToolCallUpdate(update)) {
4662
+ const callId = toolCallIdOf(update);
4663
+ if (callId === "" || !this.toolText.has(callId)) return;
4664
+ const delta = toolContentText(update);
4665
+ const accumulated = `${this.toolText.get(callId)}${delta}`;
4666
+ this.toolText.set(callId, accumulated);
4667
+ const status = update.status;
4668
+ if (isToolSettledStatus(status)) {
4669
+ const message = toolResult(callId, accumulated, isToolErrorStatus(status));
4670
+ this.session.append("tool/result", { turn, step, message }, { surfaceOp: "append" });
4671
+ this.toolText.delete(callId);
4672
+ }
4673
+ return;
4674
+ }
4675
+ }
4676
+ /** Append one live chunk and return its durable seq. */
4677
+ appendChunk(turn, step, chunk) {
4678
+ return this.session.append("assistant/chunk", { turn, step, chunk }).seq;
4679
+ }
4680
+ /** Flush the accumulated assistant blocks into one durable assistant/message. */
4681
+ flushAssistant(turn, step) {
4682
+ if (this.blocks.length === 0 && this.emittedToolCalls.size === 0) return;
4683
+ const content = [];
4684
+ const refs = [];
4685
+ for (const block of this.blocks) {
4686
+ const delta = block.text;
4687
+ content.push(block.type === "text" ? { type: "text", text: delta } : { type: "reasoning", text: delta });
4688
+ block.refs.push(this.appendChunk(turn, step, { type: "block-end", index: block.index, block: block.type === "text" ? { type: "text", text: delta } : { type: "reasoning", text: delta } }));
4689
+ refs.push(...block.refs);
4690
+ }
4691
+ this.session.append("assistant/message", {
4692
+ turn,
4693
+ step,
4694
+ message: createAssistantMessage4({
4695
+ content,
4696
+ source: { provider: PROVIDER4, model: this.modelLabel() }
4697
+ })
4698
+ }, {
4699
+ surfaceOp: "append",
4700
+ sourceEventSeqs: refs
4701
+ });
4702
+ }
4703
+ };
4704
+
4705
+ // src/engine-kimi/loop.ts
4706
+ var KIMI_DISPOSE_GRACE_MS = 3e3;
4707
+ var Config4 = z4.object({
4708
+ model: z4.string(),
4709
+ env: z4.dict(z4.string()).default({}),
4710
+ bin: z4.string()
4711
+ });
4712
+ function resolveConfig4(config) {
4713
+ return {
4714
+ model: config.model,
4715
+ env: config.env ?? {},
4716
+ bin: kimiBinResolver(config.bin)
4717
+ };
4718
+ }
4719
+ var KimiLoop = class extends Service4 {
4720
+ /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
4721
+ static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
4722
+ /** Validated configuration owned by the loop plugin. */
4723
+ config;
4724
+ ownership;
4725
+ /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
4726
+ runtime;
4727
+ /** One-shot spawn capability handed to every agent, sandboxed by the subprocess seam. */
4728
+ spawn;
4729
+ constructor(ctx, config) {
4730
+ super(ctx, "agentLoopKimi");
4731
+ this.config = resolveConfig4(config);
4732
+ this.ownership = new FactoryOwnership(ctx.fiber);
4733
+ this.runtime = { ctx };
4734
+ this.spawn = (spec) => fromSubprocess2(this.runtime.ctx.subprocess.spawn(kimiSubprocessSpec(spec, KIMI_DISPOSE_GRACE_MS)));
4735
+ ctx.effect(() => () => this.ownership.dispose(), "agentLoopKimi.transactions()");
4736
+ ctx.effect(() => ctx.agents.setFactory(this), "agentLoopKimi.setFactory()");
4737
+ ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
4738
+ ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
4739
+ ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
4740
+ }
4741
+ /**
4742
+ * Construct the driver, scope, and one memoized reverse teardown for a new
4743
+ * agent. The teardown is registered with the factory and the owner fiber
4744
+ * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
4745
+ * fuses caller cancellation with lifecycle teardown for setup awaits.
4746
+ */
4747
+ /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
4748
+ prepare(ownerCtx, id, options, session, callerSignal, handle) {
4749
+ ownerCtx.fiber.assertActive();
4750
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
4751
+ if (callerSignal?.aborted) {
4752
+ throw callerSignal.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason });
4753
+ }
4754
+ const loopCtx = this.runtime.ctx;
4755
+ const abort = new AbortController();
4756
+ const onCallerAbort = () => {
4757
+ abort.abort(callerSignal?.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal?.reason }));
4758
+ };
4759
+ const onFactoryTeardown = () => {
4760
+ abort.abort(this.ownership.signal.reason);
4761
+ };
4762
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
4763
+ this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
4764
+ let machine;
4765
+ let detachSession;
4766
+ let detachAgent;
4767
+ let disposing;
4768
+ const machineReady = Promise.withResolvers();
4769
+ const dispose = (ownerTriggered = false) => disposing ??= (async () => {
4770
+ abort.abort(new Error(`agent "${id}" lifecycle disposed`));
4771
+ callerSignal?.removeEventListener("abort", onCallerAbort);
4772
+ this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
4773
+ try {
4774
+ if (machine === void 0) await machineReady.promise;
4775
+ if (machine !== void 0) {
4776
+ machine.cancel({ kind: "disposed" });
4777
+ await machine.whenIdle();
4778
+ await machine.scope.dispose();
4779
+ }
4780
+ } finally {
4781
+ try {
4782
+ await handle?.close();
4783
+ } finally {
4784
+ try {
4785
+ detachAgent?.();
4786
+ detachSession?.();
4787
+ } finally {
4788
+ untrack();
4789
+ if (!ownerTriggered) await unfollowOwner();
4790
+ }
4791
+ }
4792
+ }
4793
+ })();
4794
+ const untrack = this.ownership.track(dispose);
4795
+ let unfollowOwner;
4796
+ try {
4797
+ unfollowOwner = ownerCtx.effect(() => () => {
4798
+ if (disposing !== void 0) return;
4799
+ abort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
4800
+ return dispose(true);
4801
+ }, `agentLoopKimi.lifecycle(${id})`);
4802
+ } catch (error) {
4803
+ untrack();
4804
+ callerSignal?.removeEventListener("abort", onCallerAbort);
4805
+ this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
4806
+ throw error;
4807
+ }
4808
+ const assertLive = () => {
4809
+ if (!abort.signal.aborted) return;
4810
+ throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
4811
+ };
4812
+ try {
4813
+ const agent = machine = new KimiAgent(loopCtx, id, options, session, this.config, this.spawn, this.config.bin);
4814
+ machineReady.resolve();
4815
+ assertLive();
4816
+ return {
4817
+ agent,
4818
+ signal: abort.signal,
4819
+ publish: (source) => {
4820
+ assertLive();
4821
+ detachSession = agent.ctx.sessions.enter(session);
4822
+ detachAgent = loopCtx.agents.enter(agent, ownerCtx.agent);
4823
+ agent.ctx.sessions.announce(session);
4824
+ assertLive();
4825
+ loopCtx.agents.announce(agent);
4826
+ assertLive();
4827
+ emitAgentEvent4(loopCtx, agent, "agent/session-start", { source });
4828
+ assertLive();
4829
+ return { agent, dispose };
4830
+ },
4831
+ dispose
4832
+ };
4833
+ } catch (error) {
4834
+ machineReady.resolve();
4835
+ void dispose();
4836
+ throw error;
4837
+ }
4838
+ }
4839
+ /** Prepare one Agent around an acquired Session, run setup, and publish it. */
4840
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored) {
4841
+ var _stack = [];
4842
+ try {
4843
+ const ownedPreparation = __using(_stack, preparation);
4844
+ const session = ownedPreparation.session;
4845
+ let prepared;
4846
+ try {
4847
+ prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle);
4848
+ } catch (error) {
4849
+ await stored?.handle.close().catch(() => {
4850
+ });
4851
+ throw error;
4852
+ }
4853
+ try {
4854
+ const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
4855
+ setupCommit?.commit();
4856
+ await this.appendUnstoredSuffix(stored, session);
4857
+ return prepared.publish(source);
4858
+ } catch (error) {
4859
+ await prepared.dispose().catch(() => {
4860
+ });
4861
+ throw error;
4862
+ }
4863
+ } catch (_) {
4864
+ var _error = _, _hasError = true;
4865
+ } finally {
4866
+ __callDispose(_stack, _error, _hasError);
4867
+ }
4868
+ }
4869
+ /**
4870
+ * Create an agent and session under one caller-supplied identity, owned by
4871
+ * the accessing fiber. When a persistence backend is mounted, the session's
4872
+ * durable identity is stored before publication.
4873
+ * @param ownerCtx - caller context that structurally owns the lifecycle.
4874
+ * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
4875
+ * @returns the published handle.
4876
+ */
4877
+ async createAgent(ownerCtx, options) {
4878
+ const preparation = SessionPreparation4.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
4879
+ ...options.seed === void 0 ? {} : { seed: options.seed },
4880
+ ...options.meta === void 0 ? {} : { meta: options.meta },
4881
+ ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
4882
+ }));
4883
+ const published = (async () => {
4884
+ let stored;
4885
+ try {
4886
+ stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
4887
+ () => this.createStoredSession(preparation.session, options.signal),
4888
+ options.signal,
4889
+ options.sessionId,
4890
+ (abandoned) => {
4891
+ void abandoned?.handle.close().catch(() => {
4892
+ });
4893
+ }
4894
+ );
4895
+ } catch (error) {
4896
+ preparation[Symbol.dispose]();
4897
+ throw error;
4898
+ }
4899
+ return this.setupAndPublish(
4900
+ ownerCtx,
4901
+ options.sessionId,
4902
+ preparation,
4903
+ options.agentOptions ?? {},
4904
+ options.setup,
4905
+ options.signal,
4906
+ "startup",
4907
+ stored
4908
+ );
4909
+ })();
4910
+ this.ownership.trackWrapper(published);
4911
+ return published;
4912
+ }
4913
+ /**
4914
+ * Take a fresh session's write ownership when persistence is mounted.
4915
+ * Nothing is appended here: the constructor seed (which never re-emits
4916
+ * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
4917
+ * publication commit point, so a failed or cancelled setup closes an
4918
+ * unmaterialized handle and leaves no stored residue — the same id can be
4919
+ * created again.
4920
+ * @param session - the unpublished session to store.
4921
+ * @param signal - optional cancellation forwarded to the backend create.
4922
+ * @returns the owned handle and stored cursor, or `undefined` without a backend.
4923
+ */
4924
+ async createStoredSession(session, signal) {
4925
+ const persistence = this.runtime.ctx.get("sessionPersistence");
4926
+ if (persistence === void 0) return void 0;
4927
+ const handle = await persistence.create(session.header, {
4928
+ inheritedEventCount: session.inheritedEventCount,
4929
+ ...signal === void 0 ? {} : { signal }
4930
+ });
4931
+ return { handle, storedCount: 0 };
4932
+ }
4933
+ /**
4934
+ * Durably store the session events appended since the last stored cursor.
4935
+ * Pre-publication appends (constructor seed markers, setup-window events)
4936
+ * never re-emit through `session/event`, so publication must flush them
4937
+ * through the handle before live events start routing into it.
4938
+ * @param stored - the session's owned handle and stored cursor, if any.
4939
+ * @param session - the unpublished session whose suffix is stored.
4940
+ */
4941
+ async appendUnstoredSuffix(stored, session) {
4942
+ if (stored === void 0) return;
4943
+ const suffix = session.snapshotEvents(SessionLogOffset4(stored.storedCount));
4944
+ if (suffix.length > 0) await stored.handle.append(suffix);
4945
+ stored.storedCount += suffix.length;
4946
+ }
4947
+ /**
4948
+ * Resume an owned agent from the configured persistence service.
4949
+ * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
4950
+ * @param options - persisted identity, loop options, setup, and cancellation.
4951
+ * @returns the published handle.
4952
+ */
4953
+ async resume(ownerCtx, options) {
4954
+ const persistence = this.runtime.ctx.get("sessionPersistence");
4955
+ if (persistence === void 0) {
4956
+ throw new Error("cannot resume: session persistence is not configured (load a dsh-session-persistence backend)");
4957
+ }
4958
+ return this.resumeWith(ownerCtx, persistence, options);
4959
+ }
4960
+ /** Resume through an explicit persistence service. */
4961
+ resumeWith(ownerCtx, persistence, options) {
4962
+ const id = options.resumeSessionId;
4963
+ const published = (async () => {
4964
+ const ownerAbort = new AbortController();
4965
+ const unfollowOwner = ownerCtx.effect(() => () => {
4966
+ ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
4967
+ }, `agentLoopKimi.resume-load(${id})`);
4968
+ const fused = AbortSignal.any([
4969
+ ...options.signal === void 0 ? [] : [options.signal],
4970
+ ownerAbort.signal,
4971
+ this.ownership.signal
4972
+ ]);
4973
+ let handle;
4974
+ let stored;
4975
+ let preparation;
4976
+ try {
4977
+ try {
4978
+ handle = await raceAbortCall(
4979
+ () => persistence.open(id, "write", { signal: fused }),
4980
+ fused,
4981
+ id,
4982
+ (abandoned) => {
4983
+ void abandoned.close();
4984
+ }
4985
+ );
4986
+ const persisted = await handle.read(0, void 0, { signal: fused });
4987
+ fused.throwIfAborted();
4988
+ const closers = interruptedTurnClosers4(persisted);
4989
+ if (closers.length > 0) await handle.append(closers);
4990
+ preparation = SessionPreparation4.create(this.runtime.ctx.sessions.prepare(id, {
4991
+ seed: [...persisted, ...closers],
4992
+ meta: structuredClone(handle.header),
4993
+ inheritedEventCount: handle.inheritedEventCount,
4994
+ seedSource: "persistence"
4995
+ }));
4996
+ stored = { handle, storedCount: persisted.length + closers.length };
4997
+ await this.appendUnstoredSuffix(stored, preparation.session);
4998
+ } finally {
4999
+ await unfollowOwner();
5000
+ }
5001
+ ownerCtx.fiber.assertActive();
5002
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
5003
+ const owned = stored;
5004
+ handle = void 0;
5005
+ return await this.setupAndPublish(
5006
+ ownerCtx,
5007
+ id,
5008
+ preparation,
5009
+ options.agentOptions ?? {},
5010
+ options.setup,
5011
+ options.signal,
5012
+ "resume",
5013
+ owned
5014
+ );
5015
+ } finally {
5016
+ preparation?.[Symbol.dispose]();
5017
+ await handle?.close().catch(() => {
5018
+ });
5019
+ }
5020
+ })();
5021
+ this.ownership.trackWrapper(published);
5022
+ return published;
5023
+ }
5024
+ };
5025
+
5026
+ // src/engine-kimi/skills.ts
5027
+ import { readdir as readdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
5028
+ import { homedir as homedir3 } from "node:os";
5029
+ import { dirname as dirname3, join as join6, resolve as resolve3 } from "node:path";
5030
+
5031
+ // src/driver-core/context-files.ts
5032
+ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
5033
+ import { join as join5, resolve as resolve2 } from "node:path";
3811
5034
 
3812
5035
  // src/skills.ts
3813
5036
  import { readFile, readdir, stat } from "node:fs/promises";
@@ -4001,101 +5224,526 @@ function toCandidate(skill, path, rank, resourceBaseDir) {
4001
5224
  resourceBase: { kind: "directory", path: resourceBaseDir }
4002
5225
  };
4003
5226
  }
4004
- async function tryParseSkill(path) {
4005
- try {
4006
- const raw = await readFile(path, { encoding: "utf8" });
4007
- return parseSkillFile(raw);
4008
- } catch {
4009
- return void 0;
4010
- }
5227
+ async function tryParseSkill(path) {
5228
+ try {
5229
+ const raw = await readFile(path, { encoding: "utf8" });
5230
+ return parseSkillFile(raw);
5231
+ } catch {
5232
+ return void 0;
5233
+ }
5234
+ }
5235
+ async function findProjectRoot(cwd) {
5236
+ let current = cwd;
5237
+ while (true) {
5238
+ try {
5239
+ await stat(join4(current, ".git"));
5240
+ return current;
5241
+ } catch {
5242
+ }
5243
+ const parent = resolve(current, "..");
5244
+ if (parent === current) return cwd;
5245
+ current = parent;
5246
+ }
5247
+ }
5248
+
5249
+ // src/driver-core/context-files.ts
5250
+ async function projectAncestors(cwd) {
5251
+ const root = await findProjectRoot(resolve2(cwd));
5252
+ const dirs = [];
5253
+ let current = resolve2(cwd);
5254
+ while (true) {
5255
+ dirs.push(current);
5256
+ if (current === root) return dirs;
5257
+ current = resolve2(current, "..");
5258
+ }
5259
+ }
5260
+ async function collectProjectContextFiles(cwd, policy) {
5261
+ const files = [];
5262
+ for (const dir of await projectAncestors(cwd)) {
5263
+ const chosen = await dirContextFile(dir, policy);
5264
+ if (chosen !== void 0) files.push(chosen);
5265
+ }
5266
+ return files;
5267
+ }
5268
+ async function dirContextFile(dir, policy) {
5269
+ if (policy.override !== void 0) {
5270
+ const override = join5(dir, policy.override);
5271
+ if (await pathExists(override)) return override;
5272
+ }
5273
+ for (const name2 of policy.primary) {
5274
+ const candidate = join5(dir, name2);
5275
+ if (await pathExists(candidate)) return candidate;
5276
+ }
5277
+ return void 0;
5278
+ }
5279
+ async function pathExists(path) {
5280
+ try {
5281
+ await stat2(path);
5282
+ return true;
5283
+ } catch {
5284
+ return false;
5285
+ }
5286
+ }
5287
+ async function readOptionalFile(path) {
5288
+ try {
5289
+ return await readFile2(path, { encoding: "utf8" });
5290
+ } catch {
5291
+ return void 0;
5292
+ }
5293
+ }
5294
+ async function anySourceNonEmpty(paths) {
5295
+ for (const path of paths) {
5296
+ const raw = await readOptionalFile(path);
5297
+ if (raw !== void 0 && raw.trim().length > 0) return true;
5298
+ }
5299
+ return false;
5300
+ }
5301
+ async function fileNonEmpty(path) {
5302
+ const raw = await readOptionalFile(path);
5303
+ return raw !== void 0 && raw.trim().length > 0;
5304
+ }
5305
+ async function readSources(paths) {
5306
+ const parts = [];
5307
+ for (const path of paths) {
5308
+ const raw = await readOptionalFile(path);
5309
+ if (raw !== void 0 && raw.trim().length > 0) parts.push(raw);
5310
+ }
5311
+ return parts.length > 0 ? parts.join("\n\n") : void 0;
5312
+ }
5313
+
5314
+ // src/engine-kimi/skills.ts
5315
+ var PROVIDER_NAME2 = "kimi";
5316
+ var KIMI_AGENTS_PROJECT_RANK = 140;
5317
+ var KIMI_SKILL_PROJECT_RANK = 150;
5318
+ var KIMI_SKILL_USER_RANK = 160;
5319
+ var KIMI_CONTEXT_POLICY = {
5320
+ primary: ["AGENTS.md"]
5321
+ };
5322
+ function kimiAgentDir() {
5323
+ const override = process.env.KIMI_CODE_HOME;
5324
+ if (override !== void 0 && override.length > 0) return resolve3(override);
5325
+ return join6(homedir3(), ".kimi-code");
5326
+ }
5327
+ var KimiSkillProvider = class {
5328
+ constructor(control) {
5329
+ this.control = control;
5330
+ }
5331
+ control;
5332
+ name = PROVIDER_NAME2;
5333
+ async list(options) {
5334
+ const candidates = [];
5335
+ const cwd = options.cwd;
5336
+ if (cwd !== void 0) {
5337
+ const projectDirs = await projectAncestors(cwd);
5338
+ const contextPaths = await collectProjectContextFiles(cwd, KIMI_CONTEXT_POLICY);
5339
+ if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, KIMI_AGENTS_PROJECT_RANK));
5340
+ for (const dir of projectDirs) {
5341
+ await this.collectSkillsDir(join6(dir, ".kimi-code", "skills"), KIMI_SKILL_PROJECT_RANK, candidates);
5342
+ }
5343
+ }
5344
+ await this.collectSkillsDir(join6(kimiAgentDir(), "skills"), KIMI_SKILL_USER_RANK, candidates);
5345
+ if (this.control.signal.aborted) return [];
5346
+ return candidates;
5347
+ }
5348
+ async get(candidate, _options) {
5349
+ const locator = candidate.locator;
5350
+ if (locator.kind === "skill-file") {
5351
+ const parsed = await this.tryParse(locator.path);
5352
+ if (parsed === void 0) return void 0;
5353
+ return {
5354
+ name: parsed.name,
5355
+ description: parsed.description,
5356
+ ...parsed.whenToUse === void 0 ? {} : { whenToUse: parsed.whenToUse },
5357
+ invocation: parsed.invocation,
5358
+ source: candidate.source,
5359
+ provider: this.name,
5360
+ content: parsed.content,
5361
+ path: locator.path,
5362
+ resourceBase: { kind: "directory", path: dirname3(locator.path) }
5363
+ };
5364
+ }
5365
+ const content = await readSources(locator.paths);
5366
+ if (content === void 0) return void 0;
5367
+ const first = locator.paths[0];
5368
+ return {
5369
+ name: candidate.name,
5370
+ description: candidate.description,
5371
+ invocation: candidate.invocation,
5372
+ source: candidate.source,
5373
+ provider: this.name,
5374
+ content,
5375
+ path: first,
5376
+ resourceBase: { kind: "file", path: first }
5377
+ };
5378
+ }
5379
+ /** One merged `agents-md` candidate for a ranked file set. */
5380
+ agentsCandidate(paths, rank) {
5381
+ const first = paths[0];
5382
+ return {
5383
+ name: "agents-md",
5384
+ description: "Kimi project instructions (AGENTS.md)",
5385
+ invocation: { modelInvocable: true, userInvocable: true },
5386
+ source: "custom",
5387
+ provider: this.name,
5388
+ rank,
5389
+ locator: { kind: "agents-md", paths },
5390
+ path: first,
5391
+ resourceBase: { kind: "file", path: first }
5392
+ };
5393
+ }
5394
+ /** Collect every skill in one skills directory, both kimi layouts. */
5395
+ async collectSkillsDir(skillsDir, rank, candidates) {
5396
+ let entries;
5397
+ try {
5398
+ entries = await readdir2(skillsDir, { withFileTypes: true, encoding: "utf8" });
5399
+ } catch {
5400
+ return;
5401
+ }
5402
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
5403
+ const entryPath = join6(skillsDir, entry.name);
5404
+ const info = await stat3(entryPath).catch(() => void 0);
5405
+ if (info === void 0) continue;
5406
+ if (info.isDirectory()) {
5407
+ const path = join6(entryPath, "SKILL.md");
5408
+ const parsed2 = await this.tryParse(path);
5409
+ if (parsed2 === void 0) continue;
5410
+ candidates.push(this.skillCandidate(parsed2, path, rank, entryPath));
5411
+ continue;
5412
+ }
5413
+ if (!entry.name.endsWith(".md")) continue;
5414
+ const parsed = await this.tryParse(entryPath);
5415
+ if (parsed === void 0) continue;
5416
+ candidates.push(this.skillCandidate(parsed, entryPath, rank, skillsDir));
5417
+ }
5418
+ }
5419
+ /** One parsed skill as a ranked candidate. */
5420
+ skillCandidate(skill, path, rank, resourceDir) {
5421
+ return {
5422
+ name: skill.name,
5423
+ description: skill.description,
5424
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
5425
+ invocation: skill.invocation,
5426
+ source: "custom",
5427
+ provider: this.name,
5428
+ rank,
5429
+ locator: { kind: "skill-file", path },
5430
+ path,
5431
+ resourceBase: { kind: "directory", path: resourceDir }
5432
+ };
5433
+ }
5434
+ /** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
5435
+ async tryParse(path) {
5436
+ try {
5437
+ const raw = await readFile3(path, { encoding: "utf8" });
5438
+ return parseSkillFile(raw);
5439
+ } catch {
5440
+ return void 0;
5441
+ }
5442
+ }
5443
+ };
5444
+
5445
+ // src/engine-kimi/commands.ts
5446
+ import { createUserMessage as createUserMessage5 } from "@deepseek-ai/dsh-llm";
5447
+ function forwardKimiCommand(name2) {
5448
+ return (invocation) => {
5449
+ invocation.agent.followup(createUserMessage5({
5450
+ content: [{ type: "text", text: `/${name2}${invocation.rawInput}` }],
5451
+ source: { kind: "user" }
5452
+ }));
5453
+ return { kind: "success" };
5454
+ };
5455
+ }
5456
+ function builtin(name2, description) {
5457
+ return { name: name2, description, handler: forwardKimiCommand(name2) };
5458
+ }
5459
+ var KIMI_COMMANDS = [
5460
+ builtin("help", "Show available Kimi Code commands"),
5461
+ builtin("status", "Show the current session runtime state"),
5462
+ builtin("compact", "Compact the conversation context to free token usage"),
5463
+ builtin("clear", "Start a fresh session, discarding the current context"),
5464
+ builtin("plan", "Toggle plan (read-only exploration) mode"),
5465
+ builtin("auto", "Toggle auto permission mode"),
5466
+ builtin("usage", "Show token usage, context, and quota information"),
5467
+ builtin("version", "Display the Kimi Code CLI version number"),
5468
+ builtin("goal", "Start or manage an autonomous goal")
5469
+ ];
5470
+
5471
+ // src/settings.ts
5472
+ import z5 from "@deepseek-ai/schemastery";
5473
+
5474
+ // src/namespace.ts
5475
+ var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
5476
+
5477
+ // src/settings.ts
5478
+ var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi", "kimi"];
5479
+ var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
5480
+ engine: z5.union([z5.const("in-process"), z5.const("claude-code"), z5.const("codex"), z5.const("pi"), z5.const("kimi")]).default("in-process"),
5481
+ showInComposer: z5.boolean().default(true)
5482
+ });
5483
+ function loopEngineSettingsNamespace() {
5484
+ return LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL;
5485
+ }
5486
+
5487
+ // src/patch-manager.ts
5488
+ var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
5489
+ var MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
5490
+ var END_MARKER_LINE = `${MANAGED_BLOCK_END}
5491
+ `;
5492
+ function renderManagedBlock(engine) {
5493
+ if (engine === "in-process") return "";
5494
+ return [
5495
+ `${MANAGED_BLOCK_BEGIN}${engine} --`,
5496
+ "- id: agent-loop",
5497
+ " disabled: true",
5498
+ "- id: command-goal",
5499
+ " disabled: true",
5500
+ END_MARKER_LINE
5501
+ ].join("\n");
5502
+ }
5503
+ var BEGIN_MARKER_RE = /^# -- dsh-loop-engine managed block: (\S+) --$/m;
5504
+ function currentEngineOf(text) {
5505
+ const engine = BEGIN_MARKER_RE.exec(text)?.[1];
5506
+ return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : "in-process";
5507
+ }
5508
+ function managedSpan(text) {
5509
+ const begin = text.indexOf(MANAGED_BLOCK_BEGIN);
5510
+ if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
5511
+ const afterBegin = begin + MANAGED_BLOCK_BEGIN.length;
5512
+ const endAt = text.indexOf(MANAGED_BLOCK_END, afterBegin);
5513
+ const spanEnd = endAt === -1 ? text.length : endAt + END_MARKER_LINE.length;
5514
+ const before = text.slice(0, begin);
5515
+ const blankBefore = before.endsWith("\n\n");
5516
+ return {
5517
+ head: blankBefore ? before.slice(0, -1) : before,
5518
+ tail: text.slice(spanEnd),
5519
+ present: true,
5520
+ blankBefore
5521
+ };
5522
+ }
5523
+ function ensureTrailingNewline(text) {
5524
+ return text.endsWith("\n") ? text : `${text}
5525
+ `;
4011
5526
  }
4012
- async function findProjectRoot(cwd) {
4013
- let current = cwd;
4014
- while (true) {
4015
- try {
4016
- await stat(join4(current, ".git"));
4017
- return current;
4018
- } catch {
5527
+ function hasRootEntry(text) {
5528
+ return /^(?:- |\[)/m.test(text);
5529
+ }
5530
+ function dropSeedPlaceholder(text) {
5531
+ return text.replace(/^\[\]\n/m, "");
5532
+ }
5533
+ function seedEmptyArray(text) {
5534
+ const head = text.replace(/\n+$/, "");
5535
+ return head === "" ? "[]\n" : `${head}
5536
+ []
5537
+ `;
5538
+ }
5539
+ function applyManagedBlock(text, engine) {
5540
+ const block = renderManagedBlock(engine);
5541
+ const span = managedSpan(text);
5542
+ let result;
5543
+ if (!span.present) {
5544
+ if (block === "") {
5545
+ result = text;
5546
+ } else {
5547
+ const base = ensureTrailingNewline(text);
5548
+ result = `${base}
5549
+ ${block}`;
4019
5550
  }
4020
- const parent = resolve(current, "..");
4021
- if (parent === current) return cwd;
4022
- current = parent;
5551
+ } else if (block === "") {
5552
+ result = span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
5553
+ } else {
5554
+ result = `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
4023
5555
  }
5556
+ if (block !== "") return dropSeedPlaceholder(result);
5557
+ if (text.trim() === "") return result;
5558
+ return hasRootEntry(result) ? result : seedEmptyArray(result);
4024
5559
  }
4025
5560
 
4026
- // src/engine-codex/skills.ts
4027
- import { homedir as homedir3 } from "node:os";
4028
- import { join as join6 } from "node:path";
4029
-
4030
- // src/driver-core/context-files.ts
4031
- import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
4032
- import { join as join5, resolve as resolve2 } from "node:path";
4033
- async function projectAncestors(cwd) {
4034
- const root = await findProjectRoot(resolve2(cwd));
4035
- const dirs = [];
4036
- let current = resolve2(cwd);
4037
- while (true) {
4038
- dirs.push(current);
4039
- if (current === root) return dirs;
4040
- current = resolve2(current, "..");
4041
- }
5561
+ // src/preset.ts
5562
+ import { mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
5563
+ import { randomUUID } from "node:crypto";
5564
+ import { dirname as dirname4, join as join7 } from "node:path";
5565
+ var HOSTED_PRESET_ID = "loop-engine";
5566
+ var USER_PRESET_DIR = ".agent-presets";
5567
+ var COMPOSITION_FILE = "agent.cordis.yml";
5568
+ var METADATA_FILE = "preset.yml";
5569
+ var SOURCE_PRESET_ID = "standard";
5570
+ var STRIPPED_ROWS = ["skill-filesystem", "tool-skill", "tool-goal", "planning", "compaction"];
5571
+ var MANAGED_HEADER = `# Managed by dsh-loop-engine: the deployment's "${SOURCE_PRESET_ID}" preset minus
5572
+ # the dsh-native command/skill rows a hosted loop engine replaces. Regenerated
5573
+ # from "${SOURCE_PRESET_ID}" on boot \u2014 hand edits are overwritten.
5574
+ `;
5575
+ var MANAGED_METADATA = "name: Hosted Engine\ndescription: Standard preset minus the dsh-native commands and skills a hosted loop engine replaces.\n";
5576
+ function isEntryStart(line) {
5577
+ return line.startsWith("- ");
4042
5578
  }
4043
- async function collectProjectContextFiles(cwd, policy) {
4044
- const files = [];
4045
- for (const dir of await projectAncestors(cwd)) {
4046
- const chosen = await dirContextFile(dir, policy);
4047
- if (chosen !== void 0) files.push(chosen);
4048
- }
4049
- return files;
5579
+ function entryId(line) {
5580
+ return /^- id:\s*(\S+)\s*$/.exec(line)?.[1];
4050
5581
  }
4051
- async function dirContextFile(dir, policy) {
4052
- if (policy.override !== void 0) {
4053
- const override = join5(dir, policy.override);
4054
- if (await pathExists(override)) return override;
4055
- }
4056
- for (const name2 of policy.primary) {
4057
- const candidate = join5(dir, name2);
4058
- if (await pathExists(candidate)) return candidate;
4059
- }
4060
- return void 0;
5582
+ function stripPresetRows(text, ids = STRIPPED_ROWS) {
5583
+ const lines = text.split("\n");
5584
+ const starts = [];
5585
+ for (const [index, line] of lines.entries()) {
5586
+ if (isEntryStart(line)) starts.push(index);
5587
+ }
5588
+ if (starts.length === 0) return text;
5589
+ const drop = new Set(ids);
5590
+ const entries = [];
5591
+ let heading = lines.slice(0, starts[0]);
5592
+ for (const [index, start] of starts.entries()) {
5593
+ const end = index + 1 < starts.length ? starts[index + 1] : lines.length;
5594
+ const span = lines.slice(start, end);
5595
+ let bodyEnd = span.length;
5596
+ while (bodyEnd > 1) {
5597
+ const line = span[bodyEnd - 1];
5598
+ if (line.trim() !== "" && !line.trimStart().startsWith("#")) break;
5599
+ bodyEnd -= 1;
5600
+ }
5601
+ entries.push({ id: entryId(span[0]), heading, body: span.slice(0, bodyEnd) });
5602
+ heading = span.slice(bodyEnd);
5603
+ }
5604
+ const out = [];
5605
+ out.push(...entries[0].heading);
5606
+ let lastKept = -1;
5607
+ for (const [index, entry] of entries.entries()) {
5608
+ if (entry.id !== void 0 && drop.has(entry.id)) continue;
5609
+ if (index > 0) out.push(...entry.heading);
5610
+ out.push(...entry.body);
5611
+ lastKept = index;
5612
+ }
5613
+ if (lastKept === entries.length - 1) out.push(...heading);
5614
+ if (out.length > 0 && out[out.length - 1] !== "") out.push("");
5615
+ return out.join("\n");
4061
5616
  }
4062
- async function pathExists(path) {
5617
+ async function writeIfDifferent(path, text) {
4063
5618
  try {
4064
- await stat2(path);
4065
- return true;
5619
+ if (await readFile4(path, "utf8") === text) return false;
4066
5620
  } catch {
4067
- return false;
4068
5621
  }
5622
+ await mkdir(dirname4(path), { recursive: true });
5623
+ const tmp = `${path}.tmp-${randomUUID()}`;
5624
+ await writeFile(tmp, text, "utf8");
5625
+ await rename(tmp, path);
5626
+ return true;
4069
5627
  }
4070
- async function readOptionalFile(path) {
5628
+ async function ensureHostedPreset(dshHome, source) {
5629
+ const composition = await source.read(SOURCE_PRESET_ID);
5630
+ const stripped = `${MANAGED_HEADER}
5631
+ ${stripPresetRows(composition)}`;
5632
+ const dir = join7(dshHome, USER_PRESET_DIR, HOSTED_PRESET_ID);
5633
+ const compositionChanged = await writeIfDifferent(join7(dir, COMPOSITION_FILE), stripped);
5634
+ const metadataChanged = await writeIfDifferent(join7(dir, METADATA_FILE), MANAGED_METADATA);
5635
+ return compositionChanged || metadataChanged;
5636
+ }
5637
+
5638
+ // src/provider-route.ts
5639
+ import { LlmAdapter, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
5640
+ var HOSTED_PROVIDER_ROUTES = {
5641
+ "claude-code": PROVIDER,
5642
+ codex: PROVIDER2,
5643
+ pi: PROVIDER3,
5644
+ kimi: PROVIDER4
5645
+ };
5646
+ var HostedEngineRouteAdapter = class extends LlmAdapter {
5647
+ /**
5648
+ * @param label - the provider route label this placeholder serves.
5649
+ */
5650
+ constructor(label) {
5651
+ super();
5652
+ this.label = label;
5653
+ }
5654
+ label;
5655
+ stream(_options) {
5656
+ throw new LlmError5(
5657
+ `provider "${this.label}" is a hosted loop engine route, not a model endpoint`,
5658
+ "HOSTED_ENGINE_ROUTE"
5659
+ );
5660
+ }
5661
+ };
5662
+
5663
+ // src/commands.ts
5664
+ import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
5665
+ import { homedir as homedir4 } from "node:os";
5666
+ import { join as join8 } from "node:path";
5667
+ import { createUserMessage as createUserMessage6 } from "@deepseek-ai/dsh-llm";
5668
+ var COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
5669
+ function forwardClaudeCodeCommand(name2) {
5670
+ return (invocation) => {
5671
+ invocation.agent.followup(createUserMessage6({
5672
+ content: [{ type: "text", text: `/${name2}${invocation.rawInput}` }],
5673
+ source: { kind: "user" }
5674
+ }));
5675
+ return { kind: "success" };
5676
+ };
5677
+ }
5678
+ function builtin2(name2, description) {
5679
+ return { name: name2, description, handler: forwardClaudeCodeCommand(name2) };
5680
+ }
5681
+ var CLAUDE_CODE_COMMANDS = [
5682
+ builtin2("help", "Show help about Claude Code commands"),
5683
+ builtin2("compact", "Compact the conversation to reduce context usage"),
5684
+ builtin2("clear", "Clear the conversation and start fresh"),
5685
+ builtin2("review", "Review recent changes (git diff)"),
5686
+ builtin2("explain", "Explain the selected code"),
5687
+ builtin2("fix", "Fix issues in the code"),
5688
+ builtin2("tests", "Add tests for the selected code")
5689
+ ];
5690
+ function discoverUserSlashCommands() {
5691
+ let entries;
4071
5692
  try {
4072
- return await readFile2(path, { encoding: "utf8" });
5693
+ entries = readdirSync(userCommandsDir(), { encoding: "utf8" });
4073
5694
  } catch {
4074
- return void 0;
5695
+ return [];
4075
5696
  }
4076
- }
4077
- async function anySourceNonEmpty(paths) {
4078
- for (const path of paths) {
4079
- const raw = await readOptionalFile(path);
4080
- if (raw !== void 0 && raw.trim().length > 0) return true;
5697
+ const definitions = [];
5698
+ const seen = new Set(CLAUDE_CODE_COMMANDS.map((command) => command.name));
5699
+ for (const entry of entries.sort()) {
5700
+ if (!entry.endsWith(".md")) continue;
5701
+ const name2 = entry.slice(0, -".md".length);
5702
+ if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
5703
+ const path = join8(userCommandsDir(), entry);
5704
+ let raw;
5705
+ try {
5706
+ raw = readFileSync2(path, "utf8");
5707
+ } catch {
5708
+ continue;
5709
+ }
5710
+ const description = commandDescription(raw);
5711
+ if (description === void 0) continue;
5712
+ seen.add(name2);
5713
+ definitions.push({ name: name2, description, handler: forwardClaudeCodeCommand(name2) });
4081
5714
  }
4082
- return false;
5715
+ return definitions;
4083
5716
  }
4084
- async function fileNonEmpty(path) {
4085
- const raw = await readOptionalFile(path);
4086
- return raw !== void 0 && raw.trim().length > 0;
5717
+ function userCommandsDir() {
5718
+ return join8(homedir4(), ".claude", "commands");
4087
5719
  }
4088
- async function readSources(paths) {
4089
- const parts = [];
4090
- for (const path of paths) {
4091
- const raw = await readOptionalFile(path);
4092
- if (raw !== void 0 && raw.trim().length > 0) parts.push(raw);
5720
+ function commandDescription(raw) {
5721
+ const trimmed = raw.trim();
5722
+ if (trimmed.length === 0) return void 0;
5723
+ let body = trimmed;
5724
+ if (trimmed.startsWith("---\n")) {
5725
+ const closing = trimmed.indexOf("\n---");
5726
+ if (closing <= 0) return void 0;
5727
+ for (const line of trimmed.slice(4, closing).split("\n")) {
5728
+ const colon = line.indexOf(":");
5729
+ if (colon < 0 || line.slice(0, colon).trim() !== "description") continue;
5730
+ const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
5731
+ if (value.length > 0) return value;
5732
+ }
5733
+ body = trimmed.slice(closing + 4);
4093
5734
  }
4094
- return parts.length > 0 ? parts.join("\n\n") : void 0;
5735
+ for (const line of body.split("\n")) {
5736
+ const candidate = line.trim();
5737
+ if (candidate.length === 0 || candidate.startsWith("#")) continue;
5738
+ return candidate.length > 120 ? `${candidate.slice(0, 119)}\u2026` : candidate;
5739
+ }
5740
+ return void 0;
4095
5741
  }
4096
5742
 
4097
5743
  // src/engine-codex/skills.ts
4098
- var PROVIDER_NAME2 = "codex";
5744
+ import { homedir as homedir5 } from "node:os";
5745
+ import { join as join9 } from "node:path";
5746
+ var PROVIDER_NAME3 = "codex";
4099
5747
  var CODEX_PROJECT_RANK = 140;
4100
5748
  var CODEX_USER_RANK = 160;
4101
5749
  var CODEX_CONTEXT_POLICY = { primary: ["AGENTS.md"] };
@@ -4104,7 +5752,7 @@ var CodexSkillProvider = class {
4104
5752
  this.control = control;
4105
5753
  }
4106
5754
  control;
4107
- name = PROVIDER_NAME2;
5755
+ name = PROVIDER_NAME3;
4108
5756
  async list(options) {
4109
5757
  const candidates = [];
4110
5758
  const cwd = options.cwd;
@@ -4112,7 +5760,7 @@ var CodexSkillProvider = class {
4112
5760
  const paths = await collectProjectContextFiles(cwd, CODEX_CONTEXT_POLICY);
4113
5761
  if (await anySourceNonEmpty(paths)) candidates.push(this.agentsCandidate(paths, CODEX_PROJECT_RANK));
4114
5762
  }
4115
- const userPath = join6(homedir3(), ".codex", "AGENTS.md");
5763
+ const userPath = join9(homedir5(), ".codex", "AGENTS.md");
4116
5764
  if (await fileNonEmpty(userPath)) candidates.push(this.agentsCandidate([userPath], CODEX_USER_RANK));
4117
5765
  if (this.control.signal.aborted) return [];
4118
5766
  return candidates;
@@ -4151,10 +5799,10 @@ var CodexSkillProvider = class {
4151
5799
  };
4152
5800
 
4153
5801
  // src/engine-pi/skills.ts
4154
- import { readdir as readdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
4155
- import { homedir as homedir4 } from "node:os";
4156
- import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
4157
- var PROVIDER_NAME3 = "pi";
5802
+ import { readdir as readdir3, readFile as readFile5, stat as stat4 } from "node:fs/promises";
5803
+ import { homedir as homedir6 } from "node:os";
5804
+ import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
5805
+ var PROVIDER_NAME4 = "pi";
4158
5806
  var PI_AGENTS_PROJECT_RANK = 140;
4159
5807
  var PI_SKILL_PROJECT_RANK = 150;
4160
5808
  var PI_AGENTS_USER_RANK = 160;
@@ -4165,15 +5813,15 @@ var PI_CONTEXT_POLICY = {
4165
5813
  };
4166
5814
  function piAgentDir() {
4167
5815
  const override = process.env.PI_CODING_AGENT_DIR;
4168
- if (override !== void 0 && override.length > 0) return resolve3(override);
4169
- return join7(homedir4(), ".pi", "agent");
5816
+ if (override !== void 0 && override.length > 0) return resolve4(override);
5817
+ return join10(homedir6(), ".pi", "agent");
4170
5818
  }
4171
5819
  var PiSkillProvider = class {
4172
5820
  constructor(control) {
4173
5821
  this.control = control;
4174
5822
  }
4175
5823
  control;
4176
- name = PROVIDER_NAME3;
5824
+ name = PROVIDER_NAME4;
4177
5825
  async list(options) {
4178
5826
  const candidates = [];
4179
5827
  const cwd = options.cwd;
@@ -4182,13 +5830,13 @@ var PiSkillProvider = class {
4182
5830
  const contextPaths = await collectProjectContextFiles(cwd, PI_CONTEXT_POLICY);
4183
5831
  if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, PI_AGENTS_PROJECT_RANK));
4184
5832
  for (const dir of projectDirs) {
4185
- await this.collectSkillsDir(join7(dir, ".pi", "skills"), PI_SKILL_PROJECT_RANK, candidates);
5833
+ await this.collectSkillsDir(join10(dir, ".pi", "skills"), PI_SKILL_PROJECT_RANK, candidates);
4186
5834
  }
4187
5835
  }
4188
5836
  const userAgentDir = piAgentDir();
4189
- const userContext = join7(userAgentDir, "AGENTS.md");
5837
+ const userContext = join10(userAgentDir, "AGENTS.md");
4190
5838
  if (await fileNonEmpty(userContext)) candidates.push(this.agentsCandidate([userContext], PI_AGENTS_USER_RANK));
4191
- await this.collectSkillsDir(join7(userAgentDir, "skills"), PI_SKILL_USER_RANK, candidates);
5839
+ await this.collectSkillsDir(join10(userAgentDir, "skills"), PI_SKILL_USER_RANK, candidates);
4192
5840
  if (this.control.signal.aborted) return [];
4193
5841
  return candidates;
4194
5842
  }
@@ -4206,7 +5854,7 @@ var PiSkillProvider = class {
4206
5854
  provider: this.name,
4207
5855
  content: parsed.content,
4208
5856
  path: locator.path,
4209
- resourceBase: { kind: "directory", path: dirname3(locator.path) }
5857
+ resourceBase: { kind: "directory", path: dirname5(locator.path) }
4210
5858
  };
4211
5859
  }
4212
5860
  const content = await readSources(locator.paths);
@@ -4242,16 +5890,16 @@ var PiSkillProvider = class {
4242
5890
  async collectSkillsDir(skillsDir, rank, candidates) {
4243
5891
  let entries;
4244
5892
  try {
4245
- entries = await readdir2(skillsDir, { withFileTypes: true, encoding: "utf8" });
5893
+ entries = await readdir3(skillsDir, { withFileTypes: true, encoding: "utf8" });
4246
5894
  } catch {
4247
5895
  return;
4248
5896
  }
4249
5897
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4250
- const entryPath = join7(skillsDir, entry.name);
4251
- const info = await stat3(entryPath).catch(() => void 0);
5898
+ const entryPath = join10(skillsDir, entry.name);
5899
+ const info = await stat4(entryPath).catch(() => void 0);
4252
5900
  if (info === void 0) continue;
4253
5901
  if (info.isDirectory()) {
4254
- const path = join7(entryPath, "SKILL.md");
5902
+ const path = join10(entryPath, "SKILL.md");
4255
5903
  const parsed2 = await this.tryParse(path);
4256
5904
  if (parsed2 === void 0) continue;
4257
5905
  candidates.push(this.skillCandidate(parsed2, path, rank, entryPath));
@@ -4281,7 +5929,7 @@ var PiSkillProvider = class {
4281
5929
  /** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
4282
5930
  async tryParse(path) {
4283
5931
  try {
4284
- const raw = await readFile3(path, { encoding: "utf8" });
5932
+ const raw = await readFile5(path, { encoding: "utf8" });
4285
5933
  return parseSkillFile(raw);
4286
5934
  } catch {
4287
5935
  return void 0;
@@ -4294,23 +5942,24 @@ var name = "loop-engine";
4294
5942
  var inject = [];
4295
5943
  var MAX_MOUNT_ATTEMPTS = 40;
4296
5944
  var MOUNT_RETRY_MS = 50;
4297
- var Config4 = z5.object({
4298
- profile: z5.string(),
4299
- patchFilename: z5.string(),
4300
- patchPath: z5.string(),
4301
- permissionMode: z5.union(CLAUDE_CODE_PERMISSION_MODES.map((mode) => z5.const(mode))),
4302
- env: z5.dict(z5.string()),
4303
- model: z5.string(),
4304
- disposeGraceMs: z5.number(),
4305
- maxTurns: z5.number(),
4306
- sandboxMode: z5.union(CODEX_SANDBOX_MODES.map((mode) => z5.const(mode))),
4307
- approvalPolicy: z5.union(CODEX_APPROVAL_POLICIES.map((policy) => z5.const(policy))),
4308
- piProvider: z5.string(),
4309
- piThinking: z5.string()
5945
+ var Config5 = z6.object({
5946
+ profile: z6.string(),
5947
+ patchFilename: z6.string(),
5948
+ patchPath: z6.string(),
5949
+ permissionMode: z6.union(CLAUDE_CODE_PERMISSION_MODES.map((mode) => z6.const(mode))),
5950
+ env: z6.dict(z6.string()),
5951
+ model: z6.string(),
5952
+ disposeGraceMs: z6.number(),
5953
+ maxTurns: z6.number(),
5954
+ sandboxMode: z6.union(CODEX_SANDBOX_MODES.map((mode) => z6.const(mode))),
5955
+ approvalPolicy: z6.union(CODEX_APPROVAL_POLICIES.map((policy) => z6.const(policy))),
5956
+ piProvider: z6.string(),
5957
+ piThinking: z6.string(),
5958
+ kimiBin: z6.string()
4310
5959
  });
4311
5960
  function resolvePatchPath(config) {
4312
5961
  if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
4313
- return join8(
5962
+ return join11(
4314
5963
  resolveDshHome(),
4315
5964
  "profiles",
4316
5965
  config.profile ?? "web",
@@ -4322,21 +5971,21 @@ function isMissing(error) {
4322
5971
  }
4323
5972
  async function readPatchOrUndefined(path) {
4324
5973
  try {
4325
- return await readFile4(path, "utf8");
5974
+ return await readFile6(path, "utf8");
4326
5975
  } catch (error) {
4327
5976
  if (isMissing(error)) return void 0;
4328
5977
  throw error;
4329
5978
  }
4330
5979
  }
4331
5980
  async function writePatchFile(path, text) {
4332
- await mkdir(dirname4(path), { recursive: true });
4333
- const tmp = `${path}.tmp-${randomUUID()}`;
4334
- await writeFile(tmp, text, "utf8");
4335
- await rename(tmp, path);
5981
+ await mkdir2(dirname6(path), { recursive: true });
5982
+ const tmp = `${path}.tmp-${randomUUID2()}`;
5983
+ await writeFile2(tmp, text, "utf8");
5984
+ await rename2(tmp, path);
4336
5985
  }
4337
5986
  function writePatchFileSync(path, text) {
4338
- mkdirSync(dirname4(path), { recursive: true });
4339
- const tmp = `${path}.tmp-${randomUUID()}`;
5987
+ mkdirSync(dirname6(path), { recursive: true });
5988
+ const tmp = `${path}.tmp-${randomUUID2()}`;
4340
5989
  writeFileSync(tmp, text, "utf8");
4341
5990
  renameSync(tmp, path);
4342
5991
  }
@@ -4381,9 +6030,19 @@ function piConfig(config) {
4381
6030
  ...config.sandboxMode === void 0 ? {} : { sandboxMode: config.sandboxMode }
4382
6031
  };
4383
6032
  }
6033
+ function kimiConfig(config) {
6034
+ return {
6035
+ ...config.model === void 0 ? {} : { model: config.model },
6036
+ ...config.env === void 0 ? {} : { env: config.env },
6037
+ ...config.kimiBin === void 0 ? {} : { bin: config.kimiBin }
6038
+ };
6039
+ }
4384
6040
  function apply(ctx, config) {
4385
6041
  const patchPath = resolvePatchPath(config);
4386
6042
  let fileEngine = currentEngineOf(readPatchFileSync(patchPath));
6043
+ const AGENT_PRESETS_NS = "agent-presets";
6044
+ const PRESET_DEFAULT_ATTEMPTS = 30;
6045
+ const PRESET_DEFAULT_RETRY_MS = 100;
4387
6046
  let engineFiber;
4388
6047
  let mountedEngine;
4389
6048
  let commandDisposers;
@@ -4396,6 +6055,105 @@ function apply(ctx, config) {
4396
6055
  mountRetry = void 0;
4397
6056
  }
4398
6057
  };
6058
+ let savedPresetDefault;
6059
+ let presetRetry;
6060
+ const CLEAR_PRESET_RETRY = () => {
6061
+ if (presetRetry !== void 0) {
6062
+ clearTimeout(presetRetry);
6063
+ presetRetry = void 0;
6064
+ }
6065
+ };
6066
+ const ROUTE_ATTEMPTS = 30;
6067
+ const ROUTE_RETRY_MS = 100;
6068
+ let routeHandle;
6069
+ let routeEngine;
6070
+ let routeRetry;
6071
+ const CLEAR_ROUTE_RETRY = () => {
6072
+ if (routeRetry !== void 0) {
6073
+ clearTimeout(routeRetry);
6074
+ routeRetry = void 0;
6075
+ }
6076
+ };
6077
+ const releaseRoute = () => {
6078
+ CLEAR_ROUTE_RETRY();
6079
+ const handle = routeHandle;
6080
+ routeHandle = void 0;
6081
+ routeEngine = void 0;
6082
+ handle?.();
6083
+ };
6084
+ const mountProviderRoute = (engine, attempt = 0) => {
6085
+ if (engine === "in-process") return;
6086
+ if (routeEngine === engine && routeHandle !== void 0) return;
6087
+ CLEAR_ROUTE_RETRY();
6088
+ const label = HOSTED_PROVIDER_ROUTES[engine];
6089
+ const llm = ctx.get("llm");
6090
+ if (llm === void 0) {
6091
+ if (attempt < ROUTE_ATTEMPTS) {
6092
+ routeRetry = setTimeout(() => {
6093
+ mountProviderRoute(engine, attempt + 1);
6094
+ }, ROUTE_RETRY_MS);
6095
+ }
6096
+ return;
6097
+ }
6098
+ try {
6099
+ routeHandle = llm.registerAdapter([label], new HostedEngineRouteAdapter(label));
6100
+ routeEngine = engine;
6101
+ } catch (error) {
6102
+ if (error instanceof Error && error.message.includes("already registered")) {
6103
+ ctx.logger.warn(`loop-engine: provider route "${label}" is already served by another adapter`);
6104
+ return;
6105
+ }
6106
+ ctx.logger.error(`loop-engine: provider route "${label}" registration failed: ${String(error)}`);
6107
+ }
6108
+ };
6109
+ const mutatePresetDefault = (op, attempt = 0) => {
6110
+ const settings = ctx.get("settings");
6111
+ if (settings === void 0) return;
6112
+ settings.mutate(AGENT_PRESETS_NS, [op]).then(() => void 0, (error) => {
6113
+ if (error instanceof Error && error.message.includes("not registered") && attempt < PRESET_DEFAULT_ATTEMPTS) {
6114
+ presetRetry = setTimeout(() => {
6115
+ mutatePresetDefault(op, attempt + 1);
6116
+ }, PRESET_DEFAULT_RETRY_MS);
6117
+ return;
6118
+ }
6119
+ ctx.logger.error(`loop-engine: preset default switch failed: ${String(error)}`);
6120
+ });
6121
+ };
6122
+ const restorePresetDefault = (attempt = 0) => {
6123
+ const presets = ctx.get("agentPresets");
6124
+ if (presets === void 0) return;
6125
+ if (presets.defaultId === HOSTED_PRESET_ID) {
6126
+ const saved = savedPresetDefault;
6127
+ savedPresetDefault = void 0;
6128
+ mutatePresetDefault(saved === void 0 ? { op: "unset", path: ["default"] } : { op: "set", path: ["default"], value: saved });
6129
+ return;
6130
+ }
6131
+ if (attempt < PRESET_DEFAULT_ATTEMPTS) {
6132
+ presetRetry = setTimeout(() => {
6133
+ restorePresetDefault(attempt + 1);
6134
+ }, PRESET_DEFAULT_RETRY_MS);
6135
+ }
6136
+ };
6137
+ const steerPresetDefault = (engine) => {
6138
+ const presets = ctx.get("agentPresets");
6139
+ if (presets === void 0) return;
6140
+ if (engine === "in-process") {
6141
+ restorePresetDefault();
6142
+ return;
6143
+ }
6144
+ void (async () => {
6145
+ try {
6146
+ await ensureHostedPreset(resolveDshHome(), presets);
6147
+ } catch (error) {
6148
+ ctx.logger.error(`loop-engine: hosted preset authoring failed: ${String(error)}`);
6149
+ return;
6150
+ }
6151
+ const current = presets.defaultId;
6152
+ if (current === HOSTED_PRESET_ID) return;
6153
+ savedPresetDefault = current;
6154
+ mutatePresetDefault({ op: "set", path: ["default"], value: HOSTED_PRESET_ID });
6155
+ })();
6156
+ };
4399
6157
  const cleanupEngineRegistrations = () => {
4400
6158
  if (commandDisposers !== void 0) {
4401
6159
  for (const dispose of commandDisposers) dispose();
@@ -4459,15 +6217,37 @@ function apply(ctx, config) {
4459
6217
  }
4460
6218
  hostFactory("pi", () => ctx.plugin(PiLoop, piConfig(config)));
4461
6219
  };
6220
+ const mountKimi = () => {
6221
+ const commands = ctx.get("commands");
6222
+ if (commands !== void 0) {
6223
+ const disposers = [];
6224
+ for (const command of KIMI_COMMANDS) {
6225
+ try {
6226
+ disposers.push(commands.register(command));
6227
+ } catch (error) {
6228
+ ctx.logger.warn(`loop-engine: skip kimi command /${command.name}: ${String(error)}`);
6229
+ }
6230
+ }
6231
+ commandDisposers = disposers;
6232
+ }
6233
+ const skills = ctx.get("skills");
6234
+ if (skills !== void 0) {
6235
+ skillDisposer = skills.registerProvider((control) => new KimiSkillProvider(control));
6236
+ }
6237
+ hostFactory("kimi", () => ctx.plugin(KimiLoop, kimiConfig(config)));
6238
+ };
4462
6239
  const mountEngine = (engine) => {
6240
+ mountProviderRoute(engine);
4463
6241
  if (engine === "claude-code") mountClaude();
4464
6242
  else if (engine === "codex") mountCodex();
4465
6243
  else if (engine === "pi") mountPi();
6244
+ else if (engine === "kimi") mountKimi();
4466
6245
  };
4467
6246
  const unmountEngine = () => {
4468
6247
  const fiber = engineFiber;
4469
6248
  mountAttempts = 0;
4470
6249
  CLEAR_RETRY();
6250
+ releaseRoute();
4471
6251
  cleanupEngineRegistrations();
4472
6252
  mountedEngine = void 0;
4473
6253
  if (fiber === void 0) return;
@@ -4477,31 +6257,40 @@ function apply(ctx, config) {
4477
6257
  }, () => void 0);
4478
6258
  };
4479
6259
  mountEngine(fileEngine);
4480
- ctx.effect(() => () => CLEAR_RETRY(), "loop-engine: mount retry cleanup");
6260
+ steerPresetDefault(fileEngine);
6261
+ ctx.effect(() => () => {
6262
+ CLEAR_RETRY();
6263
+ CLEAR_PRESET_RETRY();
6264
+ releaseRoute();
6265
+ }, "loop-engine: retry cleanup");
4481
6266
  let source;
4482
- installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine, showInComposer: true }, {
4483
- setSource: (current) => {
4484
- source = current;
4485
- },
4486
- onChange: () => {
4487
- const next = source().engine;
4488
- if (next === fileEngine) return;
4489
- if (mountedEngine !== next) {
4490
- unmountEngine();
4491
- mountEngine(next);
4492
- }
4493
- try {
4494
- const updated = applyManagedBlock(readPatchFileSync(patchPath), next);
4495
- writePatchFileSync(patchPath, updated);
4496
- fileEngine = next;
4497
- } catch (error) {
4498
- ctx.logger.error(`loop-engine: managed block write failed: ${String(error)}`);
6267
+ ctx.inject(["settings"], (settingsCtx) => {
6268
+ settingsCtx.settings.installSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine, showInComposer: true }, {
6269
+ setSource: (current) => {
6270
+ source = current;
6271
+ },
6272
+ onChange: () => {
6273
+ const next = source().engine;
6274
+ if (next === fileEngine) return;
6275
+ if (mountedEngine !== next) {
6276
+ unmountEngine();
6277
+ mountEngine(next);
6278
+ }
6279
+ try {
6280
+ const updated = applyManagedBlock(readPatchFileSync(patchPath), next);
6281
+ writePatchFileSync(patchPath, updated);
6282
+ fileEngine = next;
6283
+ } catch (error) {
6284
+ ctx.logger.error(`loop-engine: managed block write failed: ${String(error)}`);
6285
+ return;
6286
+ }
6287
+ steerPresetDefault(next);
4499
6288
  }
4500
- }
6289
+ });
4501
6290
  });
4502
6291
  }
4503
6292
  export {
4504
- Config4 as Config,
6293
+ Config5 as Config,
4505
6294
  apply,
4506
6295
  inject,
4507
6296
  name,