@astrosheep/square 0.3.94 → 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 (85) hide show
  1. package/dist/artifact.js +1 -1
  2. package/dist/attention.d.ts +19 -0
  3. package/dist/attention.js +30 -0
  4. package/dist/boundary-presentation.js +33 -40
  5. package/dist/catch-decisions.d.ts +1 -2
  6. package/dist/catch-decisions.js +41 -21
  7. package/dist/cli/context.js +0 -2
  8. package/dist/cli/express-presentation.js +3 -3
  9. package/dist/cli/json.js +1 -1
  10. package/dist/cli/program.js +1 -1
  11. package/dist/cli/registry.js +1 -3
  12. package/dist/cli/square-commands.d.ts +0 -2
  13. package/dist/cli/square-commands.js +21 -27
  14. package/dist/decisions.d.ts +0 -5
  15. package/dist/decisions.js +0 -13
  16. package/dist/delivery-attempts.d.ts +52 -0
  17. package/dist/delivery-attempts.js +73 -0
  18. package/dist/delivery-effects.d.ts +139 -0
  19. package/dist/delivery-effects.js +163 -0
  20. package/dist/delivery-operations.js +43 -68
  21. package/dist/delivery-primitives.d.ts +5 -0
  22. package/dist/delivery-primitives.js +3 -0
  23. package/dist/delivery.d.ts +1 -1
  24. package/dist/delivery.js +6 -5
  25. package/dist/help.js +4 -6
  26. package/dist/http-delivery.d.ts +16 -0
  27. package/dist/http-delivery.js +101 -0
  28. package/dist/model.d.ts +4 -6
  29. package/dist/model.js +1 -1
  30. package/dist/notifications.js +2 -0
  31. package/dist/operation-presentation.d.ts +1 -2
  32. package/dist/operation-presentation.js +12 -20
  33. package/dist/ports.d.ts +5 -14
  34. package/dist/presentation-operations.d.ts +14 -19
  35. package/dist/presentation-operations.js +47 -57
  36. package/dist/presentation.d.ts +2 -9
  37. package/dist/presentation.js +13 -48
  38. package/dist/receive.d.ts +66 -0
  39. package/dist/receive.js +54 -0
  40. package/dist/runtime.d.ts +0 -1
  41. package/dist/runtime.js +2 -3
  42. package/dist/sqlite-square.d.ts +0 -1
  43. package/dist/sqlite-square.js +29 -55
  44. package/dist/square-api.d.ts +1 -1
  45. package/dist/square-api.js +1 -1
  46. package/dist/square-core.d.ts +0 -9
  47. package/dist/square-domain.js +9 -31
  48. package/dist/square-facade.d.ts +5 -21
  49. package/dist/square-legacy-framing.d.ts +2 -0
  50. package/dist/square-legacy-framing.js +8 -0
  51. package/dist/square-migration.d.ts +3 -1
  52. package/dist/square-migration.js +46 -13
  53. package/dist/square-receive.d.ts +6 -0
  54. package/dist/square-receive.js +18 -0
  55. package/dist/square-sqlite.d.ts +5 -3
  56. package/dist/square-sqlite.js +89 -90
  57. package/dist/square-tools.js +3 -11
  58. package/dist/views.d.ts +2 -3
  59. package/dist/views.js +13 -19
  60. package/dist/wakes.d.ts +0 -3
  61. package/dist/wakes.js +3 -5
  62. package/examples/symmetric-domains.mjs +250 -0
  63. package/guides/application-facade-delivery-evidence.md +5 -5
  64. package/guides/attention-primitive.md +32 -0
  65. package/guides/change-waits.md +2 -2
  66. package/guides/codex-delivery.md +1 -1
  67. package/guides/delivery-primitives.md +180 -0
  68. package/guides/mcp.md +1 -1
  69. package/guides/migration.md +12 -6
  70. package/guides/participant-ownership.md +1 -1
  71. package/guides/pi-tools.md +2 -2
  72. package/guides/primitive-catch-consolidation.md +5 -5
  73. package/guides/primitive-history-consolidation.md +2 -2
  74. package/guides/primitive-status-consolidation.md +3 -3
  75. package/guides/primitive-storage-consolidation.md +1 -1
  76. package/guides/receive-primitive.md +90 -0
  77. package/guides/row-runtime-delivery-evidence.md +1 -1
  78. package/guides/row-storage-design.md +17 -23
  79. package/guides/square-api.md +77 -10
  80. package/guides/symmetric-domains.md +92 -0
  81. package/package.json +14 -1
  82. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  83. package/plugins/claude/skills/square/SKILL.md +2 -6
  84. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  85. package/skills/square/SKILL.md +2 -6
@@ -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,8 +174,8 @@ 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. |
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
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. |
@@ -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
@@ -40,7 +40,7 @@ an exact owner from `harnessOwner({ channel: 'codex', session })`, not a bare se
40
40
  boundary store uses Square's caller-selected `SQUARE_CODEX_BOUNDARIES` file,
41
41
  defaulting to `~/.square/codex-boundaries.json`; its JSON observations are not
42
42
  conversation storage and are not proof of current native execution or consumption.
43
- Square's format-7 artifact remains authoritative for membership, attachment and delivery evidence.
43
+ Square's format-8 artifact remains authoritative for membership, attachment and delivery evidence.
44
44
  Detached members appear away indefinitely and keep accumulating unread audience, but have no
45
45
  write or proactive delivery eligibility until same-owner reattachment. Only a host-confirmed
46
46
  permanent deletion uses fenced membership-ending cleanup; Codex SessionEnd is not that proof.
@@ -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.
package/guides/mcp.md CHANGED
@@ -4,7 +4,7 @@ Square provides a stdio MCP server through `square mcp-server`.
4
4
 
5
5
  Set `SQUARE_LOCATION` to the default `.square` artifact path; calls may override it with `location`. Relative paths use the server's startup working directory. Join with an explicit `as` name. Later calls may omit `as` only when the artifact recognizes exactly one current name held by the call's exact opaque owner; zero or multiple matches refuse and require explicit selection. An explicit name selects a voice but never grants custody. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) coordinate when present, encoded through `harnessOwner`; otherwise the server-configured owner applies. CLI startup resolves `--owner`/`SQUARE_OWNER`, a complete harness coordinate, or the local user owner in that order, refusing incomplete harness identity. Owner is never a model tool argument. Read-only calls do not need an owner.
6
6
 
7
- The server exposes `join`, `express`, `catch`, `history`, `attention`, `hold`, `done`, and `status`. `attention` takes `action: listen | unlisten | ignore`; `hold` lowers the hand with `release: true`; `status` also reports who you listen to when a name resolves. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. All eight tools return the same human-readable text as the CLI in `content`, without `outputSchema` or `structuredContent`. Square failures set `isError: true` and return readable refusal and recovery text in `content`. Clients keep their native MCP result envelope; machine-readable domain results remain available through the SDK.
7
+ The server exposes `join`, `express`, `catch`, `history`, `attention`, `done`, and `status`. `attention` takes `action: listen | unlisten | ignore`; `status` also reports who you listen to when a name resolves. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. All seven tools return the same human-readable text as the CLI in `content`, without `outputSchema` or `structuredContent`. Square failures set `isError: true` and return readable refusal and recovery text in `content`. Clients keep their native MCP result envelope; machine-readable domain results remain available through the SDK.
8
8
 
9
9
  Claude's bundled plugin configures this server automatically. Its mod supplies live session/cwd/subagent attribution through an out-of-band tool-use-ID association, not AI-visible arguments; see [Claude call identity](claude-native.md#mcp-tools-and-call-identity). The bundled endpoint requires that association rather than falling back to its startup environment.
10
10
 
@@ -4,7 +4,8 @@ Square stores its authority in one versioned SQLite artifact. Format 1 kept a sn
4
4
  format 2 introduced typed facts; format 3 added `unlisten`; format 4 moved participant custody
5
5
  and delivery evidence into the artifact; format 5 added independent delivery deferrals.
6
6
  **Format 6 separates participant membership from host attachment; format 7 replaces harness Body
7
- coordinates with opaque owners.** Existing artifacts are never converted implicitly.
7
+ coordinates with opaque owners; format 8 removes Square hold/resume activities and held state.**
8
+ Existing artifacts are never converted implicitly.
8
9
 
9
10
  Stop all processes using the artifact, make an offline backup, then run:
10
11
 
@@ -13,12 +14,16 @@ square --location /path/to/SQUARE.square migrate
13
14
  square --location /path/to/SQUARE.square doctor
14
15
  ```
15
16
 
16
- The command handles supported formats 2–6 in place and publishes only final format 7, in one
17
+ The command handles supported formats 2–7 in place and publishes only final format 8, in one
17
18
  transaction. It does not make a backup, stop processes, install a runtime or update another
18
19
  artifact. For formats 2/3, also freeze and back up the legacy HostLedger used for owner/evidence
19
20
  import. A legacy ledger needing its own conversion must be prepared separately with its old
20
21
  runtime's offline tooling. Snapshot format 1 is not accepted by this conversion.
21
22
 
23
+ Format 7 to 8 deletes hold/resume activities and rows keyed to them, clears replies that point to
24
+ deleted activities, and preserves every surviving activity index. Gaps remain legal; `lastActIndex`
25
+ is the greatest surviving index and `nextActIndex` never reuses a deleted index.
26
+
22
27
  ## Membership and attachment after conversion
23
28
 
24
29
  Membership owns a participant's join/done history, read horizon and outgoing listens. Attachment
@@ -101,9 +106,10 @@ not import. Evidence without current or verified completed ownership is skipped
101
106
  read-only legacy ledger remains available for offline diagnosis; an absent ledger permits route-
102
107
  derived custody, but an unreadable or unsupported ledger refuses before changing the square.
103
108
 
104
- One transaction imports verified authority/evidence, adds deferral and attachment support and
105
- publishes format 7. Existing conversation and membership facts are preserved; no intermediate
106
- format is published. Legacy catch ownership is generation-bound only for exact standing owner
109
+ One transaction imports verified authority/evidence, adds deferral and attachment support, applies
110
+ the format-7-to-8 deletion, and publishes format 8. Existing conversation and membership facts are
111
+ preserved apart from the deleted hold/resume rows and cleared replies; no intermediate format is
112
+ published. Legacy catch ownership is generation-bound only for exact standing owner
107
113
  spellings; otherwise it stays unbound. Legacy routes are consumed only during migration and are
108
114
  not a live authority source. The new delivery revision starts at zero. Format 2 first adds support
109
115
  for `unlisten`, then follows the same conversion.
@@ -142,7 +148,7 @@ rows are ignored. Rejoin with the exact migrated owner to republish discovery (o
142
148
  harness reconnect). This preserves generation, listens and unread activity; do not use takeover
143
149
  merely to rebuild the index.
144
150
 
145
- Upgrade all attached runtimes before reopening format 7; older runtimes must not be mixed with it.
151
+ Upgrade all attached runtimes before reopening format 8; older runtimes must not be mixed with it.
146
152
  Updating a package does not migrate an artifact, and migration does not install upgraded runtimes.
147
153
  After conversion, run read-only `doctor` before restarting clients. To roll back a committed
148
154
  conversion, stop all clients and restore the offline backup with its compatible older runtime;
@@ -53,7 +53,7 @@ for resource lifetimes and adapter selection.
53
53
  failed reattachment does not block the others. A new or forked session has a different owner and
54
54
  restores nothing. Hosts never infer restoration from a historical departure.
55
55
  - Participant mutations require both the captured generation and an attached current seat.
56
- Detach suspends existing handles; they cannot express, catch, listen, hold, leave or change
56
+ Detach suspends existing handles; they cannot express, catch, listen, leave or change
57
57
  deferrals until reattachment. Rejoin/takeover fences old handles permanently, even if the same
58
58
  owner later reacquires the name. Host cleanup checks owner and generation inside its transaction;
59
59
  stale, foreign, done or already-detached detach changes nothing. Host `cancelDeliveries` also
@@ -1,6 +1,6 @@
1
1
  # Pi native tools
2
2
 
3
- The existing Square Pi extension registers `square_join`, `square_express`, `square_catch`, `square_history`, `square_attention`, `square_hold`, `square_done`, and `square_status`. `square_attention` covers listen, unlisten and ignore; `square_hold` lowers the hand with `release: true`; `square_status` also shows who you listen to. No additional MCP server or subprocess is needed.
3
+ The existing Square Pi extension registers `square_join`, `square_express`, `square_catch`, `square_history`, `square_attention`, `square_done`, and `square_status`. `square_attention` covers listen, unlisten and ignore; `square_status` also shows who you listen to. No additional MCP server or subprocess is needed.
4
4
 
5
5
  Tools use the `square` namespace with codemode exposure. Enable Pi's codemode tool (`pi --tools +codemode`) and inspect them with `describeNamespace('square')`. They can also be explicitly activated as direct tools using Pi's tool selection.
6
6
 
@@ -22,7 +22,7 @@ overview if there are zero or multiple. The extension encodes the live Pi sessio
22
22
  `harnessOwner({channel: 'pi', session})`, never a model-provided session id, without mutating
23
23
  the process environment.
24
24
 
25
- All eight tools return the CLI's human-readable text, without an `outputSchema` or
25
+ All seven tools return the CLI's human-readable text, without an `outputSchema` or
26
26
  `structuredContent`. In Pi codemode, a successful call resolves to a `string`:
27
27
  use `text(result)`, not `text(result.text)`. Failures are marked as failed tool results;
28
28
  codemode rejects with the readable error and recovery commands rather than returning