pi-durable-subagents 1.0.12 → 1.0.13

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.13
4
+
5
+ - A subagent resumed after its execution was interrupted (the orchestrator
6
+ restarted, the process died, a failover) is told that the processes its
7
+ tools had started, background ones included, were stopped with it, and not
8
+ to wait for them; an asker that hibernated while waiting is told the same
9
+ with the answer. Before, it was told only to continue, and one slept on a
10
+ test loop that no longer ran.
11
+ - The "no execution activity" alert names the tool command running and how
12
+ long it has run (for example "running bash `make fault-matrix` for 14m"), so
13
+ a long silent command reads differently from a stuck call.
14
+
3
15
  ## 1.0.12
4
16
 
5
17
  - Slot, memory and model decisions read the orchestrator settings recorded in
package/README.md CHANGED
@@ -51,7 +51,7 @@ the npx cache, so `install-service` refuses to run from there.
51
51
  | A step runs past its `timeoutMs` (even inside a silent tool) | Stops it cleanly as `timeout`. Only time spent working counts; waiting for you does not. |
52
52
  | You quit pi (Ctrl+D, `/quit`, closing the terminal) while subagents run | That session's workflows pause: nothing more is spent, nothing is lost. When you come back, pi says so; `resume` (or `r` in the list) continues them in the same sessions. Set `"onQuit": "continue"` to let them run on instead. |
53
53
  | pi crashes or is killed (`kill -9`) while subagents run | The work keeps running. When you come back, the session that started the work is told what needs you. |
54
- | The machine or the orchestrator dies mid-run | The next pi you open resumes the work. Finished results are kept and nothing runs twice. |
54
+ | The machine or the orchestrator dies mid-run | The next pi you open resumes the work. Finished results are kept and nothing runs twice. The resumed subagent is told that processes its tools had started (background ones included) were stopped, so it checks them instead of waiting for them. |
55
55
  | You steer a subagent while it is asking you a question | Your message reaches it, in order. Nothing is rejected or lost. |
56
56
  | Two steers arrive out of order and the second replaces the first | Only the second one applies. |
57
57
  | A step is refused, or a dependency fails | The workflow stops that branch cleanly. Nothing is retried in vain. |
@@ -174,7 +174,7 @@ The main agent is interrupted only when there is something to decide:
174
174
 
175
175
  - a question;
176
176
  - a finished workflow;
177
- - a stalled subagent;
177
+ - a stalled subagent (the alert names the command it is running and for how long, so a long silent command reads differently from a stuck call);
178
178
  - an unknown outcome;
179
179
  - a reached budget.
180
180
 
@@ -88,6 +88,16 @@ async function defaultModel() {
88
88
  return settings.defaultModel ? `${settings.defaultProvider ? `${settings.defaultProvider}/` : ""}${settings.defaultModel}` : undefined;
89
89
  }
90
90
  /** P2, P9, P22: Construct the journal-owned execution authority under the engine's OS lock. */
91
+ /** Restart feedback: a resumed child believed the test loop it had started was still running and slept on it. Its
92
+ * previous execution was stopped (fenced), and with it every process its tools started, background ones included. */
93
+ export function continueMessage(dangling) {
94
+ return "Your previous execution was interrupted and this one continues the task. Processes your tools started " +
95
+ "(background ones included) were stopped with it: do not wait for them or their output; check what they left " +
96
+ "and start again what is still needed. Tool calls whose outcomes are unknown: " + (dangling.join(", ") || "none") + ".";
97
+ }
98
+ /** An asker hibernates while it waits (its execution is stopped to free the provider slot), so the answer says so. */
99
+ export const HIBERNATED_NOTE = "While you waited for the answer below your execution was stopped; processes your tools had " +
100
+ "started (background ones included) were stopped with it. Check them before relying on them.";
91
101
  export default function createExecutor(ledgers, options = {}) {
92
102
  const { home, config, orch } = ledgers;
93
103
  const containment = options.containment ?? serialContainment(), effects = options.effects ?? createEffects(ledgers);
@@ -742,7 +752,7 @@ export default function createExecutor(ledgers, options = {}) {
742
752
  else if (unresolved && !receipt)
743
753
  await sender.send(t.callId, "continue", { message: String(unresolved.message) }, { qid: String(unresolved.qid), rev: Number(unresolved.rev) }, { rid: String(unresolved.rid2) });
744
754
  else if (previous)
745
- await sender.send(t.callId, "continue", { message: `Continue the task. Tool calls whose outcomes are unknown: ${dangling.join(", ") || "none"}.` }, undefined, { rid: contentHash([exec, "dispatch"]) });
755
+ await sender.send(t.callId, "continue", { message: continueMessage(dangling) }, undefined, { rid: contentHash([exec, "dispatch"]) });
746
756
  else
747
757
  await sender.send(t.callId, "task", { message: t.opening?.message ?? t.spec.task }, undefined, { rid: contentHash([exec, "dispatch"]) });
748
758
  if (interrupted(a)) {
@@ -858,7 +868,7 @@ export default function createExecutor(ledgers, options = {}) {
858
868
  return { action: "reject", reason: "stale-rev" };
859
869
  const rid2 = forwardRid(req.rid, ctx.widRev, ctx.key, hash);
860
870
  const item = attentionEntries(ctx.journal.entries()).find(e => e.item.call === dest && e.item.qid === sleeping.qid && e.item.rev === sleeping.rev)?.item;
861
- await ctx.journal.append("answer-bound", { call: dest, qid: sleeping.qid, rev: sleeping.rev, rid: req.rid, rid2, hash, message: `Question: ${item?.text ?? sleeping.qid}\nAnswer: ${req.body.message ?? ""}` });
871
+ await ctx.journal.append("answer-bound", { call: dest, qid: sleeping.qid, rev: sleeping.rev, rid: req.rid, rid2, hash, message: `${HIBERNATED_NOTE}\n\nQuestion: ${item?.text ?? sleeping.qid}\nAnswer: ${req.body.message ?? ""}` });
862
872
  active.get(dest)?.wake();
863
873
  wake();
864
874
  return { action: "apply" };
@@ -11,12 +11,21 @@ import { monotoneTime } from "../../kernel/guards.js";
11
11
  import { readSession } from "./session.js";
12
12
  import { ActiveTime, activeTotal } from "./time.js";
13
13
  import { observation, reached, sessionUsage, totalUsage } from "./usage.js";
14
+ /** A tool call's command for a status line: bash's command, else its arguments, on one line and clipped. */
15
+ export function toolCommand(args) {
16
+ const a = args && typeof args === "object" ? args : {};
17
+ const text = typeof a.command === "string" ? a.command : Object.keys(a).length ? JSON.stringify(a) : "";
18
+ const line = text.replace(/\s+/g, " ").trim();
19
+ return line.length > 120 ? `${line.slice(0, 119)}…` : line;
20
+ }
14
21
  /** P18, P31, P9: Observe slim evidence, decide limits and fence before returning to settlement. */
15
22
  export async function observeExecution(d) {
16
23
  const { home, config, ticket: t, exec, child, serial } = d;
17
24
  const session = callSession(home, t.wid, t.key, t.gen), clock = new ActiveTime(), started = clock.last;
18
25
  let progress = started, providerError;
19
26
  const tools = new Set();
27
+ // The tool calls running now, for the stall text: a silent long command and a stuck call read differently.
28
+ const running = new Map();
20
29
  const prior = activeTotal(t.journal.entries(), t.callId);
21
30
  let size = (await fileStat(session).catch(() => ({ size: 0 }))).size, checkpoint = performance.now();
22
31
  let signal;
@@ -48,8 +57,15 @@ export async function observeExecution(d) {
48
57
  if (open && fresh)
49
58
  await t.journal.append(JT.attentionResolved, { id, rev: last.rev, resolution: "activity" });
50
59
  else if (!open && !clock.asking && performance.now() - clock.last >= (config.k?.stallMs ?? 600000))
51
- await t.journal.append(JT.attention, { exec, horizon: clock.last, item: { id, rev: (last?.rev ?? 0) + 1, kind: "stall", text: `${t.wid}/${t.key}: no execution activity for ${Math.floor((performance.now() - clock.last) / 60000)}m`, wid: t.wid, call: t.callId } });
60
+ await t.journal.append(JT.attention, { exec, horizon: clock.last, item: { id, rev: (last?.rev ?? 0) + 1, kind: "stall", text: `${t.wid}/${t.key}: no execution activity for ${Math.floor((performance.now() - clock.last) / 60000)}m` + runningText(), wid: t.wid, call: t.callId } });
52
61
  });
62
+ /** "; running bash `make matrix` for 14m (no output or CPU use seen)": a silent long command, not a stuck model. */
63
+ const runningText = () => {
64
+ const now = performance.now(), open = [...running.values()].filter(r => r.name !== "ask");
65
+ if (!open.length)
66
+ return "";
67
+ return "; running " + open.map(r => `${r.name}${r.command ? ` \`${r.command}\`` : ""} for ${Math.floor((now - r.since) / 60000)}m`).join(", ") + " (no output or CPU use seen)";
68
+ };
53
69
  const noProgress = () => serial(async () => {
54
70
  const id = `noprogress:${t.callId}`;
55
71
  const items = attentionEntries(t.journal.entries(), id), last = items.at(-1)?.item;
@@ -94,10 +110,14 @@ export async function observeExecution(d) {
94
110
  event.type === "message_end" && message?.stopReason === "error" ? message.errorMessage : undefined;
95
111
  if (typeof error === "string" && error)
96
112
  providerError = error;
97
- if (event.type === "tool_execution_start")
113
+ if (event.type === "tool_execution_start") {
98
114
  tools.add(String(event.toolCallId ?? ""));
99
- if (event.type === "tool_execution_end")
115
+ running.set(String(event.toolCallId ?? ""), { name: String(event.toolName ?? "tool"), command: toolCommand(event.args), since: performance.now() });
116
+ }
117
+ if (event.type === "tool_execution_end") {
100
118
  tools.delete(String(event.toolCallId ?? ""));
119
+ running.delete(String(event.toolCallId ?? ""));
120
+ }
101
121
  // Receipt-time progress is independent of RPC chatter, CPU and session growth; open tools suppress alerts.
102
122
  if (event.type === "message_update" || ["tool_execution_start", "tool_execution_update", "tool_execution_end"].includes(String(event.type)) ||
103
123
  event.type === "message_end" && message?.stopReason !== "error" && (message?.usage?.output ?? 0) > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-durable-subagents",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Subagents for pi that never lose work and never do it twice. Crash-safe workflows, automatic recovery, and a live view just like the main agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",