@dudousxd/nestjs-agent-opencode 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { d as OpenCodeClient, e as OpenCodeEvent, f as OpenCodeForm, g as OpenCodeFormValue, h as OpenCodeFormField, i as OpenCodeMcpEndpoint, O as OpenCodeTurns, a as OpenCodeEngineSettings } from './engine-Cp5uysaN.cjs';
2
- export { I as InMemoryOpenCodeSessionStore, M as Milestone, j as OpenCodeAmendment, k as OpenCodeCallContext, c as OpenCodeEngineOptions, b as OpenCodeHost, l as OpenCodeKeyValue, m as OpenCodeModelRef, n as OpenCodePermissionRequest, o as OpenCodePermissionRule, p as OpenCodeReplyMismatchError, q as OpenCodeRunResult, r as OpenCodeServer, s as OpenCodeSessionCreate, t as OpenCodeSessionRef, u as OpenCodeSessionStore, v as OpenCodeToolCall, w as OpenCodeToolRefusedError, x as OpenCodeToolsClaims, y as OpenCodeToolsOptions, z as OpenCodeToolsTokens, A as OpenCodeTurn, B as OpenCodeTurnContext, C as OpenCodeUsage, P as PendingAsk, S as SessionHandle, D as StepCost, E as StepModel, T as TurnOutcome, F as addUsage, G as emptyUsage, H as keyValueOpenCodeSessionStore, J as openCode, K as openCodeControllers, L as openCodeProviders, N as usageOf } from './engine-Cp5uysaN.cjs';
1
+ import { d as OpenCodeClient, e as OpenCodeEvent, f as OpenCodeForm, g as OpenCodeFormValue, h as OpenCodeFormField, i as OpenCodeMcpEndpoint, O as OpenCodeTurns, a as OpenCodeEngineSettings } from './engine-BX6MLzxs.cjs';
2
+ export { I as InMemoryOpenCodeSessionStore, L as LOST_ANSWER_ERROR, M as Milestone, j as OpenCodeAmendment, k as OpenCodeCallContext, c as OpenCodeEngineOptions, b as OpenCodeHost, l as OpenCodeKeyValue, m as OpenCodeMessage, n as OpenCodeMessagePart, o as OpenCodeModelRef, p as OpenCodePermissionRequest, q as OpenCodePermissionRule, r as OpenCodeReplyMismatchError, s as OpenCodeRunResult, t as OpenCodeServer, u as OpenCodeSessionCreate, v as OpenCodeSessionRef, w as OpenCodeSessionStore, x as OpenCodeToolCall, y as OpenCodeToolRefusedError, z as OpenCodeToolsClaims, A as OpenCodeToolsOptions, B as OpenCodeToolsTokens, C as OpenCodeTurn, D as OpenCodeTurnContext, E as OpenCodeUsage, P as PendingAsk, S as SessionHandle, F as StepCost, G as StepModel, T as TurnOutcome, H as TurnProgress, J as addUsage, K as emptyUsage, N as keyValueOpenCodeSessionStore, Q as openCode, R as openCodeControllers, U as openCodeProviders, V as usageOf } from './engine-BX6MLzxs.cjs';
3
3
  import { ElicitationRequest, ElicitationQuestion, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply } from '@dudousxd/nestjs-agent-core';
4
4
  import { IncomingMessage, ServerResponse } from 'node:http';
5
5
  import '@dudousxd/nestjs-agent';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { d as OpenCodeClient, e as OpenCodeEvent, f as OpenCodeForm, g as OpenCodeFormValue, h as OpenCodeFormField, i as OpenCodeMcpEndpoint, O as OpenCodeTurns, a as OpenCodeEngineSettings } from './engine-Cp5uysaN.js';
2
- export { I as InMemoryOpenCodeSessionStore, M as Milestone, j as OpenCodeAmendment, k as OpenCodeCallContext, c as OpenCodeEngineOptions, b as OpenCodeHost, l as OpenCodeKeyValue, m as OpenCodeModelRef, n as OpenCodePermissionRequest, o as OpenCodePermissionRule, p as OpenCodeReplyMismatchError, q as OpenCodeRunResult, r as OpenCodeServer, s as OpenCodeSessionCreate, t as OpenCodeSessionRef, u as OpenCodeSessionStore, v as OpenCodeToolCall, w as OpenCodeToolRefusedError, x as OpenCodeToolsClaims, y as OpenCodeToolsOptions, z as OpenCodeToolsTokens, A as OpenCodeTurn, B as OpenCodeTurnContext, C as OpenCodeUsage, P as PendingAsk, S as SessionHandle, D as StepCost, E as StepModel, T as TurnOutcome, F as addUsage, G as emptyUsage, H as keyValueOpenCodeSessionStore, J as openCode, K as openCodeControllers, L as openCodeProviders, N as usageOf } from './engine-Cp5uysaN.js';
1
+ import { d as OpenCodeClient, e as OpenCodeEvent, f as OpenCodeForm, g as OpenCodeFormValue, h as OpenCodeFormField, i as OpenCodeMcpEndpoint, O as OpenCodeTurns, a as OpenCodeEngineSettings } from './engine-BX6MLzxs.js';
2
+ export { I as InMemoryOpenCodeSessionStore, L as LOST_ANSWER_ERROR, M as Milestone, j as OpenCodeAmendment, k as OpenCodeCallContext, c as OpenCodeEngineOptions, b as OpenCodeHost, l as OpenCodeKeyValue, m as OpenCodeMessage, n as OpenCodeMessagePart, o as OpenCodeModelRef, p as OpenCodePermissionRequest, q as OpenCodePermissionRule, r as OpenCodeReplyMismatchError, s as OpenCodeRunResult, t as OpenCodeServer, u as OpenCodeSessionCreate, v as OpenCodeSessionRef, w as OpenCodeSessionStore, x as OpenCodeToolCall, y as OpenCodeToolRefusedError, z as OpenCodeToolsClaims, A as OpenCodeToolsOptions, B as OpenCodeToolsTokens, C as OpenCodeTurn, D as OpenCodeTurnContext, E as OpenCodeUsage, P as PendingAsk, S as SessionHandle, F as StepCost, G as StepModel, T as TurnOutcome, H as TurnProgress, J as addUsage, K as emptyUsage, N as keyValueOpenCodeSessionStore, Q as openCode, R as openCodeControllers, U as openCodeProviders, V as usageOf } from './engine-BX6MLzxs.js';
3
3
  import { ElicitationRequest, ElicitationQuestion, AgentRunner, AgentRunInput, AgentRunStartOptions, HumanReply } from '@dudousxd/nestjs-agent-core';
4
4
  import { IncomingMessage, ServerResponse } from 'node:http';
5
5
  import '@dudousxd/nestjs-agent';
package/dist/index.js CHANGED
@@ -143,7 +143,8 @@ OpenCodeMcpController = _ts_decorate([
143
143
 
144
144
  // src/mcp.ts
145
145
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
146
- import { REMEMBER_TOOL_DESCRIPTION, REMEMBER_TOOL_NAME, ToolNotFoundError, rememberInputSchema, schemaExtensionOf } from "@dudousxd/nestjs-agent-core";
146
+ import { GenuiRunRecorder, REMEMBER_TOOL_DESCRIPTION, REMEMBER_TOOL_NAME, ToolNotFoundError, rememberInputSchema, schemaExtensionOf } from "@dudousxd/nestjs-agent-core";
147
+ import { turnChannel } from "@dudousxd/nestjs-agent-core/genui";
147
148
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
148
149
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
149
150
  import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
@@ -274,6 +275,9 @@ function subUsage(a, b) {
274
275
  };
275
276
  }
276
277
  __name(subUsage, "subUsage");
278
+ var LOST_ANSWER_ERROR = "The answer was lost while the agent restarted; ask again.";
279
+ var RECOVER_PAGES = 10;
280
+ var RECOVER_PAGE_SIZE = 100;
277
281
  var ASK_TOOL_NAME = "ask";
278
282
  var FAILED_STATUSES = /* @__PURE__ */ new Set([
279
283
  "error",
@@ -362,6 +366,14 @@ var OpenCodeTurn = class {
362
366
  wroteMessage = false;
363
367
  ended = false;
364
368
  stepError;
369
+ /** What this turn object took from the session's assistant messages (see {@link TurnProgress}). */
370
+ took = {
371
+ text: /* @__PURE__ */ new Map(),
372
+ reasoning: /* @__PURE__ */ new Map(),
373
+ called: /* @__PURE__ */ new Set(),
374
+ settled: /* @__PURE__ */ new Set(),
375
+ steps: /* @__PURE__ */ new Set()
376
+ };
365
377
  constructor(a) {
366
378
  this.a = a;
367
379
  this.model = modelOfLabel(a.input.model);
@@ -379,6 +391,30 @@ var OpenCodeTurn = class {
379
391
  tokens: info.tokens
380
392
  };
381
393
  }
394
+ /** What the turn took so far: carried from before this turn object, plus what it took. */
395
+ progress() {
396
+ const before = this.a.progress;
397
+ const merged = /* @__PURE__ */ __name((carried, own) => {
398
+ const out = {
399
+ ...carried ?? {}
400
+ };
401
+ for (const [id, n] of own) out[id] = (out[id] ?? 0) + n;
402
+ return out;
403
+ }, "merged");
404
+ const union = /* @__PURE__ */ __name((carried, own) => [
405
+ .../* @__PURE__ */ new Set([
406
+ ...carried ?? [],
407
+ ...own
408
+ ])
409
+ ], "union");
410
+ return {
411
+ text: merged(before?.text, this.took.text),
412
+ reasoning: merged(before?.reasoning, this.took.reasoning),
413
+ called: union(before?.called, this.took.called),
414
+ settled: union(before?.settled, this.took.settled),
415
+ steps: union(before?.steps, this.took.steps)
416
+ };
417
+ }
382
418
  /** What the run spent so far: carried from before this turn object, plus what it saw. */
383
419
  totalUsage() {
384
420
  return addUsage(this.a.carried, this.spent);
@@ -469,7 +505,11 @@ var OpenCodeTurn = class {
469
505
  this.reported = {
470
506
  ...this.spent
471
507
  };
472
- const milestone = {
508
+ const milestone = reached.kind === "ask" ? {
509
+ ...reached,
510
+ usage,
511
+ progress: this.progress()
512
+ } : {
473
513
  ...reached,
474
514
  usage
475
515
  };
@@ -524,6 +564,7 @@ var OpenCodeTurn = class {
524
564
  innerSettled: /* @__PURE__ */ new Set()
525
565
  };
526
566
  this.tools.set(id, state);
567
+ this.took.called.add(id);
527
568
  this.segment.calls.push({
528
569
  id,
529
570
  name,
@@ -651,6 +692,7 @@ var OpenCodeTurn = class {
651
692
  case "session.text.delta": {
652
693
  const delta = typeof d.delta === "string" ? d.delta : "";
653
694
  if (!delta) return;
695
+ this.count(this.took.text, d.assistantMessageID, delta.length);
654
696
  this.endReasoning();
655
697
  if (!this.sawText && !delta.trim()) return;
656
698
  await this.step();
@@ -672,6 +714,7 @@ ${delta}` : delta;
672
714
  case "session.reasoning.delta": {
673
715
  const delta = typeof d.delta === "string" ? d.delta : "";
674
716
  if (!delta) return;
717
+ this.count(this.took.reasoning, d.assistantMessageID, delta.length);
675
718
  this.reasoningSince ??= Date.now();
676
719
  await this.step();
677
720
  this.segment.reasoning += delta;
@@ -726,6 +769,7 @@ ${delta}` : delta;
726
769
  }
727
770
  case "session.step.ended":
728
771
  this.endReasoning();
772
+ if (typeof d.assistantMessageID === "string") this.took.steps.add(d.assistantMessageID);
729
773
  await this.stepEnded(d);
730
774
  return;
731
775
  case "session.step.failed":
@@ -766,10 +810,16 @@ ${delta}` : delta;
766
810
  return;
767
811
  }
768
812
  }
813
+ /** Characters taken from an assistant message's text or reasoning. */
814
+ count(into, messageId, n) {
815
+ if (typeof messageId !== "string") return;
816
+ into.set(messageId, (into.get(messageId) ?? 0) + n);
817
+ }
769
818
  async settleTool(id, ok, error = "failed") {
770
819
  const state = this.tool(id);
771
820
  if (state.settled) return;
772
821
  state.settled = true;
822
+ this.took.settled.add(id);
773
823
  if (ok) {
774
824
  this.segment.results.push({
775
825
  id,
@@ -1319,9 +1369,171 @@ ${delta}` : delta;
1319
1369
  cost: cost < 1e-9 ? 0 : cost
1320
1370
  }, this.compacted ? "history_summary" : "title");
1321
1371
  }
1322
- async end(outcome) {
1372
+ /**
1373
+ * The session's messages since this turn's prompt, oldest first: everything after its newest user
1374
+ * message. `undefined` when they cannot be read (no `message.list`, no prompt within reach); empty
1375
+ * when the newest prompt is another run's (this turn's never reached the session).
1376
+ */
1377
+ async messagesSincePrompt() {
1378
+ const messages = this.a.client.message;
1379
+ if (messages === void 0) return void 0;
1380
+ const since = [];
1381
+ let cursor;
1382
+ for (let page = 0; page < RECOVER_PAGES; page += 1) {
1383
+ const listed = await messages.list({
1384
+ sessionID: this.a.sessionId,
1385
+ order: "desc",
1386
+ limit: RECOVER_PAGE_SIZE,
1387
+ ...cursor !== void 0 ? {
1388
+ cursor
1389
+ } : {}
1390
+ });
1391
+ for (const message of listed.data) {
1392
+ if (message.type === "user") {
1393
+ const runId = message.metadata?.runId;
1394
+ if (typeof runId === "string" && runId !== this.a.runId) return [];
1395
+ return since.reverse();
1396
+ }
1397
+ since.push(message);
1398
+ }
1399
+ const next = listed.cursor?.next;
1400
+ if (typeof next !== "string" || next.length === 0 || listed.data.length === 0) break;
1401
+ cursor = next;
1402
+ }
1403
+ return void 0;
1404
+ }
1405
+ /**
1406
+ * A turn rebuilt after a restart heard only what the session said once it re-attached: what it
1407
+ * said in between — read back from the session's messages since the prompt, less what an earlier
1408
+ * process already took ({@link TurnProgress}) and what this one heard — is taken now, as its
1409
+ * events would have been: text, reasoning, tool calls and results, each step's usage. A session
1410
+ * that left nothing for the turn fails it ({@link LOST_ANSWER_ERROR}) rather than reading as an
1411
+ * empty answer.
1412
+ */
1413
+ async recover(outcome) {
1414
+ const messages = await this.messagesSincePrompt().catch((error) => {
1415
+ this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session messages")}`);
1416
+ return void 0;
1417
+ });
1418
+ const before = this.a.progress;
1419
+ const text = [];
1420
+ let reasoning = "";
1421
+ let lastError;
1422
+ for (const message of messages ?? []) {
1423
+ if (message.type !== "assistant") continue;
1424
+ const id = message.id;
1425
+ const parts = message.content ?? [];
1426
+ text.push(...missed(parts, "text", before?.text[id] ?? 0, this.took.text.get(id) ?? 0));
1427
+ reasoning += missed(parts, "reasoning", before?.reasoning[id] ?? 0, this.took.reasoning.get(id) ?? 0).map((piece) => piece.text).join("");
1428
+ for (const part of parts) if (part.type === "tool") await this.missedTool(part);
1429
+ if (message.tokens !== void 0 && !before?.steps.includes(id) && !this.took.steps.has(id)) {
1430
+ this.took.steps.add(id);
1431
+ const model = message.model;
1432
+ if (typeof model?.id === "string" && typeof model.providerID === "string") this.model = {
1433
+ providerID: model.providerID,
1434
+ id: model.id
1435
+ };
1436
+ await this.spend({
1437
+ tokens: message.tokens,
1438
+ cost: message.cost
1439
+ }, "chat");
1440
+ }
1441
+ if (message.error?.message) lastError = message.error.message;
1442
+ }
1443
+ const recovered = text.map((piece) => piece.text).join("\n\n").trimStart();
1444
+ if (recovered.trim()) {
1445
+ await this.step();
1446
+ const joint = this.segment.text && !text.at(-1)?.continues ? "\n\n" : "";
1447
+ this.segment.text = `${recovered}${joint}${this.segment.text}`;
1448
+ this.sawText = true;
1449
+ await this.write({
1450
+ kind: "text",
1451
+ text: recovered
1452
+ });
1453
+ }
1454
+ if (reasoning) {
1455
+ await this.step();
1456
+ this.segment.reasoning = `${reasoning}${this.segment.reasoning}`;
1457
+ await this.write({
1458
+ kind: "reasoning",
1459
+ text: reasoning
1460
+ });
1461
+ }
1462
+ if (outcome.status !== "succeeded") return outcome;
1463
+ const idle = (messages ?? []).filter((m) => m.type === "idle").at(-1);
1464
+ if (idle?.outcome === "failed") return {
1465
+ status: "failed",
1466
+ error: lastError ?? this.stepError ?? "the execution failed"
1467
+ };
1468
+ if (idle?.outcome === "interrupted") return {
1469
+ status: "interrupted"
1470
+ };
1471
+ const s = this.segment;
1472
+ const said = s.text || s.reasoning || s.calls.length > 0 || s.ui.length > 0;
1473
+ const progress = this.progress();
1474
+ const tookAny = Object.keys(progress.text).length > 0 || progress.called.length > 0 || progress.steps.length > 0;
1475
+ if (!said && !this.wroteMessage && !tookAny) return {
1476
+ status: "failed",
1477
+ error: LOST_ANSWER_ERROR
1478
+ };
1479
+ return outcome;
1480
+ }
1481
+ /** A tool part of a message the turn did not hear: its call, its result, whichever it lacks. */
1482
+ async missedTool(part) {
1483
+ const id = part.id;
1484
+ const name = part.name ?? "tool";
1485
+ if (id === void 0 || HIDDEN_TOOLS.has(name)) return;
1486
+ const state = part.state ?? {};
1487
+ const before = this.a.progress;
1488
+ if (!before?.called.includes(id) && !this.tools.has(id)) {
1489
+ await this.step();
1490
+ this.setInput(id, state.input, name);
1491
+ await this.makeAvailable(id);
1492
+ }
1493
+ const ok = state.status === "completed";
1494
+ if (!ok && state.status !== "error" || before?.settled.includes(id)) return;
1495
+ const error = errorText(state.error, "tool failed");
1496
+ if (this.tools.has(id)) {
1497
+ await this.settleTool(id, ok, error);
1498
+ return;
1499
+ }
1500
+ this.took.settled.add(id);
1501
+ const result = ok ? {
1502
+ id,
1503
+ name,
1504
+ output: {
1505
+ ok: true
1506
+ }
1507
+ } : {
1508
+ id,
1509
+ name,
1510
+ output: {
1511
+ ok: false
1512
+ },
1513
+ error
1514
+ };
1515
+ const messageId = await this.messageOf(id);
1516
+ if (messageId !== void 0) await this.addResult(messageId, result);
1517
+ else this.segment.results.push(result);
1518
+ await this.write(ok ? {
1519
+ kind: "tool-output",
1520
+ id,
1521
+ output: {
1522
+ ok: true
1523
+ }
1524
+ } : {
1525
+ kind: "tool-output-error",
1526
+ id,
1527
+ error
1528
+ });
1529
+ }
1530
+ async end(ended) {
1323
1531
  if (this.ended) return;
1324
1532
  this.ended = true;
1533
+ const outcome = this.a.resumed === true ? await this.recover(ended).catch((error) => {
1534
+ this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not recover the turn")}`);
1535
+ return ended;
1536
+ }) : ended;
1325
1537
  await this.unreported().catch((error) => this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session usage")}`));
1326
1538
  if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
1327
1539
  await this.flush(true);
@@ -1338,6 +1550,26 @@ ${delta}` : delta;
1338
1550
  });
1339
1551
  }
1340
1552
  };
1553
+ function missed(parts, type, head, tail) {
1554
+ const texts = parts.flatMap((p) => p.type === type && typeof p.text === "string" ? [
1555
+ p.text
1556
+ ] : []);
1557
+ const total = texts.reduce((n, t) => n + t.length, 0);
1558
+ const end = total - tail;
1559
+ const out = [];
1560
+ let at = 0;
1561
+ for (const t of texts) {
1562
+ const from = Math.max(head, at);
1563
+ const to = Math.min(end, at + t.length);
1564
+ if (to > from) out.push({
1565
+ text: t.slice(from - at, to - at),
1566
+ continues: to < at + t.length
1567
+ });
1568
+ at += t.length;
1569
+ }
1570
+ return out;
1571
+ }
1572
+ __name(missed, "missed");
1341
1573
  function modelOfLabel(label) {
1342
1574
  const slash = label?.indexOf("/") ?? -1;
1343
1575
  if (label === void 0 || slash <= 0 || slash === label.length - 1) return void 0;
@@ -1464,6 +1696,8 @@ var OpenCodeMcpEndpoint2 = class {
1464
1696
  }
1465
1697
  turns;
1466
1698
  tokens;
1699
+ /** Genui telemetry of the UI calls OpenCode makes (its code mode calls tools here, not in the loop). */
1700
+ genui = new GenuiRunRecorder();
1467
1701
  constructor(turns, tokens) {
1468
1702
  this.turns = turns;
1469
1703
  this.tokens = tokens;
@@ -1595,7 +1829,7 @@ var OpenCodeMcpEndpoint2 = class {
1595
1829
  if (spec.kind === "action" && !await this.turns.spendApproval(turn.runId, name, turn.input)) {
1596
1830
  throw new OpenCodeToolRefusedError(`"${name}" is an action and nobody approved this call; ask for the permission first.`);
1597
1831
  }
1598
- return registry.invoke(name, args ?? {}, {
1832
+ const invoke = /* @__PURE__ */ __name(() => registry.invoke(name, args ?? {}, {
1599
1833
  actor,
1600
1834
  threadId: turn.ctx.threadId,
1601
1835
  runId: turn.runId,
@@ -1612,7 +1846,24 @@ var OpenCodeMcpEndpoint2 = class {
1612
1846
  } : {}
1613
1847
  }, this.turns.rolesPolicyFor(turn.input), allowed !== void 0 ? {
1614
1848
  allowedTools: allowed
1615
- } : {});
1849
+ } : {}), "invoke");
1850
+ return this.genui.track({
1851
+ store: this.turns.store,
1852
+ marker: spec.genui,
1853
+ runId: turn.runId,
1854
+ threadId: turn.ctx.threadId,
1855
+ actorRef: actor.id,
1856
+ ...turn.input.agentName !== void 0 ? {
1857
+ agentName: turn.input.agentName
1858
+ } : {},
1859
+ channel: turnChannel(turn.input.pageContext),
1860
+ modelId: turn.input.model ?? "opencode",
1861
+ call: {
1862
+ id: `mcp:${turn.runId}:${requestId}`,
1863
+ name,
1864
+ input: args ?? {}
1865
+ }
1866
+ }, invoke);
1616
1867
  }
1617
1868
  /** `remember`: one fact, at the actor's own scope only — as the loop serves it. */
1618
1869
  async remember(turn, args) {
@@ -2464,7 +2715,11 @@ ${body}
2464
2715
  text: prompt.text,
2465
2716
  ...prompt.files?.length ? {
2466
2717
  files: prompt.files
2467
- } : {}
2718
+ } : {},
2719
+ // Marks the prompt as this run's: a turn resumed after a restart reads what came after it.
2720
+ metadata: {
2721
+ runId
2722
+ }
2468
2723
  });
2469
2724
  } catch (error) {
2470
2725
  live.turn.fail(`OpenCode refused the message: ${error.message}`);
@@ -2477,9 +2732,11 @@ ${body}
2477
2732
  *
2478
2733
  * `spent` is what the run spent before (the sum of the `usage` of its earlier milestones): a turn
2479
2734
  * rebuilt here starts from it, so the answer it persists carries the whole run's usage.
2735
+ * `progress` is what the turn took before (its last `ask` milestone's): a turn rebuilt here reads
2736
+ * back from the session's messages, when the execution ends, what nobody heard after that.
2480
2737
  */
2481
- async observe(runId, input, handle, spent) {
2482
- const live = await this.ensureLive(runId, input, handle, true, spent);
2738
+ async observe(runId, input, handle, spent, progress) {
2739
+ const live = await this.ensureLive(runId, input, handle, true, spent, progress);
2483
2740
  const { turn, client } = live;
2484
2741
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2485
2742
  let done = false;
@@ -2518,12 +2775,12 @@ ${body}
2518
2775
  * request is gone with it: a new session is opened, told what the person decided, and prompted
2519
2776
  * to go on.
2520
2777
  */
2521
- async reply(runId, input, handle, ask, reply, spent) {
2778
+ async reply(runId, input, handle, ask, reply, spent, progress) {
2522
2779
  const server = await this.host.server(input.actor);
2523
2780
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2524
2781
  return this.continueAfterRestart(runId, input, server, ask, reply, spent);
2525
2782
  }
2526
- const live = await this.ensureLive(runId, input, handle, false, spent);
2783
+ const live = await this.ensureLive(runId, input, handle, false, spent, progress);
2527
2784
  await live.turn.decide(ask, reply);
2528
2785
  return handle;
2529
2786
  }
@@ -2541,7 +2798,10 @@ ${body}
2541
2798
  live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2542
2799
  await live.client.session.prompt({
2543
2800
  sessionID: handle.sessionId,
2544
- text: note
2801
+ text: note,
2802
+ metadata: {
2803
+ runId
2804
+ }
2545
2805
  });
2546
2806
  return handle;
2547
2807
  }
@@ -2565,9 +2825,10 @@ ${body}
2565
2825
  sessionID: known.sessionId
2566
2826
  });
2567
2827
  }
2568
- async ensureLive(runId, input, handle, catchUp, spent) {
2828
+ async ensureLive(runId, input, handle, catchUp, spent, progress) {
2569
2829
  const current = this.live.get(runId);
2570
2830
  if (current !== void 0 && current.handle.sessionId === handle.sessionId) return current;
2831
+ const resumed = catchUp && current === void 0;
2571
2832
  current?.stop();
2572
2833
  const server = await this.host.server(input.actor);
2573
2834
  const deps = this.deps.forAgent(input.agentName);
@@ -2598,6 +2859,10 @@ ${body}
2598
2859
  ...spent !== void 0 ? {
2599
2860
  carried: spent
2600
2861
  } : {},
2862
+ ...progress !== void 0 ? {
2863
+ progress
2864
+ } : {},
2865
+ resumed,
2601
2866
  logger: this.logger,
2602
2867
  onGranted: /* @__PURE__ */ __name((action, id) => {
2603
2868
  const grants = this.grants.get(runId) ?? [];
@@ -2617,7 +2882,7 @@ ${body}
2617
2882
  stop
2618
2883
  };
2619
2884
  this.live.set(runId, live);
2620
- if (catchUp && current === void 0) await turn.catchUp().catch(() => void 0);
2885
+ if (resumed) await turn.catchUp().catch(() => void 0);
2621
2886
  return live;
2622
2887
  }
2623
2888
  /** Stop following a run in this process. */
@@ -3159,6 +3424,7 @@ __name(openCodeProviders, "openCodeProviders");
3159
3424
  export {
3160
3425
  FormAnswerError,
3161
3426
  InMemoryOpenCodeSessionStore,
3427
+ LOST_ANSWER_ERROR,
3162
3428
  OPENCODE_HOST,
3163
3429
  OPENCODE_MCP_ENDPOINT,
3164
3430
  OPENCODE_OPTIONS,