@fitchmultz/pi-subagents 0.47.0 → 0.49.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 (73) hide show
  1. package/CHANGELOG.md +29 -3
  2. package/dist/extension/build-info.js +1 -1
  3. package/dist/pi-intercom/bridge-broadcast.js +60 -0
  4. package/dist/pi-intercom/bridge-catalog.js +372 -0
  5. package/dist/pi-intercom/bridge-config.js +15 -3
  6. package/dist/pi-intercom/bridge-control.js +104 -0
  7. package/dist/pi-intercom/bridge-groups.js +135 -0
  8. package/dist/pi-intercom/bridge-http.js +8 -1
  9. package/dist/pi-intercom/bridge-launch-journal.js +84 -0
  10. package/dist/pi-intercom/bridge-launch-policy.js +71 -5
  11. package/dist/pi-intercom/bridge-launch.js +186 -76
  12. package/dist/pi-intercom/bridge-mailbox.js +3 -55
  13. package/dist/pi-intercom/bridge-operations.js +127 -0
  14. package/dist/pi-intercom/bridge-presence.js +5 -1
  15. package/dist/pi-intercom/bridge-protocol.js +7 -0
  16. package/dist/pi-intercom/bridge-routes.js +195 -27
  17. package/dist/pi-intercom/bridge-rpc-process.js +43 -15
  18. package/dist/pi-intercom/bridge-session.js +83 -66
  19. package/dist/pi-intercom/bridge-storage.js +73 -0
  20. package/dist/pi-intercom/bridge-target.js +22 -0
  21. package/dist/pi-intercom/bridge-wake-owner.js +57 -0
  22. package/dist/pi-intercom/bridge-wake-state.js +293 -0
  23. package/dist/pi-intercom/bridge-wake.js +190 -99
  24. package/dist/pi-intercom/bridge.js +39 -39
  25. package/dist/pi-intercom/broker/client.js +1 -0
  26. package/dist/pi-intercom/connection.js +2 -3
  27. package/dist/pi-intercom/inbound.js +2 -4
  28. package/dist/pi-intercom/index.js +7 -1
  29. package/dist/pi-intercom/native-control-auth.js +80 -0
  30. package/dist/pi-intercom/native-control-protocol.js +111 -0
  31. package/dist/pi-intercom/native-control.js +130 -0
  32. package/dist/pi-intercom/saved-transcript.js +135 -0
  33. package/dist/pi-intercom/session-targets.js +3 -0
  34. package/dist/pi-intercom/transcript.js +175 -0
  35. package/docs/intercom-bridge.md +315 -174
  36. package/docs/intercom.md +1 -1
  37. package/docs/reference.md +1 -1
  38. package/package.json +1 -1
  39. package/scripts/intercom-bridge-wake-preflight.mjs +112 -0
  40. package/scripts/intercom-remote.mjs +213 -38
  41. package/src/pi-intercom/bridge-broadcast.ts +70 -0
  42. package/src/pi-intercom/bridge-catalog.ts +489 -0
  43. package/src/pi-intercom/bridge-config.ts +20 -3
  44. package/src/pi-intercom/bridge-control.ts +179 -0
  45. package/src/pi-intercom/bridge-groups.ts +169 -0
  46. package/src/pi-intercom/bridge-http.ts +15 -1
  47. package/src/pi-intercom/bridge-launch-journal.ts +122 -0
  48. package/src/pi-intercom/bridge-launch-policy.ts +88 -7
  49. package/src/pi-intercom/bridge-launch.ts +296 -107
  50. package/src/pi-intercom/bridge-mailbox.ts +3 -59
  51. package/src/pi-intercom/bridge-operations.ts +169 -0
  52. package/src/pi-intercom/bridge-presence.ts +7 -1
  53. package/src/pi-intercom/bridge-protocol.ts +8 -0
  54. package/src/pi-intercom/bridge-routes.ts +291 -49
  55. package/src/pi-intercom/bridge-rpc-process.ts +53 -16
  56. package/src/pi-intercom/bridge-session.ts +98 -96
  57. package/src/pi-intercom/bridge-storage.ts +83 -0
  58. package/src/pi-intercom/bridge-target.ts +35 -0
  59. package/src/pi-intercom/bridge-wake-owner.ts +58 -0
  60. package/src/pi-intercom/bridge-wake-state.ts +321 -0
  61. package/src/pi-intercom/bridge-wake.ts +224 -130
  62. package/src/pi-intercom/bridge.ts +41 -49
  63. package/src/pi-intercom/broker/client.ts +3 -0
  64. package/src/pi-intercom/connection.ts +2 -3
  65. package/src/pi-intercom/inbound.ts +2 -4
  66. package/src/pi-intercom/index.ts +7 -1
  67. package/src/pi-intercom/native-control-auth.ts +89 -0
  68. package/src/pi-intercom/native-control-protocol.ts +180 -0
  69. package/src/pi-intercom/native-control.ts +160 -0
  70. package/src/pi-intercom/saved-transcript.ts +175 -0
  71. package/src/pi-intercom/session-targets.ts +4 -0
  72. package/src/pi-intercom/transcript.ts +226 -0
  73. package/src/pi-intercom/types.ts +2 -0
@@ -0,0 +1,226 @@
1
+ import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ type ReadonlySessionManager = ExtensionContext["sessionManager"];
3
+ type BoundedOptions = TranscriptOptions & { readonly limit: number; readonly maxBytes: number };
4
+ import { isRecord, isUnknownArray } from "../shared/unknown.ts";
5
+ import { Fault, label, object } from "./bridge-protocol.ts";
6
+
7
+ export interface TranscriptOptions {
8
+ readonly limit?: number;
9
+ readonly maxBytes?: number;
10
+ readonly cursor?: string;
11
+ }
12
+ export interface TranscriptRecord {
13
+ readonly id: string;
14
+ readonly timestamp: string;
15
+ readonly role: string;
16
+ readonly text: string;
17
+ readonly truncated: boolean;
18
+ }
19
+ export interface TranscriptPage {
20
+ readonly sessionId: string;
21
+ readonly leafId: string | null;
22
+ readonly records: readonly TranscriptRecord[];
23
+ readonly bytes: number;
24
+ readonly truncated: boolean;
25
+ readonly cursor?: string;
26
+ readonly scanLimitReached: boolean;
27
+ readonly finalizedOnly: true;
28
+ readonly streaming: "unavailable";
29
+ }
30
+ const SCAN_LIMIT = 1024;
31
+ export function transcriptOptions(value: unknown = {}): BoundedOptions {
32
+ const input = object(value, ["limit", "maxBytes", "cursor"]);
33
+ const limit = integer(input.limit, 50, 1, 100);
34
+ const maxBytes = integer(input.maxBytes, 65536, 256, 131072);
35
+ const cursor = input.cursor === undefined ? undefined : label(input.cursor, "cursor", 512);
36
+ return { limit, maxBytes, cursor };
37
+ }
38
+ function integer(value: unknown, fallback: number, min: number, max: number): number {
39
+ const result = value === undefined ? fallback : value;
40
+ if (typeof result !== "number" || !Number.isInteger(result) || result < min || result > max) {
41
+ throw new Fault(
42
+ 400,
43
+ "invalid_input",
44
+ `Transcript limit must be an integer from ${min} to ${max}.`,
45
+ );
46
+ }
47
+ return result;
48
+ }
49
+ interface TextOutput {
50
+ readonly text: string;
51
+ readonly truncated: boolean;
52
+ }
53
+ function textualContent(content: unknown): TextOutput {
54
+ if (typeof content === "string") {
55
+ return { text: content, truncated: false };
56
+ }
57
+ if (!isUnknownArray(content)) {
58
+ return { text: "", truncated: false };
59
+ }
60
+ const pieces: string[] = [];
61
+ let remaining = 131073;
62
+ let truncated = content.length > 4096;
63
+ for (const part of content.slice(0, 4096)) {
64
+ if (remaining <= 0) {
65
+ truncated = true;
66
+ break;
67
+ }
68
+ if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
69
+ const text = part.text.slice(0, remaining);
70
+ pieces.push(text);
71
+ truncated ||= text.length < part.text.length;
72
+ remaining -= text.length + 1;
73
+ }
74
+ }
75
+ return { text: pieces.join("\n"), truncated };
76
+ }
77
+ function visible(entry: SessionEntry): (TextOutput & { readonly role: string }) | undefined {
78
+ if (entry.type === "custom_message") {
79
+ return entry.display ? { role: "custom", ...textualContent(entry.content) } : undefined;
80
+ }
81
+ if (entry.type !== "message") {
82
+ return undefined;
83
+ }
84
+ const message = entry.message;
85
+ if (message.role === "bashExecution") {
86
+ return {
87
+ role: "toolResult",
88
+ text: message.output,
89
+ truncated: nativeTruncation(message.truncated),
90
+ };
91
+ }
92
+ if (!["user", "assistant", "toolResult", "custom"].includes(message.role)) {
93
+ return undefined;
94
+ }
95
+ if (message.role === "custom" && !message.display) {
96
+ return undefined;
97
+ }
98
+ return "content" in message
99
+ ? { role: message.role, ...textualContent(message.content) }
100
+ : undefined;
101
+ }
102
+ function nativeTruncation(value: unknown): boolean {
103
+ // Older saved bash entries may predate the native boolean field.
104
+ return value === true;
105
+ }
106
+ function boundedText(text: string, budget: number): string {
107
+ // Slice before UTF-8 encoding to avoid copying arbitrarily large live tool output.
108
+ const bytes = Buffer.from(text.slice(0, budget));
109
+ if (bytes.length <= budget) {
110
+ return bytes.toString();
111
+ }
112
+ let end = budget;
113
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) {
114
+ end -= 1;
115
+ }
116
+ return bytes.subarray(0, end).toString();
117
+ }
118
+ function startId(
119
+ manager: ReadonlySessionManager,
120
+ leaf: string | null,
121
+ cursor?: string,
122
+ ): { readonly id: string | null; readonly scanned: number } {
123
+ if (cursor === undefined) {
124
+ return { id: leaf, scanned: 0 };
125
+ }
126
+ let raw: unknown;
127
+ try {
128
+ raw = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
129
+ } catch {
130
+ throw new Fault(400, "invalid_cursor", "Invalid transcript cursor.");
131
+ }
132
+ const data = object(raw, ["leaf", "before"]);
133
+ if (data.leaf !== leaf) {
134
+ throw new Fault(409, "branch_changed", "Active branch changed; request a new transcript page.");
135
+ }
136
+ const before = label(data.before, "cursor entry", 128);
137
+ let id = leaf;
138
+ for (let scanned = 0; id !== null && scanned < SCAN_LIMIT; scanned += 1) {
139
+ if (id === before) {
140
+ return { id, scanned };
141
+ }
142
+ id = manager.getEntry(id)?.parentId ?? null;
143
+ }
144
+ throw new Fault(400, "invalid_cursor", "Cursor is outside the bounded active branch.");
145
+ }
146
+ /** Finalized textual output on the selected branch; never builds full history/context. */
147
+ export function readLiveTranscript(
148
+ manager: ReadonlySessionManager,
149
+ input: TranscriptOptions = {},
150
+ ): TranscriptPage {
151
+ const options = transcriptOptions(input);
152
+ const leaf = manager.getLeafId();
153
+ const start = startId(manager, leaf, options.cursor);
154
+ let id = start.id;
155
+ let scanned = start.scanned;
156
+ const records: TranscriptRecord[] = [];
157
+ let bytes = 0;
158
+ let textTruncated = false;
159
+ while (id !== null && scanned < SCAN_LIMIT) {
160
+ scanned += 1;
161
+ const entry = manager.getEntry(id);
162
+ if (!entry) {
163
+ textTruncated = true;
164
+ id = null;
165
+ break;
166
+ }
167
+ const record = transcriptRecord(entry, options.maxBytes - bytes);
168
+ if (record !== undefined) {
169
+ records.push(record);
170
+ bytes += Buffer.byteLength(record.text);
171
+ textTruncated ||= record.truncated;
172
+ }
173
+ id = entry.parentId;
174
+ if (records.length >= options.limit || bytes >= options.maxBytes) {
175
+ break;
176
+ }
177
+ }
178
+ return transcriptPage(manager, {
179
+ leaf,
180
+ records,
181
+ bytes,
182
+ textTruncated,
183
+ id,
184
+ scanLimitReached: scanned >= SCAN_LIMIT && id !== null,
185
+ });
186
+ }
187
+ function transcriptRecord(entry: SessionEntry, budget: number): TranscriptRecord | undefined {
188
+ const output = visible(entry);
189
+ if (output === undefined || output.text.length === 0) {
190
+ return undefined;
191
+ }
192
+ const text = boundedText(output.text, budget);
193
+ return {
194
+ id: entry.id,
195
+ timestamp: entry.timestamp,
196
+ role: output.role,
197
+ text,
198
+ truncated: text !== output.text || output.truncated,
199
+ };
200
+ }
201
+ function transcriptPage(
202
+ manager: ReadonlySessionManager,
203
+ state: {
204
+ readonly leaf: string | null;
205
+ readonly records: TranscriptRecord[];
206
+ readonly bytes: number;
207
+ readonly textTruncated: boolean;
208
+ readonly id: string | null;
209
+ readonly scanLimitReached: boolean;
210
+ },
211
+ ): TranscriptPage {
212
+ const { leaf, records, bytes, textTruncated, id, scanLimitReached } = state;
213
+ return {
214
+ sessionId: manager.getSessionId(),
215
+ leafId: leaf,
216
+ records: records.reverse(),
217
+ bytes,
218
+ truncated: id !== null || textTruncated,
219
+ scanLimitReached,
220
+ ...(id !== null && !scanLimitReached
221
+ ? { cursor: Buffer.from(JSON.stringify({ leaf, before: id })).toString("base64url") }
222
+ : {}),
223
+ finalizedOnly: true,
224
+ streaming: "unavailable",
225
+ };
226
+ }
@@ -8,6 +8,7 @@ import {
8
8
  safeLabel,
9
9
  safeText,
10
10
  } from "./validation.ts";
11
+ import type { NativeControlEnvelope } from "./native-control-protocol.ts";
11
12
 
12
13
  export interface SessionInfo {
13
14
  readonly id: string;
@@ -106,6 +107,7 @@ export interface Message {
106
107
  readonly timestamp: number;
107
108
  readonly replyTo?: string;
108
109
  readonly expectsReply?: boolean;
110
+ readonly control?: NativeControlEnvelope;
109
111
  /** Recipients verify parent ownership before elevating human origin. */
110
112
  readonly human?: HumanMessageOrigin;
111
113
  readonly topic?: TopicUpdate;