@fitchmultz/pi-subagents 0.50.0 → 0.52.0

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 (180) hide show
  1. package/CHANGELOG.md +63 -2
  2. package/README.md +36 -5
  3. package/dist/agent/child-runtime.js +80 -0
  4. package/dist/agent/contracts.js +15 -24
  5. package/dist/agent/host.js +3 -16
  6. package/dist/agent/index.js +212 -192
  7. package/dist/agent/journal-tail.js +115 -156
  8. package/dist/agent/launch.js +20 -8
  9. package/dist/agent/legacy.js +65 -23
  10. package/dist/agent/notifications.js +138 -47
  11. package/dist/agent/observation-state.js +246 -0
  12. package/dist/agent/observation-view.js +258 -0
  13. package/dist/agent/observation.js +350 -0
  14. package/dist/agent/observe-card.js +243 -0
  15. package/dist/agent/observe-chrome.js +85 -0
  16. package/dist/agent/observe-client.js +285 -0
  17. package/dist/agent/observe-layout.js +65 -0
  18. package/dist/agent/observe-reading.js +84 -0
  19. package/dist/agent/observe-screen.js +400 -96
  20. package/dist/agent/observe-transcript.js +382 -0
  21. package/dist/agent/observe.js +250 -43
  22. package/dist/agent/parking.js +253 -0
  23. package/dist/agent/process.js +228 -28
  24. package/dist/agent/receipts.js +92 -20
  25. package/dist/agent/runner.js +229 -126
  26. package/dist/agent/shell.js +2 -1
  27. package/dist/agent/storage.js +47 -12
  28. package/dist/agent/tool-schemas.js +20 -0
  29. package/dist/agent/typebox.js +10 -5
  30. package/dist/agent/usage.js +0 -115
  31. package/dist/agent/view.js +46 -16
  32. package/dist/agent/wake.js +99 -0
  33. package/dist/build-info.js +1 -1
  34. package/dist/intercom-client.d.ts +26 -0
  35. package/dist/intercom-client.js +45 -0
  36. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  37. package/dist/pi-intercom/bridge-session.js +2 -1
  38. package/dist/pi-intercom/broker/broker.js +62 -43
  39. package/dist/pi-intercom/broker/client-last.d.ts +24 -0
  40. package/dist/pi-intercom/broker/client-last.js +42 -5
  41. package/dist/pi-intercom/broker/client-park.d.ts +9 -0
  42. package/dist/pi-intercom/broker/client-park.js +21 -0
  43. package/dist/pi-intercom/broker/client.d.ts +59 -0
  44. package/dist/pi-intercom/broker/client.js +55 -29
  45. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  46. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  47. package/dist/pi-intercom/broker/framing.js +10 -7
  48. package/dist/pi-intercom/broker/guard.js +23 -0
  49. package/dist/pi-intercom/broker/last.d.ts +30 -0
  50. package/dist/pi-intercom/broker/last.js +87 -40
  51. package/dist/pi-intercom/broker/parked.js +107 -0
  52. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  53. package/dist/pi-intercom/broker/paths.js +35 -12
  54. package/dist/pi-intercom/broker/pid.js +5 -1
  55. package/dist/pi-intercom/broker/server-connection.js +20 -1
  56. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  57. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  58. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  59. package/dist/pi-intercom/connection.js +71 -20
  60. package/dist/pi-intercom/identity.js +6 -5
  61. package/dist/pi-intercom/inbound-delivery.js +16 -60
  62. package/dist/pi-intercom/inbound-journal.js +83 -6
  63. package/dist/pi-intercom/inbound-record.js +18 -1
  64. package/dist/pi-intercom/inbound.js +22 -37
  65. package/dist/pi-intercom/index.js +41 -0
  66. package/dist/pi-intercom/last-message.js +4 -16
  67. package/dist/pi-intercom/lifecycle.js +88 -3
  68. package/dist/pi-intercom/outbound.js +5 -15
  69. package/dist/pi-intercom/reply-tracker.js +7 -6
  70. package/dist/pi-intercom/reply-wait.js +2 -2
  71. package/dist/pi-intercom/session-targets.d.ts +24 -0
  72. package/dist/pi-intercom/tool-registration.js +4 -1
  73. package/dist/pi-intercom/tool-results.js +5 -7
  74. package/dist/pi-intercom/tool-schemas.js +12 -26
  75. package/dist/pi-intercom/types.d.ts +212 -0
  76. package/dist/pi-intercom/types.js +54 -3
  77. package/dist/pi-intercom/validation.d.ts +12 -0
  78. package/dist/pi-intercom/validation.js +3 -0
  79. package/dist/shared/bounded-json.js +151 -0
  80. package/dist/shared/compact-cards.js +66 -0
  81. package/dist/shared/journal-frames.js +3 -6
  82. package/dist/shared/json-projection.js +267 -129
  83. package/dist/shared/native-journal.js +123 -56
  84. package/dist/shared/parking.js +12 -0
  85. package/dist/shared/post-exit-stdio-guard.js +120 -7
  86. package/dist/shared/tui-action.js +29 -0
  87. package/dist/shared/unknown.d.ts +9 -0
  88. package/docs/code-quality.md +5 -3
  89. package/docs/development.md +42 -3
  90. package/docs/intercom-bridge.md +9 -4
  91. package/docs/intercom.md +228 -7
  92. package/docs/performance.md +118 -14
  93. package/docs/reference.md +168 -20
  94. package/docs/retired-history-maintenance.md +252 -0
  95. package/package.json +28 -15
  96. package/scripts/archive-retired-history.mjs +464 -0
  97. package/scripts/build.mjs +48 -28
  98. package/scripts/measure-size.mjs +1 -1
  99. package/scripts/native-package-smoke.mjs +153 -12
  100. package/scripts/package-smoke.mjs +80 -11
  101. package/scripts/prepare.mjs +2 -2
  102. package/scripts/real-pi-live.mjs +377 -0
  103. package/scripts/real-pi-processes.mjs +6 -1
  104. package/scripts/real-pi-smoke.mjs +6 -282
  105. package/scripts/run-tests.mjs +4 -2
  106. package/scripts/typescript.mjs +35 -0
  107. package/skills/pi-intercom/SKILL.md +2 -1
  108. package/skills/pi-subagents/SKILL.md +23 -5
  109. package/src/agent/child-runtime.ts +105 -0
  110. package/src/agent/contracts.ts +18 -34
  111. package/src/agent/host.ts +39 -20
  112. package/src/agent/index.ts +234 -207
  113. package/src/agent/journal-tail.ts +141 -153
  114. package/src/agent/launch.ts +33 -9
  115. package/src/agent/legacy.ts +85 -31
  116. package/src/agent/notifications.ts +192 -67
  117. package/src/agent/observation-state.ts +277 -0
  118. package/src/agent/observation-view.ts +267 -0
  119. package/src/agent/observation.ts +409 -0
  120. package/src/agent/observe-card.ts +304 -0
  121. package/src/agent/observe-chrome.ts +170 -0
  122. package/src/agent/observe-client.ts +318 -0
  123. package/src/agent/observe-layout.ts +79 -0
  124. package/src/agent/observe-reading.ts +97 -0
  125. package/src/agent/observe-screen.ts +443 -129
  126. package/src/agent/observe-transcript.ts +434 -0
  127. package/src/agent/observe.ts +304 -71
  128. package/src/agent/parking.ts +341 -0
  129. package/src/agent/process.ts +280 -37
  130. package/src/agent/receipts.ts +114 -24
  131. package/src/agent/runner.ts +227 -149
  132. package/src/agent/shell.ts +2 -1
  133. package/src/agent/storage.ts +44 -11
  134. package/src/agent/tool-schemas.ts +33 -0
  135. package/src/agent/typebox.ts +12 -7
  136. package/src/agent/usage.ts +0 -130
  137. package/src/agent/view.ts +70 -20
  138. package/src/agent/wake.ts +107 -0
  139. package/src/intercom-client.ts +108 -0
  140. package/src/pi-intercom/bridge-session.ts +2 -2
  141. package/src/pi-intercom/broker/broker.ts +64 -53
  142. package/src/pi-intercom/broker/client-last.ts +65 -12
  143. package/src/pi-intercom/broker/client-park.ts +23 -0
  144. package/src/pi-intercom/broker/client.ts +63 -45
  145. package/src/pi-intercom/broker/framing.ts +11 -8
  146. package/src/pi-intercom/broker/guard.ts +33 -0
  147. package/src/pi-intercom/broker/last.ts +131 -43
  148. package/src/pi-intercom/broker/parked.ts +130 -0
  149. package/src/pi-intercom/broker/paths.ts +39 -12
  150. package/src/pi-intercom/broker/pid.ts +6 -1
  151. package/src/pi-intercom/broker/server-connection.ts +27 -2
  152. package/src/pi-intercom/connection.ts +103 -39
  153. package/src/pi-intercom/identity.ts +8 -5
  154. package/src/pi-intercom/inbound-delivery.ts +20 -66
  155. package/src/pi-intercom/inbound-journal.ts +103 -7
  156. package/src/pi-intercom/inbound-record.ts +21 -4
  157. package/src/pi-intercom/inbound.ts +32 -49
  158. package/src/pi-intercom/index.ts +44 -0
  159. package/src/pi-intercom/last-message.ts +11 -20
  160. package/src/pi-intercom/lifecycle.ts +117 -14
  161. package/src/pi-intercom/outbound.ts +7 -16
  162. package/src/pi-intercom/reply-tracker.ts +8 -6
  163. package/src/pi-intercom/reply-wait.ts +3 -2
  164. package/src/pi-intercom/runtime-types.ts +0 -1
  165. package/src/pi-intercom/tool-registration.ts +4 -1
  166. package/src/pi-intercom/tool-results.ts +10 -9
  167. package/src/pi-intercom/tool-schemas.ts +12 -44
  168. package/src/pi-intercom/types.ts +115 -4
  169. package/src/pi-intercom/validation.ts +4 -0
  170. package/src/shared/bounded-json.ts +162 -0
  171. package/src/shared/compact-cards.ts +118 -0
  172. package/src/shared/journal-frames.ts +3 -19
  173. package/src/shared/json-projection.ts +267 -147
  174. package/src/shared/native-journal.ts +157 -75
  175. package/src/shared/parking.ts +22 -0
  176. package/src/shared/post-exit-stdio-guard.ts +165 -7
  177. package/src/shared/tui-action.ts +39 -0
  178. package/tsconfig.client.json +8 -0
  179. package/tsconfig.json +1 -0
  180. package/src/shared/stream-json.d.ts +0 -10
@@ -1,12 +1,8 @@
1
1
  import * as fs from "node:fs";
2
- const READ_BYTES = 65536;
3
- const LINE_BYTES = 32768;
4
- const KEEP_LINES = 80;
5
- function record(value) {
6
- return typeof value === "object" && value !== null && !Array.isArray(value);
7
- }
2
+ import { NativeJournal } from "../shared/native-journal.js";
3
+ import { nativeProjection } from "../shared/journal-projections.js";
4
+ import { isRecord, recordAt } from "../shared/unknown.js";
8
5
  export function plainText(value, length = 512) {
9
- // Journal content is untrusted terminal input, never terminal control sequences.
10
6
  return Array.from(value.slice(0, length))
11
7
  .map((char) => {
12
8
  const code = char.codePointAt(0) ?? 0;
@@ -14,170 +10,133 @@ export function plainText(value, length = 512) {
14
10
  })
15
11
  .join("");
16
12
  }
17
- function contentLines(content) {
18
- if (typeof content === "string") {
19
- return [plainText(content)];
20
- }
21
- if (!Array.isArray(content)) {
22
- return [];
23
- }
24
- return content.slice(0, 12).flatMap((block) => {
25
- if (!record(block)) {
26
- return [];
27
- }
28
- if (block.type === "text" && typeof block.text === "string") {
29
- return [plainText(block.text)];
30
- }
31
- if (block.type === "toolCall" && typeof block.name === "string") {
32
- return [`tool ${plainText(block.name, 80)} ${plainText(JSON.stringify(block.arguments))}`];
33
- }
34
- return block.type === "thinking"
35
- ? []
36
- : [`[${typeof block.type === "string" ? plainText(block.type, 40) : "non-text"} omitted]`];
37
- });
38
- }
39
- function activity(value) {
40
- if (!record(value) || value.type !== "message" || !record(value.message)) {
41
- return [];
13
+ const PAGE_CHARS = 32 * 1024;
14
+ const callFields = new Set(["type", "id", "name"]);
15
+ const textFields = new Set(["text", "thinking"]);
16
+ const metadata = (keys, root) => {
17
+ if (keys[0] === "message" &&
18
+ keys[1] === "content" &&
19
+ keys.length > 3 &&
20
+ !callFields.has(String(keys[3]))) {
21
+ return false;
42
22
  }
43
- const message = value.message;
44
- if (message.role === "assistant") {
45
- const lines = contentLines(message.content).map((line) => `assistant: ${line}`);
46
- if (typeof message.errorMessage === "string") {
47
- lines.push(`assistant error: ${plainText(message.errorMessage)}`);
48
- }
49
- return lines;
23
+ return nativeProjection(keys, root);
24
+ };
25
+ function contentIndex(keys) {
26
+ if (keys[0] !== "message" || keys[1] !== "content") {
27
+ return undefined;
50
28
  }
51
- if (message.role === "toolResult") {
52
- const label = message.isError === true ? "tool error" : "tool result";
53
- const name = typeof message.toolName === "string" ? plainText(message.toolName, 80) : "unknown";
54
- return [`${label} ${name}:`, ...contentLines(message.content)];
29
+ if (keys.length === 2) {
30
+ return -1;
55
31
  }
56
- return [];
32
+ return typeof keys[2] === "number" && textFields.has(String(keys[3])) ? keys[2] : undefined;
57
33
  }
58
- /** A lossy display tail; the native journal remains the complete source of truth. */
59
- export class JournalTail {
60
- lines = [];
61
- notice = "Waiting for published native activity.";
62
- offset = 0;
63
- partial = Buffer.alloc(0);
64
- dropping = false;
65
- identity = "";
66
- stamp = 0;
67
- file;
68
- sessionId;
69
- constructor(file, sessionId) {
70
- this.file = file;
71
- this.sessionId = sessionId;
72
- }
73
- reset(size) {
74
- this.lines.length = 0;
75
- this.offset = 0;
76
- this.partial = Buffer.alloc(0);
77
- this.dropping = false;
78
- if (size > READ_BYTES) {
79
- this.skip(size);
80
- }
34
+ class TextPage {
35
+ chars = 0;
36
+ carry;
37
+ text = new Map();
38
+ start;
39
+ constructor(page) {
40
+ this.start = page * PAGE_CHARS;
81
41
  }
82
- skip(size) {
83
- this.offset = size - READ_BYTES;
84
- this.partial = Buffer.alloc(0);
85
- this.dropping = true;
86
- this.lines.push("[Earlier native activity omitted; full journal retained.]");
87
- }
88
- header(fd) {
89
- const head = Buffer.alloc(4096);
90
- const count = fs.readSync(fd, head, 0, head.length, 0);
91
- const lf = head.indexOf(10, 0);
92
- if (lf < 0 || lf >= count) {
93
- throw new Error("Native journal header is incomplete or too large.");
94
- }
95
- const value = JSON.parse(head.subarray(0, lf).toString("utf8"));
96
- if (!record(value) || value.type !== "session" || value.id !== this.sessionId) {
97
- throw new Error("Native journal identity does not match this child.");
42
+ add(index, chunk) {
43
+ const previous = this.carry;
44
+ this.carry = undefined;
45
+ if (previous && previous.index !== index) {
46
+ this.take(previous.index, previous.text);
47
+ }
48
+ const text = previous?.index === index ? previous.text + chunk : chunk;
49
+ const last = text.charCodeAt(text.length - 1);
50
+ const pending = last >= 0xd800 && last <= 0xdbff;
51
+ this.take(index, pending ? text.slice(0, -1) : text);
52
+ if (pending) {
53
+ this.carry = { index, text: text.slice(-1) };
98
54
  }
99
55
  }
100
- line(line) {
101
- if (line.length > LINE_BYTES) {
102
- this.lines.push("[Large native entry omitted; read the full journal.]");
103
- return;
104
- }
105
- try {
106
- const value = JSON.parse(line.toString("utf8"));
107
- this.lines.push(...activity(value));
108
- }
109
- catch {
110
- this.lines.push("[Malformed published native entry omitted.]");
111
- }
56
+ take(index, text) {
57
+ const points = Array.from(text);
58
+ const from = Math.max(0, this.start - this.chars);
59
+ const to = Math.min(points.length, this.start + PAGE_CHARS - this.chars);
60
+ if (to > from) {
61
+ this.text.set(index, (this.text.get(index) ?? "") + points.slice(from, to).join(""));
62
+ }
63
+ this.chars += points.length;
112
64
  }
113
- consume(chunk) {
114
- const bytes = Buffer.concat([this.partial, chunk]);
115
- let start = 0;
116
- let lf = bytes.indexOf(10);
117
- while (lf >= 0) {
118
- if (!this.dropping) {
119
- this.line(bytes.subarray(start, lf));
65
+ finish(value, page) {
66
+ if (this.carry) {
67
+ this.take(this.carry.index, this.carry.text);
68
+ }
69
+ const message = { ...recordAt(value, "message") };
70
+ if (typeof message.content === "string") {
71
+ message.content = this.text.get(-1) ?? "";
72
+ }
73
+ if (Array.isArray(message.content)) {
74
+ for (const [index, block] of message.content.entries()) {
75
+ if (isRecord(block) && typeof block.type === "string" && textFields.has(block.type)) {
76
+ block[block.type] = this.text.get(index) ?? "";
77
+ }
120
78
  }
121
- this.dropping = false;
122
- start = lf + 1;
123
- lf = bytes.indexOf(10, start);
124
- }
125
- this.partial = bytes.subarray(start);
126
- if (this.partial.length > LINE_BYTES) {
127
- this.partial = Buffer.alloc(0);
128
- this.dropping = true;
129
- this.lines.push("[Large native entry omitted; read the full journal.]");
130
79
  }
131
- this.lines.splice(0, Math.max(0, this.lines.length - KEEP_LINES));
132
- if (this.partial.length > 0) {
133
- this.notice = "Waiting for newline publication of the latest native entry.";
80
+ return { message, page, pages: Math.max(1, Math.ceil(this.chars / PAGE_CHARS)) };
81
+ }
82
+ }
83
+ /** Native indexing owns ancestry and publication; only requested message pages are hydrated. */
84
+ export class ConversationJournal {
85
+ native;
86
+ branch;
87
+ constructor(file, sessionId) {
88
+ const stat = fs.lstatSync(file);
89
+ if (!stat.isFile() || stat.isSymbolicLink()) {
90
+ throw new Error("Child journal is not a regular file.");
91
+ }
92
+ this.native = new NativeJournal(file, "live", true, {
93
+ readBytes: 256 * 1024,
94
+ select: metadata,
95
+ });
96
+ if (this.native.records[0]?.value.id !== sessionId) {
97
+ throw new Error("Native journal identity does not match this child.");
134
98
  }
135
99
  }
136
- changed(stat, identity) {
137
- return (identity !== this.identity ||
138
- stat.size < this.offset ||
139
- (stat.size === this.offset && stat.mtimeMs !== this.stamp));
100
+ entries(requested) {
101
+ const leaf = typeof requested === "string" && !this.native.byId.has(requested) ? undefined : requested;
102
+ if (this.branch?.length !== this.native.records.length || this.branch.leaf !== leaf) {
103
+ this.branch = {
104
+ length: this.native.records.length,
105
+ leaf,
106
+ entries: this.native.branch(leaf).filter((entry) => entry.value.type === "message"),
107
+ };
108
+ }
109
+ return this.branch.entries;
140
110
  }
141
- observe(fd, stat) {
142
- const identity = `${stat.dev}:${stat.ino}`;
143
- if (this.changed(stat, identity)) {
144
- this.reset(stat.size);
145
- }
146
- else if (stat.size - this.offset > READ_BYTES) {
147
- this.skip(stat.size);
148
- }
149
- this.identity = identity;
150
- this.stamp = stat.mtimeMs;
151
- const chunk = Buffer.alloc(Math.min(READ_BYTES, stat.size - this.offset));
152
- const count = fs.readSync(fd, chunk, 0, chunk.length, this.offset);
153
- this.offset += count;
154
- this.notice =
155
- this.offset > READ_BYTES ? "Recent journal tail only; full journal retained." : "";
156
- this.consume(chunk.subarray(0, count));
157
- if (this.lines.length === 0 && this.notice.length === 0) {
158
- this.notice = "Waiting for published native activity.";
159
- }
111
+ async call(id, result, signal) {
112
+ const contains = (entry) => {
113
+ const content = recordAt(entry.value, "message")?.content;
114
+ return (Array.isArray(content) &&
115
+ content.some((part) => isRecord(part) && part.type === "toolCall" && part.id === id));
116
+ };
117
+ const entries = this.branch?.entries ?? [];
118
+ const entry = entries
119
+ .slice(0, entries.findIndex((item) => item.value.id === result))
120
+ .findLast(contains);
121
+ const content = entry && (await this.page(entry, 0, { signal })).message.content;
122
+ return Array.isArray(content)
123
+ ? content.find((part) => isRecord(part) && part.type === "toolCall" && part.id === id)
124
+ : undefined;
160
125
  }
161
- poll() {
162
- let fd;
163
- try {
164
- fd = fs.openSync(this.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
165
- const stat = fs.fstatSync(fd);
166
- if (!stat.isFile()) {
167
- throw new Error("Native journal is not a regular file.");
126
+ async page(entry, page = 0, options = {}) {
127
+ const text = new TextPage(page);
128
+ const value = await this.native.project(entry, (keys, root) => {
129
+ if (contentIndex(keys) !== undefined && keys.length > 2) {
130
+ return 0;
168
131
  }
169
- this.header(fd);
170
- this.observe(fd, stat);
171
- }
172
- catch (error) {
173
- this.lines.length = 0;
174
- this.identity = "";
175
- this.notice = error instanceof Error ? plainText(error.message) : "Journal unavailable.";
176
- }
177
- finally {
178
- if (fd !== undefined) {
179
- fs.closeSync(fd);
132
+ return nativeProjection(keys, root);
133
+ }, (keys, chunk) => {
134
+ const index = contentIndex(keys);
135
+ if (index === undefined || (options.hideThinking === true && keys[3] === "thinking")) {
136
+ return;
180
137
  }
181
- }
138
+ text.add(index, chunk);
139
+ }, options.signal);
140
+ return text.finish(value, page);
182
141
  }
183
142
  }
@@ -3,22 +3,34 @@ import * as path from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { loadSDK, packageRoot, runtimeEntry } from "./host.js";
5
5
  import { runDirectory, write } from "./storage.js";
6
- function selectedModel(input, context) {
6
+ function selectedModel(input, context, thinking, sdk) {
7
7
  if (input.model === undefined) {
8
8
  if (!context.model) {
9
9
  throw new Error("No selected model; no fallback was used.");
10
10
  }
11
- return context.model;
11
+ return { model: context.model, thinking };
12
12
  }
13
13
  const slash = input.model.indexOf("/");
14
14
  if (slash < 1) {
15
15
  throw new Error("model must be an exact provider/model ID.");
16
16
  }
17
17
  const model = context.modelRegistry.find(input.model.slice(0, slash), input.model.slice(slash + 1));
18
- if (!model) {
18
+ if (model) {
19
+ return { model, thinking };
20
+ }
21
+ if (typeof sdk.resolveModelReference !== "function") {
22
+ throw new Error(`Model ${input.model} is unavailable; no fallback was used. Check the provider/model ID. This host supports literal IDs only. If you intended a thinking suffix, update Pi for native thinking-suffix support.`);
23
+ }
24
+ const resolved = sdk.resolveModelReference({
25
+ text: input.model,
26
+ provider: input.model.slice(0, slash),
27
+ thinkingSuffix: "strict",
28
+ fuzzy: false,
29
+ }, context.modelRegistry.getAll(), () => true);
30
+ if (resolved.status !== "resolved") {
19
31
  throw new Error(`Model ${input.model} is unavailable; no fallback was used.`);
20
32
  }
21
- return model;
33
+ return { model: resolved.model, thinking: resolved.thinkingLevel ?? thinking };
22
34
  }
23
35
  function targetTrust(sdk, cwd, context, agentDir) {
24
36
  if (fs.realpathSync(cwd) === fs.realpathSync(context.cwd)) {
@@ -43,7 +55,7 @@ export async function prepareLaunch(options) {
43
55
  if (!fs.statSync(cwd).isDirectory()) {
44
56
  throw new Error("Delegation cwd is not a directory.");
45
57
  }
46
- const model = selectedModel(input, context);
58
+ const selection = selectedModel(input, context, thinking, sdk);
47
59
  const trusted = targetTrust(sdk, cwd, context, agentDir);
48
60
  const id = randomUUID();
49
61
  const directory = runDirectory(base, id);
@@ -73,9 +85,9 @@ export async function prepareLaunch(options) {
73
85
  sessionId: manager.getSessionId(),
74
86
  prompt: input.prompt,
75
87
  context: input.context ?? "fresh",
76
- provider: model.provider,
77
- model: model.id,
78
- thinking,
88
+ provider: selection.model.provider,
89
+ model: selection.model.id,
90
+ thinking: selection.thinking,
79
91
  trusted,
80
92
  name: input.name,
81
93
  createdAt: Date.now(),
@@ -4,31 +4,45 @@ import { journalStamp } from "../shared/native-journal.js";
4
4
  import { receiptId } from "./receipts.js";
5
5
  import { resolveTempRootDir } from "../shared/types.js";
6
6
  import { isRecord } from "../shared/unknown.js";
7
- import { hold, release, isHeld } from "./notifications.js";
7
+ import { release, isHeld } from "./notifications.js";
8
8
  import { missing } from "./storage.js";
9
- function legacyInput(file, owner) {
9
+ import { errorText } from "./host.js";
10
+ // Retain only foreign classifications, never their saved output, for each live native manager.
11
+ const ignoredInputs = new WeakMap();
12
+ function fileStamp(file) {
10
13
  const stat = fs.lstatSync(file, { bigint: true });
11
- if (!stat.isFile() || stat.isSymbolicLink()) {
14
+ return stat.isFile() && !stat.isSymbolicLink() ? journalStamp(stat) : undefined;
15
+ }
16
+ function legacyInput(file, scan) {
17
+ const stamp = fileStamp(file);
18
+ if (stamp === undefined) {
19
+ return;
20
+ }
21
+ if (scan.known?.get(file) === stamp) {
22
+ scan.ignored.set(file, stamp);
12
23
  return;
13
24
  }
14
25
  const raw = fs.readFileSync(file, "utf8");
15
26
  const data = JSON.parse(raw);
16
- if (!isRecord(data) || data.sessionId !== owner) {
27
+ if (!isRecord(data) || data.sessionId !== scan.owner) {
28
+ if (fileStamp(file) === stamp) {
29
+ scan.ignored.set(file, stamp);
30
+ }
17
31
  return;
18
32
  }
33
+ return ownedInput({ file, raw, stamp, owner: scan.owner }, data);
34
+ }
35
+ function ownedInput(source, data) {
19
36
  const id = typeof data.runId === "string" ? data.runId : data.id;
20
- if (typeof id !== "string" || path.basename(file, ".json") !== id) {
37
+ if (typeof id !== "string" || path.basename(source.file, ".json") !== id) {
21
38
  return;
22
39
  }
23
40
  const legacy = `legacy:${id}:${typeof data.timestamp === "number" ? data.timestamp : "unknown"}`;
24
41
  const completionId = typeof data.completionId === "string" ? data.completionId : legacy;
25
42
  return {
26
- file,
27
- raw,
28
- stamp: journalStamp(stat),
43
+ ...source,
29
44
  id,
30
45
  key: `completion:${completionId}`,
31
- owner,
32
46
  single: data.mode === "single",
33
47
  };
34
48
  }
@@ -81,7 +95,7 @@ function published(ctx, input) {
81
95
  return (receiptId(ctx.sessionManager.getSessionFile(), input.owner, (body) => receipt(body, input)) !==
82
96
  undefined);
83
97
  }
84
- function deliver(pi, ctx, input) {
98
+ function notification(ctx, input) {
85
99
  const identity = `legacy:${input.id}:${input.key}`;
86
100
  if (published(ctx, input)) {
87
101
  release(input.owner, identity);
@@ -95,24 +109,51 @@ function deliver(pi, ctx, input) {
95
109
  if (isHeld(input.owner, identity)) {
96
110
  return;
97
111
  }
98
- hold(input.owner, identity);
99
- try {
100
- pi.sendMessage({
112
+ return {
113
+ owner: input.owner,
114
+ id: identity,
115
+ message: {
101
116
  customType: "subagent-notify",
102
117
  display: true,
103
118
  // Full saved bytes, not a replacement summary. No owner/run record is invented.
104
119
  content: input.raw,
105
120
  details: { completion: { runId: input.id, key: input.key, ownerSessionId: input.owner } },
106
- }, { triggerTurn: true, deliverAs: "followUp" });
121
+ },
122
+ };
123
+ }
124
+ function legacyNotice(ctx, file, scan) {
125
+ try {
126
+ const input = legacyInput(file, scan);
127
+ return input ? notification(ctx, input) : undefined;
107
128
  }
108
129
  catch (error) {
109
- release(input.owner, identity);
110
- throw error;
130
+ console.error(`Skipped legacy completion ${file}: ${errorText(error)}`);
131
+ return;
111
132
  }
112
133
  }
113
- export function notifyLegacy(pi, ctx) {
114
- const root = resolveTempRootDir();
115
- const directory = path.join(root, "async-subagent-results");
134
+ function startScan(ctx, directory) {
135
+ const manager = ctx.sessionManager;
136
+ const owner = manager.getSessionId();
137
+ const prior = ignoredInputs.get(manager);
138
+ const scan = {
139
+ directory,
140
+ owner,
141
+ known: prior?.directory === directory && prior.owner === owner ? prior.ignored : undefined,
142
+ ignored: new Map(),
143
+ };
144
+ // Replace even on a failed directory scan; vanished/unsafe inputs keep no cached decision.
145
+ ignoredInputs.set(manager, { directory, owner, ignored: scan.ignored });
146
+ return scan;
147
+ }
148
+ /** Retired releases created this directory at session start, before any of their runners. */
149
+ export function legacyDirectory() {
150
+ return path.join(resolveTempRootDir(), "async-subagent-results");
151
+ }
152
+ export function legacyNotifications(ctx) {
153
+ const notices = [];
154
+ const directory = legacyDirectory();
155
+ const root = path.dirname(directory);
156
+ const scan = startScan(ctx, directory);
116
157
  let names;
117
158
  try {
118
159
  for (const candidate of [root, directory]) {
@@ -125,7 +166,7 @@ export function notifyLegacy(pi, ctx) {
125
166
  }
126
167
  catch (error) {
127
168
  if (missing(error)) {
128
- return;
169
+ return notices;
129
170
  }
130
171
  throw error;
131
172
  }
@@ -133,9 +174,10 @@ export function notifyLegacy(pi, ctx) {
133
174
  if (!name.endsWith(".json")) {
134
175
  continue;
135
176
  }
136
- const input = legacyInput(path.join(directory, name), ctx.sessionManager.getSessionId());
137
- if (input) {
138
- deliver(pi, ctx, input);
177
+ const notice = legacyNotice(ctx, path.join(directory, name), scan);
178
+ if (notice) {
179
+ notices.push(notice);
139
180
  }
140
181
  }
182
+ return notices;
141
183
  }