@astrosheep/square 0.3.106 → 0.4.1

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 (206) hide show
  1. package/dist/activity-feed.js +1 -1
  2. package/dist/artifact-store.d.ts +41 -0
  3. package/dist/artifact-store.js +272 -0
  4. package/dist/artifact.d.ts +6 -12
  5. package/dist/artifact.js +43 -344
  6. package/dist/attention-presentation.d.ts +13 -7
  7. package/dist/attention-presentation.js +29 -68
  8. package/dist/boundary-presentation.d.ts +3 -16
  9. package/dist/boundary-presentation.js +29 -109
  10. package/dist/card-presentation.d.ts +1 -0
  11. package/dist/card-presentation.js +2 -0
  12. package/dist/catch-decisions.d.ts +2 -2
  13. package/dist/catch-decisions.js +7 -3
  14. package/dist/catch-selection.d.ts +8 -0
  15. package/dist/catch-selection.js +5 -0
  16. package/dist/claude-delivery.d.ts +14 -12
  17. package/dist/claude-delivery.js +42 -142
  18. package/dist/claude-mcp-context.js +2 -2
  19. package/dist/claude-mod.js +28 -140
  20. package/dist/claude-transport.d.ts +5 -0
  21. package/dist/claude-transport.js +34 -0
  22. package/dist/cli/context.d.ts +2 -2
  23. package/dist/cli/context.js +7 -10
  24. package/dist/cli/maintenance-commands.d.ts +0 -4
  25. package/dist/cli/maintenance-commands.js +2 -29
  26. package/dist/cli/observation-commands.d.ts +2 -0
  27. package/dist/cli/observation-commands.js +12 -2
  28. package/dist/cli/program.js +1 -1
  29. package/dist/cli/registry.js +4 -3
  30. package/dist/codex-address.d.ts +2 -2
  31. package/dist/codex-boundary-state.d.ts +1 -5
  32. package/dist/codex-boundary-state.js +0 -4
  33. package/dist/codex-hook.d.ts +7 -10
  34. package/dist/codex-hook.js +69 -33
  35. package/dist/codex-queue.d.ts +10 -11
  36. package/dist/codex-queue.js +32 -34
  37. package/dist/codex-watch.d.ts +2 -0
  38. package/dist/codex-watch.js +100 -0
  39. package/dist/decisions.js +3 -3
  40. package/dist/delivery-deferral.d.ts +1 -19
  41. package/dist/delivery-deferral.js +1 -29
  42. package/dist/delivery-frame.d.ts +7 -0
  43. package/dist/delivery-frame.js +11 -0
  44. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  45. package/dist/delivery-ledger-sqlite.js +496 -0
  46. package/dist/delivery-ledger.d.ts +196 -0
  47. package/dist/delivery-ledger.js +268 -0
  48. package/dist/delivery-lifetime.d.ts +6 -0
  49. package/dist/delivery-lifetime.js +45 -0
  50. package/dist/harness-links.js +61 -7
  51. package/dist/harness.js +12 -17
  52. package/dist/help.js +3 -6
  53. package/dist/http-delivery.d.ts +5 -9
  54. package/dist/http-delivery.js +20 -79
  55. package/dist/inbox.d.ts +6 -57
  56. package/dist/inbox.js +24 -380
  57. package/dist/index.d.ts +7 -4
  58. package/dist/index.js +5 -2
  59. package/dist/mcp.d.ts +2 -1
  60. package/dist/mcp.js +40 -70
  61. package/dist/message.d.ts +7 -0
  62. package/dist/message.js +23 -0
  63. package/dist/model.d.ts +15 -91
  64. package/dist/model.js +2 -140
  65. package/dist/neutral.d.ts +72 -0
  66. package/dist/neutral.js +27 -0
  67. package/dist/notifications.d.ts +24 -26
  68. package/dist/notifications.js +39 -143
  69. package/dist/operation-control.d.ts +4 -0
  70. package/dist/operation-control.js +20 -0
  71. package/dist/operation-presentation.d.ts +1 -2
  72. package/dist/operation-presentation.js +1 -7
  73. package/dist/owner.d.ts +2 -2
  74. package/dist/owner.js +1 -1
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  77. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  78. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  79. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  80. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  81. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  82. package/dist/packages/agent-delivery/src/codex.js +41 -4
  83. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  84. package/dist/packages/agent-delivery/src/index.js +1 -1
  85. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  86. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  87. package/dist/paseo-delivery.d.ts +5 -5
  88. package/dist/paseo-delivery.js +19 -99
  89. package/dist/paseo.d.ts +1 -1
  90. package/dist/paseo.js +1 -1
  91. package/dist/plugins/opencode/index.d.ts +5 -3
  92. package/dist/plugins/opencode/index.js +99 -21
  93. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  94. package/dist/plugins/opencode-shared/delivery.js +109 -0
  95. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  96. package/dist/plugins/opencode-v1/index.js +41 -0
  97. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  98. package/dist/plugins/opencode-v1/runtime.js +152 -0
  99. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  100. package/dist/plugins/opencode-v1/transport.js +50 -0
  101. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  102. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  103. package/dist/ports.d.ts +33 -214
  104. package/dist/presentation.d.ts +5 -5
  105. package/dist/presentation.js +1 -0
  106. package/dist/runtime.js +2 -2
  107. package/dist/search.js +1 -1
  108. package/dist/session-lifecycle.d.ts +8 -7
  109. package/dist/session-lifecycle.js +40 -42
  110. package/dist/sqlite-framing.d.ts +8 -0
  111. package/dist/sqlite-framing.js +31 -0
  112. package/dist/sqlite-square.d.ts +16 -26
  113. package/dist/sqlite-square.js +199 -262
  114. package/dist/sqlite-values.d.ts +21 -0
  115. package/dist/sqlite-values.js +78 -0
  116. package/dist/square-api.d.ts +1 -1
  117. package/dist/square-api.js +5 -8
  118. package/dist/square-core.d.ts +0 -1
  119. package/dist/square-domain.d.ts +12 -10
  120. package/dist/square-domain.js +105 -223
  121. package/dist/square-facade.d.ts +25 -76
  122. package/dist/square-location.d.ts +6 -1
  123. package/dist/square-location.js +1 -1
  124. package/dist/square-post.d.ts +53 -0
  125. package/dist/square-post.js +4 -0
  126. package/dist/square-posting.d.ts +13 -0
  127. package/dist/square-posting.js +174 -0
  128. package/dist/square-producer-sqlite.d.ts +11 -0
  129. package/dist/square-producer-sqlite.js +210 -0
  130. package/dist/square-receive.d.ts +2 -3
  131. package/dist/square-sqlite.d.ts +33 -42
  132. package/dist/square-sqlite.js +236 -768
  133. package/dist/square-tools.d.ts +8 -0
  134. package/dist/square-tools.js +39 -0
  135. package/dist/square-values.d.ts +27 -0
  136. package/dist/square-values.js +90 -0
  137. package/dist/views.d.ts +6 -6
  138. package/dist/views.js +14 -9
  139. package/dist/wake-sink.d.ts +4 -11
  140. package/dist/wake-sink.js +8 -32
  141. package/dist/wake-transports.d.ts +4 -0
  142. package/dist/wake-transports.js +11 -0
  143. package/examples/symmetric-domains.mjs +29 -249
  144. package/guides/change-waits.md +13 -53
  145. package/guides/claude-native.md +19 -172
  146. package/guides/codex-delivery.md +55 -78
  147. package/guides/delivery-primitives.md +48 -207
  148. package/guides/operation-cancellation.md +2 -5
  149. package/guides/participant-ownership.md +25 -89
  150. package/guides/pi-native-delivery.md +19 -56
  151. package/guides/pi-tools.md +6 -8
  152. package/guides/row-runtime-delivery-evidence.md +2 -0
  153. package/guides/row-storage-design.md +19 -44
  154. package/guides/square-api.md +113 -538
  155. package/package.json +8 -8
  156. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  157. package/plugins/claude/hooks/hooks.json +1 -1
  158. package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
  159. package/plugins/claude/types.d.ts +1 -1
  160. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  161. package/plugins/codex/hooks/hooks.json +11 -23
  162. package/plugins/opencode/index.ts +91 -23
  163. package/plugins/opencode-shared/delivery.ts +91 -0
  164. package/plugins/opencode-v1/index.ts +36 -0
  165. package/plugins/opencode-v1/runtime.ts +134 -0
  166. package/plugins/opencode-v1/transport.ts +41 -0
  167. package/plugins/paseo/OVERVIEW.md +5 -5
  168. package/plugins/paseo/README.md +7 -6
  169. package/plugins/paseo/client/markdown.tsx +5 -5
  170. package/plugins/paseo/client/square-message.tsx +28 -24
  171. package/plugins/paseo/client/transform.ts +8 -5
  172. package/plugins/paseo/shared/card-presentation.ts +16 -0
  173. package/plugins/paseo/shared/message.ts +6 -11
  174. package/plugins/pi/square-pi.js +167 -451
  175. package/dist/channel.d.ts +0 -74
  176. package/dist/channel.js +0 -92
  177. package/dist/claude-delivery-status.d.ts +0 -13
  178. package/dist/claude-delivery-status.js +0 -46
  179. package/dist/cmd/notify-once.d.ts +0 -2
  180. package/dist/cmd/notify-once.js +0 -29
  181. package/dist/delivery-attempts.d.ts +0 -52
  182. package/dist/delivery-attempts.js +0 -73
  183. package/dist/delivery-effects.d.ts +0 -139
  184. package/dist/delivery-effects.js +0 -163
  185. package/dist/delivery-health.d.ts +0 -51
  186. package/dist/delivery-health.js +0 -173
  187. package/dist/delivery-operations.d.ts +0 -9
  188. package/dist/delivery-operations.js +0 -168
  189. package/dist/delivery-primitives.d.ts +0 -5
  190. package/dist/delivery-primitives.js +0 -3
  191. package/dist/delivery.d.ts +0 -45
  192. package/dist/delivery.js +0 -106
  193. package/dist/plugins/opencode/delivery.d.ts +0 -19
  194. package/dist/plugins/opencode/delivery.js +0 -238
  195. package/dist/presentation-operations.d.ts +0 -18
  196. package/dist/presentation-operations.js +0 -86
  197. package/dist/square-index.d.ts +0 -16
  198. package/dist/square-index.js +0 -106
  199. package/dist/square-legacy-framing.d.ts +0 -11
  200. package/dist/square-legacy-framing.js +0 -64
  201. package/dist/square-migration-host-ledger.d.ts +0 -26
  202. package/dist/square-migration-host-ledger.js +0 -169
  203. package/dist/square-migration.d.ts +0 -30
  204. package/dist/square-migration.js +0 -599
  205. package/guides/migration.md +0 -156
  206. package/plugins/opencode/delivery.ts +0 -211
@@ -1,120 +1,40 @@
1
- import { squareIndexPath } from './square-index.js';
2
- import { createHash } from 'node:crypto';
3
- import { withFileLock } from './file-lock.js';
4
- import { createArtifactPort } from './artifact.js';
5
- import { preparePendingPresentation } from './presentation-operations.js';
6
- import { presentPreparedBatch } from './delivery-effects.js';
7
- import { sessionInbox, projectBoundaryEligibility, pendingAtBoundary, pendingKey } from './inbox.js';
8
- import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
9
- export { pendingAtBoundary };
1
+ import { DeliveryLedger } from './delivery-ledger.js';
2
+ import { deliveryFrameOverhead, renderDeliveryReminder } from './delivery-frame.js';
10
3
  const CONTEXT_MAX = 1200;
11
- export function renderPendingAtBoundary(inbox) {
12
- return renderBoundary(inbox).context;
13
- }
14
- function renderBoundary(inbox) {
15
- const queued = inbox.flatMap((membership) => membership.notifications.map((notification) => ({ membership, notification })));
16
- const blocks = [];
17
- const agoraBlocks = [];
18
- const deliveries = [];
19
- let remainingAgora = queued.filter(({ notification }) => notification.delivery?.mode !== 'direct').length;
20
- const entries = [];
21
- let omitted = 0;
22
- const omission = (count) => `${count} more ${count === 1 ? 'activity' : 'activities'} not shown.`;
23
- for (const entry of queued) {
24
- const { membership, notification } = entry;
25
- // The omission line represents the rest of this batch too, but never consumes its bodies.
26
- const coverage = { membership, actIndex: notification.actIndex, markSeen: false };
27
- entries.push(coverage);
28
- const block = renderAttentionPreview({
29
- squarePath: membership.location,
30
- actIndex: notification.actIndex,
31
- recipient: membership.seat.participant,
32
- actor: notification.actor,
33
- route: notification.route,
34
- body: notification.body,
35
- delivery: notification.delivery,
36
- });
37
- if (notification.delivery?.mode !== 'direct') {
38
- remainingAgora -= 1;
39
- const omittedAfter = omitted + remainingAgora;
40
- const prospective = renderSquareReminder([...agoraBlocks, block, ...(omittedAfter > 0 ? [omission(omittedAfter)] : [])]);
41
- if (prospective.length > CONTEXT_MAX) {
42
- omitted += 1;
43
- continue;
44
- }
45
- agoraBlocks.push(block);
46
- }
47
- blocks.push(block);
48
- deliveries.push(notification.delivery);
49
- coverage.markSeen = !attentionBodyIsClipped(notification.body, notification.delivery);
50
- }
51
- return {
52
- context: renderSquareReminder([...blocks, ...(omitted > 0 ? [omission(omitted)] : [])], deliveries),
53
- entries,
54
- };
55
- }
56
- async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sessionInbox, env = process.env, signal) {
57
- const inbox = await lookup(identity, env, signal);
58
- if (signal?.aborted)
59
- return undefined;
60
- const pending = await projectBoundaryEligibility(inbox);
61
- if (pending.length === 0 || signal?.aborted)
62
- return undefined;
63
- const artifacts = new Map();
64
- const claims = [];
4
+ /** The sink must resolve only after the host has actually accepted the output. */
5
+ export async function presentPendingAtBoundary(owner, present, env = process.env, signal) {
6
+ const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
65
7
  try {
8
+ const session = await ledger.session(owner);
9
+ if (!session || session.state !== 'attached' || signal?.aborted)
10
+ return undefined;
11
+ const claim = await ledger.claimPresentation(owner, { generation: session.generation, budgetChars: CONTEXT_MAX - deliveryFrameOverhead({ tagged: true }), signal });
12
+ if (!claim)
13
+ return undefined;
14
+ let current;
66
15
  try {
67
- // Enumeration owns discovery/connections only; returned handles belong to the common runner.
68
- for (const entry of renderBoundary(pending).entries) {
69
- const { location, seat } = entry.membership;
70
- let artifact = artifacts.get(location);
71
- if (artifact === undefined) {
72
- // Keep connections alive through all signal-independent settlement/cleanup.
73
- artifact = createArtifactPort(location);
74
- artifacts.set(location, artifact);
75
- }
76
- const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
77
- activity: entry.actIndex, seat, markSeen: entry.markSeen, signal });
78
- if (claim !== undefined)
79
- claims.push({ key: pendingKey(entry.membership, entry.actIndex), claim });
80
- }
16
+ current = await claim.current(signal);
81
17
  }
82
18
  catch (error) {
83
- // Transfer has not happened: enumeration must abandon any custody already acquired.
84
- await Promise.all(claims.map(({ claim }) => claim.release()));
19
+ await claim.settle('released');
85
20
  throw error;
86
21
  }
87
- const report = await presentPreparedBatch(claims.map(({ claim }) => claim), eligibleClaims => {
88
- const eligible = new Set(claims.filter(({ claim }) => eligibleClaims.includes(claim)).map(({ key }) => key));
89
- const delivered = pending.map(membership => ({ ...membership,
90
- notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
91
- })).filter(membership => membership.notifications.length > 0);
92
- return { payload: { context: renderBoundary(delivered).context, delivered }, included: eligibleClaims };
93
- }, payload => present(payload.context, payload.delivered), { signal });
94
- if (report.observation.kind === 'threw')
95
- throw report.observation.error;
96
- for (const { result } of report.settlements)
97
- if (result.persistence.status === 'error')
98
- throw result.persistence.error;
99
- return report.observation.kind === 'returned' ? report.observation.value : undefined;
22
+ if (!current || signal?.aborted) {
23
+ await claim.settle('released');
24
+ return undefined;
25
+ }
26
+ let result;
27
+ try {
28
+ result = await present(renderDeliveryReminder(claim.cards.map(card => card.text), { tagged: true }));
29
+ }
30
+ catch (error) {
31
+ await claim.settle('unknown');
32
+ throw error;
33
+ }
34
+ await claim.settle('presented');
35
+ return result;
100
36
  }
101
37
  finally {
102
- await Promise.all([...artifacts.values()].map(artifact => artifact.close()));
38
+ await ledger.close();
103
39
  }
104
40
  }
105
- /**
106
- * Serialize a boundary across hook processes so one stale inbox cannot render twice.
107
- *
108
- * `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
109
- * verified against the current presence binding and artifact revision; the boundary still projects
110
- * presentation evidence live. Every item is claimed and revalidated before the batch sink;
111
- * after success, captured-seat commits record the winning batch, including budget omissions as
112
- * clipped (no consumption receipt), without an eligibility veto. Coverage is conservative if a
113
- * competing claim's removal lets a previously omitted body fit in the final preview.
114
- * A source that cannot verify its reuse must read the inbox again.
115
- */
116
- export async function presentPendingAtBoundary(identity, present, lookup = sessionInbox, env = process.env, signal) {
117
- const indexPath = squareIndexPath(env);
118
- const ownerKey = createHash('sha256').update(JSON.stringify(identity)).digest('hex');
119
- return withFileLock(`${indexPath}.boundary-${ownerKey}.lock`, { retryMs: 10, signal }, () => presentPendingAtBoundaryUnlocked(identity, present, lookup, env, signal));
120
- }
@@ -0,0 +1 @@
1
+ export { isCardPresentation, type CardPresentation } from './plugins/paseo/shared/card-presentation.js';
@@ -0,0 +1,2 @@
1
+ // Keep the shared schema inside the standalone Paseo plugin's package boundary.
2
+ export { isCardPresentation } from './plugins/paseo/shared/card-presentation.js';
@@ -1,4 +1,4 @@
1
- import { type StoredAct } from './model.js';
1
+ import { type HydratedAct } from './model.js';
2
2
  import type { ActivityIndex, DirectedFilter, SquareRead, SquareWrite } from './ports.js';
3
3
  import type { CatchOptions, CatchReason, PerceivedActivity } from './square-facade.js';
4
4
  export interface CatchSelection {
@@ -14,7 +14,7 @@ export interface CatchSelection {
14
14
  readonly hint?: string;
15
15
  }
16
16
  /** Capture a selected page (or an empty completion) without opening another resource. */
17
- export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly StoredAct[], filter: DirectedFilter, at: number, remaining?: number): CatchSelection;
17
+ export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly HydratedAct[], filter: DirectedFilter, at: number, remaining?: number): CatchSelection;
18
18
  export declare const CATCH_DEFAULT_LIMIT = 10;
19
19
  export declare const CATCH_MAX_LIMIT = 100;
20
20
  export declare function catchLimit(value: number | undefined): number;
@@ -33,7 +33,7 @@ function catchPageSelection(facts, requestedName, options) {
33
33
  const participant = resolveKnownName(facts, requestedName);
34
34
  const limit = catchLimit(options.limit);
35
35
  const senders = options.from?.map(name => resolveKnownName(facts, name));
36
- const filter = { unseenOnly: true, ...(senders === undefined ? {} : { senders }), ...(options.mention === true ? { attentionOnly: true } : {}) };
36
+ const filter = { kinds: ['say'], unseenOnly: true, ...(senders === undefined ? {} : { senders }), ...(options.mention === true ? { attentionOnly: true } : {}) };
37
37
  return { participant, filter, selection: { kind: 'page', query: filter, limit } };
38
38
  }
39
39
  /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
@@ -44,7 +44,7 @@ export function selectCatchPage(facts, requestedName, options) {
44
44
  /** Selection, receipts and resulting cursor belong to one artifact write transaction. */
45
45
  export function decideCatch(facts, requestedName, options, at) {
46
46
  let viewer;
47
- let filter = { unseenOnly: true };
47
+ let filter = { kinds: ['say'], unseenOnly: true };
48
48
  let selection;
49
49
  if (options.id !== undefined) {
50
50
  viewer = resolveKnownName(facts, requestedName);
@@ -66,6 +66,10 @@ export function decideCatch(facts, requestedName, options, at) {
66
66
  try {
67
67
  consumed = consumeReceived({
68
68
  ...squareReceiveRead(facts),
69
+ addressed(recipient, index) {
70
+ const head = facts.directed(recipient, { indexes: [index], kinds: ['say'] }, { edge: 'oldest', limit: 1 }).items[0];
71
+ return head === undefined ? undefined : { key: head.index, via: head.route };
72
+ },
69
73
  insertReceipt: (ref, time) => facts.markSeen(ref.recipient, [ref.item], time) > 0,
70
74
  }, viewer, selection, at);
71
75
  }
@@ -85,7 +89,7 @@ export function decideCatch(facts, requestedName, options, at) {
85
89
  const delivered = consumed.items.map(item => item.data);
86
90
  const observed = observeCatchSelection(facts, viewer, delivered, filter, at, consumed.remaining);
87
91
  // Receipts are the durable catch count: everything directed here minus what is still unseen.
88
- const caught = facts.directedCount(viewer, {}) - facts.directedCount(viewer, { unseenOnly: true });
92
+ const caught = facts.directedCount(viewer, { kinds: ['say'] }) - facts.directedCount(viewer, { kinds: ['say'], unseenOnly: true });
89
93
  // An exact id catch is a targeted fetch and stays repeatable, so it carries no nudge.
90
94
  const hint = options.id === undefined ? catchHint(caught - consumed.newlyConsumed, caught) : undefined;
91
95
  return hint === undefined ? observed : { ...observed, hint };
@@ -4,3 +4,11 @@ export declare function matchesCatchSelection(actor: string, audience: Audience,
4
4
  readonly participants?: readonly string[];
5
5
  readonly mention?: string;
6
6
  }, replyAuthor?: string): boolean;
7
+ /** One directed arrival and the upper reason it reached its recipient. */
8
+ export interface RoutedNotification {
9
+ readonly actor: string;
10
+ readonly route: import('./model.js').DirectedNotificationRoute;
11
+ readonly recipient: string;
12
+ }
13
+ /** A live catch owns every matching notification, independent of its page size. */
14
+ export declare function leaseOwnsNotification(lease: import('./model.js').WatchLease, notification: RoutedNotification): boolean;
@@ -12,3 +12,8 @@ export function matchesCatchSelection(actor, audience, filter, replyAuthor) {
12
12
  return audienceIncludes(audience, filter.mention)
13
13
  || (replyAuthor !== undefined && sameName(replyAuthor, filter.mention));
14
14
  }
15
+ /** A live catch owns every matching notification, independent of its page size. */
16
+ export function leaseOwnsNotification(lease, notification) {
17
+ return matchesCatchSelection(notification.actor, notification.route === 'bell' ? { kind: 'bell' }
18
+ : { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {}, notification.route === 'reply' ? notification.recipient : undefined);
19
+ }
@@ -1,15 +1,17 @@
1
- import type { WakeTransportPort } from './ports.js';
2
- export declare function nativeTokens(text: string): string[];
3
- export declare function nativePayload(token: string, preview: string): string;
4
- export declare function createClaudeWakeTransport(env?: NodeJS.ProcessEnv): WakeTransportPort;
5
- /** Recognize the whole frame first; one inactive token vetoes it before any evidence/receipt write. */
6
- export declare function observeClaudeDelivery(sessionId: string, text: string, operation: 'guard' | 'admitted' | 'stored', env?: NodeJS.ProcessEnv): Promise<{
7
- recognized: boolean;
8
- current: boolean;
9
- }>;
1
+ import { type DeliveryHandoffRef } from './delivery-ledger.js';
2
+ export { createClaudeWakeTransport, nativeTokens, nativePayload } from './claude-transport.js';
10
3
  export type ClaudeBinding = {
11
- readonly location: string;
12
- readonly participant: string;
13
4
  readonly session: string;
14
- readonly generation?: import('./square-core.js').ActivityId;
5
+ readonly generation: string;
15
6
  };
7
+ /** Extract complete frames and hash the exact bytes observed by the native host. */
8
+ export declare function claudeFrames(text: string): readonly {
9
+ token: string;
10
+ payloadHash: string;
11
+ }[];
12
+ /** Guard pre-effect; admit and observe only the exact captured handoff after each native effect. */
13
+ export declare function observeClaudeDelivery(sessionId: string, text: string, operation: 'guard' | 'admitted' | 'stored', env?: NodeJS.ProcessEnv, captured?: readonly DeliveryHandoffRef[]): Promise<{
14
+ recognized: boolean;
15
+ current: boolean;
16
+ refs?: readonly DeliveryHandoffRef[];
17
+ }>;
@@ -1,155 +1,55 @@
1
- import { stat } from 'node:fs/promises';
2
- import { createHash, randomUUID } from 'node:crypto';
3
- import { createArtifactPort } from './artifact.js';
4
- import { presentPending } from './presentation-operations.js';
5
- import { nativePendingPreview } from './inbox.js';
6
- import { parseActivityId } from './square-core.js';
7
- import { decodeHarnessOwner } from './owner.js';
8
- import { indexedSquareEntries } from './square-index.js';
9
- import { asSeatRef } from './seats.js';
10
- import { writeClaudeNative } from './packages/agent-delivery/src/claude-native.js';
11
- const marker = /(?:^|\n)\[square-inbox:([a-zA-Z0-9-]+)\](?:\n|$)/g;
12
- export function nativeTokens(text) { return [...text.matchAll(marker)].map((match) => match[1]); }
13
- export function nativePayload(token, preview) { return `[square-inbox:${token}]\n${preview}\n[/square-inbox:${token}]`; }
14
- function nativeSupported(route) { return route.address.loaded === 'true' && !!route.address.endpoint; }
15
- function hashPayload(payload) { return createHash('sha256').update(payload).digest('hex'); }
16
- function previewFacts(facts, request) {
17
- const index = parseActivityId(request.activity);
18
- if (index === undefined || request.route.kind !== 'claude-native' || request.route.address.endpoint === undefined)
19
- return undefined;
20
- return nativePendingPreview(facts, { location: request.location, routeKind: 'claude-native', generation: request.seat.generation,
21
- address: request.route.address,
22
- participant: request.participant, actIndex: index, now: Date.now() });
23
- }
24
- async function preview(artifact, request) {
25
- return (await artifact.read((facts) => previewFacts(facts, request))).value;
26
- }
27
- async function writeClaudeWake(sessionId, endpoint, messageId, payload, timeoutMs, signal) {
28
- const result = await writeClaudeNative({ sessionId, endpoint, messageId }, payload, { deadline: Date.now() + timeoutMs, signal });
29
- if (result.outcome === 'unavailable')
30
- return { outcome: 'failed', signature: 'claude_endpoint_unavailable', message: result.message };
31
- return { outcome: 'unknown', signature: 'native_admission_unconfirmed', message: result.outcome === 'written' ? 'Claude accepted the local write; native admission is unconfirmed.' : result.message };
32
- }
33
- export function createClaudeWakeTransport(env = process.env) {
34
- return {
35
- async probe(route) {
36
- if (!nativeSupported(route))
37
- return { outcome: 'not-capable', diagnostic: 'Claude native inbox unavailable: a loaded interactive Square mod is required.' };
38
- try {
39
- return (await stat(route.address.endpoint)).isSocket() || { outcome: 'not-capable', diagnostic: 'Claude native inbox endpoint is not a socket.' };
40
- }
41
- catch {
42
- return { outcome: 'not-capable', diagnostic: 'Claude native inbox endpoint unavailable.' };
43
- }
44
- },
45
- attempt: (request, timeoutMs, beforeSend) => dispatchClaude(request, timeoutMs, beforeSend, env),
46
- };
47
- }
48
- async function dispatchClaude(request, timeoutMs, beforeSend, env = process.env) {
49
- void env;
50
- const signal = AbortSignal.timeout(timeoutMs);
51
- if (!nativeSupported(request.route) || request.artifact === undefined || request.claimToken === undefined)
52
- return { outcome: 'not-capable', diagnostic: 'Claude native delivery needs a current artifact claim and loaded inbox.' };
53
- if (signal.aborted || !await beforeSend())
54
- return { outcome: 'unknown', signature: 'native_send_suppressed' };
55
- const prepared = await preview(request.artifact, request);
56
- if (prepared === undefined)
57
- return { outcome: 'not-capable', diagnostic: 'Claude activity is no longer current.' };
58
- const messageId = randomUUID();
59
- const payload = nativePayload(request.claimToken, prepared.payload);
60
- const correlation = { harness: 'claude', token: request.claimToken, messageId, payloadHash: hashPayload(payload) };
61
- if (!await request.artifact.prepareDelivery({ seat: request.seat, claimToken: request.claimToken, correlation }))
62
- return { outcome: 'not-capable', diagnostic: 'Claude delivery claim is no longer current.' };
63
- const fresh = await preview(request.artifact, request);
64
- if (fresh === undefined || nativePayload(request.claimToken, fresh.payload) !== payload)
65
- return { outcome: 'not-capable', diagnostic: 'Claude activity changed before native write.' };
66
- if (signal.aborted || !await beforeSend())
67
- return { outcome: 'not-capable', diagnostic: 'Claude activity was consumed before native write.' };
68
- const sessionId = request.route.address.sessionId;
69
- if (sessionId === undefined)
70
- return { outcome: 'not-capable', diagnostic: 'Claude native route has no session address.' };
71
- return writeClaudeWake(sessionId, request.route.address.endpoint, messageId, payload, timeoutMs, signal);
72
- }
73
- function currentPayload(facts, request) {
74
- const seat = facts.seat(request.participant), evidence = facts.deliveryByToken(request.claimToken);
75
- if (seat === undefined || seat.attachment !== 'attached' || evidence === undefined || seat.generation !== evidence.generation
76
- || seat.generation !== request.seat.generation || seat.owner !== request.seat.owner
77
- || evidence.activity !== request.activity || !['dispatching', 'unknown', 'accepted'].includes(evidence.outcome))
78
- return undefined;
79
- const native = previewFacts(facts, request);
80
- if (native === undefined)
81
- return undefined;
82
- const payload = nativePayload(request.claimToken, native.payload);
83
- return evidence.correlation?.payloadHash === hashPayload(payload) ? payload : undefined;
84
- }
85
- async function rowsForSession(sessionId, env, tokens) {
86
- const rows = [];
87
- const candidates = (await indexedSquareEntries({}, env)).filter(entry => {
88
- const coordinate = decodeHarnessOwner(entry.owner);
89
- return coordinate?.channel === 'claude-code' && coordinate.session === sessionId;
1
+ import { DeliveryLedger } from './delivery-ledger.js';
2
+ import { harnessOwner } from './owner.js';
3
+ import { renderDeliveryReminder } from './delivery-frame.js';
4
+ import { hashPayload, nativeTokens } from './claude-transport.js';
5
+ export { createClaudeWakeTransport, nativeTokens, nativePayload } from './claude-transport.js';
6
+ /** Extract complete frames and hash the exact bytes observed by the native host. */
7
+ export function claudeFrames(text) {
8
+ const prefix = renderDeliveryReminder([''], { tagged: false });
9
+ return [...text.matchAll(/(?:^|\n)(\[square-inbox:([a-zA-Z0-9-]+)\]\n[\s\S]*?\n\[\/square-inbox:\2\])(?=\n|$)/g)]
10
+ .flatMap(frame => {
11
+ const start = frame.index + frame[0].length - frame[1].length;
12
+ return text.slice(start - prefix.length, start) === prefix
13
+ ? [{ token: frame[2], payloadHash: hashPayload(prefix + frame[1]) }] : [];
90
14
  });
91
- const paths = new Set(candidates.map(candidate => candidate.location));
92
- for (const location of paths) {
93
- const artifact = createArtifactPort(location);
94
- try {
95
- const found = await artifact.read((facts) => tokens.flatMap((token) => {
96
- const evidence = facts.deliveryByToken(token);
97
- if (evidence?.kind !== 'wake' || evidence.correlation?.harness !== 'claude' || evidence.correlation.token !== token
98
- || evidence.route?.kind !== 'claude-native' || evidence.route.address.sessionId !== sessionId)
99
- return [];
100
- const row = { artifact, evidence };
101
- const seat = facts.seat(evidence.participant), index = parseActivityId(evidence.activity);
102
- const item = index === undefined ? undefined : facts.activity(index);
103
- if (seat === undefined || item?.kind !== 'say')
104
- return [row];
105
- const route = { location, participant: seat.participant,
106
- ...evidence.route, updatedAt: evidence.at };
107
- const request = { location, participant: seat.participant, activity: evidence.activity,
108
- actor: item.actor, seat: asSeatRef(seat), claimToken: token, attemptN: evidence.attemptN, route, artifact };
109
- return [{ ...row, request, payload: currentPayload(facts, request) }];
110
- }));
111
- rows.push(...found.value);
112
- if (found.value.length === 0)
113
- await artifact.close();
114
- }
115
- catch {
116
- await artifact.close();
117
- }
118
- }
119
- return rows;
120
15
  }
121
- /** Recognize the whole frame first; one inactive token vetoes it before any evidence/receipt write. */
122
- export async function observeClaudeDelivery(sessionId, text, operation, env = process.env) {
16
+ /** Guard pre-effect; admit and observe only the exact captured handoff after each native effect. */
17
+ export async function observeClaudeDelivery(sessionId, text, operation, env = process.env, captured) {
123
18
  const tokens = nativeTokens(text);
124
- if (tokens.length === 0)
19
+ if (!tokens.length)
125
20
  return { recognized: false, current: false };
126
- const rows = await rowsForSession(sessionId, env, [...new Set(tokens)]);
127
- const recognized = rows.length > 0;
128
- const frames = [...text.matchAll(/(?:^|\n)(\[square-inbox:([a-zA-Z0-9-]+)\]\n[\s\S]*?\n\[\/square-inbox:\2\])(?=\n|$)/g)];
21
+ const frames = claudeFrames(text);
22
+ const owner = harnessOwner({ channel: 'claude-code', session: sessionId });
23
+ const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
129
24
  try {
130
- if (!recognized || rows.some(row => {
131
- const matching = frames.filter(frame => frame[2] === row.evidence.claimToken);
132
- return row.payload === undefined || matching.length !== tokens.filter(token => token === row.evidence.claimToken).length
133
- || matching.some(frame => frame[1] !== row.payload);
134
- }))
25
+ const found = await Promise.all(tokens.map(token => ledger.handoff(token)));
26
+ const recognized = found.some(Boolean);
27
+ if (!recognized)
28
+ return { recognized: false, current: false };
29
+ if (frames.length !== tokens.length || new Set(tokens).size !== tokens.length || found.some(ref => !ref))
135
30
  return { recognized, current: false };
136
- for (const row of rows) {
137
- if (operation === 'guard')
138
- continue;
139
- const request = row.request, payload = row.payload;
140
- const admitted = await row.artifact.transactAs({ kind: 'participant', seat: request.seat }, facts => currentPayload(facts, request) === payload && facts.completeDelivery({ seat: request.seat,
141
- activity: row.evidence.activity, claimToken: request.claimToken, outcome: 'accepted', at: Date.now() })).catch(() => ({ value: false }));
142
- if (!admitted.value)
31
+ const refs = found;
32
+ if (refs.some((ref, i) => ref.owner !== owner || ref.routeKind !== 'claude-native' || ref.token !== frames[i].token || ref.payloadHash !== frames[i].payloadHash))
33
+ return { recognized, current: false };
34
+ if (operation === 'admitted' && (!Array.isArray(captured) || captured.length !== refs.length || refs.some((ref, i) => {
35
+ const old = captured[i];
36
+ return !old || old.token !== ref.token || old.owner !== ref.owner || old.generation !== ref.generation || old.payloadHash !== ref.payloadHash || old.routeKind !== ref.routeKind;
37
+ })))
38
+ return { recognized, current: false };
39
+ if (operation === 'guard') {
40
+ for (const ref of refs)
41
+ if (!await ledger.guardHandoff(ref))
42
+ return { recognized, current: false };
43
+ return { recognized, current: true, refs };
44
+ }
45
+ for (const ref of refs) {
46
+ const recorded = operation === 'admitted' ? await ledger.acceptHandoff(ref) : await ledger.observePresentation(ref);
47
+ if (!recorded)
143
48
  return { recognized, current: false };
144
- if (operation === 'stored')
145
- await presentPending({ artifact: row.artifact, location: request.location,
146
- // Claude's stored event consumes even Agora previews; retain that contract for both modes.
147
- participant: request.participant, activity: request.activity, seat: request.seat, observationOf: request.claimToken, markSeen: true, sink: { present() { } },
148
- current: facts => currentPayload(facts, request) === payload });
149
49
  }
150
50
  return { recognized, current: true };
151
51
  }
152
52
  finally {
153
- await Promise.all([...new Set(rows.map(row => row.artifact))].map(artifact => artifact.close()));
53
+ await ledger.close();
154
54
  }
155
55
  }
@@ -1,9 +1,9 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { deliveryLedgerLocation } from './delivery-ledger.js';
4
5
  import { z } from 'zod/v4';
5
6
  import { withFileLock } from './file-lock.js';
6
- import { squareIndexPath } from './square-index.js';
7
7
  import { SquareError } from './model.js';
8
8
  const LEASE_MS = 10 * 60 * 1000;
9
9
  const text = z.string().max(4096).refine(value => value.trim().length > 0 && !/[\x00-\x1f\x7f]/u.test(value));
@@ -12,7 +12,7 @@ const coordinate = z.object({
12
12
  });
13
13
  const record = coordinate.extend({ token: z.string().regex(/^[a-f0-9]{64}$/u), expiresAt: z.number() });
14
14
  const unavailable = () => new SquareError('unavailable', 'Claude MCP call context unavailable');
15
- const storeRoot = (env) => path.join(path.dirname(squareIndexPath(env)), 'claude-mcp-calls');
15
+ const storeRoot = (env) => path.join(path.dirname(deliveryLedgerLocation(env)), 'claude-mcp-calls');
16
16
  const callFile = (root, id) => path.join(root, `${createHash('sha256').update(id).digest('hex')}.json`);
17
17
  async function readRecord(file) {
18
18
  try {