teapot-coding-agent 0.26.4 → 0.26.6

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.
@@ -536,6 +536,43 @@ export class Agent {
536
536
  async load() {
537
537
  await this.ensureReady();
538
538
  }
539
+ /**
540
+ * Answer tool_calls in a RESTORED history that have no tool_result (#54).
541
+ *
542
+ * The ordinary backstop (`answerUnansweredToolCalls`) works on an assistant
543
+ * turn inside a live loop. After a restart there is no loop: the call is in
544
+ * the log, the process that was running it is gone, and nothing else will ever
545
+ * write its result.
546
+ */
547
+ async answerUnansweredRestoredCalls() {
548
+ const seen = new Set();
549
+ for (const ev of await readEvents(this.log.filePath)) {
550
+ const d = (ev.data ?? {});
551
+ const id = d.callId ? String(d.callId) : null;
552
+ if (!id)
553
+ continue;
554
+ if (ev.type === "tool_call")
555
+ seen.add(id);
556
+ else if (ev.type === "tool_result")
557
+ seen.delete(id);
558
+ }
559
+ for (const id of seen) {
560
+ await this.log.append("tool_result", this.currentSession, this.currentBranch, {
561
+ callId: id,
562
+ name: "unknown",
563
+ ok: false,
564
+ durationMs: 0,
565
+ result: "not completed — the agent restarted while this tool was running",
566
+ synthesized: true,
567
+ });
568
+ }
569
+ if (seen.size)
570
+ await this.log.append("system_note", this.currentSession, this.currentBranch, {
571
+ event: "unfinished-tools-closed",
572
+ count: seen.size,
573
+ detail: "a restart left these tool calls without a result; they are marked not completed (#54)",
574
+ });
575
+ }
539
576
  /** harness-managed files inside the session directory */
540
577
  get goalFile() {
541
578
  return path.join(this.opts.sessionDir, "goal.md");
@@ -705,6 +742,24 @@ export class Agent {
705
742
  // seen and acted on that output, and the pruned form keeps the head plus
706
743
  // a marker, exactly as it did live.
707
744
  this.maybePrune();
745
+ // #54: answer tool_calls the log says are still open.
746
+ //
747
+ // The restore path rebuilds `msgs` straight from the log, so a call whose
748
+ // process died mid-command comes back as an assistant turn with
749
+ // `tool_calls` and NO matching `tool_result`. Nothing ever answers it —
750
+ // not `stop()` (there is no turn to stop) and not `dispose()` (the process
751
+ // is already gone). The row then renders as still running for good.
752
+ //
753
+ // Measured across every session log: 43,483 tool_calls, and 4 unanswered —
754
+ // every one a `bash`, each followed immediately by
755
+ // `session-restored` / `idle->stopped disposed`. The shape is a restart
756
+ // (or a crash) landing while a long command was in flight, which is
757
+ // precisely when a human is most likely to notice "it never finished".
758
+ //
759
+ // So close them ON RESTORE, before the restored history is used. This is
760
+ // the same backstop as the mid-turn paths, reached from the one place
761
+ // those could not: after the process is gone.
762
+ await this.answerUnansweredRestoredCalls();
708
763
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
709
764
  event: "session-restored",
710
765
  branch: last.branch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.26.4",
3
+ "version": "0.26.6",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",