@astrosheep/square 0.3.70 → 0.3.72

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