@astrosheep/square 0.3.86 → 0.3.87

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 (141) hide show
  1. package/dist/artifact.d.ts +6 -1
  2. package/dist/artifact.js +79 -10
  3. package/dist/boundary-presentation.d.ts +5 -4
  4. package/dist/boundary-presentation.js +21 -25
  5. package/dist/catch-decisions.js +2 -1
  6. package/dist/claude-delivery-status.d.ts +8 -2
  7. package/dist/claude-delivery-status.js +26 -10
  8. package/dist/claude-delivery.d.ts +8 -4
  9. package/dist/claude-delivery.js +114 -104
  10. package/dist/claude-mcp-context.js +2 -2
  11. package/dist/claude-mod.js +119 -105
  12. package/dist/cli/context.d.ts +3 -1
  13. package/dist/cli/context.js +25 -1
  14. package/dist/cli/express-presentation.d.ts +3 -1
  15. package/dist/cli/express-presentation.js +14 -10
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.js +1 -0
  18. package/dist/cli/registry.js +0 -2
  19. package/dist/cli/square-commands.d.ts +4 -2
  20. package/dist/cli/square-commands.js +21 -6
  21. package/dist/codex-hook.d.ts +3 -3
  22. package/dist/codex-hook.js +4 -4
  23. package/dist/codex-queue.d.ts +5 -6
  24. package/dist/codex-queue.js +9 -5
  25. package/dist/decisions.d.ts +3 -2
  26. package/dist/decisions.js +18 -6
  27. package/dist/delivery-health.d.ts +32 -2
  28. package/dist/delivery-health.js +108 -45
  29. package/dist/delivery-operations.d.ts +1 -2
  30. package/dist/delivery-operations.js +155 -196
  31. package/dist/delivery.d.ts +14 -23
  32. package/dist/delivery.js +48 -18
  33. package/dist/file-lock.d.ts +8 -1
  34. package/dist/file-lock.js +2 -0
  35. package/dist/help.js +8 -11
  36. package/dist/inbox.d.ts +35 -9
  37. package/dist/inbox.js +228 -144
  38. package/dist/index.d.ts +3 -4
  39. package/dist/index.js +1 -3
  40. package/dist/list.js +9 -4
  41. package/dist/mcp.js +12 -1
  42. package/dist/model.d.ts +39 -5
  43. package/dist/model.js +84 -0
  44. package/dist/notifications.d.ts +8 -6
  45. package/dist/notifications.js +56 -143
  46. package/dist/operation-presentation.js +3 -4
  47. package/dist/participant-identity.d.ts +0 -3
  48. package/dist/participant-identity.js +0 -31
  49. package/dist/paseo-delivery.d.ts +5 -6
  50. package/dist/paseo-delivery.js +13 -8
  51. package/dist/paseo.d.ts +1 -1
  52. package/dist/paseo.js +1 -1
  53. package/dist/plugins/opencode/delivery.d.ts +8 -3
  54. package/dist/plugins/opencode/delivery.js +187 -132
  55. package/dist/plugins/opencode/index.js +12 -38
  56. package/dist/ports.d.ts +112 -40
  57. package/dist/presentation-operations.d.ts +1 -1
  58. package/dist/presentation-operations.js +51 -35
  59. package/dist/presentation.js +8 -3
  60. package/dist/seats.d.ts +7 -0
  61. package/dist/seats.js +27 -0
  62. package/dist/session-lifecycle.d.ts +2 -2
  63. package/dist/session-lifecycle.js +10 -12
  64. package/dist/sqlite-square.d.ts +20 -9
  65. package/dist/sqlite-square.js +471 -339
  66. package/dist/square-api.d.ts +1 -0
  67. package/dist/square-api.js +3 -4
  68. package/dist/square-core.d.ts +3 -2
  69. package/dist/square-domain.d.ts +12 -5
  70. package/dist/square-domain.js +163 -7
  71. package/dist/square-facade.d.ts +14 -10
  72. package/dist/square-index.d.ts +24 -0
  73. package/dist/square-index.js +131 -0
  74. package/dist/square-legacy-framing.d.ts +9 -0
  75. package/dist/square-legacy-framing.js +56 -0
  76. package/dist/square-migration-host-ledger.d.ts +26 -0
  77. package/dist/square-migration-host-ledger.js +168 -0
  78. package/dist/square-migration.d.ts +5 -5
  79. package/dist/square-migration.js +373 -71
  80. package/dist/square-sqlite.d.ts +9 -9
  81. package/dist/square-sqlite.js +329 -79
  82. package/dist/square-stdio.d.ts +1 -1
  83. package/dist/square-stdio.js +12 -7
  84. package/dist/square-tools.js +29 -2
  85. package/dist/stdio-server.js +4 -2
  86. package/dist/stream.d.ts +3 -2
  87. package/dist/stream.js +9 -10
  88. package/dist/views.d.ts +9 -14
  89. package/dist/views.js +50 -70
  90. package/dist/wakes.d.ts +6 -5
  91. package/dist/wakes.js +20 -17
  92. package/guides/application-facade-delivery-evidence.md +1 -1
  93. package/guides/claude-native.md +9 -9
  94. package/guides/mcp.md +1 -1
  95. package/guides/migration.md +107 -67
  96. package/guides/row-storage-design.md +1 -1
  97. package/guides/square-api.md +61 -28
  98. package/package.json +1 -1
  99. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  100. package/plugins/claude/hooks/register.js +1 -1
  101. package/plugins/claude/skills/square/SKILL.md +1 -1
  102. package/plugins/claude/types.d.ts +1 -1
  103. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  104. package/plugins/opencode/delivery.ts +170 -118
  105. package/plugins/opencode/index.ts +12 -30
  106. package/plugins/pi/square-pi.js +55 -17
  107. package/skills/square/SKILL.md +1 -1
  108. package/dist/cli/ledger-command.d.ts +0 -12
  109. package/dist/cli/ledger-command.js +0 -72
  110. package/dist/host-ledger-file-adapter.d.ts +0 -47
  111. package/dist/host-ledger-file-adapter.js +0 -384
  112. package/dist/host-ledger-migration.d.ts +0 -27
  113. package/dist/host-ledger-migration.js +0 -264
  114. package/dist/host-ledger-root.d.ts +0 -2
  115. package/dist/host-ledger-root.js +0 -9
  116. package/dist/host-ledger-sqlite.d.ts +0 -65
  117. package/dist/host-ledger-sqlite.js +0 -368
  118. package/dist/host-ledger.d.ts +0 -195
  119. package/dist/host-ledger.js +0 -1
  120. package/dist/ledger-maintenance.d.ts +0 -30
  121. package/dist/ledger-maintenance.js +0 -110
  122. package/dist/open-square.d.ts +0 -38
  123. package/dist/open-square.js +0 -81
  124. package/dist/participant-host.d.ts +0 -23
  125. package/dist/participant-host.js +0 -92
  126. package/dist/registry.d.ts +0 -55
  127. package/dist/registry.js +0 -123
  128. package/dist/routes.d.ts +0 -39
  129. package/dist/routes.js +0 -99
  130. package/dist/square-actions.d.ts +0 -56
  131. package/dist/square-actions.js +0 -353
  132. package/dist/square-projections.d.ts +0 -31
  133. package/dist/square-projections.js +0 -64
  134. package/dist/wake-attempts.d.ts +0 -10
  135. package/dist/wake-attempts.js +0 -39
  136. package/dist/wake-eligibility.d.ts +0 -53
  137. package/dist/wake-eligibility.js +0 -123
  138. package/dist/wake-evidence.d.ts +0 -33
  139. package/dist/wake-evidence.js +0 -32
  140. package/guides/host-ledger.md +0 -58
  141. package/guides/indexed-host-ledger-performance.md +0 -39
@@ -1,26 +1,26 @@
1
+ import { formatActivityId } from './square-core.js';
1
2
  import { squareLocation, localSquareLocation } from './square-location.js';
2
3
  import crypto from 'node:crypto';
3
4
  import { setTimeout as sleep } from 'node:timers/promises';
4
- import { SquareError, isSquareError, nameKey, validateName } from './model.js';
5
- import { resolveKnownName, validateDoneBody } from './decisions.js';
5
+ import { InternalSquareError, SquareError, isSquareError, nameKey, sameBody, validateName } from './model.js';
6
+ import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, coreUnlisten, decideAct, decideImplicitJoin, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
6
7
  import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
7
- import { catchLimit, observeCatchSelection } from './catch-decisions.js';
8
+ import { catchLimit, decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
8
9
  import { directedSuffix } from './activity-feed.js';
9
10
  import { expressHint } from './hints.js';
10
- import { assertLiveOwner } from './participant-host.js';
11
- import { buildMemorySquare, openSquare, closeOpenSquare } from './open-square.js';
12
- import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, unlisten, resume, retireEndedSessionRoutes } from './square-actions.js';
13
- import { recoveryObservation, currentParticipant, entryPresentation, resolveParticipant, snapshot, queryStatus, queryParticipants, catchPresence, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
14
- import { hasAutomaticDeliveryIdentity, hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
15
- import { projectSessionBindings } from './square-projections.js';
16
- import { sessionIdsFromEnvironment } from './participant-identity.js';
11
+ import { asSeatRef, participantAuthority, requireBody, requireJoinName, requireSeat } from './seats.js';
12
+ import { recoveryObservation, entryPresentation, snapshot, queryStatus, queryParticipants, catchPresence, parseRequiredActivityId, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
13
+ import { sessionInboxCounts } from './inbox.js';
14
+ import { rememberSquarePath, squareIndexPath } from './square-index.js';
17
15
  import { createDefaultWakeTransport } from './notifications.js';
16
+ import { deliverPending } from './delivery-operations.js';
17
+ import { participantIdentity } from './participant-identity.js';
18
18
  import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
19
- import { createSquare, diagnoseSquareFile } from './artifact.js';
19
+ import { createArtifactPort, createSquare, diagnoseSquareFile, prepareSquareBuild } from './artifact.js';
20
20
  import { migrateSquareFile } from './square-migration.js';
21
21
  import { discoverSquares } from './list.js';
22
22
  import { streamSquare } from './stream.js';
23
- export { createSquareResourceOwner } from './open-square.js';
23
+ export { createSquareResourceOwner } from './artifact.js';
24
24
  export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
25
25
  export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
26
26
  export { HELD_WAIT_BUDGET_MS } from './runtime.js';
@@ -35,337 +35,469 @@ export function squareClock(env, clock) {
35
35
  return clock;
36
36
  if (env.SQUARE_NOW_MS === undefined)
37
37
  return Date.now;
38
- return () => {
39
- const value = Number.parseInt(env.SQUARE_NOW_MS, 10);
40
- if (!Number.isFinite(value))
41
- throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
42
- return value;
43
- };
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;
44
42
  }
45
43
  function failure(error, detail) {
46
44
  const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
47
45
  Object.assign(normalized, { operation: detail });
48
46
  return normalized;
49
47
  }
50
- function defaultTransport(square) {
51
- let transport;
52
- return () => transport ??= square.wakeTransport !== undefined ? Promise.resolve(square.wakeTransport)
53
- : square.automaticDelivery === false || square.location === 'memory' || square.hostLedger === undefined ? Promise.resolve(undefined)
54
- : createDefaultWakeTransport(square.hostLedger, square.clock, square.env).catch(() => undefined);
55
- }
56
- function observedContext(square, standingCount, held, participant, now = square.clock()) {
57
- return { location: square.location, standingCount, held, now,
58
- ...(participant === undefined ? {} : { participant }), automaticDeliveryAvailable: square.automaticDelivery !== false && hasAutomaticDeliveryIdentity(square.env) };
59
- }
60
- async function contextObservation(square, participant, control) {
61
- const observation = (await square.artifact.read((facts) => ({ standingCount: inSquareCount(facts), held: facts.hold().active }), control?.signal)).value;
62
- return observedContext(square, observation.standingCount, observation.held, participant);
63
- }
64
- async function prepareExpression(square, requested, control) {
65
- try {
66
- checkControl(control);
67
- const result = await join(square, requested, control, { requireKnown: true });
68
- return { participant: result.name, activity: result.activity };
69
- }
70
- catch (error) {
71
- throw failure(error, { operation: 'express', subject: 'caller', participant: requested, expressEffect: 'unsent' });
72
- }
73
- }
74
- /** One retry/commit owner for every participant, regardless of transport. */
75
- async function landExpression(square, name, body, options, transport, control) {
76
- let effect = 'unsent';
77
- let waited = false, heldWaitedMs = 0;
78
- try {
79
- const wakeTransport = await transport();
80
- while (true) {
81
- checkControl(control);
82
- try {
83
- effect = 'unknown';
84
- return { ...await express({ ...square, wakeTransport }, name, body, options, control), waited };
85
- }
86
- catch (error) {
87
- if (isSquareError(error) && error.facts?.activityUnsent === true)
88
- effect = 'unsent';
89
- if (!isSquareError(error) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
90
- throw error;
91
- if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
92
- throw error;
93
- waited = true;
94
- control?.onProgress?.({ kind: 'waiting', reason: error.code,
95
- ...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
96
- ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
97
- ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
98
- });
99
- if (error.code === 'throttled')
100
- await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
101
- else {
102
- const started = Date.now();
103
- const observation = await square.artifact.read((facts) => facts.hold().active, control?.signal);
104
- if (observation.value)
105
- await square.artifact.changed(observation.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control?.signal);
106
- heldWaitedMs += Date.now() - started;
107
- }
108
- }
109
- }
110
- }
111
- catch (error) {
112
- const recipient = isSquareError(error) && ['not_standing', 'bell_mention_conflict'].includes(error.facts?.reason ?? '');
113
- throw failure(error, { operation: 'express', participant: name, subject: recipient ? 'recipient' : 'caller', expressEffect: effect });
114
- }
115
- }
116
- /** Leases, quiet waiting and receipt selection share one lifecycle. */
117
- async function catchOwned(square, requested, options = {}, control) {
118
- let name = requested, subject = 'caller';
119
- let leaseId;
120
- let waitStarted = false, replaced = false;
121
- let selection = options;
122
- const idle = options.idle ?? 0;
123
- async function complete(caught, stop) {
124
- const selected = caught ?? (await square.artifact.read((facts) => observeCatchSelection(facts, name, [], {
125
- unseenOnly: true, senders: selection.from, ...(selection.mention === true ? { attentionOnly: true } : {}),
126
- }, square.clock()))).value;
127
- const terminal = stop === 'empty-now' || stop === undefined ? selected.terminalStatus ?? stop : stop;
128
- if (leaseId !== undefined) {
129
- await releaseWatchLease(square, name, leaseId).catch(() => undefined);
130
- leaseId = undefined;
131
- }
132
- return { selection: selected, stop: terminal, waitStarted, replaced };
133
- }
134
- const empty = (stop) => complete(undefined, stop);
48
+ function rejectUnsentActivity(operation) {
135
49
  try {
136
- checkControl(control);
137
- if (!Number.isFinite(idle) || idle < 0)
138
- throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
139
- if (options.id !== undefined && ['idle', 'from', 'mention', 'limit', 'replace'].some((key) => Object.hasOwn(options, key)))
140
- throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention, limit or replace');
141
- if (Object.hasOwn(options, 'replace') && idle === 0)
142
- throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
143
- catchLimit(options.limit);
144
- const from = (await square.artifact.read((facts) => {
145
- name = resolveKnownName(facts, requested);
146
- subject = 'filter';
147
- return options.from?.map((target) => resolveKnownName(facts, target));
148
- }, control?.signal)).value;
149
- selection = { ...options, ...(from === undefined ? {} : { from }) };
150
- subject = 'caller';
151
- if (idle === 0) {
152
- const data = await catchUp(square, name, selection, control);
153
- return await complete(data, data.activities.length === 0 ? 'empty-now' : undefined);
154
- }
155
- if (!await assertLiveOwner(square, name))
156
- throw new SquareError('already_joined', `${name} already stands here — another session holds the name`);
157
- const watch = {
158
- ...(from === undefined ? {} : { participants: [...from] }), ...(options.mention ? { mention: name } : {}),
159
- ...(options.replace === undefined ? {} : { replace: options.replace }),
160
- };
161
- const start = await acquireWatchLease(square, name, crypto.randomUUID(), watch, control?.signal);
162
- if (start.type === 'active')
163
- return await empty('already-active');
164
- leaseId = start.leaseId;
165
- waitStarted = true;
166
- replaced = start.replaced;
167
- control?.onProgress?.({ kind: 'catch-started', participant: name, idleMs: idle, replacementRequested: options.replace ?? false, replaced });
168
- let quietSince = square.clock(), wasHeld = false, nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
169
- while (true) {
170
- checkControl(control);
171
- // Capture before evaluation so a commit during selection cannot be missed.
172
- const baseline = await square.artifact.revision(control?.signal);
173
- const pulse = await pulseWatchLease(square, name, leaseId, watch, square.clock() >= nextHeartbeat, control?.signal);
174
- if (pulse.type === 'replaced')
175
- return await empty('replaced');
176
- if (pulse.type === 'terminal')
177
- return await empty(pulse.status);
178
- if (pulse.heartbeatAt !== undefined)
179
- nextHeartbeat = pulse.heartbeatAt + WATCH_HEARTBEAT_MS;
180
- if (pulse.type === 'sleep') {
181
- const caught = await catchUp(square, name, selection, control, leaseId);
182
- if (caught === null)
183
- return await empty('replaced');
184
- if (caught.activities.length > 0)
185
- return await complete(caught);
186
- }
187
- if (pulse.type === 'held' || wasHeld)
188
- quietSince = square.clock();
189
- wasHeld = pulse.type === 'held';
190
- if (!wasHeld && square.clock() - quietSince >= idle) {
191
- const owns = await ownsWatchLease(square, name, leaseId, control?.signal);
192
- return await empty(owns ? 'stale' : 'replaced');
193
- }
194
- const deadline = Math.min(nextHeartbeat, wasHeld ? Infinity : quietSince + idle);
195
- await square.artifact.changed(baseline, Math.max(0, deadline - square.clock()), control?.signal);
196
- }
50
+ return operation();
197
51
  }
198
52
  catch (error) {
199
- throw failure(error, { operation: 'catch', subject, participant: name });
200
- }
201
- finally {
202
- // An aborted request still releases its own lease, never its replacement's.
203
- if (leaseId !== undefined)
204
- await releaseWatchLease(square, name, leaseId).catch(() => undefined);
53
+ if (isSquareError(error))
54
+ error.facts = { ...error.facts, activityUnsent: true };
55
+ throw error;
205
56
  }
206
57
  }
207
- class ParticipantHandle {
208
- name;
209
- owner;
210
- square;
211
- transport;
212
- constructor(name, owner, square, transport) {
213
- this.name = name;
214
- this.owner = owner;
215
- this.square = square;
216
- this.transport = transport;
217
- }
218
- async express(body, options = {}, control) {
219
- const entry = await prepareExpression(this.square, this.name, control);
220
- const name = entry.participant;
221
- let result;
222
- try {
223
- const feed = !options.observe ? undefined : (await this.square.artifact.read((facts) => {
224
- const pendingPublic = directedSuffix(facts, name);
225
- return { pendingPublic, sayNumbers: facts.sayOrdinals(pendingPublic.map((item) => item.index)),
226
- perceptions: Object.fromEntries(pendingPublic.map((item) => [item.index, 'full'])) };
227
- }, control?.signal)).value;
228
- result = await landExpression(this.square, name, body, options, this.transport, control);
229
- const { observation: committed, ...data } = result;
230
- const hint = expressHint(committed.ownActivityCount);
231
- return { ...data, ...(hint === undefined ? {} : { hint }), ...(feed === undefined ? {} : { observation: {
232
- ...observedContext(this.square, committed.standingCount, committed.held, name, committed.now), ...committed, ...feed, participant: name,
233
- ...(entry.activity === null ? {} : { reentry: entry.activity }),
234
- } }) };
235
- }
236
- catch (error) {
237
- if (result === undefined && error?.operation?.operation === 'express')
238
- throw error;
239
- throw failure(error, { operation: 'express', participant: name, subject: 'caller', expressEffect: result === undefined ? 'unsent' : 'landed',
240
- ...(result === undefined ? {} : { landedId: result.activity.id }) });
241
- }
242
- }
243
- async listener(verb, target, options, control) {
244
- const name = (await resolveParticipant(this.square, this.name, control)).name;
245
- const { observation: committed, ...result } = await (verb === 'listen' ? listen : verb === 'ignore' ? ignore : unlisten)(this.square, name, target, control);
246
- return { ...result, ...(!options.observe ? {} : { observation: {
247
- ...observedContext(this.square, committed.standingCount, committed.held, name, committed.now),
248
- participant: name, requestedTarget: target, target: result.activity?.target ?? target,
249
- } }) };
250
- }
251
- listen(target, options = {}, control) { return this.listener('listen', target, options, control); }
252
- ignore(target, options = {}, control) { return this.listener('ignore', target, options, control); }
253
- unlisten(target, options = {}, control) { return this.listener('unlisten', target, options, control); }
254
- async listening(options = {}, control) {
255
- const name = (await resolveParticipant(this.square, this.name, control)).name;
256
- return { listening: await listening(this.square, name, control),
257
- ...(!options.observe ? {} : { observation: await contextObservation(this.square, name, control) }) };
258
- }
259
- async catch(options = {}, control) {
260
- const result = await catchOwned(this.square, this.name, options, control), view = result.selection, stop = result.stop;
261
- const presence = !options.observe ? undefined : await catchPresence(this.square, control).catch(() => undefined);
262
- const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum' ? stop : undefined;
263
- return { activities: view.activities.map((activity) => ({ ...toPublicActivity(activity), perception: 'full' })),
264
- consumedThrough: view.consumedThrough, remaining: view.remaining, idleExpired: stop === 'stale',
265
- ...(status === undefined ? {} : { status }), ...(view.hint === undefined ? {} : { hint: view.hint }), ...(!options.observe ? {} : { observation: {
266
- ...observedContext(this.square, view.standingCount, view.held, view.participant, view.now), participant: view.participant,
267
- sayNumbers: view.sayNumbers, ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
268
- ...(presence === undefined ? { enrichmentUnavailable: true } : { presence }), waitStarted: result.waitStarted, replaced: result.replaced,
269
- } }) };
270
- }
271
- history(query, control) { return this.owner.history(query, control); }
272
- async lifecycle(verb, body, options, control) {
273
- if (verb === 'done')
274
- validateDoneBody(body);
275
- const identity = localSessionIdentities(this.square.env ?? {})[0];
276
- const owner = verb !== 'done' || identity === undefined || this.square.hostLedger === undefined ? undefined
277
- : await readParticipantOwner(this.square.location, this.name, this.square.hostLedger);
278
- const fenced = identity !== undefined && owner?.session === identity.sessionId && presenceEpoch(owner) > 0;
279
- const name = fenced ? this.name : (await join(this.square, this.name, control)).name;
280
- const result = verb === 'done' ? await done(this.square, name, body ?? '', fenced ? { expectedEpoch: presenceEpoch(owner) } : {}, control)
281
- : verb === 'hold' ? await hold(this.square, name, body, control) : await resume(this.square, name, control);
282
- const { observation: committed, ...receipt } = result;
283
- return { ...receipt, ...(!options.observe ? {} : { observation: {
284
- ...observedContext(this.square, committed.standingCount, committed.held, name, committed.now), ...committed,
285
- } }) };
286
- }
287
- hold(reason, options = {}, control) { return this.lifecycle('hold', reason, options, control); }
288
- resume(options = {}, control) { return this.lifecycle('resume', undefined, options, control); }
289
- done(body, options = {}, control) { return this.lifecycle('done', body, options, control); }
58
+ /** A generic managed address must not displace the receiver's native registration on reconnect. */
59
+ function canRefreshWakeAddress(current, proposed) {
60
+ return proposed.kind !== 'paseo' || current === undefined || current.kind === 'paseo';
290
61
  }
291
- /** One concrete square resource. Request handles may borrow a server-owned connection. */
62
+ /** One concrete local resource, including its entry-resolved dependencies and lifetime. */
292
63
  export class SqliteSquare {
293
64
  location;
294
- square;
65
+ artifact;
66
+ clock;
67
+ body;
68
+ wakeAddress;
69
+ wakeTransport;
70
+ indexPath;
71
+ automaticDelivery;
72
+ ownsArtifact;
295
73
  defaultParticipant;
296
- transport;
297
- constructor(location, square, defaultParticipant) {
74
+ constructor(location, artifact, clock, body, wakeAddress, wakeTransport, indexPath, automaticDelivery = true, ownsArtifact = true, defaultParticipant) {
298
75
  this.location = location;
299
- this.square = square;
76
+ this.artifact = artifact;
77
+ this.clock = clock;
78
+ this.body = body;
79
+ this.wakeAddress = wakeAddress;
80
+ this.wakeTransport = wakeTransport;
81
+ this.indexPath = indexPath;
82
+ this.automaticDelivery = automaticDelivery;
83
+ this.ownsArtifact = ownsArtifact;
300
84
  this.defaultParticipant = defaultParticipant;
301
- this.transport = defaultTransport(square);
302
85
  }
303
86
  static async at(input, control, automaticDelivery = true) {
304
87
  const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env });
305
- return new SqliteSquare(location, await openSquare(location, { ...input, env, automaticDelivery, clock: squareClock(env, input.clock), artifact: input.resources?.artifact(location) }, control), input.participant);
88
+ const clock = squareClock(env, input.clock);
89
+ const wakeTransport = input.wakeTransport ?? (automaticDelivery
90
+ ? await createDefaultWakeTransport(env).catch(() => undefined) : undefined);
91
+ // A request can abort its own borrowed reads, never the owner's connection lifetime.
92
+ const borrowed = input.resources?.artifact(location);
93
+ const artifact = borrowed ?? createArtifactPort(location, undefined, control?.signal);
94
+ try {
95
+ await artifact.read(facts => facts.header(), control?.signal);
96
+ const resource = new SqliteSquare(location, artifact, clock, input.body, input.wakeAddress, wakeTransport, squareIndexPath(env), automaticDelivery, borrowed === undefined, input.participant);
97
+ await resource.publishIdentity();
98
+ return resource;
99
+ }
100
+ catch (error) {
101
+ if (borrowed === undefined)
102
+ await artifact.close();
103
+ if (error instanceof InternalSquareError && error.code === 'not_found')
104
+ throw new SquareError('unavailable', `Square is unavailable at ${location}`);
105
+ throw error;
106
+ }
306
107
  }
307
108
  static async build(input, control, automaticDelivery = true) {
308
109
  checkControl(control);
309
110
  const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env });
310
111
  await createSquare(location, { hardCap: input.hardCap, throttlePerMinute: input.throttlePerMinute,
311
112
  template: input.template, force: input.force ?? false }, input.markdown, control?.signal);
312
- // A completed build survives cancellation arriving after its commit.
113
+ // Publication already committed; cancellation cannot erase the completed build.
313
114
  return SqliteSquare.at({ ...input, path: location, env }, undefined, automaticDelivery);
314
115
  }
315
116
  static inMemory(input) {
316
117
  const env = { ...(input.env ?? process.env) };
317
- return new SqliteSquare('memory', buildMemorySquare({ ...input, env, clock: squareClock(env, input.clock) }), input.participant);
118
+ return new SqliteSquare('memory', createArtifactPort(':memory:', prepareSquareBuild(input, input.markdown)), squareClock(env, input.clock), input.body, input.wakeAddress, input.wakeTransport, undefined, true, true, input.participant);
119
+ }
120
+ /** Discovery is best effort and never grants write authority. Finish it before returning. */
121
+ async publishIdentity() {
122
+ if (this.location !== 'memory' && this.body !== undefined && this.indexPath !== undefined)
123
+ await rememberSquarePath(this.location, this.body, this.indexPath).catch(() => undefined);
124
+ }
125
+ observation(standingCount, held, participant, now = this.clock()) {
126
+ return { location: this.location, standingCount, held, now,
127
+ ...(participant === undefined ? {} : { participant }), automaticDeliveryAvailable: this.automaticDelivery && this.body !== undefined };
318
128
  }
319
- handle(name) { return new ParticipantHandle(name, this, this.square, this.transport); }
129
+ async contextObservation(participant, control) {
130
+ return (await this.artifact.read(facts => this.observation(inSquareCount(facts), facts.hold().active, participant), control?.signal)).value;
131
+ }
132
+ handle(name, seat) { return new SqliteSquare.ParticipantHandle(name, this, seat); }
133
+ // The nested handle can use its resource's private dependencies without widening
134
+ // the concrete-derived backend contract or copying them into a second context.
135
+ static ParticipantHandle = class {
136
+ name;
137
+ owner;
138
+ seat;
139
+ constructor(name, owner, seat) {
140
+ this.name = name;
141
+ this.owner = owner;
142
+ this.seat = seat;
143
+ }
144
+ async express(bodyOrInput, optionsOrControl = {}, operationControl) {
145
+ const square = this.owner;
146
+ const input = typeof bodyOrInput === 'object' && bodyOrInput !== null ? bodyOrInput : undefined;
147
+ const body = typeof bodyOrInput === 'string' || bodyOrInput === undefined ? bodyOrInput : bodyOrInput.body;
148
+ const options = (input ?? optionsOrControl);
149
+ const control = input === undefined ? operationControl : optionsOrControl;
150
+ let name = this.name, effect = 'unsent';
151
+ let landedId;
152
+ let waited = false, heldWaitedMs = 0;
153
+ try {
154
+ checkControl(control);
155
+ name = (await square.artifact.read(facts => resolveKnownName(facts, this.name), control?.signal)).value;
156
+ const seat = requireSeat(this.seat, square.body, name);
157
+ const feed = !options.observe ? undefined : (await square.artifact.read(facts => {
158
+ const pendingPublic = directedSuffix(facts, name);
159
+ return { pendingPublic, sayNumbers: facts.sayOrdinals(pendingPublic.map(item => item.index)),
160
+ perceptions: Object.fromEntries(pendingPublic.map(item => [item.index, 'full'])) };
161
+ }, control?.signal)).value;
162
+ while (true) {
163
+ checkControl(control);
164
+ try {
165
+ effect = 'unknown';
166
+ const now = square.clock();
167
+ const reply = rejectUnsentActivity(() => {
168
+ try {
169
+ return options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
170
+ }
171
+ catch (error) {
172
+ if (isSquareError(error))
173
+ error.facts = { ...error.facts, reason: 'invalid_reply', activityId: options.reply };
174
+ throw error;
175
+ }
176
+ });
177
+ const committed = (await square.artifact.transactAs(participantAuthority(seat), facts => {
178
+ // Decoding selected rows is storage work, not proof of safe admission refusal.
179
+ const proposed = decideAct(facts, { name, body, force: options.force ?? false, metadata: options.metadata, now, mentions: options.mentions,
180
+ ...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...(reply === undefined ? {} : { reply }) });
181
+ const holder = proposed.type === 'held' ? facts.hold().actor : undefined;
182
+ const decision = rejectUnsentActivity(() => {
183
+ if (proposed.type === 'blocked')
184
+ throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending: proposed.activitySummaries.reduce((count, summary) => count + summary.count, 0) });
185
+ if (proposed.type === 'held')
186
+ throw new SquareError('held', 'The square is held', { ...(holder === undefined ? {} : { holder }), ...(proposed.reason === undefined ? {} : { holdReason: proposed.reason }) });
187
+ if (proposed.type === 'capped')
188
+ throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
189
+ if (proposed.type === 'throttled')
190
+ throw new SquareError('throttled', `${name} is throttled`, { retryAfterMs: proposed.delayMs });
191
+ if (proposed.type === 'bell_quota')
192
+ throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, proposed.nextAt - now) });
193
+ return proposed;
194
+ });
195
+ return { stored: facts.land(decision.act), standingCount: inSquareCount(facts), held: facts.hold().active,
196
+ ownActivityCount: facts.participant(name).sayCount, hardCap: facts.header().hardCap,
197
+ ...(decision.unmatchedMentions === undefined ? {} : { unmatchedMentions: decision.unmatchedMentions }),
198
+ ...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }) };
199
+ }, control?.signal)).value;
200
+ landedId = formatActivityId(committed.stored.index);
201
+ effect = 'landed';
202
+ await square.publishIdentity();
203
+ const delivery = square.wakeTransport !== undefined && square.location !== 'memory'
204
+ ? await deliverPending({ artifact: square.artifact, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now })
205
+ .catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }))
206
+ : { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
207
+ const { stored, standingCount, held, ownActivityCount, hardCap, ...metadata } = committed;
208
+ const hint = expressHint(ownActivityCount);
209
+ return { activity: toPublicActivity(stored), delivery, waited, ...metadata,
210
+ ...(hint === undefined ? {} : { hint }), ...(feed === undefined ? {} : { observation: {
211
+ ...square.observation(standingCount, held, name, now), ownActivityCount, hardCap, ...feed, participant: name,
212
+ } }) };
213
+ }
214
+ catch (error) {
215
+ if (effect !== 'landed' && isSquareError(error) && error.facts?.activityUnsent === true)
216
+ effect = 'unsent';
217
+ if (effect === 'landed' || !isSquareError(error) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
218
+ throw error;
219
+ if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
220
+ throw error;
221
+ waited = true;
222
+ control?.onProgress?.({ kind: 'waiting', reason: error.code,
223
+ ...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
224
+ ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
225
+ ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }) });
226
+ if (error.code === 'throttled')
227
+ await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
228
+ else {
229
+ const started = Date.now();
230
+ const observation = await square.artifact.read(facts => facts.hold().active, control?.signal);
231
+ if (observation.value)
232
+ await square.artifact.changed(observation.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control?.signal);
233
+ heldWaitedMs += Date.now() - started;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ catch (error) {
239
+ const recipient = isSquareError(error) && ['not_standing', 'bell_mention_conflict'].includes(error.facts?.reason ?? '');
240
+ throw failure(error, { operation: 'express', participant: name, subject: recipient ? 'recipient' : 'caller', expressEffect: effect,
241
+ ...(landedId === undefined ? {} : { landedId }) });
242
+ }
243
+ }
244
+ async listener(verb, target, options, control) {
245
+ const square = this.owner;
246
+ const name = (await square.artifact.read(facts => resolveKnownName(facts, this.name), control?.signal)).value;
247
+ checkControl(control);
248
+ const seat = requireSeat(this.seat, square.body, name), now = square.clock();
249
+ return (await square.artifact.transactAs(participantAuthority(seat), facts => {
250
+ const act = verb === 'listen' ? coreListen(facts, name, target, now) : verb === 'ignore' ? coreIgnore(facts, name, target, now) : coreUnlisten(facts, name, target, now);
251
+ const activity = act === undefined ? null : toPublicActivity(facts.land(act));
252
+ return { activity, ...(!options.observe ? {} : { observation: {
253
+ ...square.observation(inSquareCount(facts), facts.hold().active, name, now), participant: name,
254
+ requestedTarget: target, target: activity?.target ?? target,
255
+ } }) };
256
+ }, control?.signal)).value;
257
+ }
258
+ listen(target, options = {}, control) { return this.listener('listen', target, options, control); }
259
+ ignore(target, options = {}, control) { return this.listener('ignore', target, options, control); }
260
+ unlisten(target, options = {}, control) { return this.listener('unlisten', target, options, control); }
261
+ async listening(options = {}, control) {
262
+ const square = this.owner;
263
+ return (await square.artifact.read(facts => {
264
+ const name = resolveKnownName(facts, this.name);
265
+ return { listening: coreListening(facts, name), ...(!options.observe ? {} : { observation: square.observation(inSquareCount(facts), facts.hold().active, name) }) };
266
+ }, control?.signal)).value;
267
+ }
268
+ /** Receipt selection rechecks the lease inside the writer, not at its last heartbeat. */
269
+ async catchPage(name, seat, options, control, leaseId) {
270
+ return (await this.owner.artifact.transactAs(participantAuthority(seat), facts => {
271
+ if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
272
+ return null;
273
+ return decideCatch(facts, name, options, this.owner.clock());
274
+ }, control?.signal)).value;
275
+ }
276
+ async catch(options = {}, control) {
277
+ const square = this.owner;
278
+ let name = this.name, subject = 'caller';
279
+ let leaseId, seat;
280
+ let waitStarted = false, replaced = false;
281
+ let selection = options;
282
+ const idle = options.idle ?? 0;
283
+ const complete = async (caught, stop) => {
284
+ const view = caught ?? (await square.artifact.read(facts => observeCatchSelection(facts, name, [], {
285
+ unseenOnly: true, senders: selection.from, ...(selection.mention === true ? { attentionOnly: true } : {}),
286
+ }, square.clock()))).value;
287
+ if (stop === 'empty-now' || stop === undefined)
288
+ stop = view.terminalStatus ?? stop;
289
+ if (leaseId !== undefined) {
290
+ await releaseWatchLease(square.artifact, seat, leaseId).catch(() => undefined);
291
+ leaseId = undefined;
292
+ }
293
+ let presence;
294
+ if (options.observe) {
295
+ try {
296
+ presence = (await square.artifact.read(facts => catchPresence(facts, square.clock()), control?.signal)).value;
297
+ }
298
+ catch { /* Optional presence cannot erase the completed selection or receipts. */ }
299
+ }
300
+ const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum' ? stop : undefined;
301
+ return { activities: view.activities.map(activity => ({ ...toPublicActivity(activity), perception: 'full' })),
302
+ consumedThrough: view.consumedThrough, remaining: view.remaining, idleExpired: stop === 'stale',
303
+ ...(status === undefined ? {} : { status }), ...(view.hint === undefined ? {} : { hint: view.hint }), ...(!options.observe ? {} : { observation: {
304
+ ...square.observation(view.standingCount, view.held, view.participant, view.now), participant: view.participant,
305
+ sayNumbers: view.sayNumbers, ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
306
+ ...(presence === undefined ? { enrichmentUnavailable: true } : { presence }), waitStarted, replaced,
307
+ } }) };
308
+ };
309
+ try {
310
+ checkControl(control);
311
+ validateName(name);
312
+ if (!Number.isFinite(idle) || idle < 0)
313
+ throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
314
+ if (options.id !== undefined && ['idle', 'from', 'mention', 'limit', 'replace'].some(key => Object.hasOwn(options, key)))
315
+ throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention, limit or replace');
316
+ if (Object.hasOwn(options, 'replace') && idle === 0)
317
+ throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
318
+ catchLimit(options.limit);
319
+ const probe = (await square.artifact.read(facts => {
320
+ name = resolveKnownName(facts, this.name);
321
+ seat = requireSeat(this.seat, square.body, name);
322
+ subject = 'filter';
323
+ const from = options.from?.map(target => resolveKnownName(facts, target));
324
+ selection = { ...options, ...(from === undefined ? {} : { from }) };
325
+ subject = 'caller';
326
+ if (idle !== 0 || options.id !== undefined)
327
+ return undefined;
328
+ const page = selectCatchPage(facts, name, selection);
329
+ return page.indexes.length === 0 ? observeCatchSelection(facts, page.participant, [], page.filter, square.clock()) : undefined;
330
+ }, control?.signal)).value;
331
+ if (idle === 0) {
332
+ const data = probe ?? (await this.catchPage(name, seat, selection, control));
333
+ return await complete(data, data.activities.length === 0 ? 'empty-now' : undefined);
334
+ }
335
+ const watch = {
336
+ ...(selection.from === undefined ? {} : { participants: [...selection.from] }), ...(options.mention ? { mention: name } : {}),
337
+ ...(options.replace === undefined ? {} : { replace: options.replace }),
338
+ };
339
+ const start = await acquireWatchLease(square.artifact, seat, square.clock(), crypto.randomUUID(), watch, control?.signal);
340
+ if (start.type === 'active')
341
+ return await complete(undefined, 'already-active');
342
+ leaseId = start.leaseId;
343
+ waitStarted = true;
344
+ replaced = start.replaced;
345
+ control?.onProgress?.({ kind: 'catch-started', participant: name, idleMs: idle, replacementRequested: options.replace ?? false, replaced });
346
+ let quietSince = square.clock(), wasHeld = false, nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
347
+ while (true) {
348
+ checkControl(control);
349
+ // Arm before selecting: a commit during selection must not be missed.
350
+ const baseline = await square.artifact.revision(control?.signal);
351
+ const pulse = await pulseWatchLease(square.artifact, seat, square.clock(), leaseId, watch, square.clock() >= nextHeartbeat, control?.signal);
352
+ if (pulse.type === 'replaced')
353
+ return await complete(undefined, 'replaced');
354
+ if (pulse.type === 'terminal')
355
+ return await complete(undefined, pulse.status);
356
+ if (pulse.heartbeatAt !== undefined)
357
+ nextHeartbeat = pulse.heartbeatAt + WATCH_HEARTBEAT_MS;
358
+ if (pulse.type === 'sleep') {
359
+ const caught = await this.catchPage(name, seat, selection, control, leaseId);
360
+ if (caught === null)
361
+ return await complete(undefined, 'replaced');
362
+ if (caught.activities.length > 0)
363
+ return await complete(caught);
364
+ }
365
+ if (pulse.type === 'held' || wasHeld)
366
+ quietSince = square.clock();
367
+ wasHeld = pulse.type === 'held';
368
+ if (!wasHeld && square.clock() - quietSince >= idle) {
369
+ const owns = await ownsWatchLease(square.artifact, seat, square.clock(), leaseId, control?.signal);
370
+ return await complete(undefined, owns ? 'stale' : 'replaced');
371
+ }
372
+ const deadline = Math.min(nextHeartbeat, wasHeld ? Infinity : quietSince + idle);
373
+ await square.artifact.changed(baseline, Math.max(0, deadline - square.clock()), control?.signal);
374
+ }
375
+ }
376
+ catch (error) {
377
+ throw failure(error, { operation: 'catch', subject, participant: name });
378
+ }
379
+ finally {
380
+ // Cancellation releases only this captured generation and this lease.
381
+ if (leaseId !== undefined)
382
+ await releaseWatchLease(square.artifact, seat, leaseId).catch(() => undefined);
383
+ }
384
+ }
385
+ history(query, control) { return this.owner.history(query, control); }
386
+ async lifecycle(verb, body, options, control) {
387
+ if (verb === 'done')
388
+ validateDoneBody(body);
389
+ const square = this.owner;
390
+ const name = (await square.artifact.read(facts => resolveKnownName(facts, this.name), control?.signal)).value;
391
+ const seat = requireSeat(this.seat, square.body, name), now = square.clock();
392
+ return (await square.artifact.transactAs(participantAuthority(seat), facts => {
393
+ const stored = facts.land(verb === 'done' ? coreDone(facts, name, body ?? '', now) : verb === 'hold' ? coreHold(facts, name, body ?? '', now) : coreResume(facts, name, now));
394
+ return { activity: toPublicActivity(stored), ...(!options.observe ? {} : { observation: {
395
+ ...square.observation(inSquareCount(facts), facts.hold().active, name, now), activity: stored,
396
+ } }) };
397
+ }, control?.signal)).value;
398
+ }
399
+ hold(reason, options = {}, control) { return this.lifecycle('hold', reason, options, control); }
400
+ resume(options = {}, control) { return this.lifecycle('resume', undefined, options, control); }
401
+ done(body, options = {}, control) { return this.lifecycle('done', body, options, control); }
402
+ };
320
403
  async participant(requested) {
321
- const selected = requested ?? this.defaultParticipant ?? this.square.env?.SQUARE_PARTICIPANT_NAME;
322
- if (requested !== undefined || this.defaultParticipant !== undefined || selected?.trim()) {
404
+ const selected = requested ?? this.defaultParticipant;
405
+ if (selected !== undefined) {
323
406
  validateName(selected);
324
- return this.handle(selected);
407
+ const seat = this.body === undefined ? undefined : (await this.artifact.read(facts => facts.seat(selected))).value;
408
+ return this.handle(selected, seat);
409
+ }
410
+ if (this.body !== undefined) {
411
+ const seats = (await this.artifact.read(facts => facts.seatsForBody(this.body))).value;
412
+ if (seats.length === 1)
413
+ return this.handle(seats[0].participant, seats[0]);
414
+ if (seats.length > 1)
415
+ throw new SquareError('invalid_args', 'Square needs an explicit participant name: this body holds multiple names');
325
416
  }
326
- const discovered = this.square.hostLedger === undefined ? undefined : await localParticipantName(this.location, this.square.hostLedger, this.square.env);
327
- if (discovered !== undefined)
328
- return this.handle(discovered);
329
417
  throw new SquareError('invalid_args', 'Square needs one unambiguous participant name');
330
418
  }
331
419
  async join(requested, options = {}, control) {
332
420
  let name = requested;
333
421
  try {
334
422
  checkControl(control);
335
- name = (await this.participant(requested)).name;
423
+ name = requireJoinName(requested);
336
424
  const last = options.last ?? HISTORY_DEFAULT_LIMIT;
337
425
  if (!Number.isSafeInteger(last) || last < 1 || last > HISTORY_MAX_LIMIT)
338
426
  throw new SquareError('invalid_args', 'Join history limit must be between 1 and 100');
339
- let committed;
340
- let result;
341
- if (options.takeover && await currentParticipant(this.square, name, control) !== undefined) {
342
- const taken = await takeover(this.square, name, control);
343
- result = { participant: taken.name, activity: taken.activities.at(-1) ?? null, kind: 'taken-over', participantCount: taken.participantCount };
344
- committed = taken.observation;
345
- }
346
- else {
347
- const joined = await join(this.square, name, control);
348
- result = { participant: joined.name, activity: joined.activity, kind: joined.reconnected ? 'reconnected' : 'joined', participantCount: joined.participantCount };
349
- committed = joined.observation;
427
+ const body = requireBody(this.body), now = this.clock();
428
+ const initial = !options.takeover ? undefined : (await this.artifact.read(facts => facts.participant(name)?.joined ? facts.seat(name) : undefined, control?.signal)).value;
429
+ const authority = initial === undefined ? { kind: 'join', participant: name, body, wakeAddress: this.wakeAddress }
430
+ : { kind: 'takeover', participant: initial.participant, body, expectedGeneration: initial.generation, wakeAddress: this.wakeAddress };
431
+ const committed = (await this.artifact.transactAs(authority, facts => {
432
+ const participant = facts.participant(name)?.name ?? name;
433
+ const current = facts.participant(participant), currentSeat = facts.seat(participant);
434
+ let kind = 'joined', activity = null;
435
+ if (initial !== undefined) {
436
+ if (!current?.joined)
437
+ throw new SquareError('already_done', `${participantIdentity(participant)} is no longer standing here`);
438
+ facts.land(coreDone(facts, participant, '', now));
439
+ kind = 'taken-over';
440
+ }
441
+ else if (current?.joined) {
442
+ if (currentSeat === undefined || !sameBody(currentSeat.body, body))
443
+ throw new SquareError('already_joined', `${participantIdentity(participant)} is held by another body; take it over explicitly`);
444
+ if (this.wakeAddress !== undefined && canRefreshWakeAddress(currentSeat.wakeAddress, this.wakeAddress))
445
+ facts.updateWakeAddress(asSeatRef(currentSeat), this.wakeAddress);
446
+ kind = 'reconnected';
447
+ }
448
+ if (kind !== 'reconnected') {
449
+ const decision = decideJoin(facts, participant, now);
450
+ if (decision.joinAct === undefined)
451
+ throw new SquareError('already_joined', `${participantIdentity(participant)} could not be joined`);
452
+ activity = toPublicActivity(facts.land(decision.joinAct));
453
+ }
454
+ const seat = facts.seat(participant);
455
+ if (seat === undefined)
456
+ throw new SquareError('invalid_args', 'The join activity did not establish its artifact seat.');
457
+ return { participant: this.handle(seat.participant, seat), activity, kind,
458
+ participantCount: facts.roster().filter(item => item.joined || item.done).length,
459
+ ...(!options.observe ? {} : { observation: this.observation(inSquareCount(facts), facts.hold().active, seat.participant, now) }) };
460
+ }, control?.signal)).value;
461
+ await this.publishIdentity();
462
+ if (!options.observe)
463
+ return committed;
464
+ let entry;
465
+ try {
466
+ entry = (await this.artifact.read(facts => entryPresentation(facts, committed.participant.name, last), control?.signal)).value;
350
467
  }
351
- const entry = !options.observe ? undefined : await entryPresentation(this.square, result.participant, last, control).catch(() => undefined);
352
- return { ...result, participant: this.handle(result.participant), ...(!options.observe ? {} : {
353
- observation: {
354
- ...observedContext(this.square, committed.standingCount, committed.held, result.participant, committed.now),
355
- ...(entry === undefined ? { enrichmentUnavailable: true } : { entry }),
356
- },
357
- }) };
468
+ catch { /* Keep the committed join even when optional history is unavailable. */ }
469
+ return { ...committed, observation: { ...committed.observation, ...(entry === undefined ? { enrichmentUnavailable: true } : { entry }) } };
358
470
  }
359
471
  catch (error) {
360
472
  throw failure(error, { operation: 'join', subject: 'caller', participant: name });
361
473
  }
362
474
  }
363
- doneOwnedSession(name, body, expectedEpoch, control) { return done(this.square, name, body, { expectedEpoch }, control); }
364
475
  async implicitJoin(name, control) {
365
- const result = await implicitJoin(this.square, name, control);
366
- return { state: result.state, ...(result.state === 'done' ? {} : { participant: this.handle(result.name) }) };
476
+ checkControl(control);
477
+ validateName(name);
478
+ const body = requireBody(this.body), now = this.clock();
479
+ const result = (await this.artifact.transactAs({ kind: 'join', participant: name, body, wakeAddress: this.wakeAddress }, facts => {
480
+ const decision = decideImplicitJoin(facts, name, now);
481
+ let seat = facts.seat(decision.joinedName);
482
+ if (decision.joinAct === undefined) {
483
+ if (decision.state === 'active' && (seat === undefined || !sameBody(seat.body, body)))
484
+ throw new SquareError('already_joined', `${participantIdentity(decision.joinedName)} is held by another body`);
485
+ if (decision.state === 'active' && this.wakeAddress !== undefined && canRefreshWakeAddress(seat.wakeAddress, this.wakeAddress))
486
+ facts.updateWakeAddress(asSeatRef(seat), this.wakeAddress);
487
+ }
488
+ else {
489
+ facts.land(decision.joinAct);
490
+ seat = facts.seat(decision.joinedName);
491
+ if (seat === undefined)
492
+ throw new SquareError('invalid_args', 'The implicit join did not establish its artifact seat.');
493
+ }
494
+ return { state: decision.state, ...(decision.state === 'done' ? {} : { participant: this.handle(decision.joinedName, seat) }) };
495
+ }, control?.signal)).value;
496
+ if (result.participant !== undefined)
497
+ await this.publishIdentity();
498
+ return result;
367
499
  }
368
- snapshot(control) { return snapshot(this.square, control); }
500
+ async snapshot(control) { return (await this.artifact.read(facts => snapshot(facts, this.clock()), control?.signal)).value; }
369
501
  async history(query = {}, control) {
370
502
  if (query.limit !== undefined && query.limit !== null && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT))
371
503
  throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
@@ -381,89 +513,89 @@ export class SqliteSquare {
381
513
  const limit = query.limit === undefined ? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT) : query.limit;
382
514
  const contextual = !!query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined;
383
515
  const kinds = contextual || query.since !== undefined ? undefined : NON_READ;
384
- return (await this.square.artifact.read((facts) => {
516
+ return (await this.artifact.read(facts => {
385
517
  const selected = selectHistory(facts, query, limit, kinds, query.observe === true);
386
518
  if (contextual && limit === null && selected.totalMatches > HISTORY_MAX_LIMIT)
387
519
  throw failure(new SquareError('invalid_args', `History context exceeds ${HISTORY_MAX_LIMIT} activities`), { operation: 'history', historyBounds: { maxLimit: HISTORY_MAX_LIMIT, totalMatches: selected.totalMatches } });
388
520
  const activities = publicActivities(selected.activities);
389
521
  if (!query.observe)
390
522
  return { activities };
391
- const indexes = selected.activities.map((activity) => activity.index), presenceAnchors = {};
523
+ const indexes = selected.activities.map(activity => activity.index), presenceAnchors = {};
392
524
  for (const participant of facts.roster()) {
393
525
  const anchor = presenceAnchor(facts, participant.name);
394
526
  if (indexes.includes(anchor))
395
527
  (presenceAnchors[anchor] ??= []).push(participant.name);
396
528
  }
397
- return { activities, observation: { ...observedContext(this.square, inSquareCount(facts), facts.hold().active), ...selected,
529
+ return { activities, observation: { ...this.observation(inSquareCount(facts), facts.hold().active), ...selected,
398
530
  sayNumbers: facts.sayOrdinals(indexes), presenceAnchors } };
399
531
  }, control?.signal)).value;
400
532
  }
401
533
  async status(options = {}, control) {
402
534
  const viewer = !options.observe ? '' : options.viewer ?? (await this.participant().catch(() => undefined))?.name ?? '';
403
- return (await this.square.artifact.read((facts) => queryStatus(facts, {
404
- location: this.location, now: this.square.clock(), automaticDeliveryAvailable: this.square.automaticDelivery !== false && hasAutomaticDeliveryIdentity(this.square.env),
535
+ return (await this.artifact.read(facts => queryStatus(facts, {
536
+ location: this.location, now: this.clock(), automaticDeliveryAvailable: this.automaticDelivery && this.body !== undefined,
405
537
  }, viewer, options.observe === true), control?.signal)).value;
406
538
  }
407
539
  async participants(options = {}, control) {
408
- return (await this.square.artifact.read((facts) => queryParticipants(facts, {
409
- location: this.location, now: this.square.clock(), automaticDeliveryAvailable: this.square.automaticDelivery !== false && hasAutomaticDeliveryIdentity(this.square.env),
540
+ return (await this.artifact.read(facts => queryParticipants(facts, {
541
+ location: this.location, now: this.clock(), automaticDeliveryAvailable: this.automaticDelivery && this.body !== undefined,
410
542
  }, options.observe === true), control?.signal)).value;
411
543
  }
412
544
  async recovery(requested, control) {
413
545
  const name = (await this.participant(requested).catch(() => undefined))?.name;
414
- const facts = name === undefined ? undefined : await recoveryObservation(this.square, name, control).catch(() => undefined);
415
- return facts ?? await contextObservation(this.square, name, control);
546
+ const facts = name === undefined ? undefined : (await this.artifact.read(facts => recoveryObservation(facts, name, this.clock()), control?.signal).catch(() => undefined))?.value;
547
+ return facts ?? await this.contextObservation(name, control);
416
548
  }
417
- stream(options, control) { return streamSquare(this.location, options, control, this.square); }
418
- async recognize(env) {
419
- if (this.square.hostLedger === undefined)
420
- return null;
421
- const candidates = (await Promise.all(sessionIdsFromEnvironment(env).map((sessionId) => projectSessionBindings({ hostLedger: this.square.hostLedger, location: this.location, sessionId })))).flat();
422
- if (candidates.length !== 1)
549
+ stream(options, control) {
550
+ // A path stream follows replacements independently of this connection's inode
551
+ // and lifetime. Only explicit artifact readers borrow an existing port.
552
+ return streamSquare(this.location, options, control);
553
+ }
554
+ async recognize() {
555
+ if (this.body === undefined)
556
+ return [];
557
+ const candidates = (await this.artifact.read(facts => facts.seatsForBody(this.body))).value;
558
+ return candidates.map(seat => this.handle(seat.participant, seat));
559
+ }
560
+ close() { return this.ownsArtifact ? this.artifact.close() : Promise.resolve(); }
561
+ async cancelDeliveries(name, expectedGeneration) {
562
+ const body = this.body;
563
+ if (body === undefined)
564
+ return false;
565
+ const seat = (await this.artifact.read(facts => facts.seat(name))).value;
566
+ if (seat === undefined || seat.generation !== expectedGeneration || !sameBody(seat.body, body))
567
+ return false;
568
+ return this.artifact.cancelDeliveries(asSeatRef(seat));
569
+ }
570
+ async endOwnedSession(name, sessionId, expectedGeneration, body) {
571
+ const captured = (await this.artifact.read(facts => facts.seat(name))).value;
572
+ if (captured === undefined || captured.body.session !== sessionId || (body !== undefined && !sameBody(captured.body, body))
573
+ || (expectedGeneration !== undefined && captured.generation !== expectedGeneration))
423
574
  return null;
424
- const name = await currentParticipant(this.square, candidates[0].participant);
425
- return name === undefined ? null : this.handle(name);
575
+ return (await this.artifact.transactAs(participantAuthority(captured), facts => {
576
+ const current = facts.seat(name);
577
+ if (current === undefined || current.generation !== captured.generation || !sameBody(current.body, captured.body))
578
+ return null;
579
+ return { activity: toPublicActivity(facts.land(coreDone(facts, current.participant, '', this.clock()))) };
580
+ })).value;
426
581
  }
427
- close() { return closeOpenSquare(this.square); }
428
- endOwnedSession(name, sessionId, expectedEpoch) { return endOwnedSession(this.square, name, sessionId, expectedEpoch); }
429
- retireEndedSessionRoutes(sessionId) { return retireEndedSessionRoutes(this.square, sessionId); }
430
582
  }
431
583
  export function diagnoseSquare(input, control) { checkControl(control); return diagnoseSquareFile(localSquareLocation(input)); }
432
- export function migrateSquare(input, control) { checkControl(control); return migrateSquareFile(localSquareLocation(input), control?.signal); }
584
+ export function migrateSquare(input, control) { checkControl(control); const env = input.env ?? process.env; return migrateSquareFile(localSquareLocation(input), control?.signal, env); }
433
585
  export function discover(input, options = {}, control) {
434
586
  if (input.path !== undefined || (input.env ?? process.env).SQUARE_LOCATION)
435
587
  localSquareLocation(input);
436
588
  const env = { ...(input.env ?? process.env) };
437
- return discoverSquares(input.cwd ?? process.cwd(), options, control, { ...input, env, clock: squareClock(env, input.clock) });
589
+ return discoverSquares(input.cwd ?? process.cwd(), options, control, { clock: squareClock(env, input.clock) });
438
590
  }
439
- export async function inbox(input, sessionId, options = {}, control) {
591
+ export async function inbox(input, identity, options = {}, control) {
440
592
  checkControl(control);
441
- const env = { ...(input.env ?? process.env) }, hostLedger = input.hostLedger ?? hostLedgerForEnv(env), limit = options.limit ?? 20;
593
+ const env = { ...(input.env ?? process.env) }, limit = options.limit ?? 20;
442
594
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
443
595
  throw new SquareError('invalid_args', 'Inbox limit must be between 1 and 100');
444
- const rows = [];
445
- for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
446
- checkControl(control);
447
- let square;
448
- try {
449
- square = await openSquare(binding.location, { ...input, env, hostLedger, clock: squareClock(env, input.clock) }, control);
450
- const row = (await square.artifact.read((facts) => {
451
- const participant = facts.participant(binding.participant);
452
- return !participant?.joined ? undefined : { name: participant.name, squarePath: binding.location,
453
- pending: facts.directedCount(participant.name, { unseenOnly: true }) };
454
- }, control?.signal)).value;
455
- if (row !== undefined)
456
- rows.push(row);
457
- }
458
- catch (error) {
459
- if (control?.signal?.aborted)
460
- throw error;
461
- }
462
- finally {
463
- if (square !== undefined)
464
- await closeOpenSquare(square);
465
- }
466
- }
596
+ const query = typeof identity === 'string' ? { sessionId: identity, env } : { body: identity, env };
597
+ const rows = await sessionInboxCounts(query, control?.signal);
598
+ checkControl(control);
467
599
  const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
468
600
  rows.sort((a, b) => compare(a.squarePath, b.squarePath) || compare(nameKey(a.name), nameKey(b.name)) || compare(a.name, b.name));
469
601
  return { rows: rows.slice(0, limit), total: rows.length };