@astrosheep/square 0.3.93 → 0.3.95

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 (146) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +3 -3
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.d.ts +2 -2
  7. package/dist/boundary-presentation.js +33 -40
  8. package/dist/catch-decisions.d.ts +5 -8
  9. package/dist/catch-decisions.js +45 -25
  10. package/dist/claude-delivery-status.js +10 -4
  11. package/dist/claude-delivery.js +19 -11
  12. package/dist/claude-mod.js +24 -53
  13. package/dist/cli/context.d.ts +9 -4
  14. package/dist/cli/context.js +30 -27
  15. package/dist/cli/express-presentation.d.ts +3 -1
  16. package/dist/cli/express-presentation.js +28 -16
  17. package/dist/cli/json.d.ts +3 -0
  18. package/dist/cli/json.js +14 -0
  19. package/dist/cli/mcp-server-command.js +2 -2
  20. package/dist/cli/observation-commands.d.ts +1 -3
  21. package/dist/cli/observation-commands.js +43 -56
  22. package/dist/cli/program.js +3 -3
  23. package/dist/cli/registry.js +7 -6
  24. package/dist/cli/square-commands.d.ts +0 -2
  25. package/dist/cli/square-commands.js +30 -44
  26. package/dist/codex-hook.d.ts +3 -2
  27. package/dist/codex-hook.js +13 -4
  28. package/dist/codex-queue.js +4 -4
  29. package/dist/decisions.d.ts +5 -16
  30. package/dist/decisions.js +9 -33
  31. package/dist/delivery-attempts.d.ts +52 -0
  32. package/dist/delivery-attempts.js +73 -0
  33. package/dist/delivery-deferral.js +2 -2
  34. package/dist/delivery-effects.d.ts +139 -0
  35. package/dist/delivery-effects.js +163 -0
  36. package/dist/delivery-health.d.ts +0 -1
  37. package/dist/delivery-health.js +1 -3
  38. package/dist/delivery-operations.d.ts +2 -2
  39. package/dist/delivery-operations.js +76 -101
  40. package/dist/delivery-primitives.d.ts +5 -0
  41. package/dist/delivery-primitives.js +3 -0
  42. package/dist/delivery.d.ts +1 -1
  43. package/dist/delivery.js +7 -6
  44. package/dist/help.js +14 -14
  45. package/dist/http-delivery.d.ts +16 -0
  46. package/dist/http-delivery.js +101 -0
  47. package/dist/inbox.d.ts +9 -22
  48. package/dist/inbox.js +23 -27
  49. package/dist/index.d.ts +1 -3
  50. package/dist/index.js +1 -1
  51. package/dist/mcp.d.ts +3 -0
  52. package/dist/mcp.js +23 -24
  53. package/dist/model.d.ts +12 -21
  54. package/dist/model.js +5 -8
  55. package/dist/notifications.d.ts +1 -1
  56. package/dist/notifications.js +8 -5
  57. package/dist/operation-presentation.d.ts +6 -6
  58. package/dist/operation-presentation.js +55 -58
  59. package/dist/owner.d.ts +11 -0
  60. package/dist/owner.js +32 -0
  61. package/dist/paseo-delivery.d.ts +4 -1
  62. package/dist/paseo-delivery.js +7 -12
  63. package/dist/paseo-state.d.ts +2 -2
  64. package/dist/paseo-state.js +6 -4
  65. package/dist/plugins/opencode/delivery.js +27 -15
  66. package/dist/ports.d.ts +18 -47
  67. package/dist/presentation-operations.d.ts +14 -19
  68. package/dist/presentation-operations.js +51 -61
  69. package/dist/presentation.d.ts +7 -17
  70. package/dist/presentation.js +46 -91
  71. package/dist/receive.d.ts +66 -0
  72. package/dist/receive.js +54 -0
  73. package/dist/runtime.d.ts +1 -8
  74. package/dist/runtime.js +2 -17
  75. package/dist/seats.d.ts +2 -3
  76. package/dist/seats.js +7 -15
  77. package/dist/session-lifecycle.d.ts +6 -2
  78. package/dist/session-lifecycle.js +39 -11
  79. package/dist/sqlite-square.d.ts +36 -23
  80. package/dist/sqlite-square.js +162 -211
  81. package/dist/square-api.d.ts +16 -11
  82. package/dist/square-api.js +27 -23
  83. package/dist/square-core.d.ts +0 -14
  84. package/dist/square-core.js +0 -9
  85. package/dist/square-domain.d.ts +4 -2
  86. package/dist/square-domain.js +65 -52
  87. package/dist/square-facade.d.ts +47 -105
  88. package/dist/square-index.d.ts +5 -13
  89. package/dist/square-index.js +13 -38
  90. package/dist/square-legacy-framing.d.ts +2 -0
  91. package/dist/square-legacy-framing.js +8 -0
  92. package/dist/square-location.d.ts +1 -14
  93. package/dist/square-location.js +4 -70
  94. package/dist/square-migration-host-ledger.d.ts +2 -2
  95. package/dist/square-migration-host-ledger.js +6 -5
  96. package/dist/square-migration.d.ts +4 -1
  97. package/dist/square-migration.js +93 -38
  98. package/dist/square-receive.d.ts +6 -0
  99. package/dist/square-receive.js +18 -0
  100. package/dist/square-sqlite.d.ts +9 -3
  101. package/dist/square-sqlite.js +139 -114
  102. package/dist/square-tools.d.ts +7 -10
  103. package/dist/square-tools.js +21 -61
  104. package/dist/stream.js +4 -5
  105. package/dist/views.d.ts +23 -13
  106. package/dist/views.js +47 -53
  107. package/dist/wakes.d.ts +1 -5
  108. package/dist/wakes.js +4 -6
  109. package/examples/symmetric-domains.mjs +250 -0
  110. package/guides/application-facade-delivery-evidence.md +6 -6
  111. package/guides/attention-primitive.md +32 -0
  112. package/guides/change-waits.md +2 -2
  113. package/guides/claude-native.md +18 -9
  114. package/guides/codex-delivery.md +17 -6
  115. package/guides/delivery-primitives.md +180 -0
  116. package/guides/mcp-connection-lifetime.md +1 -1
  117. package/guides/mcp.md +3 -3
  118. package/guides/migration.md +148 -151
  119. package/guides/operation-cancellation.md +1 -1
  120. package/guides/participant-ownership.md +78 -47
  121. package/guides/pi-tools.md +34 -5
  122. package/guides/primitive-catch-consolidation.md +5 -5
  123. package/guides/primitive-history-consolidation.md +2 -2
  124. package/guides/primitive-status-consolidation.md +3 -3
  125. package/guides/primitive-storage-consolidation.md +1 -1
  126. package/guides/receive-primitive.md +90 -0
  127. package/guides/row-runtime-delivery-evidence.md +1 -1
  128. package/guides/row-storage-design.md +22 -28
  129. package/guides/square-api.md +314 -91
  130. package/guides/symmetric-domains.md +92 -0
  131. package/package.json +14 -1
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/skills/square/SKILL.md +2 -6
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +27 -17
  136. package/plugins/paseo/client/palette.ts +18 -18
  137. package/plugins/paseo/client/square-message.tsx +17 -23
  138. package/plugins/pi/square-pi.js +17 -17
  139. package/skills/square/SKILL.md +2 -6
  140. package/dist/square-backend.d.ts +0 -13
  141. package/dist/square-backend.js +0 -1
  142. package/dist/square-stdio.d.ts +0 -22
  143. package/dist/square-stdio.js +0 -235
  144. package/dist/stdio-server.d.ts +0 -3
  145. package/dist/stdio-server.js +0 -212
  146. package/guides/backends.md +0 -193
@@ -0,0 +1,250 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { realpathSync } from 'node:fs';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { resolveAudience } from '@astrosheep/square/attention';
5
+ import { receive, consumeReceived, recordConsumed } from '@astrosheep/square/receive';
6
+ import { claimAttempt, settleAttempt, dispatchEffect, preparePresentation, presentPrepared } from '@astrosheep/square/delivery';
7
+
8
+ // Application routing fixture, not discovery or a core identity format.
9
+ const directory = new Map([
10
+ ['e/author', 'east'], ['e/reader', 'east'], ['e/other', 'east'],
11
+ ['w/author', 'west'], ['w/reader', 'west'], ['w/other', 'west'],
12
+ ]);
13
+
14
+ // One structural ReceiveWrite adapter. Array insertion order is local order;
15
+ // opaque item keys are only compared for equality, never parsed or sorted.
16
+ function receiveStore(state) {
17
+ const tx = {
18
+ insertItem({ key, data }) {
19
+ if (state.items.has(key)) throw new Error('duplicate item');
20
+ state.items.set(key, structuredClone(data));
21
+ },
22
+ insertAddress(key, address) {
23
+ if (tx.addressed(address.recipient, key)) throw new Error('duplicate address');
24
+ state.addresses.push({ key, ...address });
25
+ },
26
+ heads(recipient, query, { unconsumed, limit }) {
27
+ return state.addresses.filter(row => row.recipient === recipient
28
+ && (!query.via || row.via === query.via)
29
+ && (!unconsumed || !tx.receipt({ recipient, item: row.key }))).slice(0, limit);
30
+ },
31
+ addressed: (recipient, key) => state.addresses.find(row => row.recipient === recipient && row.key === key),
32
+ load: keys => new Map(keys.filter(key => state.items.has(key)).map(key => [key, state.items.get(key)])),
33
+ count: (recipient, query, unconsumed) => tx.heads(recipient, query, { unconsumed, limit: Infinity }).length,
34
+ receipt: ({ recipient, item }) => state.receipts.get(recipient)?.get(item),
35
+ insertReceipt({ recipient, item }, at) {
36
+ if (!state.receipts.has(recipient)) state.receipts.set(recipient, new Map());
37
+ const receipts = state.receipts.get(recipient);
38
+ if (receipts.has(item)) return false;
39
+ receipts.set(item, { at });
40
+ return true;
41
+ },
42
+ };
43
+ return tx;
44
+ }
45
+
46
+ // A tiny illustrative consumer, NOT a storage backend or durable runtime.
47
+ class Domain {
48
+ #state = { items: new Map(), addresses: [], receipts: new Map(), series: [] };
49
+
50
+ constructor(name, nextKey) {
51
+ this.name = name;
52
+ this.nextKey = nextKey;
53
+ }
54
+
55
+ transaction(change) {
56
+ const draft = structuredClone(this.#state);
57
+ const result = change(draft, receiveStore(draft));
58
+ if (result instanceof Promise) throw new Error('no awaits inside memory transactions');
59
+ // A throw above discards the draft. No capability or mutable row escapes.
60
+ const captured = structuredClone(result);
61
+ this.#state = draft;
62
+ return captured;
63
+ }
64
+
65
+ originate(actor, mentions, text) {
66
+ // Attention names its route; receive stores that application route as via.
67
+ const audience = resolveAudience({ actor, recipients: [...directory.keys()], mentions })
68
+ .map(({ recipient, route }) => ({ recipient, via: route }));
69
+ const key = this.nextKey();
70
+ const data = { text, origin: { domain: this.name, key } };
71
+ this.transaction((_state, tx) => receive(tx, {
72
+ item: { key, data }, audience: audience.filter(a => directory.get(a.recipient) === this.name),
73
+ }));
74
+ const outgoing = new Map();
75
+ for (const address of audience) {
76
+ const destination = directory.get(address.recipient);
77
+ if (destination === this.name) continue;
78
+ if (!outgoing.has(destination)) outgoing.set(destination, []);
79
+ outgoing.get(destination).push(address);
80
+ }
81
+ return { key, data, outgoing };
82
+ }
83
+
84
+ admit(data, audience) {
85
+ const key = this.nextKey(); // Destination owns this identity, not origin.key.
86
+ this.transaction((_state, tx) => receive(tx, {
87
+ item: { key, data }, audience,
88
+ }));
89
+ return key;
90
+ }
91
+
92
+ // Captured item/target/purpose are application coordinates, not an encoded key.
93
+ // For dispatch target is a destination; for presentation it is one recipient.
94
+ effectPort(purpose, item, target, fullReceipts = []) {
95
+ const rows = state => {
96
+ let series = state.series.find(s => s.purpose === purpose && s.item === item && s.target === target);
97
+ if (!series) {
98
+ series = { purpose, item, target, attempts: new Map() };
99
+ state.series.push(series);
100
+ }
101
+ return {
102
+ list: () => structuredClone([...series.attempts.values()].sort((a, b) => b.ordinal - a.ordinal)),
103
+ byToken: token => structuredClone(series.attempts.get(token)),
104
+ insert: row => { series.attempts.set(row.token, structuredClone(row)); },
105
+ replace: row => { series.attempts.set(row.token, structuredClone(row)); },
106
+ };
107
+ };
108
+ const current = (state, tx) => state.items.has(item) && (purpose === 'dispatch'
109
+ || Boolean(tx.addressed(target, item)) && !tx.receipt({ recipient: target, item }));
110
+ return {
111
+ claim: async () => this.transaction((state, tx) => {
112
+ if (!current(state, tx)) return undefined;
113
+ const claim = claimAttempt(rows(state), {
114
+ purpose, token: randomUUID(), at: Date.now(), leaseMs: 30_000, data: {},
115
+ });
116
+ return claim.status === 'acquired' ? { token: claim.token, ordinal: claim.ordinal } : undefined;
117
+ }),
118
+ current: async () => this.transaction(current),
119
+ settle: async (claim, completion) => this.transaction((state, tx) => {
120
+ const recorded = settleAttempt(rows(state), {
121
+ token: claim.token, outcome: completion.outcome, at: Date.now(), data: completion,
122
+ });
123
+ // ONE commit: attempt completion plus captured full-consumption receipts.
124
+ // No post-effect cancellation/currentness veto and no re-selection.
125
+ if (recorded && completion.outcome === 'presented') recordConsumed(tx, fullReceipts, Date.now());
126
+ return recorded;
127
+ }),
128
+ };
129
+ }
130
+
131
+ consume(recipient, selection) {
132
+ return this.transaction((_state, tx) => consumeReceived(tx, recipient, selection, Date.now()));
133
+ }
134
+
135
+ pending(recipient) {
136
+ return this.transaction((_state, tx) => tx.heads(recipient, {}, { unconsumed: true, limit: Infinity }).map(h => h.key));
137
+ }
138
+
139
+ receipts() {
140
+ return this.transaction(state => [...state.receipts].flatMap(([recipient, items]) =>
141
+ [...items].map(([item, { at }]) => ({ recipient, item, at }))));
142
+ }
143
+
144
+ attempts() {
145
+ return this.transaction(state => state.series.map(({ purpose, item, target, attempts }) =>
146
+ ({ purpose, item, target, outcomes: [...attempts.values()].map(a => a.outcome) })));
147
+ }
148
+
149
+ async present(recipient, item, sink) {
150
+ const payload = this.transaction((_state, tx) => {
151
+ if (!tx.addressed(recipient, item)) throw new Error('not addressed');
152
+ return tx.load([item]).get(item);
153
+ });
154
+ const prepared = await preparePresentation({
155
+ payload, coverage: 'full',
156
+ port: this.effectPort('presentation', item, recipient, [{ recipient, item }]),
157
+ });
158
+ return prepared ? presentPrepared(prepared, sink) : undefined;
159
+ }
160
+ }
161
+
162
+ export async function demonstrateSymmetricDomains() {
163
+ // Same implementation, truly separate stores and unrelated key spaces.
164
+ let westKey = -700;
165
+ const east = new Domain('east', () => `opaque?${randomUUID()}`);
166
+ const west = new Domain('west', () => westKey--);
167
+ const domains = new Map([['east', east], ['west', west]]);
168
+ const handoffs = [];
169
+ let online = true;
170
+
171
+ async function sendAll(source, message, mode = 'accepted') {
172
+ const reports = [];
173
+ for (const [destination, audience] of message.outgoing) {
174
+ const report = await dispatchEffect(source.effectPort('dispatch', message.key, destination), async context => {
175
+ if (!await context.beforeSend()) return { outcome: 'not-capable' };
176
+ // A final policy gate may finish after the runner's timeout. Never mutate
177
+ // the destination after our deadline or cancellation, even if it said yes.
178
+ if (context.signal?.aborted || Date.now() >= context.deadline) return { outcome: 'not-capable' };
179
+ if (!online) return { outcome: 'failed' }; // Known no-send in this in-process fixture.
180
+ const key = domains.get(destination).admit(message.data, audience);
181
+ handoffs.push({ from: source.name, to: destination, key, audience: structuredClone(audience) });
182
+ // Unknown deliberately happens AFTER admission. This is not a dedup protocol.
183
+ return { outcome: mode, data: { destinationKey: key } };
184
+ }, { timeoutMs: 1000 });
185
+ reports.push(report);
186
+ }
187
+ return reports;
188
+ }
189
+
190
+ const eastMessage = east.originate('e/author', ['e/reader', 'w/reader', 'w/other'], 'East says hello');
191
+ const westMessage = west.originate('w/author', ['w/reader', 'e/reader', 'e/other'], 'West says hello');
192
+ // Establish origin receipts before destination presentation: neither can overwrite the other.
193
+ east.consume('e/reader', { kind: 'exact', key: eastMessage.key, remainingQuery: {} });
194
+ west.consume('w/reader', { kind: 'exact', key: westMessage.key, remainingQuery: {} });
195
+
196
+ const exchanges = [];
197
+ for (const [source, destination, message, mode, first, second] of [
198
+ [east, west, eastMessage, 'accepted', 'w/reader', 'w/other'],
199
+ [west, east, westMessage, 'unknown', 'e/reader', 'e/other'],
200
+ ]) {
201
+ const [report] = await sendAll(source, message, mode);
202
+ const destinationKey = report.settlement.completion.data.destinationKey;
203
+ const admission = [destination.pending(first), destination.pending(second)];
204
+ const originReceiptsBefore = source.receipts();
205
+ const output = [];
206
+ // The sink proves only that this example callback captured the full payload.
207
+ const presentation = await destination.present(first, destinationKey, payload => { output.push(payload); });
208
+ const afterPresentation = [destination.pending(first), destination.pending(second)];
209
+ const presentationReceipts = destination.receipts().filter(ref => ref.item === destinationKey);
210
+ const originReceiptsAfter = source.receipts();
211
+ const exact = { kind: 'exact', key: destinationKey, remainingQuery: {} };
212
+ const unmatched = destination.consume(second, { kind: 'page', query: { via: 'bell' }, limit: 1 });
213
+ const consumed = destination.consume(second, { kind: 'page', query: { via: 'mention' }, limit: 1 });
214
+ const repeated = destination.consume(second, exact);
215
+ const handoffsBeforeReplay = handoffs.length;
216
+ const [replay] = await sendAll(source, message, mode);
217
+ exchanges.push({
218
+ from: source.name, to: destination.name, sourceKey: message.key, destinationKey,
219
+ dispatch: report, admission, presentation, output, afterPresentation, presentationReceipts,
220
+ originReceiptsBefore, originReceiptsAfter, unmatched, consumed, repeated, replay,
221
+ handoffsBeforeReplay, handoffsAfterReplay: handoffs.length,
222
+ });
223
+ }
224
+
225
+ online = false;
226
+ const handoffsBeforeOffline = handoffs.length;
227
+ const offline = [];
228
+ for (const [domain, author, recipient] of [[east, 'e/author', 'e/reader'], [west, 'w/author', 'w/reader']]) {
229
+ const messages = [];
230
+ for (const text of ['oldest', 'middle', 'newest']) {
231
+ const message = domain.originate(author, [recipient], text);
232
+ messages.push(message);
233
+ await sendAll(domain, message); // Local work has no remote destination at all.
234
+ }
235
+ const exact = { kind: 'exact', key: messages[1].key, remainingQuery: {} };
236
+ const middle = domain.consume(recipient, exact);
237
+ const repeated = domain.consume(recipient, exact);
238
+ const page = () => domain.consume(recipient, { kind: 'page', query: { via: 'mention' }, limit: 1 });
239
+ offline.push({ domain: domain.name, keys: messages.map(m => m.key), middle, repeated, pages: [page(), page()] });
240
+ }
241
+ return {
242
+ exchanges, handoffs, offline, handoffsBeforeOffline, handoffsAfterOffline: handoffs.length,
243
+ attempts: { east: east.attempts(), west: west.attempts() },
244
+ receipts: { east: east.receipts(), west: west.receipts() },
245
+ };
246
+ }
247
+
248
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
249
+ console.log(JSON.stringify(await demonstrateSymmetricDomains(), null, 2));
250
+ }
@@ -117,7 +117,7 @@ production line delta -7
117
117
  - Root `activity.ts` (271 lines) and `watch.ts` (328 lines) are deleted. Their surviving draft/retry/rendering code is counted in the new CLI helpers (206 + 68 lines); moving renderers is not claimed as semantic deletion.
118
118
  - CLI build/join/listener/lifecycle/history/status/catch no longer open storage for admission, before/after facts, projections or sweep coordination. `cli/context.ts` no longer constructs a ledger or discovers bindings during parsing. Application `selected` captures an actor per operation, not a mutable-binding cache.
119
119
  - Catch has one leased quiet-wait owner; the old action absolute-idle loop and CLI heartbeat/revision/finish wrappers are removed. Selection carries records/ordinals/header facts in its receipt transaction. Optional nearby presence does not rehydrate the selected page.
120
- - Express has one preparation/reentry and retry/commit owner. It selects the pending directed feed once, and uses commit-observed count/hold/cap facts rather than rebuilding that feed or issuing a duplicate post-success header query.
120
+ - Express has one preparation/reentry and retry/commit owner. It selects the pending directed feed once, and uses commit-observed count/cap facts rather than rebuilding that feed or issuing a duplicate post-success header query.
121
121
  - MCP `publicResult`, stored-activity conversion and status-result policy are deleted. Private producers reuse the facade DTOs instead of maintaining parallel status/unread declarations.
122
122
  - Deleted unused `Square.joinWithActivity`, `takeoverWithActivity`, `joinFacts`, `standingCount`; deleted `views.activityPresentation`, `standingCount`, `joinFacts`, `watchPresentation`, and `presentation.renderWatchOutput`. Focused `recoveryObservation` and optional `catchPresence` replace the displaced mixed queries. Supported SDK/session methods remain.
123
123
  - `list.ts` no longer parses argv or writes process output; `stream.ts` no longer owns stdout/drain. The CLI owns those adaptations.
@@ -141,7 +141,7 @@ rg exit: 1
141
141
 
142
142
  ## private action imports
143
143
  pattern: from\s*['"][^'"]*square-actions\.js['"]
144
- square-application.ts:14:import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, resume, retireEndedSessionRoutes, type ExpressCommittedResult } from './square-actions.js';
144
+ square-application.ts:14:import { catchUp, done, endOwnedSession, express, ignore, implicitJoin, join, takeover, listen, listening, retireEndedSessionRoutes, type ExpressCommittedResult } from './square-actions.js';
145
145
  rg exit: 0
146
146
 
147
147
  ## adapter direct private execution
@@ -157,7 +157,7 @@ rg exit: 1
157
157
 
158
158
  The adapter grep's sole match resolves to **`cli/registry.ts`**, the argv registry, not the root host-binding `registry.ts`. The AST dependency test resolves paths instead of mistaking equal basenames for equal ownership.
159
159
 
160
- `test/architecture-boundary.test.js` passed the transitive adapter/helper traversal and its deliberate hidden-bypass fixtures. It checks all CLI roots, MCP and presentation helpers; permits only named pure bindings (core IDs, immutable held limit, model syntax/errors, identity formatting, search formatting) and closed protocol edges (harness, native hook I/O, MCP immutable `operationEnv`). It rejects private execution hidden through nested helpers, namespace/extra mixed-module bindings, an exceeded native exception and unbounded imports. These are not blanket directory exemptions. Existing storage-boundary/synchronous-capability tests also pass.
160
+ `test/architecture-boundary.test.js` passed the transitive adapter/helper traversal and its deliberate hidden-bypass fixtures. It checks all CLI roots, MCP and presentation helpers; permits only named pure bindings (core IDs, model syntax/errors, identity formatting, search formatting) and closed protocol edges (harness, native hook I/O, MCP immutable `operationEnv`). It rejects private execution hidden through nested helpers, namespace/extra mixed-module bindings, an exceeded native exception and unbounded imports. These are not blanket directory exemptions. Existing storage-boundary/synchronous-capability tests also pass.
161
161
 
162
162
  ## Closure map for pre-delivery findings
163
163
 
@@ -174,9 +174,9 @@ These are deliverer closures, not a reviewer verdict. Rows distinguish behavior
174
174
  | Actor discovery cached across binding changes | No actor cache. Reused-app fixture joins Alice, leaves, binds Bob, then expresses as current Bob without reentering Alice or claiming a second name. |
175
175
  | Join count derived by racy pre-read arithmetic | `square-actions.join` returns known joined-or-done count from its authoritative join transaction; takeover returns the same fact from its atomic done+join transaction. `joinOwned` uses returned count, never adds to a pre-read. Verified by transaction/source inspection; existing concurrent-owner/join tests remain passing. Standing headers remain distinct. |
176
176
  | Express cleanup/enrichment erases commit or calls it unsent | Resource close cannot replace a completed result. Action route/host publication is best effort after commit. Commit observation replaces duplicate post-read. Boundary fixtures prove postcommit host-publication failure returns the activity, projection-clock failure retains landed ID without replay, and selected-row corruption is unknown rather than proven unsent. Operation-cancellation has before-return commit abort coverage. |
177
- | Catch completion/enrichment erases a committed page | Raw page, ordinals/count/cap/hold/terminal facts travel with receipt selection; completion never rereads them. Only optional `catchPresence` can be unavailable. `performance`: real selection hydrates exactly its page; presence/rendering adds no hydration/reads. Operation-cancellation preserves committed consumption after abort. |
178
- | Replaced/cancelled waiter consumes later activity or releases new lease | Catch selection transaction checks lease ID. Finally cleanup is non-aborted and ID-fenced. Boundary fixtures prove replacement owns consumption, old waiter returns replaced, cancelled waiter releases lease, later attention stays pending. Hold/expiry and CLI watch-wait heartbeat fixtures pass. |
179
- | SDK projection roundtrip / leaked observations | SDK express uses `prepareExpression` + `landExpression`, strips observation/waited and retains delivery; SDK catch uses `catchOwned` and returns only established CatchResult. SDK test asserts established fields and avoids application body projections. SDK Square history stays unbounded, Participant recent-ten; parity history fixture proves both, and projection test preserves detached delivered predicate. |
177
+ | Catch completion/enrichment erases a committed page | Raw page, ordinals/count/cap/terminal facts travel with receipt selection; completion never rereads them. Only optional `catchPresence` can be unavailable. `performance`: real selection hydrates exactly its page; presence/rendering adds no hydration/reads. Operation-cancellation preserves committed consumption after abort. |
178
+ | Replaced/cancelled waiter consumes later activity or releases new lease | Catch selection transaction checks lease ID. Finally cleanup is non-aborted and ID-fenced. Boundary fixtures prove replacement owns consumption, old waiter returns replaced, cancelled waiter releases lease, later attention stays pending. Lease expiry and CLI watch-wait heartbeat fixtures pass. |
179
+ | SDK projection roundtrip / leaked observations | The current SDK result shapes are documented in [Square/Participant API](square-api.md); parity tests exercise the real resource and projection tests preserve the detached delivered predicate. |
180
180
  | Duplicate contracts, aliases and dead mixed views | Private status/unread producers import/re-export facade contracts; catch observation extends shared expression facts and enriched projection reuses it; required express participant replaces name alias. Retired helper search is empty. Recovery selects only gates/bounded unread summaries, not discarded feed/ordinals/perceptions. |
181
181
  | Packed owner orchestration hides effects/count integrity | History, lifecycle branches, maintenance/discovery/recovery, catch conflict and SDK recognize are expanded into readable steps. Remaining one-line SDK delegates are supported simple resource calls, not packed async policy. Inspect implementation diff; all new source is included in count. |
182
182
  | Invalid reply / done repair regressions | Typed `invalid_reply` covers malformed and valid-but-absent IDs; typed reach/done reasons stay domain facts, CLI owns glyphs/flags. Express-recovery tests execute printed repaired commands, preserving raw drafts, body candidates and other recipients. Unknown/landed effects offer reconciliation, never replay. |
@@ -0,0 +1,32 @@
1
+ # Attention audience resolution
2
+
3
+ The standalone `@astrosheep/square/attention` subpath computes an audience from
4
+ caller data. It does not open a square, send or consume anything, or grant permission.
5
+
6
+ ```ts
7
+ import { resolveAudience } from '@astrosheep/square/attention';
8
+
9
+ const audience = resolveAudience({
10
+ actor: 'east:alice',
11
+ recipients: ['east:bob', 'west:carol'],
12
+ mentions: ['west:carol'],
13
+ incoming: [{ listener: 'east:bob', kind: 'listen' }],
14
+ });
15
+ // [{ recipient: 'west:carol', route: 'mention' },
16
+ // { recipient: 'east:bob', route: 'attention' }]
17
+ ```
18
+
19
+ The caller supplies eligible recipients in canonical spelling and order, and owns
20
+ their validation and persistence. Identity is exact by default; `sameIdentity` may
21
+ provide a deterministic equivalence relation. The first equivalent recipient gives
22
+ the output spelling. Mentions precede the reply author, followed by listeners in
23
+ recipient order; ignores exclude all three. A `reach: 'bell'` reaches all eligible
24
+ recipients except the actor, bypassing ignores. Equivalent recipients appear once.
25
+
26
+ The caller decides whether a bell's recipient snapshot is local or global, and when
27
+ that snapshot is authoritative. Square supplies its standing participants and
28
+ incoming relationships at landing, inside its persistence transaction. The result
29
+ is computed attention, separate from delivery and consumption.
30
+
31
+ For an installed consumer composing this with receive and delivery in two equal,
32
+ independent domains, see the [symmetric domains example](symmetric-domains.md).
@@ -28,9 +28,9 @@ notifications are hints, not activity delivery or consumption receipts.
28
28
  returns promptly. An existing active delivery observer will reconsider release/expiry; absent an
29
29
  observer, pending work waits for the next normal delivery opportunity. A lease does not create a
30
30
  process that was not already running.
31
- - Blocking catch waits for conversation revision or its heartbeat/quiet deadline. A held catch keeps
31
+ - Blocking catch waits for conversation revision or its heartbeat/quiet deadline. An active catch keeps
32
32
  its single lease alive and pauses quiet expiry. Core catch and streams use the same storage change
33
- detector. Held express also waits for changes; throttling waits for its eligibility time.
33
+ detector. Throttling waits for its eligibility time.
34
34
  - Deferrals are independent of catch filters, presentation claims, full-delivery seen receipts,
35
35
  clipped previews and session-local cancellation. Sharing storage detection does not share session
36
36
  delivery state. Cancellation never creates a receipt or removes archive history. A deferral cannot
@@ -5,6 +5,10 @@ contains only `{"modules":["./register.js"]}`. Install with `square install clau
5
5
  and load/approve it in a trusted interactive Claude terminal. Installation is
6
6
  not evidence of a live receiver.
7
7
 
8
+ Public `Square.at` and `Square.build` use the default Claude/Codex/Paseo wake transport when
9
+ `wakeTransport` is omitted. Supply a port to replace it or `false` to disable proactive wake;
10
+ synchronous `Square.inMemory` uses only an explicit port. See [square-api.md](square-api.md).
11
+
8
12
  ## Validated capability
9
13
 
10
14
  The supported baseline is **macOS Claude Code with the mod actually loaded**;
@@ -24,7 +28,7 @@ An absent/broken endpoint is unavailable; it does not consume Square activity.
24
28
  Native delivery is preferred, including under Paseo. CLI and call-local MCP Codex
25
29
  joins register the native queue for their thread. Claude and OpenCode receivers
26
30
  register their native wake addresses only through their loaded integrations. An
27
- ordinary reconnect or implicit join cannot replace a current native address with a
31
+ ordinary reconnect or reattachment cannot replace a current native address with a
28
32
  generic Paseo proposal. Addresses belong to the artifact seat generation, not an
29
33
  expiring routes table: explicit receiver withdrawal permits fallback, and a new
30
34
  seat generation cannot inherit the previous generation's native registration.
@@ -95,15 +99,20 @@ Initial activation and hot reload use `session.start`. New conversations use
95
99
  `classic.SessionStart`: clear reads the new API coordinate; **resume's event
96
100
  `session_id` is already the resumed coordinate while `$.session.id()` still reads
97
101
  the ending one (observed in 2.1.295)**. The event coordinate is used at that boundary, with
98
- API cwd/version/socket. Modern `session.end` retires only its pinned `e.sessionId`
99
- and captured ownership epochs, within the event budget. Late committed joins
102
+ API cwd/version/socket. Modern `session.end` detaches only its pinned `e.sessionId`
103
+ and captured owner/join generations, within the event budget. Late committed joins
100
104
  are discovered at the pinned end timestamp. A process crash can omit end.
101
105
 
102
- A resumed conversation restores only memberships whose last lifecycle activity
103
- is exactly the `done` recorded by its previous confirmed end in the existing
104
- host ledger. It never takes another owner's participant or re-enters a later
105
- explicit user `done`. Restoration advances the binding epoch, so callbacks and
106
- old ends cannot retire that replacement. Unchanged reload preserves epochs.
106
+ Host end is not a participant departure: no `done` lands, and membership, listens,
107
+ read horizon and generation remain intact. Catch ownership, deferrals and wake readiness
108
+ are cleared; the participant appears away and keeps accumulating unread audience without
109
+ proactive delivery. Before arming a receiver, start/resume discovers the exact owner's
110
+ standing seats through `seatsOf(owner)` and restores only detached ones with
111
+ `reattach(name, {owner, wakeAddress?})`. The adapter obtains that owner with
112
+ `harnessOwner({channel: 'claude-code', session, agent?})`. One failed seat or location does not block the
113
+ others. New/forked harness owners restore nothing. Neither a voluntary `done` nor a takeover is
114
+ revived; there is no completed-seat recovery path or timeout that turns away into done.
115
+ See [participant-ownership.md](participant-ownership.md).
107
116
 
108
117
  ## Body, admission, and presentation
109
118
 
@@ -143,7 +152,7 @@ Explicit plugin `turn.abort` and terminal `turn.complete(reason:'aborted')`
143
152
  suppress the outstanding local batch without recording seen. Ordinary answer,
144
153
  refusal, and error completion are not user cancellation. Separate engine-owned
145
154
  family state survives hot reload, containing the abort timestamp and frozen
146
- membership epochs. Same-epoch host-ledger presence stores a monotonic activity
155
+ membership generations. The artifact stores a generation-scoped monotonic activity
147
156
  cutoff; replacement generations cannot inherit the old abort.
148
157
 
149
158
  Engine state reads are snapshots of one dispatch. Confirmation therefore uses
@@ -5,14 +5,22 @@ maintaining a second hook protocol, boundary store or CLI sender. The standalone
5
5
  package's [README](../packages/agent-delivery/README.md) describes both its native
6
6
  app-server capability and its host-owned hook integration.
7
7
 
8
+ Public `Square.at` and `Square.build` use the default Claude/Codex/Paseo wake transport when
9
+ `wakeTransport` is omitted. Supply a port to replace it or `false` to disable proactive wake;
10
+ synchronous `Square.inMemory` uses only an explicit port. See [square-api.md](square-api.md).
11
+
8
12
  ## Square's hook path
9
13
 
10
14
  `plugins/codex/hooks/hooks.json` invokes `square codex-hook`:
11
15
 
12
16
  - `SessionStart`, `SessionResume` and `UserPromptSubmit` invalidate the previous
13
- Stop observation. `SessionEnd` marks the session ended and ends Square's owned
14
- memberships. Later Stop/tool callbacks cannot re-enable queue until a new
15
- `SessionStart` or `SessionResume`.
17
+ Stop observation. `SessionEnd` marks the host session ended and detaches its exact
18
+ owner's seats without landing `done`, ending membership, dropping listens or resetting
19
+ the read horizon. Later Stop/tool callbacks cannot re-enable queue until a new
20
+ `SessionStart` or `SessionResume`. Before enabling delivery, those start paths use
21
+ `seatsOf(owner)` and `reattach(name, {owner, wakeAddress?})` to restore only detached
22
+ standing seats of that exact owner; voluntary departures and takeovers are never revived.
23
+ A new/forked session restores nothing.
16
24
  - `PostToolUse` asks Square for current pending activity and returns the existing
17
25
  bounded `additionalContext` presentation.
18
26
  - `Stop` records queue eligibility and sweeps pending work, but does not itself
@@ -20,7 +28,7 @@ app-server capability and its host-owned hook integration.
20
28
 
21
29
  The shared handler owns hook framing, lifecycle sequencing, its cancellation
22
30
  budget and bounded sweeps. Square supplies activity selection, formatting,
23
- artifact-owned presentation claims, cancellation, membership cleanup and
31
+ artifact-owned presentation claims, cancellation, attachment cleanup and
24
32
  pending-work sweeps. The handler cooperatively awaits Square's pull: the signal
25
33
  cancels work before presentation, but a successful presentation's `settleAs`
26
34
  finishes independently of request cancellation. Even when that settlement crosses
@@ -28,11 +36,14 @@ the internal budget, the hook waits for it and returns the associated context;
28
36
  it never abandons settlement that can mark activity seen without returning text.
29
37
  After expiry, no sweep starts, and a sweep timeout cannot erase successful pull
30
38
  output. Settlement must still finish within Codex's external hook timeout. Hook inbox lookup uses
31
- an exact Body (`{ channel: 'codex', session }`), not a bare session id. The shared
39
+ an exact owner from `harnessOwner({ channel: 'codex', session })`, not a bare session id. The shared
32
40
  boundary store uses Square's caller-selected `SQUARE_CODEX_BOUNDARIES` file,
33
41
  defaulting to `~/.square/codex-boundaries.json`; its JSON observations are not
34
42
  conversation storage and are not proof of current native execution or consumption.
35
- Square's format5 artifact remains authoritative for seats and delivery evidence.
43
+ Square's format-8 artifact remains authoritative for membership, attachment and delivery evidence.
44
+ Detached members appear away indefinitely and keep accumulating unread audience, but have no
45
+ write or proactive delivery eligibility until same-owner reattachment. Only a host-confirmed
46
+ permanent deletion uses fenced membership-ending cleanup; Codex SessionEnd is not that proof.
36
47
 
37
48
  `CodexQueueTransport` implements the existing `WakeTransportPort` and uses the public
38
49
  `connect({ harness: 'codex', transport: 'cli', sessionId, boundaries, ... })`
@@ -0,0 +1,180 @@
1
+ # Delivery custody primitives
2
+
3
+ `@astrosheep/square/delivery` provides storage-independent attempt transitions, suppression predicates,
4
+ and four bound effect functions: `dispatchEffect`, `preparePresentation`, `presentPrepared`, and
5
+ `presentPreparedBatch`. It does not open a square, interpret identities, own transactions, or schedule
6
+ retries. Square's actual dispatch, single presentation and boundary batch adapters compose these same
7
+ lifecycles; its SQLite adapter uses the attempt transitions with existing tables. Ownership, membership,
8
+ cancellation and delivery-deferral policy remain in the bound application capability.
9
+
10
+ ## Bind a series inside a transaction
11
+
12
+ `AttemptRows<D>` is a capability for **one logical item / recipient / authority / purpose series**.
13
+ The caller chooses those coordinates, including arbitrary opaque strings, and binds the store to them.
14
+ No Square name, seat, generation, activity-id codec or composite-key format is required.
15
+
16
+ - `list()` returns that series newest ordinal first.
17
+ - `byToken(token)` returns only a matching attempt in that series.
18
+ - `insert(attempt)` and `replace(attempt)` write within the caller's current transaction.
19
+ - Replacements preserve purpose, token, ordinal and the caller's external series coordinates.
20
+
21
+ The caller guarantees isolation against competing claims, token uniqueness within its token lookup
22
+ scope, atomic commit/rollback, and durable storage where needed. Run each operation and its reads and
23
+ writes inside that boundary; an asynchronous read followed later by an unguarded write is not custody.
24
+ Stores must not retain mutable references that allow a caller to change durable rows without a write.
25
+ `D` belongs entirely to the caller. The primitive does not merge, serialize or interpret it.
26
+
27
+ ```ts
28
+ import { claimAttempt, prepareAttempt, settleAttempt, type AttemptRows } from '@astrosheep/square/delivery';
29
+
30
+ type Payload = { route?: string; nativeToken?: string; ack?: string };
31
+ // Supplied by the application, already scoped and transaction-bound:
32
+ declare const rows: AttemptRows<Payload>;
33
+
34
+ const claim = claimAttempt(rows, {
35
+ purpose: 'dispatch', token: 'application-generated-unique-token',
36
+ at: 1000, leaseMs: 30_000, data: { route: 'opaque endpoint' },
37
+ });
38
+ if (claim.status === 'acquired') {
39
+ prepareAttempt(rows, { token: claim.token, data: { route: 'opaque endpoint', nativeToken: 'correlation' } });
40
+ }
41
+ // In a later transaction, only after external proof of acceptance:
42
+ settleAttempt(rows, {
43
+ token: 'application-generated-unique-token', outcome: 'accepted', at: 1100,
44
+ data: { route: 'opaque endpoint', nativeToken: 'correlation', ack: 'external proof' },
45
+ });
46
+ ```
47
+
48
+ The example shows only transaction-scoped transitions. Commit a successful claim before external I/O;
49
+ settle its exact token in a new transaction after observing the result. The bound effect capability
50
+ below connects those operations without introducing a generic transaction runtime. Authority fencing,
51
+ currentness policy and the meaning of external proof remain the application's responsibility.
52
+ An `accepted` dispatch is not proof of presentation or consumption. Only the responsible presentation
53
+ boundary may assert `presented`; consuming an item is a separate application operation.
54
+
55
+ ## Transitions
56
+
57
+ An `Attempt<D>` has `purpose: 'dispatch' | 'presentation'`, `token`, zero-based `ordinal`, `outcome`,
58
+ finite `at`, optional `expiresAt`, and `data`.
59
+
60
+ - `claimAttempt` first returns terminal evidence: dispatch `accepted`/`unknown`, presentation
61
+ `presented`/`unknown`. Otherwise a live `dispatching` attempt is `busy`; missing expiry is live.
62
+ - At exact expiry or later, an interrupted `dispatching` attempt becomes `unknown`, keeps its original
63
+ payload, loses expiry, and returns `terminal` with `interrupted: true`. There is no new attempt and
64
+ **no automatic replay of uncertainty**. Square maps that flag to its interruption diagnostic.
65
+ - With neither terminal nor dispatching evidence, claim inserts `dispatching` at the next ordinal.
66
+ Failed/released attempts can therefore be retried. Purpose-specific terminal meanings are not
67
+ inferred from other completion labels.
68
+ - `prepareAttempt` replaces only the data of the exact `dispatching` token. Pass the complete merged
69
+ payload. It returns false for an absent or no-longer-dispatching token. Callers that already know
70
+ the payload is unchanged may skip the replacement; core performs no JSON equality checks.
71
+ - `settleAttempt` allows `dispatching` to any defined completion, and late `unknown` to `accepted`.
72
+ It clears expiry and sets time/outcome/data. `accepted` to `accepted` returns true **without any
73
+ write or payload/time mutation**. All other transitions return false, including `unknown` to
74
+ `presented`. Missing or foreign tokens cannot settle another series.
75
+
76
+ Invalid purpose/completion/token/time, non-positive or unsafe lease durations, non-finite expiry, and
77
+ unsafe next ordinals reject before mutation with `TypeError` or `RangeError`, not `SquareError`.
78
+ The caller supplies valid stored attempts and consistently scoped, ordered rows.
79
+
80
+ ## Suppression without payloads
81
+
82
+ `presentationBlocksDispatch(attempts, now)` and `automaticDispatchEligible(attempts, now)` accept only
83
+ `purpose`, `outcome`, and optional `expiresAt`; no dummy payload or identity is needed. These predicates
84
+ may see both purposes for the same logical delivery.
85
+
86
+ Presentation `presented`, `clipped`, `unknown`, or live `dispatching` blocks dispatch. Expired
87
+ presentation dispatching alone does not. Automatic dispatch additionally requires no dispatch
88
+ `accepted`, `unknown`, or `dispatching`; even expired dispatch custody blocks automatic replay.
89
+ These are preselection predicates, not substitutes for a transactional claim or application gates.
90
+
91
+
92
+ ## Bound effects: one captured capability, four functions
93
+
94
+ `EffectPort<C>` is bound by the caller to **one captured work item / recipient / authority / purpose**.
95
+ It exposes only:
96
+
97
+ - `claim(signal?)`: the application's existing atomic claim, returning `{ token, ordinal }` or
98
+ `undefined` when busy, terminal, stale or deferred. The runner does not claim the attempt twice.
99
+ - `current(signal?)`: pre-effect policy; a rejected check is unverifiable and treated as false.
100
+ - `settle(claim, completion)`: one captured settlement operation, with no request signal. Return true
101
+ only when it commits, false when not recorded (not necessarily stale), or throw on persistence error.
102
+
103
+ The application owns tokens, leases, semantic timestamps, authority, proof/diagnostic payloads and
104
+ resource lifetimes. There is no public transaction type, series-key codec or extra persisted lifecycle.
105
+ `Settlement<C>` keeps the selected completion separate from persistence: `recorded`, `not-recorded`,
106
+ or `error` with the original error. Observations are never erased by persistence refusal/error.
107
+
108
+ ### Dispatch
109
+
110
+ ```ts
111
+ import { dispatchEffect, type EffectPort, type DispatchCompletion } from '@astrosheep/square/delivery';
112
+
113
+ declare const port: EffectPort<DispatchCompletion>; // bound claim/current/settle
114
+ // A sender must check its absolute deadline/cancellation immediately before external mutation.
115
+ const report = await dispatchEffect(port, async context => {
116
+ if (!await context.beforeSend()) return { outcome: 'not-capable' };
117
+ if (context.signal?.aborted || Date.now() >= context.deadline) return { outcome: 'unknown' };
118
+ // Call the application's transport here, passing the remaining budget and captured claim.
119
+ return { outcome: 'accepted', data: { proof: 'application acknowledgement' } };
120
+ }, { timeoutMs: 5000 });
121
+ ```
122
+
123
+ `dispatchEffect` owns claim → initial currentness → send → settlement. Cancellation suppresses
124
+ pre-effect work. It starts its timer immediately before invoking the sender, not during claim,
125
+ initial policy reads or persistence. Exactly one returned/thrown/timeout decision is frozen before
126
+ settlement. A sender exception defaults to `unknown`, not failed; returned `not-capable` releases
127
+ custody. A denied final gate releases with `not-current / final` under the sender's obligation to
128
+ obey it. No automatic retry occurs.
129
+
130
+ `beforeSend()` is **policy currentness, not an elapsed-budget gate**. A stalled policy check may
131
+ return true after the outer timeout. The sender must recheck `deadline` and `signal` after that await
132
+ and immediately before I/O; true does not authorize a late send. Outer timeout stays unknown even
133
+ when no I/O is known to have started. Late gates and late promise results cannot rewrite the frozen
134
+ settlement. Separately correlated native proof may still use the attempt transition's permitted late
135
+ unknown → accepted path; the timed-out runner does not reconcile itself.
136
+
137
+ ### Prepared presentation
138
+
139
+ `preparePresentation({ payload, coverage, port }, { signal? })` acquires custody without running a
140
+ sink. A returned `PreparedPresentation<P>` exposes `payload`, pre-effect `eligible(signal?)`, and
141
+ promise-cached `release()` for abandonment. It is a local one-shot capability, not a durable row and
142
+ not a public success writer.
143
+
144
+ `presentPrepared(handle, sink, control?)` specializes the batch lifecycle to one item.
145
+ `presentPreparedBatch(handles, render, sink, control?)` takes all supplied custody before its first
146
+ await. It rejects duplicate, foreign or used handles before any effect, checks eligibility, then calls
147
+ synchronous side-effect-free `render(eligible)` returning `{ payload, included }`. Included handles
148
+ must be a unique exact subset of those eligible handles; invalid rendering releases custody and
149
+ rejects without presentation evidence. No included items means no sink call.
150
+
151
+ Cancellation is checked immediately before the one sink invocation. There is **no presentation timer**
152
+ and no post-effect cancellation, deferral or currentness veto. The runner awaits the sink and marks
153
+ **all included handles** effect-observed before starting asynchronous settlements. Full coverage
154
+ settles `presented`; clipped coverage settles `clipped`. A thrown sink defaults to `unknown`;
155
+ `classifyError` may explicitly return `failed` (classifier failure stays unknown). Unused handles are
156
+ released, all sibling settlements and cleanup drain even on persistence failure, and later cleanup
157
+ cannot overwrite success or uncertainty.
158
+
159
+ ### Mandatory atomic full-consumption settlement
160
+
161
+ The presentation binding's single `settle` operation must enforce:
162
+
163
+ | Completion | Atomic persistence |
164
+ | --- | --- |
165
+ | `presented` | Complete the captured attempt **and all captured full-consumption receipts in the same transaction**. Return true only when the combined operation commits. |
166
+ | `clipped` | Complete the attempt; no full-consumption receipt. |
167
+ | `failed`, `unknown`, `released` | Complete the attempt; no success consumption receipt. |
168
+
169
+ If receipt writing throws, both receipt writes and attempt completion must roll back. Do not reselect
170
+ items or split `complete` and `recordConsumed` into separately awaited callbacks. Captured ownership
171
+ still fences settlement; request cancellation and new delivery deferrals do not. TypeScript cannot
172
+ supply isolation for a caller, so this is a mandatory port contract, tested in Square's real transaction.
173
+
174
+ Presentation reports distinguish observed sink behavior from durable persistence. A returned sink
175
+ with `not-recorded` or `error` still returned successfully; that alone proves only the supplied sink's
176
+ documented effect, not model consumption or human display. Square explicitly classifies its existing
177
+ rejected sinks as failed and rethrows the original exception after draining. Its single result stays
178
+ `{ presented: true, activity }` only on recorded success, false on ownership refusal; unexpected
179
+ success-settlement errors propagate. Batch returns its original sink value after all settlements,
180
+ with the same error propagation. No schema, storage format or public Square result changes are needed.