dsh-loop-engine 1.0.0-rc7 → 1.0.0-rc9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -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 readFile5, rename, writeFile } from "node:fs/promises";
50
- import { randomUUID } from "node:crypto";
51
- import { dirname as dirname5, join as join10 } from "node:path";
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
52
  import z6 from "@deepseek-ai/schemastery";
53
- import { installSettingsSection } from "@deepseek-ai/dsh-settings";
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");
@@ -1020,11 +1019,15 @@ var ClaudeCodeAgent = class {
1020
1019
  };
1021
1020
 
1022
1021
  // src/driver-core/ownership.ts
1023
- import { FiberState } from "@deepseek-ai/cordis";
1022
+ var INACTIVE_FIBER_VALUES = {
1023
+ FAILED: 3,
1024
+ DISPOSED: 4,
1025
+ UNLOADING: 5
1026
+ };
1024
1027
  var INACTIVE_STATES = /* @__PURE__ */ new Set([
1025
- FiberState.UNLOADING,
1026
- FiberState.DISPOSED,
1027
- FiberState.FAILED
1028
+ INACTIVE_FIBER_VALUES.UNLOADING,
1029
+ INACTIVE_FIBER_VALUES.DISPOSED,
1030
+ INACTIVE_FIBER_VALUES.FAILED
1028
1031
  ]);
1029
1032
  var FactoryOwnership = class {
1030
1033
  constructor(fiber) {
@@ -1162,7 +1165,7 @@ var ClaudeCodeLoop = class extends Service {
1162
1165
  * fuses caller cancellation with lifecycle teardown for setup awaits.
1163
1166
  */
1164
1167
  /* 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) {
1168
+ prepare(ownerCtx, id, options, session, callerSignal, handle) {
1166
1169
  ownerCtx.fiber.assertActive();
1167
1170
  if (!this.ownership.isActive()) throw new Error("agent loop is not active");
1168
1171
  if (callerSignal?.aborted) {
@@ -1196,11 +1199,15 @@ var ClaudeCodeLoop = class extends Service {
1196
1199
  }
1197
1200
  } finally {
1198
1201
  try {
1199
- detachAgent?.();
1200
- detachSession?.();
1202
+ await handle?.close();
1201
1203
  } finally {
1202
- untrack();
1203
- if (!ownerTriggered) await unfollowOwner();
1204
+ try {
1205
+ detachAgent?.();
1206
+ detachSession?.();
1207
+ } finally {
1208
+ untrack();
1209
+ if (!ownerTriggered) await unfollowOwner();
1210
+ }
1204
1211
  }
1205
1212
  }
1206
1213
  })();
@@ -1250,18 +1257,27 @@ var ClaudeCodeLoop = class extends Service {
1250
1257
  }
1251
1258
  }
1252
1259
  /** Prepare one Agent around an acquired Session, run setup, and publish it. */
1253
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
1260
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored) {
1254
1261
  var _stack = [];
1255
1262
  try {
1256
1263
  const ownedPreparation = __using(_stack, preparation);
1257
1264
  const session = ownedPreparation.session;
1258
- const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
1265
+ let prepared;
1266
+ try {
1267
+ prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle);
1268
+ } catch (error) {
1269
+ await stored?.handle.close().catch(() => {
1270
+ });
1271
+ throw error;
1272
+ }
1259
1273
  try {
1260
1274
  const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
1261
1275
  setupCommit?.commit();
1276
+ await this.appendUnstoredSuffix(stored, session);
1262
1277
  return prepared.publish(source);
1263
1278
  } catch (error) {
1264
- await prepared.dispose();
1279
+ await prepared.dispose().catch(() => {
1280
+ });
1265
1281
  throw error;
1266
1282
  }
1267
1283
  } catch (_) {
@@ -1272,7 +1288,8 @@ var ClaudeCodeLoop = class extends Service {
1272
1288
  }
1273
1289
  /**
1274
1290
  * Create an agent and session under one caller-supplied identity, owned by
1275
- * the accessing fiber.
1291
+ * the accessing fiber. When a persistence backend is mounted, the session's
1292
+ * durable identity is stored before publication.
1276
1293
  * @param ownerCtx - caller context that structurally owns the lifecycle.
1277
1294
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
1278
1295
  * @returns the published handle.
@@ -1280,20 +1297,73 @@ var ClaudeCodeLoop = class extends Service {
1280
1297
  async createAgent(ownerCtx, options) {
1281
1298
  const preparation = SessionPreparation.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
1282
1299
  ...options.seed === void 0 ? {} : { seed: options.seed },
1283
- ...options.meta === void 0 ? {} : { meta: options.meta }
1300
+ ...options.meta === void 0 ? {} : { meta: options.meta },
1301
+ ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
1284
1302
  }));
1285
- const published = this.setupAndPublish(
1286
- ownerCtx,
1287
- options.sessionId,
1288
- preparation,
1289
- options.agentOptions ?? {},
1290
- options.setup,
1291
- options.signal,
1292
- "startup"
1293
- );
1303
+ const published = (async () => {
1304
+ let stored;
1305
+ try {
1306
+ stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
1307
+ () => this.createStoredSession(preparation.session, options.signal),
1308
+ options.signal,
1309
+ options.sessionId,
1310
+ (abandoned) => {
1311
+ void abandoned?.handle.close().catch(() => {
1312
+ });
1313
+ }
1314
+ );
1315
+ } catch (error) {
1316
+ preparation[Symbol.dispose]();
1317
+ throw error;
1318
+ }
1319
+ return this.setupAndPublish(
1320
+ ownerCtx,
1321
+ options.sessionId,
1322
+ preparation,
1323
+ options.agentOptions ?? {},
1324
+ options.setup,
1325
+ options.signal,
1326
+ "startup",
1327
+ stored
1328
+ );
1329
+ })();
1294
1330
  this.ownership.trackWrapper(published);
1295
1331
  return published;
1296
1332
  }
1333
+ /**
1334
+ * Take a fresh session's write ownership when persistence is mounted.
1335
+ * Nothing is appended here: the constructor seed (which never re-emits
1336
+ * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
1337
+ * publication commit point, so a failed or cancelled setup closes an
1338
+ * unmaterialized handle and leaves no stored residue — the same id can be
1339
+ * created again.
1340
+ * @param session - the unpublished session to store.
1341
+ * @param signal - optional cancellation forwarded to the backend create.
1342
+ * @returns the owned handle and stored cursor, or `undefined` without a backend.
1343
+ */
1344
+ async createStoredSession(session, signal) {
1345
+ const persistence = this.runtime.ctx.get("sessionPersistence");
1346
+ if (persistence === void 0) return void 0;
1347
+ const handle = await persistence.create(session.header, {
1348
+ inheritedEventCount: session.inheritedEventCount,
1349
+ ...signal === void 0 ? {} : { signal }
1350
+ });
1351
+ return { handle, storedCount: 0 };
1352
+ }
1353
+ /**
1354
+ * Durably store the session events appended since the last stored cursor.
1355
+ * Pre-publication appends (constructor seed markers, setup-window events)
1356
+ * never re-emit through `session/event`, so publication must flush them
1357
+ * through the handle before live events start routing into it.
1358
+ * @param stored - the session's owned handle and stored cursor, if any.
1359
+ * @param session - the unpublished session whose suffix is stored.
1360
+ */
1361
+ async appendUnstoredSuffix(stored, session) {
1362
+ if (stored === void 0) return;
1363
+ const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
1364
+ if (suffix.length > 0) await stored.handle.append(suffix);
1365
+ stored.storedCount += suffix.length;
1366
+ }
1297
1367
  /**
1298
1368
  * Resume an owned agent from the configured persistence service.
1299
1369
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
@@ -1307,11 +1377,10 @@ var ClaudeCodeLoop = class extends Service {
1307
1377
  }
1308
1378
  return this.resumeWith(ownerCtx, persistence, options);
1309
1379
  }
1310
- /** Resume through an explicit persistence handle. */
1311
- async resumeWith(ownerCtx, persistence, options) {
1380
+ /** Resume through an explicit persistence service. */
1381
+ resumeWith(ownerCtx, persistence, options) {
1312
1382
  const id = options.resumeSessionId;
1313
- let preparation;
1314
- try {
1383
+ const published = (async () => {
1315
1384
  const ownerAbort = new AbortController();
1316
1385
  const unfollowOwner = ownerCtx.effect(() => () => {
1317
1386
  ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
@@ -1321,32 +1390,56 @@ var ClaudeCodeLoop = class extends Service {
1321
1390
  ownerAbort.signal,
1322
1391
  this.ownership.signal
1323
1392
  ]);
1393
+ let handle;
1394
+ let stored;
1395
+ let preparation;
1324
1396
  try {
1325
- preparation = await raceAbortCall(
1326
- () => persistence.prepare(id, fused),
1327
- fused,
1397
+ try {
1398
+ handle = await raceAbortCall(
1399
+ () => persistence.open(id, "write", { signal: fused }),
1400
+ fused,
1401
+ id,
1402
+ (abandoned) => {
1403
+ void abandoned.close();
1404
+ }
1405
+ );
1406
+ const persisted = await handle.read(0, void 0, { signal: fused });
1407
+ fused.throwIfAborted();
1408
+ const closers = interruptedTurnClosers(persisted);
1409
+ if (closers.length > 0) await handle.append(closers);
1410
+ preparation = SessionPreparation.create(this.runtime.ctx.sessions.prepare(id, {
1411
+ seed: [...persisted, ...closers],
1412
+ meta: structuredClone(handle.header),
1413
+ inheritedEventCount: handle.inheritedEventCount,
1414
+ seedSource: "persistence"
1415
+ }));
1416
+ stored = { handle, storedCount: persisted.length + closers.length };
1417
+ await this.appendUnstoredSuffix(stored, preparation.session);
1418
+ } finally {
1419
+ await unfollowOwner();
1420
+ }
1421
+ ownerCtx.fiber.assertActive();
1422
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
1423
+ const owned = stored;
1424
+ handle = void 0;
1425
+ return await this.setupAndPublish(
1426
+ ownerCtx,
1328
1427
  id,
1329
- (abandoned) => {
1330
- abandoned[Symbol.dispose]();
1331
- }
1428
+ preparation,
1429
+ options.agentOptions ?? {},
1430
+ options.setup,
1431
+ options.signal,
1432
+ "resume",
1433
+ owned
1332
1434
  );
1333
1435
  } finally {
1334
- await unfollowOwner();
1436
+ preparation?.[Symbol.dispose]();
1437
+ await handle?.close().catch(() => {
1438
+ });
1335
1439
  }
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
- }
1440
+ })();
1441
+ this.ownership.trackWrapper(published);
1442
+ return published;
1350
1443
  }
1351
1444
  };
1352
1445
 
@@ -1354,7 +1447,7 @@ var ClaudeCodeLoop = class extends Service {
1354
1447
  import { Service as Service2 } from "@deepseek-ai/cordis";
1355
1448
  import z2 from "@deepseek-ai/schemastery";
1356
1449
  import { emitAgentEvent as emitAgentEvent2 } from "@deepseek-ai/dsh-agent";
1357
- import { SessionPreparation as SessionPreparation2 } from "@deepseek-ai/dsh-session";
1450
+ import { interruptedTurnClosers as interruptedTurnClosers2, SessionLogOffset as SessionLogOffset2, SessionPreparation as SessionPreparation2 } from "@deepseek-ai/dsh-session";
1358
1451
 
1359
1452
  // src/engine-codex/agent.ts
1360
1453
  import { Inbox as Inbox2, agentEvents as agentEvents2 } from "@deepseek-ai/dsh-agent";
@@ -1441,7 +1534,7 @@ var AppServerClient = class _AppServerClient {
1441
1534
  clientInfo: {
1442
1535
  name: "dsh-loop-engine",
1443
1536
  title: null,
1444
- version: "0.1.1-rc.2"
1537
+ version: "1.0.0-rc9"
1445
1538
  },
1446
1539
  capabilities: { experimentalApi: true, requestAttestation: false }
1447
1540
  };
@@ -1656,7 +1749,7 @@ function noopNotificationHandler() {
1656
1749
  }
1657
1750
 
1658
1751
  // src/engine-codex/appserver/mapping.ts
1659
- import { CallId as CallId2, createToolResultMessage as createToolResultMessage2 } from "@deepseek-ai/dsh-llm";
1752
+ import { ToolCallId as ToolCallId2, createToolResultMessage as createToolResultMessage2 } from "@deepseek-ai/dsh-llm";
1660
1753
  function mapUsage2(usage) {
1661
1754
  return {
1662
1755
  inputTokens: usage.inputTokens,
@@ -1668,12 +1761,12 @@ function mapUsage2(usage) {
1668
1761
  function mapCommandExecution(item) {
1669
1762
  return {
1670
1763
  call: {
1671
- callId: CallId2(item.id),
1764
+ callId: ToolCallId2(item.id),
1672
1765
  name: "command_execution",
1673
1766
  arguments: JSON.stringify({ command: item.command ?? "" })
1674
1767
  },
1675
1768
  result: createToolResultMessage2({
1676
- callId: CallId2(item.id),
1769
+ callId: ToolCallId2(item.id),
1677
1770
  content: [{ type: "text", text: item.aggregatedOutput ?? "" }],
1678
1771
  isError: (item.exitCode ?? 0) !== 0 || item.status === "failed"
1679
1772
  })
@@ -1682,12 +1775,12 @@ function mapCommandExecution(item) {
1682
1775
  function mapFileChange(item) {
1683
1776
  return {
1684
1777
  call: {
1685
- callId: CallId2(item.id),
1778
+ callId: ToolCallId2(item.id),
1686
1779
  name: "apply_patch",
1687
1780
  arguments: JSON.stringify(item.changes ?? [])
1688
1781
  },
1689
1782
  result: createToolResultMessage2({
1690
- callId: CallId2(item.id),
1783
+ callId: ToolCallId2(item.id),
1691
1784
  content: [{ type: "text", text: `patch ${item.status ?? "completed"}` }],
1692
1785
  isError: item.status === "failed"
1693
1786
  })
@@ -1698,12 +1791,12 @@ function mapMcpToolCall(item) {
1698
1791
  const isError = item.error !== void 0 && item.error !== null;
1699
1792
  return {
1700
1793
  call: {
1701
- callId: CallId2(item.id),
1794
+ callId: ToolCallId2(item.id),
1702
1795
  name: name2,
1703
1796
  arguments: JSON.stringify(item.arguments ?? {})
1704
1797
  },
1705
1798
  result: createToolResultMessage2({
1706
- callId: CallId2(item.id),
1799
+ callId: ToolCallId2(item.id),
1707
1800
  content: isError ? [{ type: "text", text: item.error?.message ?? "tool call failed" }] : [{ type: "text", text: JSON.stringify(item.result?.content ?? []) }],
1708
1801
  isError
1709
1802
  })
@@ -1732,7 +1825,7 @@ var CodexAgent = class {
1732
1825
  this.dispatch.emit("agent/inbox/claimed", { message, turn });
1733
1826
  }
1734
1827
  });
1735
- const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
1828
+ const lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
1736
1829
  this.phase = { kind: "idle", lastTurn };
1737
1830
  this.scope = createScope2(loopCtx, this);
1738
1831
  this.ctx = this.scope.ctx.extend({ agent: this });
@@ -1945,7 +2038,7 @@ var CodexAgent = class {
1945
2038
  * @returns the permission fields of the query spec.
1946
2039
  */
1947
2040
  queryPermission() {
1948
- const fold = resolveSessionPermission2(this.session.events);
2041
+ const fold = resolveSessionPermission2(this.session.snapshotEvents());
1949
2042
  return {
1950
2043
  sandboxMode: this.config.sandboxMode ?? fold.sandboxMode,
1951
2044
  approvalPolicy: this.config.approvalPolicy ?? fold.approvalPolicy
@@ -2298,7 +2391,7 @@ var CodexLoop = class extends Service2 {
2298
2391
  * fuses caller cancellation with lifecycle teardown for setup awaits.
2299
2392
  */
2300
2393
  /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
2301
- prepare(ownerCtx, id, options, session, callerSignal) {
2394
+ prepare(ownerCtx, id, options, session, callerSignal, handle) {
2302
2395
  ownerCtx.fiber.assertActive();
2303
2396
  if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2304
2397
  if (callerSignal?.aborted) {
@@ -2332,11 +2425,15 @@ var CodexLoop = class extends Service2 {
2332
2425
  }
2333
2426
  } finally {
2334
2427
  try {
2335
- detachAgent?.();
2336
- detachSession?.();
2428
+ await handle?.close();
2337
2429
  } finally {
2338
- untrack();
2339
- if (!ownerTriggered) await unfollowOwner();
2430
+ try {
2431
+ detachAgent?.();
2432
+ detachSession?.();
2433
+ } finally {
2434
+ untrack();
2435
+ if (!ownerTriggered) await unfollowOwner();
2436
+ }
2340
2437
  }
2341
2438
  }
2342
2439
  })();
@@ -2386,18 +2483,27 @@ var CodexLoop = class extends Service2 {
2386
2483
  }
2387
2484
  }
2388
2485
  /** Prepare one Agent around an acquired Session, run setup, and publish it. */
2389
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
2486
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored) {
2390
2487
  var _stack = [];
2391
2488
  try {
2392
2489
  const ownedPreparation = __using(_stack, preparation);
2393
2490
  const session = ownedPreparation.session;
2394
- const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
2491
+ let prepared;
2492
+ try {
2493
+ prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle);
2494
+ } catch (error) {
2495
+ await stored?.handle.close().catch(() => {
2496
+ });
2497
+ throw error;
2498
+ }
2395
2499
  try {
2396
2500
  const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
2397
2501
  setupCommit?.commit();
2502
+ await this.appendUnstoredSuffix(stored, session);
2398
2503
  return prepared.publish(source);
2399
2504
  } catch (error) {
2400
- await prepared.dispose();
2505
+ await prepared.dispose().catch(() => {
2506
+ });
2401
2507
  throw error;
2402
2508
  }
2403
2509
  } catch (_) {
@@ -2408,7 +2514,8 @@ var CodexLoop = class extends Service2 {
2408
2514
  }
2409
2515
  /**
2410
2516
  * Create an agent and session under one caller-supplied identity, owned by
2411
- * the accessing fiber.
2517
+ * the accessing fiber. When a persistence backend is mounted, the session's
2518
+ * durable identity is stored before publication.
2412
2519
  * @param ownerCtx - caller context that structurally owns the lifecycle.
2413
2520
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
2414
2521
  * @returns the published handle.
@@ -2416,20 +2523,73 @@ var CodexLoop = class extends Service2 {
2416
2523
  async createAgent(ownerCtx, options) {
2417
2524
  const preparation = SessionPreparation2.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
2418
2525
  ...options.seed === void 0 ? {} : { seed: options.seed },
2419
- ...options.meta === void 0 ? {} : { meta: options.meta }
2526
+ ...options.meta === void 0 ? {} : { meta: options.meta },
2527
+ ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
2420
2528
  }));
2421
- const published = this.setupAndPublish(
2422
- ownerCtx,
2423
- options.sessionId,
2424
- preparation,
2425
- options.agentOptions ?? {},
2426
- options.setup,
2427
- options.signal,
2428
- "startup"
2429
- );
2529
+ const published = (async () => {
2530
+ let stored;
2531
+ try {
2532
+ stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
2533
+ () => this.createStoredSession(preparation.session, options.signal),
2534
+ options.signal,
2535
+ options.sessionId,
2536
+ (abandoned) => {
2537
+ void abandoned?.handle.close().catch(() => {
2538
+ });
2539
+ }
2540
+ );
2541
+ } catch (error) {
2542
+ preparation[Symbol.dispose]();
2543
+ throw error;
2544
+ }
2545
+ return this.setupAndPublish(
2546
+ ownerCtx,
2547
+ options.sessionId,
2548
+ preparation,
2549
+ options.agentOptions ?? {},
2550
+ options.setup,
2551
+ options.signal,
2552
+ "startup",
2553
+ stored
2554
+ );
2555
+ })();
2430
2556
  this.ownership.trackWrapper(published);
2431
2557
  return published;
2432
2558
  }
2559
+ /**
2560
+ * Take a fresh session's write ownership when persistence is mounted.
2561
+ * Nothing is appended here: the constructor seed (which never re-emits
2562
+ * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
2563
+ * publication commit point, so a failed or cancelled setup closes an
2564
+ * unmaterialized handle and leaves no stored residue — the same id can be
2565
+ * created again.
2566
+ * @param session - the unpublished session to store.
2567
+ * @param signal - optional cancellation forwarded to the backend create.
2568
+ * @returns the owned handle and stored cursor, or `undefined` without a backend.
2569
+ */
2570
+ async createStoredSession(session, signal) {
2571
+ const persistence = this.runtime.ctx.get("sessionPersistence");
2572
+ if (persistence === void 0) return void 0;
2573
+ const handle = await persistence.create(session.header, {
2574
+ inheritedEventCount: session.inheritedEventCount,
2575
+ ...signal === void 0 ? {} : { signal }
2576
+ });
2577
+ return { handle, storedCount: 0 };
2578
+ }
2579
+ /**
2580
+ * Durably store the session events appended since the last stored cursor.
2581
+ * Pre-publication appends (constructor seed markers, setup-window events)
2582
+ * never re-emit through `session/event`, so publication must flush them
2583
+ * through the handle before live events start routing into it.
2584
+ * @param stored - the session's owned handle and stored cursor, if any.
2585
+ * @param session - the unpublished session whose suffix is stored.
2586
+ */
2587
+ async appendUnstoredSuffix(stored, session) {
2588
+ if (stored === void 0) return;
2589
+ const suffix = session.snapshotEvents(SessionLogOffset2(stored.storedCount));
2590
+ if (suffix.length > 0) await stored.handle.append(suffix);
2591
+ stored.storedCount += suffix.length;
2592
+ }
2433
2593
  /**
2434
2594
  * Resume an owned agent from the configured persistence service.
2435
2595
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
@@ -2443,11 +2603,10 @@ var CodexLoop = class extends Service2 {
2443
2603
  }
2444
2604
  return this.resumeWith(ownerCtx, persistence, options);
2445
2605
  }
2446
- /** Resume through an explicit persistence handle. */
2447
- async resumeWith(ownerCtx, persistence, options) {
2606
+ /** Resume through an explicit persistence service. */
2607
+ resumeWith(ownerCtx, persistence, options) {
2448
2608
  const id = options.resumeSessionId;
2449
- let preparation;
2450
- try {
2609
+ const published = (async () => {
2451
2610
  const ownerAbort = new AbortController();
2452
2611
  const unfollowOwner = ownerCtx.effect(() => () => {
2453
2612
  ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
@@ -2457,32 +2616,56 @@ var CodexLoop = class extends Service2 {
2457
2616
  ownerAbort.signal,
2458
2617
  this.ownership.signal
2459
2618
  ]);
2619
+ let handle;
2620
+ let stored;
2621
+ let preparation;
2460
2622
  try {
2461
- preparation = await raceAbortCall(
2462
- () => persistence.prepare(id, fused),
2463
- fused,
2623
+ try {
2624
+ handle = await raceAbortCall(
2625
+ () => persistence.open(id, "write", { signal: fused }),
2626
+ fused,
2627
+ id,
2628
+ (abandoned) => {
2629
+ void abandoned.close();
2630
+ }
2631
+ );
2632
+ const persisted = await handle.read(0, void 0, { signal: fused });
2633
+ fused.throwIfAborted();
2634
+ const closers = interruptedTurnClosers2(persisted);
2635
+ if (closers.length > 0) await handle.append(closers);
2636
+ preparation = SessionPreparation2.create(this.runtime.ctx.sessions.prepare(id, {
2637
+ seed: [...persisted, ...closers],
2638
+ meta: structuredClone(handle.header),
2639
+ inheritedEventCount: handle.inheritedEventCount,
2640
+ seedSource: "persistence"
2641
+ }));
2642
+ stored = { handle, storedCount: persisted.length + closers.length };
2643
+ await this.appendUnstoredSuffix(stored, preparation.session);
2644
+ } finally {
2645
+ await unfollowOwner();
2646
+ }
2647
+ ownerCtx.fiber.assertActive();
2648
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2649
+ const owned = stored;
2650
+ handle = void 0;
2651
+ return await this.setupAndPublish(
2652
+ ownerCtx,
2464
2653
  id,
2465
- (abandoned) => {
2466
- abandoned[Symbol.dispose]();
2467
- }
2654
+ preparation,
2655
+ options.agentOptions ?? {},
2656
+ options.setup,
2657
+ options.signal,
2658
+ "resume",
2659
+ owned
2468
2660
  );
2469
2661
  } finally {
2470
- await unfollowOwner();
2662
+ preparation?.[Symbol.dispose]();
2663
+ await handle?.close().catch(() => {
2664
+ });
2471
2665
  }
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
- }
2666
+ })();
2667
+ this.ownership.trackWrapper(published);
2668
+ return published;
2486
2669
  }
2487
2670
  };
2488
2671
 
@@ -2493,11 +2676,11 @@ import { fileURLToPath } from "node:url";
2493
2676
  import { Service as Service3 } from "@deepseek-ai/cordis";
2494
2677
  import z3 from "@deepseek-ai/schemastery";
2495
2678
  import { emitAgentEvent as emitAgentEvent3 } from "@deepseek-ai/dsh-agent";
2496
- import { SessionPreparation as SessionPreparation3 } from "@deepseek-ai/dsh-session";
2679
+ import { interruptedTurnClosers as interruptedTurnClosers3, SessionLogOffset as SessionLogOffset3, SessionPreparation as SessionPreparation3 } from "@deepseek-ai/dsh-session";
2497
2680
 
2498
2681
  // src/engine-pi/agent.ts
2499
2682
  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";
2683
+ import { ToolCallId as ToolCallId4, LlmError as LlmError3, createAssistantMessage as createAssistantMessage3, createUserMessage as createUserMessage3, errorChain as errorChain3 } from "@deepseek-ai/dsh-llm";
2501
2684
  import { createScope as createScope3 } from "@deepseek-ai/dsh-scope";
2502
2685
  import { canonicalHeader as canonicalHeader3 } from "@deepseek-ai/dsh-session";
2503
2686
 
@@ -2717,7 +2900,7 @@ var PiRpcClient = class _PiRpcClient {
2717
2900
 
2718
2901
  // src/engine-pi/rpc/mapping.ts
2719
2902
  import {
2720
- CallId as CallId3,
2903
+ ToolCallId as ToolCallId3,
2721
2904
  createToolResultMessage as createToolResultMessage3
2722
2905
  } from "@deepseek-ai/dsh-llm";
2723
2906
  function mapUsage3(usage) {
@@ -2748,7 +2931,7 @@ function resultText(content) {
2748
2931
  }
2749
2932
  function mapToolResult(ev) {
2750
2933
  return createToolResultMessage3({
2751
- callId: CallId3(ev.toolCallId),
2934
+ callId: ToolCallId3(ev.toolCallId),
2752
2935
  content: [{ type: "text", text: resultText(ev.result) || "(no content)" }],
2753
2936
  isError: ev.isError
2754
2937
  });
@@ -2783,7 +2966,7 @@ var PiAgent = class {
2783
2966
  this.dispatch.emit("agent/inbox/claimed", { message, turn });
2784
2967
  }
2785
2968
  });
2786
- const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
2969
+ const lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
2787
2970
  this.phase = { kind: "idle", lastTurn };
2788
2971
  this.scope = createScope3(loopCtx, this);
2789
2972
  this.ctx = this.scope.ctx.extend({ agent: this });
@@ -3004,7 +3187,7 @@ var PiAgent = class {
3004
3187
  * @returns the permission fields of the query spec.
3005
3188
  */
3006
3189
  queryPermission() {
3007
- const fold = resolveSessionPermission3(this.session.events);
3190
+ const fold = resolveSessionPermission3(this.session.snapshotEvents());
3008
3191
  const sandboxMode = this.config.sandboxMode ?? fold.sandboxMode;
3009
3192
  return {
3010
3193
  sandboxMode,
@@ -3102,14 +3285,32 @@ var PiAgent = class {
3102
3285
  });
3103
3286
  this.requestHeaderLogged = true;
3104
3287
  }
3288
+ /**
3289
+ * The harness Session's web-side model selection, if any was stored. The
3290
+ * durable `model/selection` event carries `{ provider, model, ... }`; when a
3291
+ * user picked a model via `/model`, this is the newest pick, and it overrides
3292
+ * the deployment config (which stays the fallback). Returns `undefined` when
3293
+ * no selection was stored, so the deployment config governs.
3294
+ */
3295
+ dynamicModel() {
3296
+ for (const event of [...this.session.snapshotEvents()].reverse()) {
3297
+ const type = event.type;
3298
+ if (type !== "model/selection") continue;
3299
+ const data = event.data;
3300
+ const model = data?.model;
3301
+ if (typeof model === "string" && model.length > 0) return model;
3302
+ }
3303
+ return void 0;
3304
+ }
3105
3305
  /** Build the `pi --mode rpc` argv/cwd/env for one step's child process. */
3106
3306
  spawnSpec(cwd) {
3107
3307
  const argv = [];
3308
+ const model = this.dynamicModel() ?? this.config.model;
3108
3309
  if (this.config.provider !== void 0) argv.push("--provider", this.config.provider);
3109
- if (this.config.model !== void 0 && this.config.thinkingLevel !== void 0) {
3110
- argv.push("--model", `${this.config.model}:${this.config.thinkingLevel}`);
3111
- } else if (this.config.model !== void 0) {
3112
- argv.push("--model", this.config.model);
3310
+ if (model !== void 0 && this.config.thinkingLevel !== void 0) {
3311
+ argv.push("--model", `${model}:${this.config.thinkingLevel}`);
3312
+ } else if (model !== void 0) {
3313
+ argv.push("--model", model);
3113
3314
  } else if (this.config.thinkingLevel !== void 0) {
3114
3315
  argv.push("--model", `:${this.config.thinkingLevel}`);
3115
3316
  }
@@ -3215,7 +3416,7 @@ var PiAgent = class {
3215
3416
  this.session.append("tool/call", {
3216
3417
  turn,
3217
3418
  step,
3218
- callId: CallId4(callId),
3419
+ callId: ToolCallId4(callId),
3219
3420
  name: name2,
3220
3421
  arguments: argumentsValue
3221
3422
  });
@@ -3364,6 +3565,67 @@ var PiAgent = class {
3364
3565
  }
3365
3566
  };
3366
3567
 
3568
+ // src/engine-pi/probe.ts
3569
+ function waitForExit(child) {
3570
+ return new Promise((resolve5) => {
3571
+ ;
3572
+ child.onExit(
3573
+ (code) => resolve5(code ?? 0)
3574
+ );
3575
+ });
3576
+ }
3577
+ function collectStdout(child) {
3578
+ return new Promise((resolve5) => {
3579
+ let text = "";
3580
+ if (child.stdout.on === void 0) {
3581
+ resolve5("");
3582
+ return;
3583
+ }
3584
+ const out = child.stdout;
3585
+ out.setEncoding("utf8");
3586
+ out.on("data", (data) => {
3587
+ text += String(data);
3588
+ });
3589
+ out.on("end", () => {
3590
+ resolve5(text);
3591
+ });
3592
+ out.on("close", () => {
3593
+ resolve5(text);
3594
+ });
3595
+ });
3596
+ }
3597
+ function parsePiModelList(output) {
3598
+ const entries = [];
3599
+ for (const line of output.split("\n")) {
3600
+ const trimmed = line.trim();
3601
+ if (trimmed.length === 0) continue;
3602
+ const match = /^(\S+)\s{2,}(\S+)/.exec(trimmed);
3603
+ if (match === null) continue;
3604
+ const provider = match[1];
3605
+ if (provider === "provider") continue;
3606
+ entries.push({ provider, model: match[2] });
3607
+ }
3608
+ return entries;
3609
+ }
3610
+ async function probePiModels(bin, spawn4) {
3611
+ const spec = {
3612
+ argv: [bin, "--mode", "rpc", "--list-models"],
3613
+ cwd: process.cwd(),
3614
+ env: {}
3615
+ };
3616
+ let child;
3617
+ try {
3618
+ child = spawn4(spec);
3619
+ } catch {
3620
+ return [];
3621
+ }
3622
+ const stdoutPromise = collectStdout(child);
3623
+ const exitCode = await waitForExit(child);
3624
+ if (exitCode !== 0) return [];
3625
+ const stdout = await stdoutPromise;
3626
+ return parsePiModelList(stdout);
3627
+ }
3628
+
3367
3629
  // src/engine-pi/loop.ts
3368
3630
  var PI_SANDBOX_MODES = [
3369
3631
  "read-only",
@@ -3376,7 +3638,8 @@ var Config3 = z3.object({
3376
3638
  provider: z3.string(),
3377
3639
  model: z3.string(),
3378
3640
  thinkingLevel: z3.string(),
3379
- env: z3.dict(z3.string()).default({})
3641
+ env: z3.dict(z3.string()).default({}),
3642
+ piCatalogHolder: z3.any()
3380
3643
  });
3381
3644
  function resolveConfig3(config) {
3382
3645
  return {
@@ -3415,7 +3678,8 @@ function fromSubprocess(handle) {
3415
3678
  stdout,
3416
3679
  stderr,
3417
3680
  onExit: (handler) => {
3418
- void handle.done.then(handler, handler);
3681
+ const onExit = handler;
3682
+ void handle.done.then((outcome) => onExit(outcome.exitCode), handler);
3419
3683
  },
3420
3684
  terminate: () => handle.terminate()
3421
3685
  };
@@ -3439,6 +3703,14 @@ var PiLoop = class extends Service3 {
3439
3703
  this.runtime = { ctx };
3440
3704
  this.bin = piCliEntrypoint();
3441
3705
  this.spawn = (spec) => fromSubprocess(this.runtime.ctx.subprocess.spawn(piSubprocessSpec(spec, PI_DISPOSE_GRACE_MS)));
3706
+ const holder = config.piCatalogHolder;
3707
+ if (holder !== void 0) {
3708
+ void probePiModels(this.bin, (spec) => this.spawn(spec)).then((models) => {
3709
+ holder.entries = [...models];
3710
+ }).catch(() => {
3711
+ holder.entries = [];
3712
+ });
3713
+ }
3442
3714
  ctx.effect(() => () => this.ownership.dispose(), "agentLoopPi.transactions()");
3443
3715
  ctx.effect(() => ctx.agents.setFactory(this), "agentLoopPi.setFactory()");
3444
3716
  ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
@@ -3452,7 +3724,7 @@ var PiLoop = class extends Service3 {
3452
3724
  * fuses caller cancellation with lifecycle teardown for setup awaits.
3453
3725
  */
3454
3726
  /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
3455
- prepare(ownerCtx, id, options, session, callerSignal) {
3727
+ prepare(ownerCtx, id, options, session, callerSignal, handle) {
3456
3728
  ownerCtx.fiber.assertActive();
3457
3729
  if (!this.ownership.isActive()) throw new Error("agent loop is not active");
3458
3730
  if (callerSignal?.aborted) {
@@ -3486,11 +3758,15 @@ var PiLoop = class extends Service3 {
3486
3758
  }
3487
3759
  } finally {
3488
3760
  try {
3489
- detachAgent?.();
3490
- detachSession?.();
3761
+ await handle?.close();
3491
3762
  } finally {
3492
- untrack();
3493
- if (!ownerTriggered) await unfollowOwner();
3763
+ try {
3764
+ detachAgent?.();
3765
+ detachSession?.();
3766
+ } finally {
3767
+ untrack();
3768
+ if (!ownerTriggered) await unfollowOwner();
3769
+ }
3494
3770
  }
3495
3771
  }
3496
3772
  })();
@@ -3540,18 +3816,27 @@ var PiLoop = class extends Service3 {
3540
3816
  }
3541
3817
  }
3542
3818
  /** Prepare one Agent around an acquired Session, run setup, and publish it. */
3543
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
3819
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored) {
3544
3820
  var _stack = [];
3545
3821
  try {
3546
3822
  const ownedPreparation = __using(_stack, preparation);
3547
3823
  const session = ownedPreparation.session;
3548
- const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
3824
+ let prepared;
3825
+ try {
3826
+ prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle);
3827
+ } catch (error) {
3828
+ await stored?.handle.close().catch(() => {
3829
+ });
3830
+ throw error;
3831
+ }
3549
3832
  try {
3550
3833
  const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
3551
3834
  setupCommit?.commit();
3835
+ await this.appendUnstoredSuffix(stored, session);
3552
3836
  return prepared.publish(source);
3553
3837
  } catch (error) {
3554
- await prepared.dispose();
3838
+ await prepared.dispose().catch(() => {
3839
+ });
3555
3840
  throw error;
3556
3841
  }
3557
3842
  } catch (_) {
@@ -3562,7 +3847,8 @@ var PiLoop = class extends Service3 {
3562
3847
  }
3563
3848
  /**
3564
3849
  * Create an agent and session under one caller-supplied identity, owned by
3565
- * the accessing fiber.
3850
+ * the accessing fiber. When a persistence backend is mounted, the session's
3851
+ * durable identity is stored before publication.
3566
3852
  * @param ownerCtx - caller context that structurally owns the lifecycle.
3567
3853
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
3568
3854
  * @returns the published handle.
@@ -3570,20 +3856,73 @@ var PiLoop = class extends Service3 {
3570
3856
  async createAgent(ownerCtx, options) {
3571
3857
  const preparation = SessionPreparation3.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
3572
3858
  ...options.seed === void 0 ? {} : { seed: options.seed },
3573
- ...options.meta === void 0 ? {} : { meta: options.meta }
3859
+ ...options.meta === void 0 ? {} : { meta: options.meta },
3860
+ ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
3574
3861
  }));
3575
- const published = this.setupAndPublish(
3576
- ownerCtx,
3577
- options.sessionId,
3578
- preparation,
3579
- options.agentOptions ?? {},
3580
- options.setup,
3581
- options.signal,
3582
- "startup"
3583
- );
3862
+ const published = (async () => {
3863
+ let stored;
3864
+ try {
3865
+ stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
3866
+ () => this.createStoredSession(preparation.session, options.signal),
3867
+ options.signal,
3868
+ options.sessionId,
3869
+ (abandoned) => {
3870
+ void abandoned?.handle.close().catch(() => {
3871
+ });
3872
+ }
3873
+ );
3874
+ } catch (error) {
3875
+ preparation[Symbol.dispose]();
3876
+ throw error;
3877
+ }
3878
+ return this.setupAndPublish(
3879
+ ownerCtx,
3880
+ options.sessionId,
3881
+ preparation,
3882
+ options.agentOptions ?? {},
3883
+ options.setup,
3884
+ options.signal,
3885
+ "startup",
3886
+ stored
3887
+ );
3888
+ })();
3584
3889
  this.ownership.trackWrapper(published);
3585
3890
  return published;
3586
3891
  }
3892
+ /**
3893
+ * Take a fresh session's write ownership when persistence is mounted.
3894
+ * Nothing is appended here: the constructor seed (which never re-emits
3895
+ * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
3896
+ * publication commit point, so a failed or cancelled setup closes an
3897
+ * unmaterialized handle and leaves no stored residue — the same id can be
3898
+ * created again.
3899
+ * @param session - the unpublished session to store.
3900
+ * @param signal - optional cancellation forwarded to the backend create.
3901
+ * @returns the owned handle and stored cursor, or `undefined` without a backend.
3902
+ */
3903
+ async createStoredSession(session, signal) {
3904
+ const persistence = this.runtime.ctx.get("sessionPersistence");
3905
+ if (persistence === void 0) return void 0;
3906
+ const handle = await persistence.create(session.header, {
3907
+ inheritedEventCount: session.inheritedEventCount,
3908
+ ...signal === void 0 ? {} : { signal }
3909
+ });
3910
+ return { handle, storedCount: 0 };
3911
+ }
3912
+ /**
3913
+ * Durably store the session events appended since the last stored cursor.
3914
+ * Pre-publication appends (constructor seed markers, setup-window events)
3915
+ * never re-emit through `session/event`, so publication must flush them
3916
+ * through the handle before live events start routing into it.
3917
+ * @param stored - the session's owned handle and stored cursor, if any.
3918
+ * @param session - the unpublished session whose suffix is stored.
3919
+ */
3920
+ async appendUnstoredSuffix(stored, session) {
3921
+ if (stored === void 0) return;
3922
+ const suffix = session.snapshotEvents(SessionLogOffset3(stored.storedCount));
3923
+ if (suffix.length > 0) await stored.handle.append(suffix);
3924
+ stored.storedCount += suffix.length;
3925
+ }
3587
3926
  /**
3588
3927
  * Resume an owned agent from the configured persistence service.
3589
3928
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
@@ -3597,11 +3936,10 @@ var PiLoop = class extends Service3 {
3597
3936
  }
3598
3937
  return this.resumeWith(ownerCtx, persistence, options);
3599
3938
  }
3600
- /** Resume through an explicit persistence handle. */
3601
- async resumeWith(ownerCtx, persistence, options) {
3939
+ /** Resume through an explicit persistence service. */
3940
+ resumeWith(ownerCtx, persistence, options) {
3602
3941
  const id = options.resumeSessionId;
3603
- let preparation;
3604
- try {
3942
+ const published = (async () => {
3605
3943
  const ownerAbort = new AbortController();
3606
3944
  const unfollowOwner = ownerCtx.effect(() => () => {
3607
3945
  ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
@@ -3611,32 +3949,56 @@ var PiLoop = class extends Service3 {
3611
3949
  ownerAbort.signal,
3612
3950
  this.ownership.signal
3613
3951
  ]);
3952
+ let handle;
3953
+ let stored;
3954
+ let preparation;
3614
3955
  try {
3615
- preparation = await raceAbortCall(
3616
- () => persistence.prepare(id, fused),
3617
- fused,
3956
+ try {
3957
+ handle = await raceAbortCall(
3958
+ () => persistence.open(id, "write", { signal: fused }),
3959
+ fused,
3960
+ id,
3961
+ (abandoned) => {
3962
+ void abandoned.close();
3963
+ }
3964
+ );
3965
+ const persisted = await handle.read(0, void 0, { signal: fused });
3966
+ fused.throwIfAborted();
3967
+ const closers = interruptedTurnClosers3(persisted);
3968
+ if (closers.length > 0) await handle.append(closers);
3969
+ preparation = SessionPreparation3.create(this.runtime.ctx.sessions.prepare(id, {
3970
+ seed: [...persisted, ...closers],
3971
+ meta: structuredClone(handle.header),
3972
+ inheritedEventCount: handle.inheritedEventCount,
3973
+ seedSource: "persistence"
3974
+ }));
3975
+ stored = { handle, storedCount: persisted.length + closers.length };
3976
+ await this.appendUnstoredSuffix(stored, preparation.session);
3977
+ } finally {
3978
+ await unfollowOwner();
3979
+ }
3980
+ ownerCtx.fiber.assertActive();
3981
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
3982
+ const owned = stored;
3983
+ handle = void 0;
3984
+ return await this.setupAndPublish(
3985
+ ownerCtx,
3618
3986
  id,
3619
- (abandoned) => {
3620
- abandoned[Symbol.dispose]();
3621
- }
3987
+ preparation,
3988
+ options.agentOptions ?? {},
3989
+ options.setup,
3990
+ options.signal,
3991
+ "resume",
3992
+ owned
3622
3993
  );
3623
3994
  } finally {
3624
- await unfollowOwner();
3995
+ preparation?.[Symbol.dispose]();
3996
+ await handle?.close().catch(() => {
3997
+ });
3625
3998
  }
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
- }
3999
+ })();
4000
+ this.ownership.trackWrapper(published);
4001
+ return published;
3640
4002
  }
3641
4003
  };
3642
4004
 
@@ -3644,11 +4006,11 @@ var PiLoop = class extends Service3 {
3644
4006
  import { Service as Service4 } from "@deepseek-ai/cordis";
3645
4007
  import z4 from "@deepseek-ai/schemastery";
3646
4008
  import { emitAgentEvent as emitAgentEvent4 } from "@deepseek-ai/dsh-agent";
3647
- import { SessionPreparation as SessionPreparation4 } from "@deepseek-ai/dsh-session";
4009
+ import { interruptedTurnClosers as interruptedTurnClosers4, SessionLogOffset as SessionLogOffset4, SessionPreparation as SessionPreparation4 } from "@deepseek-ai/dsh-session";
3648
4010
 
3649
4011
  // src/engine-kimi/agent.ts
3650
4012
  import { Inbox as Inbox4, agentEvents as agentEvents4 } from "@deepseek-ai/dsh-agent";
3651
- import { CallId as CallId6, LlmError as LlmError4, createAssistantMessage as createAssistantMessage4, createUserMessage as createUserMessage4, errorChain as errorChain4 } from "@deepseek-ai/dsh-llm";
4013
+ import { ToolCallId as ToolCallId6, LlmError as LlmError4, createAssistantMessage as createAssistantMessage4, createUserMessage as createUserMessage4, errorChain as errorChain4 } from "@deepseek-ai/dsh-llm";
3652
4014
  import { createScope as createScope4 } from "@deepseek-ai/dsh-scope";
3653
4015
  import { canonicalHeader as canonicalHeader4 } from "@deepseek-ai/dsh-session";
3654
4016
 
@@ -3903,7 +4265,7 @@ var AcpClient = class _AcpClient {
3903
4265
  };
3904
4266
 
3905
4267
  // src/engine-kimi/acp/mapping.ts
3906
- import { CallId as CallId5, createToolResultMessage as createToolResultMessage4 } from "@deepseek-ai/dsh-llm";
4268
+ import { ToolCallId as ToolCallId5, createToolResultMessage as createToolResultMessage4 } from "@deepseek-ai/dsh-llm";
3907
4269
  function isTextChunk(update) {
3908
4270
  return update.sessionUpdate === "agent_message_chunk";
3909
4271
  }
@@ -3940,7 +4302,7 @@ function toolContentText(update) {
3940
4302
  }
3941
4303
  function toolResult(callId, text, isError) {
3942
4304
  return createToolResultMessage4({
3943
- callId: CallId5(callId),
4305
+ callId: ToolCallId5(callId),
3944
4306
  content: [{ type: "text", text: text.length > 0 ? text : "(no content)" }],
3945
4307
  isError
3946
4308
  });
@@ -3975,7 +4337,7 @@ var KimiAgent = class {
3975
4337
  this.dispatch.emit("agent/inbox/claimed", { message, turn });
3976
4338
  }
3977
4339
  });
3978
- const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
4340
+ const lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
3979
4341
  this.phase = { kind: "idle", lastTurn };
3980
4342
  this.scope = createScope4(loopCtx, this);
3981
4343
  this.ctx = this.scope.ctx.extend({ agent: this });
@@ -4321,7 +4683,7 @@ var KimiAgent = class {
4321
4683
  signal.throwIfAborted();
4322
4684
  const client = await this.acpClient(cwd);
4323
4685
  signal.throwIfAborted();
4324
- client.onPermission(() => resolveToolApproval(this.session.events));
4686
+ client.onPermission(() => resolveToolApproval(this.session.snapshotEvents()));
4325
4687
  const acpSessionId = await client.newSession(cwd);
4326
4688
  signal.throwIfAborted();
4327
4689
  client.onUpdate((update) => this.applyUpdate(turn, step, update));
@@ -4385,7 +4747,7 @@ var KimiAgent = class {
4385
4747
  if (callId === "" || this.emittedToolCalls.has(callId)) return;
4386
4748
  this.emittedToolCalls.add(callId);
4387
4749
  const name2 = toolCallName(update);
4388
- this.session.append("tool/call", { turn, step, callId: CallId6(callId), name: name2, arguments: "{}" });
4750
+ this.session.append("tool/call", { turn, step, callId: ToolCallId6(callId), name: name2, arguments: "{}" });
4389
4751
  this.toolText.set(callId, "");
4390
4752
  return;
4391
4753
  }
@@ -4476,7 +4838,7 @@ var KimiLoop = class extends Service4 {
4476
4838
  * fuses caller cancellation with lifecycle teardown for setup awaits.
4477
4839
  */
4478
4840
  /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
4479
- prepare(ownerCtx, id, options, session, callerSignal) {
4841
+ prepare(ownerCtx, id, options, session, callerSignal, handle) {
4480
4842
  ownerCtx.fiber.assertActive();
4481
4843
  if (!this.ownership.isActive()) throw new Error("agent loop is not active");
4482
4844
  if (callerSignal?.aborted) {
@@ -4510,11 +4872,15 @@ var KimiLoop = class extends Service4 {
4510
4872
  }
4511
4873
  } finally {
4512
4874
  try {
4513
- detachAgent?.();
4514
- detachSession?.();
4875
+ await handle?.close();
4515
4876
  } finally {
4516
- untrack();
4517
- if (!ownerTriggered) await unfollowOwner();
4877
+ try {
4878
+ detachAgent?.();
4879
+ detachSession?.();
4880
+ } finally {
4881
+ untrack();
4882
+ if (!ownerTriggered) await unfollowOwner();
4883
+ }
4518
4884
  }
4519
4885
  }
4520
4886
  })();
@@ -4564,18 +4930,27 @@ var KimiLoop = class extends Service4 {
4564
4930
  }
4565
4931
  }
4566
4932
  /** Prepare one Agent around an acquired Session, run setup, and publish it. */
4567
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
4933
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored) {
4568
4934
  var _stack = [];
4569
4935
  try {
4570
4936
  const ownedPreparation = __using(_stack, preparation);
4571
4937
  const session = ownedPreparation.session;
4572
- const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
4938
+ let prepared;
4939
+ try {
4940
+ prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle);
4941
+ } catch (error) {
4942
+ await stored?.handle.close().catch(() => {
4943
+ });
4944
+ throw error;
4945
+ }
4573
4946
  try {
4574
4947
  const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
4575
4948
  setupCommit?.commit();
4949
+ await this.appendUnstoredSuffix(stored, session);
4576
4950
  return prepared.publish(source);
4577
4951
  } catch (error) {
4578
- await prepared.dispose();
4952
+ await prepared.dispose().catch(() => {
4953
+ });
4579
4954
  throw error;
4580
4955
  }
4581
4956
  } catch (_) {
@@ -4586,7 +4961,8 @@ var KimiLoop = class extends Service4 {
4586
4961
  }
4587
4962
  /**
4588
4963
  * Create an agent and session under one caller-supplied identity, owned by
4589
- * the accessing fiber.
4964
+ * the accessing fiber. When a persistence backend is mounted, the session's
4965
+ * durable identity is stored before publication.
4590
4966
  * @param ownerCtx - caller context that structurally owns the lifecycle.
4591
4967
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
4592
4968
  * @returns the published handle.
@@ -4594,20 +4970,73 @@ var KimiLoop = class extends Service4 {
4594
4970
  async createAgent(ownerCtx, options) {
4595
4971
  const preparation = SessionPreparation4.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
4596
4972
  ...options.seed === void 0 ? {} : { seed: options.seed },
4597
- ...options.meta === void 0 ? {} : { meta: options.meta }
4973
+ ...options.meta === void 0 ? {} : { meta: options.meta },
4974
+ ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
4598
4975
  }));
4599
- const published = this.setupAndPublish(
4600
- ownerCtx,
4601
- options.sessionId,
4602
- preparation,
4603
- options.agentOptions ?? {},
4604
- options.setup,
4605
- options.signal,
4606
- "startup"
4607
- );
4976
+ const published = (async () => {
4977
+ let stored;
4978
+ try {
4979
+ stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
4980
+ () => this.createStoredSession(preparation.session, options.signal),
4981
+ options.signal,
4982
+ options.sessionId,
4983
+ (abandoned) => {
4984
+ void abandoned?.handle.close().catch(() => {
4985
+ });
4986
+ }
4987
+ );
4988
+ } catch (error) {
4989
+ preparation[Symbol.dispose]();
4990
+ throw error;
4991
+ }
4992
+ return this.setupAndPublish(
4993
+ ownerCtx,
4994
+ options.sessionId,
4995
+ preparation,
4996
+ options.agentOptions ?? {},
4997
+ options.setup,
4998
+ options.signal,
4999
+ "startup",
5000
+ stored
5001
+ );
5002
+ })();
4608
5003
  this.ownership.trackWrapper(published);
4609
5004
  return published;
4610
5005
  }
5006
+ /**
5007
+ * Take a fresh session's write ownership when persistence is mounted.
5008
+ * Nothing is appended here: the constructor seed (which never re-emits
5009
+ * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
5010
+ * publication commit point, so a failed or cancelled setup closes an
5011
+ * unmaterialized handle and leaves no stored residue — the same id can be
5012
+ * created again.
5013
+ * @param session - the unpublished session to store.
5014
+ * @param signal - optional cancellation forwarded to the backend create.
5015
+ * @returns the owned handle and stored cursor, or `undefined` without a backend.
5016
+ */
5017
+ async createStoredSession(session, signal) {
5018
+ const persistence = this.runtime.ctx.get("sessionPersistence");
5019
+ if (persistence === void 0) return void 0;
5020
+ const handle = await persistence.create(session.header, {
5021
+ inheritedEventCount: session.inheritedEventCount,
5022
+ ...signal === void 0 ? {} : { signal }
5023
+ });
5024
+ return { handle, storedCount: 0 };
5025
+ }
5026
+ /**
5027
+ * Durably store the session events appended since the last stored cursor.
5028
+ * Pre-publication appends (constructor seed markers, setup-window events)
5029
+ * never re-emit through `session/event`, so publication must flush them
5030
+ * through the handle before live events start routing into it.
5031
+ * @param stored - the session's owned handle and stored cursor, if any.
5032
+ * @param session - the unpublished session whose suffix is stored.
5033
+ */
5034
+ async appendUnstoredSuffix(stored, session) {
5035
+ if (stored === void 0) return;
5036
+ const suffix = session.snapshotEvents(SessionLogOffset4(stored.storedCount));
5037
+ if (suffix.length > 0) await stored.handle.append(suffix);
5038
+ stored.storedCount += suffix.length;
5039
+ }
4611
5040
  /**
4612
5041
  * Resume an owned agent from the configured persistence service.
4613
5042
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
@@ -4621,11 +5050,10 @@ var KimiLoop = class extends Service4 {
4621
5050
  }
4622
5051
  return this.resumeWith(ownerCtx, persistence, options);
4623
5052
  }
4624
- /** Resume through an explicit persistence handle. */
4625
- async resumeWith(ownerCtx, persistence, options) {
5053
+ /** Resume through an explicit persistence service. */
5054
+ resumeWith(ownerCtx, persistence, options) {
4626
5055
  const id = options.resumeSessionId;
4627
- let preparation;
4628
- try {
5056
+ const published = (async () => {
4629
5057
  const ownerAbort = new AbortController();
4630
5058
  const unfollowOwner = ownerCtx.effect(() => () => {
4631
5059
  ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
@@ -4635,32 +5063,56 @@ var KimiLoop = class extends Service4 {
4635
5063
  ownerAbort.signal,
4636
5064
  this.ownership.signal
4637
5065
  ]);
5066
+ let handle;
5067
+ let stored;
5068
+ let preparation;
4638
5069
  try {
4639
- preparation = await raceAbortCall(
4640
- () => persistence.prepare(id, fused),
4641
- fused,
5070
+ try {
5071
+ handle = await raceAbortCall(
5072
+ () => persistence.open(id, "write", { signal: fused }),
5073
+ fused,
5074
+ id,
5075
+ (abandoned) => {
5076
+ void abandoned.close();
5077
+ }
5078
+ );
5079
+ const persisted = await handle.read(0, void 0, { signal: fused });
5080
+ fused.throwIfAborted();
5081
+ const closers = interruptedTurnClosers4(persisted);
5082
+ if (closers.length > 0) await handle.append(closers);
5083
+ preparation = SessionPreparation4.create(this.runtime.ctx.sessions.prepare(id, {
5084
+ seed: [...persisted, ...closers],
5085
+ meta: structuredClone(handle.header),
5086
+ inheritedEventCount: handle.inheritedEventCount,
5087
+ seedSource: "persistence"
5088
+ }));
5089
+ stored = { handle, storedCount: persisted.length + closers.length };
5090
+ await this.appendUnstoredSuffix(stored, preparation.session);
5091
+ } finally {
5092
+ await unfollowOwner();
5093
+ }
5094
+ ownerCtx.fiber.assertActive();
5095
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
5096
+ const owned = stored;
5097
+ handle = void 0;
5098
+ return await this.setupAndPublish(
5099
+ ownerCtx,
4642
5100
  id,
4643
- (abandoned) => {
4644
- abandoned[Symbol.dispose]();
4645
- }
5101
+ preparation,
5102
+ options.agentOptions ?? {},
5103
+ options.setup,
5104
+ options.signal,
5105
+ "resume",
5106
+ owned
4646
5107
  );
4647
5108
  } finally {
4648
- await unfollowOwner();
5109
+ preparation?.[Symbol.dispose]();
5110
+ await handle?.close().catch(() => {
5111
+ });
4649
5112
  }
4650
- ownerCtx.fiber.assertActive();
4651
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
4652
- return await this.setupAndPublish(
4653
- ownerCtx,
4654
- id,
4655
- preparation,
4656
- options.agentOptions ?? {},
4657
- options.setup,
4658
- options.signal,
4659
- "resume"
4660
- );
4661
- } finally {
4662
- preparation?.[Symbol.dispose]();
4663
- }
5113
+ })();
5114
+ this.ownership.trackWrapper(published);
5115
+ return published;
4664
5116
  }
4665
5117
  };
4666
5118
 
@@ -5102,7 +5554,6 @@ var KIMI_COMMANDS = [
5102
5554
  builtin("status", "Show the current session runtime state"),
5103
5555
  builtin("compact", "Compact the conversation context to free token usage"),
5104
5556
  builtin("clear", "Start a fresh session, discarding the current context"),
5105
- builtin("model", "Switch the LLM model used in the current session"),
5106
5557
  builtin("plan", "Toggle plan (read-only exploration) mode"),
5107
5558
  builtin("auto", "Toggle auto permission mode"),
5108
5559
  builtin("usage", "Show token usage, context, and quota information"),
@@ -5112,7 +5563,6 @@ var KIMI_COMMANDS = [
5112
5563
 
5113
5564
  // src/settings.ts
5114
5565
  import z5 from "@deepseek-ai/schemastery";
5115
- import { settingsNamespace } from "@deepseek-ai/dsh-settings";
5116
5566
 
5117
5567
  // src/namespace.ts
5118
5568
  var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
@@ -5124,7 +5574,7 @@ var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
5124
5574
  showInComposer: z5.boolean().default(true)
5125
5575
  });
5126
5576
  function loopEngineSettingsNamespace() {
5127
- return settingsNamespace(LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL);
5577
+ return LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL;
5128
5578
  }
5129
5579
 
5130
5580
  // src/patch-manager.ts
@@ -5138,6 +5588,8 @@ function renderManagedBlock(engine) {
5138
5588
  `${MANAGED_BLOCK_BEGIN}${engine} --`,
5139
5589
  "- id: agent-loop",
5140
5590
  " disabled: true",
5591
+ "- id: command-goal",
5592
+ " disabled: true",
5141
5593
  END_MARKER_LINE
5142
5594
  ].join("\n");
5143
5595
  }
@@ -5199,10 +5651,124 @@ ${block}`;
5199
5651
  return hasRootEntry(result) ? result : seedEmptyArray(result);
5200
5652
  }
5201
5653
 
5654
+ // src/preset.ts
5655
+ import { mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
5656
+ import { randomUUID } from "node:crypto";
5657
+ import { dirname as dirname4, join as join7 } from "node:path";
5658
+ var HOSTED_PRESET_ID = "loop-engine";
5659
+ var USER_PRESET_DIR = ".agent-presets";
5660
+ var COMPOSITION_FILE = "agent.cordis.yml";
5661
+ var METADATA_FILE = "preset.yml";
5662
+ var SOURCE_PRESET_ID = "standard";
5663
+ var STRIPPED_ROWS = ["skill-filesystem", "tool-skill", "tool-goal", "planning", "compaction"];
5664
+ var MANAGED_HEADER = `# Managed by dsh-loop-engine: the deployment's "${SOURCE_PRESET_ID}" preset minus
5665
+ # the dsh-native command/skill rows a hosted loop engine replaces. Regenerated
5666
+ # from "${SOURCE_PRESET_ID}" on boot \u2014 hand edits are overwritten.
5667
+ `;
5668
+ var MANAGED_METADATA = "name: Hosted Engine\ndescription: Standard preset minus the dsh-native commands and skills a hosted loop engine replaces.\n";
5669
+ function isEntryStart(line) {
5670
+ return line.startsWith("- ");
5671
+ }
5672
+ function entryId(line) {
5673
+ return /^- id:\s*(\S+)\s*$/.exec(line)?.[1];
5674
+ }
5675
+ function stripPresetRows(text, ids = STRIPPED_ROWS) {
5676
+ const lines = text.split("\n");
5677
+ const starts = [];
5678
+ for (const [index, line] of lines.entries()) {
5679
+ if (isEntryStart(line)) starts.push(index);
5680
+ }
5681
+ if (starts.length === 0) return text;
5682
+ const drop = new Set(ids);
5683
+ const entries = [];
5684
+ let heading = lines.slice(0, starts[0]);
5685
+ for (const [index, start] of starts.entries()) {
5686
+ const end = index + 1 < starts.length ? starts[index + 1] : lines.length;
5687
+ const span = lines.slice(start, end);
5688
+ let bodyEnd = span.length;
5689
+ while (bodyEnd > 1) {
5690
+ const line = span[bodyEnd - 1];
5691
+ if (line.trim() !== "" && !line.trimStart().startsWith("#")) break;
5692
+ bodyEnd -= 1;
5693
+ }
5694
+ entries.push({ id: entryId(span[0]), heading, body: span.slice(0, bodyEnd) });
5695
+ heading = span.slice(bodyEnd);
5696
+ }
5697
+ const out = [];
5698
+ out.push(...entries[0].heading);
5699
+ let lastKept = -1;
5700
+ for (const [index, entry] of entries.entries()) {
5701
+ if (entry.id !== void 0 && drop.has(entry.id)) continue;
5702
+ if (index > 0) out.push(...entry.heading);
5703
+ out.push(...entry.body);
5704
+ lastKept = index;
5705
+ }
5706
+ if (lastKept === entries.length - 1) out.push(...heading);
5707
+ if (out.length > 0 && out[out.length - 1] !== "") out.push("");
5708
+ return out.join("\n");
5709
+ }
5710
+ async function writeIfDifferent(path, text) {
5711
+ try {
5712
+ if (await readFile4(path, "utf8") === text) return false;
5713
+ } catch {
5714
+ }
5715
+ await mkdir(dirname4(path), { recursive: true });
5716
+ const tmp = `${path}.tmp-${randomUUID()}`;
5717
+ await writeFile(tmp, text, "utf8");
5718
+ await rename(tmp, path);
5719
+ return true;
5720
+ }
5721
+ async function ensureHostedPreset(dshHome, source) {
5722
+ const composition = await source.read(SOURCE_PRESET_ID);
5723
+ const stripped = `${MANAGED_HEADER}
5724
+ ${stripPresetRows(composition)}`;
5725
+ const dir = join7(dshHome, USER_PRESET_DIR, HOSTED_PRESET_ID);
5726
+ const compositionChanged = await writeIfDifferent(join7(dir, COMPOSITION_FILE), stripped);
5727
+ const metadataChanged = await writeIfDifferent(join7(dir, METADATA_FILE), MANAGED_METADATA);
5728
+ return compositionChanged || metadataChanged;
5729
+ }
5730
+
5731
+ // src/provider-route.ts
5732
+ import { LlmAdapter, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
5733
+ var HOSTED_PROVIDER_ROUTES = {
5734
+ "claude-code": PROVIDER,
5735
+ codex: PROVIDER2,
5736
+ pi: PROVIDER3,
5737
+ kimi: PROVIDER4
5738
+ };
5739
+ var HostedEngineRouteAdapter = class extends LlmAdapter {
5740
+ /**
5741
+ * @param label - the provider route label this placeholder serves.
5742
+ * @param options - optional catalog source; omit for an empty catalog.
5743
+ */
5744
+ constructor(label, options = {}) {
5745
+ super();
5746
+ this.label = label;
5747
+ this.options = options;
5748
+ }
5749
+ label;
5750
+ options;
5751
+ /** Advertise the injected Pi models (if any) under this route's provider label. */
5752
+ async listModels(_provider) {
5753
+ const catalog = this.options.listModels?.() ?? [];
5754
+ return catalog.map((entry) => ({
5755
+ provider: this.label,
5756
+ id: `${entry.provider}/${entry.model}`,
5757
+ name: `${entry.provider}/${entry.model}`
5758
+ }));
5759
+ }
5760
+ stream(_options) {
5761
+ throw new LlmError5(
5762
+ `provider "${this.label}" is a hosted loop engine route, not a model endpoint`,
5763
+ "HOSTED_ENGINE_ROUTE"
5764
+ );
5765
+ }
5766
+ };
5767
+
5202
5768
  // src/commands.ts
5203
5769
  import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
5204
5770
  import { homedir as homedir4 } from "node:os";
5205
- import { join as join7 } from "node:path";
5771
+ import { join as join8 } from "node:path";
5206
5772
  import { createUserMessage as createUserMessage6 } from "@deepseek-ai/dsh-llm";
5207
5773
  var COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
5208
5774
  function forwardClaudeCodeCommand(name2) {
@@ -5239,7 +5805,7 @@ function discoverUserSlashCommands() {
5239
5805
  if (!entry.endsWith(".md")) continue;
5240
5806
  const name2 = entry.slice(0, -".md".length);
5241
5807
  if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
5242
- const path = join7(userCommandsDir(), entry);
5808
+ const path = join8(userCommandsDir(), entry);
5243
5809
  let raw;
5244
5810
  try {
5245
5811
  raw = readFileSync2(path, "utf8");
@@ -5254,7 +5820,7 @@ function discoverUserSlashCommands() {
5254
5820
  return definitions;
5255
5821
  }
5256
5822
  function userCommandsDir() {
5257
- return join7(homedir4(), ".claude", "commands");
5823
+ return join8(homedir4(), ".claude", "commands");
5258
5824
  }
5259
5825
  function commandDescription(raw) {
5260
5826
  const trimmed = raw.trim();
@@ -5281,7 +5847,7 @@ function commandDescription(raw) {
5281
5847
 
5282
5848
  // src/engine-codex/skills.ts
5283
5849
  import { homedir as homedir5 } from "node:os";
5284
- import { join as join8 } from "node:path";
5850
+ import { join as join9 } from "node:path";
5285
5851
  var PROVIDER_NAME3 = "codex";
5286
5852
  var CODEX_PROJECT_RANK = 140;
5287
5853
  var CODEX_USER_RANK = 160;
@@ -5299,7 +5865,7 @@ var CodexSkillProvider = class {
5299
5865
  const paths = await collectProjectContextFiles(cwd, CODEX_CONTEXT_POLICY);
5300
5866
  if (await anySourceNonEmpty(paths)) candidates.push(this.agentsCandidate(paths, CODEX_PROJECT_RANK));
5301
5867
  }
5302
- const userPath = join8(homedir5(), ".codex", "AGENTS.md");
5868
+ const userPath = join9(homedir5(), ".codex", "AGENTS.md");
5303
5869
  if (await fileNonEmpty(userPath)) candidates.push(this.agentsCandidate([userPath], CODEX_USER_RANK));
5304
5870
  if (this.control.signal.aborted) return [];
5305
5871
  return candidates;
@@ -5338,9 +5904,9 @@ var CodexSkillProvider = class {
5338
5904
  };
5339
5905
 
5340
5906
  // src/engine-pi/skills.ts
5341
- import { readdir as readdir3, readFile as readFile4, stat as stat4 } from "node:fs/promises";
5907
+ import { readdir as readdir3, readFile as readFile5, stat as stat4 } from "node:fs/promises";
5342
5908
  import { homedir as homedir6 } from "node:os";
5343
- import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
5909
+ import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
5344
5910
  var PROVIDER_NAME4 = "pi";
5345
5911
  var PI_AGENTS_PROJECT_RANK = 140;
5346
5912
  var PI_SKILL_PROJECT_RANK = 150;
@@ -5353,7 +5919,7 @@ var PI_CONTEXT_POLICY = {
5353
5919
  function piAgentDir() {
5354
5920
  const override = process.env.PI_CODING_AGENT_DIR;
5355
5921
  if (override !== void 0 && override.length > 0) return resolve4(override);
5356
- return join9(homedir6(), ".pi", "agent");
5922
+ return join10(homedir6(), ".pi", "agent");
5357
5923
  }
5358
5924
  var PiSkillProvider = class {
5359
5925
  constructor(control) {
@@ -5369,13 +5935,13 @@ var PiSkillProvider = class {
5369
5935
  const contextPaths = await collectProjectContextFiles(cwd, PI_CONTEXT_POLICY);
5370
5936
  if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, PI_AGENTS_PROJECT_RANK));
5371
5937
  for (const dir of projectDirs) {
5372
- await this.collectSkillsDir(join9(dir, ".pi", "skills"), PI_SKILL_PROJECT_RANK, candidates);
5938
+ await this.collectSkillsDir(join10(dir, ".pi", "skills"), PI_SKILL_PROJECT_RANK, candidates);
5373
5939
  }
5374
5940
  }
5375
5941
  const userAgentDir = piAgentDir();
5376
- const userContext = join9(userAgentDir, "AGENTS.md");
5942
+ const userContext = join10(userAgentDir, "AGENTS.md");
5377
5943
  if (await fileNonEmpty(userContext)) candidates.push(this.agentsCandidate([userContext], PI_AGENTS_USER_RANK));
5378
- await this.collectSkillsDir(join9(userAgentDir, "skills"), PI_SKILL_USER_RANK, candidates);
5944
+ await this.collectSkillsDir(join10(userAgentDir, "skills"), PI_SKILL_USER_RANK, candidates);
5379
5945
  if (this.control.signal.aborted) return [];
5380
5946
  return candidates;
5381
5947
  }
@@ -5393,7 +5959,7 @@ var PiSkillProvider = class {
5393
5959
  provider: this.name,
5394
5960
  content: parsed.content,
5395
5961
  path: locator.path,
5396
- resourceBase: { kind: "directory", path: dirname4(locator.path) }
5962
+ resourceBase: { kind: "directory", path: dirname5(locator.path) }
5397
5963
  };
5398
5964
  }
5399
5965
  const content = await readSources(locator.paths);
@@ -5434,11 +6000,11 @@ var PiSkillProvider = class {
5434
6000
  return;
5435
6001
  }
5436
6002
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
5437
- const entryPath = join9(skillsDir, entry.name);
6003
+ const entryPath = join10(skillsDir, entry.name);
5438
6004
  const info = await stat4(entryPath).catch(() => void 0);
5439
6005
  if (info === void 0) continue;
5440
6006
  if (info.isDirectory()) {
5441
- const path = join9(entryPath, "SKILL.md");
6007
+ const path = join10(entryPath, "SKILL.md");
5442
6008
  const parsed2 = await this.tryParse(path);
5443
6009
  if (parsed2 === void 0) continue;
5444
6010
  candidates.push(this.skillCandidate(parsed2, path, rank, entryPath));
@@ -5468,7 +6034,7 @@ var PiSkillProvider = class {
5468
6034
  /** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
5469
6035
  async tryParse(path) {
5470
6036
  try {
5471
- const raw = await readFile4(path, { encoding: "utf8" });
6037
+ const raw = await readFile5(path, { encoding: "utf8" });
5472
6038
  return parseSkillFile(raw);
5473
6039
  } catch {
5474
6040
  return void 0;
@@ -5498,7 +6064,7 @@ var Config5 = z6.object({
5498
6064
  });
5499
6065
  function resolvePatchPath(config) {
5500
6066
  if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
5501
- return join10(
6067
+ return join11(
5502
6068
  resolveDshHome(),
5503
6069
  "profiles",
5504
6070
  config.profile ?? "web",
@@ -5510,21 +6076,21 @@ function isMissing(error) {
5510
6076
  }
5511
6077
  async function readPatchOrUndefined(path) {
5512
6078
  try {
5513
- return await readFile5(path, "utf8");
6079
+ return await readFile6(path, "utf8");
5514
6080
  } catch (error) {
5515
6081
  if (isMissing(error)) return void 0;
5516
6082
  throw error;
5517
6083
  }
5518
6084
  }
5519
6085
  async function writePatchFile(path, text) {
5520
- await mkdir(dirname5(path), { recursive: true });
5521
- const tmp = `${path}.tmp-${randomUUID()}`;
5522
- await writeFile(tmp, text, "utf8");
5523
- await rename(tmp, path);
6086
+ await mkdir2(dirname6(path), { recursive: true });
6087
+ const tmp = `${path}.tmp-${randomUUID2()}`;
6088
+ await writeFile2(tmp, text, "utf8");
6089
+ await rename2(tmp, path);
5524
6090
  }
5525
6091
  function writePatchFileSync(path, text) {
5526
- mkdirSync(dirname5(path), { recursive: true });
5527
- const tmp = `${path}.tmp-${randomUUID()}`;
6092
+ mkdirSync(dirname6(path), { recursive: true });
6093
+ const tmp = `${path}.tmp-${randomUUID2()}`;
5528
6094
  writeFileSync(tmp, text, "utf8");
5529
6095
  renameSync(tmp, path);
5530
6096
  }
@@ -5579,10 +6145,14 @@ function kimiConfig(config) {
5579
6145
  function apply(ctx, config) {
5580
6146
  const patchPath = resolvePatchPath(config);
5581
6147
  let fileEngine = currentEngineOf(readPatchFileSync(patchPath));
6148
+ const AGENT_PRESETS_NS = "agent-presets";
6149
+ const PRESET_DEFAULT_ATTEMPTS = 30;
6150
+ const PRESET_DEFAULT_RETRY_MS = 100;
5582
6151
  let engineFiber;
5583
6152
  let mountedEngine;
5584
6153
  let commandDisposers;
5585
6154
  let skillDisposer;
6155
+ const piCatalogHolder = { entries: [] };
5586
6156
  let mountAttempts = 0;
5587
6157
  let mountRetry;
5588
6158
  const CLEAR_RETRY = () => {
@@ -5591,6 +6161,106 @@ function apply(ctx, config) {
5591
6161
  mountRetry = void 0;
5592
6162
  }
5593
6163
  };
6164
+ let savedPresetDefault;
6165
+ let presetRetry;
6166
+ const CLEAR_PRESET_RETRY = () => {
6167
+ if (presetRetry !== void 0) {
6168
+ clearTimeout(presetRetry);
6169
+ presetRetry = void 0;
6170
+ }
6171
+ };
6172
+ const ROUTE_ATTEMPTS = 30;
6173
+ const ROUTE_RETRY_MS = 100;
6174
+ let routeHandle;
6175
+ let routeEngine;
6176
+ let routeRetry;
6177
+ const CLEAR_ROUTE_RETRY = () => {
6178
+ if (routeRetry !== void 0) {
6179
+ clearTimeout(routeRetry);
6180
+ routeRetry = void 0;
6181
+ }
6182
+ };
6183
+ const releaseRoute = () => {
6184
+ CLEAR_ROUTE_RETRY();
6185
+ const handle = routeHandle;
6186
+ routeHandle = void 0;
6187
+ routeEngine = void 0;
6188
+ handle?.();
6189
+ };
6190
+ const mountProviderRoute = (engine, attempt = 0) => {
6191
+ if (engine === "in-process") return;
6192
+ if (routeEngine === engine && routeHandle !== void 0) return;
6193
+ CLEAR_ROUTE_RETRY();
6194
+ const label = HOSTED_PROVIDER_ROUTES[engine];
6195
+ const llm = ctx.get("llm");
6196
+ if (llm === void 0) {
6197
+ if (attempt < ROUTE_ATTEMPTS) {
6198
+ routeRetry = setTimeout(() => {
6199
+ mountProviderRoute(engine, attempt + 1);
6200
+ }, ROUTE_RETRY_MS);
6201
+ }
6202
+ return;
6203
+ }
6204
+ try {
6205
+ const options = engine === "pi" ? { listModels: () => piCatalogHolder.entries } : void 0;
6206
+ routeHandle = llm.registerAdapter([label], new HostedEngineRouteAdapter(label, options));
6207
+ routeEngine = engine;
6208
+ } catch (error) {
6209
+ if (error instanceof Error && error.message.includes("already registered")) {
6210
+ ctx.logger.warn(`loop-engine: provider route "${label}" is already served by another adapter`);
6211
+ return;
6212
+ }
6213
+ ctx.logger.error(`loop-engine: provider route "${label}" registration failed: ${String(error)}`);
6214
+ }
6215
+ };
6216
+ const mutatePresetDefault = (op, attempt = 0) => {
6217
+ const settings = ctx.get("settings");
6218
+ if (settings === void 0) return;
6219
+ settings.mutate(AGENT_PRESETS_NS, [op]).then(() => void 0, (error) => {
6220
+ if (error instanceof Error && error.message.includes("not registered") && attempt < PRESET_DEFAULT_ATTEMPTS) {
6221
+ presetRetry = setTimeout(() => {
6222
+ mutatePresetDefault(op, attempt + 1);
6223
+ }, PRESET_DEFAULT_RETRY_MS);
6224
+ return;
6225
+ }
6226
+ ctx.logger.error(`loop-engine: preset default switch failed: ${String(error)}`);
6227
+ });
6228
+ };
6229
+ const restorePresetDefault = (attempt = 0) => {
6230
+ const presets = ctx.get("agentPresets");
6231
+ if (presets === void 0) return;
6232
+ if (presets.defaultId === HOSTED_PRESET_ID) {
6233
+ const saved = savedPresetDefault;
6234
+ savedPresetDefault = void 0;
6235
+ mutatePresetDefault(saved === void 0 ? { op: "unset", path: ["default"] } : { op: "set", path: ["default"], value: saved });
6236
+ return;
6237
+ }
6238
+ if (attempt < PRESET_DEFAULT_ATTEMPTS) {
6239
+ presetRetry = setTimeout(() => {
6240
+ restorePresetDefault(attempt + 1);
6241
+ }, PRESET_DEFAULT_RETRY_MS);
6242
+ }
6243
+ };
6244
+ const steerPresetDefault = (engine) => {
6245
+ const presets = ctx.get("agentPresets");
6246
+ if (presets === void 0) return;
6247
+ if (engine === "in-process") {
6248
+ restorePresetDefault();
6249
+ return;
6250
+ }
6251
+ void (async () => {
6252
+ try {
6253
+ await ensureHostedPreset(resolveDshHome(), presets);
6254
+ } catch (error) {
6255
+ ctx.logger.error(`loop-engine: hosted preset authoring failed: ${String(error)}`);
6256
+ return;
6257
+ }
6258
+ const current = presets.defaultId;
6259
+ if (current === HOSTED_PRESET_ID) return;
6260
+ savedPresetDefault = current;
6261
+ mutatePresetDefault({ op: "set", path: ["default"], value: HOSTED_PRESET_ID });
6262
+ })();
6263
+ };
5594
6264
  const cleanupEngineRegistrations = () => {
5595
6265
  if (commandDisposers !== void 0) {
5596
6266
  for (const dispose of commandDisposers) dispose();
@@ -5652,7 +6322,10 @@ function apply(ctx, config) {
5652
6322
  if (skills !== void 0) {
5653
6323
  skillDisposer = skills.registerProvider((control) => new PiSkillProvider(control));
5654
6324
  }
5655
- hostFactory("pi", () => ctx.plugin(PiLoop, piConfig(config)));
6325
+ hostFactory("pi", () => ctx.plugin(PiLoop, {
6326
+ ...piConfig(config),
6327
+ piCatalogHolder
6328
+ }));
5656
6329
  };
5657
6330
  const mountKimi = () => {
5658
6331
  const commands = ctx.get("commands");
@@ -5674,6 +6347,7 @@ function apply(ctx, config) {
5674
6347
  hostFactory("kimi", () => ctx.plugin(KimiLoop, kimiConfig(config)));
5675
6348
  };
5676
6349
  const mountEngine = (engine) => {
6350
+ mountProviderRoute(engine);
5677
6351
  if (engine === "claude-code") mountClaude();
5678
6352
  else if (engine === "codex") mountCodex();
5679
6353
  else if (engine === "pi") mountPi();
@@ -5683,6 +6357,7 @@ function apply(ctx, config) {
5683
6357
  const fiber = engineFiber;
5684
6358
  mountAttempts = 0;
5685
6359
  CLEAR_RETRY();
6360
+ releaseRoute();
5686
6361
  cleanupEngineRegistrations();
5687
6362
  mountedEngine = void 0;
5688
6363
  if (fiber === void 0) return;
@@ -5692,27 +6367,36 @@ function apply(ctx, config) {
5692
6367
  }, () => void 0);
5693
6368
  };
5694
6369
  mountEngine(fileEngine);
5695
- ctx.effect(() => () => CLEAR_RETRY(), "loop-engine: mount retry cleanup");
6370
+ steerPresetDefault(fileEngine);
6371
+ ctx.effect(() => () => {
6372
+ CLEAR_RETRY();
6373
+ CLEAR_PRESET_RETRY();
6374
+ releaseRoute();
6375
+ }, "loop-engine: retry cleanup");
5696
6376
  let source;
5697
- installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine, showInComposer: true }, {
5698
- setSource: (current) => {
5699
- source = current;
5700
- },
5701
- onChange: () => {
5702
- const next = source().engine;
5703
- if (next === fileEngine) return;
5704
- if (mountedEngine !== next) {
5705
- unmountEngine();
5706
- mountEngine(next);
5707
- }
5708
- try {
5709
- const updated = applyManagedBlock(readPatchFileSync(patchPath), next);
5710
- writePatchFileSync(patchPath, updated);
5711
- fileEngine = next;
5712
- } catch (error) {
5713
- ctx.logger.error(`loop-engine: managed block write failed: ${String(error)}`);
6377
+ ctx.inject(["settings"], (settingsCtx) => {
6378
+ settingsCtx.settings.installSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine, showInComposer: true }, {
6379
+ setSource: (current) => {
6380
+ source = current;
6381
+ },
6382
+ onChange: () => {
6383
+ const next = source().engine;
6384
+ if (next === fileEngine) return;
6385
+ if (mountedEngine !== next) {
6386
+ unmountEngine();
6387
+ mountEngine(next);
6388
+ }
6389
+ try {
6390
+ const updated = applyManagedBlock(readPatchFileSync(patchPath), next);
6391
+ writePatchFileSync(patchPath, updated);
6392
+ fileEngine = next;
6393
+ } catch (error) {
6394
+ ctx.logger.error(`loop-engine: managed block write failed: ${String(error)}`);
6395
+ return;
6396
+ }
6397
+ steerPresetDefault(next);
5714
6398
  }
5715
- }
6399
+ });
5716
6400
  });
5717
6401
  }
5718
6402
  export {