@astrosheep/square 0.3.70 → 0.3.71

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 (115) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/help.js +6 -1
  36. package/dist/inbox.d.ts +27 -0
  37. package/dist/inbox.js +55 -18
  38. package/dist/index.d.ts +2 -4
  39. package/dist/index.js +1 -1
  40. package/dist/list.d.ts +7 -1
  41. package/dist/list.js +49 -122
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +7 -37
  44. package/dist/model.d.ts +4 -44
  45. package/dist/model.js +1 -3
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/notifications.js +13 -7
  48. package/dist/open-square.d.ts +16 -2
  49. package/dist/open-square.js +40 -1
  50. package/dist/perception-projection.d.ts +6 -15
  51. package/dist/perception-projection.js +6 -26
  52. package/dist/plugins/opencode/delivery.js +9 -9
  53. package/dist/plugins/opencode/index.js +3 -3
  54. package/dist/ports.d.ts +156 -11
  55. package/dist/presentation-operations.d.ts +1 -1
  56. package/dist/presentation-operations.js +35 -44
  57. package/dist/presentation.d.ts +12 -19
  58. package/dist/presentation.js +33 -54
  59. package/dist/routes.d.ts +7 -14
  60. package/dist/routes.js +33 -62
  61. package/dist/runtime.d.ts +12 -39
  62. package/dist/runtime.js +33 -164
  63. package/dist/square-actions.d.ts +11 -21
  64. package/dist/square-actions.js +171 -271
  65. package/dist/square-application.d.ts +31 -55
  66. package/dist/square-application.js +637 -141
  67. package/dist/square-core.d.ts +3 -94
  68. package/dist/square-core.js +8 -353
  69. package/dist/square-domain.d.ts +24 -0
  70. package/dist/square-domain.js +77 -0
  71. package/dist/square-facade.d.ts +360 -9
  72. package/dist/square-migration.d.ts +24 -0
  73. package/dist/square-migration.js +211 -0
  74. package/dist/square-projections.d.ts +2 -42
  75. package/dist/square-projections.js +1 -37
  76. package/dist/square-sqlite.d.ts +59 -0
  77. package/dist/square-sqlite.js +702 -0
  78. package/dist/stream.d.ts +3 -10
  79. package/dist/stream.js +81 -52
  80. package/dist/views.d.ts +27 -95
  81. package/dist/views.js +217 -87
  82. package/dist/wake-eligibility.d.ts +15 -5
  83. package/dist/wake-eligibility.js +32 -16
  84. package/dist/wakes.d.ts +3 -3
  85. package/dist/wakes.js +31 -27
  86. package/guides/application-facade-delivery-evidence.md +200 -0
  87. package/guides/application-facade-design.md +95 -0
  88. package/guides/change-waits.md +4 -4
  89. package/guides/migration.md +77 -0
  90. package/guides/primitive-catch-consolidation.md +85 -0
  91. package/guides/primitive-history-consolidation.md +76 -0
  92. package/guides/primitive-status-consolidation.md +87 -0
  93. package/guides/primitive-storage-consolidation.md +128 -0
  94. package/guides/row-runtime-delivery-evidence.md +160 -0
  95. package/guides/row-storage-design.md +567 -0
  96. package/package.json +1 -1
  97. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  98. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  99. package/plugins/opencode/delivery.ts +11 -11
  100. package/plugins/opencode/index.ts +3 -3
  101. package/plugins/paseo/client/square-message.tsx +70 -52
  102. package/plugins/paseo/shared/message.ts +15 -0
  103. package/plugins/pi/square-pi.js +3 -0
  104. package/dist/activity.d.ts +0 -14
  105. package/dist/activity.js +0 -248
  106. package/dist/square-file-adapter.d.ts +0 -26
  107. package/dist/square-file-adapter.js +0 -85
  108. package/dist/square-state.d.ts +0 -6
  109. package/dist/square-state.js +0 -138
  110. package/dist/square-storage.d.ts +0 -14
  111. package/dist/square-storage.js +0 -188
  112. package/dist/square-wiring.d.ts +0 -33
  113. package/dist/square-wiring.js +0 -109
  114. package/dist/watch.d.ts +0 -8
  115. package/dist/watch.js +0 -252
@@ -1,11 +1,10 @@
1
- import { deriveDeliveryModel } from './delivery.js';
1
+ 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 { projectWakeEvidenceFromState } from './square-projections.js';
6
- import { openSquare } from './square-file-adapter.js';
5
+ import { projectWakeEvidence } from './wake-eligibility.js';
6
+ import { openSquare } from './open-square.js';
7
7
  import { closeOpenSquare } from './open-square.js';
8
- import { pendingDeliveriesFromState } from './views.js';
9
8
  import { readWakeReleaseDiagnostics } from './wake-attempts.js';
10
9
  const DISPLAY_ORDER = [
11
10
  'awaiting',
@@ -23,21 +22,13 @@ export async function classifyDeliveryHealth(squarePath, opts) {
23
22
  const env = opts.env ?? process.env;
24
23
  const square = await openSquare(squarePath, { clock: () => now, env });
25
24
  try {
26
- const { state } = await square.artifact.read();
27
- const delivery = deriveDeliveryModel(state);
28
- const pending = pendingDeliveriesFromState(state, delivery);
29
- const evidenceProjection = await projectWakeEvidenceFromState({
30
- location: squarePath,
31
- state,
32
- hostLedger: square.hostLedger,
33
- now,
34
- delivery,
35
- });
25
+ const { scan, routes, pending } = await collectPending(square.artifact);
26
+ const evidenceProjection = await projectWakeEvidence({ location: squarePath, routes, pending, hostLedger: square.hostLedger, now });
36
27
  const items = [];
37
- for (const pendingDelivery of pending) {
38
- for (const note of pendingDelivery.notifications) {
39
- const ageMs = Math.max(0, now - note.item.at);
40
- const evidence = evidenceProjection.evidence(pendingDelivery.recipient, note.item.index);
28
+ for (const participant of scan.roster) {
29
+ for (const note of pending.filter((head) => head.recipient === participant.name)) {
30
+ const ageMs = Math.max(0, now - note.at);
31
+ const evidence = evidenceProjection.evidence(note.recipient, note.index);
41
32
  const kind = evidence.presented
42
33
  ? 'presented-not-delivered'
43
34
  : evidence.terminal?.outcome === 'accepted'
@@ -50,10 +41,10 @@ export async function classifyDeliveryHealth(squarePath, opts) {
50
41
  const attempt = evidence.terminal ?? evidence.attempts.at(-1);
51
42
  items.push({
52
43
  squarePath,
53
- recipient: pendingDelivery.recipient,
54
- actIndex: note.item.index,
55
- actor: note.item.actor,
56
- at: note.item.at,
44
+ recipient: note.recipient,
45
+ actIndex: note.index,
46
+ actor: note.actor,
47
+ at: note.at,
57
48
  ageMs,
58
49
  route: note.route,
59
50
  kind,
@@ -1,6 +1,4 @@
1
- import type { SquareState } from './model.js';
2
1
  import type { HostLedgerPort, SquareArtifactPort, DeliverPendingInput, DeliveryResult } from './ports.js';
3
- import { deriveDeliveryModel } from './delivery.js';
4
2
  export declare function deliverPending(input: DeliverPendingInput): Promise<DeliveryResult>;
5
3
  export declare function sweepPending(input: {
6
4
  readonly artifact: SquareArtifactPort;
@@ -10,12 +8,3 @@ export declare function sweepPending(input: {
10
8
  readonly graceMs: number;
11
9
  readonly limit: number;
12
10
  }): Promise<number[]>;
13
- export declare function sweepPendingFromState(input: {
14
- readonly state: SquareState;
15
- readonly hostLedger: HostLedgerPort;
16
- readonly location: string;
17
- readonly now: number;
18
- readonly graceMs: number;
19
- readonly limit: number;
20
- readonly deriveDelivery?: (snapshot: SquareState) => ReturnType<typeof deriveDeliveryModel>;
21
- }): Promise<number[]>;
@@ -1,7 +1,6 @@
1
1
  import { formatActivityId, parseActivityId } from './square-core.js';
2
- import { deriveDeliveryModel } from './delivery.js';
3
- import { projectWakeEvidenceFromState } from './square-projections.js';
4
- import { wakeIsEligible } from './wake-eligibility.js';
2
+ import { capturePendingScan, collectPending, pendingFacts, pendingHead, pendingPages, sameJoin } from './delivery.js';
3
+ import { projectWakeEvidence, wakeIsEligible } from './wake-eligibility.js';
5
4
  import { canonicalPath } from './canonical-path.js';
6
5
  import { retireWakeRouteFromArtifact } from './routes.js';
7
6
  import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
@@ -35,12 +34,20 @@ async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
35
34
  }
36
35
  export async function deliverPending(input) {
37
36
  const location = await canonicalPath(input.location);
38
- const { state } = await input.artifact.read();
39
- const delivery = deriveDeliveryModel(state);
40
- const eligibility = await projectWakeEvidenceFromState({ state, location, hostLedger: input.hostLedger, now: input.now ?? Date.now(), delivery });
37
+ const requested = input.activity === undefined ? undefined : typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
38
+ if (input.activity !== undefined && requested === undefined)
39
+ 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() });
41
43
  const observeCurrentness = async (request, now) => {
42
- const { state: currentState } = await input.artifact.read();
43
- const projection = await projectWakeEvidenceFromState({ state: currentState, location, hostLedger: input.hostLedger, now });
44
+ 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 });
44
51
  return { ...projection.currentness({ ...request, location }), observationAvailable: true };
45
52
  };
46
53
  let attempted = 0;
@@ -48,14 +55,10 @@ export async function deliverPending(input) {
48
55
  let failed = 0;
49
56
  let unknown = 0;
50
57
  let notCapable = 0;
51
- for (const recipient of delivery.joinedRecipients()) {
52
- for (const notification of delivery.pendingFor(recipient)) {
53
- if (input.activity !== undefined) {
54
- const requested = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
55
- if (requested === undefined || requested !== notification.item.index)
56
- continue;
57
- }
58
- const evidence = eligibility.evidence(recipient, notification.item.index);
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);
59
62
  if (evidence.terminal !== undefined)
60
63
  continue;
61
64
  const candidates = evidence.attemptableRoutes;
@@ -66,8 +69,8 @@ export async function deliverPending(input) {
66
69
  if (candidates.length === 0 && !evidence.presented)
67
70
  notCapableForAttention = true;
68
71
  for (const route of candidates) {
69
- const activity = formatActivityId(notification.item.index);
70
- const attention = { squarePath: location, actIndex: notification.item.index, recipient };
72
+ const activity = formatActivityId(notification.index);
73
+ const attention = { squarePath: location, actIndex: notification.index, recipient };
71
74
  const leaseMs = input.timeoutMs ?? 5000;
72
75
  const requestRoute = route;
73
76
  if (input.transport.probe !== undefined) {
@@ -102,7 +105,7 @@ export async function deliverPending(input) {
102
105
  if (claim.status === 'terminal')
103
106
  break;
104
107
  const { claimToken, attemptN } = claim;
105
- const request = { location: input.location, participant: recipient, activity, actor: notification.item.actor, route: requestRoute };
108
+ const request = { location: input.location, participant: recipient, activity, actor: notification.actor, route: requestRoute };
106
109
  const claimRelease = { hostLedger: input.hostLedger, location, participant: recipient, session: route.sessionId, activity, claimToken, routeKind: route.kind, attemptN };
107
110
  // One teardown path for every exit after claim acquisition; each call site keeps its own error policy.
108
111
  const abandon = async (details, options = {}) => {
@@ -202,20 +205,28 @@ export async function deliverPending(input) {
202
205
  return { attempted, accepted, failed, unknown, notCapable };
203
206
  }
204
207
  export async function sweepPending(input) {
205
- const { state } = await input.artifact.read();
206
- return sweepPendingFromState({ ...input, state });
207
- }
208
- export async function sweepPendingFromState(input) {
209
- const delivery = input.deriveDelivery?.(input.state) ?? deriveDeliveryModel(input.state);
210
- const eligibility = await projectWakeEvidenceFromState({ ...input, delivery });
211
- const selected = new Set();
212
- for (const recipient of delivery.joinedRecipients()) {
213
- for (const notification of delivery.pendingFor(recipient)) {
214
- if (input.now - notification.item.at <= input.graceMs)
215
- continue;
216
- if (wakeIsEligible(eligibility.evidence(recipient, notification.item.index)))
217
- selected.add(notification.item.index);
208
+ if (input.limit <= 0)
209
+ 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);
213
+ 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
+ }
218
225
  }
226
+ if (indexes.size >= input.limit)
227
+ break;
219
228
  }
220
- return [...selected].sort((left, right) => left - right).slice(0, Math.max(0, input.limit));
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))];
221
232
  }
@@ -1,6 +1,5 @@
1
- import { type DirectedNotificationRoute, type SquareState, type StoredAct, type WatchLease, type WakeRouteKind } from './model.js';
2
- import { type Perception } from './square-core.js';
3
- import type { WakeOutcome } from './ports.js';
1
+ import type { DirectedNotificationRoute, StoredAct, WatchLease, WakeRouteKind } from './model.js';
2
+ import type { DirectedHead, ParticipantFact, SquareArtifactPort, SquareRead, WakeOutcome } from './ports.js';
4
3
  export type { DirectedNotificationRoute } from './model.js';
5
4
  export type SayItem = StoredAct & {
6
5
  kind: 'say';
@@ -10,21 +9,13 @@ export interface PlannedNotification {
10
9
  recipient: string;
11
10
  route: DirectedNotificationRoute;
12
11
  }
13
- /**
14
- * What one adapter reports about a wake it was handed.
15
- * `gate-rejected` means exactly one thing: the supplied beforeSend gate returned false and nothing was sent.
16
- * It is not a bin for other not-sent reasons; those stay `failed` with `unavailable: true`.
17
- */
12
+ /** gate-rejected means the supplied beforeSend returned false and nothing was sent. */
18
13
  export type WakeAdapterResult = Exclude<WakeOutcome, {
19
14
  outcome: 'not-capable';
20
15
  }> | {
21
16
  outcome: 'gate-rejected';
22
17
  };
23
- /**
24
- * The coordinates of one wake attempt. An adapter that must hand the receiver a
25
- * stable dedup key derives it from these, so a retry of the same attempt reuses
26
- * the same key and a later attempt gets a new one.
27
- */
18
+ /** One attempt's stable coordinates; retries reuse its dedup key. */
28
19
  export interface WakeDispatchContext {
29
20
  readonly location: string;
30
21
  readonly participant: string;
@@ -40,24 +31,34 @@ export interface RoutedNotification {
40
31
  route: DirectedNotificationRoute;
41
32
  recipient: string;
42
33
  }
43
- export interface DeliveryModel {
44
- plan(item: StoredAct): PlannedNotification[];
45
- pendingFor(recipient: string): PlannedNotification[];
46
- directedTo(item: StoredAct, recipient: string): boolean;
47
- perceive(item: StoredAct, viewer: string): Perception;
48
- cursorFor(recipient: string): number;
49
- isSeen(recipient: string, actOrIndex: StoredAct | number): boolean;
50
- knownParticipant(name: string): string | undefined;
51
- participants(): readonly string[];
52
- joinedRecipients(): readonly string[];
53
- readonly replayedActivityCount: number;
34
+ /** Notification pending is not cursor suffix or historical catch membership. */
35
+ export declare function pendingHead(facts: SquareRead, name: string, index: number): DirectedHead | undefined;
36
+ /** Complete recipient projection, assembled inside its one transaction. */
37
+ export declare function pendingNotifications(facts: SquareRead, name: string): PlannedNotification[];
38
+ export interface PendingScan {
39
+ readonly roster: readonly ParticipantFact[];
40
+ readonly throughIndex: number;
54
41
  }
55
- export declare function isActivitySeen(squareState: SquareState, name: string, actOrIndex: StoredAct | number): boolean;
56
- /**
57
- * Derive delivery behavior once from the decoded Square state.
58
- * All consumers share these targets instead of reinterpreting artifact text or cursor state.
59
- */
60
- export declare function deriveDeliveryModel(squareState: SquareState): DeliveryModel;
61
- export declare function perceiveActivity(squareState: SquareState, item: StoredAct, viewer: string, delivery?: DeliveryModel): Perception;
62
- /** True only when the live catch's own filters would deliver this unread notification, independent of page size. */
42
+ export declare function capturePendingScan(facts: SquareRead): PendingScan;
43
+ export declare function sameJoin(facts: SquareRead, captured: ParticipantFact): boolean;
44
+ /** Finite bodyless pass: unrelated revisions never rewind the last examined group. */
45
+ export declare function pendingPages(artifact: SquareArtifactPort, scan: PendingScan, afterIndex?: number): AsyncGenerator<{
46
+ index: number;
47
+ at: number;
48
+ actor: string;
49
+ recipient: string;
50
+ route: "bell" | "mention" | "reply" | "attention";
51
+ }[], void, unknown>;
52
+ /** A pass retains only its captured joins; routes and individual receipts are fresh. */
53
+ export declare function pendingFacts(facts: SquareRead, scan: PendingScan, heads: readonly DirectedHead[]): {
54
+ routes: readonly import("./ports.js").RouteFact[];
55
+ pending: DirectedHead[];
56
+ };
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[];
60
+ pending: DirectedHead[];
61
+ scan: PendingScan;
62
+ }>;
63
+ /** A live catch owns every matching notification, independent of its page size. */
63
64
  export declare function leaseOwnsNotification(lease: WatchLease, notification: RoutedNotification): boolean;
package/dist/delivery.js CHANGED
@@ -1,90 +1,79 @@
1
- import { findParticipantName, sameName, } from './model.js';
2
- import { activityAuthors, audienceOf, replayLandedAudiences } from './square-core.js';
3
- import { derivePerceptionProjection } from './perception-projection.js';
4
- import { matchesMentionTarget } from './runtime.js';
5
1
  import { matchesCatchSelection } from './catch-selection.js';
6
- export function isActivitySeen(squareState, name, actOrIndex) {
7
- return deriveDeliveryModel(squareState).isSeen(name, actOrIndex);
2
+ /** Notification pending is not cursor suffix or historical catch membership. */
3
+ export function pendingHead(facts, name, index) {
4
+ const participant = facts.participant(name);
5
+ if (!participant?.joined || index <= (participant.lastJoinIndex ?? -1) || facts.receipt(participant.name, index))
6
+ return undefined;
7
+ return facts.directedAt(participant.name, index);
8
8
  }
9
- /**
10
- * Derive delivery behavior once from the decoded Square state.
11
- * All consumers share these targets instead of reinterpreting artifact text or cursor state.
12
- */
13
- export function deriveDeliveryModel(squareState) {
14
- const landed = replayLandedAudiences(squareState.acts);
15
- const authors = activityAuthors(squareState.acts);
16
- const perception = derivePerceptionProjection(squareState, landed);
17
- const roster = [...landed.joined];
18
- const plannedByIndex = new Map();
19
- let pendingByRecipient;
20
- function isSeen(requestedRecipient, actOrIndex) {
21
- const index = typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index;
22
- return perception.isSeen(requestedRecipient, index);
9
+ /** Complete recipient projection, assembled inside its one transaction. */
10
+ export function pendingNotifications(facts, name) {
11
+ const participant = facts.participant(name);
12
+ if (!participant?.joined)
13
+ return [];
14
+ const heads = [];
15
+ let afterIndex = participant.lastJoinIndex ?? -1;
16
+ for (;;) {
17
+ const page = facts.directed(participant.name, { afterIndex, unseenOnly: true }, { edge: 'oldest', limit: 100 });
18
+ heads.push(...page.items);
19
+ if (!page.hasMore)
20
+ break;
21
+ afterIndex = page.items.at(-1).index;
23
22
  }
24
- function plan(item) {
25
- if (item.kind !== 'say')
26
- return [];
27
- const cached = plannedByIndex.get(item.index);
28
- if (cached !== undefined)
29
- return [...cached];
30
- const sayItem = item;
31
- const audience = audienceOf(sayItem);
32
- const recipients = landed.recipientsFor(sayItem);
33
- const replyAuthor = sayItem.reply === undefined ? undefined : authors.get(sayItem.reply);
34
- const planned = recipients.map((recipient) => {
35
- const route = audience.kind === 'bell'
36
- ? 'bell'
37
- : matchesMentionTarget(sayItem, recipient) ? 'mention'
38
- : replyAuthor !== undefined && sameName(replyAuthor, recipient) ? 'reply'
39
- : 'attention';
40
- return { item: sayItem, recipient, route };
23
+ const routes = new Map(heads.map((head) => [head.index, head.route]));
24
+ return facts.activities(heads.map((head) => head.index)).flatMap((item) => item.kind === 'say'
25
+ ? [{ item, recipient: participant.name, route: routes.get(item.index) }] : []);
26
+ }
27
+ export function capturePendingScan(facts) {
28
+ return { roster: facts.roster().filter((participant) => participant.joined), throughIndex: facts.header().lastActIndex };
29
+ }
30
+ export function sameJoin(facts, captured) {
31
+ const current = facts.participant(captured.name);
32
+ return current?.joined === captured.joined && current?.lastJoinIndex === captured.lastJoinIndex;
33
+ }
34
+ /** Finite bodyless pass: unrelated revisions never rewind the last examined group. */
35
+ export async function* pendingPages(artifact, scan, afterIndex = -1) {
36
+ const captured = new Map(scan.roster.map((participant) => [participant.name, participant]));
37
+ while (afterIndex < scan.throughIndex) {
38
+ const { value: page } = await artifact.read((facts) => {
39
+ const page = facts.pendingAudience(afterIndex, scan.throughIndex, 100);
40
+ return { ...page, groups: page.groups.map((group) => ({ ...group, recipients: group.recipients.filter((recipient) => {
41
+ const participant = captured.get(recipient.recipient);
42
+ return participant !== undefined && sameJoin(facts, participant);
43
+ }) })) };
41
44
  });
42
- plannedByIndex.set(item.index, planned);
43
- return [...planned];
45
+ yield page.groups.flatMap((group) => group.recipients.map(({ recipient, route }) => ({
46
+ index: group.index, at: group.at, actor: group.actor, recipient, route,
47
+ })));
48
+ if (!page.hasMore)
49
+ break;
50
+ afterIndex = page.groups.at(-1).index;
44
51
  }
45
- function pendingFor(requestedRecipient) {
46
- const recipient = findParticipantName(roster, requestedRecipient);
47
- if (recipient === undefined)
48
- return [];
49
- if (pendingByRecipient === undefined) {
50
- pendingByRecipient = new Map(roster.map((name) => [name, []]));
51
- const joinedAfter = new Map(roster.map((name) => [name, landed.lastJoinIndex(name)]));
52
- for (const act of squareState.acts) {
53
- if (act.kind !== 'say')
54
- continue;
55
- for (const planned of plan(act)) {
56
- const joinedAt = joinedAfter.get(planned.recipient);
57
- if (joinedAt === undefined || act.index <= joinedAt)
58
- continue;
59
- if (isSeen(planned.recipient, act.index))
60
- continue;
61
- pendingByRecipient.get(planned.recipient)?.push(planned);
62
- }
63
- }
64
- }
65
- return [...(pendingByRecipient.get(recipient) ?? [])];
66
- }
67
- return {
68
- plan,
69
- pendingFor,
70
- directedTo: perception.directedTo,
71
- perceive: perception.perceive,
72
- cursorFor: perception.cursorFor,
73
- isSeen,
74
- knownParticipant: (name) => landed.resolveParticipant(name),
75
- participants: () => landed.participants,
76
- joinedRecipients: () => roster,
77
- replayedActivityCount: perception.replayedActivityCount,
78
- };
79
52
  }
80
- export function perceiveActivity(squareState, item, viewer, delivery = deriveDeliveryModel(squareState)) {
81
- return delivery.perceive(item, viewer);
53
+ /** A pass retains only its captured joins; routes and individual receipts are fresh. */
54
+ export function pendingFacts(facts, scan, heads) {
55
+ const joined = new Set(scan.roster.filter((participant) => sameJoin(facts, participant)).map((participant) => participant.name));
56
+ return { routes: facts.routes(), pending: heads.filter((head) => joined.has(head.recipient)
57
+ && pendingHead(facts, head.recipient, head.index) !== undefined) };
58
+ }
59
+ /** Global discovery alone uses pendingAudience; exact dispatch uses its one landed audience. */
60
+ export async function collectPending(artifact, index) {
61
+ const { value: initial } = await artifact.read((facts) => ({
62
+ scan: capturePendingScan(facts),
63
+ pending: index === undefined ? [] : facts.audience(index).flatMap(({ recipient }) => {
64
+ const head = pendingHead(facts, recipient, index);
65
+ return head === undefined ? [] : [head];
66
+ }),
67
+ }));
68
+ const pending = [...initial.pending];
69
+ if (index === undefined)
70
+ for await (const page of pendingPages(artifact, initial.scan))
71
+ pending.push(...page);
72
+ const { value } = await artifact.read((facts) => ({ scan: initial.scan, ...pendingFacts(facts, initial.scan, pending) }));
73
+ return value;
82
74
  }
83
- /** True only when the live catch's own filters would deliver this unread notification, independent of page size. */
75
+ /** A live catch owns every matching notification, independent of its page size. */
84
76
  export function leaseOwnsNotification(lease, notification) {
85
- return matchesCatchSelection(notification.actor, notification.route === 'bell'
86
- ? { kind: 'bell' }
87
- : { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {},
88
- // A reply reaches the recipient as the author of the activity it answers.
89
- notification.route === 'reply' ? notification.recipient : undefined);
77
+ return matchesCatchSelection(notification.actor, notification.route === 'bell' ? { kind: 'bell' }
78
+ : { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {}, notification.route === 'reply' ? notification.recipient : undefined);
90
79
  }
package/dist/help.js CHANGED
@@ -74,6 +74,11 @@ const COMMANDS = [
74
74
  names: ['doctor'], usage: 'doctor', usesSquare: true, group: 'maintenance',
75
75
  summary: 'Validate artifact integrity.',
76
76
  },
77
+ {
78
+ names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
79
+ summary: 'Convert an existing version 1 square to row storage in place.',
80
+ details: ['It is the only migration command: there is no --check, --backup or --force.', 'A version 2 square is left untouched; an unrelated or unsupported artifact is refused without a write.'],
81
+ },
77
82
  ];
78
83
  function definitionFor(command) {
79
84
  return COMMANDS.find((item) => item.names.includes(command));
@@ -85,7 +90,7 @@ export function renderGlobalHelp() {
85
90
  const groups = [
86
91
  { key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'ignore', 'listening', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
87
92
  { key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants'] },
88
- { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor'] },
93
+ { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor', 'migrate'] },
89
94
  ];
90
95
  const commandLines = groups.flatMap(({ key, title, order }) => [
91
96
  title,
package/dist/inbox.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type InboxMembership } from './model.js';
2
+ import type { PresentationProjection, SessionBindingProjection, SquareRead } from './ports.js';
2
3
  export interface PendingWaitOptions {
3
4
  signal?: AbortSignal;
4
5
  /** Local delivery suppression, never persisted as evidence of receipt. */
@@ -7,6 +8,32 @@ export interface PendingWaitOptions {
7
8
  skipImmediate?: boolean;
8
9
  onChangeArmed?: (armed: boolean) => void;
9
10
  }
11
+ export declare function projectPresentation(facts: SquareRead, requested: SessionBindingProjection, now?: number): PresentationProjection;
12
+ export interface NativePendingPreview {
13
+ readonly payload: string;
14
+ readonly clipped: boolean;
15
+ }
16
+ export type NativePendingPreviewInput = {
17
+ readonly location: string;
18
+ readonly sessionId: string;
19
+ readonly participant: string;
20
+ readonly actIndex: number;
21
+ readonly epoch?: number;
22
+ readonly cancelledThrough?: number;
23
+ readonly now: number;
24
+ } & ({
25
+ readonly routeKind: 'claude-native';
26
+ readonly address: {
27
+ readonly endpoint: string;
28
+ };
29
+ } | {
30
+ readonly routeKind: 'opencode-server';
31
+ readonly address: {
32
+ readonly sessionId: string;
33
+ };
34
+ });
35
+ /** First matching ordered route, exact native epoch/address and catch ownership. */
36
+ export declare function nativePendingPreview(facts: SquareRead, input: NativePendingPreviewInput): NativePendingPreview | undefined;
10
37
  export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<InboxMembership[]>;
11
38
  export interface SessionPendingObserver {
12
39
  wait(timeoutMs: number, options?: PendingWaitOptions): Promise<InboxMembership[]>;
package/dist/inbox.js CHANGED
@@ -1,21 +1,55 @@
1
1
  import { nameKey } from './model.js';
2
- import { openSquareArtifact, observeSquareChanges } from './square-file-adapter.js';
2
+ import { createArtifactPort, observeSquareRevision } from './artifact.js';
3
3
  import { hostLedgerForEnv } from './registry.js';
4
4
  import { canonicalPath } from './canonical-path.js';
5
- import { projectBoundaryEligibility, projectPresentation, projectSessionBindings } from './square-projections.js';
6
- import { WATCH_STALE_MS } from './runtime.js';
5
+ import { projectBoundaryEligibility, projectSessionBindings } from './square-projections.js';
6
+ import { freshWatchLease, WATCH_STALE_MS } from './runtime.js';
7
+ import { pendingHead, pendingNotifications, leaseOwnsNotification } from './delivery.js';
8
+ import { attentionBodyIsClipped, renderAttentionPreview } from './attention-presentation.js';
9
+ function bindingCoordinate(binding) {
10
+ return JSON.stringify([binding.location, nameKey(binding.participant), binding.sessionId, binding.channel, binding.epoch, binding.cancelledThrough ?? -1]);
11
+ }
7
12
  function notificationKey(membership, actIndex) {
8
13
  return membership.squarePath + '\0' + membership.name.toLocaleLowerCase() + '\0' + actIndex;
9
14
  }
15
+ export function projectPresentation(facts, requested, now = Date.now()) {
16
+ const participant = facts.participant(requested.participant);
17
+ if (!participant?.joined)
18
+ return { binding: requested, joined: false, notifications: [] };
19
+ const binding = { ...requested, participant: participant.name };
20
+ const notifications = pendingNotifications(facts, participant.name).map(({ item, route }) => ({
21
+ actIndex: item.index, actor: item.actor, at: item.at, body: item.body, route,
22
+ }));
23
+ const catchLease = freshWatchLease(facts, participant.name, now);
24
+ return { binding, joined: true, notifications, ...(catchLease === undefined ? {} : { catchLease }) };
25
+ }
26
+ /** First matching ordered route, exact native epoch/address and catch ownership. */
27
+ export function nativePendingPreview(facts, input) {
28
+ const route = facts.routes().find((candidate) => candidate.kind === input.routeKind && candidate.sessionId === input.sessionId
29
+ && candidate.epoch === input.epoch && nameKey(candidate.participant) === nameKey(input.participant)
30
+ && (input.routeKind === 'claude-native' ? candidate.address.endpoint === input.address.endpoint : candidate.address.sessionId === input.address.sessionId));
31
+ if (!route || input.actIndex <= (input.cancelledThrough ?? -1))
32
+ return undefined;
33
+ const head = pendingHead(facts, input.participant, input.actIndex);
34
+ const lease = freshWatchLease(facts, input.participant, input.now);
35
+ if (!head || (lease !== undefined && leaseOwnsNotification(lease, head)))
36
+ return undefined;
37
+ const item = facts.activity(input.actIndex);
38
+ if (item?.kind !== 'say')
39
+ return undefined;
40
+ return { payload: renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
41
+ actor: item.actor, route: head.route, body: item.body }), clipped: attentionBodyIsClipped(item.body) };
42
+ }
10
43
  async function readInboxMembership(binding, signal) {
11
- const artifact = openSquareArtifact(binding.location, signal);
44
+ const artifact = createArtifactPort(binding.location, undefined, signal);
12
45
  try {
13
- const projection = await projectPresentation({ artifact, binding, now: Date.now() });
14
- if (!projection.joined)
15
- return undefined;
16
- return { name: projection.binding.participant, squarePath: projection.binding.location,
17
- notifications: [...projection.notifications],
18
- ...(projection.catchLease === undefined ? {} : { catchLease: projection.catchLease }) };
46
+ return await artifact.read((facts) => {
47
+ const projection = projectPresentation(facts, binding);
48
+ if (!projection.joined)
49
+ return undefined;
50
+ return { name: projection.binding.participant, squarePath: projection.binding.location, notifications: [...projection.notifications],
51
+ ...(projection.catchLease === undefined ? {} : { catchLease: projection.catchLease }) };
52
+ }, signal);
19
53
  }
20
54
  finally {
21
55
  await artifact.close();
@@ -28,7 +62,7 @@ export async function sessionInbox(sessionId, env = process.env, signal) {
28
62
  if (signal?.aborted)
29
63
  throw signal.reason;
30
64
  try {
31
- const membership = await readInboxMembership(binding, signal);
65
+ const { value: membership } = await readInboxMembership(binding, signal);
32
66
  if (membership !== undefined)
33
67
  inbox.push(membership);
34
68
  }
@@ -100,19 +134,18 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
100
134
  for (const file of paths) {
101
135
  if (artifacts.has(file))
102
136
  continue;
103
- const observer = await observeSquareChanges(file);
137
+ const observer = await observeSquareRevision(file);
104
138
  if (signal.aborted) {
105
139
  observer.close();
106
140
  return [];
107
141
  }
108
142
  artifacts.set(file, { observer, inbox: new Map() });
109
143
  }
144
+ const revisions = new Map();
110
145
  for (const [file, entry] of artifacts) {
111
146
  const source = await capture(entry.observer, 'artifact', signal);
112
147
  sources.push(source);
113
- if (source.value === undefined || source.value !== entry.revision)
114
- entry.inbox.clear();
115
- entry.revision = source.value;
148
+ revisions.set(file, source.value);
116
149
  const names = new Set(bindings.filter((binding) => binding.location === file).map((binding) => nameKey(binding.participant)));
117
150
  for (const name of entry.inbox.keys())
118
151
  if (!names.has(name))
@@ -125,9 +158,13 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
125
158
  const entry = artifacts.get(binding.location);
126
159
  const name = nameKey(binding.participant);
127
160
  try {
128
- if (!entry.inbox.has(name))
129
- entry.inbox.set(name, await readInboxMembership(binding, signal));
130
- const membership = entry.inbox.get(name);
161
+ const coordinate = bindingCoordinate(binding);
162
+ const cached = entry.inbox.get(name);
163
+ if (cached?.version !== revisions.get(binding.location) || cached?.binding !== coordinate) {
164
+ const projection = await readInboxMembership(binding, signal);
165
+ entry.inbox.set(name, { ...projection, binding: coordinate });
166
+ }
167
+ const membership = entry.inbox.get(name)?.value;
131
168
  if (membership === undefined)
132
169
  continue;
133
170
  const { catchLease, ...rest } = membership;