@fitchmultz/pi-subagents 0.50.0 → 0.51.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 (169) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +36 -5
  3. package/dist/agent/contracts.js +6 -24
  4. package/dist/agent/host.js +3 -16
  5. package/dist/agent/index.js +207 -173
  6. package/dist/agent/journal-tail.js +115 -156
  7. package/dist/agent/launch.js +20 -8
  8. package/dist/agent/legacy.js +65 -23
  9. package/dist/agent/notifications.js +138 -47
  10. package/dist/agent/observation-state.js +246 -0
  11. package/dist/agent/observation-view.js +258 -0
  12. package/dist/agent/observation.js +346 -0
  13. package/dist/agent/observe-card.js +243 -0
  14. package/dist/agent/observe-chrome.js +85 -0
  15. package/dist/agent/observe-client.js +285 -0
  16. package/dist/agent/observe-layout.js +65 -0
  17. package/dist/agent/observe-reading.js +84 -0
  18. package/dist/agent/observe-screen.js +400 -96
  19. package/dist/agent/observe-transcript.js +382 -0
  20. package/dist/agent/observe.js +250 -43
  21. package/dist/agent/process.js +198 -25
  22. package/dist/agent/receipts.js +92 -20
  23. package/dist/agent/runner.js +125 -20
  24. package/dist/agent/shell.js +2 -1
  25. package/dist/agent/storage.js +29 -12
  26. package/dist/agent/tool-schemas.js +20 -0
  27. package/dist/agent/typebox.js +10 -5
  28. package/dist/agent/usage.js +0 -115
  29. package/dist/agent/view.js +46 -16
  30. package/dist/agent/wake.js +99 -0
  31. package/dist/build-info.js +1 -1
  32. package/dist/intercom-client.d.ts +26 -0
  33. package/dist/intercom-client.js +45 -0
  34. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  35. package/dist/pi-intercom/bridge-session.js +2 -1
  36. package/dist/pi-intercom/broker/broker.js +27 -21
  37. package/dist/pi-intercom/broker/client-last.d.ts +13 -0
  38. package/dist/pi-intercom/broker/client-last.js +17 -5
  39. package/dist/pi-intercom/broker/client.d.ts +60 -0
  40. package/dist/pi-intercom/broker/client.js +47 -16
  41. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  42. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  43. package/dist/pi-intercom/broker/framing.js +10 -7
  44. package/dist/pi-intercom/broker/guard.js +23 -0
  45. package/dist/pi-intercom/broker/last.d.ts +26 -0
  46. package/dist/pi-intercom/broker/last.js +57 -32
  47. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  48. package/dist/pi-intercom/broker/paths.js +35 -12
  49. package/dist/pi-intercom/broker/pid.js +5 -1
  50. package/dist/pi-intercom/broker/server-connection.js +19 -0
  51. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  52. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  53. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  54. package/dist/pi-intercom/connection.js +10 -2
  55. package/dist/pi-intercom/identity.js +6 -5
  56. package/dist/pi-intercom/inbound-delivery.js +16 -60
  57. package/dist/pi-intercom/inbound-journal.js +83 -6
  58. package/dist/pi-intercom/inbound-record.js +18 -1
  59. package/dist/pi-intercom/inbound.js +12 -38
  60. package/dist/pi-intercom/index.js +3 -0
  61. package/dist/pi-intercom/last-message.js +4 -16
  62. package/dist/pi-intercom/lifecycle.js +88 -3
  63. package/dist/pi-intercom/outbound.js +5 -15
  64. package/dist/pi-intercom/reply-tracker.js +7 -6
  65. package/dist/pi-intercom/reply-wait.js +2 -2
  66. package/dist/pi-intercom/session-targets.d.ts +24 -0
  67. package/dist/pi-intercom/tool-registration.js +4 -1
  68. package/dist/pi-intercom/tool-results.js +5 -7
  69. package/dist/pi-intercom/tool-schemas.js +12 -26
  70. package/dist/pi-intercom/types.d.ts +212 -0
  71. package/dist/pi-intercom/types.js +54 -3
  72. package/dist/pi-intercom/validation.d.ts +12 -0
  73. package/dist/pi-intercom/validation.js +3 -0
  74. package/dist/shared/bounded-json.js +151 -0
  75. package/dist/shared/compact-cards.js +66 -0
  76. package/dist/shared/journal-frames.js +3 -6
  77. package/dist/shared/json-projection.js +267 -129
  78. package/dist/shared/native-journal.js +123 -56
  79. package/dist/shared/post-exit-stdio-guard.js +120 -7
  80. package/dist/shared/tui-action.js +29 -0
  81. package/dist/shared/unknown.d.ts +9 -0
  82. package/docs/code-quality.md +2 -0
  83. package/docs/development.md +40 -1
  84. package/docs/intercom-bridge.md +9 -4
  85. package/docs/intercom.md +226 -6
  86. package/docs/performance.md +87 -11
  87. package/docs/reference.md +157 -20
  88. package/docs/retired-history-maintenance.md +252 -0
  89. package/package.json +28 -15
  90. package/scripts/archive-retired-history.mjs +464 -0
  91. package/scripts/build.mjs +48 -28
  92. package/scripts/measure-size.mjs +1 -1
  93. package/scripts/native-package-smoke.mjs +153 -12
  94. package/scripts/package-smoke.mjs +80 -11
  95. package/scripts/prepare.mjs +2 -2
  96. package/scripts/real-pi-live.mjs +377 -0
  97. package/scripts/real-pi-processes.mjs +6 -1
  98. package/scripts/real-pi-smoke.mjs +6 -282
  99. package/scripts/run-tests.mjs +4 -2
  100. package/scripts/typescript.mjs +35 -0
  101. package/skills/pi-intercom/SKILL.md +2 -1
  102. package/skills/pi-subagents/SKILL.md +23 -5
  103. package/src/agent/contracts.ts +8 -34
  104. package/src/agent/host.ts +19 -20
  105. package/src/agent/index.ts +224 -190
  106. package/src/agent/journal-tail.ts +141 -153
  107. package/src/agent/launch.ts +33 -9
  108. package/src/agent/legacy.ts +85 -31
  109. package/src/agent/notifications.ts +192 -67
  110. package/src/agent/observation-state.ts +277 -0
  111. package/src/agent/observation-view.ts +267 -0
  112. package/src/agent/observation.ts +405 -0
  113. package/src/agent/observe-card.ts +304 -0
  114. package/src/agent/observe-chrome.ts +170 -0
  115. package/src/agent/observe-client.ts +318 -0
  116. package/src/agent/observe-layout.ts +79 -0
  117. package/src/agent/observe-reading.ts +97 -0
  118. package/src/agent/observe-screen.ts +443 -129
  119. package/src/agent/observe-transcript.ts +434 -0
  120. package/src/agent/observe.ts +304 -71
  121. package/src/agent/process.ts +240 -34
  122. package/src/agent/receipts.ts +114 -24
  123. package/src/agent/runner.ts +125 -27
  124. package/src/agent/shell.ts +2 -1
  125. package/src/agent/storage.ts +27 -11
  126. package/src/agent/tool-schemas.ts +33 -0
  127. package/src/agent/typebox.ts +12 -7
  128. package/src/agent/usage.ts +0 -130
  129. package/src/agent/view.ts +70 -20
  130. package/src/agent/wake.ts +107 -0
  131. package/src/intercom-client.ts +108 -0
  132. package/src/pi-intercom/bridge-session.ts +2 -2
  133. package/src/pi-intercom/broker/broker.ts +27 -23
  134. package/src/pi-intercom/broker/client-last.ts +25 -12
  135. package/src/pi-intercom/broker/client.ts +55 -33
  136. package/src/pi-intercom/broker/framing.ts +11 -8
  137. package/src/pi-intercom/broker/guard.ts +33 -0
  138. package/src/pi-intercom/broker/last.ts +86 -33
  139. package/src/pi-intercom/broker/paths.ts +39 -12
  140. package/src/pi-intercom/broker/pid.ts +6 -1
  141. package/src/pi-intercom/broker/server-connection.ts +25 -0
  142. package/src/pi-intercom/connection.ts +21 -10
  143. package/src/pi-intercom/identity.ts +8 -5
  144. package/src/pi-intercom/inbound-delivery.ts +20 -66
  145. package/src/pi-intercom/inbound-journal.ts +103 -7
  146. package/src/pi-intercom/inbound-record.ts +21 -4
  147. package/src/pi-intercom/inbound.ts +18 -49
  148. package/src/pi-intercom/index.ts +3 -0
  149. package/src/pi-intercom/last-message.ts +10 -19
  150. package/src/pi-intercom/lifecycle.ts +117 -14
  151. package/src/pi-intercom/outbound.ts +7 -16
  152. package/src/pi-intercom/reply-tracker.ts +8 -6
  153. package/src/pi-intercom/reply-wait.ts +3 -2
  154. package/src/pi-intercom/runtime-types.ts +0 -1
  155. package/src/pi-intercom/tool-registration.ts +4 -1
  156. package/src/pi-intercom/tool-results.ts +10 -9
  157. package/src/pi-intercom/tool-schemas.ts +12 -44
  158. package/src/pi-intercom/types.ts +115 -4
  159. package/src/pi-intercom/validation.ts +4 -0
  160. package/src/shared/bounded-json.ts +162 -0
  161. package/src/shared/compact-cards.ts +118 -0
  162. package/src/shared/journal-frames.ts +3 -19
  163. package/src/shared/json-projection.ts +267 -147
  164. package/src/shared/native-journal.ts +157 -75
  165. package/src/shared/post-exit-stdio-guard.ts +165 -7
  166. package/src/shared/tui-action.ts +39 -0
  167. package/tsconfig.client.json +8 -0
  168. package/tsconfig.json +1 -0
  169. package/src/shared/stream-json.d.ts +0 -10
@@ -1,42 +1,28 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import { Type } from "../agent/typebox.js";
3
3
  export const intercomSchema = Type.Object({
4
- action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "last"], {
5
- description: "List, send, ask, reply, pending asks, connection status, or last completed assistant response.",
6
- }),
4
+ action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "last"]),
7
5
  scope: Type.Optional(StringEnum(["project", "all"], {
8
- description: "For list/status: 'all' (default) includes every reachable session; 'project' further filters this repository and its worktrees.",
9
- })),
10
- to: Type.Optional(Type.String({
11
- description: "Target session name or ID (for 'send', 'ask', or disambiguating 'reply')",
12
- })),
13
- message: Type.Optional(Type.String({
14
- description: "Message for send/ask/reply",
6
+ description: "all (default) or project (repo and worktrees)",
15
7
  })),
8
+ to: Type.Optional(Type.String({ description: "Session name or ID" })),
9
+ message: Type.Optional(Type.String()),
16
10
  attachments: Type.Optional(Type.Array(Type.Object({
17
11
  type: StringEnum(["file", "snippet", "context"]),
18
12
  name: Type.String(),
19
13
  content: Type.String(),
20
14
  language: Type.Optional(Type.String()),
21
15
  }))),
22
- replyTo: Type.Optional(Type.String({
23
- description: "Message ID to reply to (for threading or responding to an 'ask')",
24
- })),
16
+ replyTo: Type.Optional(Type.String({ description: "Message ID to answer or thread" })),
25
17
  delivery: Type.Optional(StringEnum(["queue", "steer", "passive"], {
26
- description: "Delivery mode. Omitted send delivery defaults to 'steer', which injects after the current tool call. Use 'queue' only to intentionally wait behind active work; 'passive' does not wake the recipient model.",
18
+ description: "steer (default) at next tool boundary; queue after active work; passive never wakes the model",
27
19
  })),
28
20
  queueMode: Type.Optional(StringEnum(["stack", "replace"], {
29
- description: "For delivery='queue': 'stack' keeps all messages; 'replace' keeps only the latest undelivered message for the same threadId.",
30
- })),
31
- threadId: Type.Optional(Type.String({ description: "Stable topic key for queueMode='replace'." })),
32
- entryId: Type.Optional(Type.String({ description: "Last response entry ID for stable paging." })),
33
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "Last response character offset (UTF-16)." })),
34
- limit: Type.Optional(Type.Integer({
35
- minimum: 1,
36
- maximum: 16384,
37
- description: "Last response page size; default/max 16384 characters.",
38
- })),
39
- passive: Type.Optional(Type.Boolean({
40
- description: "For action='send' only: legacy alias for delivery='passive'. Discouraged for agent-to-agent messages.",
21
+ description: "stack keeps all; replace keeps only the latest per threadId",
41
22
  })),
23
+ threadId: Type.Optional(Type.String()),
24
+ entryId: Type.Optional(Type.String()),
25
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "UTF-16 characters" })),
26
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 16384 })),
27
+ passive: Type.Optional(Type.Boolean({ description: "Legacy alias for delivery=passive" })),
42
28
  });
@@ -0,0 +1,212 @@
1
+ export interface SessionInfo {
2
+ readonly id: string;
3
+ readonly name?: string;
4
+ readonly cwd: string;
5
+ readonly model: string;
6
+ /** Opaque same-repository/worktree identity used for ambient peer awareness. */
7
+ readonly projectId?: string;
8
+ readonly rootSessionId?: string;
9
+ readonly parentSessionId?: string;
10
+ readonly incarnation?: string;
11
+ readonly guardedDelivery?: 1;
12
+ readonly applicationGuard?: string;
13
+ readonly status?: string;
14
+ readonly lastSeen?: number;
15
+ readonly lastIntercomActivity?: number;
16
+ readonly pendingAsks?: number;
17
+ readonly acceptsAsks?: boolean;
18
+ readonly subscriptions?: readonly TopicSubscription[];
19
+ readonly topics?: readonly TopicUpdate[];
20
+ }
21
+ export interface TopicSubscription {
22
+ readonly topic: string;
23
+ readonly awaitRelease?: boolean;
24
+ }
25
+ export interface TopicUpdate {
26
+ readonly topic: string;
27
+ readonly text: string;
28
+ readonly event: "update" | "blocker" | "decision" | "release";
29
+ readonly resource?: string;
30
+ readonly ownership?: "held" | "released";
31
+ readonly revision: number;
32
+ readonly updatedAt: number;
33
+ }
34
+ export declare function isTopicSubscription(value: unknown): value is TopicSubscription;
35
+ export declare function isTopicUpdate(value: unknown): value is TopicUpdate;
36
+ export type MessageDelivery = "queue" | "steer" | "passive";
37
+ export type QueueMode = "stack" | "replace";
38
+ export interface ContextTarget {
39
+ readonly rootSessionId: string;
40
+ readonly peerId: string;
41
+ readonly incarnation: string;
42
+ }
43
+ export interface DeliveryGuard extends ContextTarget {
44
+ readonly applicationGuard: string;
45
+ }
46
+ export interface PublicationReplyHint {
47
+ readonly tool: string;
48
+ readonly commandId: string;
49
+ readonly commandFingerprint?: string;
50
+ }
51
+ export type SendOptions = Omit<Message, "id" | "timestamp" | "content"> & Message["content"] & {
52
+ readonly messageId?: string;
53
+ };
54
+ export declare function isContextTarget(value: unknown): value is ContextTarget;
55
+ export declare function isDeliveryGuard(value: unknown): value is DeliveryGuard;
56
+ export declare function isPublicationReplyHint(value: unknown): value is PublicationReplyHint;
57
+ export interface HumanMessageOrigin {
58
+ readonly ownerSessionId: string;
59
+ readonly runId: string;
60
+ readonly index: number;
61
+ }
62
+ export declare function isHumanMessageOrigin(value: unknown): value is HumanMessageOrigin;
63
+ export interface Message {
64
+ readonly id: string;
65
+ readonly timestamp: number;
66
+ readonly replyTo?: string;
67
+ readonly expectsReply?: boolean;
68
+ /** Recipients verify parent ownership before elevating human origin. */
69
+ readonly human?: HumanMessageOrigin;
70
+ readonly topic?: TopicUpdate;
71
+ readonly delivery?: MessageDelivery;
72
+ readonly queueMode?: QueueMode;
73
+ readonly threadId?: string;
74
+ readonly passive?: boolean;
75
+ readonly guard?: DeliveryGuard;
76
+ readonly replyHint?: PublicationReplyHint;
77
+ readonly content: {
78
+ readonly text: string;
79
+ readonly attachments?: readonly Attachment[];
80
+ };
81
+ }
82
+ export interface Attachment {
83
+ readonly type: "file" | "snippet" | "context";
84
+ readonly name: string;
85
+ readonly content: string;
86
+ readonly language?: string;
87
+ }
88
+ export declare function isAttachment(value: unknown): value is Attachment;
89
+ export declare function isMessage(value: unknown): value is Message;
90
+ export declare function isSessionRegistration(value: unknown): value is Omit<SessionInfo, "id">;
91
+ export declare function normalizeSessionInfo(value: unknown): SessionInfo | null;
92
+ export interface SendResult {
93
+ readonly id: string;
94
+ readonly accepted: boolean;
95
+ readonly delivered: boolean;
96
+ readonly queued?: boolean;
97
+ readonly reason?: string;
98
+ }
99
+ export type TopicChange = {
100
+ readonly action: "publish" | "restore";
101
+ readonly topic: TopicUpdate;
102
+ } | {
103
+ readonly action: "subscribe" | "restore";
104
+ readonly subscription: TopicSubscription;
105
+ } | {
106
+ readonly action: "unsubscribe";
107
+ readonly topic: string;
108
+ };
109
+ export interface SessionSnapshot {
110
+ readonly sessions: readonly SessionInfo[];
111
+ readonly receipts: readonly (SendResult & {
112
+ readonly to: string;
113
+ })[];
114
+ }
115
+ type CompactTopicMessage = Omit<Message, "topic"> & {
116
+ readonly topic?: Omit<TopicUpdate, "text"> & {
117
+ readonly text?: string;
118
+ };
119
+ };
120
+ /** Topic text and the standard coalescing key need only one copy on the wire. */
121
+ export declare function compactTopicMessage(message: Message): CompactTopicMessage;
122
+ export declare function normalizeMessage(value: unknown): Message | null;
123
+ export interface LastMessageQuery {
124
+ readonly entryId?: string;
125
+ readonly offset?: number;
126
+ readonly limit?: number;
127
+ }
128
+ export declare const LAST_MESSAGE_PAGE_LIMIT = 16384;
129
+ export declare function isLastMessageQuery(value: unknown): value is LastMessageQuery;
130
+ export type LastMessageResult = {
131
+ readonly session: SessionInfo;
132
+ readonly entryId: string | null;
133
+ readonly text: string;
134
+ readonly offset: number;
135
+ readonly totalLength: number;
136
+ readonly nextOffset?: number;
137
+ } | {
138
+ readonly error: string;
139
+ };
140
+ export interface ReceiptQuery {
141
+ readonly rootSessionId: string;
142
+ readonly messageId: string;
143
+ }
144
+ export declare function isReceiptQuery(value: unknown): value is ReceiptQuery;
145
+ export interface NativeReceipt extends ReceiptQuery {
146
+ readonly version: 1;
147
+ readonly sender: {
148
+ readonly peerId: string;
149
+ readonly rootSessionId?: string;
150
+ readonly incarnation?: string;
151
+ };
152
+ readonly textSha256: string;
153
+ readonly delivery: MessageDelivery;
154
+ readonly guard?: DeliveryGuard;
155
+ readonly replyHint?: PublicationReplyHint;
156
+ readonly checkpointEntryId: string;
157
+ readonly receivedAt: number;
158
+ /** Historical observations, not a promise that an input is still queued. */
159
+ readonly queued: boolean;
160
+ readonly nativeHandoff: boolean;
161
+ readonly placementEntryId: string | null;
162
+ readonly discarded: boolean;
163
+ }
164
+ export type ReceiptResult = {
165
+ readonly receipt: NativeReceipt;
166
+ } | {
167
+ readonly error: string;
168
+ };
169
+ export type BrokerMessage = {
170
+ readonly type: "registered";
171
+ readonly sessionId: string;
172
+ readonly topicsSupported?: true;
173
+ readonly topicFrames?: true;
174
+ readonly lastMessages?: true;
175
+ readonly receiptReads?: 1;
176
+ readonly guardedDelivery?: 1;
177
+ } | {
178
+ readonly type: "sessions";
179
+ readonly requestId: string;
180
+ readonly sessions: readonly (Partial<SessionInfo> & Pick<SessionInfo, "id">)[];
181
+ readonly more?: boolean;
182
+ readonly receipts?: SessionSnapshot["receipts"];
183
+ readonly error?: string;
184
+ } | {
185
+ readonly type: "message";
186
+ readonly from: SessionInfo;
187
+ readonly message: Message | CompactTopicMessage;
188
+ } | {
189
+ readonly type: "last_request";
190
+ readonly requestId: string;
191
+ readonly from: SessionInfo;
192
+ readonly query: LastMessageQuery | ReceiptQuery;
193
+ } | {
194
+ readonly type: "last_response";
195
+ readonly requestId: string;
196
+ readonly result: LastMessageResult | ReceiptResult;
197
+ } | {
198
+ readonly type: "session_left";
199
+ readonly sessionId: string;
200
+ } | {
201
+ readonly type: "delivered";
202
+ readonly messageId: string;
203
+ } | {
204
+ readonly type: "delivery_queued";
205
+ readonly messageId: string;
206
+ readonly reason: string;
207
+ } | {
208
+ readonly type: "delivery_failed";
209
+ readonly messageId: string;
210
+ readonly reason: string;
211
+ };
212
+ export {};
@@ -1,4 +1,4 @@
1
- import { finiteNumber, isRecord, isUnknownArray, optionalBoolean, optionalFiniteNumber, optionalLabel, safeLabel, safeText, } from "./validation.js";
1
+ import { finiteNumber, isRecord, isUnknownArray, nonNegativeInteger, optionalBoolean, optionalFiniteNumber, optionalLabel, safeLabel, safeText, } from "./validation.js";
2
2
  export function isTopicSubscription(value) {
3
3
  return (isRecord(value) &&
4
4
  typeof value.topic === "string" &&
@@ -30,6 +30,23 @@ export function isTopicUpdate(value) {
30
30
  finiteNumber(value.updatedAt) &&
31
31
  validTopicOwnership(value));
32
32
  }
33
+ export function isContextTarget(value) {
34
+ return (isRecord(value) &&
35
+ ["rootSessionId", "peerId", "incarnation"].every((key) => safeLabel(value[key])));
36
+ }
37
+ export function isDeliveryGuard(value) {
38
+ return isContextTarget(value) && "applicationGuard" in value && safeLabel(value.applicationGuard);
39
+ }
40
+ export function isPublicationReplyHint(value) {
41
+ return (isRecord(value) &&
42
+ typeof value.tool === "string" &&
43
+ /^[a-zA-Z_][a-zA-Z0-9_]{0,79}$/.test(value.tool) &&
44
+ typeof value.commandId === "string" &&
45
+ /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(value.commandId) &&
46
+ (value.commandFingerprint === undefined ||
47
+ (typeof value.commandFingerprint === "string" &&
48
+ /^[a-f0-9]{64}$/.test(value.commandFingerprint))));
49
+ }
33
50
  export function isHumanMessageOrigin(value) {
34
51
  if (!isRecord(value)) {
35
52
  return false;
@@ -50,6 +67,9 @@ export function isAttachment(value) {
50
67
  return (validType && safeLabel(value.name) && safeText(value.content) && optionalLabel(value.language));
51
68
  }
52
69
  function validMessageOrigin(message) {
70
+ if (!validMessageGuard(message)) {
71
+ return false;
72
+ }
53
73
  if (message.human !== undefined && !isHumanMessageOrigin(message.human)) {
54
74
  return false;
55
75
  }
@@ -61,6 +81,17 @@ function validMessageOrigin(message) {
61
81
  (message.expectsReply === undefined || message.expectsReply === false) &&
62
82
  message.replyTo === undefined);
63
83
  }
84
+ function validMessageGuard(message) {
85
+ if (message.guard === undefined) {
86
+ return message.replyHint === undefined;
87
+ }
88
+ return (isDeliveryGuard(message.guard) &&
89
+ message.expectsReply === false &&
90
+ message.replyTo === undefined &&
91
+ message.human === undefined &&
92
+ message.topic === undefined &&
93
+ (message.replyHint === undefined || isPublicationReplyHint(message.replyHint)));
94
+ }
64
95
  function validDelivery(message) {
65
96
  if (message.delivery !== undefined &&
66
97
  message.delivery !== "queue" &&
@@ -136,8 +167,11 @@ function validSessionTopics(session) {
136
167
  (isUnknownArray(session.topics) && session.topics.every(isTopicUpdate))));
137
168
  }
138
169
  function validSessionLineage(value) {
139
- return (optionalLabel(value.rootSessionId) &&
140
- optionalLabel(value.parentSessionId) &&
170
+ return (["rootSessionId", "parentSessionId", "incarnation", "applicationGuard"].every((key) => optionalLabel(value[key])) &&
171
+ (value.guardedDelivery === undefined ||
172
+ (value.guardedDelivery === 1 &&
173
+ safeLabel(value.incarnation) &&
174
+ safeLabel(value.rootSessionId))) &&
141
175
  (value.parentSessionId === undefined || value.rootSessionId === value.parentSessionId));
142
176
  }
143
177
  function validSessionProject(value) {
@@ -205,3 +239,20 @@ export function normalizeMessage(value) {
205
239
  };
206
240
  return isMessage(restored) ? restored : null;
207
241
  }
242
+ export const LAST_MESSAGE_PAGE_LIMIT = 16384;
243
+ function optionalEntryId(value) {
244
+ return value === undefined || (typeof value === "string" && value.length > 0);
245
+ }
246
+ export function isLastMessageQuery(value) {
247
+ return (isRecord(value) &&
248
+ value.messageId === undefined &&
249
+ optionalEntryId(value.entryId) &&
250
+ (value.offset === undefined || nonNegativeInteger(value.offset)) &&
251
+ (value.limit === undefined ||
252
+ (nonNegativeInteger(value.limit) &&
253
+ value.limit > 0 &&
254
+ value.limit <= LAST_MESSAGE_PAGE_LIMIT)));
255
+ }
256
+ export function isReceiptQuery(value) {
257
+ return isRecord(value) && [value.rootSessionId, value.messageId].every(safeLabel);
258
+ }
@@ -0,0 +1,12 @@
1
+ /** JSON objects are untrusted at the broker and journal boundaries. */
2
+ export declare function isRecord(value: unknown): value is Readonly<Record<string, unknown>>;
3
+ export declare function isUnknownArray(value: unknown): value is readonly unknown[];
4
+ export declare function errorMessage(error: unknown): string;
5
+ export declare function asError(error: unknown): Error;
6
+ export declare function safeText(value: unknown): boolean;
7
+ export declare function safeLabel(value: unknown): boolean;
8
+ export declare function optionalLabel(value: unknown): boolean;
9
+ export declare function optionalBoolean(value: unknown): boolean;
10
+ export declare function finiteNumber(value: unknown): value is number;
11
+ export declare function nonNegativeInteger(value: unknown): value is number;
12
+ export declare function optionalFiniteNumber(value: unknown): boolean;
@@ -54,6 +54,9 @@ export function optionalBoolean(value) {
54
54
  export function finiteNumber(value) {
55
55
  return typeof value === "number" && Number.isFinite(value);
56
56
  }
57
+ export function nonNegativeInteger(value) {
58
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
59
+ }
57
60
  export function optionalFiniteNumber(value) {
58
61
  return value === undefined || finiteNumber(value);
59
62
  }
@@ -0,0 +1,151 @@
1
+ import { types } from "node:util";
2
+ class Limit extends Error {
3
+ reason;
4
+ constructor(reason) {
5
+ super(reason);
6
+ this.reason = reason;
7
+ }
8
+ }
9
+ /**
10
+ * Proxies, accessors and custom serializers would run arbitrary code inside the actor; binary
11
+ * values allocate their full representation. None of them is read.
12
+ */
13
+ function opaque(value) {
14
+ return (types.isProxy(value) ||
15
+ ArrayBuffer.isView(value) ||
16
+ types.isAnyArrayBuffer(value) ||
17
+ "toJSON" in value);
18
+ }
19
+ /** An own data property's value, without invoking a getter. */
20
+ function data(value, key) {
21
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
22
+ if (descriptor !== undefined && !("value" in descriptor)) {
23
+ throw new Limit("unsupported");
24
+ }
25
+ return descriptor?.value;
26
+ }
27
+ /** Serialize like JSON.stringify within byte, node and depth limits, without first cloning. */
28
+ class Writer {
29
+ parts = [];
30
+ bytes = 0;
31
+ nodes = 0;
32
+ ancestors = new Set();
33
+ budget;
34
+ constructor(budget) {
35
+ this.budget = budget;
36
+ }
37
+ text() {
38
+ return this.parts.join("");
39
+ }
40
+ add(text, bytes = text.length) {
41
+ this.bytes += bytes;
42
+ if (this.bytes > this.budget.bytes) {
43
+ throw new Limit("oversize");
44
+ }
45
+ this.parts.push(text);
46
+ }
47
+ string(value) {
48
+ // A string never encodes to fewer bytes than code units; refuse before copying it.
49
+ if (value.length > this.budget.bytes - this.bytes) {
50
+ throw new Limit("oversize");
51
+ }
52
+ const json = JSON.stringify(value);
53
+ this.add(json, Buffer.byteLength(json));
54
+ }
55
+ /** Returns false for values JSON omits from objects and nulls in arrays. */
56
+ value(value, depth) {
57
+ if (++this.nodes > this.budget.nodes) {
58
+ throw new Limit("oversize");
59
+ }
60
+ switch (typeof value) {
61
+ case "string":
62
+ this.string(value);
63
+ return true;
64
+ case "number":
65
+ this.add(Number.isFinite(value) ? String(value) : "null");
66
+ return true;
67
+ case "boolean":
68
+ this.add(String(value));
69
+ return true;
70
+ case "bigint":
71
+ throw new Limit("unsupported");
72
+ case "object":
73
+ this.object(value, depth);
74
+ return true;
75
+ case "undefined":
76
+ case "function":
77
+ case "symbol":
78
+ return false;
79
+ }
80
+ }
81
+ object(value, depth) {
82
+ if (value === null) {
83
+ this.add("null");
84
+ return;
85
+ }
86
+ if (depth >= this.budget.depth) {
87
+ throw new Limit("oversize");
88
+ }
89
+ if (this.ancestors.has(value)) {
90
+ throw new Limit("cyclic");
91
+ }
92
+ if (types.isDate(value) && !types.isProxy(value)) {
93
+ const time = Date.prototype.getTime.call(value);
94
+ this.add(Number.isNaN(time) ? "null" : JSON.stringify(new Date(time).toISOString()));
95
+ return;
96
+ }
97
+ if (opaque(value)) {
98
+ throw new Limit("unsupported");
99
+ }
100
+ this.ancestors.add(value);
101
+ if (Array.isArray(value)) {
102
+ this.array(value, depth + 1);
103
+ }
104
+ else {
105
+ this.record(value, depth + 1);
106
+ }
107
+ this.ancestors.delete(value);
108
+ }
109
+ array(value, depth) {
110
+ this.add("[");
111
+ for (let index = 0; index < value.length; index++) {
112
+ if (index > 0) {
113
+ this.add(",");
114
+ }
115
+ if (!this.value(data(value, String(index)), depth)) {
116
+ this.add("null");
117
+ }
118
+ }
119
+ this.add("]");
120
+ }
121
+ record(value, depth) {
122
+ this.add("{");
123
+ let first = true;
124
+ for (const key of Object.keys(value)) {
125
+ const mark = { parts: this.parts.length, bytes: this.bytes };
126
+ if (!first) {
127
+ this.add(",");
128
+ }
129
+ this.string(key);
130
+ this.add(":");
131
+ if (this.value(data(value, key), depth)) {
132
+ first = false;
133
+ }
134
+ else {
135
+ this.parts.length = mark.parts;
136
+ this.bytes = mark.bytes;
137
+ }
138
+ }
139
+ this.add("}");
140
+ }
141
+ }
142
+ export function boundedJson(value, budget) {
143
+ const writer = new Writer(budget);
144
+ try {
145
+ return writer.value(value, 0) ? { json: writer.text() } : { unavailable: "unsupported" };
146
+ }
147
+ catch (error) {
148
+ // Getters, proxies and revoked proxies are untrusted tool data, not observer failures.
149
+ return { unavailable: error instanceof Limit ? error.reason : "unreadable" };
150
+ }
151
+ }
@@ -0,0 +1,66 @@
1
+ import { getMarkdownTheme, keyHint, truncateToVisualLines, } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Container, Markdown, Spacer, Text, truncateToWidth, } from "@earendil-works/pi-tui";
3
+ import { actionText } from "./tui-action.js";
4
+ function preview(content, expanded, theme, showHint = true) {
5
+ if (expanded) {
6
+ return content;
7
+ }
8
+ let cache;
9
+ return {
10
+ render(width) {
11
+ if (cache?.width !== width) {
12
+ const { visualLines, skippedCount } = truncateToVisualLines(content.render(width).join("\n"), 4, width, 0, "start");
13
+ const hint = theme.fg("dim", `… ${skippedCount} more lines${showHint ? ` · ${keyHint("app.tools.expand", "to expand")}` : ""}`);
14
+ cache = {
15
+ width,
16
+ lines: skippedCount > 0 ? [...visualLines, truncateToWidth(hint, width)] : visualLines,
17
+ };
18
+ }
19
+ return cache.lines;
20
+ },
21
+ invalidate() {
22
+ cache = undefined;
23
+ content.invalidate();
24
+ },
25
+ };
26
+ }
27
+ export const renderCompactResult = (result, { expanded }, theme) => preview(new Text(theme.fg("toolOutput", result.content
28
+ .filter((block) => block.type === "text")
29
+ .map((block) => block.text)
30
+ .join("\n")), 0, 0), expanded, theme);
31
+ export function registerCompactMessages(pi, types) {
32
+ let ui;
33
+ pi.on("session_start", (_event, ctx) => {
34
+ ui = ctx.mode === "tui" ? ctx.ui : undefined;
35
+ });
36
+ pi.on("session_shutdown", () => {
37
+ ui = undefined;
38
+ });
39
+ for (const type of types) {
40
+ pi.registerMessageRenderer(type, (message, { expanded, outputPad }, theme) => {
41
+ const text = typeof message.content === "string"
42
+ ? message.content
43
+ : message.content
44
+ .filter((block) => block.type === "text")
45
+ .map((block) => block.text)
46
+ .join("\n");
47
+ const box = new Box(outputPad, 1, (line) => theme.bg("customMessageBg", line));
48
+ const markdown = new Markdown(text, 0, 0, getMarkdownTheme(), {
49
+ color: (line) => theme.fg("customMessageText", line),
50
+ });
51
+ box.addChild(new (class extends Container {
52
+ render(width) {
53
+ this.clear();
54
+ const label = theme.fg("customMessageLabel", theme.bold(`[${type}]`));
55
+ this.addChild(markdown.render(width).length > 4
56
+ ? actionText(`${label} · ${keyHint("app.tools.expand", expanded ? "to collapse" : "to expand")}`, () => ui?.setToolsExpanded(!expanded))
57
+ : new Text(label, 0, 0));
58
+ return super.render(width);
59
+ }
60
+ })());
61
+ box.addChild(new Spacer(1));
62
+ box.addChild(preview(markdown, expanded, theme, false));
63
+ return box;
64
+ });
65
+ }
66
+ }
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import { JsonProjection, } from "./json-projection.js";
2
+ import { JsonProjection } from "./json-projection.js";
3
3
  import { errorDescription, isRecord } from "./unknown.js";
4
4
  /** Byte-framed JSONL. Publication framing is independent of malformed-record tolerance. */
5
5
  export class JournalFrames {
@@ -26,7 +26,7 @@ export class JournalFrames {
26
26
  this.projection = this.newProjection();
27
27
  }
28
28
  newProjection() {
29
- return new JsonProjection(this.select, this.options.stringChunk, this.options.keys, this.options.limits);
29
+ return new JsonProjection(this.select, this.options.stringChunk);
30
30
  }
31
31
  part(bytes) {
32
32
  if (!this.nonblank && bytes.some((byte) => ![9, 10, 13, 32].includes(byte))) {
@@ -76,10 +76,7 @@ export class JournalFrames {
76
76
  this.error = undefined;
77
77
  this.failed = false;
78
78
  this.nonblank = false;
79
- if (failed) {
80
- this.options.malformed?.(start, end, error);
81
- }
82
- else if (record) {
79
+ if (!failed && record) {
83
80
  this.record(record);
84
81
  }
85
82
  }