@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.
- package/dist/activity-feed.d.ts +3 -5
- package/dist/activity-feed.js +16 -14
- package/dist/artifact.js +3 -3
- package/dist/attention.d.ts +19 -0
- package/dist/attention.js +30 -0
- package/dist/boundary-presentation.d.ts +2 -2
- package/dist/boundary-presentation.js +33 -40
- package/dist/catch-decisions.d.ts +5 -8
- package/dist/catch-decisions.js +45 -25
- package/dist/claude-delivery-status.js +10 -4
- package/dist/claude-delivery.js +19 -11
- package/dist/claude-mod.js +24 -53
- package/dist/cli/context.d.ts +9 -4
- package/dist/cli/context.js +30 -27
- package/dist/cli/express-presentation.d.ts +3 -1
- package/dist/cli/express-presentation.js +28 -16
- package/dist/cli/json.d.ts +3 -0
- package/dist/cli/json.js +14 -0
- package/dist/cli/mcp-server-command.js +2 -2
- package/dist/cli/observation-commands.d.ts +1 -3
- package/dist/cli/observation-commands.js +43 -56
- package/dist/cli/program.js +3 -3
- package/dist/cli/registry.js +7 -6
- package/dist/cli/square-commands.d.ts +0 -2
- package/dist/cli/square-commands.js +30 -44
- package/dist/codex-hook.d.ts +3 -2
- package/dist/codex-hook.js +13 -4
- package/dist/codex-queue.js +4 -4
- package/dist/decisions.d.ts +5 -16
- package/dist/decisions.js +9 -33
- package/dist/delivery-attempts.d.ts +52 -0
- package/dist/delivery-attempts.js +73 -0
- package/dist/delivery-deferral.js +2 -2
- package/dist/delivery-effects.d.ts +139 -0
- package/dist/delivery-effects.js +163 -0
- package/dist/delivery-health.d.ts +0 -1
- package/dist/delivery-health.js +1 -3
- package/dist/delivery-operations.d.ts +2 -2
- package/dist/delivery-operations.js +76 -101
- package/dist/delivery-primitives.d.ts +5 -0
- package/dist/delivery-primitives.js +3 -0
- package/dist/delivery.d.ts +1 -1
- package/dist/delivery.js +7 -6
- package/dist/help.js +14 -14
- package/dist/http-delivery.d.ts +16 -0
- package/dist/http-delivery.js +101 -0
- package/dist/inbox.d.ts +9 -22
- package/dist/inbox.js +23 -27
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +23 -24
- package/dist/model.d.ts +12 -21
- package/dist/model.js +5 -8
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +8 -5
- package/dist/operation-presentation.d.ts +6 -6
- package/dist/operation-presentation.js +55 -58
- package/dist/owner.d.ts +11 -0
- package/dist/owner.js +32 -0
- package/dist/paseo-delivery.d.ts +4 -1
- package/dist/paseo-delivery.js +7 -12
- package/dist/paseo-state.d.ts +2 -2
- package/dist/paseo-state.js +6 -4
- package/dist/plugins/opencode/delivery.js +27 -15
- package/dist/ports.d.ts +18 -47
- package/dist/presentation-operations.d.ts +14 -19
- package/dist/presentation-operations.js +51 -61
- package/dist/presentation.d.ts +7 -17
- package/dist/presentation.js +46 -91
- package/dist/receive.d.ts +66 -0
- package/dist/receive.js +54 -0
- package/dist/runtime.d.ts +1 -8
- package/dist/runtime.js +2 -17
- package/dist/seats.d.ts +2 -3
- package/dist/seats.js +7 -15
- package/dist/session-lifecycle.d.ts +6 -2
- package/dist/session-lifecycle.js +39 -11
- package/dist/sqlite-square.d.ts +36 -23
- package/dist/sqlite-square.js +162 -211
- package/dist/square-api.d.ts +16 -11
- package/dist/square-api.js +27 -23
- package/dist/square-core.d.ts +0 -14
- package/dist/square-core.js +0 -9
- package/dist/square-domain.d.ts +4 -2
- package/dist/square-domain.js +65 -52
- package/dist/square-facade.d.ts +47 -105
- package/dist/square-index.d.ts +5 -13
- package/dist/square-index.js +13 -38
- package/dist/square-legacy-framing.d.ts +2 -0
- package/dist/square-legacy-framing.js +8 -0
- package/dist/square-location.d.ts +1 -14
- package/dist/square-location.js +4 -70
- package/dist/square-migration-host-ledger.d.ts +2 -2
- package/dist/square-migration-host-ledger.js +6 -5
- package/dist/square-migration.d.ts +4 -1
- package/dist/square-migration.js +93 -38
- package/dist/square-receive.d.ts +6 -0
- package/dist/square-receive.js +18 -0
- package/dist/square-sqlite.d.ts +9 -3
- package/dist/square-sqlite.js +139 -114
- package/dist/square-tools.d.ts +7 -10
- package/dist/square-tools.js +21 -61
- package/dist/stream.js +4 -5
- package/dist/views.d.ts +23 -13
- package/dist/views.js +47 -53
- package/dist/wakes.d.ts +1 -5
- package/dist/wakes.js +4 -6
- package/examples/symmetric-domains.mjs +250 -0
- package/guides/application-facade-delivery-evidence.md +6 -6
- package/guides/attention-primitive.md +32 -0
- package/guides/change-waits.md +2 -2
- package/guides/claude-native.md +18 -9
- package/guides/codex-delivery.md +17 -6
- package/guides/delivery-primitives.md +180 -0
- package/guides/mcp-connection-lifetime.md +1 -1
- package/guides/mcp.md +3 -3
- package/guides/migration.md +148 -151
- package/guides/operation-cancellation.md +1 -1
- package/guides/participant-ownership.md +78 -47
- package/guides/pi-tools.md +34 -5
- package/guides/primitive-catch-consolidation.md +5 -5
- package/guides/primitive-history-consolidation.md +2 -2
- package/guides/primitive-status-consolidation.md +3 -3
- package/guides/primitive-storage-consolidation.md +1 -1
- package/guides/receive-primitive.md +90 -0
- package/guides/row-runtime-delivery-evidence.md +1 -1
- package/guides/row-storage-design.md +22 -28
- package/guides/square-api.md +314 -91
- package/guides/symmetric-domains.md +92 -0
- package/package.json +14 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +2 -6
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +27 -17
- package/plugins/paseo/client/palette.ts +18 -18
- package/plugins/paseo/client/square-message.tsx +17 -23
- package/plugins/pi/square-pi.js +17 -17
- package/skills/square/SKILL.md +2 -6
- package/dist/square-backend.d.ts +0 -13
- package/dist/square-backend.js +0 -1
- package/dist/square-stdio.d.ts +0 -22
- package/dist/square-stdio.js +0 -235
- package/dist/stdio-server.d.ts +0 -3
- package/dist/stdio-server.js +0 -212
- 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/
|
|
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,
|
|
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,
|
|
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/
|
|
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.
|
|
179
|
-
| SDK projection roundtrip / leaked observations |
|
|
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).
|
package/guides/change-waits.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
package/guides/claude-native.md
CHANGED
|
@@ -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
|
|
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`
|
|
99
|
-
and captured
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
package/guides/codex-delivery.md
CHANGED
|
@@ -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
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
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
|
|
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
|
|
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.
|