@astrosheep/square 0.3.106 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/activity-feed.js +1 -1
  2. package/dist/artifact-store.d.ts +41 -0
  3. package/dist/artifact-store.js +272 -0
  4. package/dist/artifact.d.ts +6 -12
  5. package/dist/artifact.js +43 -344
  6. package/dist/attention-presentation.d.ts +13 -7
  7. package/dist/attention-presentation.js +29 -68
  8. package/dist/boundary-presentation.d.ts +3 -16
  9. package/dist/boundary-presentation.js +29 -109
  10. package/dist/card-presentation.d.ts +1 -0
  11. package/dist/card-presentation.js +2 -0
  12. package/dist/catch-decisions.d.ts +2 -2
  13. package/dist/catch-decisions.js +7 -3
  14. package/dist/catch-selection.d.ts +8 -0
  15. package/dist/catch-selection.js +5 -0
  16. package/dist/claude-delivery.d.ts +14 -12
  17. package/dist/claude-delivery.js +42 -142
  18. package/dist/claude-mcp-context.js +2 -2
  19. package/dist/claude-mod.js +28 -140
  20. package/dist/claude-transport.d.ts +5 -0
  21. package/dist/claude-transport.js +34 -0
  22. package/dist/cli/context.d.ts +2 -2
  23. package/dist/cli/context.js +7 -10
  24. package/dist/cli/maintenance-commands.d.ts +0 -4
  25. package/dist/cli/maintenance-commands.js +2 -29
  26. package/dist/cli/observation-commands.d.ts +2 -0
  27. package/dist/cli/observation-commands.js +12 -2
  28. package/dist/cli/program.js +1 -1
  29. package/dist/cli/registry.js +4 -3
  30. package/dist/codex-address.d.ts +2 -2
  31. package/dist/codex-boundary-state.d.ts +1 -5
  32. package/dist/codex-boundary-state.js +0 -4
  33. package/dist/codex-hook.d.ts +7 -10
  34. package/dist/codex-hook.js +69 -33
  35. package/dist/codex-queue.d.ts +10 -11
  36. package/dist/codex-queue.js +32 -34
  37. package/dist/codex-watch.d.ts +2 -0
  38. package/dist/codex-watch.js +100 -0
  39. package/dist/decisions.js +3 -3
  40. package/dist/delivery-deferral.d.ts +1 -19
  41. package/dist/delivery-deferral.js +1 -29
  42. package/dist/delivery-frame.d.ts +7 -0
  43. package/dist/delivery-frame.js +11 -0
  44. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  45. package/dist/delivery-ledger-sqlite.js +496 -0
  46. package/dist/delivery-ledger.d.ts +196 -0
  47. package/dist/delivery-ledger.js +268 -0
  48. package/dist/delivery-lifetime.d.ts +6 -0
  49. package/dist/delivery-lifetime.js +45 -0
  50. package/dist/harness-links.js +61 -7
  51. package/dist/harness.js +12 -17
  52. package/dist/help.js +3 -6
  53. package/dist/http-delivery.d.ts +5 -9
  54. package/dist/http-delivery.js +20 -79
  55. package/dist/inbox.d.ts +6 -57
  56. package/dist/inbox.js +24 -380
  57. package/dist/index.d.ts +7 -4
  58. package/dist/index.js +5 -2
  59. package/dist/mcp.d.ts +2 -1
  60. package/dist/mcp.js +40 -70
  61. package/dist/message.d.ts +7 -0
  62. package/dist/message.js +23 -0
  63. package/dist/model.d.ts +15 -91
  64. package/dist/model.js +2 -140
  65. package/dist/neutral.d.ts +72 -0
  66. package/dist/neutral.js +27 -0
  67. package/dist/notifications.d.ts +24 -26
  68. package/dist/notifications.js +39 -143
  69. package/dist/operation-control.d.ts +4 -0
  70. package/dist/operation-control.js +20 -0
  71. package/dist/operation-presentation.d.ts +1 -2
  72. package/dist/operation-presentation.js +1 -7
  73. package/dist/owner.d.ts +2 -2
  74. package/dist/owner.js +1 -1
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  77. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  78. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  79. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  80. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  81. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  82. package/dist/packages/agent-delivery/src/codex.js +41 -4
  83. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  84. package/dist/packages/agent-delivery/src/index.js +1 -1
  85. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  86. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  87. package/dist/paseo-delivery.d.ts +5 -5
  88. package/dist/paseo-delivery.js +19 -99
  89. package/dist/paseo.d.ts +1 -1
  90. package/dist/paseo.js +1 -1
  91. package/dist/plugins/opencode/index.d.ts +5 -3
  92. package/dist/plugins/opencode/index.js +99 -21
  93. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  94. package/dist/plugins/opencode-shared/delivery.js +109 -0
  95. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  96. package/dist/plugins/opencode-v1/index.js +41 -0
  97. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  98. package/dist/plugins/opencode-v1/runtime.js +152 -0
  99. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  100. package/dist/plugins/opencode-v1/transport.js +50 -0
  101. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  102. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  103. package/dist/ports.d.ts +33 -214
  104. package/dist/presentation.d.ts +5 -5
  105. package/dist/presentation.js +1 -0
  106. package/dist/runtime.js +2 -2
  107. package/dist/search.js +1 -1
  108. package/dist/session-lifecycle.d.ts +8 -7
  109. package/dist/session-lifecycle.js +40 -42
  110. package/dist/sqlite-framing.d.ts +8 -0
  111. package/dist/sqlite-framing.js +31 -0
  112. package/dist/sqlite-square.d.ts +16 -26
  113. package/dist/sqlite-square.js +199 -262
  114. package/dist/sqlite-values.d.ts +21 -0
  115. package/dist/sqlite-values.js +78 -0
  116. package/dist/square-api.d.ts +1 -1
  117. package/dist/square-api.js +5 -8
  118. package/dist/square-core.d.ts +0 -1
  119. package/dist/square-domain.d.ts +12 -10
  120. package/dist/square-domain.js +105 -223
  121. package/dist/square-facade.d.ts +25 -76
  122. package/dist/square-location.d.ts +6 -1
  123. package/dist/square-location.js +1 -1
  124. package/dist/square-post.d.ts +53 -0
  125. package/dist/square-post.js +4 -0
  126. package/dist/square-posting.d.ts +13 -0
  127. package/dist/square-posting.js +174 -0
  128. package/dist/square-producer-sqlite.d.ts +11 -0
  129. package/dist/square-producer-sqlite.js +210 -0
  130. package/dist/square-receive.d.ts +2 -3
  131. package/dist/square-sqlite.d.ts +33 -42
  132. package/dist/square-sqlite.js +236 -768
  133. package/dist/square-tools.d.ts +8 -0
  134. package/dist/square-tools.js +39 -0
  135. package/dist/square-values.d.ts +27 -0
  136. package/dist/square-values.js +90 -0
  137. package/dist/views.d.ts +6 -6
  138. package/dist/views.js +14 -9
  139. package/dist/wake-sink.d.ts +4 -11
  140. package/dist/wake-sink.js +8 -32
  141. package/dist/wake-transports.d.ts +4 -0
  142. package/dist/wake-transports.js +11 -0
  143. package/examples/symmetric-domains.mjs +29 -249
  144. package/guides/change-waits.md +13 -53
  145. package/guides/claude-native.md +19 -172
  146. package/guides/codex-delivery.md +55 -78
  147. package/guides/delivery-primitives.md +48 -207
  148. package/guides/operation-cancellation.md +2 -5
  149. package/guides/participant-ownership.md +25 -89
  150. package/guides/pi-native-delivery.md +19 -56
  151. package/guides/pi-tools.md +6 -8
  152. package/guides/row-runtime-delivery-evidence.md +2 -0
  153. package/guides/row-storage-design.md +19 -44
  154. package/guides/square-api.md +113 -538
  155. package/package.json +8 -8
  156. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  157. package/plugins/claude/hooks/hooks.json +1 -1
  158. package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
  159. package/plugins/claude/types.d.ts +1 -1
  160. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  161. package/plugins/codex/hooks/hooks.json +11 -23
  162. package/plugins/opencode/index.ts +91 -23
  163. package/plugins/opencode-shared/delivery.ts +91 -0
  164. package/plugins/opencode-v1/index.ts +36 -0
  165. package/plugins/opencode-v1/runtime.ts +134 -0
  166. package/plugins/opencode-v1/transport.ts +41 -0
  167. package/plugins/paseo/OVERVIEW.md +5 -5
  168. package/plugins/paseo/README.md +7 -6
  169. package/plugins/paseo/client/markdown.tsx +5 -5
  170. package/plugins/paseo/client/square-message.tsx +28 -24
  171. package/plugins/paseo/client/transform.ts +8 -5
  172. package/plugins/paseo/shared/card-presentation.ts +16 -0
  173. package/plugins/paseo/shared/message.ts +6 -11
  174. package/plugins/pi/square-pi.js +167 -451
  175. package/dist/channel.d.ts +0 -74
  176. package/dist/channel.js +0 -92
  177. package/dist/claude-delivery-status.d.ts +0 -13
  178. package/dist/claude-delivery-status.js +0 -46
  179. package/dist/cmd/notify-once.d.ts +0 -2
  180. package/dist/cmd/notify-once.js +0 -29
  181. package/dist/delivery-attempts.d.ts +0 -52
  182. package/dist/delivery-attempts.js +0 -73
  183. package/dist/delivery-effects.d.ts +0 -139
  184. package/dist/delivery-effects.js +0 -163
  185. package/dist/delivery-health.d.ts +0 -51
  186. package/dist/delivery-health.js +0 -173
  187. package/dist/delivery-operations.d.ts +0 -9
  188. package/dist/delivery-operations.js +0 -168
  189. package/dist/delivery-primitives.d.ts +0 -5
  190. package/dist/delivery-primitives.js +0 -3
  191. package/dist/delivery.d.ts +0 -45
  192. package/dist/delivery.js +0 -106
  193. package/dist/plugins/opencode/delivery.d.ts +0 -19
  194. package/dist/plugins/opencode/delivery.js +0 -238
  195. package/dist/presentation-operations.d.ts +0 -18
  196. package/dist/presentation-operations.js +0 -86
  197. package/dist/square-index.d.ts +0 -16
  198. package/dist/square-index.js +0 -106
  199. package/dist/square-legacy-framing.d.ts +0 -11
  200. package/dist/square-legacy-framing.js +0 -64
  201. package/dist/square-migration-host-ledger.d.ts +0 -26
  202. package/dist/square-migration-host-ledger.js +0 -169
  203. package/dist/square-migration.d.ts +0 -30
  204. package/dist/square-migration.js +0 -599
  205. package/guides/migration.md +0 -156
  206. package/plugins/opencode/delivery.ts +0 -211
@@ -1,12 +1,27 @@
1
- import type { DeliveryDeferral, MetadataMatch } from './delivery-deferral.js';
2
- export type { DeliveryDeferral, MetadataMatch, MetadataScalar } from './delivery-deferral.js';
1
+ import type { MetadataMatch } from './delivery-deferral.js';
2
+ export type { MetadataMatch, MetadataScalar } from './delivery-deferral.js';
3
3
  import type { ActivityId, Perception, Reach } from './square-core.js';
4
- import type { Owner, StoredAct, WakeAddress, ActivityMetadata, ActivityDelivery, DirectedNotificationRoute } from './model.js';
5
- import type { WakeTransportPort, SquareResourceOwner } from './ports.js';
4
+ import type { Owner, HydratedAct, WakeAddress, ActivityMetadata, DirectedNotificationRoute } from './model.js';
5
+ import type { SquareResourceOwner } from './ports.js';
6
+ import type { OperationControl } from './neutral.js';
7
+ export interface ActivitySearch {
8
+ readonly text: string;
9
+ readonly mode: 'literal' | 'regex';
10
+ }
11
+ export interface AckResult {
12
+ readonly consumed: number;
13
+ }
14
+ import type { FutureAck } from './square-post.js';
15
+ export type { NotificationId, SendInput, SendResult, FutureAck } from './square-post.js';
16
+ export interface HeldSeat {
17
+ readonly name: string;
18
+ readonly generation: string;
19
+ }
20
+ export type { CatchWaitStart, OperationProgress, OperationControl, OperationFailureDetail, OperationError } from './neutral.js';
6
21
  export type { ActivityId, Perception, Reach } from './square-core.js';
7
- export type { Owner, WakeAddress, ActivityMetadata, ActivityDelivery, JsonValue, DirectedNotificationRoute } from './model.js';
8
- /** Indexed archive event, including read bookkeeping. */
9
- export type ArchiveRecord = Readonly<StoredAct>;
22
+ export type { Owner, WakeAddress, ActivityMetadata, JsonValue, DirectedNotificationRoute } from './model.js';
23
+ /** Indexed archive event, including read bookkeeping and a say's linked content. */
24
+ export type ArchiveRecord = Readonly<HydratedAct>;
10
25
  export interface Activity {
11
26
  readonly id: ActivityId;
12
27
  readonly at: number;
@@ -14,7 +29,6 @@ export interface Activity {
14
29
  readonly actor: string;
15
30
  readonly body?: string;
16
31
  readonly metadata?: ActivityMetadata;
17
- readonly delivery?: ActivityDelivery;
18
32
  readonly mentions: readonly string[];
19
33
  readonly reach?: Reach;
20
34
  readonly target?: string;
@@ -50,22 +64,6 @@ export interface ExpressResult {
50
64
  readonly hint?: string;
51
65
  readonly observation?: ExpressObservation;
52
66
  }
53
- export type ChannelTruncate = number | false;
54
- export interface ChannelSendInput {
55
- readonly from: string;
56
- readonly to: string | readonly string[];
57
- readonly body: string;
58
- readonly truncate?: ChannelTruncate;
59
- readonly metadata?: ActivityMetadata;
60
- }
61
- export interface ChannelSendResult {
62
- readonly id: ActivityId;
63
- readonly at: number;
64
- readonly from: string;
65
- readonly to: readonly string[];
66
- readonly body: string;
67
- readonly metadata?: ActivityMetadata;
68
- }
69
67
  export interface ListenerChangeResult {
70
68
  readonly activity: Activity | null;
71
69
  readonly observation?: ListenerObservation;
@@ -86,10 +84,6 @@ export interface CatchOptions extends ObservationOptions {
86
84
  readonly limit?: number;
87
85
  readonly replace?: boolean;
88
86
  }
89
- export interface ActivitySearch {
90
- readonly text: string;
91
- readonly mode: 'literal' | 'regex';
92
- }
93
87
  export interface ActivityCursor {
94
88
  readonly id: ActivityId;
95
89
  readonly direction: 'before' | 'after';
@@ -111,10 +105,6 @@ export interface PendingResult {
111
105
  /** Matching unconsumed activities outside this page. */
112
106
  readonly remaining: number;
113
107
  }
114
- export interface AckResult {
115
- /** Newly consumed addressed IDs; repeated acknowledgement is a no-op. */
116
- readonly consumed: number;
117
- }
118
108
  export type CatchReason = 'drained' | 'idle' | 'capped' | 'quorum' | 'replaced' | 'already-active';
119
109
  export interface CatchResult {
120
110
  readonly activities: readonly PerceivedActivity[];
@@ -124,37 +114,6 @@ export interface CatchResult {
124
114
  readonly hint?: string;
125
115
  readonly observation?: CatchObservation;
126
116
  }
127
- export interface CatchWaitStart {
128
- readonly idleMs: number;
129
- readonly replacementRequested: boolean;
130
- readonly replaced: boolean;
131
- }
132
- export type OperationProgress = {
133
- readonly kind: 'waiting';
134
- readonly reason: 'throttled';
135
- readonly delayMs?: number;
136
- } | ({
137
- readonly kind: 'catch-started';
138
- readonly participant: string;
139
- } & CatchWaitStart);
140
- export interface OperationControl {
141
- readonly signal?: AbortSignal;
142
- readonly onProgress?: (progress: OperationProgress) => void;
143
- }
144
- export interface OperationFailureDetail {
145
- readonly operation: 'express' | 'join' | 'catch' | 'history' | 'other';
146
- readonly subject?: 'caller' | 'filter' | 'recipient';
147
- readonly participant?: string;
148
- readonly expressEffect?: 'unsent' | 'landed' | 'unknown';
149
- readonly landedId?: ActivityId;
150
- readonly historyBounds?: {
151
- readonly maxLimit: number;
152
- readonly totalMatches: number;
153
- };
154
- }
155
- export interface OperationError extends Error {
156
- readonly operation?: OperationFailureDetail;
157
- }
158
117
  export interface HistoryQuery extends ActivityQuery, ObservationOptions {
159
118
  /** Ten by default; null explicitly reads the full archive. */
160
119
  readonly limit?: number | null;
@@ -170,7 +129,6 @@ export interface HistoryResult {
170
129
  }
171
130
  export interface OpenOptions {
172
131
  readonly clock?: () => number;
173
- readonly wakeTransport?: WakeTransportPort | false;
174
132
  }
175
133
  export interface SquareLocation {
176
134
  readonly path?: string;
@@ -202,7 +160,7 @@ export interface JoinResult {
202
160
  }
203
161
  export interface Participant {
204
162
  readonly name: string;
205
- readonly generation: ActivityId;
163
+ readonly generation: string;
206
164
  express(body: string | undefined, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
207
165
  express(input: ExpressInput, control?: OperationControl): Promise<ExpressResult>;
208
166
  listen(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
@@ -211,20 +169,11 @@ export interface Participant {
211
169
  listening(options?: ObservationOptions, control?: OperationControl): Promise<ListeningResult>;
212
170
  catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
213
171
  pending(options?: PendingOptions, control?: OperationControl): Promise<PendingResult>;
172
+ /** Local custody only: ledger-only detach/delete is observed on privileged reconciliation. */
214
173
  ack(ids: readonly ActivityId[], control?: OperationControl): Promise<AckResult>;
174
+ ack(input: FutureAck, control?: OperationControl): Promise<AckResult>;
215
175
  /** Leave without a message. Only an omitted or empty body is accepted. */
216
176
  done(body?: string, options?: ObservationOptions, control?: OperationControl): Promise<LifecycleResult>;
217
- deferDelivery(input: Readonly<{
218
- match: MetadataMatch;
219
- durationMs: number;
220
- }>, control?: OperationControl): Promise<DeliveryDeferral>;
221
- renewDeliveryDeferral(id: string, durationMs: number, control?: OperationControl): Promise<DeliveryDeferral | null>;
222
- releaseDeliveryDeferral(id: string, control?: OperationControl): Promise<boolean>;
223
- }
224
- export interface HeldSeat {
225
- readonly name: string;
226
- readonly generation: ActivityId;
227
- readonly attachment: 'attached' | 'detached';
228
177
  }
229
178
  export type ParticipantPresence = 'never-joined' | 'active' | 'watching' | 'away' | 'done';
230
179
  export interface ParticipantSummary {
@@ -1,2 +1,7 @@
1
- import type { SquareLocation } from './square-facade.js';
1
+ interface SquareLocation {
2
+ readonly path?: string;
3
+ readonly cwd?: string;
4
+ readonly env?: NodeJS.ProcessEnv;
5
+ }
2
6
  export declare function squareLocation(input: SquareLocation): string;
7
+ export {};
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { SquareError } from './model.js';
2
+ import { SquareError } from './neutral.js';
3
3
  export function squareLocation(input) {
4
4
  const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
5
5
  if (selected === undefined || selected.trim() === '')
@@ -0,0 +1,53 @@
1
+ import { type ActivityMetadata, type DirectedNotificationRoute, type Owner } from './model.js';
2
+ import type { MetadataMatch } from './delivery-deferral.js';
3
+ import type { SeatRef } from './ports.js';
4
+ /** Producer notices are not activities in the square. */
5
+ export type NotificationId = `notice/${number}`;
6
+ export interface SendInput {
7
+ readonly from: string;
8
+ readonly to: string | readonly string[];
9
+ readonly body: string;
10
+ readonly metadata?: ActivityMetadata;
11
+ }
12
+ export interface SendResult {
13
+ readonly id: NotificationId;
14
+ readonly at: number;
15
+ readonly from: string;
16
+ readonly to: readonly string[];
17
+ readonly body: string;
18
+ readonly metadata?: ActivityMetadata;
19
+ }
20
+ /** The caller supplies every scalar equality identifying one exact outcome. */
21
+ export interface FutureAck {
22
+ readonly from: string;
23
+ readonly match: MetadataMatch;
24
+ readonly future: 'exact';
25
+ }
26
+ /** One Square-owned recipient card; neither an endpoint nor a shared message resource. */
27
+ export interface SquarePost {
28
+ readonly kind: 'activity' | 'notice';
29
+ readonly index: number;
30
+ readonly recipient: string;
31
+ readonly owner: Owner;
32
+ readonly generation: string;
33
+ readonly from: string;
34
+ readonly at: number;
35
+ readonly body?: string;
36
+ readonly metadata?: ActivityMetadata;
37
+ readonly route?: DirectedNotificationRoute;
38
+ readonly full?: boolean;
39
+ readonly consumed?: boolean;
40
+ }
41
+ export interface ProducerRead {
42
+ pendingPosts(): readonly SquarePost[];
43
+ postedPosts(): readonly SquarePost[];
44
+ ownsPost(token: string, exportAt?: number): boolean;
45
+ }
46
+ export interface ProducerWrite {
47
+ insertNotice(input: SendInput, at: number): SendResult;
48
+ ackMatching(seat: SeatRef, input: FutureAck, at: number): number;
49
+ acquirePost(post: SquarePost, token: string, pid: number, now: number, exportAt?: number): false | 'initial' | 'recovered';
50
+ settlePost(token: string, full: boolean, disposition: 'posted' | 'uncertain' | 'never-exported'): boolean;
51
+ foldReceipt(post: SquarePost, at: number): boolean;
52
+ }
53
+ export declare function squarePostKey(location: string, post: Pick<SquarePost, 'kind' | 'index' | 'recipient' | 'generation'>): string;
@@ -0,0 +1,4 @@
1
+ import { nameKey } from './model.js';
2
+ export function squarePostKey(location, post) {
3
+ return `square:${location}#${post.kind === 'activity' ? 'act' : 'notice'}/${post.index}/recipient/${encodeURIComponent(nameKey(post.recipient))}/join/${encodeURIComponent(post.generation)}`;
4
+ }
@@ -0,0 +1,13 @@
1
+ import { DeliveryLedger, type DeliveryRoute, type DeliverySession } from './delivery-ledger.js';
2
+ import { type Seat } from './model.js';
3
+ import type { SquareArtifactPort } from './ports.js';
4
+ /** Local producer custody and current host attachment are different observations. */
5
+ export declare function deliverySessionsForSeats(ledger: DeliveryLedger, seats: readonly Seat[]): Promise<ReadonlyMap<string, DeliverySession | undefined>>;
6
+ /** Read-only host observations for status; callers fence them against the captured World seats. */
7
+ export declare function readSquareSessions(seats: readonly Seat[], env: NodeJS.ProcessEnv): Promise<ReadonlyMap<string, DeliverySession | undefined>>;
8
+ /** A first producer join may register an absent owner, never resume or revive one. */
9
+ export declare function ensureInitialDeliverySession(owner: Seat['owner'], env: NodeJS.ProcessEnv, route?: DeliveryRoute): Promise<boolean>;
10
+ /** Called only by privileged mutations. Sandbox lease/ACK/selection never enters here. */
11
+ export declare function reconcileSquareDelivery(artifact: SquareArtifactPort, location: string, env: NodeJS.ProcessEnv, clock: () => number): Promise<void>;
12
+ /** One export path for Square activities and Square-owned notices. No transaction spans I/O. */
13
+ export declare function flushSquarePosts(artifact: SquareArtifactPort, location: string, env: NodeJS.ProcessEnv, clock: () => number): Promise<void>;
@@ -0,0 +1,174 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { DeliveryLedger, deliveryLedgerLocation } from './delivery-ledger.js';
3
+ import { dispatchPostedCard } from './notifications.js';
4
+ import { retainDeliveryLedger } from './delivery-lifetime.js';
5
+ import { projectNotification, displayAttentionPath } from './attention-presentation.js';
6
+ import { coreDone } from './decisions.js';
7
+ import { activityMetadataHasContent } from './model.js';
8
+ import { formatActivityId } from './square-core.js';
9
+ import { squarePostKey } from './square-post.js';
10
+ const openLedger = (env) => DeliveryLedger.open({
11
+ location: deliveryLedgerLocation(env),
12
+ });
13
+ function renderPost(location, post) {
14
+ if (post.kind === 'notice')
15
+ return { text: post.body ?? '', full: true };
16
+ const projected = projectNotification({ squarePath: location, actIndex: post.index, recipient: post.recipient,
17
+ actor: post.from, route: post.route, body: post.body ?? '',
18
+ metadataOnly: !(post.body?.trim()) && activityMetadataHasContent(post.metadata) });
19
+ return { text: projected.text, full: projected.isFull };
20
+ }
21
+ /** Local producer custody and current host attachment are different observations. */
22
+ export async function deliverySessionsForSeats(ledger, seats) {
23
+ const owners = [...new Set(seats.map(seat => seat.owner))];
24
+ return new Map(await Promise.all(owners.map(async (owner) => [owner, await ledger.session(owner)])));
25
+ }
26
+ /** Read-only host observations for status; callers fence them against the captured World seats. */
27
+ export async function readSquareSessions(seats, env) {
28
+ if (!seats.length)
29
+ return new Map();
30
+ const ledger = await openLedger(env);
31
+ try {
32
+ return await deliverySessionsForSeats(ledger, seats);
33
+ }
34
+ finally {
35
+ await ledger.close();
36
+ }
37
+ }
38
+ /** A first producer join may register an absent owner, never resume or revive one. */
39
+ export async function ensureInitialDeliverySession(owner, env, route) {
40
+ const ledger = await openLedger(env);
41
+ try {
42
+ const current = await ledger.session(owner);
43
+ if (!current) {
44
+ return !!await ledger.attach({ owner, expectedGeneration: null, routes: route ? [route] : [] });
45
+ }
46
+ if (current.state !== 'attached')
47
+ return false;
48
+ if (route)
49
+ return ledger.updateRoutes({ owner, generation: current.generation,
50
+ routes: [...current.routes.filter(candidate => candidate.kind !== route.kind), route] });
51
+ return true;
52
+ }
53
+ finally {
54
+ await ledger.close();
55
+ }
56
+ }
57
+ /** Called only by privileged mutations. Sandbox lease/ACK/selection never enters here. */
58
+ export async function reconcileSquareDelivery(artifact, location, env, clock) {
59
+ const snapshot = (await artifact.read(facts => ({ seats: facts.roster().filter(person => person.joined)
60
+ .flatMap(person => { const seat = facts.seat(person.name); return seat ? [seat] : []; }), posts: facts.postedPosts() }))).value;
61
+ if (!snapshot.seats.length && !snapshot.posts.length)
62
+ return;
63
+ const ledger = await openLedger(env);
64
+ try {
65
+ const sessions = await deliverySessionsForSeats(ledger, snapshot.seats);
66
+ for (const seat of snapshot.seats) {
67
+ const session = sessions.get(seat.owner);
68
+ if (!session)
69
+ continue;
70
+ if (session.state === 'deleted') {
71
+ await artifact.transactAs({ kind: 'host-cleanup', seat }, facts => {
72
+ const current = facts.seat(seat.participant);
73
+ if (current?.owner === seat.owner && current.generation === seat.generation && facts.participant(seat.participant)?.joined)
74
+ facts.land(coreDone(facts, seat.participant, '', clock()));
75
+ });
76
+ }
77
+ else if (seat.attachment !== session.state) {
78
+ const attachment = session.state;
79
+ await artifact.transactAs({ kind: 'producer' }, facts => facts.setSeatAttachment(seat, attachment));
80
+ }
81
+ }
82
+ // A full receipt is monotonic external presentation evidence, never inferred from silence.
83
+ const receipts = new Map();
84
+ for (const owner of new Set(snapshot.posts.map(post => post.owner))) {
85
+ const keys = snapshot.posts.filter(post => post.owner === owner).map(post => squarePostKey(location, post));
86
+ receipts.set(owner, new Map((await ledger.receipts(owner, { keys })).map(receipt => [receipt.key, receipt.at])));
87
+ }
88
+ for (const post of snapshot.posts) {
89
+ const cardKey = squarePostKey(location, post);
90
+ const receipt = receipts.get(post.owner)?.get(cardKey);
91
+ if (receipt !== undefined && post.full === true && !post.consumed)
92
+ await artifact.transactAs({ kind: 'producer' }, facts => facts.foldReceipt(post, receipt));
93
+ }
94
+ await suppressWithdrawn(artifact, ledger, location);
95
+ }
96
+ finally {
97
+ await ledger.close();
98
+ }
99
+ }
100
+ async function suppressWithdrawn(artifact, ledger, location) {
101
+ const posts = (await artifact.read(facts => facts.postedPosts().filter(post => {
102
+ const seat = facts.seat(post.recipient);
103
+ return post.consumed || !seat || seat.owner !== post.owner || seat.generation !== post.generation;
104
+ }))).value;
105
+ // Departure withdraws only the captured notification, never producer read history or held effects.
106
+ for (const post of posts)
107
+ await ledger.suppress({ owner: post.owner, key: squarePostKey(location, post), scope: 'exact' });
108
+ }
109
+ /** One export path for Square activities and Square-owned notices. No transaction spans I/O. */
110
+ export async function flushSquarePosts(artifact, location, env, clock) {
111
+ // Delivery enrichment cannot turn a committed producer mutation into a retry of that mutation.
112
+ let lifetime;
113
+ try {
114
+ const snapshot = (await artifact.read(facts => ({ pending: facts.pendingPosts(), posted: facts.postedPosts() }))).value;
115
+ if (!snapshot.pending.length && !snapshot.posted.length)
116
+ return;
117
+ const ledger = await openLedger(env);
118
+ lifetime = retainDeliveryLedger(ledger);
119
+ for (const post of snapshot.pending) {
120
+ const session = await ledger.session(post.owner);
121
+ if (session?.state !== 'attached')
122
+ continue;
123
+ const rendered = renderPost(location, post), token = randomUUID();
124
+ const owned = (await artifact.transactAs({ kind: 'producer' }, facts => facts.acquirePost({ ...post, full: rendered.full }, token, process.pid, clock(), Date.now()))).value;
125
+ if (!owned)
126
+ continue;
127
+ let posted = false, knownUnsent = true;
128
+ try {
129
+ // This check is not custody across the await; the ledger independently fences delivery.
130
+ const current = await ledger.session(post.owner);
131
+ if (current?.state !== 'attached' || current.generation !== session.generation
132
+ || !(await artifact.read(facts => facts.ownsPost(token, Date.now()))).value)
133
+ continue;
134
+ knownUnsent = false;
135
+ const result = await ledger.post({ owner: post.owner, key: squarePostKey(location, post), text: rendered.text,
136
+ ...(post.kind === 'activity' ? { details: { kind: 'presentation', origin: displayAttentionPath(location),
137
+ ref: formatActivityId(post.index), author: post.from, body: post.body ?? '',
138
+ ...(post.route === 'bell' ? { badge: '🔔' } : {}) } } : {}),
139
+ });
140
+ posted = ['posted', 'duplicate', 'suppressed'].includes(result.status);
141
+ knownUnsent = !posted;
142
+ if (result.status === 'conflict')
143
+ throw new Error('A Square card key already holds different text.');
144
+ }
145
+ finally {
146
+ const disposition = posted ? 'posted' : owned === 'initial' && knownUnsent ? 'never-exported' : 'uncertain';
147
+ await artifact.settleAs({ kind: 'producer' }, facts => facts.settlePost(token, rendered.full, disposition));
148
+ }
149
+ }
150
+ await suppressWithdrawn(artifact, ledger, location);
151
+ const posts = (await artifact.read(facts => facts.postedPosts().filter(post => {
152
+ const seat = facts.seat(post.recipient);
153
+ return !post.consumed && seat?.owner === post.owner && seat.generation === post.generation;
154
+ }))).value;
155
+ for (const owner of new Set(posts.map(post => post.owner))) {
156
+ const session = await ledger.session(owner);
157
+ if (session?.state !== 'attached')
158
+ continue;
159
+ const pendingKeys = new Set((await ledger.pending(owner, { generation: session.generation })).map(card => card.key));
160
+ for (const post of posts.filter(post => post.owner === owner)) {
161
+ const key = squarePostKey(location, post);
162
+ if (pendingKeys.has(key))
163
+ await dispatchPostedCard(ledger, owner, key, lifetime.track, env);
164
+ }
165
+ }
166
+ }
167
+ catch {
168
+ // Unsettled/uncertain producer work is retried with its stable key at the next mutation.
169
+ // Missing ledger evidence never proves that an external card was not inserted or delivered.
170
+ }
171
+ finally {
172
+ await lifetime?.close();
173
+ }
174
+ }
@@ -0,0 +1,11 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ProducerRead, ProducerWrite } from './square-post.js';
3
+ /** Private Square producer bookkeeping, on the artifact's existing connection/transaction. */
4
+ export declare const producerSchemaSql = "CREATE TABLE square_notices (\n notice_index INTEGER PRIMARY KEY CHECK (notice_index >= 0), at REAL NOT NULL,\n sender_json TEXT NOT NULL, body_json TEXT NOT NULL, metadata_json TEXT\n);\nCREATE TABLE notice_audience (\n notice_index INTEGER NOT NULL, recipient_key_json TEXT NOT NULL, recipient_json TEXT NOT NULL,\n owner_json TEXT NOT NULL, generation_json TEXT NOT NULL, consumed_at REAL,\n PRIMARY KEY (notice_index,recipient_key_json)\n) WITHOUT ROWID;\nCREATE TABLE producer_consumption (\n recipient_key_json TEXT NOT NULL, owner_json TEXT NOT NULL, generation_json TEXT NOT NULL,\n sender_json TEXT NOT NULL, match_json TEXT NOT NULL, at REAL NOT NULL,\n PRIMARY KEY (recipient_key_json,generation_json,sender_json,match_json)\n) WITHOUT ROWID;\nCREATE TABLE square_posts (\n kind TEXT NOT NULL CHECK (kind IN ('activity','notice')), item_index INTEGER NOT NULL,\n recipient_key_json TEXT NOT NULL, token TEXT UNIQUE, owner_pid INTEGER,\n first_export_at REAL NOT NULL, claim_at REAL NOT NULL,\n posted INTEGER NOT NULL DEFAULT 0 CHECK (posted IN (0,1)), full INTEGER CHECK (full IN (0,1)),\n PRIMARY KEY (kind,item_index,recipient_key_json)\n) WITHOUT ROWID;";
5
+ export declare function initializeProducerSqlite(db: DatabaseSync): void;
6
+ export declare function resetProducerSqlite(db: DatabaseSync): void;
7
+ export declare function createProducerDriver(db: DatabaseSync): ProducerRead & ProducerWrite & {
8
+ readonly changed: boolean;
9
+ readonly deliveryChanged: boolean;
10
+ consumeActivity(index: number, at: number): void;
11
+ };
@@ -0,0 +1,210 @@
1
+ import { matchesMetadata, validateMetadataMatch } from './delivery-deferral.js';
2
+ import { SquareError, sameName, validateName, validateActivityMetadata } from './model.js';
3
+ import { decoded, encode, finite, integer, json, key, object } from './sqlite-values.js';
4
+ /** Private Square producer bookkeeping, on the artifact's existing connection/transaction. */
5
+ export const producerSchemaSql = `CREATE TABLE square_notices (
6
+ notice_index INTEGER PRIMARY KEY CHECK (notice_index >= 0), at REAL NOT NULL,
7
+ sender_json TEXT NOT NULL, body_json TEXT NOT NULL, metadata_json TEXT
8
+ );
9
+ CREATE TABLE notice_audience (
10
+ notice_index INTEGER NOT NULL, recipient_key_json TEXT NOT NULL, recipient_json TEXT NOT NULL,
11
+ owner_json TEXT NOT NULL, generation_json TEXT NOT NULL, consumed_at REAL,
12
+ PRIMARY KEY (notice_index,recipient_key_json)
13
+ ) WITHOUT ROWID;
14
+ CREATE TABLE producer_consumption (
15
+ recipient_key_json TEXT NOT NULL, owner_json TEXT NOT NULL, generation_json TEXT NOT NULL,
16
+ sender_json TEXT NOT NULL, match_json TEXT NOT NULL, at REAL NOT NULL,
17
+ PRIMARY KEY (recipient_key_json,generation_json,sender_json,match_json)
18
+ ) WITHOUT ROWID;
19
+ CREATE TABLE square_posts (
20
+ kind TEXT NOT NULL CHECK (kind IN ('activity','notice')), item_index INTEGER NOT NULL,
21
+ recipient_key_json TEXT NOT NULL, token TEXT UNIQUE, owner_pid INTEGER,
22
+ first_export_at REAL NOT NULL, claim_at REAL NOT NULL,
23
+ posted INTEGER NOT NULL DEFAULT 0 CHECK (posted IN (0,1)), full INTEGER CHECK (full IN (0,1)),
24
+ PRIMARY KEY (kind,item_index,recipient_key_json)
25
+ ) WITHOUT ROWID;`;
26
+ export function initializeProducerSqlite(db) { db.exec(producerSchemaSql); }
27
+ export function resetProducerSqlite(db) {
28
+ for (const table of ['square_posts', 'producer_consumption', 'notice_audience', 'square_notices'])
29
+ db.exec(`DROP TABLE ${table}`);
30
+ }
31
+ function canonicalMatch(match) {
32
+ const fields = validateMetadataMatch(match).fields.map(field => ({ path: [...field.path], equals: field.equals }));
33
+ fields.sort((a, b) => {
34
+ const left = JSON.stringify(a.path), right = JSON.stringify(b.path);
35
+ return left < right ? -1 : left > right ? 1 : 0;
36
+ });
37
+ return encode({ fields });
38
+ }
39
+ const EXPORT_IDENTITY_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
40
+ const EXPORT_OWNERSHIP_MS = 60_000;
41
+ function withinExportWindow(first, now) {
42
+ return now >= first && now - first < EXPORT_IDENTITY_WINDOW_MS;
43
+ }
44
+ function processIsAlive(pid) {
45
+ try {
46
+ process.kill(pid, 0);
47
+ return true;
48
+ }
49
+ catch (error) {
50
+ return error.code !== 'ESRCH';
51
+ }
52
+ }
53
+ export function createProducerDriver(db) {
54
+ let changed = false;
55
+ const rows = (sql, args = []) => db.prepare(sql).all(...args);
56
+ const one = (sql, args = []) => db.prepare(sql).get(...args);
57
+ const run = (sql, args = []) => {
58
+ const count = Number(db.prepare(sql).run(...args).changes);
59
+ if (count)
60
+ changed = true;
61
+ return count;
62
+ };
63
+ function authority(seat) {
64
+ const current = one(`SELECT s.*,p.joined FROM participant_seats s JOIN participants p
65
+ ON p.name_key_json=s.participant_key_json WHERE s.participant_key_json=?`, [key(seat.participant)]);
66
+ if (!current || current.joined !== 1 || current.owner_json !== encode(seat.owner)
67
+ || current.generation_json !== encode(seat.generation) || current.attachment !== 'attached')
68
+ throw new SquareError('already_joined', 'This participant seat is away or has been replaced.', { reason: 'needs_seat' });
69
+ }
70
+ function consumed(post) {
71
+ return rows(`SELECT sender_json,match_json FROM producer_consumption WHERE recipient_key_json=? AND owner_json=? AND generation_json=?`, [key(post.recipient), encode(post.owner), encode(post.generation)])
72
+ .some(row => sameName(decoded(row.sender_json, true), post.from) && matchesMetadata(post.metadata, json(row.match_json)));
73
+ }
74
+ function knownConsumed(post) {
75
+ return post.kind === 'activity'
76
+ ? !!one('SELECT 1 FROM caught_activity WHERE recipient_key_json=? AND act_index=?', [key(post.recipient), post.index])
77
+ || !!one(`SELECT 1 FROM activities WHERE kind='read' AND actor_key_json=? AND json_extract(payload_json,'$.through')>=?`, [key(post.recipient), post.index])
78
+ : one('SELECT consumed_at FROM notice_audience WHERE recipient_key_json=? AND notice_index=?', [key(post.recipient), post.index])?.consumed_at != null;
79
+ }
80
+ function consume(post, at) {
81
+ return post.kind === 'activity'
82
+ ? run('INSERT INTO caught_activity(recipient_key_json,act_index,at) VALUES(?,?,?) ON CONFLICT DO NOTHING', [key(post.recipient), post.index, at])
83
+ : run('UPDATE notice_audience SET consumed_at=? WHERE recipient_key_json=? AND notice_index=? AND consumed_at IS NULL', [at, key(post.recipient), post.index]);
84
+ }
85
+ function allPosts() {
86
+ const activity = rows(`SELECT a.act_index,a.at,a.actor_json,a.payload_json,d.*,p.name_json,
87
+ x.full,x.posted FROM activities a JOIN landed_audience d ON d.act_index=a.act_index
88
+ JOIN participants p ON p.name_key_json=d.recipient_key_json
89
+ LEFT JOIN square_posts x ON x.kind='activity' AND x.item_index=a.act_index AND x.recipient_key_json=d.recipient_key_json
90
+ WHERE a.kind='say' AND d.owner_json IS NOT NULL AND d.generation_json IS NOT NULL ORDER BY a.act_index,d.recipient_order`)
91
+ .map((row) => {
92
+ const payload = object(json(row.payload_json));
93
+ return { kind: 'activity', index: integer(row.act_index), recipient: decoded(row.name_json, true),
94
+ owner: decoded(row.owner_json, true), generation: decoded(row.generation_json, true), from: decoded(row.actor_json, true), at: finite(row.at),
95
+ ...(payload.body === undefined ? {} : { body: payload.body }),
96
+ ...(payload.metadata === undefined ? {} : { metadata: payload.metadata }), route: row.reason,
97
+ ...(row.full === null ? {} : { full: row.full === 1 }) };
98
+ });
99
+ const notices = rows(`SELECT n.*,a.*,x.full FROM square_notices n JOIN notice_audience a USING(notice_index)
100
+ LEFT JOIN square_posts x ON x.kind='notice' AND x.item_index=n.notice_index AND x.recipient_key_json=a.recipient_key_json ORDER BY n.notice_index,a.recipient_key_json`)
101
+ .map((row) => ({ kind: 'notice', index: integer(row.notice_index), recipient: decoded(row.recipient_json, true),
102
+ owner: decoded(row.owner_json, true), generation: decoded(row.generation_json, true), from: decoded(row.sender_json, true), at: finite(row.at),
103
+ body: decoded(row.body_json), ...(row.metadata_json === null ? {} : { metadata: json(row.metadata_json) }),
104
+ ...(row.full === null ? {} : { full: row.full === 1 }) }));
105
+ return [...activity, ...notices].map(post => ({ ...post, consumed: knownConsumed(post) }));
106
+ }
107
+ const driver = {
108
+ pendingPosts() {
109
+ return allPosts().filter(post => !post.consumed && !one(`SELECT 1 FROM square_posts WHERE kind=? AND item_index=? AND recipient_key_json=? AND posted=1`, [post.kind, post.index, key(post.recipient)]));
110
+ },
111
+ postedPosts() {
112
+ return allPosts().filter(post => !!one(`SELECT 1 FROM square_posts WHERE kind=? AND item_index=? AND recipient_key_json=? AND (posted=1 OR token IS NOT NULL)`, [post.kind, post.index, key(post.recipient)]));
113
+ },
114
+ ownsPost(token, exportAt = Date.now()) {
115
+ const row = one('SELECT * FROM square_posts WHERE token=?', [token]);
116
+ return !!row && row.posted !== 1 && withinExportWindow(finite(row.first_export_at), finite(exportAt))
117
+ && exportAt >= finite(row.claim_at) && exportAt - finite(row.claim_at) < EXPORT_OWNERSHIP_MS;
118
+ },
119
+ insertNotice(input, at) {
120
+ validateName(input.from);
121
+ if (typeof input.body !== 'string' || !input.body.trim())
122
+ throw new SquareError('invalid_args', 'A notification needs a non-empty body.');
123
+ const metadata = validateActivityMetadata(input.metadata);
124
+ const targets = typeof input.to === 'string' ? [input.to] : input.to;
125
+ if (!Array.isArray(targets) || targets.length < 1 || targets.length > 10)
126
+ throw new SquareError('invalid_args', 'A notification needs between 1 and 10 recipients.');
127
+ const recipients = [];
128
+ for (const name of targets) {
129
+ validateName(name);
130
+ if (recipients.some(row => sameName(decoded(row.participant_json, true), name)))
131
+ continue;
132
+ const row = one(`SELECT s.* FROM participant_seats s JOIN participants p ON p.name_key_json=s.participant_key_json WHERE p.joined=1 AND p.name_key_json=?`, [key(name)]);
133
+ if (!row)
134
+ throw new SquareError('invalid_args', `Notification recipient ${name} is not standing.`, { reason: 'not_standing', targets: [name] });
135
+ recipients.push(row);
136
+ }
137
+ const index = integer(one('SELECT COALESCE(MAX(notice_index),-1)+1 AS n FROM square_notices').n);
138
+ if (!Number.isSafeInteger(index + 1))
139
+ throw new SquareError('invalid_args', 'Notification index is exhausted.');
140
+ const body = input.body;
141
+ run('INSERT INTO square_notices VALUES(?,?,?,?,?)', [index, finite(at), encode(input.from), encode(body), metadata === undefined ? null : encode(metadata)]);
142
+ for (const row of recipients) {
143
+ const post = { kind: 'notice', index, recipient: decoded(row.participant_json, true), owner: decoded(row.owner_json, true),
144
+ generation: decoded(row.generation_json, true), from: input.from, body, at, ...(metadata === undefined ? {} : { metadata }) };
145
+ run('INSERT INTO notice_audience VALUES(?,?,?,?,?,?)', [index, key(post.recipient), encode(post.recipient), encode(post.owner), encode(post.generation), consumed(post) ? at : null]);
146
+ }
147
+ return { id: `notice/${index}`, at, from: input.from, to: recipients.map(row => decoded(row.participant_json, true)), body, ...(metadata === undefined ? {} : { metadata }) };
148
+ },
149
+ ackMatching(seat, input, at) {
150
+ authority(seat);
151
+ validateName(input.from);
152
+ if (input.future !== 'exact')
153
+ throw new SquareError('invalid_args', 'Future consumption requires an explicit exact outcome.');
154
+ const match = canonicalMatch(input.match);
155
+ run('INSERT INTO producer_consumption VALUES(?,?,?,?,?,?) ON CONFLICT DO NOTHING', [key(seat.participant), encode(seat.owner), encode(seat.generation), key(input.from), match, finite(at)]);
156
+ let count = 0;
157
+ for (const post of allPosts()) {
158
+ if (sameName(post.recipient, seat.participant) && post.owner === seat.owner && post.generation === seat.generation
159
+ && sameName(post.from, input.from) && matchesMetadata(post.metadata, input.match) && !post.consumed)
160
+ count += consume(post, at);
161
+ }
162
+ return count;
163
+ },
164
+ acquirePost(post, token, pid, now, exportAt = Date.now()) {
165
+ // A scanned value is not custody. Re-read all source and policy facts under the World writer.
166
+ const current = allPosts().find(candidate => candidate.kind === post.kind && candidate.index === post.index && candidate.recipient === post.recipient);
167
+ if (!current || current.owner !== post.owner || current.generation !== post.generation || current.consumed)
168
+ return false;
169
+ const seat = one('SELECT * FROM participant_seats WHERE participant_key_json=?', [key(post.recipient)]);
170
+ if (!seat || seat.owner_json !== encode(post.owner) || seat.generation_json !== encode(post.generation) || seat.attachment !== 'attached')
171
+ return false;
172
+ if (consumed(current)) {
173
+ consume(current, now);
174
+ return false;
175
+ }
176
+ const previous = one('SELECT * FROM square_posts WHERE kind=? AND item_index=? AND recipient_key_json=?', [post.kind, post.index, key(post.recipient)]);
177
+ finite(exportAt);
178
+ if (previous?.posted === 1)
179
+ return false;
180
+ const first = previous ? finite(previous.first_export_at) : exportAt;
181
+ // Missing ledger identity after retention cannot prove an abandoned export was unsent.
182
+ if (!withinExportWindow(first, exportAt) || previous && exportAt < finite(previous.claim_at))
183
+ return false;
184
+ if (previous?.token != null && exportAt >= finite(previous.claim_at)
185
+ && exportAt - finite(previous.claim_at) < EXPORT_OWNERSHIP_MS && processIsAlive(integer(previous.owner_pid, 1)))
186
+ return false;
187
+ run(`INSERT INTO square_posts(kind,item_index,recipient_key_json,token,owner_pid,full,first_export_at,claim_at) VALUES(?,?,?,?,?,?,?,?)
188
+ ON CONFLICT(kind,item_index,recipient_key_json) DO UPDATE SET token=excluded.token,owner_pid=excluded.owner_pid,full=excluded.full,claim_at=excluded.claim_at`, [post.kind, post.index, key(post.recipient), token, integer(pid, 1), post.full === undefined ? null : Number(post.full), first, exportAt]);
189
+ return previous ? 'recovered' : 'initial';
190
+ },
191
+ settlePost(token, full, disposition) {
192
+ // Only a proved-unsent initial claim may erase its export age; prior uncertainty survives.
193
+ if (disposition === 'never-exported')
194
+ return run('DELETE FROM square_posts WHERE token=?', [token]) > 0;
195
+ return run('UPDATE square_posts SET token=NULL,owner_pid=NULL,posted=?,full=? WHERE token=?', [Number(disposition === 'posted'), Number(full), token]) > 0;
196
+ },
197
+ foldReceipt(post, at) {
198
+ const current = allPosts().find(candidate => candidate.kind === post.kind && candidate.index === post.index && candidate.recipient === post.recipient);
199
+ if (!current || current.owner !== post.owner || current.generation !== post.generation || current.full !== true || current.consumed)
200
+ return false;
201
+ return consume(current, finite(at)) > 0;
202
+ },
203
+ consumeActivity(index, at) {
204
+ for (const post of allPosts())
205
+ if (post.kind === 'activity' && post.index === index && consumed(post))
206
+ consume(post, at);
207
+ },
208
+ };
209
+ return Object.defineProperties(driver, { changed: { get: () => changed }, deliveryChanged: { get: () => changed } });
210
+ }