@astrosheep/square 0.3.62 → 0.3.64

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 (168) 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.js +12 -24
  10. package/dist/attention-presentation.js +1 -0
  11. package/dist/automatic-session.d.ts +3 -1
  12. package/dist/automatic-session.js +21 -34
  13. package/dist/boundary-presentation.js +4 -13
  14. package/dist/canonical-path.d.ts +11 -0
  15. package/dist/canonical-path.js +47 -0
  16. package/dist/catch-decisions.js +14 -10
  17. package/dist/catch-selection.d.ts +6 -0
  18. package/dist/catch-selection.js +14 -0
  19. package/dist/claude-delivery.d.ts +11 -0
  20. package/dist/claude-delivery.js +145 -0
  21. package/dist/claude-mod.d.ts +8 -0
  22. package/dist/claude-mod.js +164 -0
  23. package/dist/cli/context.d.ts +0 -1
  24. package/dist/cli/context.js +9 -21
  25. package/dist/cli/harness-command.d.ts +0 -1
  26. package/dist/cli/harness-command.js +0 -6
  27. package/dist/cli/observation-commands.d.ts +1 -1
  28. package/dist/cli/observation-commands.js +31 -58
  29. package/dist/cli/program.js +20 -18
  30. package/dist/cli/registry.js +2 -2
  31. package/dist/cli/square-commands.js +10 -6
  32. package/dist/codex-hook.js +9 -12
  33. package/dist/codex-queue.d.ts +2 -2
  34. package/dist/codex-queue.js +7 -5
  35. package/dist/decisions.d.ts +2 -6
  36. package/dist/decisions.js +16 -17
  37. package/dist/delivery-health.d.ts +1 -2
  38. package/dist/delivery-health.js +4 -5
  39. package/dist/delivery-operations.d.ts +2 -5
  40. package/dist/delivery-operations.js +77 -214
  41. package/dist/delivery.d.ts +25 -47
  42. package/dist/delivery.js +14 -46
  43. package/dist/diagnostic-redaction.js +7 -9
  44. package/dist/file-changes.d.ts +0 -1
  45. package/dist/file-changes.js +2 -19
  46. package/dist/harness-claude.js +8 -6
  47. package/dist/harness-codex.d.ts +0 -2
  48. package/dist/harness-codex.js +0 -2
  49. package/dist/harness-links.js +13 -17
  50. package/dist/harness.js +3 -7
  51. package/dist/help.js +4 -3
  52. package/dist/host-ledger-file-adapter.d.ts +17 -8
  53. package/dist/host-ledger-file-adapter.js +164 -80
  54. package/dist/host-ledger.d.ts +55 -50
  55. package/dist/inbox.d.ts +0 -3
  56. package/dist/inbox.js +13 -30
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.js +1 -1
  59. package/dist/list.js +1 -4
  60. package/dist/mcp.js +18 -40
  61. package/dist/mention-parse.js +3 -9
  62. package/dist/model.d.ts +11 -2
  63. package/dist/model.js +1 -1
  64. package/dist/notifications.d.ts +3 -16
  65. package/dist/notifications.js +41 -122
  66. package/dist/opencode-delivery.d.ts +14 -0
  67. package/dist/opencode-delivery.js +171 -0
  68. package/dist/opencode.d.ts +39 -18
  69. package/dist/opencode.js +145 -40
  70. package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
  71. package/dist/packages/agent-delivery/src/claude-auth.js +70 -0
  72. package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
  73. package/dist/packages/agent-delivery/src/claude-native.js +65 -0
  74. package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
  75. package/dist/packages/agent-delivery/src/claude.js +66 -0
  76. package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
  77. package/dist/packages/agent-delivery/src/deadline.js +41 -0
  78. package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
  79. package/dist/packages/agent-delivery/src/errors.js +41 -0
  80. package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
  81. package/dist/packages/agent-delivery/src/guards.js +6 -0
  82. package/dist/packages/agent-delivery/src/index.d.ts +83 -0
  83. package/dist/packages/agent-delivery/src/index.js +110 -0
  84. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
  85. package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
  86. package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
  87. package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
  88. package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
  89. package/dist/packages/agent-delivery/src/opencode.js +127 -0
  90. package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
  91. package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
  92. package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
  93. package/dist/packages/agent-delivery/src/paseo.js +106 -0
  94. package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
  95. package/dist/packages/agent-delivery/src/pi-client.js +106 -0
  96. package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
  97. package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
  98. package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
  99. package/dist/packages/agent-delivery/src/pi-send.js +4 -0
  100. package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
  101. package/dist/packages/agent-delivery/src/pi.js +209 -0
  102. package/dist/packages/agent-delivery/src/types.d.ts +120 -0
  103. package/dist/participant-host.js +12 -13
  104. package/dist/participant-identity.d.ts +21 -0
  105. package/dist/participant-identity.js +32 -11
  106. package/dist/paseo-delivery.d.ts +2 -2
  107. package/dist/paseo-delivery.js +35 -27
  108. package/dist/paseo-timeline.js +25 -21
  109. package/dist/paseo.d.ts +2 -2
  110. package/dist/paseo.js +1 -2
  111. package/dist/ports.d.ts +7 -20
  112. package/dist/presentation-operations.js +16 -4
  113. package/dist/presentation.d.ts +19 -10
  114. package/dist/presentation.js +99 -177
  115. package/dist/registry.d.ts +15 -66
  116. package/dist/registry.js +45 -123
  117. package/dist/routes.d.ts +1 -28
  118. package/dist/routes.js +22 -104
  119. package/dist/runtime.d.ts +3 -9
  120. package/dist/runtime.js +14 -67
  121. package/dist/search.d.ts +0 -2
  122. package/dist/search.js +3 -10
  123. package/dist/square-actions.js +48 -72
  124. package/dist/square-application.js +25 -23
  125. package/dist/square-core.d.ts +3 -0
  126. package/dist/square-core.js +22 -5
  127. package/dist/square-facade.d.ts +2 -0
  128. package/dist/square-file-adapter.js +4 -7
  129. package/dist/square-projections.d.ts +40 -45
  130. package/dist/square-projections.js +43 -73
  131. package/dist/square-state.js +1 -4
  132. package/dist/square-storage.d.ts +5 -8
  133. package/dist/square-storage.js +13 -36
  134. package/dist/square-wiring.d.ts +0 -5
  135. package/dist/square-wiring.js +3 -21
  136. package/dist/stream.js +4 -6
  137. package/dist/time.js +2 -7
  138. package/dist/views.d.ts +4 -16
  139. package/dist/views.js +12 -19
  140. package/dist/wake-attempts.d.ts +2 -56
  141. package/dist/wake-attempts.js +15 -110
  142. package/dist/wake-eligibility.d.ts +43 -0
  143. package/dist/wake-eligibility.js +106 -0
  144. package/dist/wake-evidence.d.ts +32 -9
  145. package/dist/wake-evidence.js +29 -24
  146. package/dist/wake-sink.d.ts +13 -9
  147. package/dist/wake-sink.js +35 -54
  148. package/dist/watch.js +13 -21
  149. package/extensions/square-pi.js +10 -5
  150. package/guides/architect.md +77 -77
  151. package/guides/brainstorm.md +197 -197
  152. package/guides/claude-native.md +101 -0
  153. package/guides/host-ledger.md +3 -1
  154. package/guides/participant.md +14 -7
  155. package/guides/pi-native-delivery.md +43 -0
  156. package/package.json +4 -3
  157. package/dist/claude-hook.d.ts +0 -10
  158. package/dist/claude-hook.js +0 -60
  159. package/dist/paseo-connection.d.ts +0 -15
  160. package/dist/paseo-connection.js +0 -141
  161. package/dist/presence.d.ts +0 -2
  162. package/dist/presence.js +0 -24
  163. package/dist/presented.d.ts +0 -2
  164. package/dist/presented.js +0 -10
  165. package/dist/state-cell.d.ts +0 -14
  166. package/dist/wake-port.d.ts +0 -23
  167. package/dist/wake-port.js +0 -27
  168. /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
@@ -1,8 +1,8 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { formatActivityId, parseActivityId } from './square-core.js';
3
- import { nameKey } from './model.js';
4
2
  import { deriveDeliveryModel } from './delivery.js';
5
- import { currentSessionBindings, isWakeRouteAttemptable, presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
3
+ import { projectWakeEvidenceFromState } from './square-projections.js';
4
+ import { wakeIsEligible } from './wake-eligibility.js';
5
+ import { canonicalPath } from './canonical-path.js';
6
6
  import { retireWakeRouteFromArtifact } from './routes.js';
7
7
  import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
8
8
  async function releaseWakeClaim(input, details) {
@@ -33,80 +33,43 @@ async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
33
33
  clearTimeout(timer);
34
34
  }
35
35
  }
36
- export async function observeSquare(input) {
37
- const snapshot = await input.artifact.read();
38
- const delivery = deriveDeliveryModel(snapshot.state);
39
- let rows = [];
40
- if (input.hostLedger !== undefined) {
41
- try {
42
- rows = await input.hostLedger.listPresence({ location: input.location, now: input.now });
43
- }
44
- catch {
45
- rows = [];
46
- }
47
- }
48
- const bindings = rows.map((record) => ({
49
- location: record.location,
50
- participant: record.participant,
51
- sessionId: record.session,
52
- channel: record.channel,
53
- ...(record.route === undefined ? {} : { route: { location: record.location, participant: record.participant, sessionId: record.session, channel: record.channel, kind: record.route.kind, address: { ...record.route.address }, updatedAt: record.updatedAt ?? 0 } }),
54
- updatedAt: record.updatedAt ?? 0,
55
- }));
56
- return { ...(input.location === undefined ? {} : { location: input.location }), version: snapshot.version, state: snapshot.state, pending: delivery.joinedRecipients().map((recipient) => ({ recipient, notifications: delivery.pendingFor(recipient) })), bindings };
57
- }
58
36
  export async function deliverPending(input) {
59
- const observation = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
60
- const routes = (observation.state.routes ?? []).map((route) => ({
61
- location: route.location,
62
- participant: route.participant,
63
- session: route.sessionId,
64
- channel: route.channel,
65
- route: { kind: route.kind, address: route.address },
66
- updatedAt: route.updatedAt,
67
- }));
68
- const liveRoutes = currentSessionBindings(routes.filter((route) => observation.bindings.some((binding) => nameKey(binding.participant) === nameKey(route.participant)
69
- && binding.sessionId === route.session
70
- && binding.location === route.location)));
71
- let presentations = [];
72
- try {
73
- presentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, now: input.now });
74
- }
75
- catch { /* capability is handled by the route-level wake checks */ }
37
+ const location = await canonicalPath(input.location);
38
+ const { state } = await input.artifact.read();
39
+ const delivery = deriveDeliveryModel(state);
40
+ const eligibility = await projectWakeEvidenceFromState({ state, location, hostLedger: input.hostLedger, now: input.now ?? Date.now(), delivery });
41
+ const observeCurrentness = async (request, now) => {
42
+ const { state: currentState } = await input.artifact.read();
43
+ const projection = await projectWakeEvidenceFromState({ state: currentState, location, hostLedger: input.hostLedger, now });
44
+ return { ...projection.currentness({ ...request, location }), observationAvailable: true };
45
+ };
76
46
  let attempted = 0;
77
47
  let accepted = 0;
78
48
  let failed = 0;
79
49
  let unknown = 0;
80
50
  let notCapable = 0;
81
- for (const membership of observation.pending) {
82
- for (const notification of membership.notifications) {
51
+ for (const recipient of delivery.joinedRecipients()) {
52
+ for (const notification of delivery.pendingFor(recipient)) {
83
53
  if (input.activity !== undefined) {
84
54
  const requested = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
85
55
  if (requested === undefined || requested !== notification.item.index)
86
56
  continue;
87
57
  }
88
- const candidates = liveRoutes.filter((route) => nameKey(route.participant) === nameKey(membership.recipient));
58
+ const evidence = eligibility.evidence(recipient, notification.item.index);
59
+ if (evidence.terminal !== undefined)
60
+ continue;
61
+ const candidates = evidence.attemptableRoutes;
89
62
  let acceptedForAttention = false;
90
63
  let failedForAttention = false;
91
64
  let unknownForAttention = false;
92
65
  let notCapableForAttention = false;
93
- try {
94
- const prior = await input.hostLedger.listWakeAttempts({ attention: { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient }, now: Date.now() });
95
- acceptedForAttention = prior.some((attempt) => attempt.outcome === 'accepted');
96
- }
97
- catch { /* capability is handled by the route-level probe */ }
98
- if (acceptedForAttention)
99
- continue;
100
- if (candidates.length === 0)
66
+ if (candidates.length === 0 && !evidence.presented)
101
67
  notCapableForAttention = true;
102
68
  for (const route of candidates) {
103
69
  const activity = formatActivityId(notification.item.index);
104
- const presented = presentations.filter((row) => row.activity === activity && row.participant.toLocaleLowerCase() === membership.recipient.toLocaleLowerCase() && row.sessionId === route.session && presentationSuppressesWake([row]));
105
- if (presented.length > 0)
106
- continue;
107
- const attention = { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient };
70
+ const attention = { squarePath: location, actIndex: notification.item.index, recipient };
108
71
  const leaseMs = input.timeoutMs ?? 5000;
109
- const requestRoute = { location: route.location, participant: route.participant, sessionId: route.session, channel: route.channel, kind: route.route.kind, address: { ...route.route.address }, updatedAt: route.updatedAt ?? 0 };
72
+ const requestRoute = route;
110
73
  if (input.transport.probe !== undefined) {
111
74
  try {
112
75
  const probe = await input.transport.probe(requestRoute);
@@ -120,167 +83,101 @@ export async function deliverPending(input) {
120
83
  continue;
121
84
  }
122
85
  }
123
- let leaseId = randomUUID();
124
- let lease;
86
+ let claim;
125
87
  try {
126
- lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session });
88
+ claim = await input.hostLedger.claimWakeAttempt({ attention, session: route.sessionId, routeKind: route.kind, leaseMs });
127
89
  }
128
90
  catch {
129
91
  notCapableForAttention = true;
130
92
  continue;
131
93
  }
132
- if (lease.type === 'ambiguous') {
133
- let recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
134
- if (recovered.status === 'busy' && recovered.record.claimToken !== undefined) {
135
- await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken: recovered.record.claimToken, routeKind: lease.lease.routeKind ?? route.route.kind, attemptN: lease.lease.attemptN }, {
136
- signature: 'stale_dispatch_claim_recovered',
137
- message: 'A stale dispatch claim was released so recovery could continue.',
138
- });
139
- recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
140
- }
141
- if (recovered.status === 'acquired') {
142
- await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: 'unknown', routeKind: lease.lease.routeKind ?? route.route.kind, attemptN: lease.lease.attemptN ?? 1, signature: 'worker_interrupted_during_dispatch', message: 'The notification worker ended after dispatch began; transport acceptance is unknown.', claimToken: recovered.claimToken });
143
- }
144
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId: lease.lease.leaseId, session: route.session });
145
- break;
146
- }
147
- if (lease.type === 'busy')
148
- break;
149
- if (lease.type !== 'acquired')
150
- continue;
151
- try {
152
- const completed = await input.hostLedger.listWakeAttempts({ attention, now: Date.now() });
153
- if (completed.some((attempt) => attempt.outcome === 'accepted')) {
154
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
155
- acceptedForAttention = true;
156
- break;
157
- }
158
- }
159
- catch { /* continue with the evidence claim */ }
160
- let attempts;
161
- try {
162
- attempts = await input.hostLedger.listWakeAttempts({ attention, now: Date.now() });
163
- }
164
- catch {
94
+ if (claim.status === 'degraded') {
165
95
  notCapableForAttention = true;
166
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
167
96
  continue;
168
97
  }
169
- if (attempts.some((attempt) => attempt.outcome === 'unknown')) {
170
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
98
+ // Busy and terminal outcomes are attention-wide: no other session or route may dispatch.
99
+ // A terminal attempt was already accepted or may already have been sent, so it reports nothing new.
100
+ if (claim.status === 'busy')
171
101
  break;
172
- }
173
- const sessionAttempts = attempts.filter((attempt) => attempt.session === route.session);
174
- const attemptN = sessionAttempts.reduce((highest, record) => Math.max(highest, record.attemptN ?? 0), 0) + 1;
175
- const request = { location: input.location, participant: membership.recipient, activity, actor: notification.item.actor, route: requestRoute };
176
- let outcome;
177
- const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: leaseId });
178
- if (claim.status !== 'acquired') {
179
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
180
- if (claim.status === 'degraded')
181
- notCapableForAttention = true;
182
- continue;
183
- }
184
- const claimToken = claim.claimToken;
185
- const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session });
186
- if (!dispatching) {
187
- await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
188
- signature: 'dispatch_claim_transition_failed',
189
- message: 'The wake dispatch claim could not enter the dispatching phase.',
190
- }).catch(() => undefined);
191
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
192
- continue;
193
- }
102
+ if (claim.status === 'terminal')
103
+ break;
104
+ const { claimToken, attemptN } = claim;
105
+ const request = { location: input.location, participant: recipient, activity, actor: notification.item.actor, route: requestRoute };
106
+ const claimRelease = { hostLedger: input.hostLedger, location, participant: recipient, session: route.sessionId, activity, claimToken, routeKind: route.kind, attemptN };
107
+ // One teardown path for every exit after claim acquisition; each call site keeps its own error policy.
108
+ const abandon = async (details, options = {}) => {
109
+ const release = releaseWakeClaim(claimRelease, details);
110
+ if (options.bestEffortClaim === true)
111
+ await release.catch(() => undefined);
112
+ else
113
+ await release;
114
+ };
194
115
  let current;
195
116
  try {
196
- current = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
117
+ current = await observeCurrentness(request, input.now ?? Date.now());
197
118
  }
198
119
  catch {
199
- current = { ...observation, pending: [], bindings: [] };
200
- }
201
- const stillPending = current.pending.some((entry) => nameKey(entry.recipient) === nameKey(membership.recipient)
202
- && entry.notifications.some((entryNotification) => entryNotification.item.index === notification.item.index));
203
- const stillBound = current.bindings.some((binding) => nameKey(binding.participant) === nameKey(route.participant)
204
- && binding.sessionId === route.session
205
- && binding.location === route.location);
206
- const stillPublished = (current.state.routes ?? []).some((published) => published.location === route.location
207
- && nameKey(published.participant) === nameKey(route.participant)
208
- && published.sessionId === route.session
209
- && published.kind === route.route.kind
210
- && JSON.stringify(published.address) === JSON.stringify(route.route.address));
211
- if (!stillPending || !stillBound || !stillPublished) {
212
- await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
213
- signature: 'pre_send_revalidation_failed',
214
- message: 'Wake was not sent because the activity, session binding, or published route changed before dispatch.',
215
- diagnostic: { pending: stillPending, bound: stillBound, publishedRoute: stillPublished },
216
- }).catch(() => undefined);
217
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
120
+ await abandon({ signature: 'pre_send_revalidation_failed', message: 'Wake was not sent because current Square delivery state could not be verified.', diagnostic: { observationAvailable: false } }, { bestEffortClaim: true }).catch(() => undefined);
218
121
  continue;
219
122
  }
220
- let latestPresentations = [];
221
- try {
222
- latestPresentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, sessionId: route.session, activity, now: input.now });
223
- }
224
- catch { /* capability is handled by the transport path */ }
225
- if (presentationSuppressesWake(latestPresentations)) {
226
- await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
227
- signature: 'presentation_already_recorded',
228
- message: 'Wake was suppressed because presentation evidence already exists.',
229
- diagnostic: { outcomes: latestPresentations.map((row) => row.outcome) },
230
- }).catch(() => undefined);
231
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
123
+ if (!current.current) {
124
+ await abandon({
125
+ signature: current.presented ? 'presentation_already_recorded' : 'pre_send_revalidation_failed',
126
+ message: current.presented ? 'Wake was suppressed because presentation evidence already exists.' : 'Wake was not sent because current attention, session binding, or route no longer matches.',
127
+ diagnostic: current,
128
+ }, { bestEffortClaim: true }).catch(() => undefined);
232
129
  continue;
233
130
  }
131
+ let outcome;
234
132
  let suppressedDuringSend = false;
133
+ let finalCurrentness;
235
134
  const beforeSend = async () => {
236
- let finalPresentations = [];
237
135
  try {
238
- finalPresentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, sessionId: route.session, activity, now: Date.now() });
136
+ finalCurrentness = await observeCurrentness(request, Date.now());
137
+ suppressedDuringSend = !finalCurrentness.current;
138
+ }
139
+ catch {
140
+ suppressedDuringSend = true;
239
141
  }
240
- catch { /* capability is handled by the transport path */ }
241
- suppressedDuringSend = presentationSuppressesWake(finalPresentations);
242
142
  return !suppressedDuringSend;
243
143
  };
244
144
  try {
245
- outcome = await attemptWakeWithin(input.transport, request, leaseMs, beforeSend);
145
+ outcome = await attemptWakeWithin(input.transport, { ...request, claimToken, attemptN }, leaseMs, beforeSend);
246
146
  }
247
147
  catch (error) {
248
148
  outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
249
149
  }
250
150
  if (suppressedDuringSend) {
251
- await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
252
- signature: 'presentation_recorded_during_dispatch',
253
- message: 'Wake was cancelled because presentation evidence appeared before the final send check.',
254
- }).catch(() => undefined);
255
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
151
+ await abandon({
152
+ signature: finalCurrentness?.presented === true ? 'presentation_recorded_during_dispatch' : 'pre_send_revalidation_failed',
153
+ message: finalCurrentness === undefined ? 'Wake was not sent because current Square delivery state could not be verified.' : 'Wake was cancelled because current attention, ownership, cancellation, or presentation changed before the final send check.',
154
+ diagnostic: finalCurrentness ?? { observationAvailable: false },
155
+ }, { bestEffortClaim: true }).catch(() => undefined);
256
156
  continue;
257
157
  }
258
158
  attempted += 1;
259
159
  if (outcome.outcome === 'not-capable') {
260
- await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
160
+ await abandon({
261
161
  signature: 'transport_not_capable',
262
162
  message: 'The wake transport reported that it could not deliver this route.',
263
163
  diagnostic: outcome.diagnostic,
264
- }).catch(() => undefined);
265
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
164
+ }, { bestEffortClaim: true }).catch(() => undefined);
266
165
  notCapableForAttention = true;
267
166
  continue;
268
167
  }
269
168
  if (outcome.outcome === 'failed' && outcome.unavailable) {
270
169
  if (outcome.routeStale === true)
271
- await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.session });
272
- await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN: outcome.attemptN ?? attemptN }, {
170
+ await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.sessionId });
171
+ await abandon({
273
172
  signature: outcome.signature ?? 'transport_unavailable',
274
173
  message: outcome.message ?? 'The wake transport was unavailable.',
275
174
  diagnostic: outcome.diagnostic,
276
175
  });
277
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
278
176
  failedForAttention = true;
279
177
  continue;
280
178
  }
281
179
  const safeOutcome = redactCurrentDiagnostic(outcome);
282
- await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: safeOutcome.outcome, routeKind: route.route.kind, attemptN: safeOutcome.attemptN ?? attemptN, ...(safeOutcome.signature === undefined ? {} : { signature: safeOutcome.signature }), ...(safeOutcome.outcome !== 'accepted' && safeOutcome.message !== undefined ? { message: safeOutcome.message } : {}), ...(safeOutcome.outcome !== 'accepted' && safeOutcome.diagnostic !== undefined ? { diagnostic: safeOutcome.diagnostic } : {}), claimToken });
283
- await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
180
+ await input.hostLedger.appendEvidence({ location, participant: recipient, session: route.sessionId, activity, kind: 'wake', outcome: safeOutcome.outcome, routeKind: route.kind, attemptN, ...(safeOutcome.signature === undefined ? {} : { signature: safeOutcome.signature }), ...(safeOutcome.outcome !== 'accepted' && safeOutcome.message !== undefined ? { message: safeOutcome.message } : {}), ...(safeOutcome.outcome !== 'accepted' && safeOutcome.diagnostic !== undefined ? { diagnostic: safeOutcome.diagnostic } : {}), claimToken });
284
181
  if (outcome.outcome === 'accepted') {
285
182
  acceptedForAttention = true;
286
183
  break;
@@ -304,55 +201,21 @@ export async function deliverPending(input) {
304
201
  }
305
202
  return { attempted, accepted, failed, unknown, notCapable };
306
203
  }
307
- export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state), presentations = []) {
308
- const selected = new Set();
309
- for (const membership of delivery.joinedRecipients()) {
310
- for (const notification of delivery.pendingFor(membership)) {
311
- if (now - notification.item.at <= graceMs)
312
- continue;
313
- if (attempts.some((attempt) => attempt.attention.actIndex === notification.item.index && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.outcome === 'accepted'))
314
- continue;
315
- const eligible = routes.some((binding) => {
316
- if (binding.route === undefined || nameKey(binding.participant) !== nameKey(membership))
317
- return false;
318
- const presented = presentations.filter((row) => row.activity === formatActivityId(notification.item.index) && row.participant.toLocaleLowerCase() === membership.toLocaleLowerCase() && row.sessionId === binding.session && presentationSuppressesWake([row]));
319
- if (presented.length > 0)
320
- return false;
321
- const matching = attempts.filter((attempt) => attempt.session === binding.session && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.attention.actIndex === notification.item.index);
322
- return isWakeRouteAttemptable({ kind: binding.route.kind, updatedAt: binding.updatedAt ?? 0 }, matching);
323
- });
324
- if (eligible)
325
- selected.add(notification.item.index);
326
- }
327
- }
328
- return [...selected].sort((left, right) => left - right).slice(0, Math.max(0, limit));
329
- }
330
204
  export async function sweepPending(input) {
331
205
  const { state } = await input.artifact.read();
332
206
  return sweepPendingFromState({ ...input, state });
333
207
  }
334
208
  export async function sweepPendingFromState(input) {
335
- const bindings = currentSessionBindings((input.state.routes ?? []).map((route) => ({ location: input.location, participant: route.participant, session: route.sessionId, channel: route.channel, route: { kind: route.kind, address: route.address }, updatedAt: route.updatedAt })));
336
- let records = [];
337
- try {
338
- records = await input.hostLedger.listWakeAttempts({ now: input.now });
339
- }
340
- catch {
341
- records = [];
342
- }
343
- let presentations = [];
344
- try {
345
- presentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, now: input.now });
346
- }
347
- catch {
348
- presentations = [];
349
- }
350
- const attempts = records.flatMap((record) => {
351
- const index = parseActivityId(record.activity);
352
- if (index === undefined || record.routeKind === undefined || typeof record.attemptN !== 'number')
353
- return [];
354
- return [{ at: record.at ?? input.now, attention: { squarePath: record.location, actIndex: index, recipient: record.participant }, routeKind: record.routeKind, outcome: record.outcome, attemptN: record.attemptN, ...(record.session === undefined ? {} : { session: record.session }) }];
355
- });
356
209
  const delivery = input.deriveDelivery?.(input.state) ?? deriveDeliveryModel(input.state);
357
- return selectPendingWakeActivities(input.state, bindings, attempts, input.now, input.graceMs, input.limit, delivery, presentations);
210
+ const eligibility = await projectWakeEvidenceFromState({ ...input, delivery });
211
+ const selected = new Set();
212
+ for (const recipient of delivery.joinedRecipients()) {
213
+ for (const notification of delivery.pendingFor(recipient)) {
214
+ if (input.now - notification.item.at <= input.graceMs)
215
+ continue;
216
+ if (wakeIsEligible(eligibility.evidence(recipient, notification.item.index)))
217
+ selected.add(notification.item.index);
218
+ }
219
+ }
220
+ return [...selected].sort((left, right) => left - right).slice(0, Math.max(0, input.limit));
358
221
  }
@@ -1,61 +1,44 @@
1
- import { type DirectedNotificationRoute, type SquareState, type StoredAct, type WatchLease, type WatchLeaseFilter, type WakeRouteKind, type Reach } from './model.js';
1
+ import { type DirectedNotificationRoute, type SquareState, type StoredAct, type WatchLease, type WakeRouteKind } from './model.js';
2
2
  import { type Perception } from './square-core.js';
3
+ import type { WakeOutcome } from './ports.js';
3
4
  export type { DirectedNotificationRoute } from './model.js';
4
5
  export type SayItem = StoredAct & {
5
6
  kind: 'say';
6
7
  };
7
- export declare function notificationMessageId(squarePath: string, actIndex: number): string;
8
8
  export interface PlannedNotification {
9
9
  item: SayItem;
10
10
  recipient: string;
11
11
  route: DirectedNotificationRoute;
12
12
  }
13
- export interface WakeRequest {
14
- squarePath: string;
15
- actIndex: number;
16
- recipient: string;
17
- actor: string;
18
- route: DirectedNotificationRoute;
19
- }
20
- export type WakeDispatchResult = {
21
- outcome: 'accepted';
22
- } | {
23
- outcome: 'unknown';
24
- signature: string;
25
- message: string;
26
- diagnostic?: unknown;
27
- } | {
28
- outcome: 'failed';
29
- signature: string;
30
- message: string;
31
- diagnostic?: unknown;
32
- } | {
33
- outcome: 'unavailable';
34
- signature: string;
35
- message: string;
36
- diagnostic?: unknown;
37
- retainRoute?: boolean;
38
- routeStale?: boolean;
39
- } | {
40
- outcome: 'cancelled';
13
+ /**
14
+ * What one adapter reports about a wake it was handed.
15
+ * `gate-rejected` means exactly one thing: the supplied beforeSend gate returned false and nothing was sent.
16
+ * It is not a bin for other not-sent reasons; those stay `failed` with `unavailable: true`.
17
+ */
18
+ export type WakeAdapterResult = Exclude<WakeOutcome, {
19
+ outcome: 'not-capable';
20
+ }> | {
21
+ outcome: 'gate-rejected';
41
22
  };
23
+ /**
24
+ * The coordinates of one wake attempt. An adapter that must hand the receiver a
25
+ * stable dedup key derives it from these, so a retry of the same attempt reuses
26
+ * the same key and a later attempt gets a new one.
27
+ */
28
+ export interface WakeDispatchContext {
29
+ readonly location: string;
30
+ readonly participant: string;
31
+ readonly activity: string;
32
+ readonly attemptN?: number;
33
+ }
42
34
  export interface WakeAdapter {
43
35
  readonly kind: WakeRouteKind;
44
- dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number): Promise<WakeDispatchResult>;
36
+ dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number, context?: WakeDispatchContext): Promise<WakeAdapterResult>;
45
37
  }
46
38
  export interface RoutedNotification {
47
39
  actor: string;
48
- body: string;
49
- mentions?: readonly string[];
50
40
  route: DirectedNotificationRoute;
51
- recipient?: string;
52
- }
53
- export interface CatchFilterShape {
54
- actor: string;
55
- body: string;
56
- mentions?: readonly string[];
57
- reach?: Reach;
58
- recipients?: readonly string[];
41
+ recipient: string;
59
42
  }
60
43
  export interface DeliveryModel {
61
44
  plan(item: StoredAct): PlannedNotification[];
@@ -75,11 +58,6 @@ export declare function isActivitySeen(squareState: SquareState, name: string, a
75
58
  * All consumers share these targets instead of reinterpreting artifact text or cursor state.
76
59
  */
77
60
  export declare function deriveDeliveryModel(squareState: SquareState): DeliveryModel;
78
- export declare function planActNotifications(squareState: SquareState, item: StoredAct, delivery?: DeliveryModel): PlannedNotification[];
79
61
  export declare function perceiveActivity(squareState: SquareState, item: StoredAct, viewer: string, delivery?: DeliveryModel): Perception;
80
- /** Mark only the notifications selected by the canonical catch projection as fully seen. */
81
- export declare function markSeenNotifications(squareState: SquareState, recipient: string, delivered: StoredAct[], at?: number, delivery?: DeliveryModel): boolean;
82
- /** Canonical say-activity filter shared by catch selection and hook ownership. */
83
- export declare function matchesCatchFilter(activity: CatchFilterShape, filter: WatchLeaseFilter): boolean;
84
- /** True only when the live catch's own filters would deliver this notification. */
62
+ /** True only when the live catch's own filters would deliver this unread notification, independent of page size. */
85
63
  export declare function leaseOwnsNotification(lease: WatchLease, notification: RoutedNotification): boolean;
package/dist/delivery.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { findParticipantName, sameName, } from './model.js';
2
- import { audienceOf, formatActivityId, replayLandedAudiences } from './square-core.js';
2
+ import { activityAuthors, audienceOf, replayLandedAudiences } from './square-core.js';
3
3
  import { derivePerceptionProjection } from './perception-projection.js';
4
- import { matchesMentionTarget, recordObservation } from './runtime.js';
5
- export function notificationMessageId(squarePath, actIndex) {
6
- return `square:${squarePath}#${formatActivityId(actIndex)}`;
7
- }
4
+ import { matchesMentionTarget } from './runtime.js';
5
+ import { matchesCatchSelection } from './catch-selection.js';
8
6
  export function isActivitySeen(squareState, name, actOrIndex) {
9
7
  return deriveDeliveryModel(squareState).isSeen(name, actOrIndex);
10
8
  }
@@ -14,6 +12,7 @@ export function isActivitySeen(squareState, name, actOrIndex) {
14
12
  */
15
13
  export function deriveDeliveryModel(squareState) {
16
14
  const landed = replayLandedAudiences(squareState.acts);
15
+ const authors = activityAuthors(squareState.acts);
17
16
  const perception = derivePerceptionProjection(squareState, landed);
18
17
  const roster = [...landed.joined];
19
18
  const plannedByIndex = new Map();
@@ -31,10 +30,13 @@ export function deriveDeliveryModel(squareState) {
31
30
  const sayItem = item;
32
31
  const audience = audienceOf(sayItem);
33
32
  const recipients = landed.recipientsFor(sayItem);
33
+ const replyAuthor = sayItem.reply === undefined ? undefined : authors.get(sayItem.reply);
34
34
  const planned = recipients.map((recipient) => {
35
35
  const route = audience.kind === 'bell'
36
36
  ? 'bell'
37
- : matchesMentionTarget(sayItem, recipient) ? 'mention' : 'attention';
37
+ : matchesMentionTarget(sayItem, recipient) ? 'mention'
38
+ : replyAuthor !== undefined && sameName(replyAuthor, recipient) ? 'reply'
39
+ : 'attention';
38
40
  return { item: sayItem, recipient, route };
39
41
  });
40
42
  plannedByIndex.set(item.index, planned);
@@ -75,48 +77,14 @@ export function deriveDeliveryModel(squareState) {
75
77
  replayedActivityCount: perception.replayedActivityCount,
76
78
  };
77
79
  }
78
- export function planActNotifications(squareState, item, delivery = deriveDeliveryModel(squareState)) {
79
- return delivery.plan(item);
80
- }
81
80
  export function perceiveActivity(squareState, item, viewer, delivery = deriveDeliveryModel(squareState)) {
82
81
  return delivery.perceive(item, viewer);
83
82
  }
84
- /** Mark only the notifications selected by the canonical catch projection as fully seen. */
85
- export function markSeenNotifications(squareState, recipient, delivered, at = Date.now(), delivery = deriveDeliveryModel(squareState)) {
86
- const deliveredIndexes = new Set(delivered.map((item) => item.index));
87
- let changed = false;
88
- for (const notification of delivery.pendingFor(recipient)) {
89
- if (!deliveredIndexes.has(notification.item.index))
90
- continue;
91
- changed = recordObservation(squareState, notification.recipient, notification.item.index, 'seen', at) || changed;
92
- }
93
- return changed;
94
- }
95
- /** Canonical say-activity filter shared by catch selection and hook ownership. */
96
- export function matchesCatchFilter(activity, filter) {
97
- if (filter.participants !== undefined &&
98
- !filter.participants.some((participant) => sameName(participant, activity.actor))) {
99
- return false;
100
- }
101
- if (audienceOf(activity).kind === 'bell')
102
- return true;
103
- if (filter.mention === undefined)
104
- return true;
105
- return activity.recipients?.some((recipient) => sameName(recipient, filter.mention)) === true
106
- || matchesMentionTarget(activity, filter.mention);
107
- }
108
- /** True only when the live catch's own filters would deliver this notification. */
83
+ /** True only when the live catch's own filters would deliver this unread notification, independent of page size. */
109
84
  export function leaseOwnsNotification(lease, notification) {
110
- if (lease.filter?.mention !== undefined
111
- && notification.route !== 'bell'
112
- && notification.recipient !== undefined
113
- && !sameName(lease.filter.mention, notification.recipient))
114
- return false;
115
- return matchesCatchFilter({
116
- actor: notification.actor,
117
- body: notification.body,
118
- ...(notification.mentions === undefined ? {} : { mentions: notification.mentions }),
119
- ...(notification.recipient === undefined ? {} : { recipients: [notification.recipient] }),
120
- ...(notification.route === 'bell' ? { reach: 'bell' } : {}),
121
- }, lease.filter ?? {});
85
+ return matchesCatchSelection(notification.actor, notification.route === 'bell'
86
+ ? { kind: 'bell' }
87
+ : { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {},
88
+ // A reply reaches the recipient as the author of the activity it answers.
89
+ notification.route === 'reply' ? notification.recipient : undefined);
122
90
  }
@@ -1,16 +1,14 @@
1
1
  const MAX_STRING_LENGTH = 1_000;
2
2
  const MAX_COLLECTION_ITEMS = 32;
3
3
  const MAX_NESTING = 8;
4
- function redactText(value, secret) {
5
- const withoutSecret = secret?.trim() ? value.split(secret.trim()).join('[redacted]') : value;
6
- const withoutUrlPassword = withoutSecret.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
7
- return [...withoutUrlPassword].length <= MAX_STRING_LENGTH
8
- ? withoutUrlPassword
9
- : `${[...withoutUrlPassword].slice(0, MAX_STRING_LENGTH - 1).join('')}…`;
10
- }
11
4
  function redactValue(value, secret, seen, depth) {
12
- if (typeof value === 'string')
13
- return redactText(value, secret);
5
+ if (typeof value === 'string') {
6
+ const withoutSecret = secret?.trim() ? value.split(secret.trim()).join('[redacted]') : value;
7
+ const withoutUrlPassword = withoutSecret.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
8
+ return [...withoutUrlPassword].length <= MAX_STRING_LENGTH
9
+ ? withoutUrlPassword
10
+ : `${[...withoutUrlPassword].slice(0, MAX_STRING_LENGTH - 1).join('')}…`;
11
+ }
14
12
  if (depth >= MAX_NESTING)
15
13
  return '[truncated]';
16
14
  if (Array.isArray(value)) {
@@ -1,4 +1,3 @@
1
- export declare function canonicalFilePath(value: string): Promise<string>;
2
1
  export interface VersionObserver<T> {
3
2
  readonly generation: number;
4
3
  hinted(since: number, timeoutMs: number, signal?: AbortSignal): Promise<boolean>;