@astrosheep/square 0.3.105 → 0.4.0

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 (143) 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 +8 -12
  5. package/dist/artifact.js +66 -343
  6. package/dist/attention-presentation.d.ts +33 -7
  7. package/dist/attention-presentation.js +33 -69
  8. package/dist/boundary-presentation.d.ts +0 -2
  9. package/dist/boundary-presentation.js +17 -22
  10. package/dist/catch-decisions.d.ts +2 -2
  11. package/dist/catch-decisions.js +14 -4
  12. package/dist/catch-selection.d.ts +8 -0
  13. package/dist/catch-selection.js +5 -0
  14. package/dist/channel-artifact.d.ts +10 -0
  15. package/dist/channel-artifact.js +55 -0
  16. package/dist/channel-delivery.d.ts +11 -0
  17. package/dist/channel-delivery.js +51 -0
  18. package/dist/channel-domain.d.ts +3 -0
  19. package/dist/channel-domain.js +83 -0
  20. package/dist/channel-facade.d.ts +76 -0
  21. package/dist/channel-facade.js +1 -0
  22. package/dist/channel-internal.d.ts +126 -0
  23. package/dist/channel-internal.js +440 -0
  24. package/dist/channel-metadata.d.ts +11 -0
  25. package/dist/channel-metadata.js +1 -0
  26. package/dist/channel-ports.d.ts +241 -0
  27. package/dist/channel-ports.js +1 -0
  28. package/dist/channel-presentation.d.ts +17 -0
  29. package/dist/channel-presentation.js +22 -0
  30. package/dist/channel-sqlite-schema.d.ts +13 -0
  31. package/dist/channel-sqlite-schema.js +81 -0
  32. package/dist/channel-transport.d.ts +62 -0
  33. package/dist/channel-transport.js +1 -0
  34. package/dist/channel-values.d.ts +27 -0
  35. package/dist/channel-values.js +90 -0
  36. package/dist/channel.d.ts +31 -21
  37. package/dist/channel.js +42 -74
  38. package/dist/claude-delivery-status.js +1 -1
  39. package/dist/claude-delivery.d.ts +2 -5
  40. package/dist/claude-delivery.js +56 -103
  41. package/dist/claude-mod.js +4 -4
  42. package/dist/claude-transport.d.ts +5 -0
  43. package/dist/claude-transport.js +58 -0
  44. package/dist/cli/context.d.ts +2 -2
  45. package/dist/cli/context.js +0 -1
  46. package/dist/cli/maintenance-commands.d.ts +0 -4
  47. package/dist/cli/maintenance-commands.js +2 -29
  48. package/dist/cli/program.js +1 -1
  49. package/dist/cli/registry.js +1 -2
  50. package/dist/codex-address.d.ts +1 -1
  51. package/dist/codex-queue.d.ts +2 -2
  52. package/dist/codex-queue.js +1 -3
  53. package/dist/decisions.js +3 -3
  54. package/dist/delivery-deferral.d.ts +9 -18
  55. package/dist/delivery-deferral.js +4 -4
  56. package/dist/delivery-health.d.ts +4 -4
  57. package/dist/delivery-health.js +11 -11
  58. package/dist/delivery-operations.d.ts +15 -2
  59. package/dist/delivery-operations.js +60 -30
  60. package/dist/delivery.d.ts +44 -33
  61. package/dist/delivery.js +49 -75
  62. package/dist/help.js +1 -6
  63. package/dist/http-delivery.d.ts +2 -2
  64. package/dist/http-delivery.js +2 -4
  65. package/dist/inbox.d.ts +10 -18
  66. package/dist/inbox.js +44 -53
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/mcp.d.ts +2 -1
  70. package/dist/mcp.js +40 -70
  71. package/dist/message.d.ts +7 -0
  72. package/dist/message.js +23 -0
  73. package/dist/model.d.ts +41 -77
  74. package/dist/model.js +7 -139
  75. package/dist/neutral.d.ts +72 -0
  76. package/dist/neutral.js +27 -0
  77. package/dist/notifications.d.ts +2 -8
  78. package/dist/notifications.js +15 -50
  79. package/dist/operation-presentation.d.ts +1 -2
  80. package/dist/operation-presentation.js +1 -7
  81. package/dist/owner.d.ts +2 -2
  82. package/dist/owner.js +1 -1
  83. package/dist/paseo-delivery.d.ts +1 -1
  84. package/dist/paseo-delivery.js +2 -4
  85. package/dist/paseo-timeline.js +1 -0
  86. package/dist/plugins/opencode/delivery.js +27 -42
  87. package/dist/ports.d.ts +46 -168
  88. package/dist/presentation-operations.d.ts +1 -14
  89. package/dist/presentation-operations.js +16 -16
  90. package/dist/presentation.d.ts +5 -5
  91. package/dist/presentation.js +1 -0
  92. package/dist/runtime.js +2 -2
  93. package/dist/search.js +1 -1
  94. package/dist/sqlite-channel.d.ts +15 -0
  95. package/dist/sqlite-channel.js +600 -0
  96. package/dist/sqlite-framing.d.ts +8 -0
  97. package/dist/sqlite-framing.js +31 -0
  98. package/dist/sqlite-square.d.ts +12 -13
  99. package/dist/sqlite-square.js +192 -248
  100. package/dist/sqlite-values.d.ts +21 -0
  101. package/dist/sqlite-values.js +78 -0
  102. package/dist/square-api.d.ts +1 -1
  103. package/dist/square-api.js +1 -1
  104. package/dist/square-core.d.ts +1 -1
  105. package/dist/square-domain.d.ts +21 -7
  106. package/dist/square-domain.js +93 -94
  107. package/dist/square-facade.d.ts +12 -66
  108. package/dist/square-index.d.ts +1 -1
  109. package/dist/square-index.js +1 -1
  110. package/dist/square-location.d.ts +6 -1
  111. package/dist/square-location.js +1 -1
  112. package/dist/square-receive.d.ts +2 -3
  113. package/dist/square-sqlite.d.ts +45 -44
  114. package/dist/square-sqlite.js +308 -764
  115. package/dist/square-tools.d.ts +8 -0
  116. package/dist/square-tools.js +39 -0
  117. package/dist/views.d.ts +6 -6
  118. package/dist/views.js +15 -9
  119. package/dist/wake-preview.d.ts +7 -0
  120. package/dist/wake-preview.js +15 -0
  121. package/dist/wake-sink.d.ts +2 -2
  122. package/dist/wake-sink.js +1 -1
  123. package/dist/wake-transports.d.ts +7 -0
  124. package/dist/wake-transports.js +42 -0
  125. package/guides/claude-native.md +13 -10
  126. package/guides/codex-delivery.md +3 -1
  127. package/guides/participant-ownership.md +1 -1
  128. package/guides/row-runtime-delivery-evidence.md +2 -0
  129. package/guides/row-storage-design.md +19 -44
  130. package/guides/square-api.md +137 -60
  131. package/package.json +3 -3
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/types.d.ts +1 -1
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +28 -41
  136. package/plugins/pi/square-pi.js +28 -51
  137. package/dist/square-legacy-framing.d.ts +0 -11
  138. package/dist/square-legacy-framing.js +0 -64
  139. package/dist/square-migration-host-ledger.d.ts +0 -26
  140. package/dist/square-migration-host-ledger.js +0 -169
  141. package/dist/square-migration.d.ts +0 -30
  142. package/dist/square-migration.js +0 -599
  143. package/guides/migration.md +0 -156
@@ -0,0 +1,440 @@
1
+ import crypto from 'node:crypto';
2
+ import { openChannelArtifact, createChannelArtifactFile, createMemoryChannelArtifact, channelCourier } from './channel-artifact.js';
3
+ import { canonicalPath } from './canonical-path.js';
4
+ import { codexWakeAddress } from './codex-address.js';
5
+ import { validateDeferralDuration, validateDeferralId, validateMetadataMatch } from './delivery-deferral.js';
6
+ import { compileSearchPattern } from './search.js';
7
+ import { InternalSquareError, SquareError } from './neutral.js';
8
+ import { messageMetadataHasContent, sameName, validateMessageMetadata, validateName } from './channel-values.js';
9
+ import { consumeReceived } from './receive.js';
10
+ import { harnessOwner } from './owner.js';
11
+ import { harnessSessionSources } from './participant-identity.js';
12
+ import { formatMessageId, newEndpointGeneration, parseMessageId, validateEndpointGeneration } from './message.js';
13
+ import { rememberSquarePath, squareIndexPath } from './square-index.js';
14
+ import { squareLocation } from './square-location.js';
15
+ export { harnessOwner, decodeHarnessOwner, localUserOwner } from './owner.js';
16
+ export function resolveHarnessSession(env = process.env) {
17
+ for (const { variable, channel } of harnessSessionSources) {
18
+ const sessionId = env[variable]?.trim();
19
+ if (!sessionId)
20
+ continue;
21
+ const agentId = channel === 'claude-code' && env.CLAUDE_CODE_CHILD_SESSION?.trim() === '1'
22
+ ? env.CLAUDE_CODE_AGENT_ID?.trim() || undefined : undefined;
23
+ const wakeAddress = channel === 'codex' ? codexWakeAddress(sessionId, env)
24
+ : channel === 'paseo' ? { kind: 'paseo', address: { agentId: sessionId } } : undefined;
25
+ return { channel, sessionId, ...(agentId === undefined ? {} : { agentId }),
26
+ owner: harnessOwner({ channel, session: sessionId, ...(agentId === undefined ? {} : { agent: agentId }) }),
27
+ ...(wakeAddress === undefined ? {} : { wakeAddress }) };
28
+ }
29
+ return undefined;
30
+ }
31
+ export function checkControl(control) {
32
+ if (control?.signal?.aborted)
33
+ throw control.signal.reason ?? new Error('Square operation was cancelled');
34
+ }
35
+ export function squareClock(env, clock) {
36
+ if (clock !== undefined)
37
+ return clock;
38
+ if (env.SQUARE_NOW_MS === undefined)
39
+ return Date.now;
40
+ const value = Number.parseInt(env.SQUARE_NOW_MS, 10);
41
+ if (!Number.isFinite(value))
42
+ throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
43
+ return () => value;
44
+ }
45
+ export function failure(error, detail) {
46
+ const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
47
+ Object.assign(normalized, { operation: detail });
48
+ return normalized;
49
+ }
50
+ function explicitLocation(input) {
51
+ if (typeof input?.path !== 'string' || input.path.trim() === '')
52
+ throw new SquareError('invalid_args', 'Channel path is required.');
53
+ return squareLocation(input);
54
+ }
55
+ function requireOwner(owner) {
56
+ if (typeof owner !== 'string' || !owner || Buffer.byteLength(owner, 'utf8') > 512 || /[\u0000-\u001f\u007f-\u009f]/u.test(owner))
57
+ throw new SquareError('invalid_args', 'A participant write requires a non-empty owner without control characters (at most 512 UTF-8 bytes).');
58
+ return owner;
59
+ }
60
+ /** A generic managed address must not displace the receiver's native registration on reconnect. */
61
+ function canRefreshWakeAddress(current, proposed) {
62
+ return proposed.kind !== 'paseo' || current === undefined || current.kind === 'paseo';
63
+ }
64
+ /** Foundational point-to-point endpoint and messaging resource over one artifact port. */
65
+ export class Channel {
66
+ location;
67
+ artifact;
68
+ clock;
69
+ deliverEffect;
70
+ indexPath;
71
+ closesArtifact;
72
+ constructor(location, artifact, clock, deliverEffect, indexPath, closesArtifact = true) {
73
+ this.location = location;
74
+ this.artifact = artifact;
75
+ this.clock = clock;
76
+ this.deliverEffect = deliverEffect;
77
+ this.indexPath = indexPath;
78
+ this.closesArtifact = closesArtifact;
79
+ }
80
+ /** Internal composition point for upper resources; no wrapping square is involved. */
81
+ static fromPort(init) {
82
+ return new Channel(init.location, init.artifact, init.clock, init.deliverEffect, init.indexPath, init.closesArtifact ?? true);
83
+ }
84
+ static async open(input, control) {
85
+ const path = explicitLocation(input);
86
+ const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env, path });
87
+ const clock = squareClock(env, input.clock);
88
+ const artifact = openChannelArtifact(location, control?.signal);
89
+ const transport = input.wakeTransport !== undefined
90
+ ? input.wakeTransport || undefined
91
+ : await import('./wake-transports.js').then(({ createDefaultWakeTransport }) => createDefaultWakeTransport(env)).catch(() => undefined);
92
+ try {
93
+ await artifact.read(facts => facts.header(), control?.signal);
94
+ const deliveryLocation = await canonicalPath(location);
95
+ return Channel.fromPort({ location, artifact, clock,
96
+ ...(transport === undefined ? {} : { deliverEffect: channelCourier(artifact, transport, deliveryLocation, clock) }),
97
+ indexPath: squareIndexPath(env) });
98
+ }
99
+ catch (error) {
100
+ await artifact.close();
101
+ if (error instanceof InternalSquareError && error.code === 'not_found')
102
+ throw new SquareError('unavailable', `Channel is unavailable at ${location}`);
103
+ throw error;
104
+ }
105
+ }
106
+ static async create(input, control) {
107
+ const path = explicitLocation(input);
108
+ const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env, path });
109
+ // Endpoints and messages carry no Agora chatroom warmup.
110
+ await createChannelArtifactFile(location, input.description ?? '# Channel\n', input.force ?? false, control?.signal);
111
+ return Channel.open({ ...input, path: location, env }, control);
112
+ }
113
+ static inMemory(input = {}) {
114
+ const env = { ...(input.env ?? process.env) };
115
+ const artifact = createMemoryChannelArtifact(input.description ?? '# Channel\n');
116
+ const location = `memory://${crypto.randomUUID()}`, clock = squareClock(env, input.clock);
117
+ const transport = input.wakeTransport || undefined;
118
+ return Channel.fromPort({ location, artifact, clock,
119
+ ...(transport === undefined ? {} : { deliverEffect: channelCourier(artifact, transport, location, clock) }) });
120
+ }
121
+ async remember(owner) {
122
+ if (this.indexPath !== undefined)
123
+ await rememberSquarePath(this.location, owner, this.indexPath).catch(() => undefined);
124
+ }
125
+ /** Establish a new opaque endpoint generation, or take over the existing one explicitly. */
126
+ async attach(name, options, control) {
127
+ checkControl(control);
128
+ validateName(name);
129
+ const owner = requireOwner(options?.owner);
130
+ const endpoint = (await this.artifact.transactAs({ kind: 'endpoint', participant: name, owner }, (facts) => {
131
+ const current = facts.endpoint(name);
132
+ if (current !== undefined && !options.takeover) {
133
+ if (current.owner !== owner)
134
+ throw new SquareError('already_joined', `${name} is held by another owner; take it over explicitly.`);
135
+ if (current.attachment === 'detached')
136
+ facts.reattachEndpoint(current, options.wakeAddress);
137
+ else if (options.wakeAddress !== undefined && canRefreshWakeAddress(current.wakeAddress, options.wakeAddress))
138
+ facts.updateWakeAddress(current, options.wakeAddress);
139
+ return facts.endpoint(name);
140
+ }
141
+ const endpoint = { participant: name, owner, generation: newEndpointGeneration(), attachment: 'attached',
142
+ ...(options.wakeAddress === undefined ? {} : { wakeAddress: options.wakeAddress }) };
143
+ facts.storeEndpoint(endpoint);
144
+ return endpoint;
145
+ }, control?.signal)).value;
146
+ await this.remember(owner);
147
+ return new ChannelEndpoint(this, endpoint);
148
+ }
149
+ /** Restore an exact standing endpoint without changing its generation. */
150
+ async reattach(name, { owner, wakeAddress }, control) {
151
+ checkControl(control);
152
+ validateName(name);
153
+ requireOwner(owner);
154
+ const current = (await this.artifact.read(facts => facts.endpoint(name), control?.signal)).value;
155
+ if (current === undefined || current.owner !== owner)
156
+ return null;
157
+ return this.reattachEndpoint(current, wakeAddress, control);
158
+ }
159
+ async reattachEndpoint(current, wakeAddress, control) {
160
+ const endpoint = (await this.artifact.transactAs({ kind: 'endpoint', participant: current.participant, owner: current.owner }, (facts) => {
161
+ if (current.attachment === 'detached')
162
+ facts.reattachEndpoint(current, wakeAddress);
163
+ else if (wakeAddress !== undefined && canRefreshWakeAddress(current.wakeAddress, wakeAddress))
164
+ facts.updateWakeAddress(current, wakeAddress);
165
+ return facts.endpoint(current.participant);
166
+ }, control?.signal)).value;
167
+ return new ChannelEndpoint(this, endpoint);
168
+ }
169
+ /** Select one attached endpoint held by an exact owner. */
170
+ async endpoint(name, { owner }) {
171
+ validateName(name);
172
+ requireOwner(owner);
173
+ const endpoint = (await this.artifact.read(facts => {
174
+ const current = facts.endpoint(name);
175
+ if (current === undefined || current.owner !== owner || current.attachment !== 'attached') {
176
+ throw new SquareError('already_joined', current === undefined
177
+ ? `${name} has no endpoint in this channel.`
178
+ : current.owner !== owner ? `${name} is not held by this owner.` : `${name} is away.`, { reason: 'needs_seat' });
179
+ }
180
+ return current;
181
+ })).value;
182
+ return new ChannelEndpoint(this, endpoint);
183
+ }
184
+ async endpointsOf(owner) {
185
+ requireOwner(owner);
186
+ return (await this.artifact.read(facts => facts.endpointsForOwner(owner).map(({ participant, generation, attachment }) => ({ name: participant, generation, attachment })))).value;
187
+ }
188
+ async listEndpoints(control) {
189
+ checkControl(control);
190
+ return (await this.artifact.read(facts => facts.endpoints()
191
+ .map(endpoint => ({
192
+ name: endpoint.participant,
193
+ generation: endpoint.generation,
194
+ attachment: endpoint.attachment,
195
+ deliveryAvailable: endpoint.wakeAddress !== undefined,
196
+ unreadCount: facts.directedCount(endpoint.participant, { unseenOnly: true, currentGeneration: true }),
197
+ })), control?.signal)).value;
198
+ }
199
+ async detach(name, identity) {
200
+ const ref = { participant: name, ...identity };
201
+ const current = await this.held(name, ref);
202
+ if (current === undefined)
203
+ return null;
204
+ return (await this.artifact.transactAs({ kind: 'endpoint-cleanup', seat: ref }, facts => facts.detachEndpoint(ref) ? true : null)).value;
205
+ }
206
+ async cancelDeliveries(name, identity, control) {
207
+ const ref = { participant: name, ...identity };
208
+ const current = await this.held(name, ref);
209
+ if (current === undefined)
210
+ return false;
211
+ return (await this.artifact.transactAs({ kind: 'participant', seat: ref }, facts => {
212
+ const boundary = facts.header().lastMessageIndex;
213
+ return boundary < 0 ? false : facts.cancelThrough(ref, boundary);
214
+ }, control?.signal)).value;
215
+ }
216
+ async remove(name, identity) {
217
+ const ref = { participant: name, ...identity };
218
+ const current = await this.held(name, ref);
219
+ if (current === undefined)
220
+ return false;
221
+ return (await this.artifact.transactAs({ kind: 'endpoint-cleanup', seat: ref }, facts => facts.removeEndpoint(ref))).value;
222
+ }
223
+ async held(name, identity) {
224
+ const current = (await this.artifact.read(facts => facts.endpoint(name))).value;
225
+ return current?.generation === identity.generation && current.owner === identity.owner ? current : undefined;
226
+ }
227
+ /** Commit one message, then run the shared custody runner outside the transaction. */
228
+ async send(input, control) {
229
+ checkControl(control);
230
+ const { from } = input;
231
+ if (typeof from !== 'string')
232
+ throw new SquareError('invalid_args', 'Channel sender must be a name.');
233
+ validateName(from);
234
+ return this.sendAs({ kind: 'send', actor: from }, input, control);
235
+ }
236
+ async sendAs(authority, input, control) {
237
+ checkControl(control);
238
+ const { from, to, body, truncate = false } = input;
239
+ const targets = typeof to === 'string' ? [to] : to;
240
+ if (!Array.isArray(targets) || targets.length < 1 || targets.length > 10)
241
+ throw new SquareError('invalid_args', 'Channel needs between 1 and 10 recipients.');
242
+ const unique = [];
243
+ for (const target of targets) {
244
+ if (typeof target !== 'string')
245
+ throw new SquareError('invalid_args', 'Channel recipients must be names.');
246
+ validateName(target);
247
+ if (!unique.some(name => sameName(name, target)))
248
+ unique.push(target);
249
+ }
250
+ if (typeof body !== 'string' || body.trim() === '')
251
+ throw new SquareError('invalid_args', 'Channel body must be non-empty.');
252
+ if (truncate !== false && (!Number.isSafeInteger(truncate) || truncate < 1))
253
+ throw new SquareError('invalid_args', 'Channel truncate must be false or a positive integer.');
254
+ const fullBody = body.replace(/\r\n/g, '\n');
255
+ const preview = truncate !== false && fullBody.length > truncate
256
+ ? `${fullBody.slice(0, truncate).trimEnd()}… (truncated ${fullBody.length - truncate} chars)` : undefined;
257
+ const metadata = validateMessageMetadata(input.metadata);
258
+ const { published, addresses } = (await this.artifact.transactAs(authority, (facts) => {
259
+ const current = facts;
260
+ const resolved = unique.flatMap(target => { const endpoint = current.endpoint(target); return endpoint === undefined ? [] : [endpoint]; });
261
+ const missing = unique.filter(target => !resolved.some(endpoint => sameName(endpoint.participant, target)));
262
+ if (missing.length > 0)
263
+ throw new SquareError('invalid_args', `Channel recipients are not standing: ${missing.join(', ')}`, {
264
+ reason: 'not_standing', targets: missing, activityUnsent: true,
265
+ });
266
+ const draft = { from, at: this.clock(), body: fullBody,
267
+ ...(messageMetadataHasContent(metadata) ? { metadata } : {}), ...(preview === undefined ? {} : { preview }),
268
+ // An ordinary Channel send captures no endpoint generation: the message survives takeover until consumed.
269
+ to: resolved.map(({ participant }) => ({ recipient: participant })) };
270
+ const published = current.publishMessage(draft);
271
+ return { published, addresses: current.addresses(published.index).map(address => address.recipient) };
272
+ }, control?.signal)).value;
273
+ await this.deliver(published.index);
274
+ return { id: formatMessageId(published.index), at: published.at, from, to: addresses, body: fullBody,
275
+ ...(published.metadata === undefined ? {} : { metadata: published.metadata }) };
276
+ }
277
+ /** Shared post-commit custody runner. */
278
+ async deliver(message) {
279
+ await this.deliverEffect?.(message).catch(() => undefined);
280
+ }
281
+ async history(query = {}, control) {
282
+ checkControl(control);
283
+ const limit = query.limit === undefined ? 100 : query.limit;
284
+ if (limit !== null && (!Number.isSafeInteger(limit) || limit < 1 || limit > 100))
285
+ throw new SquareError('invalid_args', 'History limit must be a positive integer no greater than 100.');
286
+ if (query.order !== undefined && query.order !== 'asc' && query.order !== 'desc')
287
+ throw new SquareError('invalid_args', 'History order must be asc or desc.');
288
+ const filter = {
289
+ ...(query.at === undefined ? {} : { indexes: query.at.map(requireMessageIndex) }),
290
+ ...(query.from === undefined ? {} : { senders: query.from }),
291
+ ...(query.match === undefined ? {} : { metadataMatch: validateMetadataMatch(query.match) }),
292
+ ...(query.search === undefined ? {} : { search: channelSearch(query.search) }),
293
+ ...(query.cursor === undefined ? {} : query.cursor.direction === 'after' ? { afterIndex: requireMessageIndex(query.cursor.id) } : { beforeIndex: requireMessageIndex(query.cursor.id) }),
294
+ };
295
+ return (await this.artifact.read(facts => {
296
+ const current = facts;
297
+ const count = current.messageCount(filter);
298
+ const page = current.messageHeads(filter, { edge: query.cursor?.direction === 'after' ? 'oldest' : 'newest', limit: limit ?? Math.max(1, count) });
299
+ const messages = page.items.map(item => channelMessage(item, current));
300
+ if (query.order === 'desc')
301
+ messages.reverse();
302
+ return { messages };
303
+ }, control?.signal)).value;
304
+ }
305
+ async close() {
306
+ if (this.closesArtifact)
307
+ await this.artifact.close();
308
+ }
309
+ }
310
+ function requireMessageIndex(id) {
311
+ const index = parseMessageId(id);
312
+ if (index === undefined)
313
+ throw new SquareError('invalid_args', `Invalid message id: ${id}`);
314
+ return index;
315
+ }
316
+ function channelMessage(item, facts) {
317
+ return { id: formatMessageId(item.index), at: item.at, from: item.from,
318
+ to: facts.addresses(item.index).map(address => address.recipient), body: item.body,
319
+ ...(item.metadata === undefined ? {} : { metadata: item.metadata }), ...(item.preview === undefined ? {} : { preview: item.preview }) };
320
+ }
321
+ function channelSearch(search) {
322
+ if (typeof search !== 'object' || search === null || typeof search.text !== 'string' || search.text === ''
323
+ || (search.mode !== 'literal' && search.mode !== 'regex') || Object.keys(search).some(key => key !== 'text' && key !== 'mode'))
324
+ throw new SquareError('invalid_args', 'Query search requires non-empty text and literal/regex mode.');
325
+ compileSearchPattern(search.text, search.mode === 'literal');
326
+ return { text: search.text, mode: search.mode };
327
+ }
328
+ function channelPendingSelection(options) {
329
+ const limit = options.limit === undefined ? 100 : options.limit;
330
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
331
+ throw new SquareError('invalid_args', 'Pending limit must be a positive integer no greater than 100.');
332
+ const filter = {
333
+ ...(options.at === undefined ? {} : { indexes: options.at.map(requireMessageIndex) }),
334
+ ...(options.from === undefined ? {} : { senders: options.from }),
335
+ ...(options.match === undefined ? {} : { metadataMatch: validateMetadataMatch(options.match) }),
336
+ ...(options.search === undefined ? {} : { search: channelSearch(options.search) }),
337
+ ...(options.cursor === undefined ? {} : options.cursor.direction === 'after' ? { afterIndex: requireMessageIndex(options.cursor.id) } : { beforeIndex: requireMessageIndex(options.cursor.id) }),
338
+ unseenOnly: true, currentGeneration: true,
339
+ };
340
+ return { limit, filter };
341
+ }
342
+ /** Captures one recipient endpoint through the channel's own storage, never a wrapped square. */
343
+ export class ChannelEndpoint {
344
+ channel;
345
+ endpointFact;
346
+ constructor(channel, endpointFact) {
347
+ this.channel = channel;
348
+ this.endpointFact = endpointFact;
349
+ }
350
+ get name() { return this.endpointFact.participant; }
351
+ get generation() { return this.endpointFact.generation; }
352
+ get ref() { return this.endpointFact; }
353
+ get storage() { return this.channel.artifact; }
354
+ get participant() { return this.endpointFact.participant; }
355
+ async pending(options = {}, control) {
356
+ checkControl(control);
357
+ const { limit, filter } = channelPendingSelection(options);
358
+ return (await this.storage.read(facts => {
359
+ assertEndpoint(facts, this.endpointFact);
360
+ const page = facts.directed(this.participant, filter, { edge: 'oldest', limit });
361
+ const loaded = new Map(facts.messages(page.items.map(item => item.index)).map(item => [item.index, item]));
362
+ return { messages: page.items.flatMap(head => { const item = loaded.get(head.index); return item === undefined ? [] : [channelMessage(item, facts)]; }),
363
+ remaining: facts.directedCount(this.participant, filter) - page.items.length };
364
+ }, control?.signal)).value;
365
+ }
366
+ async ack(ids, control) {
367
+ checkControl(control);
368
+ if (!Array.isArray(ids))
369
+ throw new SquareError('invalid_args', 'Ack requires an array of message IDs.');
370
+ const indexes = [...new Set(Array.from(ids, requireMessageIndex))];
371
+ return (await this.storage.transactAs({ kind: 'participant', seat: this.endpointFact }, (facts) => {
372
+ for (const index of indexes) {
373
+ if (facts.directed(this.participant, { afterIndex: index - 1, throughIndex: index, currentGeneration: true }, { edge: 'oldest', limit: 1 }).items.length === 0)
374
+ throw new SquareError('invalid_args', `${formatMessageId(index)} is not addressed to you.`, {
375
+ reason: 'not_addressed', activityId: formatMessageId(index),
376
+ });
377
+ }
378
+ return { consumed: facts.markSeen(this.participant, indexes, this.channel.clock()) };
379
+ }, control?.signal)).value;
380
+ }
381
+ /** Select and consume matching unconsumed messages in one transaction. */
382
+ async receive(options = {}, control) {
383
+ checkControl(control);
384
+ const { limit, filter } = channelPendingSelection(options);
385
+ return (await this.storage.transactAs({ kind: 'participant', seat: this.endpointFact }, (facts) => {
386
+ const lower = facts;
387
+ const consumed = consumeReceived({ heads: (recipient, query, page) => lower.directed(recipient, query, { edge: 'oldest', limit: page.limit }).items.map(item => ({ key: item.index, via: item })),
388
+ addressed: (recipient, index) => { const head = lower.directed(recipient, { indexes: [index] }, { edge: 'oldest', limit: 1 }).items[0]; return head === undefined ? undefined : { key: head.index, via: head }; },
389
+ load: (keys) => new Map(lower.messages(keys).map(item => [item.index, item])),
390
+ count: (recipient, query) => lower.directedCount(recipient, query), insertReceipt: (ref, at) => lower.markSeen(ref.recipient, [ref.item], at) > 0,
391
+ }, this.participant, { kind: 'page', query: filter, limit }, this.channel.clock());
392
+ return { messages: consumed.items.map(item => channelMessage(item.data, lower)),
393
+ consumed: consumed.newlyConsumed, remaining: consumed.remaining };
394
+ }, control?.signal)).value;
395
+ }
396
+ async deferDelivery(input, control) {
397
+ checkControl(control);
398
+ const match = validateMetadataMatch(input?.match), duration = validateDeferralDuration(input?.durationMs);
399
+ return (await this.storage.transactAs({ kind: 'participant', seat: this.endpointFact }, (facts) => {
400
+ const now = this.channel.clock(), expiresAt = now + duration, id = crypto.randomUUID();
401
+ const result = { id, expiresAt: new Date(expiresAt).toISOString() };
402
+ facts.putDeliveryDeferral(this.endpointFact, id, match, expiresAt, now);
403
+ return result;
404
+ }, control?.signal)).value;
405
+ }
406
+ async renewDeliveryDeferral(id, durationMs, control) {
407
+ checkControl(control);
408
+ validateDeferralId(id);
409
+ const duration = validateDeferralDuration(durationMs);
410
+ return (await this.storage.transactAs({ kind: 'participant', seat: this.endpointFact }, (facts) => {
411
+ const now = this.channel.clock(), expiresAt = now + duration;
412
+ const result = { id, expiresAt: new Date(expiresAt).toISOString() };
413
+ return facts.renewDeliveryDeferral(this.endpointFact, id, expiresAt, now) ? result : null;
414
+ }, control?.signal)).value;
415
+ }
416
+ async releaseDeliveryDeferral(id, control) {
417
+ checkControl(control);
418
+ validateDeferralId(id);
419
+ return (await this.storage.transactAs({ kind: 'participant', seat: this.endpointFact }, (facts) => facts.releaseDeliveryDeferral(this.endpointFact, id, this.channel.clock()), control?.signal)).value;
420
+ }
421
+ send(input, control) {
422
+ return this.channel.sendAs({ kind: 'participant', seat: this.endpointFact }, { ...input, from: this.participant }, control);
423
+ }
424
+ /** Standalone Channel retirement; it ends no Agora membership. */
425
+ async leave(control) {
426
+ checkControl(control);
427
+ const current = (await this.storage.read(facts => facts.endpoint(this.participant), control?.signal)).value;
428
+ if (current === undefined || current.owner !== this.endpointFact.owner || current.generation !== this.endpointFact.generation || current.attachment !== 'attached')
429
+ throw new SquareError('already_joined', 'This participant endpoint is not attached.');
430
+ await this.channel.remove(this.participant, { owner: this.endpointFact.owner, generation: this.endpointFact.generation });
431
+ }
432
+ }
433
+ function assertEndpoint(facts, expected) {
434
+ const current = facts.endpoint(expected.participant);
435
+ if (current === undefined || current.owner !== expected.owner || current.generation !== expected.generation)
436
+ throw new SquareError('already_joined', 'This participant endpoint has been replaced.');
437
+ if (current.attachment !== 'attached')
438
+ throw new SquareError('already_joined', 'This participant is away; attach again before writing.', { reason: 'needs_seat', activityUnsent: true });
439
+ }
440
+ export { validateEndpointGeneration };
@@ -0,0 +1,11 @@
1
+ export type MetadataScalar = null | boolean | number | string;
2
+ export interface MetadataMatch {
3
+ readonly fields: readonly Readonly<{
4
+ path: readonly string[];
5
+ equals: MetadataScalar;
6
+ }>[];
7
+ }
8
+ export interface DeliveryDeferral {
9
+ readonly id: string;
10
+ readonly expiresAt: string;
11
+ }
@@ -0,0 +1 @@
1
+ export {};