@astrosheep/square 0.3.69 → 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 (132) 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/harness-claude.js +1 -1
  36. package/dist/harness-codex.js +1 -1
  37. package/dist/harness-pi.js +1 -1
  38. package/dist/help.js +6 -1
  39. package/dist/inbox.d.ts +27 -0
  40. package/dist/inbox.js +55 -18
  41. package/dist/index.d.ts +2 -4
  42. package/dist/index.js +1 -1
  43. package/dist/list.d.ts +7 -1
  44. package/dist/list.js +49 -122
  45. package/dist/mcp.d.ts +1 -1
  46. package/dist/mcp.js +7 -37
  47. package/dist/model.d.ts +4 -44
  48. package/dist/model.js +1 -3
  49. package/dist/notifications.d.ts +1 -1
  50. package/dist/notifications.js +13 -7
  51. package/dist/open-square.d.ts +16 -2
  52. package/dist/open-square.js +40 -1
  53. package/dist/perception-projection.d.ts +6 -15
  54. package/dist/perception-projection.js +6 -26
  55. package/dist/{opencode-delivery.d.ts → plugins/opencode/delivery.d.ts} +1 -1
  56. package/dist/{opencode-delivery.js → plugins/opencode/delivery.js} +18 -18
  57. package/dist/{opencode.d.ts → plugins/opencode/index.d.ts} +2 -2
  58. package/dist/{opencode.js → plugins/opencode/index.js} +9 -9
  59. package/dist/ports.d.ts +156 -11
  60. package/dist/presentation-operations.d.ts +1 -1
  61. package/dist/presentation-operations.js +35 -44
  62. package/dist/presentation.d.ts +12 -19
  63. package/dist/presentation.js +33 -54
  64. package/dist/routes.d.ts +7 -14
  65. package/dist/routes.js +33 -62
  66. package/dist/runtime.d.ts +12 -39
  67. package/dist/runtime.js +33 -164
  68. package/dist/square-actions.d.ts +11 -21
  69. package/dist/square-actions.js +171 -271
  70. package/dist/square-application.d.ts +31 -55
  71. package/dist/square-application.js +637 -141
  72. package/dist/square-core.d.ts +3 -94
  73. package/dist/square-core.js +8 -353
  74. package/dist/square-domain.d.ts +24 -0
  75. package/dist/square-domain.js +77 -0
  76. package/dist/square-facade.d.ts +360 -9
  77. package/dist/square-migration.d.ts +24 -0
  78. package/dist/square-migration.js +211 -0
  79. package/dist/square-projections.d.ts +2 -42
  80. package/dist/square-projections.js +1 -37
  81. package/dist/square-sqlite.d.ts +59 -0
  82. package/dist/square-sqlite.js +702 -0
  83. package/dist/stream.d.ts +3 -10
  84. package/dist/stream.js +81 -52
  85. package/dist/views.d.ts +27 -95
  86. package/dist/views.js +217 -87
  87. package/dist/wake-eligibility.d.ts +15 -5
  88. package/dist/wake-eligibility.js +32 -16
  89. package/dist/wakes.d.ts +3 -3
  90. package/dist/wakes.js +31 -27
  91. package/guides/application-facade-delivery-evidence.md +200 -0
  92. package/guides/application-facade-design.md +95 -0
  93. package/guides/change-waits.md +4 -4
  94. package/guides/migration.md +77 -0
  95. package/guides/pi-native-delivery.md +1 -1
  96. package/guides/primitive-catch-consolidation.md +85 -0
  97. package/guides/primitive-history-consolidation.md +76 -0
  98. package/guides/primitive-status-consolidation.md +87 -0
  99. package/guides/primitive-storage-consolidation.md +128 -0
  100. package/guides/row-runtime-delivery-evidence.md +160 -0
  101. package/guides/row-storage-design.md +567 -0
  102. package/package.json +5 -7
  103. package/{claude-plugin → plugins/claude}/.claude-plugin/plugin.json +1 -1
  104. package/{codex-plugin → plugins/codex}/.codex-plugin/plugin.json +1 -1
  105. package/plugins/opencode/delivery.ts +149 -0
  106. package/plugins/opencode/index.ts +136 -0
  107. package/plugins/paseo/OVERVIEW.md +14 -0
  108. package/plugins/paseo/README.md +54 -0
  109. package/plugins/paseo/client/square-message.tsx +106 -0
  110. package/plugins/paseo/client/transform.ts +24 -0
  111. package/plugins/paseo/index.client.tsx +19 -0
  112. package/plugins/paseo/package.json +19 -0
  113. package/plugins/paseo/paseo-plugin.json +7 -0
  114. package/plugins/paseo/shared/message.ts +29 -0
  115. package/{extensions → plugins/pi}/square-pi.js +8 -5
  116. package/dist/activity.d.ts +0 -14
  117. package/dist/activity.js +0 -248
  118. package/dist/square-file-adapter.d.ts +0 -26
  119. package/dist/square-file-adapter.js +0 -85
  120. package/dist/square-state.d.ts +0 -6
  121. package/dist/square-state.js +0 -138
  122. package/dist/square-storage.d.ts +0 -14
  123. package/dist/square-storage.js +0 -188
  124. package/dist/square-wiring.d.ts +0 -33
  125. package/dist/square-wiring.js +0 -109
  126. package/dist/watch.d.ts +0 -8
  127. package/dist/watch.js +0 -252
  128. /package/{claude-plugin → plugins/claude}/hooks/hooks.json +0 -0
  129. /package/{claude-plugin → plugins/claude}/hooks/register.js +0 -0
  130. /package/{claude-plugin → plugins/claude}/skills/square/SKILL.md +0 -0
  131. /package/{claude-plugin → plugins/claude}/types.d.ts +0 -0
  132. /package/{codex-plugin → plugins/codex}/hooks/hooks.json +0 -0
@@ -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
  }
@@ -31,7 +31,7 @@ export async function installClaudePlugin(homeDir, run = runClaude) {
31
31
  const marketplaceRoot = claudeMarketplaceRoot(homeDir);
32
32
  const staged = stageReplacement(marketplaceRoot, (stage) => {
33
33
  const plugin = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
34
- fs.cpSync(fileURLToPath(new URL('../claude-plugin/', import.meta.url)), plugin, { recursive: true });
34
+ fs.cpSync(fileURLToPath(new URL('../plugins/claude/', import.meta.url)), plugin, { recursive: true });
35
35
  const skill = path.join(plugin, 'skills', 'square', 'SKILL.md');
36
36
  fs.mkdirSync(path.dirname(skill), { recursive: true });
37
37
  fs.copyFileSync(path.join(fileURLToPath(new URL('../skills/square/', import.meta.url)), 'SKILL.md'), skill);
@@ -85,7 +85,7 @@ export async function installCodexPlugin(homeDir, run = runCodex, codexHomeDir)
85
85
  const root = activeMarketplaceRoot(homeDir, current, resolvedCodexHome);
86
86
  const staged = stageReplacement(root, (stage) => {
87
87
  const plugin = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
88
- fs.cpSync(fileURLToPath(new URL('../codex-plugin/', import.meta.url)), plugin, { recursive: true });
88
+ fs.cpSync(fileURLToPath(new URL('../plugins/codex/', import.meta.url)), plugin, { recursive: true });
89
89
  const skill = path.join(plugin, 'skills', 'square', 'SKILL.md');
90
90
  fs.mkdirSync(path.dirname(skill), { recursive: true });
91
91
  fs.copyFileSync(path.join(fileURLToPath(new URL('../skills/square/', import.meta.url)), 'SKILL.md'), skill);
@@ -50,7 +50,7 @@ export function doctorPiPackage(homeDir, run = runPi) {
50
50
  manifest?.version === SQUARE_IDENTITY.packageVersion
51
51
  ? `✓ Pi package ${SQUARE_IDENTITY.packageVersion} installed at ${root}`
52
52
  : `○ Pi package ${SQUARE_IDENTITY.packageVersion} missing at ${root}`,
53
- Array.isArray(extensions) && extensions.includes('./extensions/square-pi.js')
53
+ Array.isArray(extensions) && extensions.includes('./plugins/pi/square-pi.js')
54
54
  ? '✓ Pi Square extension declared'
55
55
  : '○ Pi Square extension not declared',
56
56
  ];
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[]>;