@astrosheep/square 0.3.93 → 0.3.95

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 (146) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +3 -3
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.d.ts +2 -2
  7. package/dist/boundary-presentation.js +33 -40
  8. package/dist/catch-decisions.d.ts +5 -8
  9. package/dist/catch-decisions.js +45 -25
  10. package/dist/claude-delivery-status.js +10 -4
  11. package/dist/claude-delivery.js +19 -11
  12. package/dist/claude-mod.js +24 -53
  13. package/dist/cli/context.d.ts +9 -4
  14. package/dist/cli/context.js +30 -27
  15. package/dist/cli/express-presentation.d.ts +3 -1
  16. package/dist/cli/express-presentation.js +28 -16
  17. package/dist/cli/json.d.ts +3 -0
  18. package/dist/cli/json.js +14 -0
  19. package/dist/cli/mcp-server-command.js +2 -2
  20. package/dist/cli/observation-commands.d.ts +1 -3
  21. package/dist/cli/observation-commands.js +43 -56
  22. package/dist/cli/program.js +3 -3
  23. package/dist/cli/registry.js +7 -6
  24. package/dist/cli/square-commands.d.ts +0 -2
  25. package/dist/cli/square-commands.js +30 -44
  26. package/dist/codex-hook.d.ts +3 -2
  27. package/dist/codex-hook.js +13 -4
  28. package/dist/codex-queue.js +4 -4
  29. package/dist/decisions.d.ts +5 -16
  30. package/dist/decisions.js +9 -33
  31. package/dist/delivery-attempts.d.ts +52 -0
  32. package/dist/delivery-attempts.js +73 -0
  33. package/dist/delivery-deferral.js +2 -2
  34. package/dist/delivery-effects.d.ts +139 -0
  35. package/dist/delivery-effects.js +163 -0
  36. package/dist/delivery-health.d.ts +0 -1
  37. package/dist/delivery-health.js +1 -3
  38. package/dist/delivery-operations.d.ts +2 -2
  39. package/dist/delivery-operations.js +76 -101
  40. package/dist/delivery-primitives.d.ts +5 -0
  41. package/dist/delivery-primitives.js +3 -0
  42. package/dist/delivery.d.ts +1 -1
  43. package/dist/delivery.js +7 -6
  44. package/dist/help.js +14 -14
  45. package/dist/http-delivery.d.ts +16 -0
  46. package/dist/http-delivery.js +101 -0
  47. package/dist/inbox.d.ts +9 -22
  48. package/dist/inbox.js +23 -27
  49. package/dist/index.d.ts +1 -3
  50. package/dist/index.js +1 -1
  51. package/dist/mcp.d.ts +3 -0
  52. package/dist/mcp.js +23 -24
  53. package/dist/model.d.ts +12 -21
  54. package/dist/model.js +5 -8
  55. package/dist/notifications.d.ts +1 -1
  56. package/dist/notifications.js +8 -5
  57. package/dist/operation-presentation.d.ts +6 -6
  58. package/dist/operation-presentation.js +55 -58
  59. package/dist/owner.d.ts +11 -0
  60. package/dist/owner.js +32 -0
  61. package/dist/paseo-delivery.d.ts +4 -1
  62. package/dist/paseo-delivery.js +7 -12
  63. package/dist/paseo-state.d.ts +2 -2
  64. package/dist/paseo-state.js +6 -4
  65. package/dist/plugins/opencode/delivery.js +27 -15
  66. package/dist/ports.d.ts +18 -47
  67. package/dist/presentation-operations.d.ts +14 -19
  68. package/dist/presentation-operations.js +51 -61
  69. package/dist/presentation.d.ts +7 -17
  70. package/dist/presentation.js +46 -91
  71. package/dist/receive.d.ts +66 -0
  72. package/dist/receive.js +54 -0
  73. package/dist/runtime.d.ts +1 -8
  74. package/dist/runtime.js +2 -17
  75. package/dist/seats.d.ts +2 -3
  76. package/dist/seats.js +7 -15
  77. package/dist/session-lifecycle.d.ts +6 -2
  78. package/dist/session-lifecycle.js +39 -11
  79. package/dist/sqlite-square.d.ts +36 -23
  80. package/dist/sqlite-square.js +162 -211
  81. package/dist/square-api.d.ts +16 -11
  82. package/dist/square-api.js +27 -23
  83. package/dist/square-core.d.ts +0 -14
  84. package/dist/square-core.js +0 -9
  85. package/dist/square-domain.d.ts +4 -2
  86. package/dist/square-domain.js +65 -52
  87. package/dist/square-facade.d.ts +47 -105
  88. package/dist/square-index.d.ts +5 -13
  89. package/dist/square-index.js +13 -38
  90. package/dist/square-legacy-framing.d.ts +2 -0
  91. package/dist/square-legacy-framing.js +8 -0
  92. package/dist/square-location.d.ts +1 -14
  93. package/dist/square-location.js +4 -70
  94. package/dist/square-migration-host-ledger.d.ts +2 -2
  95. package/dist/square-migration-host-ledger.js +6 -5
  96. package/dist/square-migration.d.ts +4 -1
  97. package/dist/square-migration.js +93 -38
  98. package/dist/square-receive.d.ts +6 -0
  99. package/dist/square-receive.js +18 -0
  100. package/dist/square-sqlite.d.ts +9 -3
  101. package/dist/square-sqlite.js +139 -114
  102. package/dist/square-tools.d.ts +7 -10
  103. package/dist/square-tools.js +21 -61
  104. package/dist/stream.js +4 -5
  105. package/dist/views.d.ts +23 -13
  106. package/dist/views.js +47 -53
  107. package/dist/wakes.d.ts +1 -5
  108. package/dist/wakes.js +4 -6
  109. package/examples/symmetric-domains.mjs +250 -0
  110. package/guides/application-facade-delivery-evidence.md +6 -6
  111. package/guides/attention-primitive.md +32 -0
  112. package/guides/change-waits.md +2 -2
  113. package/guides/claude-native.md +18 -9
  114. package/guides/codex-delivery.md +17 -6
  115. package/guides/delivery-primitives.md +180 -0
  116. package/guides/mcp-connection-lifetime.md +1 -1
  117. package/guides/mcp.md +3 -3
  118. package/guides/migration.md +148 -151
  119. package/guides/operation-cancellation.md +1 -1
  120. package/guides/participant-ownership.md +78 -47
  121. package/guides/pi-tools.md +34 -5
  122. package/guides/primitive-catch-consolidation.md +5 -5
  123. package/guides/primitive-history-consolidation.md +2 -2
  124. package/guides/primitive-status-consolidation.md +3 -3
  125. package/guides/primitive-storage-consolidation.md +1 -1
  126. package/guides/receive-primitive.md +90 -0
  127. package/guides/row-runtime-delivery-evidence.md +1 -1
  128. package/guides/row-storage-design.md +22 -28
  129. package/guides/square-api.md +314 -91
  130. package/guides/symmetric-domains.md +92 -0
  131. package/package.json +14 -1
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/skills/square/SKILL.md +2 -6
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +27 -17
  136. package/plugins/paseo/client/palette.ts +18 -18
  137. package/plugins/paseo/client/square-message.tsx +17 -23
  138. package/plugins/pi/square-pi.js +17 -17
  139. package/skills/square/SKILL.md +2 -6
  140. package/dist/square-backend.d.ts +0 -13
  141. package/dist/square-backend.js +0 -1
  142. package/dist/square-stdio.d.ts +0 -22
  143. package/dist/square-stdio.js +0 -235
  144. package/dist/stdio-server.d.ts +0 -3
  145. package/dist/stdio-server.js +0 -212
  146. package/guides/backends.md +0 -193
@@ -1,6 +1,4 @@
1
1
  import type { SquareRead } from './ports.js';
2
- import type { StoredAct } from './model.js';
3
- /** Directed suffix includes seen rows beyond an earlier hole, unlike notification pending. */
4
- export declare function directedSuffix(facts: SquareRead, name: string): Extract<StoredAct, {
5
- kind: 'say';
6
- }>[];
2
+ import type { ExpressContext } from './square-facade.js';
3
+ /** A bounded, nonconsuming glimpse; seen rows beyond a cursor hole must not reappear. */
4
+ export declare function expressionContext(facts: SquareRead, name: string): ExpressContext;
@@ -1,15 +1,17 @@
1
- import { readCursor } from './runtime.js';
2
- /** Directed suffix includes seen rows beyond an earlier hole, unlike notification pending. */
3
- export function directedSuffix(facts, name) {
4
- const result = [];
5
- let afterIndex = readCursor(facts, name);
6
- while (true) {
7
- const page = facts.directed(name, { afterIndex }, { edge: 'oldest', limit: 100 });
8
- for (const act of facts.activities(page.items.map((head) => head.index)))
9
- if (act.kind === 'say')
10
- result.push(act);
11
- if (!page.hasMore)
12
- return result;
13
- afterIndex = page.items.at(-1).index;
14
- }
1
+ import { formatActivityId } from './square-core.js';
2
+ const CONTEXT_LIMIT = 5;
3
+ const PREVIEW_CHARS = 400;
4
+ /** A bounded, nonconsuming glimpse; seen rows beyond a cursor hole must not reappear. */
5
+ export function expressionContext(facts, name) {
6
+ const filter = { afterIndex: facts.participant(name)?.lastJoinIndex ?? -1, unseenOnly: true };
7
+ const page = facts.directed(name, filter, { edge: 'newest', limit: CONTEXT_LIMIT });
8
+ const activities = facts.activities(page.items.map(head => head.index)).filter(act => act.kind === 'say')
9
+ .sort((a, b) => a.index - b.index).map(act => {
10
+ const body = act.body ?? '(structured activity)';
11
+ // Keep the boundary between Unicode code points, including non-BMP characters.
12
+ const chars = Array.from(body.slice(0, PREVIEW_CHARS * 2));
13
+ const preview = chars.slice(0, PREVIEW_CHARS).join('');
14
+ return { id: formatActivityId(act.index), actor: act.actor, preview, truncated: preview.length < body.length };
15
+ });
16
+ return { activities, remaining: Math.max(0, facts.directedCount(name, filter) - activities.length) };
15
17
  }
package/dist/artifact.js CHANGED
@@ -117,13 +117,13 @@ function synchronous(value) {
117
117
  }
118
118
  }
119
119
  const readMethods = [
120
- 'header', 'participant', 'roster', 'attention', 'incomingAttention', 'hold',
120
+ 'header', 'participant', 'roster', 'attention', 'incomingAttention',
121
121
  'activity', 'activityHeads', 'activities', 'activityCount', 'contextIndexes', 'sayOrdinals', 'recentSayTimes',
122
122
  'audience', 'directed', 'directedAt', 'directedCount', 'pendingAudience',
123
- 'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'participantCounts', 'seat', 'seatsForBody', 'completedSeatsForBody',
123
+ 'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'participantCounts', 'seat', 'seatsForOwner',
124
124
  'deliveryEvidence', 'deliveryByToken', 'cancelledThrough', 'deliveryDeferrals',
125
125
  ];
126
- const writeMethods = ['applyLanding', 'markSeen', 'putLease', 'removeLease', 'storeSeat', 'removeSeat', 'updateWakeAddress', 'cancelThrough', 'claimDelivery', 'prepareDelivery', 'completeDelivery', 'observeNativeDelivery', 'putDeliveryDeferral', 'renewDeliveryDeferral', 'releaseDeliveryDeferral'];
126
+ const writeMethods = ['applyLanding', 'markSeen', 'putLease', 'removeLease', 'storeSeat', 'removeSeat', 'updateWakeAddress', 'reattachSeat', 'detachSeat', 'cancelThrough', 'claimDelivery', 'prepareDelivery', 'completeDelivery', 'observeNativeDelivery', 'putDeliveryDeferral', 'renewDeliveryDeferral', 'releaseDeliveryDeferral'];
127
127
  /** One connection, ordered operations, guarded synchronous transaction capabilities. */
128
128
  export function createArtifactPort(location, initial, externalSignal) {
129
129
  let database;
@@ -0,0 +1,19 @@
1
+ export type AudienceRoute = 'mention' | 'reply' | 'attention' | 'bell';
2
+ export interface AudienceRecipient {
3
+ readonly recipient: string;
4
+ readonly route: AudienceRoute;
5
+ }
6
+ export interface AudienceInput {
7
+ readonly actor: string;
8
+ readonly recipients: readonly string[];
9
+ readonly mentions?: readonly string[];
10
+ readonly replyAuthor?: string;
11
+ readonly reach?: 'bell';
12
+ readonly incoming?: readonly {
13
+ readonly listener: string;
14
+ readonly kind: 'listen' | 'ignore';
15
+ }[];
16
+ readonly sameIdentity?: (a: string, b: string) => boolean;
17
+ }
18
+ /** Resolve attention against the caller's eligible recipient snapshot. */
19
+ export declare function resolveAudience(input: AudienceInput): readonly AudienceRecipient[];
@@ -0,0 +1,30 @@
1
+ /** Resolve attention against the caller's eligible recipient snapshot. */
2
+ export function resolveAudience(input) {
3
+ const same = input.sameIdentity ?? ((a, b) => a === b);
4
+ const incoming = input.incoming ?? [];
5
+ const audience = [];
6
+ const add = (requested, route) => {
7
+ const recipient = input.recipients.find((item) => same(item, requested));
8
+ if (recipient === undefined || same(recipient, input.actor)
9
+ || audience.some((item) => same(item.recipient, recipient)))
10
+ return;
11
+ if (input.reach !== 'bell' && incoming.some((item) => item.kind === 'ignore' && same(item.listener, recipient)))
12
+ return;
13
+ audience.push({ recipient, route });
14
+ };
15
+ if (input.reach === 'bell') {
16
+ for (const recipient of input.recipients)
17
+ add(recipient, 'bell');
18
+ }
19
+ else {
20
+ for (const mention of input.mentions ?? [])
21
+ add(mention, 'mention');
22
+ if (input.replyAuthor !== undefined)
23
+ add(input.replyAuthor, 'reply');
24
+ for (const recipient of input.recipients) {
25
+ if (incoming.some((item) => item.kind === 'listen' && same(item.listener, recipient)))
26
+ add(recipient, 'attention');
27
+ }
28
+ }
29
+ return audience;
30
+ }
@@ -1,5 +1,5 @@
1
1
  import { pendingAtBoundary } from './inbox.js';
2
- import type { Body, PendingMembership } from './model.js';
2
+ import type { Owner, PendingMembership } from './model.js';
3
3
  export { pendingAtBoundary };
4
4
  export declare function renderPendingAtBoundary(inbox: PendingMembership[]): string;
5
5
  /**
@@ -11,4 +11,4 @@ export declare function renderPendingAtBoundary(inbox: PendingMembership[]): str
11
11
  * after success, captured-seat commits record only its included items without an eligibility veto.
12
12
  * A source that cannot verify its reuse must read the inbox again.
13
13
  */
14
- export declare function presentPendingAtBoundary<T>(identity: Body, present: (context: string) => T | Promise<T>, lookup?: (identity: Body, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
14
+ export declare function presentPendingAtBoundary<T>(identity: Owner, present: (context: string) => T | Promise<T>, lookup?: (identity: Owner, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { withFileLock } from './file-lock.js';
4
4
  import { createArtifactPort } from './artifact.js';
5
5
  import { preparePendingPresentation } from './presentation-operations.js';
6
+ import { presentPreparedBatch } from './delivery-effects.js';
6
7
  import { sessionInbox, projectBoundaryEligibility, pendingAtBoundary, pendingKey } from './inbox.js';
7
8
  import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
8
9
  export { pendingAtBoundary };
@@ -56,53 +57,45 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
56
57
  const artifacts = new Map();
57
58
  const claims = [];
58
59
  try {
59
- // Claim the entire candidate batch before its one external effect. No later item needs
60
- // a new claim (and new eligibility decision) after its bytes have already been rendered.
61
- for (const entry of renderBoundary(pending).complete) {
62
- const { location, seat } = entry.membership;
63
- let artifact = artifacts.get(location);
64
- if (artifact === undefined) {
65
- // This boundary owns the connection until all effect evidence/cleanup has settled.
66
- // Request cancellation travels with pre-effect operations, not that lifetime.
67
- artifact = createArtifactPort(location);
68
- artifacts.set(location, artifact);
69
- }
70
- for (const index of entry.actIndexes) {
71
- const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
72
- activity: index, seat, markSeen: entry.markSeen, signal });
73
- if (claim !== undefined)
74
- claims.push({ key: pendingKey(entry.membership, index), claim });
75
- }
76
- }
77
- const eligible = new Set();
78
- for (const { key, claim } of claims)
79
- if (await claim.eligible())
80
- eligible.add(key);
81
- const fresh = renderBoundary(pending.map(membership => ({ ...membership,
82
- notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
83
- })));
84
- const included = new Set(fresh.complete.flatMap(entry => entry.actIndexes.map(index => pendingKey(entry.membership, index))));
85
- if (included.size === 0 || signal?.aborted)
86
- return undefined;
87
- const rendered = claims.filter(({ key }) => included.has(key));
88
- let result;
89
60
  try {
90
- result = await present(fresh.context);
61
+ // Enumeration owns discovery/connections only; returned handles belong to the common runner.
62
+ for (const entry of renderBoundary(pending).complete) {
63
+ const { location, seat } = entry.membership;
64
+ let artifact = artifacts.get(location);
65
+ if (artifact === undefined) {
66
+ // Keep connections alive through all signal-independent settlement/cleanup.
67
+ artifact = createArtifactPort(location);
68
+ artifacts.set(location, artifact);
69
+ }
70
+ for (const index of entry.actIndexes) {
71
+ const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
72
+ activity: index, seat, markSeen: entry.markSeen, signal });
73
+ if (claim !== undefined)
74
+ claims.push({ key: pendingKey(entry.membership, index), claim });
75
+ }
76
+ }
91
77
  }
92
78
  catch (error) {
93
- await Promise.all(rendered.map(({ claim }) => claim.release('failed', error instanceof Error ? error.message : String(error))));
79
+ // Transfer has not happened: enumeration must abandon any custody already acquired.
80
+ await Promise.all(claims.map(({ claim }) => claim.release()));
94
81
  throw error;
95
82
  }
96
- // These are successful sink effects, not fresh delivery attempts. Settle the entire
97
- // rendered set before closing any connection, even if one member's settlement fails.
98
- const settlements = await Promise.allSettled(rendered.map(({ claim }) => claim.recordPresented()));
99
- for (const settlement of settlements)
100
- if (settlement.status === 'rejected')
101
- throw settlement.reason;
102
- return result;
83
+ const report = await presentPreparedBatch(claims.map(({ claim }) => claim), eligibleClaims => {
84
+ const eligible = new Set(claims.filter(({ claim }) => eligibleClaims.includes(claim)).map(({ key }) => key));
85
+ const fresh = renderBoundary(pending.map(membership => ({ ...membership,
86
+ notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
87
+ })));
88
+ const included = new Set(fresh.complete.flatMap(entry => entry.actIndexes.map(index => pendingKey(entry.membership, index))));
89
+ return { payload: fresh.context, included: claims.filter(({ key }) => included.has(key)).map(({ claim }) => claim) };
90
+ }, present, { signal, classifyError: () => 'failed' });
91
+ if (report.observation.kind === 'threw')
92
+ throw report.observation.error;
93
+ for (const { result } of report.settlements)
94
+ if (result.persistence.status === 'error')
95
+ throw result.persistence.error;
96
+ return report.observation.kind === 'returned' ? report.observation.value : undefined;
103
97
  }
104
98
  finally {
105
- await Promise.all(claims.map(({ claim }) => claim.release()));
106
99
  await Promise.all([...artifacts.values()].map(artifact => artifact.close()));
107
100
  }
108
101
  }
@@ -1,23 +1,20 @@
1
- import { type ActivityId } from './square-core.js';
2
1
  import { type StoredAct } from './model.js';
3
2
  import type { ActivityIndex, DirectedFilter, SquareRead, SquareWrite } from './ports.js';
4
- import type { CatchOptions } from './square-facade.js';
3
+ import type { CatchOptions, CatchReason, PerceivedActivity } from './square-facade.js';
5
4
  export interface CatchSelection {
6
5
  readonly participant: string;
7
- readonly activities: readonly StoredAct[];
8
- readonly consumedThrough: ActivityId | null;
6
+ readonly activities: readonly PerceivedActivity[];
9
7
  readonly remaining: number;
10
- readonly sayNumbers: Readonly<Record<number, number>>;
11
8
  readonly standingCount: number;
12
- readonly held: boolean;
9
+ readonly deliveryAvailable: boolean;
13
10
  readonly now: number;
14
11
  readonly ownActivityCount: number;
15
12
  readonly hardCap: number | null;
16
- readonly terminalStatus: 'capped' | 'quorum' | undefined;
13
+ readonly terminalStatus: Extract<CatchReason, 'capped' | 'quorum'> | undefined;
17
14
  readonly hint?: string;
18
15
  }
19
16
  /** Capture a selected page (or an empty completion) without opening another resource. */
20
- export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly StoredAct[], filter: DirectedFilter, at: number): CatchSelection;
17
+ export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly StoredAct[], filter: DirectedFilter, at: number, remaining?: number): CatchSelection;
21
18
  export declare const CATCH_DEFAULT_LIMIT = 10;
22
19
  export declare const CATCH_MAX_LIMIT = 100;
23
20
  export declare function catchLimit(value: number | undefined): number;
@@ -1,19 +1,21 @@
1
1
  import { formatActivityId, parseActivityId } from './square-core.js';
2
- import { readCursor, terminalStatusFor } from './runtime.js';
2
+ import { terminalStatusFor } from './runtime.js';
3
3
  import { SquareError } from './model.js';
4
4
  import { resolveKnownName } from './decisions.js';
5
+ import { toPerceivedActivity } from './views.js';
5
6
  import { catchHint } from './hints.js';
7
+ import { consumeReceived, ReceiveError, selectReceivedHeads } from './receive.js';
8
+ import { squareReceiveRead } from './square-receive.js';
6
9
  /** Capture a selected page (or an empty completion) without opening another resource. */
7
- export function observeCatchSelection(facts, participant, activities, filter, at) {
10
+ export function observeCatchSelection(facts, participant, activities, filter, at, remaining) {
8
11
  if (typeof at !== 'number' || !Number.isFinite(at))
9
12
  throw new SquareError('invalid_args', 'Catch time must be a finite number.');
10
- const cursor = readCursor(facts, participant);
11
13
  const counts = facts.participantCounts(), header = facts.header(), current = facts.participant(participant);
12
14
  return {
13
- participant, activities, consumedThrough: cursor < 0 ? null : formatActivityId(cursor),
14
- remaining: facts.directedCount(participant, filter),
15
- sayNumbers: activities.length === 0 ? {} : facts.sayOrdinals(activities.map((item) => item.index)),
16
- standingCount: counts.joined, held: facts.hold().active, now: at,
15
+ participant, activities: activities.map(activity => toPerceivedActivity(facts, activity, participant)),
16
+ remaining: remaining ?? facts.directedCount(participant, filter),
17
+ standingCount: counts.joined, now: at,
18
+ deliveryAvailable: facts.seat(participant)?.wakeAddress !== undefined,
17
19
  ownActivityCount: current.sayCount, hardCap: header.hardCap,
18
20
  terminalStatus: terminalStatusFor(current, header.hardCap, counts),
19
21
  };
@@ -27,19 +29,23 @@ export function catchLimit(value) {
27
29
  }
28
30
  return limit;
29
31
  }
30
- /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
31
- export function selectCatchPage(facts, requestedName, options) {
32
+ function catchPageSelection(facts, requestedName, options) {
32
33
  const participant = resolveKnownName(facts, requestedName);
33
34
  const limit = catchLimit(options.limit);
34
- const senders = options.from?.map((name) => resolveKnownName(facts, name));
35
+ const senders = options.from?.map(name => resolveKnownName(facts, name));
35
36
  const filter = { unseenOnly: true, ...(senders === undefined ? {} : { senders }), ...(options.mention === true ? { attentionOnly: true } : {}) };
36
- return { participant, indexes: facts.directed(participant, filter, { edge: 'oldest', limit }).items.map((item) => item.index), filter };
37
+ return { participant, filter, selection: { kind: 'page', query: filter, limit } };
38
+ }
39
+ /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
40
+ export function selectCatchPage(facts, requestedName, options) {
41
+ const { participant, filter, selection } = catchPageSelection(facts, requestedName, options);
42
+ return { participant, indexes: selectReceivedHeads(squareReceiveRead(facts), participant, selection).map(item => item.key), filter };
37
43
  }
38
44
  /** Selection, receipts and resulting cursor belong to one artifact write transaction. */
39
45
  export function decideCatch(facts, requestedName, options, at) {
40
46
  let viewer;
41
- let delivered;
42
47
  let filter = { unseenOnly: true };
48
+ let selection;
43
49
  if (options.id !== undefined) {
44
50
  viewer = resolveKnownName(facts, requestedName);
45
51
  catchLimit(options.limit);
@@ -48,25 +54,39 @@ export function decideCatch(facts, requestedName, options, at) {
48
54
  const index = parseActivityId(options.id);
49
55
  if (index === undefined)
50
56
  throw new SquareError('invalid_args', 'Invalid catch id: expected an activity id like act/12.');
51
- const activity = facts.activity(index);
52
- if (activity === undefined || activity.kind !== 'say' || facts.directedAt(viewer, index) === undefined) {
53
- throw new SquareError('invalid_args', `✕ ${formatActivityId(index)} isn't addressed to you`, {
54
- reason: 'not_addressed', activityId: formatActivityId(index), activityExists: activity !== undefined,
55
- });
56
- }
57
- delivered = [activity];
57
+ selection = { kind: 'exact', key: index, remainingQuery: filter };
58
58
  }
59
59
  else {
60
- const page = selectCatchPage(facts, requestedName, options);
60
+ const page = catchPageSelection(facts, requestedName, options);
61
61
  viewer = page.participant;
62
62
  filter = page.filter;
63
- delivered = facts.activities(page.indexes);
63
+ selection = page.selection;
64
+ }
65
+ let consumed;
66
+ try {
67
+ consumed = consumeReceived({
68
+ ...squareReceiveRead(facts),
69
+ insertReceipt: (ref, time) => facts.markSeen(ref.recipient, [ref.item], time) > 0,
70
+ }, viewer, selection, at);
71
+ }
72
+ catch (error) {
73
+ if ((error instanceof TypeError || error instanceof RangeError) && (typeof at !== 'number' || !Number.isFinite(at))) {
74
+ throw new SquareError('invalid_args', 'Invalid square artifact: row is malformed.');
75
+ }
76
+ if (!(error instanceof ReceiveError))
77
+ throw error;
78
+ if (error.code === 'missing_item')
79
+ throw new SquareError('invalid_args', 'Invalid square artifact: addressed activity is missing.');
80
+ const index = error.key;
81
+ throw new SquareError('invalid_args', `✕ ${formatActivityId(index)} isn't addressed to you`, {
82
+ reason: 'not_addressed', activityId: formatActivityId(index), activityExists: facts.activity(index) !== undefined,
83
+ });
64
84
  }
65
- const marked = facts.markSeen(viewer, delivered.map((item) => item.index), at);
66
- const selection = observeCatchSelection(facts, viewer, delivered, filter, at);
85
+ const delivered = consumed.items.map(item => item.data);
86
+ const observed = observeCatchSelection(facts, viewer, delivered, filter, at, consumed.remaining);
67
87
  // Receipts are the durable catch count: everything directed here minus what is still unseen.
68
88
  const caught = facts.directedCount(viewer, {}) - facts.directedCount(viewer, { unseenOnly: true });
69
89
  // An exact id catch is a targeted fetch and stays repeatable, so it carries no nudge.
70
- const hint = options.id === undefined ? catchHint(caught - marked, caught) : undefined;
71
- return hint === undefined ? selection : { ...selection, hint };
90
+ const hint = options.id === undefined ? catchHint(caught - consumed.newlyConsumed, caught) : undefined;
91
+ return hint === undefined ? observed : { ...observed, hint };
72
92
  }
@@ -1,4 +1,5 @@
1
1
  import { createArtifactPort } from './artifact.js';
2
+ import { decodeHarnessOwner } from './owner.js';
2
3
  import { indexedSquareEntries } from './square-index.js';
3
4
  const holdReasons = new Set(['explicit-setting', 'managed-setting', 'repo-setting', 'invalid-setting', 'bypass-default', 'mode-unknown', 'mode-mismatch', 'no-mode-asserted']);
4
5
  const dropReasons = new Set(['expired', 'refused', 'dropped', 'discarded']);
@@ -13,12 +14,17 @@ export async function recordClaudeDeliveryStatus(input, env = process.env) {
13
14
  || (input.state === 'held' ? !holdReasons.has(input.reason ?? '')
14
15
  : input.state === 'dropped' ? !dropReasons.has(input.reason ?? '') : input.reason !== undefined))
15
16
  return false;
16
- for (const entry of await indexedSquareEntries({ channel: 'claude-code', session: input.sessionId }, env)) {
17
- const artifact = createArtifactPort(entry.location);
17
+ const paths = new Set((await indexedSquareEntries({}, env)).filter(entry => {
18
+ const coordinate = decodeHarnessOwner(entry.owner);
19
+ return coordinate?.channel === 'claude-code' && coordinate.session === input.sessionId;
20
+ }).map(entry => entry.location));
21
+ for (const location of paths) {
22
+ const artifact = createArtifactPort(location);
18
23
  try {
19
24
  const { value: match } = await artifact.read((facts) => facts.roster().flatMap((participant) => {
20
25
  const seat = facts.seat(participant.name);
21
- if (seat === undefined || seat.body.session !== input.sessionId || seat.body.channel !== 'claude-code')
26
+ if (seat === undefined || seat.attachment !== 'attached' || seat.wakeAddress?.kind !== 'claude-native'
27
+ || seat.wakeAddress.address.sessionId !== input.sessionId)
22
28
  return [];
23
29
  const evidence = facts.deliveryEvidence(seat.participant, seat.generation).find((row) => row.correlation?.harness === 'claude' && row.correlation.messageId === input.messageId && row.claimToken !== undefined);
24
30
  return evidence === undefined ? [] : [{ seat, evidence }];
@@ -26,7 +32,7 @@ export async function recordClaudeDeliveryStatus(input, env = process.env) {
26
32
  if (match.length === 0)
27
33
  continue;
28
34
  const { seat, evidence } = match[0];
29
- const ref = { participant: seat.participant, body: seat.body, generation: seat.generation };
35
+ const ref = { participant: seat.participant, owner: seat.owner, generation: seat.generation };
30
36
  return (await artifact.transactAs({ kind: 'participant', seat: ref }, (facts) => facts.observeNativeDelivery({ seat: ref, claimToken: evidence.claimToken, observation: {
31
37
  eventId: input.eventId, messageId: input.messageId, state: input.state,
32
38
  ...(input.reason === undefined ? {} : { reason: input.reason }), observedAt: input.observedAt,
@@ -2,9 +2,10 @@ import { stat } from 'node:fs/promises';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { createArtifactPort } from './artifact.js';
4
4
  import { presentPending } from './presentation-operations.js';
5
- import { projectSessionCandidates, nativePendingPreview } from './inbox.js';
6
- import { sameBody } from './model.js';
5
+ import { nativePendingPreview } from './inbox.js';
7
6
  import { parseActivityId } from './square-core.js';
7
+ import { decodeHarnessOwner } from './owner.js';
8
+ import { indexedSquareEntries } from './square-index.js';
8
9
  import { asSeatRef } from './seats.js';
9
10
  import { writeClaudeNative } from './packages/agent-delivery/src/claude-native.js';
10
11
  const marker = /(?:^|\n)\[square-inbox:([a-zA-Z0-9-]+)\](?:\n|$)/g;
@@ -22,8 +23,9 @@ function previewFacts(facts, request) {
22
23
  const index = parseActivityId(request.activity);
23
24
  if (index === undefined || request.route.kind !== 'claude-native' || request.route.address.endpoint === undefined)
24
25
  return undefined;
25
- return nativePendingPreview(facts, { location: request.location, routeKind: 'claude-native', sessionId: request.route.sessionId,
26
- participant: request.participant, actIndex: index, address: { endpoint: request.route.address.endpoint }, now: Date.now() });
26
+ return nativePendingPreview(facts, { location: request.location, routeKind: 'claude-native', generation: request.seat.generation,
27
+ address: request.route.address,
28
+ participant: request.participant, actIndex: index, now: Date.now() });
27
29
  }
28
30
  async function preview(artifact, request) {
29
31
  return (await artifact.read((facts) => previewFacts(facts, request))).value;
@@ -31,7 +33,7 @@ async function preview(artifact, request) {
31
33
  async function writeClaudeWake(sessionId, endpoint, messageId, payload, timeoutMs, signal) {
32
34
  const result = await writeClaudeNative({ sessionId, endpoint, messageId }, payload, { deadline: Date.now() + timeoutMs, signal });
33
35
  if (result.outcome === 'unavailable')
34
- return { outcome: 'failed', unavailable: true, retainRoute: true, signature: 'claude_endpoint_unavailable', message: result.message };
36
+ return { outcome: 'failed', signature: 'claude_endpoint_unavailable', message: result.message };
35
37
  return { outcome: 'unknown', signature: 'native_admission_unconfirmed', message: result.outcome === 'written' ? 'Claude accepted the local write; native admission is unconfirmed.' : result.message };
36
38
  }
37
39
  export function createClaudeWakeTransport(env = process.env) {
@@ -69,13 +71,15 @@ async function dispatchClaude(request, timeoutMs, beforeSend, env = process.env)
69
71
  return { outcome: 'not-capable', diagnostic: 'Claude activity changed before native write.' };
70
72
  if (signal.aborted || !await beforeSend())
71
73
  return { outcome: 'not-capable', diagnostic: 'Claude activity was consumed before native write.' };
72
- return writeClaudeWake(request.route.sessionId, request.route.address.endpoint, messageId, payload, timeoutMs, signal);
74
+ const sessionId = request.route.address.sessionId;
75
+ if (sessionId === undefined)
76
+ return { outcome: 'not-capable', diagnostic: 'Claude native route has no session address.' };
77
+ return writeClaudeWake(sessionId, request.route.address.endpoint, messageId, payload, timeoutMs, signal);
73
78
  }
74
79
  function currentPayload(facts, request) {
75
80
  const seat = facts.seat(request.participant), evidence = facts.deliveryByToken(request.claimToken);
76
- if (seat === undefined || evidence === undefined || seat.generation !== evidence.generation
77
- || seat.generation !== request.seat.generation || !sameBody(seat.body, request.seat.body)
78
- || seat.body.channel !== 'claude-code' || seat.body.session !== request.route.sessionId
81
+ if (seat === undefined || seat.attachment !== 'attached' || evidence === undefined || seat.generation !== evidence.generation
82
+ || seat.generation !== request.seat.generation || seat.owner !== request.seat.owner
79
83
  || evidence.activity !== request.activity || !['dispatching', 'unknown', 'accepted'].includes(evidence.outcome)
80
84
  || seat.wakeAddress?.kind !== 'claude-native' || !sameAddress(seat.wakeAddress.address, request.route.address))
81
85
  return undefined;
@@ -87,7 +91,11 @@ function currentPayload(facts, request) {
87
91
  }
88
92
  async function rowsForSession(sessionId, env, tokens) {
89
93
  const rows = [];
90
- const paths = new Set((await projectSessionCandidates({ sessionId, channel: 'claude-code', env })).map(candidate => candidate.location));
94
+ const candidates = (await indexedSquareEntries({}, env)).filter(entry => {
95
+ const coordinate = decodeHarnessOwner(entry.owner);
96
+ return coordinate?.channel === 'claude-code' && coordinate.session === sessionId;
97
+ });
98
+ const paths = new Set(candidates.map(candidate => candidate.location));
91
99
  for (const location of paths) {
92
100
  const artifact = createArtifactPort(location);
93
101
  try {
@@ -101,7 +109,7 @@ async function rowsForSession(sessionId, env, tokens) {
101
109
  const item = index === undefined ? undefined : facts.activity(index);
102
110
  if (seat === undefined || item?.kind !== 'say')
103
111
  return [row];
104
- const route = { location, participant: seat.participant, sessionId, channel: 'claude-code',
112
+ const route = { location, participant: seat.participant,
105
113
  ...evidence.route, updatedAt: evidence.at };
106
114
  const request = { location, participant: seat.participant, activity: evidence.activity,
107
115
  actor: item.actor, seat: asSeatRef(seat), claimToken: token, attemptN: evidence.attemptN, route, artifact };
@@ -7,11 +7,13 @@ import { processActNotificationsOnce } from './notifications.js';
7
7
  import { sweepPending } from './delivery-operations.js';
8
8
  import { beginClaudeMcpCall, endClaudeMcpCall } from './claude-mcp-context.js';
9
9
  import { asSeatRef } from './seats.js';
10
- import { projectSessionSeats, projectSessionCandidates } from './inbox.js';
11
- import { InternalSquareError, SquareError, sameBody } from './model.js';
10
+ import { projectSessionSeats } from './inbox.js';
11
+ import { restoreSessionSeats } from './session-lifecycle.js';
12
+ import { harnessOwner } from './owner.js';
13
+ import { InternalSquareError, SquareError } from './model.js';
12
14
  /** Persisted hook state keeps its wire coordinate; internal consumers keep the located seat. */
13
- function claudeBinding({ location, seat }) {
14
- return { location, participant: seat.participant, session: seat.body.session, generation: seat.generation };
15
+ function claudeBinding({ location, seat }, sessionId) {
16
+ return { location, participant: seat.participant, session: sessionId, generation: seat.generation };
15
17
  }
16
18
  /** JSON stdin bridge. Identity is explicit per operation; process.env is never rewritten. */
17
19
  export async function runClaudeMod(inputText, env = process.env) {
@@ -33,29 +35,29 @@ export async function runClaudeMod(inputText, env = process.env) {
33
35
  }
34
36
  const scoped = operationEnv('claude', input.sessionId, env);
35
37
  const signal = AbortSignal.timeout(2500);
36
- const body = { channel: 'claude-code', session: input.sessionId, ...(input.agentId === undefined ? {} : { agent: input.agentId }) };
38
+ const owner = harnessOwner({ channel: 'claude-code', session: input.sessionId, ...(input.agentId === undefined ? {} : { agent: input.agentId }) });
37
39
  const supported = process.platform === 'darwin' && typeof input.endpoint === 'string' && input.endpoint.startsWith('/');
38
- const sessionSeats = () => projectSessionSeats({ body, env }, signal);
40
+ const sessionSeats = async () => (await projectSessionSeats({ owner, env }, signal)).filter(member => member.seat.attachment === 'attached');
39
41
  if (input.operation === 'end') {
40
42
  const retired = [];
41
43
  const captured = [...input.bindings ?? []];
42
44
  // Discover committed late joins at the pinned end boundary, never later owners.
43
45
  if (input.endedAt !== undefined)
44
- for (const member of await sessionSeats()) {
46
+ for (const member of await projectSessionSeats({ owner, env }, signal)) {
45
47
  if (member.joinedAt > input.endedAt || captured.some((item) => item.location === member.location
46
48
  && item.participant === member.seat.participant && item.generation === member.seat.generation))
47
49
  continue;
48
- captured.push(claudeBinding(member));
50
+ captured.push(claudeBinding(member, input.sessionId));
49
51
  }
50
52
  for (const binding of captured) {
51
53
  if (binding.session !== input.sessionId)
52
54
  continue;
53
- const square = await Square.at({ path: binding.location, env: scoped, body,
54
- participant: binding.participant });
55
+ const square = await Square.at({ path: binding.location, env: scoped });
55
56
  try {
56
- const ended = await square.endOwnedSession(binding.participant, input.sessionId, binding.generation, body);
57
- if (ended?.activity)
58
- retired.push({ ...binding, generation: binding.generation });
57
+ const detached = binding.generation === undefined ? null
58
+ : await square.detach(binding.participant, { owner, generation: binding.generation });
59
+ if (detached)
60
+ retired.push(binding);
59
61
  }
60
62
  finally {
61
63
  await square.close();
@@ -67,41 +69,10 @@ export async function runClaudeMod(inputText, env = process.env) {
67
69
  }
68
70
  const recovery = new Set();
69
71
  const cancelledBindings = [...input.cancelledBindings ?? []];
70
- if (input.operation === 'start' && input.resume) {
71
- // Resume is proven by the artifact's latest participant lifecycle, while HOME
72
- // contributes only candidate paths. A stale candidate never establishes a seat.
73
- const candidates = await projectSessionCandidates({ body, env });
74
- const seen = new Set();
75
- for (const candidate of candidates) {
76
- if (signal.aborted)
77
- break;
78
- const probe = createArtifactPort(candidate.location, undefined, signal);
79
- let names;
80
- try {
81
- names = (await probe.read(facts => facts.completedSeatsForBody(body).map(seat => seat.participant), signal)).value;
82
- }
83
- catch (error) {
84
- if (error instanceof InternalSquareError && error.code === 'not_found')
85
- throw new SquareError('unavailable', `Square is unavailable at ${candidate.location}`);
86
- throw error;
87
- }
88
- finally {
89
- await probe.close();
90
- }
91
- for (const participant of names) {
92
- const key = `${candidate.location}\0${participant}`;
93
- if (seen.has(key))
94
- continue;
95
- seen.add(key);
96
- const square = await Square.at({ path: candidate.location, env: scoped, body, participant });
97
- try {
98
- await square.join(participant);
99
- }
100
- finally {
101
- await square.close();
102
- }
103
- }
104
- }
72
+ if (input.operation === 'start') {
73
+ await restoreSessionSeats(owner, scoped, supported ? {
74
+ kind: 'claude-native', address: { sessionId: input.sessionId, endpoint: input.endpoint, version: input.version, platform: 'darwin', loaded: 'true' },
75
+ } : undefined, signal);
105
76
  }
106
77
  if (input.operation === 'start' || input.operation === 'reconcile' || input.operation === 'cancel' || input.cancelAt !== undefined) {
107
78
  for (const member of await sessionSeats()) {
@@ -109,21 +80,21 @@ export async function runClaudeMod(inputText, env = process.env) {
109
80
  break;
110
81
  const { location, seat: capturedSeat, joinedAt } = member;
111
82
  const matches = (item) => item !== undefined
112
- && item.location === location && item.participant === capturedSeat.participant && item.session === capturedSeat.body.session
83
+ && item.location === location && item.participant === capturedSeat.participant && item.session === input.sessionId
113
84
  && (item.generation === undefined || item.generation === capturedSeat.generation);
114
85
  const expected = input.bindings?.find(matches);
115
86
  const cancelled = input.cancelledBindings?.find(matches) ?? (input.operation === 'cancel' ? expected : undefined);
116
87
  const lateCancelledJoin = input.operation === 'cancel' && input.cancelAt !== undefined
117
88
  && joinedAt <= input.cancelAt && expected === undefined;
118
89
  if (lateCancelledJoin && !cancelledBindings.some(matches))
119
- cancelledBindings.push(claudeBinding(member));
90
+ cancelledBindings.push(claudeBinding(member, input.sessionId));
120
91
  if (input.operation !== 'start' && input.operation !== 'reconcile' && !lateCancelledJoin && expected === undefined)
121
92
  continue;
122
93
  const artifact = createArtifactPort(location, undefined, signal);
123
94
  try {
124
95
  const { value: currentSeat } = await artifact.read((facts) => facts.seat(capturedSeat.participant), signal);
125
- if (currentSeat === undefined || currentSeat.generation !== capturedSeat.generation
126
- || !sameBody(currentSeat.body, capturedSeat.body))
96
+ if (currentSeat === undefined || currentSeat.attachment !== 'attached' || currentSeat.generation !== capturedSeat.generation
97
+ || currentSeat.owner !== capturedSeat.owner)
127
98
  continue;
128
99
  const seat = asSeatRef(currentSeat);
129
100
  const existing = currentSeat.wakeAddress?.kind === 'claude-native' ? currentSeat.wakeAddress : undefined;
@@ -193,7 +164,7 @@ export async function runClaudeMod(inputText, env = process.env) {
193
164
  await processActNotificationsOnce(location, index, { env: { ...env, SQUARE_NOTIFY_DELIVERY_WAIT_MS: '200' } });
194
165
  }
195
166
  }
196
- return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings: members.map(claudeBinding), cancelledBindings })}\n`;
167
+ return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings: members.map(member => claudeBinding(member, input.sessionId)), cancelledBindings })}\n`;
197
168
  }
198
169
  if (!['guard', 'admitted', 'stored'].includes(input.operation) || typeof input.text !== 'string')
199
170
  throw new Error('Unknown Claude bridge operation.');