@astrosheep/square 0.3.106 → 0.4.1

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