@astrosheep/square 0.3.105 → 0.4.0

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 (143) 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 +8 -12
  5. package/dist/artifact.js +66 -343
  6. package/dist/attention-presentation.d.ts +33 -7
  7. package/dist/attention-presentation.js +33 -69
  8. package/dist/boundary-presentation.d.ts +0 -2
  9. package/dist/boundary-presentation.js +17 -22
  10. package/dist/catch-decisions.d.ts +2 -2
  11. package/dist/catch-decisions.js +14 -4
  12. package/dist/catch-selection.d.ts +8 -0
  13. package/dist/catch-selection.js +5 -0
  14. package/dist/channel-artifact.d.ts +10 -0
  15. package/dist/channel-artifact.js +55 -0
  16. package/dist/channel-delivery.d.ts +11 -0
  17. package/dist/channel-delivery.js +51 -0
  18. package/dist/channel-domain.d.ts +3 -0
  19. package/dist/channel-domain.js +83 -0
  20. package/dist/channel-facade.d.ts +76 -0
  21. package/dist/channel-facade.js +1 -0
  22. package/dist/channel-internal.d.ts +126 -0
  23. package/dist/channel-internal.js +440 -0
  24. package/dist/channel-metadata.d.ts +11 -0
  25. package/dist/channel-metadata.js +1 -0
  26. package/dist/channel-ports.d.ts +241 -0
  27. package/dist/channel-ports.js +1 -0
  28. package/dist/channel-presentation.d.ts +17 -0
  29. package/dist/channel-presentation.js +22 -0
  30. package/dist/channel-sqlite-schema.d.ts +13 -0
  31. package/dist/channel-sqlite-schema.js +81 -0
  32. package/dist/channel-transport.d.ts +62 -0
  33. package/dist/channel-transport.js +1 -0
  34. package/dist/channel-values.d.ts +27 -0
  35. package/dist/channel-values.js +90 -0
  36. package/dist/channel.d.ts +31 -21
  37. package/dist/channel.js +42 -74
  38. package/dist/claude-delivery-status.js +1 -1
  39. package/dist/claude-delivery.d.ts +2 -5
  40. package/dist/claude-delivery.js +56 -103
  41. package/dist/claude-mod.js +4 -4
  42. package/dist/claude-transport.d.ts +5 -0
  43. package/dist/claude-transport.js +58 -0
  44. package/dist/cli/context.d.ts +2 -2
  45. package/dist/cli/context.js +0 -1
  46. package/dist/cli/maintenance-commands.d.ts +0 -4
  47. package/dist/cli/maintenance-commands.js +2 -29
  48. package/dist/cli/program.js +1 -1
  49. package/dist/cli/registry.js +1 -2
  50. package/dist/codex-address.d.ts +1 -1
  51. package/dist/codex-queue.d.ts +2 -2
  52. package/dist/codex-queue.js +1 -3
  53. package/dist/decisions.js +3 -3
  54. package/dist/delivery-deferral.d.ts +9 -18
  55. package/dist/delivery-deferral.js +4 -4
  56. package/dist/delivery-health.d.ts +4 -4
  57. package/dist/delivery-health.js +11 -11
  58. package/dist/delivery-operations.d.ts +15 -2
  59. package/dist/delivery-operations.js +60 -30
  60. package/dist/delivery.d.ts +44 -33
  61. package/dist/delivery.js +49 -75
  62. package/dist/help.js +1 -6
  63. package/dist/http-delivery.d.ts +2 -2
  64. package/dist/http-delivery.js +2 -4
  65. package/dist/inbox.d.ts +10 -18
  66. package/dist/inbox.js +44 -53
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/mcp.d.ts +2 -1
  70. package/dist/mcp.js +40 -70
  71. package/dist/message.d.ts +7 -0
  72. package/dist/message.js +23 -0
  73. package/dist/model.d.ts +41 -77
  74. package/dist/model.js +7 -139
  75. package/dist/neutral.d.ts +72 -0
  76. package/dist/neutral.js +27 -0
  77. package/dist/notifications.d.ts +2 -8
  78. package/dist/notifications.js +15 -50
  79. package/dist/operation-presentation.d.ts +1 -2
  80. package/dist/operation-presentation.js +1 -7
  81. package/dist/owner.d.ts +2 -2
  82. package/dist/owner.js +1 -1
  83. package/dist/paseo-delivery.d.ts +1 -1
  84. package/dist/paseo-delivery.js +2 -4
  85. package/dist/paseo-timeline.js +1 -0
  86. package/dist/plugins/opencode/delivery.js +27 -42
  87. package/dist/ports.d.ts +46 -168
  88. package/dist/presentation-operations.d.ts +1 -14
  89. package/dist/presentation-operations.js +16 -16
  90. package/dist/presentation.d.ts +5 -5
  91. package/dist/presentation.js +1 -0
  92. package/dist/runtime.js +2 -2
  93. package/dist/search.js +1 -1
  94. package/dist/sqlite-channel.d.ts +15 -0
  95. package/dist/sqlite-channel.js +600 -0
  96. package/dist/sqlite-framing.d.ts +8 -0
  97. package/dist/sqlite-framing.js +31 -0
  98. package/dist/sqlite-square.d.ts +12 -13
  99. package/dist/sqlite-square.js +192 -248
  100. package/dist/sqlite-values.d.ts +21 -0
  101. package/dist/sqlite-values.js +78 -0
  102. package/dist/square-api.d.ts +1 -1
  103. package/dist/square-api.js +1 -1
  104. package/dist/square-core.d.ts +1 -1
  105. package/dist/square-domain.d.ts +21 -7
  106. package/dist/square-domain.js +93 -94
  107. package/dist/square-facade.d.ts +12 -66
  108. package/dist/square-index.d.ts +1 -1
  109. package/dist/square-index.js +1 -1
  110. package/dist/square-location.d.ts +6 -1
  111. package/dist/square-location.js +1 -1
  112. package/dist/square-receive.d.ts +2 -3
  113. package/dist/square-sqlite.d.ts +45 -44
  114. package/dist/square-sqlite.js +308 -764
  115. package/dist/square-tools.d.ts +8 -0
  116. package/dist/square-tools.js +39 -0
  117. package/dist/views.d.ts +6 -6
  118. package/dist/views.js +15 -9
  119. package/dist/wake-preview.d.ts +7 -0
  120. package/dist/wake-preview.js +15 -0
  121. package/dist/wake-sink.d.ts +2 -2
  122. package/dist/wake-sink.js +1 -1
  123. package/dist/wake-transports.d.ts +7 -0
  124. package/dist/wake-transports.js +42 -0
  125. package/guides/claude-native.md +13 -10
  126. package/guides/codex-delivery.md +3 -1
  127. package/guides/participant-ownership.md +1 -1
  128. package/guides/row-runtime-delivery-evidence.md +2 -0
  129. package/guides/row-storage-design.md +19 -44
  130. package/guides/square-api.md +137 -60
  131. package/package.json +3 -3
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/types.d.ts +1 -1
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +28 -41
  136. package/plugins/pi/square-pi.js +28 -51
  137. package/dist/square-legacy-framing.d.ts +0 -11
  138. package/dist/square-legacy-framing.js +0 -64
  139. package/dist/square-migration-host-ledger.d.ts +0 -26
  140. package/dist/square-migration-host-ledger.js +0 -169
  141. package/dist/square-migration.d.ts +0 -30
  142. package/dist/square-migration.js +0 -599
  143. package/guides/migration.md +0 -156
package/dist/delivery.js CHANGED
@@ -1,96 +1,68 @@
1
- import { renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
2
- import { parseActivityId } from './square-core.js';
1
+ import { formatMessageId } from './message.js';
3
2
  import { deliveryDeferralExpiry } from './delivery-deferral.js';
4
- import { automaticDispatchEligible, presentationBlocksDispatch } from './delivery-attempts.js';
5
- import { matchesCatchSelection } from './catch-selection.js';
6
- /** Exact stored activity projection for transports that send an awareness reminder. */
7
- export function wakePreview(facts, request) {
8
- const index = parseActivityId(request.activity);
9
- const head = index === undefined ? undefined : pendingHead(facts, request.participant, index);
10
- const item = index === undefined ? undefined : facts.activity(index);
11
- const seat = facts.seat(request.participant);
12
- if (head === undefined || item?.kind !== 'say' || seat === undefined || seat.attachment !== 'attached'
13
- || deliveryDeferralExpiry(facts, request.seat, item.index, Date.now()) !== undefined)
14
- return undefined;
15
- return renderSquareReminder([renderAttentionPreview({
16
- squarePath: request.location, recipient: request.participant, actIndex: item.index,
17
- actor: item.actor, route: head.route, body: item.body ?? '(structured activity)', delivery: item.delivery,
18
- })], [item.delivery]);
19
- }
20
- function custody(evidence) {
21
- return evidence.map(row => ({ purpose: row.kind === 'wake' ? 'dispatch' : 'presentation', outcome: row.outcome, expiresAt: row.expiresAt }));
22
- }
23
- /** Presentation custody blocks a wake, independently of native wake acceptance. */
24
- export function presentationBlocksWake(evidence, now) {
25
- return presentationBlocksDispatch(custody(evidence), now);
26
- }
27
- /** Preselection only: an expired wake dispatch is uncertain, never safe automatic replay. */
28
- export function automaticWakeEligible(evidence, now) {
29
- return automaticDispatchEligible(custody(evidence), now);
3
+ export { pendingHeadAt, pendingMessageHeads, pendingAddresses, presentationBlocksWake, automaticWakeEligible } from './channel-delivery.js';
4
+ import { pendingHeadAt, pendingMessageHeads } from './channel-delivery.js';
5
+ /** Upper enrichment: the reason this recipient was reached, when an Agora say owns the message. */
6
+ function enriched(facts, head) {
7
+ const relation = facts.activityByMessage(head.index);
8
+ return { index: head.index, at: head.at, actor: head.from, recipient: head.recipient,
9
+ ...(head.generation === undefined ? {} : { generation: head.generation }),
10
+ ...(relation === undefined ? {} : { activityIndex: relation.index, route: facts.directedAt(head.recipient, relation.index)?.route }) };
30
11
  }
31
12
  /** Notification pending is not cursor suffix or historical catch membership. */
32
- export function pendingHead(facts, name, index) {
33
- const participant = facts.participant(name);
34
- if (!participant?.joined || index <= (participant.lastJoinIndex ?? -1) || facts.receipt(participant.name, index))
35
- return undefined;
36
- return facts.directedAt(participant.name, index);
13
+ export function pendingHead(facts, name, index, now = Date.now()) {
14
+ const head = pendingHeadAt(facts.channel, name, index, now);
15
+ return head === undefined ? undefined : enriched(facts, head);
37
16
  }
38
- /** One activity's landed recipients that still owe a wake; the rest of the roster is untouched. */
39
- export function pendingRecipients(facts, index) {
40
- return facts.audience(index).flatMap(({ recipient }) => {
41
- const head = pendingHead(facts, recipient, index);
42
- return head === undefined ? [] : [head];
43
- });
17
+ export function nativePendingHead(facts, input) {
18
+ const seat = facts.channel.endpoint(input.participant);
19
+ if (seat === undefined || seat.attachment !== 'attached' || seat.generation !== input.generation
20
+ || input.owner !== undefined && seat.owner !== input.owner
21
+ || !sameWakeAddress(seat.wakeAddress, input.route))
22
+ return undefined;
23
+ const cancelledThrough = facts.channel.cancelledThrough(seat.participant, seat.generation);
24
+ if (input.index <= Math.max(input.cancelledThrough ?? -1, cancelledThrough ?? -1))
25
+ return undefined;
26
+ if (deliveryDeferralExpiry(facts.channel, seat, input.index, input.now) !== undefined)
27
+ return undefined;
28
+ return pendingHead(facts, seat.participant, input.index, input.now);
44
29
  }
45
- /** Still-pending directed heads for one recipient, oldest first. Bodies belong to presentation. */
46
- export function pendingHeads(facts, name) {
47
- const participant = facts.participant(name);
48
- if (!participant?.joined)
49
- return [];
50
- const heads = [];
51
- let afterIndex = participant.lastJoinIndex ?? -1;
52
- for (;;) {
53
- const page = facts.directed(participant.name, { afterIndex, unseenOnly: true }, { edge: 'oldest', limit: 100 });
54
- heads.push(...page.items);
55
- if (!page.hasMore)
56
- break;
57
- afterIndex = page.items.at(-1).index;
58
- }
59
- return heads;
30
+ /** Still-pending directed messages for one recipient, oldest first. Bodies belong to presentation. */
31
+ export function pendingHeads(facts, name, now = Date.now(), suppress = true) {
32
+ return pendingMessageHeads(facts.channel, name, now, suppress).map(head => enriched(facts, head));
60
33
  }
61
34
  export function capturePendingScan(facts) {
62
- return { roster: facts.roster().filter((participant) => participant.joined), throughIndex: facts.header().lastActIndex };
35
+ return { roster: facts.channel.endpoints().filter(endpoint => endpoint.attachment === 'attached').map(endpoint => ({ name: endpoint.participant, generation: endpoint.generation })),
36
+ throughIndex: facts.channel.header().lastMessageIndex };
63
37
  }
64
38
  export function sameJoin(facts, captured) {
65
- const current = facts.participant(captured.name);
66
- return current?.joined === captured.joined && current?.lastJoinIndex === captured.lastJoinIndex;
39
+ const current = facts.channel.endpoint(captured.name);
40
+ return current?.attachment === 'attached' && current.generation === captured.generation;
67
41
  }
68
- /** Finite bodyless pass: unrelated revisions never rewind the last examined group. */
42
+ /** Finite bodyless pass: unrelated revisions never rewind the last examined message. */
69
43
  export async function* pendingPages(artifact, scan, afterIndex = -1) {
70
44
  const captured = new Map(scan.roster.map((participant) => [participant.name, participant]));
71
- while (afterIndex < scan.throughIndex) {
45
+ let cursor = afterIndex;
46
+ while (cursor < scan.throughIndex) {
72
47
  const { value: page } = await artifact.read((facts) => {
73
- const page = facts.pendingAudience(afterIndex, scan.throughIndex, 100);
74
- return { ...page, groups: page.groups.map((group) => ({ ...group, recipients: group.recipients.filter((recipient) => {
75
- const participant = captured.get(recipient.recipient);
76
- return participant !== undefined && sameJoin(facts, participant);
77
- }) })) };
48
+ const items = facts.channel.endpoints().filter(endpoint => endpoint.attachment === 'attached' && captured.has(endpoint.participant)).flatMap(endpoint => facts.channel.directed(endpoint.participant, { afterIndex: cursor, throughIndex: scan.throughIndex, unseenOnly: true, currentGeneration: true }, { edge: 'oldest', limit: 100 }).items
49
+ .filter(() => sameJoin(facts, captured.get(endpoint.participant)))
50
+ .map(head => enriched(facts, head)));
51
+ return { items, next: items.at(-1)?.index, done: items.length === 0 };
78
52
  });
79
- yield page.groups.flatMap((group) => group.recipients.map(({ recipient, route }) => ({
80
- index: group.index, at: group.at, actor: group.actor, recipient, route,
81
- })));
82
- if (!page.hasMore)
53
+ yield page.items;
54
+ if (page.done || page.next === undefined)
83
55
  break;
84
- afterIndex = page.groups.at(-1).index;
56
+ cursor = page.next;
85
57
  }
86
58
  }
87
59
  /** A pass retains only its captured joins; routes and individual receipts are fresh. */
88
60
  export function pendingFacts(facts, scan, heads) {
89
61
  const joined = new Set(scan.roster.filter((participant) => sameJoin(facts, participant)).map((participant) => participant.name));
90
62
  return { pending: heads.filter((head) => joined.has(head.recipient)
91
- && pendingHead(facts, head.recipient, head.index) !== undefined) };
63
+ && pendingHeadAt(facts.channel, head.recipient, head.index) !== undefined) };
92
64
  }
93
- /** Global discovery alone sweeps pendingAudience; exact dispatch uses pendingRecipients. */
65
+ /** Global discovery alone sweeps every endpoint; exact dispatch uses pendingAddresses. */
94
66
  export async function collectPending(artifact) {
95
67
  const { value: scan } = await artifact.read(capturePendingScan);
96
68
  const pending = [];
@@ -99,8 +71,10 @@ export async function collectPending(artifact) {
99
71
  const { value } = await artifact.read((facts) => ({ scan, ...pendingFacts(facts, scan, pending) }));
100
72
  return value;
101
73
  }
102
- /** A live catch owns every matching notification, independent of its page size. */
103
- export function leaseOwnsNotification(lease, notification) {
104
- return matchesCatchSelection(notification.actor, notification.route === 'bell' ? { kind: 'bell' }
105
- : { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {}, notification.route === 'reply' ? notification.recipient : undefined);
74
+ function sameWakeAddress(left, right) {
75
+ if (left === undefined || right === undefined || left.kind !== right.kind)
76
+ return false;
77
+ const keys = Object.keys(left.address);
78
+ return keys.length === Object.keys(right.address).length && keys.every(key => left.address[key] === right.address[key]);
106
79
  }
80
+ export { formatMessageId };
package/dist/help.js CHANGED
@@ -89,11 +89,6 @@ const COMMANDS = [
89
89
  names: ['doctor'], usage: 'doctor', usesSquare: true, group: 'maintenance',
90
90
  summary: 'Validate artifact integrity.',
91
91
  },
92
- {
93
- names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
94
- summary: 'Convert a supported older square to the current format in place.',
95
- details: ['For the selected .square artifact only. Stop sessions using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'Versions 2–7 migrate explicitly to format 8. Format 7 migration deletes hold/resume activities and their keyed rows, clears replies to those activities, and preserves surviving activity ids with gaps. Format 6 attachment and opaque-owner conversion are preserved; versions 4/5 first gain attached seats, and 4 also gains empty deferrals. Version 1, unrelated or unsupported artifacts refuse without a write. Names whose legacy owner and routes agree may share an owner; ambiguous names stay standing without custody. Rejoin with the same owner after migration to republish discovery. There is no --check, --backup or --force.'],
96
- },
97
92
  ];
98
93
  function definitionFor(command) {
99
94
  return COMMANDS.find((item) => item.names.includes(command));
@@ -105,7 +100,7 @@ export function renderGlobalHelp() {
105
100
  const groups = [
106
101
  { key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'unlisten', 'ignore', 'listening', 'catch', 'history', 'status', 'done'] },
107
102
  { key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants'] },
108
- { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor', 'migrate'] },
103
+ { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor'] },
109
104
  ];
110
105
  const commandLines = groups.flatMap(({ key, title, order }) => [
111
106
  title,
@@ -1,5 +1,5 @@
1
- import type { WakeRoute } from './model.js';
2
- import type { WakeOutcome, WakeRequest, WakeTransportPort } from './ports.js';
1
+ import type { WakeRoute } from './channel-values.js';
2
+ import type { WakeOutcome, WakeRequest, WakeTransportPort } from './channel-transport.js';
3
3
  export interface HttpWakeTransportOptions {
4
4
  fetch?: typeof fetch;
5
5
  env?: NodeJS.ProcessEnv;
@@ -1,4 +1,3 @@
1
- import { wakePreview } from './delivery.js';
2
1
  /** Configuration only: probing never contacts the receiver. */
3
2
  function configuration(route, env) {
4
3
  if (route.kind !== 'http')
@@ -53,12 +52,11 @@ export class HttpWakeTransport {
53
52
  });
54
53
  const send = async () => {
55
54
  try {
56
- const preview = request.artifact === undefined ? undefined
57
- : (await request.artifact.read(facts => wakePreview(facts, request), controller.signal)).value;
55
+ const preview = request.presentation?.payload;
58
56
  if (preview === undefined)
59
57
  return { outcome: 'not-capable', diagnostic: 'No pending Square activity to preview.' };
60
58
  const body = JSON.stringify({
61
- location: request.location, participant: request.participant, activity: request.activity, actor: request.actor,
59
+ location: request.location, participant: request.participant, message: request.message, actor: request.actor,
62
60
  ...(request.attemptN === undefined ? {} : { attemptN: request.attemptN }), preview,
63
61
  });
64
62
  if (controller.signal.aborted || Date.now() >= deadline)
package/dist/inbox.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type Owner, type LocatedSeat, type PendingMembership } from './model.js';
2
2
  import type { SquareRead } from './ports.js';
3
+ import { type NativePendingInput } from './delivery.js';
3
4
  export interface PendingWaitOptions {
4
5
  signal?: AbortSignal;
5
6
  /** Local delivery suppression, never persisted as evidence of receipt. */
@@ -8,8 +9,7 @@ export interface PendingWaitOptions {
8
9
  skipImmediate?: boolean;
9
10
  onChangeArmed?: (armed: boolean) => void;
10
11
  }
11
- export declare function pendingKey(membership: LocatedSeat, actIndex: number): string;
12
- export declare function pendingAtBoundary(inbox: readonly PendingMembership[]): PendingMembership[];
12
+ export declare function pendingKey(membership: LocatedSeat, messageIndex: number): string;
13
13
  /** Revalidate a supplied batch immediately before the boundary's independent claim/send checks. */
14
14
  export declare function projectBoundaryEligibility(inbox: readonly PendingMembership[]): Promise<PendingMembership[]>;
15
15
  export interface SessionBindingCandidate {
@@ -31,22 +31,14 @@ export interface NativePendingPreview {
31
31
  readonly payload: string;
32
32
  readonly clipped: boolean;
33
33
  }
34
- export type NativePendingPreviewInput = {
35
- readonly location: string;
36
- readonly generation: import('./model.js').ActivityId;
37
- readonly participant: string;
38
- readonly actIndex: number;
39
- readonly cancelledThrough?: number;
40
- readonly now: number;
41
- } & ({
42
- readonly routeKind: 'claude-native';
43
- readonly address: Readonly<Record<string, string>>;
44
- } | {
45
- readonly routeKind: 'opencode-server';
46
- readonly address: Readonly<Record<string, string>>;
47
- });
48
- /** Current-generation native attention, owned by its exact wake address and catch state. */
49
- export declare function nativePendingPreview(facts: SquareRead, input: NativePendingPreviewInput): NativePendingPreview | undefined;
34
+ export interface NativePendingSource {
35
+ readonly text: string;
36
+ readonly clipped: boolean;
37
+ }
38
+ /** One current eligibility + source projection read; senders use text, ACK coverage uses clipped. */
39
+ export declare function nativePendingSource(facts: SquareRead, input: NativePendingInput): NativePendingSource | undefined;
40
+ /** The sender-facing fixed reminder over the shared source projection. */
41
+ export declare function nativePendingPreview(facts: SquareRead, input: NativePendingInput): NativePendingPreview | undefined;
50
42
  export declare function sessionInbox(owner: Owner, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<PendingMembership[]>;
51
43
  export interface SessionPendingObserver {
52
44
  /** Read committed revisions before reusing content; filesystem hints are only for waiting. */
package/dist/inbox.js CHANGED
@@ -1,35 +1,27 @@
1
1
  import { createArtifactPort, observeSquareDeliveryRevision, observeSquareRevision } from './artifact.js';
2
2
  import { indexedSquareEntries, observeSquareIndex } from './square-index.js';
3
3
  import { canonicalPath } from './canonical-path.js';
4
- import { formatActivityId, parseActivityId } from './square-core.js';
4
+ import { formatMessageId } from './message.js';
5
5
  import { freshWatchLease, WATCH_STALE_MS } from './runtime.js';
6
- import { pendingHead, pendingHeads, leaseOwnsNotification } from './delivery.js';
6
+ import { nativePendingHead, pendingHeads } from './delivery.js';
7
7
  import { deliveryDeferralExpiry } from './delivery-deferral.js';
8
- import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
9
- export function pendingKey(membership, actIndex) {
10
- return membership.location + '\0' + membership.seat.participant.toLocaleLowerCase() + '\0' + actIndex;
11
- }
12
- export function pendingAtBoundary(inbox) {
13
- return inbox.map((membership) => {
14
- const lease = membership.catchLease;
15
- if (lease === undefined)
16
- return membership;
17
- return { ...membership, notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.seat.participant })) };
18
- }).filter((membership) => membership.notifications.length > 0);
8
+ import { projectMessageNotification, renderSquareReminder } from './attention-presentation.js';
9
+ export function pendingKey(membership, messageIndex) {
10
+ return membership.location + '\0' + membership.seat.participant.toLocaleLowerCase() + '\0' + messageIndex;
19
11
  }
20
12
  function eligibleNotifications(facts, membership, now = Date.now()) {
21
13
  const seat = facts.seat(membership.seat.participant);
22
14
  if (seat === undefined || seat.attachment !== 'attached' || seat.generation !== membership.seat.generation
23
15
  || seat.owner !== membership.seat.owner)
24
16
  return { notifications: [] };
25
- const cancelledThrough = facts.cancelledThrough(seat.participant, seat.generation);
17
+ const cancelledThrough = facts.channel.cancelledThrough(seat.participant, seat.generation);
26
18
  let expiry = Infinity;
27
19
  const notifications = membership.notifications.filter(notification => {
28
- if (cancelledThrough !== undefined && notification.actIndex <= cancelledThrough
29
- || facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(notification.actIndex))
20
+ if (cancelledThrough !== undefined && notification.messageIndex <= cancelledThrough
21
+ || facts.deliveryEvidence(seat.participant, seat.generation, formatMessageId(notification.messageIndex))
30
22
  .some(row => row.outcome !== 'failed' && row.outcome !== 'released'))
31
23
  return false;
32
- const deferredUntil = deliveryDeferralExpiry(facts, seat, notification.actIndex, now);
24
+ const deferredUntil = deliveryDeferralExpiry(facts.channel, seat, notification.messageIndex, now);
33
25
  if (deferredUntil === undefined)
34
26
  return true;
35
27
  expiry = Math.min(expiry, deferredUntil);
@@ -40,7 +32,7 @@ function eligibleNotifications(facts, membership, now = Date.now()) {
40
32
  /** Revalidate a supplied batch immediately before the boundary's independent claim/send checks. */
41
33
  export async function projectBoundaryEligibility(inbox) {
42
34
  const grouped = new Map();
43
- for (const membership of pendingAtBoundary(inbox)) {
35
+ for (const membership of inbox.filter(candidate => candidate.notifications.length > 0)) {
44
36
  const entries = grouped.get(membership.location) ?? [];
45
37
  entries.push(membership);
46
38
  grouped.set(membership.location, entries);
@@ -88,9 +80,10 @@ function groupCandidates(candidates) {
88
80
  }
89
81
  function locatedSeats(facts, location, seats) {
90
82
  return seats.flatMap(seat => {
91
- const index = parseActivityId(seat.generation);
83
+ const participant = facts.participant(seat.participant);
84
+ const index = participant?.lastJoinIndex;
92
85
  const joined = index === undefined ? undefined : facts.activity(index);
93
- return joined?.kind !== 'join' ? [] : [{ location, seat, joinedAt: joined.at }];
86
+ return joined?.kind === 'join' ? [{ location, seat, joinedAt: joined.at }] : [{ location, seat, joinedAt: 0 }];
94
87
  });
95
88
  }
96
89
  /** Lifecycle reads resolve seats and join times, never pending activity bodies. */
@@ -120,7 +113,7 @@ export async function sessionInboxCounts(input, signal) {
120
113
  const artifact = createArtifactPort(location, undefined, signal);
121
114
  try {
122
115
  rows.push(...(await artifact.read(facts => locatedSeats(facts, location, owners.flatMap(owner => facts.seatsForOwner(owner).filter(seat => seat.attachment === 'attached'))).map(({ seat }) => ({
123
- name: seat.participant, squarePath: location, pending: facts.directedCount(seat.participant, { unseenOnly: true }),
116
+ name: seat.participant, squarePath: location, pending: facts.directedCount(seat.participant, { unseenOnly: true, currentGeneration: true }),
124
117
  })), signal)).value);
125
118
  }
126
119
  catch (error) {
@@ -147,19 +140,24 @@ async function projectPending(candidates, cache, signal) {
147
140
  let cached = cache?.get(location);
148
141
  if (cached?.coordinate !== coordinate || cached.version !== header.revision) {
149
142
  cached = { coordinate, version: header.revision, value: locatedSeats(facts, location, seats).map(membership => {
150
- const heads = pendingHeads(facts, membership.seat.participant);
151
- const routes = new Map(heads.map(head => [head.index, head.route]));
152
- return { ...membership, notifications: facts.activities(heads.map(head => head.index)).flatMap(item => item.kind === 'say'
153
- ? [{ actIndex: item.index, actor: item.actor, at: item.at, body: item.body ?? '(structured activity)',
154
- route: routes.get(item.index), ...(item.delivery === undefined ? {} : { delivery: item.delivery }) }]
155
- : []) };
143
+ const heads = pendingHeads(facts, membership.seat.participant, Date.now(), false);
144
+ const messages = new Map(facts.channel.messages(heads.map(head => head.index)).map(item => [item.index, item]));
145
+ return { ...membership, notifications: heads.flatMap(head => {
146
+ const item = messages.get(head.index);
147
+ if (item === undefined)
148
+ return [];
149
+ return [{ messageIndex: item.index, ...(head.activityIndex === undefined ? {} : { actIndex: head.activityIndex }),
150
+ actor: item.from, at: item.at, body: item.body, ...(item.preview === undefined ? {} : { preview: item.preview }), ...(head.route === undefined ? {} : { route: head.route }) }];
151
+ }) };
156
152
  }) };
157
153
  }
158
154
  // Cache conversation content, never leases or delivery eligibility: evidence can change
159
155
  // without a conversation revision and must not force body hydration or self-wake retries.
160
156
  return { cached, pending: cached.value.map(membership => {
161
157
  const catchLease = freshWatchLease(facts, membership.seat.participant);
162
- return { ...membership, ...eligibleNotifications(facts, membership),
158
+ // Cached conversation content, but claim coverage and deferrals are re-read on every projection.
159
+ const observed = { ...membership, notifications: membership.notifications.filter(note => !facts.channel.suppressed(membership.seat.participant, membership.seat.generation, note.messageIndex, Date.now())) };
160
+ return { ...observed, ...eligibleNotifications(facts, observed),
163
161
  ...(catchLease === undefined ? {} : { catchLease }) };
164
162
  }) };
165
163
  }, signal);
@@ -178,30 +176,23 @@ async function projectPending(candidates, cache, signal) {
178
176
  }
179
177
  return pending;
180
178
  }
181
- function sameWakeAddress(address, kind, requested) {
182
- if (address?.kind !== kind || Object.keys(address.address).length !== Object.keys(requested).length)
183
- return false;
184
- return Object.entries(requested).every(([key, value]) => address.address[key] === value);
185
- }
186
- /** Current-generation native attention, owned by its exact wake address and catch state. */
187
- export function nativePendingPreview(facts, input) {
188
- const seat = facts.seat(input.participant);
189
- if (seat === undefined || seat.attachment !== 'attached' || seat.generation !== input.generation
190
- || !sameWakeAddress(seat.wakeAddress, input.routeKind, input.address))
179
+ /** One current eligibility + source projection read; senders use text, ACK coverage uses clipped. */
180
+ export function nativePendingSource(facts, input) {
181
+ const head = nativePendingHead(facts, input);
182
+ if (head === undefined)
191
183
  return undefined;
192
- const cancelledThrough = facts.cancelledThrough(seat.participant, seat.generation);
193
- if (input.actIndex <= Math.max(input.cancelledThrough ?? -1, cancelledThrough ?? -1))
184
+ const item = facts.channel.message(input.index);
185
+ if (item === undefined)
194
186
  return undefined;
195
- const head = pendingHead(facts, seat.participant, input.actIndex);
196
- const lease = freshWatchLease(facts, input.participant, input.now);
197
- if (!head || (lease !== undefined && leaseOwnsNotification(lease, head)))
198
- return undefined;
199
- const item = facts.activity(input.actIndex);
200
- if (item?.kind !== 'say' || deliveryDeferralExpiry(facts, seat, input.actIndex, input.now) !== undefined)
201
- return undefined;
202
- return { payload: renderSquareReminder([renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
203
- actor: item.actor, route: head.route, body: item.body ?? '(structured activity)', delivery: item.delivery })], [item.delivery]),
204
- clipped: attentionBodyIsClipped(item.body ?? '(structured activity)', item.delivery) };
187
+ const projected = projectMessageNotification({ squarePath: input.location, recipient: input.participant,
188
+ actor: item.from, body: item.body, ...(item.preview === undefined ? {} : { preview: item.preview }),
189
+ ...(head.activityIndex === undefined || head.route === undefined ? {} : { agora: { actIndex: head.activityIndex, route: head.route } }) });
190
+ return { text: projected.text, clipped: projected.clipped };
191
+ }
192
+ /** The sender-facing fixed reminder over the shared source projection. */
193
+ export function nativePendingPreview(facts, input) {
194
+ const source = nativePendingSource(facts, input);
195
+ return source === undefined ? undefined : { payload: renderSquareReminder([source.text]), clipped: source.clipped };
205
196
  }
206
197
  export async function sessionInbox(owner, env = process.env, signal) {
207
198
  return projectPending(await projectSessionCandidates({ owner, env }), undefined, signal);
@@ -252,9 +243,9 @@ export async function observeSessionPending(owner, suppliedEnv = process.env) {
252
243
  });
253
244
  }
254
245
  function eligibleInbox(inbox, excludeKeys) {
255
- const eligible = pendingAtBoundary(inbox);
246
+ const eligible = inbox.filter(membership => membership.notifications.length > 0);
256
247
  return excludeKeys === undefined ? eligible : eligible.flatMap((membership) => {
257
- const notifications = membership.notifications.filter((notification) => !excludeKeys.has(pendingKey(membership, notification.actIndex)));
248
+ const notifications = membership.notifications.filter((notification) => !excludeKeys.has(pendingKey(membership, notification.messageIndex)));
258
249
  return notifications.length === 0 ? [] : [{ ...membership, notifications }];
259
250
  });
260
251
  }
@@ -320,7 +311,7 @@ export async function observeSessionPending(owner, suppliedEnv = process.env) {
320
311
  }
321
312
  const inbox = await currentInbox(candidates, signal);
322
313
  const pending = eligibleInbox(inbox, options.excludeKeys);
323
- const eligibleKeys = JSON.stringify(pending.flatMap((membership) => membership.notifications.map((note) => pendingKey(membership, note.actIndex))).sort());
314
+ const eligibleKeys = JSON.stringify(pending.flatMap((membership) => membership.notifications.map((note) => pendingKey(membership, note.messageIndex))).sort());
324
315
  if (previousEligible !== undefined && previousEligible !== eligibleKeys)
325
316
  canReturn = true;
326
317
  previousEligible = eligibleKeys;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare, migrateSquare } from './square-api.js';
1
+ export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare } from './square-api.js';
2
2
  export { SquareError, ACTIVITY_METADATA_MAX_BYTES } from './model.js';
3
3
  export { selectOwnSeat, requireOwner, localUserOwner, harnessOwner, decodeHarnessOwner } from './square-api.js';
4
4
  export type { WakeTransportPort, WakeRequest, WakeOutcome } from './ports.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare, migrateSquare } from './square-api.js';
1
+ export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare } from './square-api.js';
2
2
  export { SquareError, ACTIVITY_METADATA_MAX_BYTES } from './model.js';
3
3
  export { selectOwnSeat, requireOwner, localUserOwner, harnessOwner, decodeHarnessOwner } from './square-api.js';
package/dist/mcp.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type Owner } from './square-api.js';
3
3
  import type { SquareAtInput } from './square-facade.js';
4
+ import type { WakeAddress } from './model.js';
4
5
  /** The server owns its artifact connection lifetime, so callers cannot lend one. */
5
6
  export interface SquareMcpOptions extends Omit<SquareAtInput, 'resources'> {
6
7
  readonly owner?: Owner;
7
- readonly wakeAddress?: import('./model.js').WakeAddress;
8
+ readonly wakeAddress?: WakeAddress;
8
9
  }
9
10
  export type SquareMcpServer = McpServer;
10
11
  export declare function createSquareMcpServer(options: SquareMcpOptions): SquareMcpServer;
package/dist/mcp.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, SquareExpressDrafts, squareToolsEnabled, squareToolFailure, squareToolFailureText } from './square-tools.js';
3
+ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, createSquareToolRunner, squareToolsEnabled } from './square-tools.js';
4
4
  import { operationEnv } from './participant-identity.js';
5
5
  import { codexWakeAddress } from './codex-address.js';
6
6
  import { readClaudeMcpCall } from './claude-mcp-context.js';
@@ -12,7 +12,7 @@ export function createSquareMcpServer(options) {
12
12
  const baseEnv = { ...(resourceOptions.env ?? process.env) };
13
13
  const toolsEnabled = squareToolsEnabled(baseEnv);
14
14
  const resources = createSquareResourceOwner();
15
- const drafts = new SquareExpressDrafts();
15
+ const invoke = createSquareToolRunner();
16
16
  // Calls to the configured square borrow one server-owned connection.
17
17
  const inFlight = new Set();
18
18
  const server = new McpServer({ name: 'square', version: SQUARE_IDENTITY.packageVersion }, {
@@ -21,77 +21,47 @@ export function createSquareMcpServer(options) {
21
21
  if (toolsEnabled)
22
22
  for (const tool of SQUARE_TOOLS) {
23
23
  server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
24
- const call = (async () => {
25
- let square;
26
- let toolArgs = args;
27
- let owner;
28
- let draftToken;
29
- let expressAttempted = false;
30
- try {
31
- const threadId = extra._meta?.threadId;
32
- const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
33
- // Claude's plugin endpoint requires the mod's call-local coordinate, never startup identity.
34
- const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
35
- ? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
36
- : undefined;
37
- owner = claude !== undefined
38
- ? harnessOwner({ channel: 'claude-code', session: claude.sessionId, ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) })
39
- : typeof threadId === 'string' && threadId.trim()
40
- ? harnessOwner({ channel: 'codex', session: threadId })
41
- : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
42
- ? harnessOwner({ channel: 'opencode', session: opencodeSessionId })
43
- : configuredOwner;
44
- if (tool.name === 'express') {
45
- const resolved = drafts.resolve(toolArgs, owner);
46
- toolArgs = resolved.args;
47
- draftToken = resolved.token;
48
- }
49
- const { location } = toolArgs;
50
- const env = claude !== undefined
51
- ? { ...operationEnv('claude', claude.sessionId, baseEnv),
52
- // Preserve Claude's session + child flag semantics; an agent loop ID is not a session ID.
53
- CLAUDE_CODE_CHILD_SESSION: claude.agentId === undefined ? '' : '1' }
54
- : typeof threadId === 'string' && threadId.trim()
55
- ? operationEnv('codex', threadId, baseEnv)
56
- : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
57
- ? operationEnv('opencode', opencodeSessionId, baseEnv)
58
- : baseEnv;
59
- square = await Square.at({
24
+ const call = invoke(tool, args, { signal: extra.signal }, async () => {
25
+ const threadId = extra._meta?.threadId;
26
+ const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
27
+ // Claude's plugin endpoint requires the mod's call-local coordinate, never startup identity.
28
+ const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
29
+ ? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
30
+ : undefined;
31
+ const owner = claude !== undefined
32
+ ? harnessOwner({ channel: 'claude-code', session: claude.sessionId, ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) })
33
+ : typeof threadId === 'string' && threadId.trim()
34
+ ? harnessOwner({ channel: 'codex', session: threadId })
35
+ : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
36
+ ? harnessOwner({ channel: 'opencode', session: opencodeSessionId })
37
+ : configuredOwner;
38
+ const env = claude !== undefined
39
+ ? { ...operationEnv('claude', claude.sessionId, baseEnv),
40
+ // An agent loop ID is not a session ID.
41
+ CLAUDE_CODE_CHILD_SESSION: claude.agentId === undefined ? '' : '1' }
42
+ : typeof threadId === 'string' && threadId.trim()
43
+ ? operationEnv('codex', threadId, baseEnv)
44
+ : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
45
+ ? operationEnv('opencode', opencodeSessionId, baseEnv)
46
+ : baseEnv;
47
+ const callWake = wakeAddress !== undefined && (owner === configuredOwner
48
+ || wakeAddress.kind === 'paseo' && wakeAddress.address.agentId === baseEnv.PASEO_AGENT_ID
49
+ || wakeAddress.kind === 'claude-native' && wakeAddress.address.sessionId === claude?.sessionId
50
+ || wakeAddress.kind === 'opencode-server' && wakeAddress.address.sessionId === opencodeSessionId)
51
+ ? wakeAddress : undefined;
52
+ return {
53
+ identity: { owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
54
+ ? codexWakeAddress(threadId, env) : callWake },
55
+ open: (location, control) => Square.at({
60
56
  ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd,
61
57
  path: location ?? options.path,
62
- // The server owner is bound to its configured square; overrides close after each call.
58
+ // Overrides and Claude calls own their connections.
63
59
  resources: location === undefined && claude === undefined ? resources : undefined,
64
- }, { signal: extra.signal });
65
- const callWake = wakeAddress !== undefined && (owner === configuredOwner
66
- || wakeAddress.kind === 'paseo' && wakeAddress.address.agentId === baseEnv.PASEO_AGENT_ID
67
- || wakeAddress.kind === 'claude-native' && wakeAddress.address.sessionId === claude?.sessionId
68
- || wakeAddress.kind === 'opencode-server' && wakeAddress.address.sessionId === opencodeSessionId)
69
- ? wakeAddress : undefined;
70
- let text;
71
- try {
72
- expressAttempted = tool.name === 'express';
73
- text = await tool.run(square, toolArgs, { signal: extra.signal }, {
74
- owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
75
- ? codexWakeAddress(threadId, env) : callWake,
76
- });
77
- drafts.consume(draftToken);
78
- }
79
- finally {
80
- await square.close();
81
- }
82
- return { content: [{ type: 'text', text }] };
83
- }
84
- catch (error) {
85
- const failure = squareToolFailure(error, extra.signal);
86
- const participant = toolArgs.as;
87
- const draft = tool.name === 'express' ? drafts.retain(toolArgs, owner, draftToken, failure, expressAttempted) : undefined;
88
- return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
89
- location: square?.location ?? options.path,
90
- participant: typeof participant === 'string' ? participant : undefined,
91
- draft,
92
- }) }] };
93
- }
94
- })();
60
+ }, control),
61
+ };
62
+ }, options.path).then(({ text, isError }) => ({
63
+ ...(isError ? { isError } : {}), content: [{ type: 'text', text }],
64
+ }));
95
65
  inFlight.add(call);
96
66
  return call.finally(() => { inFlight.delete(call); });
97
67
  });
@@ -0,0 +1,7 @@
1
+ export type MessageId = `msg/${number}`;
2
+ /** Opaque endpoint custody token, allocated when an endpoint is established. */
3
+ export type EndpointGeneration = string;
4
+ export declare function formatMessageId(index: number): MessageId;
5
+ export declare function parseMessageId(value: string): number | undefined;
6
+ export declare function newEndpointGeneration(): EndpointGeneration;
7
+ export declare function validateEndpointGeneration(value: unknown): EndpointGeneration;