@astrosheep/square 0.3.70 → 0.3.71

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 (115) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/help.js +6 -1
  36. package/dist/inbox.d.ts +27 -0
  37. package/dist/inbox.js +55 -18
  38. package/dist/index.d.ts +2 -4
  39. package/dist/index.js +1 -1
  40. package/dist/list.d.ts +7 -1
  41. package/dist/list.js +49 -122
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +7 -37
  44. package/dist/model.d.ts +4 -44
  45. package/dist/model.js +1 -3
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/notifications.js +13 -7
  48. package/dist/open-square.d.ts +16 -2
  49. package/dist/open-square.js +40 -1
  50. package/dist/perception-projection.d.ts +6 -15
  51. package/dist/perception-projection.js +6 -26
  52. package/dist/plugins/opencode/delivery.js +9 -9
  53. package/dist/plugins/opencode/index.js +3 -3
  54. package/dist/ports.d.ts +156 -11
  55. package/dist/presentation-operations.d.ts +1 -1
  56. package/dist/presentation-operations.js +35 -44
  57. package/dist/presentation.d.ts +12 -19
  58. package/dist/presentation.js +33 -54
  59. package/dist/routes.d.ts +7 -14
  60. package/dist/routes.js +33 -62
  61. package/dist/runtime.d.ts +12 -39
  62. package/dist/runtime.js +33 -164
  63. package/dist/square-actions.d.ts +11 -21
  64. package/dist/square-actions.js +171 -271
  65. package/dist/square-application.d.ts +31 -55
  66. package/dist/square-application.js +637 -141
  67. package/dist/square-core.d.ts +3 -94
  68. package/dist/square-core.js +8 -353
  69. package/dist/square-domain.d.ts +24 -0
  70. package/dist/square-domain.js +77 -0
  71. package/dist/square-facade.d.ts +360 -9
  72. package/dist/square-migration.d.ts +24 -0
  73. package/dist/square-migration.js +211 -0
  74. package/dist/square-projections.d.ts +2 -42
  75. package/dist/square-projections.js +1 -37
  76. package/dist/square-sqlite.d.ts +59 -0
  77. package/dist/square-sqlite.js +702 -0
  78. package/dist/stream.d.ts +3 -10
  79. package/dist/stream.js +81 -52
  80. package/dist/views.d.ts +27 -95
  81. package/dist/views.js +217 -87
  82. package/dist/wake-eligibility.d.ts +15 -5
  83. package/dist/wake-eligibility.js +32 -16
  84. package/dist/wakes.d.ts +3 -3
  85. package/dist/wakes.js +31 -27
  86. package/guides/application-facade-delivery-evidence.md +200 -0
  87. package/guides/application-facade-design.md +95 -0
  88. package/guides/change-waits.md +4 -4
  89. package/guides/migration.md +77 -0
  90. package/guides/primitive-catch-consolidation.md +85 -0
  91. package/guides/primitive-history-consolidation.md +76 -0
  92. package/guides/primitive-status-consolidation.md +87 -0
  93. package/guides/primitive-storage-consolidation.md +128 -0
  94. package/guides/row-runtime-delivery-evidence.md +160 -0
  95. package/guides/row-storage-design.md +567 -0
  96. package/package.json +1 -1
  97. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  98. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  99. package/plugins/opencode/delivery.ts +11 -11
  100. package/plugins/opencode/index.ts +3 -3
  101. package/plugins/paseo/client/square-message.tsx +70 -52
  102. package/plugins/paseo/shared/message.ts +15 -0
  103. package/plugins/pi/square-pi.js +3 -0
  104. package/dist/activity.d.ts +0 -14
  105. package/dist/activity.js +0 -248
  106. package/dist/square-file-adapter.d.ts +0 -26
  107. package/dist/square-file-adapter.js +0 -85
  108. package/dist/square-state.d.ts +0 -6
  109. package/dist/square-state.js +0 -138
  110. package/dist/square-storage.d.ts +0 -14
  111. package/dist/square-storage.js +0 -188
  112. package/dist/square-wiring.d.ts +0 -33
  113. package/dist/square-wiring.js +0 -109
  114. package/dist/watch.d.ts +0 -8
  115. package/dist/watch.js +0 -252
@@ -1,185 +1,681 @@
1
1
  import path from 'node:path';
2
+ import crypto from 'node:crypto';
2
3
  import { setTimeout as sleep } from 'node:timers/promises';
3
- import { SquareError } from './model.js';
4
+ import { SquareError, isSquareError, nameKey, validateName } from './model.js';
4
5
  import { validateDoneBody } from './decisions.js';
5
- import { currentHold, HELD_WAIT_BUDGET_MS } from './runtime.js';
6
- import { Square } from './square-wiring.js';
7
- import { openSquare } from './square-file-adapter.js';
8
- import { closeOpenSquare } from './open-square.js';
9
- import { catchUp, express, ignore, listen, listening } from './square-actions.js';
10
- import { participantHistory, resolveParticipant, statusPresentation, participantsPresentation } from './views.js';
11
- import { hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
12
- function requireLocation(context) {
13
- const selected = context.squarePath ?? context.env.SQUARE_LOCATION;
14
- if (selected === undefined || selected.trim() === '')
15
- throw new SquareError('invalid_args', 'Square application context needs a square location');
16
- return path.resolve(context.cwd, selected);
17
- }
18
- async function requireParticipant(context, squarePath, hostLedger, env = context.env) {
19
- const selected = context.participant ?? env.SQUARE_PARTICIPANT_NAME;
20
- if (selected?.trim())
21
- return selected;
22
- const discovered = await localParticipantName(squarePath, hostLedger, env).catch(() => undefined);
23
- if (discovered !== undefined)
24
- return discovered;
25
- throw new SquareError('invalid_args', 'Square application context needs one unambiguous participant name');
26
- }
6
+ import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
7
+ import { catchLimit, observeCatchSelection } from './catch-decisions.js';
8
+ import { directedSuffix } from './activity-feed.js';
9
+ import { assertLiveOwner } from './participant-host.js';
10
+ import { buildMemorySquare, buildSquare, openSquare, closeOpenSquare } from './open-square.js';
11
+ import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, resume, retireEndedSessionRoutes } from './square-actions.js';
12
+ import { recoveryObservation, currentParticipant, entryPresentation, eventPresentation, history, participants, resolveParticipant, snapshot, queryStatus, queryParticipants, catchPresence, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, } from './views.js';
13
+ import { hasAutomaticDeliveryIdentity, hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
14
+ import { projectSessionBindings } from './square-projections.js';
15
+ import { sessionIdsFromEnvironment } from './participant-identity.js';
16
+ import { createDefaultWakeTransport, wakeGraceMs } from './notifications.js';
17
+ import { sweepPending } from './delivery-operations.js';
18
+ import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
19
+ import { createSquare, diagnoseSquareFile } from './artifact.js';
20
+ import { migrateSquareFile } from './square-migration.js';
21
+ import { discoverSquares, DEFAULT_LIST_DEPTH, DEFAULT_LIST_LIMIT } from './list.js';
22
+ import { streamSquare } from './stream.js';
23
+ export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
24
+ export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
25
+ export { HELD_WAIT_BUDGET_MS } from './runtime.js';
26
+ export { HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
27
+ export { DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from './list.js';
27
28
  function checkControl(control) {
28
29
  if (control?.signal?.aborted)
29
- throw new SquareError('invalid_args', 'Square operation was cancelled');
30
+ throw control.signal.reason ?? new Error('Square operation was cancelled');
31
+ }
32
+ function clockFor(env, clock) {
33
+ if (clock !== undefined)
34
+ return clock;
35
+ if (env.SQUARE_NOW_MS === undefined)
36
+ return Date.now;
37
+ return () => {
38
+ const value = Number.parseInt(env.SQUARE_NOW_MS, 10);
39
+ if (!Number.isFinite(value))
40
+ throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
41
+ return value;
42
+ };
43
+ }
44
+ function failure(error, detail) {
45
+ const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
46
+ Object.assign(normalized, { operation: detail });
47
+ return normalized;
48
+ }
49
+ function defaultTransport(square, inMemory = false) {
50
+ let transport;
51
+ return () => transport ??= square.wakeTransport !== undefined ? Promise.resolve(square.wakeTransport)
52
+ : inMemory || square.hostLedger === undefined ? Promise.resolve(undefined)
53
+ : createDefaultWakeTransport(square.hostLedger, square.clock, square.env).catch(() => undefined);
54
+ }
55
+ function observedContext(square, standingCount, held, participant, now = square.clock()) {
56
+ return {
57
+ location: square.location, standingCount, held, now,
58
+ ...(participant === undefined ? {} : { participant }),
59
+ automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
60
+ };
61
+ }
62
+ async function contextProjection(square, participant, control) {
63
+ const observation = (await square.artifact.read((facts) => ({
64
+ standingCount: inSquareCount(facts), held: facts.hold().active,
65
+ }), control?.signal)).value;
66
+ return observedContext(square, observation.standingCount, observation.held, participant);
67
+ }
68
+ async function joinOwned(square, requested, options = {}, control) {
69
+ checkControl(control);
70
+ if (options.takeover && await currentParticipant(square, requested, control) !== undefined) {
71
+ const result = await takeover(square, requested, [], control);
72
+ return {
73
+ participant: result.name, activity: result.activities.at(-1) ?? null,
74
+ kind: 'taken-over', participantCount: result.participantCount,
75
+ };
76
+ }
77
+ const result = await join(square, requested, control);
78
+ return {
79
+ participant: result.name, activity: result.activity,
80
+ kind: result.reconnected ? 'reconnected' : 'joined', participantCount: result.participantCount,
81
+ };
82
+ }
83
+ async function prepareExpression(square, requested, control) {
84
+ try {
85
+ checkControl(control);
86
+ const name = (await resolveParticipant(square, requested, control)).name;
87
+ return await joinOwned(square, name, {}, control);
88
+ }
89
+ catch (error) {
90
+ throw failure(error, { operation: 'express', subject: 'caller', participant: requested, expressEffect: 'unsent' });
91
+ }
92
+ }
93
+ /** The one retry/commit owner, shared by application and SDK without projections. */
94
+ async function landExpression(square, name, body, options, transport, control) {
95
+ let effect = 'unsent';
96
+ let waited = false;
97
+ let heldWaitedMs = 0;
98
+ try {
99
+ const wakeTransport = await transport();
100
+ while (true) {
101
+ checkControl(control);
102
+ try {
103
+ effect = 'unknown';
104
+ const result = await express({ ...square, wakeTransport }, name, body, options, control);
105
+ return { ...result, waited };
106
+ }
107
+ catch (error) {
108
+ if (isSquareError(error) && error.facts?.activityUnsent === true)
109
+ effect = 'unsent';
110
+ if (!isSquareError(error) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
111
+ throw error;
112
+ if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
113
+ throw error;
114
+ waited = true;
115
+ control?.onProgress?.({
116
+ kind: 'waiting', reason: error.code,
117
+ ...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
118
+ ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
119
+ ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
120
+ });
121
+ if (error.code === 'throttled') {
122
+ await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
123
+ }
124
+ else {
125
+ const started = Date.now();
126
+ const observation = await square.artifact.read((facts) => facts.hold().active, control?.signal);
127
+ if (observation.value) {
128
+ await square.artifact.changed(observation.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control?.signal);
129
+ }
130
+ heldWaitedMs += Date.now() - started;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ catch (error) {
136
+ const recipient = isSquareError(error)
137
+ && ['not_standing', 'unmatched_mention', 'bell_mention_conflict'].includes(error.facts?.reason ?? '');
138
+ throw failure(error, {
139
+ operation: 'express', participant: name, subject: recipient ? 'recipient' : 'caller', expressEffect: effect,
140
+ });
141
+ }
142
+ }
143
+ async function expressOwned(square, requested, body, options, transport, control) {
144
+ const entry = await prepareExpression(square, requested, control);
145
+ const name = entry.participant;
146
+ let result;
147
+ try {
148
+ const feed = (await square.artifact.read((facts) => {
149
+ const pendingPublic = directedSuffix(facts, name);
150
+ return {
151
+ pendingPublic,
152
+ sayNumbers: facts.sayOrdinals(pendingPublic.map((item) => item.index)),
153
+ perceptions: Object.fromEntries(pendingPublic.map((item) => [item.index, 'full'])),
154
+ };
155
+ }, control?.signal)).value;
156
+ result = await landExpression(square, name, body, options, transport, control);
157
+ // Minimal count/header facts come from the authoritative commit, not a later
158
+ // read that could erase success or a second feed that would be discarded.
159
+ const fresh = result.observation;
160
+ return {
161
+ data: {
162
+ activity: result.activity, waited: result.waited,
163
+ ...(result.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: result.replyAuthorNotHere }),
164
+ },
165
+ projection: {
166
+ ...fresh, ...feed, participant: name,
167
+ location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
168
+ ...(entry.activity === null ? {} : { reentry: entry.activity }),
169
+ ...(result.delivery === undefined ? {} : { delivery: result.delivery }),
170
+ },
171
+ };
172
+ }
173
+ catch (error) {
174
+ if (result === undefined && error instanceof Error && 'operation' in error)
175
+ throw error;
176
+ throw failure(error, {
177
+ operation: 'express', participant: name, subject: 'caller',
178
+ expressEffect: result === undefined ? 'unsent' : 'landed',
179
+ ...(result === undefined ? {} : { landedId: result.activity.id }),
180
+ });
181
+ }
182
+ }
183
+ /** Directed selection guarantees full visibility; only the API exits expose activities. */
184
+ function caughtResult({ selection, stop }) {
185
+ return {
186
+ activities: selection.activities.map((activity) => ({ ...toPublicActivity(activity), perception: 'full' })),
187
+ consumedThrough: selection.consumedThrough, remaining: selection.remaining, idleExpired: stop === 'stale',
188
+ };
189
+ }
190
+ /** Leases, quiet waiting, selection and sweep form one lifecycle for both APIs. */
191
+ async function catchOwned(square, requested, options = {}, control) {
192
+ let name = requested, subject = 'caller';
193
+ let leaseId;
194
+ let waitStarted = false, replaced = false;
195
+ let selection = options;
196
+ const idle = options.idle ?? 0;
197
+ async function complete(caught, stop) {
198
+ const selected = caught ?? (await square.artifact.read((facts) => observeCatchSelection(facts, name, [], {
199
+ unseenOnly: true, senders: selection.from,
200
+ ...(selection.mention === true ? { attentionOnly: true } : {}),
201
+ }, square.clock()))).value;
202
+ const terminal = stop === 'empty-now' || stop === undefined ? selected.terminalStatus ?? stop : stop;
203
+ if (leaseId !== undefined) {
204
+ await releaseWatchLease(square, name, leaseId).catch(() => undefined);
205
+ leaseId = undefined;
206
+ }
207
+ if (!control?.signal?.aborted && stop !== 'replaced' && stop !== 'already-active'
208
+ && (waitStarted || selected.activities.length > 0) && square.location !== 'memory'
209
+ && square.hostLedger !== undefined && square.env?.SQUARE_DISABLE_PASEO_WAKE !== '1') {
210
+ try {
211
+ await sweepPending({
212
+ artifact: square.artifact, hostLedger: square.hostLedger, location: square.location,
213
+ now: square.clock(), graceMs: wakeGraceMs(square.env), limit: 8,
214
+ });
215
+ }
216
+ catch { /* a completed receipt is not conditional on notification enrichment */ }
217
+ }
218
+ return { selection: selected, stop: terminal, waitStarted, replaced };
219
+ }
220
+ const empty = (stop) => complete(undefined, stop);
221
+ try {
222
+ checkControl(control);
223
+ if (!Number.isFinite(idle) || idle < 0)
224
+ throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
225
+ if (options.id !== undefined
226
+ && ['idle', 'from', 'mention', 'limit', 'replace'].some((key) => Object.hasOwn(options, key))) {
227
+ throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention, limit or replace');
228
+ }
229
+ if (Object.hasOwn(options, 'replace') && idle === 0)
230
+ throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
231
+ catchLimit(options.limit);
232
+ name = (await resolveParticipant(square, requested, control)).name;
233
+ subject = 'filter';
234
+ const from = options.from === undefined ? undefined : await Promise.all(options.from.map(async (target) => (await resolveParticipant(square, target, control)).name));
235
+ selection = { ...options, ...(from === undefined ? {} : { from }) };
236
+ subject = 'caller';
237
+ if (idle === 0) {
238
+ const data = await catchUp(square, name, selection, control);
239
+ return await complete(data, data.activities.length === 0 ? 'empty-now' : undefined);
240
+ }
241
+ if (!await assertLiveOwner(square, name)) {
242
+ throw new SquareError('already_joined', `${name} already stands here — another session holds the name`);
243
+ }
244
+ const watch = {
245
+ ...(from === undefined ? {} : { participants: [...from] }),
246
+ ...(options.mention ? { mention: name } : {}),
247
+ ...(options.replace === undefined ? {} : { replace: options.replace }),
248
+ };
249
+ const start = await acquireWatchLease(square, name, crypto.randomUUID(), watch, control?.signal);
250
+ if (start.type === 'active')
251
+ return await empty('already-active');
252
+ leaseId = start.leaseId;
253
+ waitStarted = true;
254
+ replaced = start.replaced;
255
+ control?.onProgress?.({
256
+ kind: 'catch-started', participant: name, idleMs: idle,
257
+ replacementRequested: options.replace ?? false, replaced,
258
+ });
259
+ let quietSince = square.clock();
260
+ let wasHeld = false;
261
+ let nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
262
+ while (true) {
263
+ checkControl(control);
264
+ // Capture before evaluation: commits during selection cannot be missed.
265
+ const baseline = await square.artifact.revision(control?.signal);
266
+ const pulse = await pulseWatchLease(square, name, leaseId, watch, square.clock() >= nextHeartbeat, control?.signal);
267
+ if (pulse.type === 'replaced')
268
+ return await empty('replaced');
269
+ if (pulse.type === 'terminal')
270
+ return await empty(pulse.status);
271
+ if (pulse.heartbeatAt !== undefined)
272
+ nextHeartbeat = pulse.heartbeatAt + WATCH_HEARTBEAT_MS;
273
+ if (pulse.type === 'sleep') {
274
+ const caught = await catchUp(square, name, selection, control, leaseId);
275
+ if (caught === null)
276
+ return await empty('replaced');
277
+ if (caught.activities.length > 0)
278
+ return await complete(caught);
279
+ }
280
+ if (pulse.type === 'held' || wasHeld)
281
+ quietSince = square.clock();
282
+ wasHeld = pulse.type === 'held';
283
+ if (!wasHeld && square.clock() - quietSince >= idle) {
284
+ const owns = await ownsWatchLease(square, name, leaseId, control?.signal);
285
+ return await empty(owns ? 'stale' : 'replaced');
286
+ }
287
+ const deadline = Math.min(nextHeartbeat, wasHeld ? Infinity : quietSince + idle);
288
+ await square.artifact.changed(baseline, Math.max(0, deadline - square.clock()), control?.signal);
289
+ }
290
+ }
291
+ catch (error) {
292
+ throw failure(error, { operation: 'catch', subject, participant: name });
293
+ }
294
+ finally {
295
+ // Cleanup deliberately does not reuse the aborted request signal, and the ID
296
+ // fence never releases a replacement lease.
297
+ if (leaseId !== undefined)
298
+ await releaseWatchLease(square, name, leaseId).catch(() => undefined);
299
+ }
300
+ }
301
+ class ParticipantHandle {
302
+ name;
303
+ square;
304
+ transport;
305
+ constructor(name, square, transport) {
306
+ this.name = name;
307
+ this.square = square;
308
+ this.transport = transport;
309
+ }
310
+ async express(body, options, control) {
311
+ const entry = await prepareExpression(this.square, this.name, control);
312
+ const { waited: _waited, observation: _observation, ...result } = await landExpression(this.square, entry.participant, body, { ...options, noWait: true }, this.transport, control);
313
+ return result;
314
+ }
315
+ listen(target, control) { return listen(this.square, this.name, target, control); }
316
+ ignore(target, control) { return ignore(this.square, this.name, target, control); }
317
+ listening(control) { return listening(this.square, this.name, control); }
318
+ async catch(options, control) {
319
+ return caughtResult(await catchOwned(this.square, this.name, options, control));
320
+ }
321
+ history(query = {}, control) {
322
+ return history(this.square, query.limit === undefined ? { ...query, limit: HISTORY_DEFAULT_LIMIT } : query, control);
323
+ }
324
+ hold(reason, control) { return hold(this.square, this.name, reason, control); }
325
+ resume(control) { return resume(this.square, this.name, control); }
326
+ done(body, control) { return done(this.square, this.name, body, {}, control); }
327
+ }
328
+ /** Supported SDK resource. It owns one open artifact; application operations use
329
+ * the same owner functions directly, not an application -> SDK -> application chain. */
330
+ export class Square {
331
+ location;
332
+ square;
333
+ transport;
334
+ constructor(location, square) {
335
+ this.location = location;
336
+ this.square = square;
337
+ this.transport = defaultTransport(square, location === 'memory');
338
+ }
339
+ static async at(input, control) {
340
+ const env = { ...(input.env ?? process.env) };
341
+ return new Square(input.path, await openSquare(input.path, { ...input, env, clock: clockFor(env, input.clock) }, control));
342
+ }
343
+ static async build(input) {
344
+ const env = { ...(input.env ?? process.env) };
345
+ return new Square(input.path, await buildSquare(input.path, { ...input, env, clock: clockFor(env, input.clock) }));
346
+ }
347
+ static inMemory(input) { return new Square('memory', buildMemorySquare(input)); }
348
+ handle(name) { return new ParticipantHandle(name, this.square, this.transport); }
349
+ async join(name, control) { return this.handle((await join(this.square, name, control)).name); }
350
+ async takeover(name, control) {
351
+ const result = await takeover(this.square, name, [], control);
352
+ return this.handle(result.name);
353
+ }
354
+ doneOwnedSession(name, body, expectedEpoch, control) { return done(this.square, name, body, { expectedEpoch }, control); }
355
+ async implicitJoin(name, control) {
356
+ const result = await implicitJoin(this.square, name, control);
357
+ return { state: result.state, ...(result.state === 'done' ? {} : { participant: this.handle(result.name) }) };
358
+ }
359
+ participants() { return participants(this.square); }
360
+ snapshot(control) { return snapshot(this.square, control); }
361
+ history(query, control) { return history(this.square, query, control); }
362
+ async recognize(env) {
363
+ if (this.square.hostLedger === undefined)
364
+ return null;
365
+ const candidates = (await Promise.all(sessionIdsFromEnvironment(env).map((sessionId) => projectSessionBindings({ hostLedger: this.square.hostLedger, location: this.location, sessionId })))).flat();
366
+ if (candidates.length !== 1)
367
+ return null;
368
+ const name = await currentParticipant(this.square, candidates[0].participant);
369
+ return name === undefined ? null : this.handle(name);
370
+ }
371
+ close() { return closeOpenSquare(this.square); }
372
+ endOwnedSession(name, sessionId, expectedEpoch) { return endOwnedSession(this.square, name, sessionId, expectedEpoch); }
373
+ retireEndedSessionRoutes(sessionId) { return retireEndedSessionRoutes(this.square, sessionId); }
30
374
  }
31
375
  export function createSquareApplication(context) {
32
376
  const env = { ...context.env };
33
- const location = () => requireLocation({ ...context, env });
377
+ const clock = clockFor(env, context.clock);
34
378
  const hostLedger = context.hostLedger ?? hostLedgerForEnv(env);
35
- const open = (control) => Square.at({ path: location(), clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, control);
36
- async function existing(operation, control) {
379
+ const openOptions = { env, clock, hostLedger, wakeTransport: context.wakeTransport };
380
+ const getTransport = defaultTransport(openOptions);
381
+ function location() {
382
+ const selected = context.squarePath ?? env.SQUARE_LOCATION;
383
+ if (selected === undefined || selected.trim() === '')
384
+ throw new SquareError('invalid_args', 'Square application context needs a square location');
385
+ return path.resolve(context.cwd, selected);
386
+ }
387
+ async function actor() {
388
+ const selected = context.participant ?? env.SQUARE_PARTICIPANT_NAME;
389
+ if (context.participant !== undefined || selected?.trim()) {
390
+ validateName(selected);
391
+ return selected;
392
+ }
393
+ const discovered = await localParticipantName(location(), hostLedger, env);
394
+ if (discovered !== undefined)
395
+ return discovered;
396
+ throw new SquareError('invalid_args', 'Square application context needs one unambiguous participant name');
397
+ }
398
+ async function scoped(operation, control) {
37
399
  checkControl(control);
38
- const squarePath = location();
39
- const participantName = await requireParticipant(context, squarePath, hostLedger, env);
40
- const square = await openSquare(squarePath, { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, control);
400
+ const square = await openSquare(location(), openOptions, control);
41
401
  try {
42
- const { name } = await resolveParticipant(square, participantName, control);
43
- checkControl(control);
44
- return await operation(square, name);
402
+ return await operation(square);
45
403
  }
46
404
  finally {
47
- await closeOpenSquare(square);
405
+ await closeOpenSquare(square).catch(() => undefined);
48
406
  }
49
407
  }
50
- async function joined(operation, control) {
51
- checkControl(control);
52
- const square = await open(control);
408
+ async function selected(operation, run, control) {
409
+ let name = context.participant ?? env.SQUARE_PARTICIPANT_NAME;
53
410
  try {
54
- return await operation(await square.join(await requireParticipant(context, square.location, hostLedger, env), control));
411
+ checkControl(control);
412
+ name = await actor();
413
+ return await scoped((square) => run(square, name), control);
55
414
  }
56
- finally {
57
- await square.close();
415
+ catch (error) {
416
+ const detail = error?.operation;
417
+ if (detail?.operation === operation)
418
+ throw error;
419
+ throw failure(error, {
420
+ operation, participant: detail?.participant ?? name, subject: 'caller',
421
+ ...(operation === 'express' ? { expressEffect: 'unsent' } : {}),
422
+ });
58
423
  }
59
424
  }
425
+ async function listener(verb, target, control) {
426
+ return selected('other', async (square, requested) => {
427
+ const name = (await resolveParticipant(square, requested, control)).name;
428
+ const result = await (verb === 'listen' ? listen : ignore)(square, name, target, control);
429
+ return {
430
+ data: result,
431
+ projection: {
432
+ ...await contextProjection(square, name), participant: name,
433
+ requestedTarget: target, target: result.activity?.target ?? target,
434
+ },
435
+ };
436
+ }, control);
437
+ }
438
+ async function lifecycle(verb, body, control) {
439
+ if (verb === 'done')
440
+ validateDoneBody(body);
441
+ return selected('other', async (square, requested) => {
442
+ const identity = localSessionIdentities(env)[0];
443
+ const owner = verb !== 'done' || identity === undefined ? undefined : await readParticipantOwner(square.location, requested, hostLedger);
444
+ const fenced = identity !== undefined && owner?.session === identity.sessionId && presenceEpoch(owner) > 0;
445
+ const name = fenced ? requested : (await join(square, requested, control)).name;
446
+ let result;
447
+ if (verb === 'done') {
448
+ const fence = fenced ? { expectedEpoch: presenceEpoch(owner) } : {};
449
+ result = await done(square, name, body ?? '', fence, control);
450
+ }
451
+ else if (verb === 'hold') {
452
+ result = await hold(square, name, body, control);
453
+ }
454
+ else {
455
+ result = await resume(square, name, control);
456
+ }
457
+ const view = await eventPresentation(square, result.activity.id);
458
+ return {
459
+ data: { activity: result.activity },
460
+ projection: { ...observedContext(square, view.participantCount, view.held, name), ...view },
461
+ };
462
+ }, control);
463
+ }
60
464
  return {
61
- async join(options = {}, control = {}) {
465
+ now: clock,
466
+ async build(input, control) {
62
467
  checkControl(control);
63
468
  const squarePath = location();
64
- const square = await open(control);
65
- try {
66
- const participantName = await requireParticipant(context, squarePath, hostLedger, env);
67
- const before = await square.snapshot(control);
68
- const standing = before.participants.some((item) => item.name.toLocaleLowerCase() === participantName.toLocaleLowerCase() && item.state === 'joined');
69
- const identity = localSessionIdentities(env)[0];
70
- const owner = standing ? await readParticipantOwner(squarePath, participantName, hostLedger).catch(() => undefined) : undefined;
71
- const reconnect = standing && identity !== undefined && owner?.session === identity.sessionId;
72
- if (standing && owner !== undefined && identity !== undefined && !options.takeover && !reconnect) {
73
- throw new SquareError('already_joined', `✕ ${participantName} already stands here — another session holds the name`, { pending: before.participants.length });
74
- }
75
- if (options.takeover && standing) {
76
- const replaced = await square.takeoverWithActivity(participantName, control);
77
- const activity = replaced.activities.at(-1) ?? null;
78
- return { participant: replaced.participant.name, activity, kind: 'taken-over', participantCount: (await square.snapshot()).participants.length };
79
- }
80
- const joined = await square.joinWithActivity(participantName, control);
81
- return { participant: joined.participant.name, activity: joined.activity, kind: reconnect ? 'reconnected' : 'joined', participantCount: (await square.snapshot()).participants.length };
82
- }
83
- finally {
84
- await square.close();
85
- }
469
+ const now = clock();
470
+ const configuration = await createSquare(squarePath, {
471
+ hardCap: input.hardCap, throttlePerMinute: input.throttlePerMinute,
472
+ template: input.template, force: input.force ?? false,
473
+ }, input.markdown, control?.signal);
474
+ return {
475
+ data: { location: squarePath, ...configuration },
476
+ projection: { location: squarePath, standingCount: 0, held: false, now,
477
+ automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(env) },
478
+ };
86
479
  },
87
- async express(body, options = {}, control = {}) {
88
- let waited = false;
89
- let heldWaitedMs = 0;
90
- while (true) {
91
- checkControl(control);
480
+ join(options = {}, control) {
481
+ return selected('join', async (square, name) => {
482
+ const last = options.last ?? 10;
483
+ if (!Number.isSafeInteger(last) || last < 1 || last > 100) {
484
+ throw new SquareError('invalid_args', 'Join history limit must be between 1 and 100');
485
+ }
486
+ const data = await joinOwned(square, name, options, control);
92
487
  try {
93
- const result = await existing((square, name) => express(square, name, body, options, control), control);
94
- return { ...result, waited };
488
+ const view = await entryPresentation(square, data.participant, last);
489
+ return {
490
+ data,
491
+ projection: { ...observedContext(square, view.participantCount, view.held, data.participant), ...view },
492
+ };
95
493
  }
96
494
  catch (error) {
97
- if (!(error instanceof SquareError) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
98
- throw error;
99
- // A held wait is a convenience, not a trap: past the budget the error
100
- // surfaces exactly like --no-wait (the CLI saves a draft and prints a retry).
101
- if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
102
- throw error;
103
- waited = true;
104
- control?.onProgress?.({
105
- kind: 'waiting',
106
- reason: error.code,
107
- ...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
108
- ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
109
- ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
110
- });
111
- if (error.code === 'throttled') {
112
- await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control.signal });
495
+ throw failure(error, { operation: 'join', participant: data.participant, subject: 'caller' });
496
+ }
497
+ }, control);
498
+ },
499
+ express(body, options = {}, control) {
500
+ return selected('express', (square, name) => expressOwned(square, name, body, options, getTransport, control), control);
501
+ },
502
+ catch(options, control) {
503
+ return selected('catch', async (square, name) => {
504
+ const result = await catchOwned(square, name, options, control);
505
+ const presence = await catchPresence(square, control).catch(() => undefined);
506
+ const view = result.selection;
507
+ const stop = result.stop;
508
+ const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum'
509
+ ? stop : undefined;
510
+ return {
511
+ data: { ...caughtResult(result), ...(status === undefined ? {} : { status }) },
512
+ projection: {
513
+ ...observedContext(square, view.standingCount, view.held, view.participant, view.now),
514
+ participant: view.participant, sayNumbers: view.sayNumbers,
515
+ ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
516
+ ...(presence === undefined ? { enrichmentUnavailable: true } : { presence }),
517
+ waitStarted: result.waitStarted, replaced: result.replaced,
518
+ },
519
+ };
520
+ }, control);
521
+ },
522
+ history(query = {}, control) {
523
+ return scoped(async (square) => {
524
+ if (query.limit !== undefined && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT)) {
525
+ throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
526
+ }
527
+ if (query.grep !== undefined && query.fixed !== undefined)
528
+ throw new SquareError('invalid_args', 'History cannot combine regular-expression and literal search');
529
+ if (query.grep === '' || query.fixed === '')
530
+ throw new SquareError('invalid_args', 'History search requires non-empty text');
531
+ if (query.since !== undefined && !Number.isFinite(query.since))
532
+ throw new SquareError('invalid_args', 'History timestamp must be finite');
533
+ for (const count of [query.contextBefore, query.contextAfter]) {
534
+ if (count !== undefined && (!Number.isSafeInteger(count) || count < 0))
535
+ throw new SquareError('invalid_args', 'History context must be a non-negative safe integer');
536
+ }
537
+ const limit = query.limit ?? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT);
538
+ const kinds = query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined || query.since !== undefined ? undefined : NON_READ;
539
+ return (await square.artifact.read((facts) => {
540
+ const selected = selectHistory(facts, query, limit, kinds);
541
+ if (limit === null && selected.totalMatches > HISTORY_MAX_LIMIT) {
542
+ throw failure(new SquareError('invalid_args', `History context exceeds ${HISTORY_MAX_LIMIT} activities`), {
543
+ operation: 'history', historyBounds: { maxLimit: HISTORY_MAX_LIMIT, totalMatches: selected.totalMatches },
544
+ });
113
545
  }
114
- else {
115
- const waitStarted = Date.now();
116
- const square = await openSquare(location(), { clock: context.clock, env, hostLedger }, control);
117
- try {
118
- const snapshot = await square.artifact.read(control.signal);
119
- if (currentHold(snapshot.state.acts).active)
120
- await square.artifact.changed(snapshot.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control.signal);
121
- }
122
- finally {
123
- await closeOpenSquare(square);
124
- }
125
- heldWaitedMs += Date.now() - waitStarted;
546
+ const indexes = selected.activities.map((activity) => activity.index);
547
+ const presenceAnchors = {};
548
+ for (const participant of facts.roster()) {
549
+ const anchor = presenceAnchor(facts, participant.name);
550
+ if (indexes.includes(anchor))
551
+ (presenceAnchors[anchor] ??= []).push(participant.name);
126
552
  }
127
- }
128
- }
553
+ return {
554
+ data: { activities: publicActivities(selected.activities) },
555
+ projection: {
556
+ ...observedContext(square, inSquareCount(facts), facts.hold().active), ...selected,
557
+ sayNumbers: facts.sayOrdinals(indexes), presenceAnchors,
558
+ },
559
+ };
560
+ }, control?.signal)).value;
561
+ }, control);
562
+ },
563
+ listen: (target, control) => listener('listen', target, control),
564
+ ignore: (target, control) => listener('ignore', target, control),
565
+ listening(control) {
566
+ return selected('other', async (square, requested) => {
567
+ const name = (await resolveParticipant(square, requested, control)).name;
568
+ const targets = await listening(square, name, control);
569
+ return { data: { listening: targets }, projection: await contextProjection(square, name, control) };
570
+ }, control);
129
571
  },
130
- catch(options, control) { return existing((square, name) => catchUp(square, name, options, undefined, control), control); },
131
- async history(query, control) {
572
+ hold: (reason, control) => lifecycle('hold', reason, control),
573
+ resume: (control) => lifecycle('resume', undefined, control),
574
+ done: (body, control) => lifecycle('done', body, control),
575
+ async status(viewer, control) {
132
576
  checkControl(control);
133
- if (context.participant !== undefined || env.SQUARE_PARTICIPANT_NAME !== undefined)
134
- return existing((square, name) => participantHistory(square, name, query, control), control);
135
- const square = await open(control);
136
- try {
137
- checkControl(control);
138
- return await square.history(query, control);
139
- }
140
- finally {
141
- await square.close();
142
- }
577
+ const selectedViewer = viewer ?? await actor().catch(() => '');
578
+ return scoped(async (square) => (await square.artifact.read((facts) => queryStatus(facts, {
579
+ location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
580
+ }, selectedViewer), control?.signal)).value, control);
143
581
  },
144
- listen(target, control) { return existing((square, name) => listen(square, name, target, control), control); },
145
- ignore(target, control) { return existing((square, name) => ignore(square, name, target, control), control); },
146
- listening(control) { return existing((square, name) => listening(square, name, control), control); },
147
- hold(reason, control) { return joined((participant) => participant.hold(reason, control), control); },
148
- resume(control) { return joined((participant) => participant.resume(control), control); },
149
- async done(body, control) {
150
- validateDoneBody(body);
582
+ participants(control) {
583
+ return scoped(async (square) => (await square.artifact.read((facts) => queryParticipants(facts, {
584
+ location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
585
+ }), control?.signal)).value, control);
586
+ },
587
+ async diagnose(control) {
588
+ checkControl(control);
151
589
  const squarePath = location();
152
- const participantName = await requireParticipant(context, squarePath, hostLedger, env);
153
- const identity = localSessionIdentities(env)[0];
154
- const owner = identity === undefined ? undefined : await readParticipantOwner(squarePath, participantName, hostLedger).catch(() => undefined);
155
- if (identity !== undefined && owner?.session === identity.sessionId && presenceEpoch(owner) > 0) {
156
- const square = await open(control);
590
+ const data = await diagnoseSquareFile(squarePath);
591
+ return {
592
+ data,
593
+ projection: { location: squarePath, now: clock(),
594
+ ...(data.standingCount === undefined ? {} : { standingCount: data.standingCount }) },
595
+ };
596
+ },
597
+ async migrate(control) {
598
+ checkControl(control);
599
+ const squarePath = location();
600
+ const data = await migrateSquareFile(squarePath, control?.signal);
601
+ return { data, projection: { location: squarePath, now: clock(), standingCount: data.standing } };
602
+ },
603
+ async discover(options, control) {
604
+ checkControl(control);
605
+ const data = await discoverSquares(context.cwd, options, control, openOptions);
606
+ return {
607
+ data,
608
+ projection: { cwd: context.cwd, now: clock(),
609
+ depth: options.depth ?? DEFAULT_LIST_DEPTH, limit: options.limit ?? DEFAULT_LIST_LIMIT },
610
+ };
611
+ },
612
+ async inbox(sessionId, options = {}, control) {
613
+ checkControl(control);
614
+ const limit = options.limit ?? 20;
615
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
616
+ throw new SquareError('invalid_args', 'Inbox limit must be between 1 and 100');
617
+ const rows = [];
618
+ for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
619
+ checkControl(control);
620
+ let square;
157
621
  try {
158
- return await square.doneOwnedSession(participantName, body ?? '', presenceEpoch(owner), control);
622
+ square = await openSquare(binding.location, openOptions, control);
623
+ const row = (await square.artifact.read((facts) => {
624
+ const participant = facts.participant(binding.participant);
625
+ if (!participant?.joined)
626
+ return undefined;
627
+ return {
628
+ name: participant.name, squarePath: binding.location,
629
+ pending: facts.directedCount(participant.name, { unseenOnly: true }),
630
+ };
631
+ }, control?.signal)).value;
632
+ if (row !== undefined)
633
+ rows.push(row);
634
+ }
635
+ catch (error) {
636
+ if (control?.signal?.aborted)
637
+ throw error;
159
638
  }
160
639
  finally {
161
- await square.close();
640
+ if (square !== undefined)
641
+ await closeOpenSquare(square);
162
642
  }
163
643
  }
164
- return joined((participant) => participant.done(body, control), control);
644
+ rows.sort((a, b) => {
645
+ const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
646
+ return compare(a.squarePath, b.squarePath) || compare(nameKey(a.name), nameKey(b.name)) || compare(a.name, b.name);
647
+ });
648
+ return { data: { rows: rows.slice(0, limit), total: rows.length }, projection: { sessionId, now: clock(), limit } };
165
649
  },
166
- async status() {
167
- const square = await openSquare(location(), { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
650
+ async recovery(control) {
651
+ let projection = { now: clock() };
168
652
  try {
169
- return await statusPresentation(square);
170
- }
171
- finally {
172
- await closeOpenSquare(square);
653
+ projection = { ...projection, location: location() };
654
+ const name = await actor().catch(() => undefined);
655
+ if (name !== undefined)
656
+ projection = { ...projection, participant: name };
657
+ await scoped(async (square) => {
658
+ if (name === undefined) {
659
+ projection = { ...projection, ...await contextProjection(square, undefined, control) };
660
+ }
661
+ else {
662
+ const facts = await recoveryObservation(square, name, control).catch(() => undefined);
663
+ projection = facts === undefined
664
+ ? { ...projection, ...await contextProjection(square, name, control) }
665
+ : {
666
+ ...projection, ...observedContext(square, facts.participantCount, facts.held, facts.name),
667
+ holdReason: facts.holdReason, ownActivityCount: facts.ownActivityCount,
668
+ hardCap: facts.hardCap, roster: facts.roster, activitySummaries: facts.activitySummaries,
669
+ };
670
+ }
671
+ }, control);
173
672
  }
673
+ catch { /* unavailable recovery observations never replace the original error */ }
674
+ return { data: projection, projection };
174
675
  },
175
- async participants() {
176
- const square = await openSquare(location(), { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
177
- try {
178
- return await participantsPresentation(square);
179
- }
180
- finally {
181
- await closeOpenSquare(square);
182
- }
676
+ stream(options, control) {
677
+ checkControl(control);
678
+ return streamSquare(location(), options, control, openOptions);
183
679
  },
184
680
  };
185
681
  }