@astrosheep/square 0.4.0 → 0.4.2

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 (196) hide show
  1. package/dist/artifact.d.ts +1 -3
  2. package/dist/artifact.js +12 -36
  3. package/dist/attention-presentation.d.ts +2 -22
  4. package/dist/attention-presentation.js +9 -12
  5. package/dist/boundary-presentation.d.ts +3 -14
  6. package/dist/boundary-presentation.js +29 -104
  7. package/dist/card-presentation.d.ts +1 -0
  8. package/dist/card-presentation.js +2 -0
  9. package/dist/catch-decisions.js +1 -7
  10. package/dist/claude-boundary-state.d.ts +20 -0
  11. package/dist/claude-boundary-state.js +84 -0
  12. package/dist/claude-hook.d.ts +2 -0
  13. package/dist/claude-hook.js +231 -0
  14. package/dist/claude-mcp-context.d.ts +1 -1
  15. package/dist/claude-mcp-context.js +12 -3
  16. package/dist/cli/context.js +7 -9
  17. package/dist/cli/observation-commands.d.ts +4 -1
  18. package/dist/cli/observation-commands.js +37 -4
  19. package/dist/cli/registry.js +5 -2
  20. package/dist/codex-address.d.ts +2 -2
  21. package/dist/codex-boundary-state.d.ts +1 -5
  22. package/dist/codex-boundary-state.js +0 -4
  23. package/dist/codex-hook.d.ts +7 -10
  24. package/dist/codex-hook.js +69 -33
  25. package/dist/codex-queue.d.ts +10 -11
  26. package/dist/codex-queue.js +32 -32
  27. package/dist/codex-watch.d.ts +2 -0
  28. package/dist/codex-watch.js +100 -0
  29. package/dist/decisions.js +2 -2
  30. package/dist/delivery-deferral.d.ts +8 -17
  31. package/dist/delivery-deferral.js +0 -28
  32. package/dist/delivery-frame.d.ts +7 -0
  33. package/dist/delivery-frame.js +11 -0
  34. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  35. package/dist/delivery-ledger-sqlite.js +496 -0
  36. package/dist/delivery-ledger.d.ts +196 -0
  37. package/dist/delivery-ledger.js +268 -0
  38. package/dist/delivery-lifetime.d.ts +6 -0
  39. package/dist/delivery-lifetime.js +45 -0
  40. package/dist/harness-claude.js +3 -4
  41. package/dist/harness-links.js +63 -7
  42. package/dist/harness.js +12 -17
  43. package/dist/help.js +4 -1
  44. package/dist/http-delivery.d.ts +5 -9
  45. package/dist/http-delivery.js +20 -77
  46. package/dist/inbox.d.ts +6 -49
  47. package/dist/inbox.js +24 -371
  48. package/dist/index.d.ts +6 -3
  49. package/dist/index.js +4 -1
  50. package/dist/mcp.js +2 -3
  51. package/dist/model.d.ts +7 -47
  52. package/dist/model.js +1 -7
  53. package/dist/notifications.d.ts +24 -20
  54. package/dist/notifications.js +39 -108
  55. package/dist/operation-control.d.ts +4 -0
  56. package/dist/operation-control.js +20 -0
  57. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  58. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  59. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  60. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  61. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  62. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  63. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  64. package/dist/packages/agent-delivery/src/codex.js +41 -4
  65. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  66. package/dist/packages/agent-delivery/src/index.js +1 -1
  67. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  68. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  69. package/dist/paseo-delivery.d.ts +5 -5
  70. package/dist/paseo-delivery.js +19 -97
  71. package/dist/paseo.d.ts +1 -1
  72. package/dist/paseo.js +1 -1
  73. package/dist/plugins/opencode/index.d.ts +5 -3
  74. package/dist/plugins/opencode/index.js +99 -21
  75. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  76. package/dist/plugins/opencode-shared/delivery.js +109 -0
  77. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  78. package/dist/plugins/opencode-v1/index.js +41 -0
  79. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  80. package/dist/plugins/opencode-v1/runtime.js +152 -0
  81. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  82. package/dist/plugins/opencode-v1/transport.js +50 -0
  83. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  84. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  85. package/dist/ports.d.ts +31 -90
  86. package/dist/session-lifecycle.d.ts +8 -7
  87. package/dist/session-lifecycle.js +40 -42
  88. package/dist/sqlite-square.d.ts +14 -23
  89. package/dist/sqlite-square.js +132 -139
  90. package/dist/square-api.js +4 -7
  91. package/dist/square-core.d.ts +0 -1
  92. package/dist/square-domain.d.ts +5 -17
  93. package/dist/square-domain.js +104 -221
  94. package/dist/square-facade.d.ts +18 -15
  95. package/dist/square-post.d.ts +53 -0
  96. package/dist/square-post.js +4 -0
  97. package/dist/square-posting.d.ts +13 -0
  98. package/dist/square-posting.js +174 -0
  99. package/dist/square-producer-sqlite.d.ts +11 -0
  100. package/dist/square-producer-sqlite.js +210 -0
  101. package/dist/square-sqlite.d.ts +15 -25
  102. package/dist/square-sqlite.js +150 -226
  103. package/dist/views.d.ts +2 -2
  104. package/dist/views.js +0 -1
  105. package/dist/wake-sink.d.ts +4 -11
  106. package/dist/wake-sink.js +8 -32
  107. package/dist/wake-transports.d.ts +4 -7
  108. package/dist/wake-transports.js +7 -39
  109. package/examples/symmetric-domains.mjs +29 -249
  110. package/guides/change-waits.md +13 -53
  111. package/guides/claude-native.md +7 -171
  112. package/guides/codex-delivery.md +55 -80
  113. package/guides/delivery-primitives.md +48 -207
  114. package/guides/mcp.md +1 -1
  115. package/guides/operation-cancellation.md +2 -5
  116. package/guides/participant-ownership.md +25 -89
  117. package/guides/pi-native-delivery.md +19 -56
  118. package/guides/pi-tools.md +6 -8
  119. package/guides/square-api.md +113 -615
  120. package/package.json +8 -8
  121. package/plugins/claude/.claude-plugin/plugin.json +2 -3
  122. package/plugins/claude/hooks/hooks.json +11 -1
  123. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  124. package/plugins/codex/hooks/hooks.json +11 -23
  125. package/plugins/opencode/index.ts +91 -23
  126. package/plugins/opencode-shared/delivery.ts +91 -0
  127. package/plugins/opencode-v1/index.ts +36 -0
  128. package/plugins/opencode-v1/runtime.ts +134 -0
  129. package/plugins/opencode-v1/transport.ts +41 -0
  130. package/plugins/paseo/OVERVIEW.md +5 -5
  131. package/plugins/paseo/README.md +7 -6
  132. package/plugins/paseo/client/markdown.tsx +5 -5
  133. package/plugins/paseo/client/square-message.tsx +28 -24
  134. package/plugins/paseo/client/transform.ts +8 -5
  135. package/plugins/paseo/shared/card-presentation.ts +16 -0
  136. package/plugins/paseo/shared/message.ts +6 -11
  137. package/plugins/pi/square-pi.js +156 -417
  138. package/dist/channel-artifact.d.ts +0 -10
  139. package/dist/channel-artifact.js +0 -55
  140. package/dist/channel-delivery.d.ts +0 -11
  141. package/dist/channel-delivery.js +0 -51
  142. package/dist/channel-domain.d.ts +0 -3
  143. package/dist/channel-domain.js +0 -83
  144. package/dist/channel-facade.d.ts +0 -76
  145. package/dist/channel-facade.js +0 -1
  146. package/dist/channel-internal.d.ts +0 -126
  147. package/dist/channel-internal.js +0 -440
  148. package/dist/channel-metadata.d.ts +0 -11
  149. package/dist/channel-metadata.js +0 -1
  150. package/dist/channel-ports.d.ts +0 -241
  151. package/dist/channel-ports.js +0 -1
  152. package/dist/channel-presentation.d.ts +0 -17
  153. package/dist/channel-presentation.js +0 -22
  154. package/dist/channel-sqlite-schema.d.ts +0 -13
  155. package/dist/channel-sqlite-schema.js +0 -81
  156. package/dist/channel-transport.d.ts +0 -62
  157. package/dist/channel-transport.js +0 -1
  158. package/dist/channel.d.ts +0 -84
  159. package/dist/channel.js +0 -60
  160. package/dist/claude-delivery-status.d.ts +0 -13
  161. package/dist/claude-delivery-status.js +0 -46
  162. package/dist/claude-delivery.d.ts +0 -12
  163. package/dist/claude-delivery.js +0 -108
  164. package/dist/claude-mod.d.ts +0 -8
  165. package/dist/claude-mod.js +0 -172
  166. package/dist/claude-transport.d.ts +0 -5
  167. package/dist/claude-transport.js +0 -58
  168. package/dist/cmd/notify-once.d.ts +0 -2
  169. package/dist/cmd/notify-once.js +0 -29
  170. package/dist/delivery-attempts.d.ts +0 -52
  171. package/dist/delivery-attempts.js +0 -73
  172. package/dist/delivery-effects.d.ts +0 -139
  173. package/dist/delivery-effects.js +0 -163
  174. package/dist/delivery-health.d.ts +0 -51
  175. package/dist/delivery-health.js +0 -173
  176. package/dist/delivery-operations.d.ts +0 -22
  177. package/dist/delivery-operations.js +0 -198
  178. package/dist/delivery-primitives.d.ts +0 -5
  179. package/dist/delivery-primitives.js +0 -3
  180. package/dist/delivery.d.ts +0 -56
  181. package/dist/delivery.js +0 -80
  182. package/dist/plugins/opencode/delivery.d.ts +0 -19
  183. package/dist/plugins/opencode/delivery.js +0 -223
  184. package/dist/presentation-operations.d.ts +0 -5
  185. package/dist/presentation-operations.js +0 -86
  186. package/dist/sqlite-channel.d.ts +0 -15
  187. package/dist/sqlite-channel.js +0 -600
  188. package/dist/square-index.d.ts +0 -16
  189. package/dist/square-index.js +0 -106
  190. package/dist/wake-preview.d.ts +0 -7
  191. package/dist/wake-preview.js +0 -15
  192. package/plugins/claude/hooks/register.js +0 -132
  193. package/plugins/claude/types.d.ts +0 -7
  194. package/plugins/opencode/delivery.ts +0 -198
  195. /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
  196. /package/dist/{channel-values.js → square-values.js} +0 -0
@@ -1,147 +1,32 @@
1
- import path from 'node:path';
2
- import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
1
+ import { Markdown } from '@earendil-works/pi-tui';
3
2
  import { z } from 'zod/v4';
4
3
 
5
- import { Square, harnessOwner } from '../../dist/square-api.js';
4
+ import { Square } from '../../dist/square-api.js';
5
+ import { harnessOwner } from '../../dist/owner.js';
6
+ import { DeliveryLedger } from '../../dist/delivery-ledger.js';
7
+ import { deliveryFrameOverhead, renderDeliveryReminder } from '../../dist/delivery-frame.js';
8
+ import { isCardPresentation } from '../../dist/card-presentation.js';
6
9
  import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, createSquareToolRunner, squareToolsEnabled } from '../../dist/square-tools.js';
7
-
8
- import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
9
10
  import { operationEnv } from '../../dist/participant-identity.js';
10
- import { detachSessionSeats, restoreSessionSeats } from '../../dist/session-lifecycle.js';
11
- import { projectSessionSeats, observeSessionPending, pendingKey } from '../../dist/inbox.js';
12
- import { projectMessageNotification } from '../../dist/attention-presentation.js';
13
- import { formatActivityId } from '../../dist/square-core.js';
14
- import { formatMessageId } from '../../dist/message.js';
11
+ import { attachDeliverySession, detachDeliverySession } from '../../dist/session-lifecycle.js';
15
12
  import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
16
13
 
17
- class PiDeliveryDroppedError extends Error {
18
- constructor() {
19
- super('Pi discarded the queued Square notification');
20
- this.name = 'PiDeliveryDroppedError';
21
- }
22
- }
23
-
24
- export function pendingInbox(inbox) {
25
- return inbox.filter((item) => item.notifications?.length > 0);
26
- }
27
-
28
- export function inboxKeys(inbox) {
29
- return pendingInbox(inbox).flatMap((item) => item.notifications.map((note) =>
30
- pendingKey(item, note.messageIndex)
31
- ));
32
- }
33
-
34
- export function renderPiInbox(inbox) {
35
- return renderPendingAtBoundary(pendingInbox(inbox));
36
- }
37
-
38
- function framePiMessage(content) {
39
- return `\n${content.replace(/^\n+/, '').replace(/\n+$/, '')}\n`;
40
- }
41
-
42
- /** Renderer-only card facts for Pi and Paseo; Pi never sends `details` to the model. */
43
- export function pendingCardDetails(pending) {
44
- const items = pending.flatMap((membership) => membership.notifications.map((note) => {
45
- const projected = projectMessageNotification({ squarePath: membership.location, recipient: membership.seat.participant,
46
- actor: note.actor, body: note.body, ...(note.preview === undefined ? {} : { preview: note.preview }),
47
- ...(note.actIndex === undefined || note.route === undefined ? {} : { agora: { actIndex: note.actIndex, route: note.route } }) });
48
- return ({
49
- square: path.basename(membership.location, '.square'),
50
- actor: note.actor,
51
- recipient: membership.seat.participant,
52
- route: note.route,
53
- act: note.actIndex === undefined ? formatMessageId(note.messageIndex) : formatActivityId(note.actIndex),
54
- preview: projected.preview,
55
- description: projected.description,
56
- body: note.body,
57
- }); }));
58
- if (items.length === 0) return undefined;
59
- const fullText = pending.flatMap((membership) => membership.notifications.map((note) => projectMessageNotification({
60
- squarePath: membership.location, recipient: membership.seat.participant, actor: note.actor, body: note.body,
61
- ...(note.preview === undefined ? {} : { preview: note.preview }),
62
- ...(note.actIndex === undefined || note.route === undefined ? {} : { agora: { actIndex: note.actIndex, route: note.route } }),
63
- }).full)).join('\n\n');
64
- return { items, fullText };
65
- }
66
-
67
- // Conservative terminal width: anything outside ASCII counts as two columns, so a
68
- // truncated line never exceeds the viewport (Pi stops the TUI when a line overflows).
69
- const columnsOf = (char) => (char.charCodeAt(0) < 0x80 ? 1 : 2);
70
- const widthOf = (text) => Array.from(text).reduce((sum, char) => sum + columnsOf(char), 0);
14
+ const BUDGET_CHARS = 12_000;
71
15
 
72
- function fitColumns(text, columns) {
73
- if (widthOf(text) <= columns) return text;
74
- let used = 1; // the ellipsis
75
- let out = '';
76
- for (const char of text) {
77
- if (used + columnsOf(char) > columns) break;
78
- used += columnsOf(char);
79
- out += char;
80
- }
81
- return `${out}…`;
16
+ function displayCard(card) {
17
+ return isCardPresentation(card.details) ? card.details : { text: card.text };
82
18
  }
83
19
 
84
- const fg = (theme, color, text) => theme.fg?.(color, text) ?? text;
85
- const style = (theme, name) => (text) => theme[name]?.(text) ?? text;
86
-
87
- function markdownTheme(theme) {
88
- const color = (name) => (text) => fg(theme, name, text);
89
- return {
20
+ export function renderSquareCard(message, { outputPad = 1 }, theme) {
21
+ if (typeof message.content !== 'string') return undefined;
22
+ const color = (name) => (text) => theme.fg?.(name, text) ?? text;
23
+ const style = (name) => (text) => theme[name]?.(text) ?? text;
24
+ return new Markdown(message.content, outputPad, 0, {
90
25
  heading: color('mdHeading'), link: color('mdLink'), linkUrl: color('mdLinkUrl'), code: color('mdCode'),
91
26
  codeBlock: color('mdCodeBlock'), codeBlockBorder: color('mdCodeBlockBorder'), quote: color('mdQuote'),
92
27
  quoteBorder: color('mdQuoteBorder'), hr: color('mdHr'), listBullet: color('mdListBullet'),
93
- bold: style(theme, 'bold'), italic: style(theme, 'italic'), underline: style(theme, 'underline'),
94
- strikethrough: style(theme, 'strikethrough'),
95
- };
96
- }
97
-
98
- /** One styled line that never exceeds the width; truncated lines stay unstyled so escapes are never cut. */
99
- function fitLine(parts, width) {
100
- const plain = parts.map(([text]) => text).join('');
101
- return widthOf(plain) <= width ? parts.map(([text, paint]) => paint(text)).join('') : fitColumns(plain, width);
102
- }
103
-
104
- /**
105
- * Collapsed: who reached you and where, then the first line of what they said.
106
- * Expanded: each activity as a heading line and its Markdown body; the model's
107
- * framing and continuation hints are not for human eyes.
108
- */
109
- export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
110
- const items = message.details?.items;
111
- if (!Array.isArray(items) || items.length === 0 || items.some((item) => typeof item.body !== 'string')) return undefined;
112
- const pad = ' '.repeat(outputPad);
113
- const dim = (text) => fg(theme, 'dim', text);
114
- if (!expanded) {
115
- const [first] = items;
116
- const more = items.length > 1 ? ` +${items.length - 1}` : '';
117
- // A glance, not a render: drop the commonest inline markers so the line reads as speech.
118
- const opening = (first.body.split('\n').find((line) => line.trim() !== '') ?? '')
119
- .trim().replace(/^(#{1,6}|>|[-*+])\s+/, '').replace(/\*\*|__|`/g, '');
120
- return {
121
- invalidate() {},
122
- render(width) {
123
- const room = Math.max(1, width - outputPad * 2 - 2);
124
- return [
125
- `${pad}${fg(theme, 'customMessageLabel', '■')} ${fitLine([[first.description, (t) => t], [` · ${first.square} · ${first.recipient}${more}`, dim]], room)}`,
126
- ...(opening ? [`${pad} ${fitLine([[opening, dim]], room)}`] : []),
127
- ];
128
- },
129
- };
130
- }
131
- const card = new Container();
132
- for (const [index, item] of items.entries()) {
133
- if (index > 0) card.addChild(new Spacer(1));
134
- card.addChild({
135
- invalidate() {},
136
- render(width) {
137
- const room = Math.max(1, width - outputPad * 2 - 2);
138
- return [`${pad}${fg(theme, 'customMessageLabel', '●')} ${fitLine([[item.description, (t) => t], [` ${item.square} · ${item.recipient} · ${item.act}`, dim]], room)}`];
139
- },
140
- });
141
- card.addChild(new Spacer(1));
142
- card.addChild(new Markdown(item.body, outputPad + 2, 0, markdownTheme(theme)));
143
- }
144
- return card;
28
+ bold: style('bold'), italic: style('italic'), underline: style('underline'), strikethrough: style('strikethrough'),
29
+ });
145
30
  }
146
31
 
147
32
  export default function squarePiExtension(pi) {
@@ -170,339 +55,193 @@ export default function squarePiExtension(pi) {
170
55
  }
171
56
  pi.registerMessageRenderer?.('square', renderSquareCard);
172
57
  pi.registerFlag('agent-delivery-socket', { type: 'string', description: 'Explicit private local delivery socket' });
173
- createPiReceiver(pi, {
174
- get endpoint() { return pi.getFlag('agent-delivery-socket'); },
175
- });
176
- let sessionId;
58
+ createPiReceiver(pi, { get endpoint() { return pi.getFlag('agent-delivery-socket'); } });
59
+
60
+ let session;
61
+ let epoch = 0;
177
62
  let watcher;
178
63
  let watcherAbort;
179
- let generation = 0;
180
- const handledPending = new Set();
181
- const observedPending = new Set();
182
- const landingAcks = new Map();
183
- let retryAfterChange = false;
184
- let retryWait;
64
+ let cancellation = Promise.resolve();
65
+ let cancellationFailed = false;
66
+ let cancelled = false;
67
+ let detachRunAbort;
185
68
  let turnIndex = 0;
186
69
  let activeTurn;
187
- let currentRunSignal;
188
- let detachRunAbort;
189
- let cancelledRun = false;
190
- let pendingCancellations = new Map();
191
- const cancellationFailures = new Set();
192
-
193
- const cancelRunDelivery = () => {
194
- if (cancelledRun) return;
195
- cancelledRun = true;
196
- for (const key of observedPending) handledPending.add(key);
197
- persistSessionCancellation(sessionId);
198
- generation += 1;
199
- stopWatcher();
200
- failAcks(new Error('Pi notification delivery cancelled'));
201
- retryAfterChange = false;
202
- retryWait = undefined;
203
- };
204
-
205
- const failAcks = (error) => {
206
- const acks = [...landingAcks.values()].flatMap((waiters) => [...waiters]);
207
- for (const ack of acks) ack.settle(error);
208
- };
70
+ const landings = new Map();
209
71
 
210
- const acknowledgeLanding = (message) => {
211
- if (message?.role !== 'custom' || message.customType !== 'square' || typeof message.content !== 'string') return;
212
- const acks = landingAcks.get(message.content);
213
- if (acks === undefined) return;
214
- for (const ack of [...acks]) ack.settle();
215
- };
216
-
217
- const waitForLanding = (content, signal, kind) => {
218
- let ack;
72
+ function finishLandings(error) {
73
+ for (const landing of [...landings.values()]) landing.finish(error);
74
+ }
75
+ function waitForLanding(content, signal) {
76
+ let landing;
219
77
  const promise = new Promise((resolve, reject) => {
220
- const abort = () => ack.settle(signal.reason || new Error('Pi native injection aborted'));
221
- ack = {
222
- kind,
223
- sentAfterTurn: turnIndex,
224
- emptyTurnWindows: 0,
225
- settle(error) {
226
- if (ack.settled) return;
227
- ack.settled = true;
78
+ const abort = () => landing.finish(signal.reason ?? new Error('Pi delivery aborted'));
79
+ landing = {
80
+ sentAfterTurn: turnIndex, emptyTurnWindows: 0,
81
+ finish(error) {
82
+ if (landings.get(content) !== landing) return;
83
+ landings.delete(content);
228
84
  signal.removeEventListener('abort', abort);
229
- const acks = landingAcks.get(content);
230
- if (acks?.delete(ack) && acks.size === 0) landingAcks.delete(content);
231
- if (error === undefined) resolve();
232
- else reject(error);
85
+ if (error) reject(error); else resolve();
233
86
  },
234
87
  };
235
- let acks = landingAcks.get(content);
236
- if (acks === undefined) {
237
- acks = new Set();
238
- landingAcks.set(content, acks);
239
- }
240
- acks.add(ack);
241
- if (signal.aborted) abort();
242
- else signal.addEventListener('abort', abort, { once: true });
243
- });
244
- return { promise, ack };
245
- };
246
-
247
- // Each operation carries its own session coordinate. Never rewrite the process environment:
248
- // Pi owns PI_SESSION_ID and several SDK sessions may coexist in one process.
249
- const sessionEnv = (id) => ({ ...process.env, ...(id === undefined ? {} : { PI_SESSION_ID: id }) });
250
-
251
- const persistSessionCancellation = (id) => {
252
- if (id === undefined) return Promise.resolve();
253
- const previous = pendingCancellations.get(id) ?? Promise.resolve();
254
- let operation;
255
- operation = previous.then(async () => {
256
- const owner = harnessOwner({ channel: 'pi', session: id });
257
- const env = sessionEnv(id);
258
- const seats = await projectSessionSeats({ owner, env });
259
- for (const { location, seat } of seats) {
260
- if (seat.attachment !== 'attached') continue;
261
- const square = await Square.at({ path: location, env });
262
- try { await square.cancelDeliveries(seat.participant, { owner, generation: seat.generation }); }
263
- finally { await square.close(); }
264
- }
265
- }).then(() => {
266
- cancellationFailures.delete(id);
267
- }, () => {
268
- cancellationFailures.add(id);
269
- }).finally(() => {
270
- if (pendingCancellations.get(id) === operation) pendingCancellations.delete(id);
88
+ landings.set(content, landing);
89
+ if (signal.aborted) abort(); else signal.addEventListener('abort', abort, { once: true });
271
90
  });
272
- pendingCancellations.set(id, operation);
273
- return operation;
274
- };
275
-
276
- const waitForSessionCancellation = async (id) => {
277
- if (id === undefined) return true;
278
- while (true) {
279
- const pending = pendingCancellations.get(id);
280
- if (pending !== undefined) await pending;
281
- if (pendingCancellations.get(id) !== pending) continue;
282
- return !cancellationFailures.has(id);
283
- }
284
- };
285
-
286
- // Aborting releases native landing waits; retain cleanup until the observer
287
- // has closed, including when cancellation precedes a lifecycle hook.
288
- const stopWatcher = () => {
91
+ return { promise, landing };
92
+ }
93
+ function stopWatcher() {
289
94
  watcherAbort?.abort();
290
95
  watcherAbort = undefined;
291
96
  return watcher;
292
- };
293
-
294
- const pause = (signal, delayMs) => new Promise((resolve) => {
295
- if (signal.aborted) { resolve(); return; }
296
- const finish = () => {
297
- signal.removeEventListener('abort', finish);
298
- clearTimeout(timer);
299
- resolve();
300
- };
301
- const timer = setTimeout(finish, delayMs);
302
- signal.addEventListener('abort', finish, { once: true });
303
- });
304
-
305
- const wake = async (token, signal) => {
306
- const boundSessionId = sessionId;
307
- const env = sessionEnv(boundSessionId);
308
- const observer = await observeSessionPending(harnessOwner({ channel: 'pi', session: boundSessionId }), env);
97
+ }
98
+ function cancelRun() {
99
+ if (cancelled) return;
100
+ cancelled = true;
101
+ const captured = session;
102
+ epoch += 1;
103
+ stopWatcher();
104
+ finishLandings(new Error('Pi notification delivery cancelled'));
105
+ if (captured) cancellation = cancellation.then(async () => {
106
+ if (!await captured.ledger.cancel({ owner: captured.owner, generation: captured.generation })) throw new Error('Pi cancellation fence lost');
107
+ cancellationFailed = false;
108
+ }).catch(() => { cancellationFailed = true; });
109
+ }
110
+ async function watch(captured, token, signal) {
111
+ const { ledger, owner, generation } = captured;
112
+ const observer = await ledger.observe(owner);
309
113
  try {
310
- if (signal.aborted || token !== generation) return;
311
- const armDeferredRetry = () => {
312
- const controller = new AbortController();
313
- let resolveArmed;
314
- const armed = new Promise((resolve) => { resolveArmed = resolve; });
315
- const abort = () => controller.abort();
316
- signal.addEventListener('abort', abort, { once: true });
317
- const pending = observer.wait(Infinity, {
318
- signal: controller.signal,
319
- excludeKeys: handledPending,
320
- skipImmediate: true,
321
- onChangeArmed: resolveArmed,
322
- }).catch(() => {
323
- resolveArmed(false);
324
- return [];
325
- }).finally(() => signal.removeEventListener('abort', abort));
326
- return { armed, pending, cancel: () => controller.abort() };
327
- };
328
-
329
- while (sessionId !== undefined && token === generation && !signal.aborted) {
330
- const deferredRetry = retryAfterChange;
331
- let pending;
114
+ while (!signal.aborted && token === epoch) {
115
+ const edge = observer.generation;
116
+ let claim;
332
117
  try {
333
- pending = deferredRetry && retryWait !== undefined
334
- ? await retryWait
335
- : await observer.wait(Infinity, { signal, excludeKeys: handledPending });
336
- } catch {
337
- await pause(signal, 1_000);
338
- continue;
339
- }
340
- if (sessionId === undefined || token !== generation || signal.aborted) return;
341
- if (pending.length === 0) {
342
- if (deferredRetry) {
343
- retryAfterChange = false;
344
- retryWait = undefined;
118
+ claim = await ledger.claimPresentation(owner, { generation, budgetChars: BUDGET_CHARS - deliveryFrameOverhead({ tagged: true }), signal });
119
+ if (!claim) {
120
+ await observer.hinted(edge, 2_000, signal);
121
+ continue;
345
122
  }
346
- continue;
347
- }
348
- retryAfterChange = false;
349
- retryWait = undefined;
350
-
351
- const keys = inboxKeys(pending);
352
- for (const key of keys) observedPending.add(key);
353
- const deferred = armDeferredRetry();
354
- const retryArmed = await deferred.armed;
355
- if (!retryArmed) {
356
- deferred.cancel();
357
- continue;
358
- }
359
- let keepDeferred = false;
360
- try {
361
- await presentPendingAtBoundary(
362
- harnessOwner({ channel: 'pi', session: boundSessionId }),
363
- async (content, delivered) => {
364
- if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
365
- // One card for the human: a compact heading, expandable to the full body.
366
- // Model context stays bounded; renderer-only details carry the complete text.
367
- const messageContent = framePiMessage(content);
368
- const landing = waitForLanding(messageContent, signal, 'steer');
369
- try {
370
- sendPiMessage(pi,
371
- { customType: 'square', content: messageContent, display: true, details: pendingCardDetails(delivered) },
372
- { deliverAs: 'steer', triggerTurn: true },
373
- );
374
- } catch (error) {
375
- landing.ack.settle(error);
376
- }
377
- return landing.promise;
378
- },
379
- async (_id, _env, boundarySignal) => {
380
- // Recheck committed revisions and current bindings before reusing observed content.
381
- const inbox = await observer.read({ signal: boundarySignal, excludeKeys: handledPending });
382
- if (signal.aborted || token !== generation) return [];
383
- for (const key of inboxKeys(inbox)) observedPending.add(key);
384
- pending = inbox;
385
- return inbox;
386
- },
387
- env,
388
- signal,
389
- );
390
- if (signal.aborted || token !== generation) return;
391
- // Only committed evidence suppresses successful delivery. A competing wake may
392
- // have won the claim and can still fail before sending, making this item eligible again.
393
- } catch (error) {
394
- if (signal.aborted || token !== generation) return;
395
- if (error instanceof PiDeliveryDroppedError) {
396
- deferred.cancel();
123
+ if (signal.aborted || token !== epoch || !await claim.current(signal) || signal.aborted || token !== epoch) {
124
+ await claim.settle('released');
397
125
  continue;
398
126
  }
399
- // The next state edge is already being observed before native injection starts.
400
- retryAfterChange = true;
401
- retryWait = deferred.pending;
402
- keepDeferred = true;
403
- } finally {
404
- if (!keepDeferred) { deferred.cancel(); await deferred.pending; }
127
+ const items = claim.cards.map(displayCard);
128
+ if (signal.aborted || token !== epoch || !await claim.current(signal) || signal.aborted || token !== epoch) {
129
+ await claim.settle('released');
130
+ continue;
131
+ }
132
+ const content = renderDeliveryReminder(claim.cards.map(card => card.text), { tagged: true });
133
+ const { promise, landing } = waitForLanding(content, signal);
134
+ try {
135
+ sendPiMessage(pi, { customType: 'square', content, display: true, details: { items } }, { deliverAs: 'steer', triggerTurn: true });
136
+ } catch (error) { landing.finish(error); }
137
+ await promise;
138
+ await claim.settle('presented');
139
+ } catch (error) {
140
+ if (claim) await claim.settle(error?.dropped ? 'failed' : 'unknown').catch(() => undefined);
141
+ if (signal.aborted || token !== epoch) return;
142
+ // Avoid re-injecting a failed batch until another ledger edge or reconciliation.
143
+ await observer.hinted(observer.generation, 2_000, signal).catch(() => undefined);
405
144
  }
406
145
  }
407
146
  } finally { observer.close(); }
408
- };
147
+ }
148
+ function startWatcher(captured, token) {
149
+ watcherAbort = new AbortController();
150
+ watcher = watch(captured, token, watcherAbort.signal).catch(() => undefined);
151
+ }
409
152
 
410
153
  pi.on('session_start', async (_event, ctx) => {
411
154
  detachRunAbort?.();
412
- cancelledRun = false;
413
- generation += 1;
414
- failAcks(new Error('Pi session replaced'));
155
+ detachRunAbort = undefined;
156
+ cancelled = false;
157
+ const token = ++epoch;
158
+ finishLandings(new Error('Pi session replaced'));
415
159
  const stopped = stopWatcher();
416
- handledPending.clear();
417
- observedPending.clear();
418
- retryAfterChange = false;
419
- retryWait = undefined;
420
- turnIndex = 0;
421
- activeTurn = undefined;
422
- currentRunSignal = undefined;
423
- sessionId = ctx.sessionManager.getSessionId();
424
- const token = generation;
160
+ const previous = session;
161
+ session = undefined;
425
162
  await stopped;
426
- await waitForSessionCancellation(sessionId);
427
- if (token !== generation || cancellationFailures.has(sessionId)) return;
428
- watcherAbort = new AbortController();
429
- const signal = watcherAbort.signal;
430
- await restoreSessionSeats(harnessOwner({ channel: 'pi', session: sessionId }), sessionEnv(sessionId), undefined, signal).catch(() => undefined);
431
- if (token !== generation || signal.aborted) return;
432
- watcher = wake(token, signal).catch(() => undefined);
163
+ await cancellation;
164
+ if (previous) {
165
+ await detachDeliverySession(previous.owner, previous.generation).catch(() => undefined);
166
+ await previous.ledger.close();
167
+ }
168
+ if (token !== epoch || cancellationFailed) return;
169
+ const owner = harnessOwner({ channel: 'pi', session: ctx.sessionManager.getSessionId() });
170
+ const attached = await attachDeliverySession(owner);
171
+ if (!attached) return;
172
+ if (token !== epoch) {
173
+ await detachDeliverySession(owner, attached.generation);
174
+ return;
175
+ }
176
+ let ledger;
177
+ try {
178
+ ledger = await DeliveryLedger.open();
179
+ if (token !== epoch) {
180
+ await detachDeliverySession(owner, attached.generation);
181
+ await ledger.close();
182
+ return;
183
+ }
184
+ session = { ledger, owner, generation: attached.generation };
185
+ turnIndex = 0;
186
+ activeTurn = undefined;
187
+ startWatcher(session, token);
188
+ } catch (error) {
189
+ await detachDeliverySession(owner, attached.generation).catch(() => undefined);
190
+ await ledger?.close();
191
+ throw error;
192
+ }
433
193
  });
434
194
 
435
195
  pi.on('agent_start', async (_event, ctx) => {
436
196
  detachRunAbort?.();
437
- currentRunSignal = ctx?.signal;
438
- if (ctx?.mode !== 'tui' || currentRunSignal === undefined) return;
439
- const runSignal = currentRunSignal;
440
- detachRunAbort = () => runSignal.removeEventListener('abort', cancelRunDelivery);
441
- if (runSignal.aborted) cancelRunDelivery();
442
- else runSignal.addEventListener('abort', cancelRunDelivery, { once: true });
197
+ if (ctx?.mode !== 'tui' || !ctx.signal) return;
198
+ const signal = ctx.signal;
199
+ detachRunAbort = () => signal.removeEventListener('abort', cancelRun);
200
+ if (signal.aborted) cancelRun(); else signal.addEventListener('abort', cancelRun, { once: true });
443
201
  });
444
-
445
202
  pi.on('message_end', async (event) => {
446
- if (activeTurn !== undefined && (event.message?.role === 'user' || event.message?.role === 'custom')) {
447
- activeTurn.hasInput = true;
203
+ const message = event.message;
204
+ if (activeTurn && (message?.role === 'user' || message?.role === 'custom')) activeTurn.hasInput = true;
205
+ if (message?.role === 'custom' && message.customType === 'square' && typeof message.content === 'string') {
206
+ landings.get(message.content)?.finish();
448
207
  }
449
- acknowledgeLanding(event.message);
450
208
  });
451
-
452
- pi.on('turn_start', async () => {
453
- turnIndex += 1;
454
- activeTurn = { index: turnIndex, hasInput: false, signal: currentRunSignal };
455
- });
456
-
209
+ pi.on('turn_start', async () => { activeTurn = { index: ++turnIndex, hasInput: false }; });
457
210
  pi.on('turn_end', async (event, ctx) => {
458
211
  const turn = activeTurn;
459
212
  activeTurn = undefined;
460
- const acks = [...landingAcks.values()].flatMap((waiters) => [...waiters]);
461
- if (event.message?.stopReason === 'aborted' && ctx?.mode === 'tui') {
462
- if (!cancelledRun) cancelRunDelivery();
463
- return;
464
- }
465
- if (turn === undefined) return;
466
- for (const ack of acks) {
467
- if (ack.kind !== 'steer' || turn.index <= ack.sentAfterTurn) continue;
468
- if (turn.hasInput) {
469
- ack.emptyTurnWindows = 0;
470
- } else {
471
- ack.emptyTurnWindows += 1;
472
- if (ack.emptyTurnWindows >= 2) ack.settle(new PiDeliveryDroppedError());
473
- }
213
+ if (event.message?.stopReason === 'aborted' && ctx?.mode === 'tui') { cancelRun(); return; }
214
+ if (!turn) return;
215
+ for (const landing of [...landings.values()]) {
216
+ if (turn.index <= landing.sentAfterTurn) continue;
217
+ if (turn.hasInput) landing.emptyTurnWindows = 0;
218
+ else if (++landing.emptyTurnWindows >= 2) landing.finish(Object.assign(new Error('Pi discarded queued notification'), { dropped: true }));
474
219
  }
475
220
  });
476
-
477
221
  pi.on('agent_settled', async () => {
478
222
  detachRunAbort?.();
479
223
  detachRunAbort = undefined;
480
- currentRunSignal = undefined;
481
- if (!cancelledRun || sessionId === undefined) return;
482
- const token = generation;
224
+ if (!cancelled || !session) return;
225
+ const token = epoch;
483
226
  await stopWatcher();
484
- await waitForSessionCancellation(sessionId);
485
- if (token !== generation || sessionId === undefined || cancellationFailures.has(sessionId)) return;
486
- cancelledRun = false;
487
- watcherAbort = new AbortController();
488
- watcher = wake(token, watcherAbort.signal).catch(() => undefined);
227
+ await cancellation;
228
+ if (token !== epoch || !session || cancellationFailed) return;
229
+ cancelled = false;
230
+ startWatcher(session, token);
489
231
  });
490
-
491
232
  pi.on('session_shutdown', async () => {
492
233
  detachRunAbort?.();
493
234
  detachRunAbort = undefined;
494
- cancelledRun = false;
495
- generation += 1;
496
- failAcks(new Error('Pi session ended'));
235
+ ++epoch;
236
+ finishLandings(new Error('Pi session ended'));
497
237
  const stopped = stopWatcher();
498
- const endedId = sessionId;
499
- const endedEnv = sessionEnv(endedId);
500
- sessionId = undefined;
501
- retryWait = undefined;
502
- activeTurn = undefined;
503
- currentRunSignal = undefined;
238
+ const ended = session;
239
+ session = undefined;
504
240
  await stopped;
505
- await waitForSessionCancellation(endedId);
506
- if (endedId) await detachSessionSeats('pi', endedId, endedEnv).catch(() => undefined);
241
+ await cancellation;
242
+ if (ended) {
243
+ await detachDeliverySession(ended.owner, ended.generation).catch(() => undefined);
244
+ await ended.ledger.close();
245
+ }
507
246
  });
508
247
  }
@@ -1,10 +0,0 @@
1
- import { type StoreTransaction } from './artifact-store.js';
2
- import type { ChannelArtifact, ChannelRead, ChannelWrite } from './channel-ports.js';
3
- import type { WakeTransportPort, WakeProjection } from './channel-transport.js';
4
- export declare function channelReads(channel: ChannelRead, guard: StoreTransaction['guard']): ChannelRead;
5
- export declare function channelWrites(channel: ChannelWrite, guard: StoreTransaction['guard']): ChannelWrite;
6
- /** Native Channel factories use only lower SQL and the shared physical-store lifetime. */
7
- export declare function openChannelArtifact(location: string, signal?: AbortSignal): ChannelArtifact;
8
- export declare function createMemoryChannelArtifact(description?: string): ChannelArtifact;
9
- export declare function createChannelArtifactFile(location: string, description: string, force: boolean, signal?: AbortSignal): Promise<void>;
10
- export declare function channelCourier(artifact: ChannelArtifact, transport: WakeTransportPort, location: string, clock: () => number, preparePresentation?: WakeProjection): (message?: number) => Promise<void>;