@astrosheep/square 0.3.63 → 0.3.65

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 (171) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +3 -2
  2. package/claude-plugin/hooks/hooks.json +1 -22
  3. package/claude-plugin/hooks/register.js +119 -0
  4. package/claude-plugin/types.d.ts +7 -0
  5. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  6. package/dist/activity-feed.d.ts +1 -10
  7. package/dist/activity-feed.js +1 -27
  8. package/dist/activity.js +57 -25
  9. package/dist/artifact.d.ts +1 -1
  10. package/dist/artifact.js +24 -25
  11. package/dist/attention-presentation.js +1 -0
  12. package/dist/automatic-session.d.ts +3 -1
  13. package/dist/automatic-session.js +21 -34
  14. package/dist/boundary-presentation.js +4 -13
  15. package/dist/canonical-path.d.ts +11 -0
  16. package/dist/canonical-path.js +47 -0
  17. package/dist/catch-decisions.js +14 -10
  18. package/dist/catch-selection.d.ts +6 -0
  19. package/dist/catch-selection.js +14 -0
  20. package/dist/claude-delivery.d.ts +11 -0
  21. package/dist/claude-delivery.js +145 -0
  22. package/dist/claude-mod.d.ts +8 -0
  23. package/dist/claude-mod.js +164 -0
  24. package/dist/cli/context.d.ts +0 -1
  25. package/dist/cli/context.js +9 -21
  26. package/dist/cli/harness-command.d.ts +0 -1
  27. package/dist/cli/harness-command.js +0 -6
  28. package/dist/cli/observation-commands.d.ts +1 -1
  29. package/dist/cli/observation-commands.js +31 -58
  30. package/dist/cli/program.js +20 -18
  31. package/dist/cli/registry.js +2 -2
  32. package/dist/cli/square-commands.js +10 -6
  33. package/dist/codex-boundary-state.d.ts +2 -2
  34. package/dist/codex-boundary-state.js +4 -4
  35. package/dist/codex-hook.js +12 -15
  36. package/dist/codex-queue.d.ts +2 -2
  37. package/dist/codex-queue.js +7 -5
  38. package/dist/decisions.d.ts +2 -6
  39. package/dist/decisions.js +16 -17
  40. package/dist/delivery-health.d.ts +1 -2
  41. package/dist/delivery-health.js +4 -5
  42. package/dist/delivery-operations.d.ts +2 -5
  43. package/dist/delivery-operations.js +77 -214
  44. package/dist/delivery.d.ts +25 -47
  45. package/dist/delivery.js +14 -46
  46. package/dist/diagnostic-redaction.js +7 -9
  47. package/dist/file-changes.d.ts +0 -1
  48. package/dist/file-changes.js +2 -19
  49. package/dist/harness-claude.js +8 -6
  50. package/dist/harness-codex.d.ts +0 -2
  51. package/dist/harness-codex.js +0 -2
  52. package/dist/harness-links.js +13 -17
  53. package/dist/harness.js +3 -7
  54. package/dist/help.js +4 -3
  55. package/dist/host-ledger-file-adapter.d.ts +17 -8
  56. package/dist/host-ledger-file-adapter.js +164 -80
  57. package/dist/host-ledger.d.ts +55 -50
  58. package/dist/inbox.d.ts +0 -3
  59. package/dist/inbox.js +88 -50
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/list.js +1 -4
  63. package/dist/mcp.js +24 -41
  64. package/dist/mention-parse.js +3 -9
  65. package/dist/model.d.ts +11 -2
  66. package/dist/model.js +1 -1
  67. package/dist/notifications.d.ts +3 -16
  68. package/dist/notifications.js +41 -122
  69. package/dist/opencode-delivery.d.ts +14 -0
  70. package/dist/opencode-delivery.js +171 -0
  71. package/dist/opencode.d.ts +39 -18
  72. package/dist/opencode.js +145 -40
  73. package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
  74. package/dist/packages/agent-delivery/src/claude-auth.js +75 -0
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +68 -0
  77. package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
  78. package/dist/packages/agent-delivery/src/claude.js +66 -0
  79. package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
  80. package/dist/packages/agent-delivery/src/deadline.js +41 -0
  81. package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
  82. package/dist/packages/agent-delivery/src/errors.js +41 -0
  83. package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
  84. package/dist/packages/agent-delivery/src/guards.js +6 -0
  85. package/dist/packages/agent-delivery/src/index.d.ts +83 -0
  86. package/dist/packages/agent-delivery/src/index.js +110 -0
  87. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
  88. package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
  89. package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
  90. package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
  91. package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
  92. package/dist/packages/agent-delivery/src/opencode.js +127 -0
  93. package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
  94. package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
  95. package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
  96. package/dist/packages/agent-delivery/src/paseo.js +106 -0
  97. package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
  98. package/dist/packages/agent-delivery/src/pi-client.js +106 -0
  99. package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
  100. package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
  101. package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
  102. package/dist/packages/agent-delivery/src/pi-send.js +4 -0
  103. package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
  104. package/dist/packages/agent-delivery/src/pi.js +209 -0
  105. package/dist/packages/agent-delivery/src/types.d.ts +120 -0
  106. package/dist/participant-host.js +12 -13
  107. package/dist/participant-identity.d.ts +21 -0
  108. package/dist/participant-identity.js +32 -11
  109. package/dist/paseo-delivery.d.ts +2 -2
  110. package/dist/paseo-delivery.js +35 -27
  111. package/dist/paseo-timeline.js +25 -21
  112. package/dist/paseo.d.ts +2 -2
  113. package/dist/paseo.js +1 -2
  114. package/dist/ports.d.ts +7 -20
  115. package/dist/presentation-operations.js +16 -4
  116. package/dist/presentation.d.ts +19 -10
  117. package/dist/presentation.js +99 -177
  118. package/dist/registry.d.ts +15 -66
  119. package/dist/registry.js +45 -123
  120. package/dist/routes.d.ts +1 -28
  121. package/dist/routes.js +22 -104
  122. package/dist/runtime.d.ts +3 -9
  123. package/dist/runtime.js +14 -67
  124. package/dist/search.d.ts +0 -2
  125. package/dist/search.js +3 -10
  126. package/dist/square-actions.js +48 -72
  127. package/dist/square-application.js +32 -30
  128. package/dist/square-core.d.ts +3 -0
  129. package/dist/square-core.js +22 -5
  130. package/dist/square-facade.d.ts +2 -0
  131. package/dist/square-file-adapter.d.ts +2 -1
  132. package/dist/square-file-adapter.js +6 -9
  133. package/dist/square-projections.d.ts +40 -45
  134. package/dist/square-projections.js +60 -74
  135. package/dist/square-state.js +1 -4
  136. package/dist/square-storage.d.ts +5 -8
  137. package/dist/square-storage.js +13 -36
  138. package/dist/square-wiring.d.ts +2 -7
  139. package/dist/square-wiring.js +6 -24
  140. package/dist/stream.js +4 -6
  141. package/dist/time.js +2 -7
  142. package/dist/views.d.ts +6 -18
  143. package/dist/views.js +14 -21
  144. package/dist/wake-attempts.d.ts +2 -56
  145. package/dist/wake-attempts.js +15 -110
  146. package/dist/wake-eligibility.d.ts +43 -0
  147. package/dist/wake-eligibility.js +106 -0
  148. package/dist/wake-evidence.d.ts +32 -9
  149. package/dist/wake-evidence.js +29 -24
  150. package/dist/wake-sink.d.ts +13 -9
  151. package/dist/wake-sink.js +35 -54
  152. package/dist/watch.js +13 -21
  153. package/extensions/square-pi.js +27 -13
  154. package/guides/change-waits.md +1 -1
  155. package/guides/claude-native.md +101 -0
  156. package/guides/host-ledger.md +3 -1
  157. package/guides/participant.md +14 -7
  158. package/guides/pi-native-delivery.md +49 -0
  159. package/package.json +4 -3
  160. package/dist/claude-hook.d.ts +0 -10
  161. package/dist/claude-hook.js +0 -60
  162. package/dist/paseo-connection.d.ts +0 -15
  163. package/dist/paseo-connection.js +0 -141
  164. package/dist/presence.d.ts +0 -2
  165. package/dist/presence.js +0 -24
  166. package/dist/presented.d.ts +0 -2
  167. package/dist/presented.js +0 -10
  168. package/dist/state-cell.d.ts +0 -14
  169. package/dist/wake-port.d.ts +0 -23
  170. package/dist/wake-port.js +0 -27
  171. /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
package/dist/runtime.js CHANGED
@@ -1,43 +1,19 @@
1
1
  import { audienceIncludes, audienceOf, fold, formatActivityId, replayLandedAudiences } from './square-core.js';
2
2
  import { SquareError, findParticipantName, nameKey, sameName, } from './model.js';
3
- function parseIntegerEnvValue(name, raw, fallback) {
4
- if (raw === undefined)
5
- return fallback;
6
- const value = parseInt(raw, 10);
7
- if (!Number.isFinite(value) || value <= 0) {
8
- throw new SquareError('invalid_args', `Invalid ${name}: expected a positive integer.`);
9
- }
10
- return value;
11
- }
12
3
  function parseIntegerEnv(name, fallback) {
13
- return parseIntegerEnvValue(name, process.env[name], fallback);
14
- }
15
- function parseIntegerEnvAlias(primary, aliases, fallback) {
16
- const candidates = [primary, ...aliases];
17
- for (const name of candidates) {
18
- if (process.env[name] !== undefined)
19
- return parseIntegerEnvValue(name, process.env[name], fallback);
20
- }
21
- return fallback;
22
- }
23
- function parseScaleEnv(name, fallback) {
24
4
  const raw = process.env[name];
25
5
  if (raw === undefined)
26
6
  return fallback;
27
- const value = parseFloat(raw);
7
+ const value = parseInt(raw, 10);
28
8
  if (!Number.isFinite(value) || value <= 0) {
29
- throw new SquareError('invalid_args', `Invalid ${name}: expected a positive number.`);
9
+ throw new SquareError('invalid_args', `Invalid ${name}: expected a positive integer.`);
30
10
  }
31
11
  return value;
32
12
  }
33
- function scaledMs(baseMs, scale) {
34
- return Math.max(1, Math.round(baseMs * scale));
35
- }
36
13
  export const WATCH_HEARTBEAT_MS = parseIntegerEnv('SQUARE_WATCH_HEARTBEAT_MS', 60000);
37
- export const SLEEP_MS = parseIntegerEnvAlias('SQUARE_WATCH_POLL_MS', ['SQUARE_SLEEP_MS'], scaledMs(WATCH_HEARTBEAT_MS, parseScaleEnv('SQUARE_WATCH_POLL_SCALE', 1 / 6)));
38
- export const STALE_MS = parseIntegerEnvAlias('SQUARE_WATCH_QUIET_MS', ['SQUARE_STALE_MS'], scaledMs(WATCH_HEARTBEAT_MS, parseScaleEnv('SQUARE_WATCH_QUIET_SCALE', 5)));
39
- export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', scaledMs(WATCH_HEARTBEAT_MS, parseScaleEnv('SQUARE_WATCH_STALE_SCALE', 3)));
40
- export const LOCK_RETRY_MS = parseIntegerEnv('SQUARE_LOCK_RETRY_MS', 25);
14
+ export const SLEEP_MS = parseIntegerEnv('SQUARE_WATCH_POLL_MS', 10000);
15
+ export const STALE_MS = parseIntegerEnv('SQUARE_WATCH_QUIET_MS', 300000);
16
+ export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', 180000);
41
17
  export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
42
18
  export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
43
19
  /** A held express waits for the hand to lower, but not forever: past this budget it gives up exactly like --no-wait (draft saved, retry printed). 0 disables waiting. */
@@ -71,10 +47,6 @@ export function inSquareCount(squareState) {
71
47
  export function resolveRosterName(squareState, name) {
72
48
  return findParticipantName(rosterNames(squareState), name);
73
49
  }
74
- export function hasQuorum(squareState, name, outs) {
75
- const peers = rosterNames(squareState).filter((participant) => !sameName(participant, name));
76
- return peers.length > 0 && peers.every((peer) => outs.has(nameKey(peer)));
77
- }
78
50
  export function countSays(acts, name) {
79
51
  return acts.filter((act) => act.kind === 'say' && sameName(act.actor, name)).length;
80
52
  }
@@ -96,12 +68,10 @@ export function sayNumberFor(acts, target) {
96
68
  export function watchTerminalStatus(squareState, name) {
97
69
  if (squareState.hardCap !== null && countSays(squareState.acts, name) >= squareState.hardCap)
98
70
  return 'capped';
99
- const done = doneNames(squareState.acts);
71
+ const done = new Set(fold(squareState.acts).done.map((participant) => nameKey(participant)));
100
72
  done.delete(nameKey(name));
101
- return hasQuorum(squareState, name, done) ? 'quorum' : undefined;
102
- }
103
- export function doneNames(acts) {
104
- return new Set(fold(acts).done.map((participant) => nameKey(participant)));
73
+ const peers = rosterNames(squareState).filter((participant) => !sameName(participant, name));
74
+ return peers.length > 0 && peers.every((peer) => done.has(nameKey(peer))) ? 'quorum' : undefined;
105
75
  }
106
76
  export function joinedNames(acts) {
107
77
  return new Set(acts.filter((act) => act.kind === 'join').map((act) => nameKey(act.actor)));
@@ -127,13 +97,9 @@ export function actId(actOrIndex) {
127
97
  const index = typeof actOrIndex === 'number' ? actOrIndex : actStableIndex(actOrIndex);
128
98
  return formatActivityId(index);
129
99
  }
130
- function observationRecipient(squareState, name) {
100
+ function canonicalRuntimeName(squareState, name) {
131
101
  return resolveRosterName(squareState, name) ?? name;
132
102
  }
133
- function observationMap(squareState, name) {
134
- const key = observationRecipient(squareState, name);
135
- return squareState.runtime.observations?.[key] ?? {};
136
- }
137
103
  export function currentHold(acts) {
138
104
  const hold = fold(acts).hold;
139
105
  return hold.active ? { active: true, at: hold.at, reason: hold.reason } : { active: false };
@@ -141,16 +107,10 @@ export function currentHold(acts) {
141
107
  export function publicActs(acts) {
142
108
  return acts.filter((act) => act.kind === 'say' || act.kind === 'done');
143
109
  }
144
- function canonicalRuntimeName(squareState, name) {
145
- return resolveRosterName(squareState, name) ?? name;
146
- }
147
- export function observationFor(squareState, name, index) {
148
- return observationMap(squareState, name)[formatActivityId(index)];
149
- }
150
110
  export function recordObservation(squareState, name, index, state, at = Date.now()) {
151
111
  if (!Number.isSafeInteger(index) || index < 0 || !Number.isFinite(at))
152
112
  return false;
153
- const key = observationRecipient(squareState, name);
113
+ const key = canonicalRuntimeName(squareState, name);
154
114
  const id = formatActivityId(index);
155
115
  const current = squareState.runtime.observations[key]?.[id];
156
116
  if (current?.state === 'seen' || (current?.state === state && current.at >= at))
@@ -183,15 +143,14 @@ export function readCursor(squareState, name, landed = replayLandedAudiences(squ
183
143
  }
184
144
  return cursor;
185
145
  }
186
- export function latestActIndex(acts) {
187
- return acts.reduce((max, act) => Math.max(max, act.index), -1);
188
- }
189
146
  /**
147
+ * The last public act the participant was verifiably present for, or -1 when there is no evidence.
148
+ *
190
149
  * "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
191
150
  * and seen receipts after their latest join. Anything else — in particular acts merely
192
151
  * not addressed to them — says nothing about where they actually were.
193
152
  */
194
- export function presenceEvidenceCursor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
153
+ export function presenceAnchor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
195
154
  const recipient = landed.resolveParticipant(name) ?? name;
196
155
  const boundary = landed.lastJoinIndex(recipient) ?? -1;
197
156
  if (boundary < 0)
@@ -205,15 +164,6 @@ export function presenceEvidenceCursor(squareState, name, landed = replayLandedA
205
164
  evidence = act.index;
206
165
  }
207
166
  }
208
- return evidence;
209
- }
210
- /** The last public act the participant was verifiably present for, or -1 when there is no evidence. */
211
- export function presenceAnchor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
212
- const recipient = landed.resolveParticipant(name) ?? name;
213
- const boundary = landed.lastJoinIndex(recipient) ?? -1;
214
- if (boundary < 0)
215
- return -1;
216
- const evidence = presenceEvidenceCursor(squareState, recipient, landed);
217
167
  for (let i = squareState.acts.length - 1; i >= 0; i--) {
218
168
  const event = squareState.acts[i];
219
169
  if (event.index <= boundary || event.index > evidence)
@@ -225,14 +175,11 @@ export function presenceAnchor(squareState, name, landed = replayLandedAudiences
225
175
  }
226
176
  export function freshWatchLease(squareState, name, at = Date.now()) {
227
177
  const key = canonicalRuntimeName(squareState, name);
228
- const lease = watchLease(squareState, key);
178
+ const lease = squareState.runtime.leases[key];
229
179
  if (lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS)
230
180
  return undefined;
231
181
  return lease;
232
182
  }
233
- export function watchLease(squareState, name) {
234
- return squareState.runtime.leases[canonicalRuntimeName(squareState, name)];
235
- }
236
183
  export function writeWatchLease(squareState, name, lease) {
237
184
  squareState.runtime.leases[canonicalRuntimeName(squareState, name)] = lease;
238
185
  }
package/dist/search.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- export declare function compileGrepPattern(pattern: string): RegExp;
2
- export declare function compileFixedPattern(pattern: string): RegExp;
3
1
  export declare function compileSearchPattern(pattern: string, fixed: boolean): RegExp;
4
2
  export interface GrepSnippet {
5
3
  before: string;
package/dist/search.js CHANGED
@@ -3,10 +3,9 @@
3
3
  // the local CLI user and are not treated as a hostile multi-tenant input. Use
4
4
  // --fixed for literal text; do not claim regex mode has ReDoS protection.
5
5
  import { SquareError } from './model.js';
6
- function escapeRegex(pattern) {
7
- return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
- }
9
- export function compileGrepPattern(pattern) {
6
+ export function compileSearchPattern(pattern, fixed) {
7
+ if (fixed)
8
+ return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
10
9
  try {
11
10
  return new RegExp(pattern, 'i');
12
11
  }
@@ -15,12 +14,6 @@ export function compileGrepPattern(pattern) {
15
14
  throw new SquareError('invalid_args', `Invalid --grep regex: ${detail}`);
16
15
  }
17
16
  }
18
- export function compileFixedPattern(pattern) {
19
- return new RegExp(escapeRegex(pattern), 'i');
20
- }
21
- export function compileSearchPattern(pattern, fixed) {
22
- return fixed ? compileFixedPattern(pattern) : compileGrepPattern(pattern);
23
- }
24
17
  function compactWhitespace(text) {
25
18
  return text.replace(/\s+/g, ' ');
26
19
  }
@@ -1,25 +1,16 @@
1
- import { formatActivityId, parseActivityId } from './square-core.js';
2
1
  import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, decideAct, decideImplicitJoin, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
3
2
  import { isSquareError, nameKey, SquareError, validateName } from './model.js';
4
3
  import { participantIdentity } from './participant-identity.js';
5
4
  import { deliverPending } from './delivery-operations.js';
6
5
  import { decideCatch } from './catch-decisions.js';
7
- import { claimSessionParticipant, claimSessionTakeover, releaseSessionParticipantClaim, readParticipantOwner, withOwnershipClaimLock } from './registry.js';
6
+ import { claimSessionParticipant, claimSessionTakeover, presenceEpoch, releaseSessionParticipantClaim, readParticipantOwner } from './registry.js';
8
7
  import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishIdentityRoute, retireIdentityRoute } from './participant-host.js';
9
8
  import { applyWakeRouteToState, dropEndedSessionWakeRoutesFromState, dropParticipantWakeRoutesFromState, dropSessionWakeRoutesFromState, sessionCanEndParticipant } from './routes.js';
9
+ import { parseRequiredActivityId, toPublicActivity } from './views.js';
10
10
  function throwIfAborted(control) { if (control?.signal?.aborted)
11
11
  throw control.signal.reason ?? new Error('Operation aborted'); }
12
12
  function exposeCaught(activity, perception) {
13
- if (activity.kind === 'read' || activity.actor === undefined)
14
- throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
15
- const result = {
16
- id: formatActivityId(activity.index), at: activity.at, kind: activity.kind, actor: activity.actor,
17
- mentions: activity.kind === 'say' ? activity.mentions ?? [] : [],
18
- ...(activity.kind === 'say' && activity.reach !== undefined ? { reach: activity.reach } : {}),
19
- ...('body' in activity && activity.body !== undefined ? { body: activity.body } : {}),
20
- ...('target' in activity ? { target: activity.target } : {}),
21
- ...(activity.kind === 'say' && activity.reply !== undefined ? { reply: formatActivityId(activity.reply) } : {}),
22
- };
13
+ const result = toPublicActivity(activity);
23
14
  if (perception === 'full' || !('body' in result))
24
15
  return { ...result, perception };
25
16
  const { body: _body, ...withoutBody } = result;
@@ -82,42 +73,25 @@ function committedActivity(stored, verb) {
82
73
  throw new Error(`${verb} activity did not commit`);
83
74
  return activity;
84
75
  }
85
- function exposeActivity(stored) {
86
- if (stored.kind === 'read' || stored.actor === undefined)
87
- throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
88
- return {
89
- id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
90
- ...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
91
- mentions: stored.kind === 'say' ? stored.mentions ?? [] : [],
92
- ...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}),
93
- ...('target' in stored ? { target: stored.target } : {}),
94
- ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}),
95
- };
96
- }
97
- function parseRequiredActivityId(id) {
98
- const index = parseActivityId(id);
99
- if (index === undefined)
100
- throw new SquareError('invalid_args', `Invalid activity id: ${id}`);
101
- return index;
102
- }
103
76
  export async function join(square, name, control) {
104
77
  // Rejected validation must not perform an ownership claim.
105
78
  throwIfAborted(control);
106
79
  validateName(name);
80
+ const hostLedger = square.hostLedger;
107
81
  const preview = await square.artifact.read(control?.signal);
108
82
  const previewDecision = decideJoin(preview.state, name, square.clock());
109
83
  if (previewDecision.joinAct === undefined) {
110
84
  // An active artifact participant is idempotent only for its current session.
111
85
  // A different owner must still pass through the registry CAS so it is refused
112
86
  // instead of silently reconnecting as a foreign session.
113
- if (square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory') {
87
+ if (hostLedger !== undefined) {
114
88
  const identity = processIdentity(square.env ?? process.env);
115
- const owner = await readParticipantOwner(square.location, previewDecision.joinedName, square.env ?? process.env);
89
+ const owner = await readParticipantOwner(square.location, previewDecision.joinedName, hostLedger);
116
90
  if (owner === undefined) {
117
91
  // Keep the claim path for an active artifact whose ledger owner is not
118
92
  // visible yet; concurrent callers must still serialize through CAS.
119
93
  }
120
- else if (owner.sessionId !== identity.session) {
94
+ else if (owner.session !== identity.session) {
121
95
  // Continue into the ownership claim below; it will produce the stable
122
96
  // already_joined error without mutating the artifact.
123
97
  }
@@ -131,8 +105,8 @@ export async function join(square, name, control) {
131
105
  }
132
106
  let epoch;
133
107
  let ownershipClaim;
134
- if (square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory') {
135
- const claim = await claimSessionParticipant(square.location, name, square.env ?? process.env, control?.signal);
108
+ if (hostLedger !== undefined) {
109
+ const claim = await claimSessionParticipant(square.location, name, hostLedger, square.env ?? process.env, control?.signal);
136
110
  ownershipClaim = claim;
137
111
  epoch = claim?.epoch;
138
112
  }
@@ -154,18 +128,20 @@ export async function join(square, name, control) {
154
128
  }, control?.signal);
155
129
  }
156
130
  catch (error) {
157
- await releaseSessionParticipantClaim(square.location, name, square.env ?? process.env, ownershipClaim).catch(() => undefined);
131
+ if (hostLedger !== undefined)
132
+ await releaseSessionParticipantClaim(hostLedger, ownershipClaim).catch(() => undefined);
158
133
  throw error;
159
134
  }
160
135
  await ensureLocalPresence(square, committed.name, epoch);
161
136
  await publishIdentityRoute(square, committed.name, epoch);
162
- return { name: committed.name, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
137
+ return { name: committed.name, activity: committed.stored === null ? null : toPublicActivity(committed.stored) };
163
138
  }
164
139
  /** End the standing participant and immediately let the caller reclaim the name. */
165
140
  export async function takeover(square, name, _oldSessionIds = [], control) {
166
141
  // Rejected validation must not perform an ownership claim.
167
142
  throwIfAborted(control);
168
143
  validateName(name);
144
+ const hostLedger = square.hostLedger;
169
145
  const commitLifecycle = async () => {
170
146
  throwIfAborted(control);
171
147
  const now = square.clock();
@@ -182,7 +158,7 @@ export async function takeover(square, name, _oldSessionIds = [], control) {
182
158
  }, control?.signal);
183
159
  return committed;
184
160
  };
185
- if (square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory') {
161
+ if (hostLedger !== undefined) {
186
162
  const env = square.env ?? process.env;
187
163
  // Gate the ownership claim on the artifact's authoritative state: a takeover that cannot
188
164
  // commit its lifecycle (a name that never joined, or a standing participant that is no
@@ -190,13 +166,13 @@ export async function takeover(square, name, _oldSessionIds = [], control) {
190
166
  const { state } = await square.artifact.read(control?.signal);
191
167
  const joinedName = resolveKnownName(state, name); // invalid_args when the name never joined
192
168
  coreDone(state, joinedName, '', square.clock()); // already_done/not_joined when not standing
193
- const owner = await readParticipantOwner(square.location, name, env);
169
+ const owner = await readParticipantOwner(square.location, name, hostLedger);
194
170
  throwIfAborted(control);
195
171
  // The claim and the artifact lifecycle are one fenced critical section: a losing or refused
196
172
  // takeover never mutates the winner, and no second takeover can interleave mid-commit.
197
- const outcome = await claimSessionTakeover(square.location, name, env, {
198
- expectedEpoch: owner?.epoch ?? 0,
199
- expectedSession: owner?.sessionId ?? '',
173
+ const outcome = await claimSessionTakeover(square.location, name, hostLedger, env, {
174
+ expectedEpoch: presenceEpoch(owner),
175
+ expectedSession: owner?.session ?? '',
200
176
  }, async (claim) => {
201
177
  throwIfAborted(control);
202
178
  const committed = await commitLifecycle();
@@ -206,12 +182,12 @@ export async function takeover(square, name, _oldSessionIds = [], control) {
206
182
  }, control?.signal);
207
183
  if (outcome.status === 'busy')
208
184
  throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`);
209
- return { name: outcome.result.name, activities: outcome.result.stored.map(exposeActivity), epoch: outcome.epoch };
185
+ return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), epoch: outcome.epoch };
210
186
  }
211
187
  const committed = await commitLifecycle();
212
188
  await ensureLocalPresence(square, committed.name);
213
189
  await publishIdentityRoute(square, committed.name);
214
- return { name: committed.name, activities: committed.stored.map(exposeActivity) };
190
+ return { name: committed.name, activities: committed.stored.map(toPublicActivity) };
215
191
  }
216
192
  export async function implicitJoin(square, name, control) {
217
193
  throwIfAborted(control);
@@ -238,7 +214,7 @@ export async function implicitJoin(square, name, control) {
238
214
  await retireIdentityRoute(square, committed.name);
239
215
  else if (committed.stored !== null)
240
216
  await publishIdentityRoute(square, committed.name);
241
- return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
217
+ return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : toPublicActivity(committed.stored) };
242
218
  }
243
219
  /** Only admission failures can advertise a safe retry; storage and post-commit
244
220
  * failures must not be mistaken for a rejected send merely by their error code. */
@@ -265,7 +241,7 @@ export async function express(square, name, body, options = {}, control) {
265
241
  const decision = rejectUnsentActivity(() => {
266
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 }) });
267
243
  if (decision.type === 'blocked') {
268
- const pending = decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) + decision.unreadRoomChanges.length;
244
+ const pending = decision.activitySummaries.reduce((count, summary) => count + summary.count, 0);
269
245
  throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending });
270
246
  }
271
247
  if (decision.type === 'held') {
@@ -284,7 +260,7 @@ export async function express(square, name, body, options = {}, control) {
284
260
  return decision;
285
261
  });
286
262
  const stored = committedActivity(storeActs(state, [decision.act]), 'express');
287
- return { state, result: { stored } };
263
+ return { state, result: { stored, ...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }) } };
288
264
  }, control?.signal);
289
265
  await ensureLocalPresence(square, name);
290
266
  await publishIdentityRoute(square, name);
@@ -295,7 +271,7 @@ export async function express(square, name, body, options = {}, control) {
295
271
  else {
296
272
  delivery = { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
297
273
  }
298
- return { activity: exposeActivity(committed.stored), delivery };
274
+ return { activity: toPublicActivity(committed.stored), delivery, ...(committed.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: committed.replyAuthorNotHere }) };
299
275
  }
300
276
  async function landListenerChange(square, verb, actor, target, control) {
301
277
  throwIfAborted(control);
@@ -306,7 +282,7 @@ async function landListenerChange(square, verb, actor, target, control) {
306
282
  return { result: null };
307
283
  return { state, result: committedActivity(storeActs(state, [act]), verb) };
308
284
  }, control?.signal);
309
- return { activity: stored === null ? null : exposeActivity(stored) };
285
+ return { activity: stored === null ? null : toPublicActivity(stored) };
310
286
  }
311
287
  export function listen(square, actor, target, control) { return landListenerChange(square, 'listen', actor, target, control); }
312
288
  export function ignore(square, actor, target, control) { return landListenerChange(square, 'ignore', actor, target, control); }
@@ -318,7 +294,8 @@ async function landCore(square, verb, actor, body = '', fence = {}, control) {
318
294
  // session's route retirement, and presence cleanup share one ownership critical section.
319
295
  // A takeover finalizing between validation and commit can never be completed by a stale
320
296
  // done, and no post-lock cleanup can remove a replacement owner's fresh rows.
321
- const fenced = verb === 'done' && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory';
297
+ const hostLedger = verb === 'done' ? square.hostLedger : undefined;
298
+ const fenced = hostLedger !== undefined;
322
299
  const session = fenced ? processIdentity(square.env ?? process.env).session : undefined;
323
300
  const commitAndCleanup = async () => {
324
301
  if (verb === 'done' && !await assertLiveOwner(square, actor, fence.expectedEpoch)) {
@@ -326,7 +303,7 @@ async function landCore(square, verb, actor, body = '', fence = {}, control) {
326
303
  }
327
304
  let ownerRecords = [];
328
305
  if (fenced && session !== undefined) {
329
- ownerRecords = (await square.hostLedger.listPresence({ location: square.location, participant: actor }))
306
+ ownerRecords = (await hostLedger.listPresence({ location: square.location, participant: actor }))
330
307
  .filter((row) => row.session === session);
331
308
  }
332
309
  const now = square.clock();
@@ -339,16 +316,16 @@ async function landCore(square, verb, actor, body = '', fence = {}, control) {
339
316
  return { state, result };
340
317
  }, control?.signal);
341
318
  for (const ownerRecord of ownerRecords) {
342
- await square.hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
319
+ await hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
343
320
  process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
344
321
  });
345
322
  }
346
323
  return stored;
347
324
  };
348
325
  const stored = fenced
349
- ? await withOwnershipClaimLock(square.env ?? process.env, commitAndCleanup, control?.signal)
326
+ ? await hostLedger.withClaimLock(commitAndCleanup, control?.signal)
350
327
  : await commitAndCleanup();
351
- return { activity: exposeActivity(stored) };
328
+ return { activity: toPublicActivity(stored) };
352
329
  }
353
330
  export function done(square, name, body = '', fence = {}, control) { return landCore(square, 'done', name, body, fence, control); }
354
331
  function liveClaimedParticipants(rows, sessionId) {
@@ -365,11 +342,9 @@ function liveClaimedParticipants(rows, sessionId) {
365
342
  claimed.delete(key);
366
343
  return claimed;
367
344
  }
368
- async function readLiveClaimedParticipants(square, location, sessionId) {
369
- if (square.hostLedger === undefined)
370
- return new Set();
345
+ async function readLiveClaimedParticipants(hostLedger, location, sessionId, now) {
371
346
  try {
372
- return liveClaimedParticipants(await square.hostLedger.listPresence({ location, now: square.clock() }), sessionId);
347
+ return liveClaimedParticipants(await hostLedger.listPresence({ location, now }), sessionId);
373
348
  }
374
349
  catch {
375
350
  return undefined;
@@ -385,23 +360,24 @@ async function readLiveClaimedParticipants(square, location, sessionId) {
385
360
  */
386
361
  export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
387
362
  const location = square.location;
363
+ const hostLedger = square.hostLedger;
388
364
  const run = async () => {
389
365
  const now = square.clock();
390
366
  let currentSessionId;
391
367
  let ownerMatches = expectedEpoch === undefined;
392
368
  let ownerRecords = [];
393
369
  let liveParticipants = new Set();
394
- if (location !== undefined && location !== 'memory' && square.hostLedger !== undefined) {
370
+ if (location !== undefined && location !== 'memory' && hostLedger !== undefined) {
395
371
  try {
396
- const rows = await square.hostLedger.listPresence({ location, now });
372
+ const rows = await hostLedger.listPresence({ location, now });
397
373
  const bindings = rows.filter((row) => nameKey(row.participant) === nameKey(name));
398
374
  currentSessionId = bindings.find((binding) => binding.session !== sessionId)?.session
399
375
  ?? bindings.toSorted((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0]?.session;
400
- ownerRecords = rows.filter((row) => row.session === sessionId);
376
+ ownerRecords = rows.filter((row) => row.session === sessionId && nameKey(row.participant) === nameKey(name));
401
377
  liveParticipants = liveClaimedParticipants(rows, sessionId);
402
378
  if (expectedEpoch !== undefined) {
403
- const owner = await readParticipantOwner(location, name, square.env ?? process.env);
404
- ownerMatches = owner?.sessionId === sessionId && owner.epoch === expectedEpoch;
379
+ const owner = await readParticipantOwner(location, name, hostLedger);
380
+ ownerMatches = owner !== undefined && owner.session === sessionId && presenceEpoch(owner) === expectedEpoch;
405
381
  }
406
382
  }
407
383
  catch {
@@ -420,24 +396,23 @@ export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
420
396
  }
421
397
  }
422
398
  if (location !== undefined && location !== 'memory') {
423
- dropEndedSessionWakeRoutesFromState(state, location, sessionId, { expectedEpoch, liveParticipants, force: committed !== null });
399
+ dropEndedSessionWakeRoutesFromState(state, location, sessionId, { expectedEpoch, liveParticipants, force: committed !== null, participant: name });
424
400
  }
425
401
  return { state, result: committed };
426
402
  });
427
403
  if (stored !== null) {
428
404
  for (const ownerRecord of ownerRecords) {
429
- await square.hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
405
+ await hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
430
406
  process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
431
407
  });
432
408
  }
433
409
  }
434
410
  return stored;
435
411
  };
436
- const fenced = location !== undefined && location !== 'memory' && square.hostLedger !== undefined;
437
- const stored = fenced
438
- ? await withOwnershipClaimLock(square.env ?? process.env, run)
412
+ const stored = hostLedger !== undefined
413
+ ? await hostLedger.withClaimLock(run)
439
414
  : await run();
440
- return stored === null ? null : { activity: exposeActivity(stored) };
415
+ return stored === null ? null : { activity: toPublicActivity(stored) };
441
416
  }
442
417
  /**
443
418
  * Retire an ended session's orphan routes when no participant name is known. The
@@ -446,10 +421,11 @@ export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
446
421
  */
447
422
  export async function retireEndedSessionRoutes(square, sessionId) {
448
423
  const location = square.location;
449
- if (location === undefined || location === 'memory' || square.hostLedger === undefined)
424
+ const hostLedger = square.hostLedger;
425
+ if (location === undefined || hostLedger === undefined)
450
426
  return;
451
- await withOwnershipClaimLock(square.env ?? process.env, async () => {
452
- const liveParticipants = await readLiveClaimedParticipants(square, location, sessionId);
427
+ await hostLedger.withClaimLock(async () => {
428
+ const liveParticipants = await readLiveClaimedParticipants(hostLedger, location, sessionId, square.clock());
453
429
  await square.artifact.transact((state) => {
454
430
  dropEndedSessionWakeRoutesFromState(state, location, sessionId, { liveParticipants, force: false });
455
431
  return { state, result: undefined };