@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
@@ -2,12 +2,8 @@ import { collectPending } from './delivery.js';
2
2
  import { formatActivityId } from './square-core.js';
3
3
  import { participantIdentity, truncateChars } from './presentation.js';
4
4
  import { formatDuration } from './time.js';
5
- import { projectWakeEvidence } from './wake-eligibility.js';
6
- import { decodeWakeEvidence } from './wake-evidence.js';
7
- import { hostLedgerForEnv } from './registry.js';
8
- import { openSquare } from './open-square.js';
9
- import { closeOpenSquare } from './open-square.js';
10
- import { readWakeReleaseDiagnostics } from './wake-attempts.js';
5
+ import { InternalSquareError, SquareError } from './model.js';
6
+ import { createArtifactPort } from './artifact.js';
11
7
  const DISPLAY_ORDER = [
12
8
  'awaiting',
13
9
  'wake-accepted',
@@ -18,46 +14,68 @@ const DISPLAY_ORDER = [
18
14
  const ACTIONABLE = new Set(['wake-unknown', 'unreachable']);
19
15
  const DETAIL_LIMIT = 20;
20
16
  const DISPLAY_FIELD_LIMIT = 160;
21
- /** Purely classify current pending attention from the artifact and durable ledgers. */
17
+ /** Classify current pending attention from the artifact's seats and delivery evidence. */
22
18
  export async function classifyDeliveryHealth(squarePath, opts) {
23
19
  const now = opts.now ?? Date.now();
24
- const env = opts.env ?? process.env;
25
- const square = await openSquare(squarePath, { clock: () => now, env });
20
+ const artifact = createArtifactPort(squarePath);
26
21
  try {
27
- const { scan, routes, pending } = await collectPending(square.artifact);
28
- const evidenceProjection = await projectWakeEvidence({ location: squarePath, routes, pending, hostLedger: square.hostLedger, now });
22
+ const { pending } = await collectPending(artifact);
23
+ const current = (await artifact.read((facts) => pending.map((note) => {
24
+ const seat = facts.seat(note.recipient);
25
+ const evidence = seat === undefined ? [] : facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(note.index));
26
+ return { note, seat, evidence };
27
+ }))).value;
29
28
  const items = [];
30
- for (const participant of scan.roster) {
31
- for (const note of pending.filter((head) => head.recipient === participant.name)) {
32
- const ageMs = Math.max(0, now - note.at);
33
- const evidence = evidenceProjection.evidence(note.recipient, note.index);
34
- const kind = evidence.presented
35
- ? 'presented-not-delivered'
36
- : evidence.terminal?.outcome === 'accepted'
37
- ? 'wake-accepted'
38
- : evidence.terminal?.outcome === 'unknown'
39
- ? 'wake-unknown'
40
- : ageMs > opts.graceMs && evidence.attemptableRoutes.length === 0
41
- ? 'unreachable'
42
- : 'awaiting';
43
- const attempt = evidence.terminal ?? evidence.attempts.at(-1);
44
- items.push({
45
- squarePath,
46
- recipient: note.recipient,
47
- actIndex: note.index,
48
- actor: note.actor,
49
- at: note.at,
50
- ageMs,
51
- route: note.route,
52
- kind,
53
- ...(attempt === undefined ? {} : { attempt }),
54
- });
55
- }
29
+ for (const { note, seat, evidence } of current) {
30
+ const ageMs = Math.max(0, now - note.at);
31
+ const presentation = evidence.some((row) => row.kind === 'presentation' && (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
32
+ || row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
33
+ const terminal = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown'));
34
+ const attemptRow = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'failed'));
35
+ const attempt = attemptRow === undefined || seat === undefined || attemptRow.route?.kind === undefined ? undefined : {
36
+ at: attemptRow.at,
37
+ attention: { squarePath, recipient: seat.participant, actIndex: note.index },
38
+ routeKind: attemptRow.route.kind,
39
+ outcome: attemptRow.outcome,
40
+ attemptN: attemptRow.attemptN,
41
+ session: seat.body.session,
42
+ ...(attemptRow.signature === undefined ? {} : { signature: attemptRow.signature }),
43
+ ...(attemptRow.message === undefined ? {} : { message: attemptRow.message }),
44
+ ...(attemptRow.diagnostic === undefined ? {} : { diagnostic: attemptRow.diagnostic }),
45
+ ...(attemptRow.nativeObservations === undefined ? {} : { nativeObservations: attemptRow.nativeObservations }),
46
+ };
47
+ const kind = presentation
48
+ ? 'presented-not-delivered'
49
+ : terminal?.outcome === 'accepted'
50
+ ? 'wake-accepted'
51
+ : terminal?.outcome === 'unknown'
52
+ ? 'wake-unknown'
53
+ : ageMs > opts.graceMs && seat?.wakeAddress === undefined
54
+ ? 'unreachable'
55
+ : 'awaiting';
56
+ items.push({
57
+ squarePath,
58
+ recipient: note.recipient,
59
+ actIndex: note.index,
60
+ actor: note.actor,
61
+ at: note.at,
62
+ ageMs,
63
+ route: note.route,
64
+ kind,
65
+ ...(attempt === undefined ? {} : { attempt }),
66
+ });
56
67
  }
68
+ // The captured pending set is the ownership boundary: a candidate that was
69
+ // not pending at the scan cannot become a health item during this read.
57
70
  return items;
58
71
  }
72
+ catch (error) {
73
+ if (error instanceof InternalSquareError && error.code === 'not_found')
74
+ throw new SquareError('unavailable', `Square is unavailable at ${squarePath}`);
75
+ throw error;
76
+ }
59
77
  finally {
60
- await closeOpenSquare(square);
78
+ await artifact.close();
61
79
  }
62
80
  }
63
81
  function displayField(value) {
@@ -110,13 +128,58 @@ function formatRelease(release, now) {
110
128
  const diagnostic = release.diagnostic === undefined ? '' : ` · ${displayField(JSON.stringify(release.diagnostic))}`;
111
129
  return ` · ${formatActivityId(release.attention.actIndex)} → ${displayField(participantIdentity(release.attention.recipient))} · ${route} · ${reason} · ${age}${message}${diagnostic}`;
112
130
  }
131
+ function deliveryAttempt(squarePath, row, session) {
132
+ if (row.route?.kind === undefined || row.outcome === 'released' || row.outcome === 'dispatching')
133
+ return undefined;
134
+ return {
135
+ at: row.at,
136
+ attention: { squarePath, recipient: row.participant, actIndex: Number(row.activity.slice(4)) },
137
+ routeKind: row.route.kind,
138
+ outcome: row.outcome,
139
+ attemptN: row.attemptN,
140
+ ...(session === undefined ? {} : { session }),
141
+ ...(row.signature === undefined ? {} : { signature: row.signature }),
142
+ ...(row.message === undefined ? {} : { message: row.message }),
143
+ ...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
144
+ ...(row.nativeObservations === undefined ? {} : { nativeObservations: row.nativeObservations }),
145
+ };
146
+ }
147
+ function deliveryRelease(squarePath, row) {
148
+ return {
149
+ at: row.at,
150
+ attention: { squarePath, recipient: row.participant, actIndex: Number(row.activity.slice(4)) },
151
+ routeKind: row.route?.kind,
152
+ attemptN: row.attemptN,
153
+ ...(row.signature === undefined ? {} : { signature: row.signature }),
154
+ ...(row.message === undefined ? {} : { message: row.message }),
155
+ ...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
156
+ };
157
+ }
113
158
  export async function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
114
159
  const items = await classifyDeliveryHealth(squarePath, { graceMs, now, env });
115
- const releases = await readWakeReleaseDiagnostics({ location: squarePath, now, env });
116
- const rows = await hostLedgerForEnv(env).listEvidence({ location: squarePath, kind: 'wake', includeReleased: true, now });
117
- const nativeAttempts = rows.flatMap((row) => {
118
- const decoded = decodeWakeEvidence(row, now);
119
- return decoded?.kind === 'attempt' && decoded.value.nativeObservations?.length ? [decoded.value] : [];
120
- }).sort((a, b) => Math.max(...b.nativeObservations.map((row) => row.observedAt)) - Math.max(...a.nativeObservations.map((row) => row.observedAt)));
121
- return renderDeliveryHealth(items, releases, now, nativeAttempts);
160
+ const artifact = createArtifactPort(squarePath);
161
+ try {
162
+ const { value: rows } = await artifact.read((facts) => facts.roster().flatMap((participant) => {
163
+ const seat = facts.seat(participant.name);
164
+ return seat === undefined ? [] : facts.deliveryEvidence(seat.participant, seat.generation);
165
+ }));
166
+ const releases = rows.filter((row) => row.kind === 'wake' && row.outcome === 'released')
167
+ .map((row) => deliveryRelease(squarePath, row))
168
+ .sort((left, right) => right.at - left.at);
169
+ const nativeAttempts = rows.flatMap((row) => {
170
+ if (row.kind !== 'wake' || row.nativeObservations?.length === 0)
171
+ return [];
172
+ const attempt = deliveryAttempt(squarePath, row);
173
+ return attempt?.nativeObservations?.length ? [attempt] : [];
174
+ }).sort((a, b) => Math.max(...b.nativeObservations.map((row) => row.observedAt)) - Math.max(...a.nativeObservations.map((row) => row.observedAt)));
175
+ return renderDeliveryHealth(items, releases, now, nativeAttempts);
176
+ }
177
+ catch (error) {
178
+ if (error instanceof InternalSquareError && error.code === 'not_found')
179
+ throw new SquareError('unavailable', `Square is unavailable at ${squarePath}`);
180
+ throw error;
181
+ }
182
+ finally {
183
+ await artifact.close();
184
+ }
122
185
  }
@@ -1,8 +1,7 @@
1
- import type { HostLedgerPort, SquareArtifactPort, DeliverPendingInput, DeliveryResult } from './ports.js';
1
+ import type { SquareArtifactPort, DeliverPendingInput, DeliveryResult } from './ports.js';
2
2
  export declare function deliverPending(input: DeliverPendingInput): Promise<DeliveryResult>;
3
3
  export declare function sweepPending(input: {
4
4
  readonly artifact: SquareArtifactPort;
5
- readonly hostLedger: HostLedgerPort;
6
5
  readonly location: string;
7
6
  readonly now: number;
8
7
  readonly graceMs: number;
@@ -1,23 +1,41 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { formatActivityId, parseActivityId } from './square-core.js';
2
- import { capturePendingScan, collectPending, pendingFacts, pendingHead, pendingPages, sameJoin } from './delivery.js';
3
- import { projectWakeEvidence, wakeIsEligible } from './wake-eligibility.js';
3
+ import { sameBody } from './model.js';
4
+ import { pendingHead, pendingHeads, pendingRecipients, presentationBlocksWake } from './delivery.js';
5
+ import { asSeatRef } from './seats.js';
4
6
  import { canonicalPath } from './canonical-path.js';
5
- import { retireWakeRouteFromArtifact } from './routes.js';
6
- import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
7
- async function releaseWakeClaim(input, details) {
8
- await input.hostLedger.releaseEvidence({
9
- location: input.location,
10
- participant: input.participant,
11
- session: input.session,
12
- activity: input.activity,
13
- kind: 'wake',
14
- claimToken: input.claimToken,
15
- ...(input.routeKind === undefined ? {} : { routeKind: input.routeKind }),
16
- ...(input.attemptN === undefined ? {} : { attemptN: input.attemptN }),
17
- signature: details.signature,
18
- message: redactCurrentDiagnostic(details.message),
19
- ...(details.diagnostic === undefined ? {} : { diagnostic: redactCurrentDiagnostic(details.diagnostic) }),
7
+ import { freshWatchLease } from './runtime.js';
8
+ import { leaseOwnsNotification } from './delivery.js';
9
+ function routeFor(location, seat, now) {
10
+ if (seat.wakeAddress === undefined)
11
+ return undefined;
12
+ return {
13
+ location, participant: seat.participant, sessionId: seat.body.session, channel: seat.body.channel,
14
+ kind: seat.wakeAddress.kind, address: { ...seat.wakeAddress.address }, updatedAt: now,
15
+ };
16
+ }
17
+ async function currentness(artifact, request, now) {
18
+ const index = parseActivityId(request.activity);
19
+ const { value } = await artifact.read((facts) => {
20
+ const currentSeat = facts.seat(request.participant);
21
+ const seatCurrent = currentSeat !== undefined && currentSeat.generation === request.seat.generation
22
+ && sameBody(currentSeat.body, request.seat.body)
23
+ && currentSeat.wakeAddress?.kind === request.route.kind
24
+ && Object.keys(currentSeat.wakeAddress.address).length === Object.keys(request.route.address).length
25
+ && Object.entries(currentSeat.wakeAddress.address).every(([key, val]) => request.route.address[key] === val);
26
+ const head = pendingHead(facts, request.participant, index);
27
+ const activityPending = seatCurrent && head !== undefined;
28
+ const cancelledThrough = facts.cancelledThrough(request.participant, request.seat.generation);
29
+ const cancelled = cancelledThrough !== undefined && index <= cancelledThrough;
30
+ const evidence = facts.deliveryEvidence(request.participant, request.seat.generation, formatActivityId(index));
31
+ const presented = presentationBlocksWake(evidence, now);
32
+ const lease = freshWatchLease(facts, request.participant, now);
33
+ const caught = head !== undefined && lease !== undefined && leaseOwnsNotification(lease, {
34
+ actor: request.actor, route: head.route, recipient: request.participant,
35
+ });
36
+ return { current: activityPending && !cancelled && !presented && !caught, activityPending, seatCurrent, cancelled, presented };
20
37
  });
38
+ return value;
21
39
  }
22
40
  async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
23
41
  let timer;
@@ -32,201 +50,142 @@ async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
32
50
  clearTimeout(timer);
33
51
  }
34
52
  }
53
+ async function candidates(artifact, requested) {
54
+ const { value } = await artifact.read((facts) => {
55
+ const heads = requested === undefined
56
+ ? facts.roster().flatMap((participant) => participant.joined ? pendingHeads(facts, participant.name) : [])
57
+ : pendingRecipients(facts, requested);
58
+ return heads.flatMap((head) => {
59
+ const seat = facts.seat(head.recipient);
60
+ return seat === undefined ? [] : [{ seat, head }];
61
+ });
62
+ });
63
+ return value;
64
+ }
65
+ function releaseClaim(artifact, seat, activity, claimToken, signature, message, diagnostic) {
66
+ return artifact.completeDelivery({ seat, activity, claimToken, outcome: 'released', at: Date.now(), signature,
67
+ ...(message === undefined ? {} : { message }),
68
+ ...(diagnostic === undefined ? {} : { diagnostic: safeJson(diagnostic) }),
69
+ }).catch(() => false);
70
+ }
71
+ function safeJson(value) {
72
+ if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
73
+ return value;
74
+ if (Array.isArray(value))
75
+ return value.map(safeJson);
76
+ if (typeof value === 'object')
77
+ return Object.fromEntries(Object.entries(value)
78
+ .filter(([, item]) => item !== undefined).map(([key, item]) => [key, safeJson(item)]));
79
+ return String(value);
80
+ }
35
81
  export async function deliverPending(input) {
36
82
  const location = await canonicalPath(input.location);
37
- const requested = input.activity === undefined ? undefined : typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
83
+ const requested = input.activity === undefined ? undefined
84
+ : typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
38
85
  if (input.activity !== undefined && requested === undefined)
39
86
  return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 0 };
40
- const owners = await input.hostLedger.listPresence({ location, now: input.now ?? Date.now() });
41
- const { scan, routes, pending } = await collectPending(input.artifact, requested);
42
- const eligibility = await projectWakeEvidence({ routes, pending, owners, location, hostLedger: input.hostLedger, now: input.now ?? Date.now() });
43
- const observeCurrentness = async (request, now) => {
44
- const index = parseActivityId(request.activity);
45
- const captured = scan.roster.find((participant) => participant.name === request.participant);
46
- const { value: facts } = await input.artifact.read((facts) => {
47
- const head = sameJoin(facts, captured) ? pendingHead(facts, request.participant, index) : undefined;
48
- return { routes: facts.routes(), pending: head === undefined ? [] : [head] };
49
- });
50
- const projection = await projectWakeEvidence({ ...facts, bindings: owners, location, hostLedger: input.hostLedger, now });
51
- return { ...projection.currentness({ ...request, location }), observationAvailable: true };
52
- };
53
- let attempted = 0;
54
- let accepted = 0;
55
- let failed = 0;
56
- let unknown = 0;
57
- let notCapable = 0;
58
- for (const participant of scan.roster) {
59
- const recipient = participant.name;
60
- for (const notification of pending.filter((head) => head.recipient === recipient)) {
61
- const evidence = eligibility.evidence(recipient, notification.index);
62
- if (evidence.terminal !== undefined)
63
- continue;
64
- const candidates = evidence.attemptableRoutes;
65
- let acceptedForAttention = false;
66
- let failedForAttention = false;
67
- let unknownForAttention = false;
68
- let notCapableForAttention = false;
69
- if (candidates.length === 0 && !evidence.presented)
70
- notCapableForAttention = true;
71
- for (const route of candidates) {
72
- const activity = formatActivityId(notification.index);
73
- const attention = { squarePath: location, actIndex: notification.index, recipient };
74
- const leaseMs = input.timeoutMs ?? 5000;
75
- const requestRoute = route;
76
- if (input.transport.probe !== undefined) {
77
- try {
78
- const probe = await input.transport.probe(requestRoute);
79
- if (probe === false || (typeof probe === 'object' && probe.outcome === 'not-capable')) {
80
- notCapableForAttention = true;
81
- continue;
82
- }
83
- }
84
- catch {
85
- notCapableForAttention = true;
86
- continue;
87
- }
88
- }
89
- let claim;
90
- try {
91
- claim = await input.hostLedger.claimWakeAttempt({ attention, session: route.sessionId, routeKind: route.kind, leaseMs });
92
- }
93
- catch {
94
- notCapableForAttention = true;
95
- continue;
96
- }
97
- if (claim.status === 'degraded') {
98
- notCapableForAttention = true;
99
- continue;
100
- }
101
- // Busy and terminal outcomes are attention-wide: no other session or route may dispatch.
102
- // A terminal attempt was already accepted or may already have been sent, so it reports nothing new.
103
- if (claim.status === 'busy')
104
- break;
105
- if (claim.status === 'terminal')
106
- break;
107
- const { claimToken, attemptN } = claim;
108
- const request = { location: input.location, participant: recipient, activity, actor: notification.actor, route: requestRoute };
109
- const claimRelease = { hostLedger: input.hostLedger, location, participant: recipient, session: route.sessionId, activity, claimToken, routeKind: route.kind, attemptN };
110
- // One teardown path for every exit after claim acquisition; each call site keeps its own error policy.
111
- const abandon = async (details, options = {}) => {
112
- const release = releaseWakeClaim(claimRelease, details);
113
- if (options.bestEffortClaim === true)
114
- await release.catch(() => undefined);
115
- else
116
- await release;
117
- };
118
- let current;
119
- try {
120
- current = await observeCurrentness(request, input.now ?? Date.now());
121
- }
122
- catch {
123
- 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);
124
- continue;
125
- }
126
- if (!current.current) {
127
- await abandon({
128
- signature: current.presented ? 'presentation_already_recorded' : 'pre_send_revalidation_failed',
129
- 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.',
130
- diagnostic: current,
131
- }, { bestEffortClaim: true }).catch(() => undefined);
132
- continue;
133
- }
134
- let outcome;
135
- let suppressedDuringSend = false;
136
- let finalCurrentness;
137
- const beforeSend = async () => {
138
- try {
139
- finalCurrentness = await observeCurrentness(request, Date.now());
140
- suppressedDuringSend = !finalCurrentness.current;
141
- }
142
- catch {
143
- suppressedDuringSend = true;
144
- }
145
- return !suppressedDuringSend;
146
- };
147
- try {
148
- outcome = await attemptWakeWithin(input.transport, { ...request, claimToken, attemptN }, leaseMs, beforeSend);
149
- }
150
- catch (error) {
151
- outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
152
- }
153
- if (suppressedDuringSend) {
154
- await abandon({
155
- signature: finalCurrentness?.presented === true ? 'presentation_recorded_during_dispatch' : 'pre_send_revalidation_failed',
156
- 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.',
157
- diagnostic: finalCurrentness ?? { observationAvailable: false },
158
- }, { bestEffortClaim: true }).catch(() => undefined);
159
- continue;
160
- }
161
- attempted += 1;
162
- if (outcome.outcome === 'not-capable') {
163
- await abandon({
164
- signature: 'transport_not_capable',
165
- message: 'The wake transport reported that it could not deliver this route.',
166
- diagnostic: outcome.diagnostic,
167
- }, { bestEffortClaim: true }).catch(() => undefined);
168
- notCapableForAttention = true;
169
- continue;
170
- }
171
- if (outcome.outcome === 'failed' && outcome.unavailable) {
172
- if (outcome.routeStale === true)
173
- await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.sessionId });
174
- await abandon({
175
- signature: outcome.signature ?? 'transport_unavailable',
176
- message: outcome.message ?? 'The wake transport was unavailable.',
177
- diagnostic: outcome.diagnostic,
178
- });
179
- failedForAttention = true;
87
+ const pending = await candidates(input.artifact, requested);
88
+ let attempted = 0, accepted = 0, failed = 0, unknown = 0, notCapable = 0;
89
+ for (const { seat, head } of pending) {
90
+ const route = routeFor(location, seat, input.now ?? Date.now());
91
+ if (route === undefined) {
92
+ notCapable += 1;
93
+ continue;
94
+ }
95
+ const request = {
96
+ location, participant: seat.participant, activity: formatActivityId(head.index), actor: head.actor,
97
+ seat: asSeatRef(seat), route,
98
+ artifact: input.artifact,
99
+ };
100
+ if (input.transport.probe !== undefined) {
101
+ try {
102
+ const probe = await input.transport.probe(route);
103
+ if (probe === false || typeof probe === 'object' && probe.outcome === 'not-capable') {
104
+ notCapable += 1;
180
105
  continue;
181
106
  }
182
- const safeOutcome = redactCurrentDiagnostic(outcome);
183
- 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 });
184
- if (outcome.outcome === 'accepted') {
185
- acceptedForAttention = true;
186
- break;
187
- }
188
- if (outcome.outcome === 'failed')
189
- failedForAttention = true;
190
- else {
191
- unknownForAttention = true;
192
- break;
193
- }
194
107
  }
195
- if (acceptedForAttention)
196
- accepted += 1;
197
- else if (unknownForAttention)
198
- unknown += 1;
199
- else if (failedForAttention)
200
- failed += 1;
201
- else if (notCapableForAttention)
108
+ catch {
202
109
  notCapable += 1;
110
+ continue;
111
+ }
203
112
  }
113
+ const claimToken = randomUUID();
114
+ const leaseMs = input.timeoutMs ?? 5000;
115
+ const claim = await input.artifact.claimDelivery({ seat: request.seat, activity: request.activity, kind: 'wake',
116
+ route: seat.wakeAddress, at: input.now ?? Date.now(), leaseMs, claimToken }).catch(() => ({ status: 'stale' }));
117
+ if (claim.status !== 'acquired')
118
+ continue;
119
+ const withClaim = { ...request, claimToken, attemptN: claim.attemptN };
120
+ let final;
121
+ let suppressed = false;
122
+ const beforeSend = async () => {
123
+ try {
124
+ final = await currentness(input.artifact, withClaim, Date.now());
125
+ suppressed = !final.current;
126
+ }
127
+ catch {
128
+ suppressed = true;
129
+ }
130
+ return !suppressed;
131
+ };
132
+ let before;
133
+ try {
134
+ before = await currentness(input.artifact, withClaim, input.now ?? Date.now());
135
+ }
136
+ catch { /* unverifiable */ }
137
+ if (!before?.current) {
138
+ await releaseClaim(input.artifact, request.seat, request.activity, claimToken, 'pre_send_revalidation_failed', before === undefined ? 'Square delivery state could not be verified.' : 'The notification is no longer current.', before);
139
+ continue;
140
+ }
141
+ let outcome;
142
+ try {
143
+ outcome = await attemptWakeWithin(input.transport, withClaim, leaseMs, beforeSend);
144
+ }
145
+ catch (error) {
146
+ outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
147
+ }
148
+ if (suppressed) {
149
+ await releaseClaim(input.artifact, request.seat, request.activity, claimToken, 'pre_send_revalidation_failed', 'The notification changed before the final send check.', final ?? { observationAvailable: false });
150
+ continue;
151
+ }
152
+ if (outcome.outcome === 'not-capable') {
153
+ await releaseClaim(input.artifact, request.seat, request.activity, claimToken, 'transport_not_capable', undefined, outcome.diagnostic);
154
+ notCapable += 1;
155
+ continue;
156
+ }
157
+ attempted += 1;
158
+ const mapped = outcome.outcome === 'accepted' ? 'accepted'
159
+ : outcome.outcome === 'unknown' ? 'unknown' : 'failed';
160
+ const completed = await input.artifact.completeDelivery({ seat: request.seat, activity: request.activity, claimToken,
161
+ outcome: mapped, at: Date.now(),
162
+ ...('signature' in outcome && outcome.signature !== undefined ? { signature: outcome.signature } : {}),
163
+ ...('message' in outcome && outcome.message !== undefined ? { message: outcome.message } : {}),
164
+ ...('diagnostic' in outcome && outcome.diagnostic !== undefined ? { diagnostic: safeJson(outcome.diagnostic) } : {}),
165
+ }).catch(() => false);
166
+ if (!completed)
167
+ continue;
168
+ if (mapped === 'accepted')
169
+ accepted += 1;
170
+ else if (mapped === 'unknown')
171
+ unknown += 1;
172
+ else
173
+ failed += 1;
204
174
  }
205
175
  return { attempted, accepted, failed, unknown, notCapable };
206
176
  }
207
177
  export async function sweepPending(input) {
208
178
  if (input.limit <= 0)
209
179
  return [];
210
- const location = await canonicalPath(input.location);
211
- const owners = await input.hostLedger.listPresence({ location, now: input.now });
212
- const { value: scan } = await input.artifact.read(capturePendingScan);
180
+ const pending = await candidates(input.artifact);
213
181
  const selected = [];
214
- const indexes = new Set();
215
- for await (const page of pendingPages(input.artifact, scan)) {
216
- const { value: facts } = await input.artifact.read((facts) => pendingFacts(facts, scan, page));
217
- const eligibility = await projectWakeEvidence({ ...facts, bindings: owners, location, hostLedger: input.hostLedger, now: input.now });
218
- for (const head of facts.pending) {
219
- if (indexes.size >= input.limit && !indexes.has(head.index))
220
- break;
221
- if (input.now - head.at > input.graceMs && wakeIsEligible(eligibility.evidence(head.recipient, head.index))) {
222
- selected.push(head);
223
- indexes.add(head.index);
224
- }
225
- }
226
- if (indexes.size >= input.limit)
182
+ for (const { head } of pending) {
183
+ if (input.now - head.at <= input.graceMs)
184
+ continue;
185
+ if (!selected.includes(head.index))
186
+ selected.push(head.index);
187
+ if (selected.length >= input.limit)
227
188
  break;
228
189
  }
229
- const { value: facts } = await input.artifact.read((facts) => pendingFacts(facts, scan, selected));
230
- const eligibility = await projectWakeEvidence({ ...facts, bindings: owners, location, hostLedger: input.hostLedger, now: input.now });
231
- return [...new Set(facts.pending.filter((head) => wakeIsEligible(eligibility.evidence(head.recipient, head.index))).map((head) => head.index))];
190
+ return selected;
232
191
  }
@@ -1,5 +1,5 @@
1
- import type { DirectedNotificationRoute, StoredAct, WatchLease, WakeRouteKind } from './model.js';
2
- import type { DirectedHead, ParticipantFact, SquareArtifactPort, SquareRead, WakeOutcome } from './ports.js';
1
+ import type { DirectedNotificationRoute, StoredAct, WatchLease } from './model.js';
2
+ import type { DeliveryEvidence, DirectedHead, ParticipantFact, SquareArtifactPort, SquareRead, WakeRequest } from './ports.js';
3
3
  export type { DirectedNotificationRoute } from './model.js';
4
4
  export type SayItem = StoredAct & {
5
5
  kind: 'say';
@@ -9,30 +9,23 @@ export interface PlannedNotification {
9
9
  recipient: string;
10
10
  route: DirectedNotificationRoute;
11
11
  }
12
- /** gate-rejected means the supplied beforeSend returned false and nothing was sent. */
13
- export type WakeAdapterResult = Exclude<WakeOutcome, {
14
- outcome: 'not-capable';
15
- }> | {
16
- outcome: 'gate-rejected';
17
- };
18
- /** One attempt's stable coordinates; retries reuse its dedup key. */
19
- export interface WakeDispatchContext {
20
- readonly location: string;
21
- readonly participant: string;
22
- readonly activity: string;
23
- readonly attemptN?: number;
24
- }
25
- export interface WakeAdapter {
26
- readonly kind: WakeRouteKind;
27
- dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number, context?: WakeDispatchContext): Promise<WakeAdapterResult>;
28
- }
12
+ /** Exact stored activity projection for transports that send an awareness reminder. */
13
+ export declare function wakePreview(facts: SquareRead, request: WakeRequest): string | undefined;
29
14
  export interface RoutedNotification {
30
15
  actor: string;
31
16
  route: DirectedNotificationRoute;
32
17
  recipient: string;
33
18
  }
19
+ /** Presentation custody blocks a wake, independently of native wake acceptance. */
20
+ export declare function presentationBlocksWake(evidence: readonly DeliveryEvidence[], now: number): boolean;
21
+ /** Preselection only: an expired wake dispatch is uncertain, never safe automatic replay. */
22
+ export declare function automaticWakeEligible(evidence: readonly DeliveryEvidence[], now: number): boolean;
34
23
  /** Notification pending is not cursor suffix or historical catch membership. */
35
24
  export declare function pendingHead(facts: SquareRead, name: string, index: number): DirectedHead | undefined;
25
+ /** One activity's landed recipients that still owe a wake; the rest of the roster is untouched. */
26
+ export declare function pendingRecipients(facts: SquareRead, index: number): DirectedHead[];
27
+ /** Still-pending directed heads for one recipient, oldest first. Bodies belong to presentation. */
28
+ export declare function pendingHeads(facts: SquareRead, name: string): DirectedHead[];
36
29
  /** Complete recipient projection, assembled inside its one transaction. */
37
30
  export declare function pendingNotifications(facts: SquareRead, name: string): PlannedNotification[];
38
31
  export interface PendingScan {
@@ -51,12 +44,10 @@ export declare function pendingPages(artifact: SquareArtifactPort, scan: Pending
51
44
  }[], void, unknown>;
52
45
  /** A pass retains only its captured joins; routes and individual receipts are fresh. */
53
46
  export declare function pendingFacts(facts: SquareRead, scan: PendingScan, heads: readonly DirectedHead[]): {
54
- routes: readonly import("./ports.js").RouteFact[];
55
47
  pending: DirectedHead[];
56
48
  };
57
- /** Global discovery alone uses pendingAudience; exact dispatch uses its one landed audience. */
58
- export declare function collectPending(artifact: SquareArtifactPort, index?: number): Promise<{
59
- routes: readonly import("./ports.js").RouteFact[];
49
+ /** Global discovery alone sweeps pendingAudience; exact dispatch uses pendingRecipients. */
50
+ export declare function collectPending(artifact: SquareArtifactPort): Promise<{
60
51
  pending: DirectedHead[];
61
52
  scan: PendingScan;
62
53
  }>;