@astrosheep/square 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/artifact.d.ts +1 -3
  2. package/dist/artifact.js +12 -36
  3. package/dist/attention-presentation.d.ts +2 -22
  4. package/dist/attention-presentation.js +9 -12
  5. package/dist/boundary-presentation.d.ts +3 -14
  6. package/dist/boundary-presentation.js +29 -104
  7. package/dist/card-presentation.d.ts +1 -0
  8. package/dist/card-presentation.js +2 -0
  9. package/dist/catch-decisions.js +1 -7
  10. package/dist/claude-boundary-state.d.ts +20 -0
  11. package/dist/claude-boundary-state.js +84 -0
  12. package/dist/claude-hook.d.ts +2 -0
  13. package/dist/claude-hook.js +231 -0
  14. package/dist/claude-mcp-context.d.ts +1 -1
  15. package/dist/claude-mcp-context.js +12 -3
  16. package/dist/cli/context.js +7 -9
  17. package/dist/cli/observation-commands.d.ts +4 -1
  18. package/dist/cli/observation-commands.js +37 -4
  19. package/dist/cli/registry.js +5 -2
  20. package/dist/codex-address.d.ts +2 -2
  21. package/dist/codex-boundary-state.d.ts +1 -5
  22. package/dist/codex-boundary-state.js +0 -4
  23. package/dist/codex-hook.d.ts +7 -10
  24. package/dist/codex-hook.js +69 -33
  25. package/dist/codex-queue.d.ts +10 -11
  26. package/dist/codex-queue.js +32 -32
  27. package/dist/codex-watch.d.ts +2 -0
  28. package/dist/codex-watch.js +100 -0
  29. package/dist/decisions.js +2 -2
  30. package/dist/delivery-deferral.d.ts +8 -17
  31. package/dist/delivery-deferral.js +0 -28
  32. package/dist/delivery-frame.d.ts +7 -0
  33. package/dist/delivery-frame.js +11 -0
  34. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  35. package/dist/delivery-ledger-sqlite.js +496 -0
  36. package/dist/delivery-ledger.d.ts +196 -0
  37. package/dist/delivery-ledger.js +268 -0
  38. package/dist/delivery-lifetime.d.ts +6 -0
  39. package/dist/delivery-lifetime.js +45 -0
  40. package/dist/harness-claude.js +3 -4
  41. package/dist/harness-links.js +63 -7
  42. package/dist/harness.js +12 -17
  43. package/dist/help.js +4 -1
  44. package/dist/http-delivery.d.ts +5 -9
  45. package/dist/http-delivery.js +20 -77
  46. package/dist/inbox.d.ts +6 -49
  47. package/dist/inbox.js +24 -371
  48. package/dist/index.d.ts +6 -3
  49. package/dist/index.js +4 -1
  50. package/dist/mcp.js +2 -3
  51. package/dist/model.d.ts +7 -47
  52. package/dist/model.js +1 -7
  53. package/dist/notifications.d.ts +24 -20
  54. package/dist/notifications.js +39 -108
  55. package/dist/operation-control.d.ts +4 -0
  56. package/dist/operation-control.js +20 -0
  57. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  58. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  59. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  60. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  61. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  62. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  63. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  64. package/dist/packages/agent-delivery/src/codex.js +41 -4
  65. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  66. package/dist/packages/agent-delivery/src/index.js +1 -1
  67. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  68. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  69. package/dist/paseo-delivery.d.ts +5 -5
  70. package/dist/paseo-delivery.js +19 -97
  71. package/dist/paseo.d.ts +1 -1
  72. package/dist/paseo.js +1 -1
  73. package/dist/plugins/opencode/index.d.ts +5 -3
  74. package/dist/plugins/opencode/index.js +99 -21
  75. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  76. package/dist/plugins/opencode-shared/delivery.js +109 -0
  77. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  78. package/dist/plugins/opencode-v1/index.js +41 -0
  79. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  80. package/dist/plugins/opencode-v1/runtime.js +152 -0
  81. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  82. package/dist/plugins/opencode-v1/transport.js +50 -0
  83. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  84. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  85. package/dist/ports.d.ts +31 -90
  86. package/dist/session-lifecycle.d.ts +8 -7
  87. package/dist/session-lifecycle.js +40 -42
  88. package/dist/sqlite-square.d.ts +14 -23
  89. package/dist/sqlite-square.js +132 -139
  90. package/dist/square-api.js +4 -7
  91. package/dist/square-core.d.ts +0 -1
  92. package/dist/square-domain.d.ts +5 -17
  93. package/dist/square-domain.js +104 -221
  94. package/dist/square-facade.d.ts +18 -15
  95. package/dist/square-post.d.ts +53 -0
  96. package/dist/square-post.js +4 -0
  97. package/dist/square-posting.d.ts +13 -0
  98. package/dist/square-posting.js +174 -0
  99. package/dist/square-producer-sqlite.d.ts +11 -0
  100. package/dist/square-producer-sqlite.js +210 -0
  101. package/dist/square-sqlite.d.ts +15 -25
  102. package/dist/square-sqlite.js +150 -226
  103. package/dist/views.d.ts +2 -2
  104. package/dist/views.js +0 -1
  105. package/dist/wake-sink.d.ts +4 -11
  106. package/dist/wake-sink.js +8 -32
  107. package/dist/wake-transports.d.ts +4 -7
  108. package/dist/wake-transports.js +7 -39
  109. package/examples/symmetric-domains.mjs +29 -249
  110. package/guides/change-waits.md +13 -53
  111. package/guides/claude-native.md +7 -171
  112. package/guides/codex-delivery.md +55 -80
  113. package/guides/delivery-primitives.md +48 -207
  114. package/guides/mcp.md +1 -1
  115. package/guides/operation-cancellation.md +2 -5
  116. package/guides/participant-ownership.md +25 -89
  117. package/guides/pi-native-delivery.md +19 -56
  118. package/guides/pi-tools.md +6 -8
  119. package/guides/square-api.md +113 -615
  120. package/package.json +8 -8
  121. package/plugins/claude/.claude-plugin/plugin.json +2 -3
  122. package/plugins/claude/hooks/hooks.json +11 -1
  123. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  124. package/plugins/codex/hooks/hooks.json +11 -23
  125. package/plugins/opencode/index.ts +91 -23
  126. package/plugins/opencode-shared/delivery.ts +91 -0
  127. package/plugins/opencode-v1/index.ts +36 -0
  128. package/plugins/opencode-v1/runtime.ts +134 -0
  129. package/plugins/opencode-v1/transport.ts +41 -0
  130. package/plugins/paseo/OVERVIEW.md +5 -5
  131. package/plugins/paseo/README.md +7 -6
  132. package/plugins/paseo/client/markdown.tsx +5 -5
  133. package/plugins/paseo/client/square-message.tsx +28 -24
  134. package/plugins/paseo/client/transform.ts +8 -5
  135. package/plugins/paseo/shared/card-presentation.ts +16 -0
  136. package/plugins/paseo/shared/message.ts +6 -11
  137. package/plugins/pi/square-pi.js +156 -417
  138. package/dist/channel-artifact.d.ts +0 -10
  139. package/dist/channel-artifact.js +0 -55
  140. package/dist/channel-delivery.d.ts +0 -11
  141. package/dist/channel-delivery.js +0 -51
  142. package/dist/channel-domain.d.ts +0 -3
  143. package/dist/channel-domain.js +0 -83
  144. package/dist/channel-facade.d.ts +0 -76
  145. package/dist/channel-facade.js +0 -1
  146. package/dist/channel-internal.d.ts +0 -126
  147. package/dist/channel-internal.js +0 -440
  148. package/dist/channel-metadata.d.ts +0 -11
  149. package/dist/channel-metadata.js +0 -1
  150. package/dist/channel-ports.d.ts +0 -241
  151. package/dist/channel-ports.js +0 -1
  152. package/dist/channel-presentation.d.ts +0 -17
  153. package/dist/channel-presentation.js +0 -22
  154. package/dist/channel-sqlite-schema.d.ts +0 -13
  155. package/dist/channel-sqlite-schema.js +0 -81
  156. package/dist/channel-transport.d.ts +0 -62
  157. package/dist/channel-transport.js +0 -1
  158. package/dist/channel.d.ts +0 -84
  159. package/dist/channel.js +0 -60
  160. package/dist/claude-delivery-status.d.ts +0 -13
  161. package/dist/claude-delivery-status.js +0 -46
  162. package/dist/claude-delivery.d.ts +0 -12
  163. package/dist/claude-delivery.js +0 -108
  164. package/dist/claude-mod.d.ts +0 -8
  165. package/dist/claude-mod.js +0 -172
  166. package/dist/claude-transport.d.ts +0 -5
  167. package/dist/claude-transport.js +0 -58
  168. package/dist/cmd/notify-once.d.ts +0 -2
  169. package/dist/cmd/notify-once.js +0 -29
  170. package/dist/delivery-attempts.d.ts +0 -52
  171. package/dist/delivery-attempts.js +0 -73
  172. package/dist/delivery-effects.d.ts +0 -139
  173. package/dist/delivery-effects.js +0 -163
  174. package/dist/delivery-health.d.ts +0 -51
  175. package/dist/delivery-health.js +0 -173
  176. package/dist/delivery-operations.d.ts +0 -22
  177. package/dist/delivery-operations.js +0 -198
  178. package/dist/delivery-primitives.d.ts +0 -5
  179. package/dist/delivery-primitives.js +0 -3
  180. package/dist/delivery.d.ts +0 -56
  181. package/dist/delivery.js +0 -80
  182. package/dist/plugins/opencode/delivery.d.ts +0 -19
  183. package/dist/plugins/opencode/delivery.js +0 -223
  184. package/dist/presentation-operations.d.ts +0 -5
  185. package/dist/presentation-operations.js +0 -86
  186. package/dist/sqlite-channel.d.ts +0 -15
  187. package/dist/sqlite-channel.js +0 -600
  188. package/dist/square-index.d.ts +0 -16
  189. package/dist/square-index.js +0 -106
  190. package/dist/wake-preview.d.ts +0 -7
  191. package/dist/wake-preview.js +0 -15
  192. package/plugins/claude/hooks/register.js +0 -132
  193. package/plugins/claude/types.d.ts +0 -7
  194. package/plugins/opencode/delivery.ts +0 -198
  195. /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
  196. /package/dist/{channel-values.js → square-values.js} +0 -0
package/dist/views.d.ts CHANGED
@@ -12,8 +12,8 @@ export declare function parseRequiredActivityId(id: ActivityId): number;
12
12
  /** History adds archive context and ordering to the shared storage-side query predicates. */
13
13
  export declare function selectHistory(facts: SquareRead, query: HistoryQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
14
14
  continuation?: HistoryContinuation | undefined;
15
- activities: HydratedAct[];
16
- visibleActivities: HydratedAct[];
15
+ activities: import("./model.js").StoredAct[];
16
+ visibleActivities: import("./model.js").StoredAct[];
17
17
  totalMatches: number;
18
18
  };
19
19
  export declare function publicActivities(activities: readonly ArchiveRecord[]): Activity[];
package/dist/views.js CHANGED
@@ -16,7 +16,6 @@ export function toPublicActivity(stored) {
16
16
  if (stored.kind === 'read')
17
17
  throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
18
18
  return { id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
19
- ...(stored.kind === 'say' && stored.messageIndex !== undefined ? { messageId: `msg/${stored.messageIndex}` } : {}),
20
19
  ...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}),
21
20
  ...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
22
21
  ...('metadata' in stored && stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
@@ -1,18 +1,11 @@
1
- /** One Paseo wake attempt: where it goes, what it says, and what identifies it. */
2
1
  export interface PaseoWakeRequest {
3
2
  agentId: string;
4
3
  prompt: string;
5
- location: string;
6
- participant: string;
7
- message: string;
8
- attemptN?: number;
4
+ messageId: string;
9
5
  }
10
- /**
11
- * The daemon's own dedup key for one attempt. Every retry of the same attempt
12
- * derives the same id; a later attempt of the same attention derives a new one.
13
- */
14
- export declare function paseoWakeMessageId(request: Pick<PaseoWakeRequest, 'location' | 'participant' | 'message' | 'attemptN'>): string;
15
- /** Steer one existing Paseo agent through the shared delivery entry. Never interrupts a turn. */
6
+ /** Steer one existing Paseo agent, without interrupting its turn. */
16
7
  export declare function sendPaseoWake(request: PaseoWakeRequest, opts?: {
17
8
  timeoutMs?: number;
9
+ beforeSend?: () => Promise<boolean>;
10
+ signal?: AbortSignal;
18
11
  }): Promise<void>;
package/dist/wake-sink.js CHANGED
@@ -1,37 +1,13 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { DeliveryError, connect } from './packages/agent-delivery/src/index.js';
3
- /**
4
- * The daemon's own dedup key for one attempt. Every retry of the same attempt
5
- * derives the same id; a later attempt of the same attention derives a new one.
6
- */
7
- export function paseoWakeMessageId(request) {
8
- const digest = createHash('sha256')
9
- .update([request.location, request.participant, request.message, request.attemptN ?? 0].join('|'))
10
- .digest('hex');
11
- return `square-${digest.slice(0, 32)}`;
12
- }
13
- /** Steer one existing Paseo agent through the shared delivery entry. Never interrupts a turn. */
2
+ /** Steer one existing Paseo agent, without interrupting its turn. */
14
3
  export async function sendPaseoWake(request, opts = {}) {
15
- const budgetMs = opts.timeoutMs;
16
- // A usable budget becomes one deadline both phases share; anything else is an
17
- // argument error, so it is handed to the entry untouched to classify.
18
- const bounded = budgetMs !== undefined && Number.isFinite(budgetMs) && budgetMs > 0;
19
- const deadline = bounded ? Date.now() + budgetMs : 0;
20
- // An elapsed budget never reached the daemon, so it is the shared entry's own
21
- // "unavailable" failure rather than a send error of its own.
22
- const connectMs = bounded ? Math.max(0, deadline - Date.now()) : budgetMs;
23
- if (bounded && connectMs === 0)
4
+ const budget = opts.timeoutMs;
5
+ const deadline = budget === undefined ? undefined : Date.now() + budget;
6
+ if (deadline !== undefined && (!Number.isFinite(budget) || budget <= 0))
24
7
  throw new DeliveryError('unavailable');
25
- const agent = await connect({
26
- harness: 'paseo',
27
- agentId: request.agentId,
28
- ...(connectMs === undefined ? {} : { timeoutMs: connectMs }),
29
- });
30
- const steerMs = bounded ? Math.max(0, deadline - Date.now()) : budgetMs;
31
- if (bounded && steerMs === 0)
8
+ const agent = await connect({ harness: 'paseo', agentId: request.agentId, ...(budget === undefined ? {} : { timeoutMs: budget }), signal: opts.signal });
9
+ if (opts.signal?.aborted || (deadline !== undefined && Date.now() >= deadline))
32
10
  throw new DeliveryError('unavailable');
33
- await agent.steer(request.prompt, {
34
- id: paseoWakeMessageId(request),
35
- ...(steerMs === undefined ? {} : { timeoutMs: steerMs }),
36
- });
11
+ const remaining = deadline === undefined ? undefined : deadline - Date.now();
12
+ await agent.steer(request.prompt, { id: request.messageId, ...(remaining === undefined ? {} : { timeoutMs: remaining }), signal: opts.signal, beforeSend: opts.beforeSend });
37
13
  }
@@ -1,7 +1,4 @@
1
- import type { WakeRouteKind } from './channel-values.js';
2
- import type { WakeTransportPort } from './channel-transport.js';
3
- export type WakeTransports = Readonly<Partial<Record<WakeRouteKind, WakeTransportPort>>>;
4
- /** Concrete transports use wall time; semantic state time belongs to Square operations. */
5
- export declare function createDefaultWakeTransport(env?: NodeJS.ProcessEnv, suppliedTransports?: WakeTransports): Promise<WakeTransportPort>;
6
- /** Route selection only: all implementations receive the same request and final gate. */
7
- export declare function createWakeTransport(transports: WakeTransports): WakeTransportPort;
1
+ import type { DeliveryTransport } from './delivery-ledger.js';
2
+ export type WakeTransports = readonly DeliveryTransport[];
3
+ /** Claude now receives only at native hooks; producers never target a socket. */
4
+ export declare function createDefaultDeliveryTransports(env?: NodeJS.ProcessEnv): Promise<WakeTransports>;
@@ -1,42 +1,10 @@
1
- /** Concrete transports use wall time; semantic state time belongs to Square operations. */
2
- export async function createDefaultWakeTransport(env = process.env, suppliedTransports) {
3
- if (suppliedTransports !== undefined)
4
- return createWakeTransport(suppliedTransports);
5
- const transports = {};
6
- try {
7
- const { CodexQueueTransport } = await import('./codex-queue.js');
8
- transports['codex-queue'] = new CodexQueueTransport({ env });
9
- }
10
- catch { /* core-only builds may omit Codex */ }
1
+ /** Claude now receives only at native hooks; producers never target a socket. */
2
+ export async function createDefaultDeliveryTransports(env = process.env) {
3
+ const { HttpWakeTransport } = await import('./http-delivery.js');
4
+ const transports = [new HttpWakeTransport({ env })];
11
5
  if (env.SQUARE_DISABLE_PASEO_WAKE !== '1') {
12
- try {
13
- const { PaseoWakeTransport } = await import('./paseo-delivery.js');
14
- transports.paseo = new PaseoWakeTransport();
15
- }
16
- catch { /* Paseo is optional */ }
6
+ const { PaseoWakeTransport } = await import('./paseo-delivery.js');
7
+ transports.push(new PaseoWakeTransport());
17
8
  }
18
- const { createClaudeWakeTransport } = await import('./claude-transport.js');
19
- transports['claude-native'] = createClaudeWakeTransport(env);
20
- const { HttpWakeTransport } = await import('./http-delivery.js');
21
- transports.http = new HttpWakeTransport({ env });
22
- return createWakeTransport(transports);
23
- }
24
- /** Route selection only: all implementations receive the same request and final gate. */
25
- export function createWakeTransport(transports) {
26
- const missing = (kind) => ({ outcome: 'not-capable', diagnostic: `no transport for ${kind}` });
27
- return {
28
- async probe(route) {
29
- const transport = transports[route.kind];
30
- if (transport === undefined)
31
- return missing(route.kind);
32
- try {
33
- return await transport.probe?.(route) ?? true;
34
- }
35
- catch (error) {
36
- return { outcome: 'not-capable', diagnostic: error instanceof Error ? error.message : String(error) };
37
- }
38
- },
39
- attempt: (request, timeoutMs, beforeSend) => transports[request.route.kind]?.attempt(request, timeoutMs, beforeSend)
40
- ?? Promise.resolve(missing(request.route.kind)),
41
- };
9
+ return transports;
42
10
  }
@@ -1,250 +1,30 @@
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));
1
+ // A producer notice and its receiver's ledger presentation, using a disposable square.
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { Square } from '@astrosheep/square';
6
+ import { DeliveryLedger } from '@astrosheep/square/delivery-ledger';
7
+
8
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'square-notice-example-'));
9
+ const ledgerPath = path.join(directory, 'delivery.sqlite');
10
+ const env = { ...process.env, SQUARE_DELIVERY: ledgerPath };
11
+ let square, ledger;
12
+ try {
13
+ square = await Square.build({ path: path.join(directory, 'example.square'), markdown: 'Example', env });
14
+ ledger = await DeliveryLedger.open({ location: ledgerPath });
15
+ await square.join('Reader', { owner: 'example:reader' });
16
+ const notice = await square.send({
17
+ from: 'Coordinator', to: 'Reader', body: 'The work is ready.', metadata: { job: { id: 7 } },
18
+ });
19
+ const session = await ledger.session('example:reader');
20
+ const claim = await ledger.claimPresentation('example:reader', { generation: session.generation, budgetChars: 10000 });
21
+ if (!claim) throw new Error('No card was available for presentation');
22
+ // This callback represents the receiver actually presenting the card.
23
+ const presented = claim.cards.map(card => card.text);
24
+ await claim.settle('presented');
25
+ console.log(JSON.stringify({ notice, presented, receipts: await ledger.receipts('example:reader') }, null, 2));
26
+ } finally {
27
+ await square?.close();
28
+ await ledger?.close();
29
+ await rm(directory, { recursive: true, force: true });
250
30
  }
@@ -1,57 +1,17 @@
1
1
  # Waiting for changes
2
2
 
3
- Artifact revisions and the rebuildable discovery index drive change detection. Filesystem
4
- notifications are hints, not activity delivery or consumption receipts.
3
+ Filesystem notifications are hints, not activity delivery or receipts. Artifact revision observation
4
+ and reconciliation let catch and stream reconsider changes even if a filesystem event is missed;
5
+ subscribers retain independent cancellation and deadlines. Throttling waits for its eligibility time.
6
+ There is no discovery index and no daemon or per-message timer.
5
7
 
6
- - A process shares a revision reader per canonical artifact and a filesystem watcher per containing
7
- directory. Subscribers have independent baselines, deadlines and cancellation. The final release
8
- closes the watcher, reconciliation timer and outstanding read.
9
- - Directory notifications invalidate lightweight reads. A two-second reconciliation checks revisions
10
- even when notifications are missing. Missing directories, unsupported watches, watcher failures
11
- and directory replacement retain that fallback. Latency is not a hard guarantee: a missed hint
12
- may wait for reconciliation, scheduling and lock contention.
13
- - Conversation revision controls the pending-content cache. Delivery revision separately invalidates
14
- eligibility, including delivery evidence, cancellation and deferral writes. Projections resolve
15
- current typed seats and selected bodies in one coherent artifact read; they never grant authority
16
- from an index entry or a cached eligibility decision. No sidecar, broker or second delivery store
17
- is introduced. An unreadable candidate cannot authorize cached content.
18
- - Each active session pending observer subscribes before selecting current candidates and facts,
19
- including when no seat is yet discoverable. Index publication can wake discovery without an
20
- artifact write. A failed injection waits for substantive change; its own failed evidence write
21
- does not cause a retry loop.
22
- - Deferral create/renew/release and opportunistic pruning change delivery revision, not conversation
23
- activity. Release rechecks eligibility without a new message. Expiry needs no synthetic write:
24
- the observer includes the earliest relevant matching live deferral expiry in its existing wait
25
- deadline, alongside catch-lease expiry/staleness. Overlapping leases remain OR; an early expiry
26
- rechecks the remaining live leases. Renewal changes that deadline without self-triggered delivery.
27
- - There is no new daemon or detached per-message timer. A one-shot producer skips deferred work and
28
- returns promptly. An existing active delivery observer will reconsider release/expiry; absent an
29
- observer, pending work waits for the next normal delivery opportunity. A lease does not create a
30
- process that was not already running.
31
- - Blocking catch waits for conversation revision or its heartbeat/quiet deadline. An active catch keeps
32
- its single lease alive and pauses quiet expiry. Core catch and streams use the same storage change
33
- detector. Throttling waits for its eligibility time.
34
- - Deferrals are independent of catch filters, presentation claims, full-delivery seen receipts,
35
- clipped previews and session-local cancellation. Sharing storage detection does not share session
36
- delivery state. Cancellation never creates a receipt or removes archive history. A deferral cannot
37
- recall a send already past its final freshness check.
8
+ ACK and export ownership serialize with insertion in producer storage; exact future ACK prevents a
9
+ matching late insertion from bypassing it, but cannot recall an already owned export. Cancellation
10
+ does not erase history or imply that activity was read. Proactive delivery suppression belongs in the
11
+ host delivery ledger; see [delivery-primitives.md](delivery-primitives.md).
38
12
 
39
- ## Verification
40
-
41
- Run all tests with the sandbox import; never attach synthetic participants to a live harness identity.
42
-
43
- ```sh
44
- npm test
45
- npm run test:cli-process
46
- node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 100 --mode poll200
47
- node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 100 --mode events
48
- node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session
49
- node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
50
- ```
51
-
52
- The benchmark creates disposable synthetic history. Process-local sharing does not deduplicate
53
- monitors across processes. Timings measure committed-write-to-projection latency, not native
54
- message landing, and discovery deadlines are probe bounds rather than an SLA. Historical benchmark
55
- evidence may describe older HostLedger implementations; it is not current custody authority.
56
- A long-running extension must reload upgraded code before using the new artifact format; this
57
- delivery does not install or restart one.
13
+ At the next normal privileged posting opportunity, already-posted eligible keys can be dispatched
14
+ again without reinserting cards or resetting producer state. Ledger attempts exclude accepted, unknown
15
+ and presented work; only never-attempted or known-unsent failed/released work can proceed. No new
16
+ scheduler or Stop watcher is needed. See [square-api.md](square-api.md) for bounded recovery of
17
+ abandoned export ownership.