@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
package/dist/inbox.js CHANGED
@@ -1,78 +1,198 @@
1
- import { nameKey } from './model.js';
2
- import { createArtifactPort, observeSquareRevision } from './artifact.js';
3
- import { hostLedgerForEnv } from './registry.js';
1
+ import { sameBody } from './model.js';
2
+ import { createArtifactPort, observeSquareDeliveryRevision, observeSquareRevision } from './artifact.js';
3
+ import { indexedSquareEntries, observeSquareIndex } from './square-index.js';
4
4
  import { canonicalPath } from './canonical-path.js';
5
- import { projectBoundaryEligibility, projectSessionBindings } from './square-projections.js';
5
+ import { formatActivityId, parseActivityId } from './square-core.js';
6
6
  import { freshWatchLease, WATCH_STALE_MS } from './runtime.js';
7
7
  import { pendingHead, pendingNotifications, leaseOwnsNotification } from './delivery.js';
8
8
  import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
9
- function bindingCoordinate(binding) {
10
- return JSON.stringify([binding.location, nameKey(binding.participant), binding.sessionId, binding.channel, binding.epoch, binding.cancelledThrough ?? -1]);
9
+ export function pendingKey(membership, actIndex) {
10
+ return membership.location + '\0' + membership.seat.participant.toLocaleLowerCase() + '\0' + actIndex;
11
11
  }
12
- function notificationKey(membership, actIndex) {
13
- return membership.squarePath + '\0' + membership.name.toLocaleLowerCase() + '\0' + actIndex;
12
+ export function pendingAtBoundary(inbox) {
13
+ return inbox.map((membership) => {
14
+ const lease = membership.catchLease;
15
+ if (lease === undefined)
16
+ return membership;
17
+ return { ...membership, notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.seat.participant })) };
18
+ }).filter((membership) => membership.notifications.length > 0);
14
19
  }
15
- export function projectPresentation(facts, requested, now = Date.now()) {
16
- const participant = facts.participant(requested.participant);
17
- if (!participant?.joined)
18
- return { binding: requested, joined: false, notifications: [] };
19
- const binding = { ...requested, participant: participant.name };
20
- const notifications = pendingNotifications(facts, participant.name).map(({ item, route }) => ({
21
- actIndex: item.index, actor: item.actor, at: item.at, body: item.body, route,
22
- }));
23
- const catchLease = freshWatchLease(facts, participant.name, now);
24
- return { binding, joined: true, notifications, ...(catchLease === undefined ? {} : { catchLease }) };
20
+ function eligibleNotifications(facts, membership) {
21
+ const seat = facts.seat(membership.seat.participant);
22
+ if (seat === undefined || seat.generation !== membership.seat.generation
23
+ || !sameBody(seat.body, membership.seat.body))
24
+ return [];
25
+ const cancelledThrough = facts.cancelledThrough(seat.participant, seat.generation);
26
+ return membership.notifications.filter(notification => (cancelledThrough === undefined || notification.actIndex > cancelledThrough)
27
+ && !facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(notification.actIndex))
28
+ .some(row => row.kind === 'presentation' && (row.outcome === 'presented' || row.outcome === 'clipped')));
25
29
  }
26
- /** First matching ordered route, exact native epoch/address and catch ownership. */
27
- export function nativePendingPreview(facts, input) {
28
- const route = facts.routes().find((candidate) => candidate.kind === input.routeKind && candidate.sessionId === input.sessionId
29
- && candidate.epoch === input.epoch && nameKey(candidate.participant) === nameKey(input.participant)
30
- && (input.routeKind === 'claude-native' ? candidate.address.endpoint === input.address.endpoint : candidate.address.sessionId === input.address.sessionId));
31
- if (!route || input.actIndex <= (input.cancelledThrough ?? -1))
32
- return undefined;
33
- const head = pendingHead(facts, input.participant, input.actIndex);
34
- const lease = freshWatchLease(facts, input.participant, input.now);
35
- if (!head || (lease !== undefined && leaseOwnsNotification(lease, head)))
36
- return undefined;
37
- const item = facts.activity(input.actIndex);
38
- if (item?.kind !== 'say')
39
- return undefined;
40
- return { payload: renderSquareReminder([renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
41
- actor: item.actor, route: head.route, body: item.body })]), clipped: attentionBodyIsClipped(item.body) };
30
+ /** Revalidate a supplied batch immediately before the boundary's independent claim/send checks. */
31
+ export async function projectBoundaryEligibility(inbox) {
32
+ const grouped = new Map();
33
+ for (const membership of pendingAtBoundary(inbox)) {
34
+ const entries = grouped.get(membership.location) ?? [];
35
+ entries.push(membership);
36
+ grouped.set(membership.location, entries);
37
+ }
38
+ const pending = [];
39
+ for (const [location, memberships] of grouped) {
40
+ const artifact = createArtifactPort(location);
41
+ try {
42
+ const { value } = await artifact.read(facts => memberships.flatMap(membership => {
43
+ const notifications = eligibleNotifications(facts, membership);
44
+ return notifications.length === 0 ? [] : [{ ...membership, notifications }];
45
+ }));
46
+ pending.push(...value);
47
+ }
48
+ catch {
49
+ // An unreadable or stale candidate cannot authorize boundary presentation.
50
+ }
51
+ finally {
52
+ await artifact.close();
53
+ }
54
+ }
55
+ return pending;
42
56
  }
43
- async function readInboxMembership(binding, signal) {
44
- const artifact = createArtifactPort(binding.location, undefined, signal);
45
- try {
46
- return await artifact.read((facts) => {
47
- const projection = projectPresentation(facts, binding);
48
- if (!projection.joined)
49
- return undefined;
50
- return { name: projection.binding.participant, squarePath: projection.binding.location, notifications: [...projection.notifications],
51
- ...(projection.catchLease === undefined ? {} : { catchLease: projection.catchLease }) };
52
- }, signal);
57
+ /** HOME is a candidate path index; current seats and delivery evidence live in the artifact. */
58
+ export async function projectSessionCandidates(input) {
59
+ const location = input.location === undefined ? undefined : await canonicalPath(input.location);
60
+ const entries = await indexedSquareEntries({ body: input.body, session: input.sessionId, channel: input.channel, agent: input.agent }, input.env ?? process.env);
61
+ const candidates = new Map();
62
+ for (const entry of entries) {
63
+ if (location !== undefined && location !== entry.location)
64
+ continue;
65
+ const key = JSON.stringify([entry.location, entry.body.channel, entry.body.session, entry.body.agent ?? null]);
66
+ candidates.set(key, { location: entry.location, body: entry.body });
67
+ }
68
+ return [...candidates.values()];
69
+ }
70
+ function groupCandidates(candidates) {
71
+ const grouped = new Map();
72
+ for (const { location, body } of candidates) {
73
+ const bodies = grouped.get(location) ?? [];
74
+ bodies.push(body);
75
+ grouped.set(location, bodies);
53
76
  }
54
- finally {
55
- await artifact.close();
77
+ return grouped;
78
+ }
79
+ function locatedSeats(facts, location, seats) {
80
+ return seats.flatMap(seat => {
81
+ const index = parseActivityId(seat.generation);
82
+ const joined = index === undefined ? undefined : facts.activity(index);
83
+ return joined?.kind !== 'join' ? [] : [{ location, seat, joinedAt: joined.at }];
84
+ });
85
+ }
86
+ /** Lifecycle reads resolve seats and join times, never pending activity bodies. */
87
+ export async function projectSessionSeats(input, signal) {
88
+ const seats = [];
89
+ for (const [location, bodies] of groupCandidates(await projectSessionCandidates(input))) {
90
+ signal?.throwIfAborted();
91
+ const artifact = createArtifactPort(location, undefined, signal);
92
+ try {
93
+ seats.push(...(await artifact.read(facts => locatedSeats(facts, location, bodies.flatMap(body => facts.seatsForBody(body))), signal)).value);
94
+ }
95
+ catch (error) {
96
+ if (signal?.aborted)
97
+ throw signal.reason ?? error;
98
+ }
99
+ finally {
100
+ await artifact.close();
101
+ }
102
+ }
103
+ return seats;
104
+ }
105
+ /** Public count boundary: one coherent read per artifact, without pending body hydration. */
106
+ export async function sessionInboxCounts(input, signal) {
107
+ const rows = [];
108
+ for (const [location, bodies] of groupCandidates(await projectSessionCandidates(input))) {
109
+ signal?.throwIfAborted();
110
+ const artifact = createArtifactPort(location, undefined, signal);
111
+ try {
112
+ rows.push(...(await artifact.read(facts => locatedSeats(facts, location, bodies.flatMap(body => facts.seatsForBody(body))).map(({ seat }) => ({
113
+ name: seat.participant, squarePath: location, pending: facts.directedCount(seat.participant, { unseenOnly: true }),
114
+ })), signal)).value);
115
+ }
116
+ catch (error) {
117
+ if (signal?.aborted)
118
+ throw signal.reason ?? error;
119
+ }
120
+ finally {
121
+ await artifact.close();
122
+ }
56
123
  }
124
+ return rows;
57
125
  }
58
- export async function sessionInbox(sessionId, env = process.env, signal) {
59
- const inbox = [];
60
- const hostLedger = hostLedgerForEnv(env);
61
- for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
62
- if (signal?.aborted)
63
- throw signal.reason;
126
+ /** One authoritative read per canonical artifact, even when discovery names several bodies. */
127
+ async function projectPending(candidates, cache, signal) {
128
+ const pending = [];
129
+ for (const [location, bodies] of groupCandidates(candidates)) {
130
+ signal?.throwIfAborted();
131
+ const artifact = createArtifactPort(location, undefined, signal);
64
132
  try {
65
- const { value: membership } = await readInboxMembership(binding, signal);
66
- if (membership !== undefined)
67
- inbox.push(membership);
133
+ const { value: current } = await artifact.read(facts => {
134
+ const seats = bodies.flatMap(body => facts.seatsForBody(body));
135
+ const coordinate = JSON.stringify([bodies, seats.map(seat => [seat.participant, seat.body, seat.generation])]);
136
+ const header = facts.header();
137
+ let cached = cache?.get(location);
138
+ if (cached?.coordinate !== coordinate || cached.version !== header.revision) {
139
+ cached = { coordinate, version: header.revision, value: locatedSeats(facts, location, seats).map(membership => ({
140
+ ...membership, notifications: pendingNotifications(facts, membership.seat.participant).map(({ item, route }) => ({
141
+ actIndex: item.index, actor: item.actor, at: item.at, body: item.body ?? '(structured activity)', route,
142
+ })),
143
+ })) };
144
+ }
145
+ // Cache conversation content, never leases or delivery eligibility: evidence can change
146
+ // without a conversation revision and must not force body hydration or self-wake retries.
147
+ return { cached, pending: cached.value.map(membership => {
148
+ const catchLease = freshWatchLease(facts, membership.seat.participant);
149
+ return { ...membership, notifications: eligibleNotifications(facts, membership),
150
+ ...(catchLease === undefined ? {} : { catchLease }) };
151
+ }) };
152
+ }, signal);
153
+ cache?.set(location, current.cached);
154
+ pending.push(...current.pending);
68
155
  }
69
156
  catch (error) {
157
+ cache?.delete(location);
70
158
  if (signal?.aborted)
71
159
  throw signal.reason ?? error;
72
- // A stale or unreadable membership cannot disable delivery for other memberships.
160
+ // Unreadable candidates remain watch targets, but never authorize cached content.
161
+ }
162
+ finally {
163
+ await artifact.close();
73
164
  }
74
165
  }
75
- return inbox;
166
+ return pending;
167
+ }
168
+ function sameWakeAddress(address, kind, requested) {
169
+ if (address?.kind !== kind)
170
+ return false;
171
+ return Object.entries(requested).every(([key, value]) => address.address[key] === value);
172
+ }
173
+ /** First matching current seat, exact body/generation/address and catch ownership. */
174
+ export function nativePendingPreview(facts, input) {
175
+ const seat = facts.seat(input.participant);
176
+ if (seat === undefined || seat.body.session !== input.sessionId
177
+ || !sameWakeAddress(seat.wakeAddress, input.routeKind, input.address))
178
+ return undefined;
179
+ const cancelledThrough = facts.cancelledThrough(seat.participant, seat.generation);
180
+ if (input.actIndex <= Math.max(input.cancelledThrough ?? -1, cancelledThrough ?? -1))
181
+ return undefined;
182
+ const head = pendingHead(facts, seat.participant, input.actIndex);
183
+ const lease = freshWatchLease(facts, input.participant, input.now);
184
+ if (!head || (lease !== undefined && leaseOwnsNotification(lease, head)))
185
+ return undefined;
186
+ const item = facts.activity(input.actIndex);
187
+ if (item?.kind !== 'say')
188
+ return undefined;
189
+ return { payload: renderSquareReminder([renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
190
+ actor: item.actor, route: head.route, body: item.body ?? '(structured activity)' })]), clipped: attentionBodyIsClipped(item.body ?? '(structured activity)') };
191
+ }
192
+ export async function sessionInbox(identity, env = process.env, signal) {
193
+ const query = 'allAgents' in identity ? { channel: identity.channel, sessionId: identity.session, env }
194
+ : { body: identity, env };
195
+ return projectPending(await projectSessionCandidates(query), undefined, signal);
76
196
  }
77
197
  async function capture(observer, kind, signal) {
78
198
  const generation = observer.generation;
@@ -103,34 +223,27 @@ async function capture(observer, kind, signal) {
103
223
  }
104
224
  }
105
225
  /** Session-scoped projection over process-shared storage change detectors. */
106
- export async function observeSessionPending(sessionId, suppliedEnv = process.env) {
226
+ export async function observeSessionPending(identity, suppliedEnv = process.env) {
107
227
  const env = { ...suppliedEnv };
108
- const hostLedger = hostLedgerForEnv(env);
109
- const ledger = await hostLedger.observeChanges();
228
+ const query = 'allAgents' in identity ? { channel: identity.channel, sessionId: identity.session, env }
229
+ : { body: identity, env };
230
+ const index = await observeSquareIndex(env);
110
231
  const artifacts = new Map();
232
+ const cache = new Map();
111
233
  const lifetime = new AbortController();
112
- const bindingsNow = async () => Promise.all((await projectSessionBindings({ hostLedger, sessionId })).map(async (binding) => ({ ...binding, location: await canonicalPath(binding.location) })));
113
- async function membershipAt(binding, version, signal) {
114
- const cache = artifacts.get(binding.location)?.inbox;
115
- const name = nameKey(binding.participant);
116
- const coordinate = bindingCoordinate(binding);
117
- let cached = cache?.get(name);
118
- if (cached === undefined || cached.version !== version || cached.binding !== coordinate) {
119
- cached = { ...await readInboxMembership(binding, signal), binding: coordinate };
120
- cache?.set(name, cached);
121
- }
122
- const membership = cached.value;
123
- if (membership === undefined)
124
- return undefined;
125
- const { catchLease, ...rest } = membership;
234
+ async function currentInbox(candidates, signal) {
235
+ const inbox = await projectPending(candidates, cache, signal);
126
236
  const now = Date.now();
127
- return catchLease !== undefined && catchLease.expiresAt > now && now - catchLease.heartbeatAt <= WATCH_STALE_MS
128
- ? membership : rest;
237
+ return inbox.map((membership) => {
238
+ const { catchLease, ...rest } = membership;
239
+ return catchLease !== undefined && catchLease.expiresAt > now && now - catchLease.heartbeatAt <= WATCH_STALE_MS
240
+ ? membership : rest;
241
+ });
129
242
  }
130
- async function eligibleInbox(inbox, excludeKeys) {
131
- const eligible = await projectBoundaryEligibility({ hostLedger, sessionId, inbox });
243
+ function eligibleInbox(inbox, excludeKeys) {
244
+ const eligible = pendingAtBoundary(inbox);
132
245
  return excludeKeys === undefined ? eligible : eligible.flatMap((membership) => {
133
- const notifications = membership.notifications.filter((notification) => !excludeKeys.has(notificationKey(membership, notification.actIndex)));
246
+ const notifications = membership.notifications.filter((notification) => !excludeKeys.has(pendingKey(membership, notification.actIndex)));
134
247
  return notifications.length === 0 ? [] : [{ ...membership, notifications }];
135
248
  });
136
249
  }
@@ -138,34 +251,8 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
138
251
  async read(options = {}) {
139
252
  const signal = AbortSignal.any([lifetime.signal, ...(options.signal === undefined ? [] : [options.signal])]);
140
253
  signal.throwIfAborted();
141
- const bindings = await bindingsNow();
142
- const revisions = new Map();
143
- const inbox = [];
144
- for (const binding of bindings) {
145
- signal.throwIfAborted();
146
- try {
147
- let version = revisions.get(binding.location);
148
- if (version === undefined) {
149
- const artifact = createArtifactPort(binding.location);
150
- try {
151
- version = await artifact.revision(signal);
152
- }
153
- finally {
154
- await artifact.close();
155
- }
156
- revisions.set(binding.location, version);
157
- }
158
- const membership = await membershipAt(binding, version, signal);
159
- if (membership !== undefined)
160
- inbox.push(membership);
161
- }
162
- catch (error) {
163
- if (signal.aborted)
164
- throw signal.reason ?? error;
165
- // An unreadable current revision must never authorize cached content.
166
- }
167
- }
168
- const pending = await eligibleInbox(inbox, options.excludeKeys);
254
+ const inbox = await currentInbox(await projectSessionCandidates(query), signal);
255
+ const pending = eligibleInbox(inbox, options.excludeKeys);
169
256
  signal.throwIfAborted();
170
257
  return pending;
171
258
  },
@@ -178,55 +265,51 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
178
265
  let canReturn = !options.skipImmediate;
179
266
  let previousEligible;
180
267
  let armed = false;
268
+ let candidates = [];
269
+ let candidatePresence;
270
+ let discoveredCandidates = false;
181
271
  try {
182
272
  while (!signal.aborted) {
183
- // Subscribe/capture the ledger BEFORE discovering bindings; a concurrent join cannot fall in a gap.
184
- const sources = await Promise.all([capture(ledger.presence, 'presence', signal), capture(ledger.evidence, 'evidence', signal)]);
185
- const bindings = await bindingsNow();
186
- const paths = new Set(bindings.map((binding) => binding.location));
273
+ // Subscribe to the candidate index before discovering artifacts; a concurrent join can
274
+ // publish its path without touching the artifact revision.
275
+ const sources = [await capture(index, 'index', signal)];
276
+ const presence = sources[0];
277
+ const currentPresence = presence.value;
278
+ if (!discoveredCandidates || currentPresence !== undefined && !Object.is(currentPresence, candidatePresence)) {
279
+ candidates = await projectSessionCandidates(query);
280
+ candidatePresence = currentPresence;
281
+ discoveredCandidates = true;
282
+ }
283
+ const paths = new Set(candidates.map((candidate) => candidate.location));
187
284
  for (const [file, entry] of artifacts) {
188
285
  if (!paths.has(file)) {
189
286
  entry.observer.close();
287
+ entry.delivery.close();
190
288
  artifacts.delete(file);
289
+ cache.delete(file);
191
290
  }
192
291
  }
193
292
  for (const file of paths) {
194
293
  if (artifacts.has(file))
195
294
  continue;
196
295
  const observer = await observeSquareRevision(file);
296
+ const delivery = await observeSquareDeliveryRevision(file);
197
297
  if (signal.aborted) {
198
298
  observer.close();
299
+ delivery.close();
199
300
  return [];
200
301
  }
201
- artifacts.set(file, { observer, inbox: new Map() });
302
+ artifacts.set(file, { observer, delivery });
202
303
  }
203
- const revisions = new Map();
204
- for (const [file, entry] of artifacts) {
205
- const source = await capture(entry.observer, 'artifact', signal);
206
- sources.push(source);
207
- revisions.set(file, source.value);
208
- const names = new Set(bindings.filter((binding) => binding.location === file).map((binding) => nameKey(binding.participant)));
209
- for (const name of entry.inbox.keys())
210
- if (!names.has(name))
211
- entry.inbox.delete(name);
212
- }
213
- const inbox = [];
214
- for (const binding of bindings) {
215
- if (signal.aborted)
216
- return [];
217
- try {
218
- const membership = await membershipAt(binding, revisions.get(binding.location), signal);
219
- if (membership !== undefined)
220
- inbox.push(membership);
221
- }
222
- catch (error) {
223
- if (signal.aborted)
224
- throw signal.reason ?? error;
225
- // Unreadable projections are retried, never cached as current.
226
- }
304
+ // Capture both revision baselines before selecting seats and pending facts. A takeover
305
+ // during an empty selection must remain visible to the already armed observers.
306
+ for (const entry of artifacts.values()) {
307
+ sources.push(await capture(entry.observer, 'artifact', signal));
308
+ sources.push(await capture(entry.delivery, 'delivery', signal));
227
309
  }
228
- const pending = await eligibleInbox(inbox, options.excludeKeys);
229
- const eligibleKeys = JSON.stringify(pending.flatMap((membership) => membership.notifications.map((note) => notificationKey(membership, note.actIndex))).sort());
310
+ const inbox = await currentInbox(candidates, signal);
311
+ const pending = eligibleInbox(inbox, options.excludeKeys);
312
+ const eligibleKeys = JSON.stringify(pending.flatMap((membership) => membership.notifications.map((note) => pendingKey(membership, note.actIndex))).sort());
230
313
  if (previousEligible !== undefined && previousEligible !== eligibleKeys)
231
314
  canReturn = true;
232
315
  previousEligible = eligibleKeys;
@@ -239,7 +322,7 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
239
322
  return [];
240
323
  // Eligibility may change at lease expiry without any storage write.
241
324
  const leaseExpiry = Math.min(Infinity, ...inbox.flatMap((membership) => membership.catchLease === undefined ? [] : [Math.min(membership.catchLease.expiresAt, membership.catchLease.heartbeatAt + WATCH_STALE_MS + 1)]));
242
- const nextExpiry = Math.min(leaseExpiry, await hostLedger.nextExpiry(sessionId));
325
+ const nextExpiry = leaseExpiry;
243
326
  const duration = Math.min(remaining, Math.max(1, nextExpiry - Date.now()));
244
327
  const cancelled = new AbortController();
245
328
  const waitSignal = AbortSignal.any([signal, cancelled.signal]);
@@ -259,8 +342,7 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
259
342
  }
260
343
  try {
261
344
  const change = await Promise.race(waits);
262
- // Evidence writes from our own failed injection must not generate a retry loop.
263
- if (change !== 'evidence')
345
+ if (change !== 'delivery')
264
346
  canReturn = true;
265
347
  }
266
348
  finally {
@@ -286,11 +368,13 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
286
368
  if (lifetime.signal.aborted)
287
369
  return;
288
370
  lifetime.abort();
289
- ledger.presence.close();
290
- ledger.evidence.close();
291
- for (const entry of artifacts.values())
371
+ index.close();
372
+ for (const entry of artifacts.values()) {
292
373
  entry.observer.close();
374
+ entry.delivery.close();
375
+ }
293
376
  artifacts.clear();
377
+ cache.clear();
294
378
  },
295
379
  };
296
380
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare, migrateSquare } from './square-api.js';
2
- export { SquareError } from './model.js';
3
- export { squareAssignedParticipantName } from './registry.js';
2
+ export { SquareError, ACTIVITY_METADATA_MAX_BYTES } from './model.js';
3
+ export type { WakeTransportPort, WakeRequest, WakeOutcome } from './ports.js';
4
+ export type { WakeTransports } from './notifications.js';
4
5
  export { createDefaultWakeTransport } from './notifications.js';
5
- export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
6
- export type { HostLedgerPort, PresenceRecord, EvidenceRecord } from './host-ledger.js';
7
6
  export type { PresentationSinkPort } from './ports.js';
8
7
  export type * from './square-backend.js';
9
8
  export type * from './square-facade.js';
package/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
1
  export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare, migrateSquare } from './square-api.js';
2
- export { SquareError } from './model.js';
3
- export { squareAssignedParticipantName } from './registry.js';
2
+ export { SquareError, ACTIVITY_METADATA_MAX_BYTES } from './model.js';
4
3
  export { createDefaultWakeTransport } from './notifications.js';
5
- export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
package/dist/list.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { openSquare } from './open-square.js';
4
- import { closeOpenSquare } from './open-square.js';
3
+ import { createArtifactPort } from './artifact.js';
5
4
  import { listPresentation } from './views.js';
6
5
  import { SquareError } from './model.js';
7
6
  export const DEFAULT_LIST_DEPTH = 4;
@@ -16,8 +15,14 @@ async function readSquareListItem(filePath, root, options, control) {
16
15
  try {
17
16
  control?.signal?.throwIfAborted();
18
17
  const stat = await fs.promises.stat(filePath);
19
- const square = await openSquare(filePath, { ...options, env: options.env ?? {}, signal: control?.signal }, control);
20
- const projection = await listPresentation(square, control).finally(() => closeOpenSquare(square));
18
+ const artifact = createArtifactPort(filePath, undefined, control?.signal);
19
+ let projection;
20
+ try {
21
+ projection = (await artifact.read(listPresentation, control?.signal)).value;
22
+ }
23
+ finally {
24
+ await artifact.close();
25
+ }
21
26
  return {
22
27
  path: path.relative(root, filePath) || path.basename(filePath),
23
28
  lastActiveAt: stat.mtimeMs,
package/dist/mcp.js CHANGED
@@ -25,6 +25,14 @@ export function createSquareMcpServer(options) {
25
25
  const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
26
26
  ? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
27
27
  : undefined;
28
+ const body = claude !== undefined
29
+ ? { channel: 'claude-code', session: claude.sessionId,
30
+ ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) }
31
+ : typeof threadId === 'string' && threadId.trim()
32
+ ? { channel: 'codex', session: threadId }
33
+ : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
34
+ ? { channel: 'opencode', session: opencodeSessionId }
35
+ : options.body;
28
36
  const env = claude !== undefined
29
37
  ? { ...operationEnv('claude', claude.sessionId, baseEnv),
30
38
  // Preserve Claude's session + child flag semantics; an agent loop ID is not a session ID.
@@ -35,7 +43,10 @@ export function createSquareMcpServer(options) {
35
43
  ? operationEnv('opencode', opencodeSessionId, baseEnv)
36
44
  : baseEnv;
37
45
  square = await Square.at({
38
- ...options, env, cwd: claude?.cwd ?? options.cwd,
46
+ ...options, env, body, cwd: claude?.cwd ?? options.cwd,
47
+ // A call-local Codex thread uses its native queue even when managed by Paseo.
48
+ wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
49
+ ? { kind: 'codex-queue', address: { threadId } } : options.wakeAddress,
39
50
  path: location ?? options.path,
40
51
  participant: participant ?? options.participant,
41
52
  // The server owner is bound to its configured square; overrides close after each call.
package/dist/model.d.ts CHANGED
@@ -3,6 +3,37 @@ export { formatActivityId, parseActivityId } from './square-core.js';
3
3
  export type { Act, ActivityId, Audience, Reach } from './square-core.js';
4
4
  export declare const WAKE_ROUTE_KINDS: readonly ["opencode-server", "codex-queue", "claude-native", "paseo"];
5
5
  export type WakeRouteKind = typeof WAKE_ROUTE_KINDS[number];
6
+ export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
7
+ readonly [key: string]: JsonValue;
8
+ };
9
+ export type ActivityMetadata = Readonly<Record<string, JsonValue>>;
10
+ export declare const ACTIVITY_METADATA_MAX_BYTES: number;
11
+ export declare const ACTIVITY_METADATA_MAX_DEPTH = 256;
12
+ /** Validate the bounded, lossless JSON object carried by a say. */
13
+ export declare function validateActivityMetadata(value: unknown): ActivityMetadata | undefined;
14
+ export declare function activityMetadataHasContent(metadata: ActivityMetadata | undefined): boolean;
15
+ export interface Body {
16
+ readonly channel: string;
17
+ readonly session: string;
18
+ readonly agent?: string;
19
+ }
20
+ export interface WakeAddress {
21
+ readonly kind: WakeRouteKind;
22
+ readonly address: Readonly<Record<string, string>>;
23
+ }
24
+ /** Artifact-owned custody binding. Generation is the join activity that established it. */
25
+ export interface Seat {
26
+ readonly participant: string;
27
+ readonly body: Body;
28
+ readonly generation: ActivityId;
29
+ readonly wakeAddress?: WakeAddress;
30
+ }
31
+ export interface SeatWatchLease {
32
+ readonly generation: ActivityId;
33
+ readonly lease: WatchLease;
34
+ }
35
+ export declare function sameBody(left: Body, right: Body): boolean;
36
+ export declare function validateBody(body: Body): void;
6
37
  export declare function isWakeRouteKind(value: unknown): value is WakeRouteKind;
7
38
  export interface WakeRoute {
8
39
  /** Ownership generation, when the receiver publishes a fenced route. */
@@ -80,11 +111,14 @@ export interface InboxNotification {
80
111
  route: DirectedNotificationRoute;
81
112
  body: string;
82
113
  }
83
- export interface InboxMembership {
84
- name: string;
85
- squarePath: string;
86
- notifications: InboxNotification[];
87
- catchLease?: WatchLease;
114
+ export interface LocatedSeat {
115
+ readonly location: string;
116
+ readonly seat: Seat;
117
+ readonly joinedAt: number;
118
+ }
119
+ export interface PendingMembership extends LocatedSeat {
120
+ readonly notifications: InboxNotification[];
121
+ readonly catchLease?: WatchLease;
88
122
  }
89
123
  export interface WatchOptions {
90
124
  id?: ActivityId;