@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
@@ -3,221 +3,153 @@ import { isSquareError, nameKey, SquareError, validateName } from './model.js';
3
3
  import { participantIdentity } from './participant-identity.js';
4
4
  import { deliverPending } from './delivery-operations.js';
5
5
  import { decideCatch } from './catch-decisions.js';
6
+ import { inSquareCount } from './runtime.js';
6
7
  import { claimSessionParticipant, claimSessionTakeover, presenceEpoch, releaseSessionParticipantClaim, readParticipantOwner } from './registry.js';
7
- import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishIdentityRoute, retireIdentityRoute } from './participant-host.js';
8
- import { applyWakeRouteToState, dropEndedSessionWakeRoutesFromState, dropParticipantWakeRoutesFromState, dropSessionWakeRoutesFromState, sessionCanEndParticipant } from './routes.js';
8
+ import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishCommittedIdentity, retireIdentityRoute } from './participant-host.js';
9
+ import { applyWakeRoute, dropEndedSessionWakeRoutes, dropParticipantWakeRoutes, dropSessionWakeRoutes, sessionCanEndParticipant } from './routes.js';
9
10
  import { parseRequiredActivityId, toPublicActivity } from './views.js';
10
11
  function throwIfAborted(control) { if (control?.signal?.aborted)
11
12
  throw control.signal.reason ?? new Error('Operation aborted'); }
12
- function exposeCaught(activity, perception) {
13
- const result = toPublicActivity(activity);
14
- if (perception === 'full' || !('body' in result))
15
- return { ...result, perception };
16
- const { body: _body, ...withoutBody } = result;
17
- return { ...withoutBody, perception };
18
- }
19
- export async function catchUp(square, name, options = {}, project, control) {
20
- const idle = options.idle ?? 0;
21
- if (!Number.isFinite(idle) || idle < 0)
22
- throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
23
- const deadline = Date.now() + idle;
24
- if (control?.signal?.aborted)
25
- throw control.signal.reason ?? new Error('Operation aborted');
26
- while (true) {
27
- if (!await assertLiveOwner(square, name)) {
28
- throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`);
29
- }
30
- const attempt = await square.artifact.transact((state, version) => {
31
- const decision = decideCatch(state, name, options, square.clock(), project);
32
- return { ...(decision.changed ? { state } : {}), result: { version, decision } };
33
- }, control?.signal);
34
- await ensureLocalPresence(square, name);
35
- await publishIdentityRoute(square, name);
36
- if (attempt.decision.delivered.length > 0 || idle === 0) {
37
- return {
38
- activities: attempt.decision.delivered.map((activity) => exposeCaught(activity, attempt.decision.perceptions.get(activity.index) ?? 'full')),
39
- consumedThrough: attempt.decision.consumedThrough,
40
- idleExpired: false,
41
- remaining: attempt.decision.remaining,
42
- };
43
- }
44
- const remaining = deadline - Date.now();
45
- if (remaining <= 0) {
46
- return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true, remaining: 0 };
47
- }
48
- // Publication above is a semantic no-op while the route and presence stay unchanged and
49
- // unexpired, so only a real external change can move the artifact version. Establish the
50
- // wait baseline after those self-side effects; if the version moved anyway (own refresh or
51
- // a racing external commit), re-snapshot instead of waiting on a stale baseline.
52
- const baseline = (await square.artifact.read(control?.signal)).version;
53
- if (baseline !== attempt.version)
54
- continue;
55
- if (!await square.artifact.changed(baseline, remaining, control?.signal)) {
56
- return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true, remaining: 0 };
57
- }
58
- }
59
- }
60
- function storeActs(state, acts) {
61
- const stored = [];
62
- for (const act of acts) {
63
- const item = { ...act, index: state.runtime.nextActIndex };
64
- state.runtime.nextActIndex += 1;
65
- state.acts.push(item);
66
- stored.push(item);
67
- }
68
- return stored;
69
- }
70
- function committedActivity(stored, verb) {
71
- const activity = stored[0];
72
- if (activity === undefined)
73
- throw new Error(`${verb} activity did not commit`);
74
- return activity;
13
+ export async function catchUp(square, name, options = {}, control, leaseId) {
14
+ throwIfAborted(control);
15
+ if (!await assertLiveOwner(square, name))
16
+ throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
17
+ const selected = (await square.artifact.transact((facts) => {
18
+ // Replacement and selection share the receipt transaction: an old waiter cannot
19
+ // consume a later activity between its heartbeat and its selection.
20
+ if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
21
+ return null;
22
+ return decideCatch(facts, name, options, square.clock());
23
+ }, control?.signal)).value;
24
+ if (selected !== null)
25
+ await publishCommittedIdentity(square, selected.participant);
26
+ return selected;
75
27
  }
76
28
  export async function join(square, name, control) {
77
- // Rejected validation must not perform an ownership claim.
78
29
  throwIfAborted(control);
79
30
  validateName(name);
80
31
  const hostLedger = square.hostLedger;
81
- const preview = await square.artifact.read(control?.signal);
82
- const previewDecision = decideJoin(preview.state, name, square.clock());
83
- if (previewDecision.joinAct === undefined) {
84
- // An active artifact participant is idempotent only for its current session.
85
- // A different owner must still pass through the registry CAS so it is refused
86
- // instead of silently reconnecting as a foreign session.
87
- if (hostLedger !== undefined) {
88
- const identity = processIdentity(square.env ?? process.env);
89
- const owner = await readParticipantOwner(square.location, previewDecision.joinedName, hostLedger);
90
- if (owner === undefined) {
91
- // Keep the claim path for an active artifact whose ledger owner is not
92
- // visible yet; concurrent callers must still serialize through CAS.
93
- }
94
- else if (owner.session !== identity.session) {
95
- // Continue into the ownership claim below; it will produce the stable
96
- // already_joined error without mutating the artifact.
97
- }
98
- else {
99
- return { name: previewDecision.joinedName, activity: null };
100
- }
101
- }
102
- else {
103
- return { name: previewDecision.joinedName, activity: null };
104
- }
32
+ const preview = (await square.artifact.read((facts) => ({
33
+ ...decideJoin(facts, name, square.clock()),
34
+ participantCount: facts.roster().filter((p) => p.joined || p.done).length,
35
+ }), control?.signal)).value;
36
+ if (preview.joinAct === undefined) {
37
+ if (hostLedger === undefined)
38
+ return { name: preview.joinedName, activity: null, participantCount: preview.participantCount, reconnected: false };
39
+ const identity = processIdentity(square.env ?? process.env);
40
+ const owner = await readParticipantOwner(square.location, preview.joinedName, hostLedger);
41
+ if (owner?.session === identity.session)
42
+ return {
43
+ name: preview.joinedName, activity: null, participantCount: preview.participantCount,
44
+ reconnected: identity.channel !== 'unknown',
45
+ };
46
+ // Missing or foreign owner must pass CAS, not become a silent reconnect.
105
47
  }
106
48
  let epoch;
107
49
  let ownershipClaim;
108
50
  if (hostLedger !== undefined) {
109
- const claim = await claimSessionParticipant(square.location, name, hostLedger, square.env ?? process.env, control?.signal);
110
- ownershipClaim = claim;
111
- epoch = claim?.epoch;
51
+ ownershipClaim = await claimSessionParticipant(square.location, name, hostLedger, square.env ?? process.env, control?.signal);
52
+ epoch = ownershipClaim?.epoch;
112
53
  }
113
54
  let committed;
114
55
  try {
115
- const now = square.clock();
116
- const route = await identityRouteDraft(square, name);
56
+ const now = square.clock(), route = await identityRouteDraft(square, name);
117
57
  throwIfAborted(control);
118
- committed = await square.artifact.transact((state) => {
119
- const decision = decideJoin(state, name, now);
120
- if (decision.joinAct === undefined) {
121
- return { result: { name: decision.joinedName, stored: null } };
58
+ committed = (await square.artifact.transact((facts) => {
59
+ const decision = decideJoin(facts, name, now);
60
+ if (decision.joinAct !== undefined) {
61
+ if (square.location !== undefined && square.location !== 'memory')
62
+ dropParticipantWakeRoutes(facts, square.location, decision.joinedName);
63
+ if (route !== undefined)
64
+ applyWakeRoute(facts, route, now);
122
65
  }
123
- if (square.location !== undefined && square.location !== 'memory')
124
- dropParticipantWakeRoutesFromState(state, square.location, decision.joinedName);
125
- if (route !== undefined)
126
- applyWakeRouteToState(state, route, now);
127
- return { state, result: { name: decision.joinedName, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
128
- }, control?.signal);
66
+ const stored = decision.joinAct === undefined ? null : facts.land(decision.joinAct);
67
+ return {
68
+ name: decision.joinedName, stored,
69
+ participantCount: facts.roster().filter((p) => p.joined || p.done).length,
70
+ };
71
+ }, control?.signal)).value;
129
72
  }
130
73
  catch (error) {
131
74
  if (hostLedger !== undefined)
132
75
  await releaseSessionParticipantClaim(hostLedger, ownershipClaim).catch(() => undefined);
133
76
  throw error;
134
77
  }
135
- await ensureLocalPresence(square, committed.name, epoch);
136
- await publishIdentityRoute(square, committed.name, epoch);
137
- return { name: committed.name, activity: committed.stored === null ? null : toPublicActivity(committed.stored) };
78
+ await publishCommittedIdentity(square, committed.name, epoch);
79
+ return {
80
+ name: committed.name, activity: committed.stored === null ? null : toPublicActivity(committed.stored),
81
+ participantCount: committed.participantCount, reconnected: false,
82
+ };
138
83
  }
139
- /** End the standing participant and immediately let the caller reclaim the name. */
84
+ /** Takeover holds the host CAS fence across the single done+join artifact transaction. */
140
85
  export async function takeover(square, name, _oldSessionIds = [], control) {
141
- // Rejected validation must not perform an ownership claim.
142
86
  throwIfAborted(control);
143
87
  validateName(name);
144
88
  const hostLedger = square.hostLedger;
145
89
  const commitLifecycle = async () => {
146
90
  throwIfAborted(control);
147
91
  const now = square.clock();
148
- const committed = await square.artifact.transact((state) => {
149
- const joinedName = resolveKnownName(state, name);
150
- const done = coreDone(state, joinedName, '', now);
151
- const storedDone = committedActivity(storeActs(state, [done]), 'kick');
152
- const decision = decideJoin(state, joinedName, now);
92
+ return (await square.artifact.transact((facts) => {
93
+ const joinedName = resolveKnownName(facts, name);
94
+ const storedDone = facts.land(coreDone(facts, joinedName, '', now));
95
+ const decision = decideJoin(facts, joinedName, now);
153
96
  if (decision.joinAct === undefined)
154
97
  throw new SquareError('already_joined', `${participantIdentity(joinedName)} could not be reclaimed`);
155
- const storedJoin = committedActivity(storeActs(state, [decision.joinAct]), 'join');
156
- state.routes = (state.routes ?? []).filter((route) => nameKey(route.participant) !== nameKey(joinedName));
157
- return { state, result: { name: joinedName, stored: [storedDone, storedJoin] } };
158
- }, control?.signal);
159
- return committed;
98
+ const storedJoin = facts.land(decision.joinAct);
99
+ facts.replaceRoutes(facts.routes().filter((route) => nameKey(route.participant) !== nameKey(joinedName)));
100
+ return {
101
+ name: joinedName, stored: [storedDone, storedJoin],
102
+ participantCount: facts.roster().filter((p) => p.joined || p.done).length,
103
+ };
104
+ }, control?.signal)).value;
160
105
  };
161
106
  if (hostLedger !== undefined) {
162
- const env = square.env ?? process.env;
163
- // Gate the ownership claim on the artifact's authoritative state: a takeover that cannot
164
- // commit its lifecycle (a name that never joined, or a standing participant that is no
165
- // longer joined) must not claim or remove presence. The refusals mirror the transaction.
166
- const { state } = await square.artifact.read(control?.signal);
167
- const joinedName = resolveKnownName(state, name); // invalid_args when the name never joined
168
- coreDone(state, joinedName, '', square.clock()); // already_done/not_joined when not standing
107
+ // Gate the claim on authoritative admission, so a refused takeover never claims presence.
108
+ await square.artifact.read((facts) => coreDone(facts, resolveKnownName(facts, name), '', square.clock()), control?.signal);
169
109
  const owner = await readParticipantOwner(square.location, name, hostLedger);
170
110
  throwIfAborted(control);
171
- // The claim and the artifact lifecycle are one fenced critical section: a losing or refused
172
- // takeover never mutates the winner, and no second takeover can interleave mid-commit.
173
- const outcome = await claimSessionTakeover(square.location, name, hostLedger, env, {
174
- expectedEpoch: presenceEpoch(owner),
175
- expectedSession: owner?.session ?? '',
111
+ const outcome = await claimSessionTakeover(square.location, name, hostLedger, square.env ?? process.env, {
112
+ expectedEpoch: presenceEpoch(owner), expectedSession: owner?.session ?? '',
176
113
  }, async (claim) => {
177
114
  throwIfAborted(control);
178
115
  const committed = await commitLifecycle();
179
- await ensureLocalPresence(square, committed.name, claim.epoch);
180
- await publishIdentityRoute(square, committed.name, claim.epoch);
116
+ await publishCommittedIdentity(square, committed.name, claim.epoch);
181
117
  return committed;
182
118
  }, control?.signal);
183
119
  if (outcome.status === 'busy')
184
- throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`);
185
- return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), epoch: outcome.epoch };
120
+ throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
121
+ return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), participantCount: outcome.result.participantCount, epoch: outcome.epoch };
186
122
  }
187
123
  const committed = await commitLifecycle();
188
- await ensureLocalPresence(square, committed.name);
189
- await publishIdentityRoute(square, committed.name);
190
- return { name: committed.name, activities: committed.stored.map(toPublicActivity) };
124
+ await publishCommittedIdentity(square, committed.name);
125
+ return { name: committed.name, activities: committed.stored.map(toPublicActivity), participantCount: committed.participantCount };
191
126
  }
192
127
  export async function implicitJoin(square, name, control) {
193
128
  throwIfAborted(control);
194
- const now = square.clock();
195
- const route = await identityRouteDraft(square, name);
129
+ const now = square.clock(), route = await identityRouteDraft(square, name);
196
130
  throwIfAborted(control);
197
- const committed = await square.artifact.transact((state) => {
198
- const decision = decideImplicitJoin(state, name, now);
131
+ const committed = (await square.artifact.transact((facts) => {
132
+ const decision = decideImplicitJoin(facts, name, now);
199
133
  if (decision.state === 'done') {
200
134
  if (square.location !== undefined && square.location !== 'memory')
201
- dropSessionWakeRoutesFromState(state, square.location, processIdentity(square.env ?? process.env).session);
202
- return { state, result: { name: decision.joinedName, state: decision.state, stored: null } };
135
+ dropSessionWakeRoutes(facts, square.location, processIdentity(square.env ?? process.env).session);
136
+ }
137
+ else if (decision.joinAct !== undefined) {
138
+ if (square.location !== undefined && square.location !== 'memory')
139
+ dropParticipantWakeRoutes(facts, square.location, decision.joinedName);
140
+ if (route !== undefined)
141
+ applyWakeRoute(facts, { ...route, participant: decision.joinedName }, now);
203
142
  }
204
- if (decision.joinAct === undefined)
205
- return { result: { name: decision.joinedName, state: decision.state, stored: null } };
206
- if (square.location !== undefined && square.location !== 'memory')
207
- dropParticipantWakeRoutesFromState(state, square.location, decision.joinedName);
208
- if (route !== undefined)
209
- applyWakeRouteToState(state, { ...route, participant: decision.joinedName }, now);
210
- return { state, result: { name: decision.joinedName, state: decision.state, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
211
- }, control?.signal);
212
- await ensureLocalPresence(square, committed.name);
143
+ return { name: decision.joinedName, state: decision.state, stored: decision.joinAct === undefined ? null : facts.land(decision.joinAct) };
144
+ }, control?.signal)).value;
145
+ if (committed.stored !== null)
146
+ await publishCommittedIdentity(square, committed.name);
147
+ else
148
+ await ensureLocalPresence(square, committed.name);
213
149
  if (committed.state === 'done')
214
150
  await retireIdentityRoute(square, committed.name);
215
- else if (committed.stored !== null)
216
- await publishIdentityRoute(square, committed.name);
217
151
  return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : toPublicActivity(committed.stored) };
218
152
  }
219
- /** Only admission failures can advertise a safe retry; storage and post-commit
220
- * failures must not be mistaken for a rejected send merely by their error code. */
221
153
  function rejectUnsentActivity(operation) {
222
154
  try {
223
155
  return operation();
@@ -229,27 +161,32 @@ function rejectUnsentActivity(operation) {
229
161
  }
230
162
  }
231
163
  export async function express(square, name, body, options = {}, control) {
232
- if (control?.signal?.aborted)
233
- throw control.signal.reason ?? new Error('Operation aborted');
234
- if (!await assertLiveOwner(square, name)) {
235
- throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`, { activityUnsent: true });
236
- }
164
+ throwIfAborted(control);
165
+ if (!await assertLiveOwner(square, name))
166
+ throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`, { activityUnsent: true });
237
167
  throwIfAborted(control);
238
168
  const now = square.clock();
239
- const reply = rejectUnsentActivity(() => options.reply === undefined ? undefined : parseRequiredActivityId(options.reply));
240
- const committed = await square.artifact.transact((state) => {
169
+ const reply = rejectUnsentActivity(() => {
170
+ try {
171
+ return options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
172
+ }
173
+ catch (error) {
174
+ if (isSquareError(error))
175
+ error.facts = { ...error.facts, reason: 'invalid_reply', activityId: options.reply };
176
+ throw error;
177
+ }
178
+ });
179
+ const committed = (await square.artifact.transact((facts) => {
180
+ // Selected-row decoding is storage work, not evidence that admission safely refused.
181
+ const proposed = decideAct(facts, { name, body, force: options.force ?? false, now, mentions: options.mentions,
182
+ ...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...('requireExplicitReach' in options ? { requireExplicitReach: options.requireExplicitReach === true } : {}), ...(reply === undefined ? {} : { reply }) });
183
+ const holder = proposed.type === 'held' ? facts.hold().actor : undefined;
241
184
  const decision = rejectUnsentActivity(() => {
242
- const decision = decideAct(state, { name, body, force: options.force ?? false, now, mentions: options.mentions, ...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...(reply === undefined ? {} : { reply }) });
243
- if (decision.type === 'blocked') {
244
- const pending = decision.activitySummaries.reduce((count, summary) => count + summary.count, 0);
245
- throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending });
246
- }
185
+ const decision = proposed;
186
+ if (decision.type === 'blocked')
187
+ throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending: decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) });
247
188
  if (decision.type === 'held') {
248
- const holder = state.acts.filter((activity) => activity.kind === 'hold').at(-1)?.actor;
249
- throw new SquareError('held', 'The square is held', {
250
- ...(holder === undefined ? {} : { holder }),
251
- ...(decision.reason === undefined ? {} : { holdReason: decision.reason }),
252
- });
189
+ throw new SquareError('held', 'The square is held', { ...(holder === undefined ? {} : { holder }), ...(decision.reason === undefined ? {} : { holdReason: decision.reason }) });
253
190
  }
254
191
  if (decision.type === 'capped')
255
192
  throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
@@ -259,85 +196,69 @@ export async function express(square, name, body, options = {}, control) {
259
196
  throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, decision.nextAt - now) });
260
197
  return decision;
261
198
  });
262
- const stored = committedActivity(storeActs(state, [decision.act]), 'express');
263
- return { state, result: { stored, ...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }) } };
264
- }, control?.signal);
265
- await ensureLocalPresence(square, name);
266
- await publishIdentityRoute(square, name);
267
- let delivery;
268
- if (square.wakeTransport !== undefined && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory') {
269
- delivery = await deliverPending({ artifact: square.artifact, hostLedger: square.hostLedger, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now }).catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }));
270
- }
271
- else {
272
- delivery = { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
273
- }
274
- return { activity: toPublicActivity(committed.stored), delivery, ...(committed.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: committed.replyAuthorNotHere }) };
199
+ const stored = facts.land(decision.act);
200
+ return {
201
+ stored,
202
+ observation: {
203
+ standingCount: inSquareCount(facts), held: facts.hold().active, now,
204
+ ownActivityCount: facts.participant(name).sayCount, hardCap: facts.header().hardCap,
205
+ },
206
+ ...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }),
207
+ };
208
+ }, control?.signal)).value;
209
+ await publishCommittedIdentity(square, name);
210
+ const delivery = square.wakeTransport !== undefined && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory'
211
+ ? await deliverPending({ artifact: square.artifact, hostLedger: square.hostLedger, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now })
212
+ .catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }))
213
+ : { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
214
+ return {
215
+ activity: toPublicActivity(committed.stored), delivery, observation: committed.observation,
216
+ ...(committed.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: committed.replyAuthorNotHere }),
217
+ };
275
218
  }
276
219
  async function landListenerChange(square, verb, actor, target, control) {
277
220
  throwIfAborted(control);
278
221
  const now = square.clock();
279
- const stored = await square.artifact.transact((state) => {
280
- const act = verb === 'listen' ? coreListen(state, actor, target, now) : coreIgnore(state, actor, target, now);
281
- if (act === undefined)
282
- return { result: null };
283
- return { state, result: committedActivity(storeActs(state, [act]), verb) };
284
- }, control?.signal);
222
+ const stored = (await square.artifact.transact((facts) => {
223
+ const act = verb === 'listen' ? coreListen(facts, actor, target, now) : coreIgnore(facts, actor, target, now);
224
+ return act === undefined ? null : facts.land(act);
225
+ }, control?.signal)).value;
285
226
  return { activity: stored === null ? null : toPublicActivity(stored) };
286
227
  }
287
228
  export function listen(square, actor, target, control) { return landListenerChange(square, 'listen', actor, target, control); }
288
229
  export function ignore(square, actor, target, control) { return landListenerChange(square, 'ignore', actor, target, control); }
289
- export async function listening(square, actor, control) { const { state } = await square.artifact.read(control?.signal); return coreListening(state, actor); }
230
+ export async function listening(square, actor, control) { return (await square.artifact.read((facts) => coreListening(facts, actor), control?.signal)).value; }
290
231
  async function landCore(square, verb, actor, body = '', fence = {}, control) {
291
232
  if (verb === 'done')
292
233
  validateDoneBody(body);
293
- // Done completes ownership: the live-owner validation, the artifact commit, the ended
294
- // session's route retirement, and presence cleanup share one ownership critical section.
295
- // A takeover finalizing between validation and commit can never be completed by a stale
296
- // done, and no post-lock cleanup can remove a replacement owner's fresh rows.
297
234
  const hostLedger = verb === 'done' ? square.hostLedger : undefined;
298
- const fenced = hostLedger !== undefined;
299
- const session = fenced ? processIdentity(square.env ?? process.env).session : undefined;
235
+ const session = hostLedger === undefined ? undefined : processIdentity(square.env ?? process.env).session;
300
236
  const commitAndCleanup = async () => {
301
- if (verb === 'done' && !await assertLiveOwner(square, actor, fence.expectedEpoch)) {
302
- throw new SquareError('already_done', `✕ ${participantIdentity(actor)} already stands here — another session holds the name`);
303
- }
304
- let ownerRecords = [];
305
- if (fenced && session !== undefined) {
306
- ownerRecords = (await hostLedger.listPresence({ location: square.location, participant: actor }))
307
- .filter((row) => row.session === session);
308
- }
237
+ // Owner admission, commit and route/presence cleanup share the host claim critical section.
238
+ if (verb === 'done' && !await assertLiveOwner(square, actor, fence.expectedEpoch))
239
+ throw new SquareError('already_done', `${participantIdentity(actor)} already stands here — another session holds the name`);
240
+ const owners = hostLedger === undefined ? [] : (await hostLedger.listPresence({ location: square.location, participant: actor })).filter((row) => row.session === session);
309
241
  const now = square.clock();
310
- const stored = await square.artifact.transact((state) => {
311
- const act = verb === 'done' ? coreDone(state, actor, body, now) : verb === 'hold' ? coreHold(state, actor, body, now) : coreResume(state, actor, now);
312
- const result = committedActivity(storeActs(state, [act]), verb);
313
- if (fenced && session !== undefined) {
314
- dropEndedSessionWakeRoutesFromState(state, square.location, session, { expectedEpoch: fence.expectedEpoch, force: true, participant: actor });
315
- }
316
- return { state, result };
317
- }, control?.signal);
318
- for (const ownerRecord of ownerRecords) {
319
- await hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
242
+ const stored = (await square.artifact.transact((facts) => {
243
+ const result = facts.land(verb === 'done' ? coreDone(facts, actor, body, now) : verb === 'hold' ? coreHold(facts, actor, body, now) : coreResume(facts, actor, now));
244
+ if (session !== undefined)
245
+ dropEndedSessionWakeRoutes(facts, square.location, session, { expectedEpoch: fence.expectedEpoch, force: true, participant: actor });
246
+ return result;
247
+ }, control?.signal)).value;
248
+ for (const row of owners)
249
+ await hostLedger.removePresenceIfUnchanged(row).catch((error) => {
320
250
  process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
321
251
  });
322
- }
323
252
  return stored;
324
253
  };
325
- const stored = fenced
326
- ? await hostLedger.withClaimLock(commitAndCleanup, control?.signal)
327
- : await commitAndCleanup();
254
+ const stored = hostLedger === undefined ? await commitAndCleanup() : await hostLedger.withClaimLock(commitAndCleanup, control?.signal);
328
255
  return { activity: toPublicActivity(stored) };
329
256
  }
330
257
  export function done(square, name, body = '', fence = {}, control) { return landCore(square, 'done', name, body, fence, control); }
331
258
  function liveClaimedParticipants(rows, sessionId) {
332
- const claimed = new Set();
333
- const foreign = new Set();
334
- for (const row of rows) {
335
- const key = nameKey(row.participant);
336
- if (row.session === sessionId)
337
- claimed.add(key);
338
- else
339
- foreign.add(key);
340
- }
259
+ const claimed = new Set(), foreign = new Set();
260
+ for (const row of rows)
261
+ (row.session === sessionId ? claimed : foreign).add(nameKey(row.participant));
341
262
  for (const key of foreign)
342
263
  claimed.delete(key);
343
264
  return claimed;
@@ -350,30 +271,19 @@ async function readLiveClaimedParticipants(hostLedger, location, sessionId, now)
350
271
  return undefined;
351
272
  }
352
273
  }
353
- /**
354
- * End one session's ownership of a participant inside the ownership claim critical
355
- * section. The artifact `done` (when this session is still the owner) and retirement of
356
- * this session's routes share one transaction, and the exact captured presence row is
357
- * removed only while the lock is still held. A rejoin or takeover therefore lands either
358
- * before the whole cleanup — and is part of the state being ended — or after it, with
359
- * fresh presence and route rows that this cleanup can never touch.
360
- */
361
274
  export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
362
- const location = square.location;
363
- const hostLedger = square.hostLedger;
275
+ const { location, hostLedger } = square;
364
276
  const run = async () => {
365
277
  const now = square.clock();
366
- let currentSessionId;
367
- let ownerMatches = expectedEpoch === undefined;
278
+ let currentSessionId, ownerMatches = expectedEpoch === undefined;
368
279
  let ownerRecords = [];
369
280
  let liveParticipants = new Set();
370
281
  if (location !== undefined && location !== 'memory' && hostLedger !== undefined) {
371
282
  try {
372
283
  const rows = await hostLedger.listPresence({ location, now });
373
284
  const bindings = rows.filter((row) => nameKey(row.participant) === nameKey(name));
374
- currentSessionId = bindings.find((binding) => binding.session !== sessionId)?.session
375
- ?? bindings.toSorted((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0]?.session;
376
- ownerRecords = rows.filter((row) => row.session === sessionId && nameKey(row.participant) === nameKey(name));
285
+ currentSessionId = bindings.find((row) => row.session !== sessionId)?.session ?? bindings.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0]?.session;
286
+ ownerRecords = bindings.filter((row) => row.session === sessionId);
377
287
  liveParticipants = liveClaimedParticipants(rows, sessionId);
378
288
  if (expectedEpoch !== undefined) {
379
289
  const owner = await readParticipantOwner(location, name, hostLedger);
@@ -381,55 +291,41 @@ export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
381
291
  }
382
292
  }
383
293
  catch {
384
- liveParticipants = undefined; /* unknown evidence preserves routes; artifact routes still fence ownership */
294
+ liveParticipants = undefined;
385
295
  }
386
296
  }
387
- const stored = await square.artifact.transact((state) => {
297
+ const stored = (await square.artifact.transact((facts) => {
388
298
  let committed = null;
389
- if (location !== undefined && location !== 'memory' && ownerMatches && sessionCanEndParticipant(state, location, name, sessionId, currentSessionId)) {
299
+ if (location !== undefined && location !== 'memory' && ownerMatches && sessionCanEndParticipant(facts, location, name, sessionId, currentSessionId)) {
390
300
  try {
391
- committed = committedActivity(storeActs(state, [coreDone(state, name, '', now)]), 'done');
301
+ committed = facts.land(coreDone(facts, name, '', now));
392
302
  }
393
303
  catch (error) {
394
304
  if (!isSquareError(error) || (error.code !== 'already_done' && error.code !== 'not_joined'))
395
305
  throw error;
396
306
  }
397
307
  }
398
- if (location !== undefined && location !== 'memory') {
399
- dropEndedSessionWakeRoutesFromState(state, location, sessionId, { expectedEpoch, liveParticipants, force: committed !== null, participant: name });
400
- }
401
- return { state, result: committed };
402
- });
403
- if (stored !== null) {
404
- for (const ownerRecord of ownerRecords) {
405
- await hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
308
+ if (location !== undefined && location !== 'memory')
309
+ dropEndedSessionWakeRoutes(facts, location, sessionId, { expectedEpoch, liveParticipants, force: committed !== null, participant: name });
310
+ return committed;
311
+ })).value;
312
+ if (stored !== null)
313
+ for (const row of ownerRecords)
314
+ await hostLedger.removePresenceIfUnchanged(row).catch((error) => {
406
315
  process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
407
316
  });
408
- }
409
- }
410
317
  return stored;
411
318
  };
412
- const stored = hostLedger !== undefined
413
- ? await hostLedger.withClaimLock(run)
414
- : await run();
319
+ const stored = hostLedger === undefined ? await run() : await hostLedger.withClaimLock(run);
415
320
  return stored === null ? null : { activity: toPublicActivity(stored) };
416
321
  }
417
- /**
418
- * Retire an ended session's orphan routes when no participant name is known. The
419
- * ownership claim lock is held across the ledger read and the artifact transaction, so a
420
- * rejoin that claims first is seen as a live participant and its route survives.
421
- */
422
322
  export async function retireEndedSessionRoutes(square, sessionId) {
423
- const location = square.location;
424
- const hostLedger = square.hostLedger;
323
+ const { location, hostLedger } = square;
425
324
  if (location === undefined || hostLedger === undefined)
426
325
  return;
427
326
  await hostLedger.withClaimLock(async () => {
428
327
  const liveParticipants = await readLiveClaimedParticipants(hostLedger, location, sessionId, square.clock());
429
- await square.artifact.transact((state) => {
430
- dropEndedSessionWakeRoutesFromState(state, location, sessionId, { liveParticipants, force: false });
431
- return { state, result: undefined };
432
- });
328
+ await square.artifact.transact((facts) => { dropEndedSessionWakeRoutes(facts, location, sessionId, { liveParticipants, force: false }); });
433
329
  });
434
330
  }
435
331
  export function hold(square, name, reason = '', control) { return landCore(square, 'hold', name, reason, {}, control); }