@fitchmultz/pi-subagents 0.51.0 → 0.52.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/agent/child-runtime.js +80 -0
  3. package/dist/agent/contracts.js +9 -0
  4. package/dist/agent/index.js +6 -20
  5. package/dist/agent/observation.js +4 -0
  6. package/dist/agent/parking.js +253 -0
  7. package/dist/agent/process.js +31 -4
  8. package/dist/agent/runner.js +124 -126
  9. package/dist/agent/shell.js +2 -2
  10. package/dist/agent/storage.js +18 -0
  11. package/dist/build-info.js +1 -1
  12. package/dist/pi-intercom/broker/broker.js +42 -29
  13. package/dist/pi-intercom/broker/client-last.d.ts +12 -1
  14. package/dist/pi-intercom/broker/client-last.js +25 -0
  15. package/dist/pi-intercom/broker/client-park.d.ts +9 -0
  16. package/dist/pi-intercom/broker/client-park.js +21 -0
  17. package/dist/pi-intercom/broker/client.d.ts +2 -3
  18. package/dist/pi-intercom/broker/client.js +23 -28
  19. package/dist/pi-intercom/broker/last.d.ts +5 -1
  20. package/dist/pi-intercom/broker/last.js +34 -12
  21. package/dist/pi-intercom/broker/parked.js +107 -0
  22. package/dist/pi-intercom/broker/server-connection.js +1 -1
  23. package/dist/pi-intercom/connection.js +65 -22
  24. package/dist/pi-intercom/inbound.js +11 -0
  25. package/dist/pi-intercom/index.js +38 -0
  26. package/dist/shared/parking.js +12 -0
  27. package/dist/shared/post-exit-stdio-guard.js +4 -2
  28. package/docs/code-quality.md +5 -5
  29. package/docs/development.md +3 -3
  30. package/docs/intercom.md +2 -1
  31. package/docs/performance.md +31 -3
  32. package/docs/reference.md +12 -1
  33. package/package.json +1 -1
  34. package/scripts/measure-size.mjs +1 -1
  35. package/src/agent/child-runtime.ts +105 -0
  36. package/src/agent/contracts.ts +10 -0
  37. package/src/agent/host.ts +22 -2
  38. package/src/agent/index.ts +10 -17
  39. package/src/agent/observation.ts +4 -0
  40. package/src/agent/parking.ts +341 -0
  41. package/src/agent/process.ts +41 -4
  42. package/src/agent/runner.ts +124 -144
  43. package/src/agent/shell.ts +6 -2
  44. package/src/agent/storage.ts +17 -0
  45. package/src/pi-intercom/broker/broker.ts +44 -37
  46. package/src/pi-intercom/broker/client-last.ts +41 -1
  47. package/src/pi-intercom/broker/client-park.ts +23 -0
  48. package/src/pi-intercom/broker/client.ts +24 -28
  49. package/src/pi-intercom/broker/last.ts +49 -14
  50. package/src/pi-intercom/broker/parked.ts +130 -0
  51. package/src/pi-intercom/broker/server-connection.ts +2 -2
  52. package/src/pi-intercom/connection.ts +87 -34
  53. package/src/pi-intercom/inbound.ts +14 -0
  54. package/src/pi-intercom/index.ts +41 -0
  55. package/src/pi-intercom/last-message.ts +1 -1
  56. package/src/shared/parking.ts +22 -0
  57. package/src/shared/post-exit-stdio-guard.ts +5 -2
@@ -106,7 +106,21 @@ export class IntercomInbound {
106
106
  }
107
107
  this.owners.delivery.queue(entry, "passive");
108
108
  }
109
+ /** Counted synchronously so a parking fence observes frames received before its ack. */
110
+ receiving = 0;
109
111
  async receive(ctx: ExtensionContext, from: SessionInfo, input: Message): Promise<void> {
112
+ this.receiving++;
113
+ try {
114
+ await this.receiveCounted(ctx, from, input);
115
+ } finally {
116
+ this.receiving--;
117
+ }
118
+ }
119
+ private async receiveCounted(
120
+ ctx: ExtensionContext,
121
+ from: SessionInfo,
122
+ input: Message,
123
+ ): Promise<void> {
110
124
  const generation = this.lifecycle.generation;
111
125
  const restore = this.owners.delivery.restoring;
112
126
  if (restore) {
@@ -13,6 +13,20 @@ import { IntercomInspection } from "./inspection.ts";
13
13
  import { IntercomTools } from "./tools.ts";
14
14
  import { IntercomSessionEvents } from "./session-events.ts";
15
15
  import { registerIntercomTools } from "./tool-registration.ts";
16
+ import { isParkRequest, PARK, PARKED_HISTORY, WAKE } from "../shared/parking.ts";
17
+ import type { NativeHistory } from "./last-message.ts";
18
+
19
+ /** The agent runtime answers for its exited parked child from the captured native session. */
20
+ function parkedHistory(pi: ExtensionAPI, peer: string): Promise<NativeHistory | undefined> {
21
+ let history: Promise<NativeHistory | undefined> | undefined;
22
+ pi.events.emit(PARKED_HISTORY, {
23
+ peer,
24
+ reply: (found: Promise<NativeHistory | undefined>) => {
25
+ history = found;
26
+ },
27
+ });
28
+ return history ?? Promise.reject(new Error("No agent runtime owns parked history."));
29
+ }
16
30
 
17
31
  export default function piIntercomExtension(pi: ExtensionAPI): void {
18
32
  const config = loadConfig();
@@ -34,6 +48,10 @@ export default function piIntercomExtension(pi: ExtensionAPI): void {
34
48
  new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }),
35
49
  );
36
50
  },
51
+ parkedHistory: (peer) => parkedHistory(pi, peer.id),
52
+ wake: (peer) => {
53
+ pi.events.emit(WAKE, peer);
54
+ },
37
55
  });
38
56
  const delivery = new InboundDeliveryOwner(pi, lifecycle, journal, {
39
57
  replies,
@@ -59,4 +77,27 @@ export default function piIntercomExtension(pi: ExtensionAPI): void {
59
77
  wait,
60
78
  }).register();
61
79
  registerIntercomTools(pi, tools);
80
+ pi.events.on(PARK, (request) => {
81
+ if (isParkRequest(request)) {
82
+ request.reply(request.park ? connection.park() : connection.unpark());
83
+ }
84
+ });
85
+ // Native fork sleep readiness; it vetoes only this package's own parking, never other saves.
86
+ Reflect.apply(pi.on.bind(pi), undefined, [
87
+ "working_session_save",
88
+ () =>
89
+ !connection.parking
90
+ ? undefined
91
+ : {
92
+ blockers: [
93
+ inbound.receiving > 0 && "intercom is receiving a message",
94
+ journal.size > 0 && "intercom inbound messages await delivery",
95
+ replies.listPending().length > 0 && "intercom asks await replies",
96
+ wait.waiting && "intercom is waiting for a reply",
97
+ (delivery.restoring !== undefined || delivery.batching) &&
98
+ "intercom delivery is restoring",
99
+ connection.reconnected && "intercom reconnected while parking",
100
+ ].filter((blocker) => typeof blocker === "string"),
101
+ },
102
+ ]);
62
103
  }
@@ -7,7 +7,7 @@ import {
7
7
  type SessionInfo,
8
8
  } from "./types.ts";
9
9
 
10
- type NativeHistory = ExtensionContext["sessionManager"];
10
+ export type NativeHistory = Pick<ExtensionContext["sessionManager"], "getBranch">;
11
11
  function completedResponse(
12
12
  manager: NativeHistory,
13
13
  entryId?: string,
@@ -0,0 +1,22 @@
1
+ import type { NativeHistory } from "../pi-intercom/last-message.ts";
2
+ import { isRecord } from "./unknown.ts";
3
+
4
+ /** A child actor asks its intercom to fence broker input before parking, or to release it. */
5
+ export const PARK = "pi-subagents:park";
6
+ export interface ParkRequest {
7
+ readonly park: boolean;
8
+ readonly reply: (parked: Promise<boolean>) => void;
9
+ }
10
+ /** A main intercom wakes or reads an exited parked child through the agent runtime. */
11
+ export const WAKE = "pi-subagents:wake";
12
+ export const PARKED_HISTORY = "pi-subagents:parked-history";
13
+ export interface HistoryRequest {
14
+ readonly peer: string;
15
+ readonly reply: (history: Promise<NativeHistory | undefined>) => void;
16
+ }
17
+ export function isParkRequest(value: unknown): value is ParkRequest {
18
+ return isRecord(value) && typeof value.park === "boolean" && typeof value.reply === "function";
19
+ }
20
+ export function isHistoryRequest(value: unknown): value is HistoryRequest {
21
+ return isRecord(value) && typeof value.peer === "string" && typeof value.reply === "function";
22
+ }
@@ -5,6 +5,9 @@ import { hasErrorCode } from "./unknown.ts";
5
5
 
6
6
  const BIRTH_FIELDS = ["lstart=", "pgid="];
7
7
 
8
+ // macOS ships /bin/ps; resolving "ps" through PATH can spawn once per missing entry.
9
+ export const PS_COMMAND = process.platform === "darwin" ? "/bin/ps" : "ps";
10
+
8
11
  interface AgentProcessExit {
9
12
  readonly pid?: number;
10
13
  readonly code: number | null;
@@ -75,7 +78,7 @@ function psOptions(zone: string | undefined) {
75
78
 
76
79
  function readPsFields(pid: number, fields: readonly string[], zone?: string): string | undefined {
77
80
  try {
78
- return execFileSync("ps", psArguments(pid, fields), {
81
+ return execFileSync(PS_COMMAND, psArguments(pid, fields), {
79
82
  ...psOptions(zone),
80
83
  stdio: ["ignore", "pipe", "ignore"],
81
84
  })
@@ -93,7 +96,7 @@ function observePsFields(
93
96
  zone?: string,
94
97
  ): Promise<string | undefined> {
95
98
  return new Promise((resolve) => {
96
- execFile("ps", psArguments(pid, fields), psOptions(zone), (error, stdout) => {
99
+ execFile(PS_COMMAND, psArguments(pid, fields), psOptions(zone), (error, stdout) => {
97
100
  resolve(error ? undefined : stdout.trim().replace(/\s+/g, " "));
98
101
  });
99
102
  });