@astrosheep/square 0.3.105 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/activity-feed.js +1 -1
  2. package/dist/artifact-store.d.ts +41 -0
  3. package/dist/artifact-store.js +272 -0
  4. package/dist/artifact.d.ts +8 -12
  5. package/dist/artifact.js +66 -343
  6. package/dist/attention-presentation.d.ts +33 -7
  7. package/dist/attention-presentation.js +33 -69
  8. package/dist/boundary-presentation.d.ts +0 -2
  9. package/dist/boundary-presentation.js +17 -22
  10. package/dist/catch-decisions.d.ts +2 -2
  11. package/dist/catch-decisions.js +14 -4
  12. package/dist/catch-selection.d.ts +8 -0
  13. package/dist/catch-selection.js +5 -0
  14. package/dist/channel-artifact.d.ts +10 -0
  15. package/dist/channel-artifact.js +55 -0
  16. package/dist/channel-delivery.d.ts +11 -0
  17. package/dist/channel-delivery.js +51 -0
  18. package/dist/channel-domain.d.ts +3 -0
  19. package/dist/channel-domain.js +83 -0
  20. package/dist/channel-facade.d.ts +76 -0
  21. package/dist/channel-facade.js +1 -0
  22. package/dist/channel-internal.d.ts +126 -0
  23. package/dist/channel-internal.js +440 -0
  24. package/dist/channel-metadata.d.ts +11 -0
  25. package/dist/channel-metadata.js +1 -0
  26. package/dist/channel-ports.d.ts +241 -0
  27. package/dist/channel-ports.js +1 -0
  28. package/dist/channel-presentation.d.ts +17 -0
  29. package/dist/channel-presentation.js +22 -0
  30. package/dist/channel-sqlite-schema.d.ts +13 -0
  31. package/dist/channel-sqlite-schema.js +81 -0
  32. package/dist/channel-transport.d.ts +62 -0
  33. package/dist/channel-transport.js +1 -0
  34. package/dist/channel-values.d.ts +27 -0
  35. package/dist/channel-values.js +90 -0
  36. package/dist/channel.d.ts +31 -21
  37. package/dist/channel.js +42 -74
  38. package/dist/claude-delivery-status.js +1 -1
  39. package/dist/claude-delivery.d.ts +2 -5
  40. package/dist/claude-delivery.js +56 -103
  41. package/dist/claude-mod.js +4 -4
  42. package/dist/claude-transport.d.ts +5 -0
  43. package/dist/claude-transport.js +58 -0
  44. package/dist/cli/context.d.ts +2 -2
  45. package/dist/cli/context.js +0 -1
  46. package/dist/cli/maintenance-commands.d.ts +0 -4
  47. package/dist/cli/maintenance-commands.js +2 -29
  48. package/dist/cli/program.js +1 -1
  49. package/dist/cli/registry.js +1 -2
  50. package/dist/codex-address.d.ts +1 -1
  51. package/dist/codex-queue.d.ts +2 -2
  52. package/dist/codex-queue.js +1 -3
  53. package/dist/decisions.js +3 -3
  54. package/dist/delivery-deferral.d.ts +9 -18
  55. package/dist/delivery-deferral.js +4 -4
  56. package/dist/delivery-health.d.ts +4 -4
  57. package/dist/delivery-health.js +11 -11
  58. package/dist/delivery-operations.d.ts +15 -2
  59. package/dist/delivery-operations.js +60 -30
  60. package/dist/delivery.d.ts +44 -33
  61. package/dist/delivery.js +49 -75
  62. package/dist/help.js +1 -6
  63. package/dist/http-delivery.d.ts +2 -2
  64. package/dist/http-delivery.js +2 -4
  65. package/dist/inbox.d.ts +10 -18
  66. package/dist/inbox.js +44 -53
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/mcp.d.ts +2 -1
  70. package/dist/mcp.js +40 -70
  71. package/dist/message.d.ts +7 -0
  72. package/dist/message.js +23 -0
  73. package/dist/model.d.ts +41 -77
  74. package/dist/model.js +7 -139
  75. package/dist/neutral.d.ts +72 -0
  76. package/dist/neutral.js +27 -0
  77. package/dist/notifications.d.ts +2 -8
  78. package/dist/notifications.js +15 -50
  79. package/dist/operation-presentation.d.ts +1 -2
  80. package/dist/operation-presentation.js +1 -7
  81. package/dist/owner.d.ts +2 -2
  82. package/dist/owner.js +1 -1
  83. package/dist/paseo-delivery.d.ts +1 -1
  84. package/dist/paseo-delivery.js +2 -4
  85. package/dist/paseo-timeline.js +1 -0
  86. package/dist/plugins/opencode/delivery.js +27 -42
  87. package/dist/ports.d.ts +46 -168
  88. package/dist/presentation-operations.d.ts +1 -14
  89. package/dist/presentation-operations.js +16 -16
  90. package/dist/presentation.d.ts +5 -5
  91. package/dist/presentation.js +1 -0
  92. package/dist/runtime.js +2 -2
  93. package/dist/search.js +1 -1
  94. package/dist/sqlite-channel.d.ts +15 -0
  95. package/dist/sqlite-channel.js +600 -0
  96. package/dist/sqlite-framing.d.ts +8 -0
  97. package/dist/sqlite-framing.js +31 -0
  98. package/dist/sqlite-square.d.ts +12 -13
  99. package/dist/sqlite-square.js +192 -248
  100. package/dist/sqlite-values.d.ts +21 -0
  101. package/dist/sqlite-values.js +78 -0
  102. package/dist/square-api.d.ts +1 -1
  103. package/dist/square-api.js +1 -1
  104. package/dist/square-core.d.ts +1 -1
  105. package/dist/square-domain.d.ts +21 -7
  106. package/dist/square-domain.js +93 -94
  107. package/dist/square-facade.d.ts +12 -66
  108. package/dist/square-index.d.ts +1 -1
  109. package/dist/square-index.js +1 -1
  110. package/dist/square-location.d.ts +6 -1
  111. package/dist/square-location.js +1 -1
  112. package/dist/square-receive.d.ts +2 -3
  113. package/dist/square-sqlite.d.ts +45 -44
  114. package/dist/square-sqlite.js +308 -764
  115. package/dist/square-tools.d.ts +8 -0
  116. package/dist/square-tools.js +39 -0
  117. package/dist/views.d.ts +6 -6
  118. package/dist/views.js +15 -9
  119. package/dist/wake-preview.d.ts +7 -0
  120. package/dist/wake-preview.js +15 -0
  121. package/dist/wake-sink.d.ts +2 -2
  122. package/dist/wake-sink.js +1 -1
  123. package/dist/wake-transports.d.ts +7 -0
  124. package/dist/wake-transports.js +42 -0
  125. package/guides/claude-native.md +13 -10
  126. package/guides/codex-delivery.md +3 -1
  127. package/guides/participant-ownership.md +1 -1
  128. package/guides/row-runtime-delivery-evidence.md +2 -0
  129. package/guides/row-storage-design.md +19 -44
  130. package/guides/square-api.md +137 -60
  131. package/package.json +3 -3
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/types.d.ts +1 -1
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +28 -41
  136. package/plugins/pi/square-pi.js +28 -51
  137. package/dist/square-legacy-framing.d.ts +0 -11
  138. package/dist/square-legacy-framing.js +0 -64
  139. package/dist/square-migration-host-ledger.d.ts +0 -26
  140. package/dist/square-migration-host-ledger.js +0 -169
  141. package/dist/square-migration.d.ts +0 -30
  142. package/dist/square-migration.js +0 -599
  143. package/guides/migration.md +0 -156
@@ -0,0 +1,78 @@
1
+ import { SquareError, nameKey } from './neutral.js';
2
+ export const encode = JSON.stringify;
3
+ export const key = (name) => encode(nameKey(name));
4
+ export function invalidArtifact(detail) {
5
+ return new SquareError('invalid_args', `Invalid square artifact: ${detail}`);
6
+ }
7
+ export function requireFact(ok, detail = 'row is malformed.') {
8
+ if (!ok)
9
+ throw invalidArtifact(detail);
10
+ }
11
+ export function integer(value, minimum = 0) {
12
+ requireFact(Number.isSafeInteger(value) && value >= minimum);
13
+ return value;
14
+ }
15
+ export function finite(value) {
16
+ requireFact(typeof value === 'number' && Number.isFinite(value));
17
+ return value;
18
+ }
19
+ export function string(value, nonempty = false) {
20
+ requireFact(typeof value === 'string' && (!nonempty || value.length > 0));
21
+ return value;
22
+ }
23
+ export function json(value) {
24
+ try {
25
+ return JSON.parse(string(value));
26
+ }
27
+ catch {
28
+ throw invalidArtifact('row contains invalid JSON.');
29
+ }
30
+ }
31
+ export function object(value) {
32
+ requireFact(typeof value === 'object' && value !== null && !Array.isArray(value));
33
+ return value;
34
+ }
35
+ export function shape(value, required, optional = []) {
36
+ const result = object(value);
37
+ requireFact(required.every(k => Object.hasOwn(result, k)) && Object.keys(result).every(k => required.includes(k) || optional.includes(k)));
38
+ return result;
39
+ }
40
+ export function strings(value) {
41
+ requireFact(Array.isArray(value) && value.every(v => typeof v === 'string'));
42
+ return value;
43
+ }
44
+ export function decoded(value, nonempty = false) {
45
+ const result = string(json(value), nonempty);
46
+ requireFact(encode(result) === value, 'string encoding is malformed.');
47
+ return result;
48
+ }
49
+ export function immutable(value) {
50
+ if (value !== null && typeof value === 'object') {
51
+ for (const item of Object.values(value))
52
+ immutable(item);
53
+ Object.freeze(value);
54
+ }
55
+ return value;
56
+ }
57
+ // Sorted object keys express small-value structural equality, never whole-state comparison.
58
+ export function stableJson(value) {
59
+ if (value === null || typeof value !== 'object') {
60
+ requireFact(value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value), 'value contains a non-finite or unsupported JSON value.');
61
+ return encode(value);
62
+ }
63
+ if (Array.isArray(value))
64
+ return '[' + value.map(stableJson).join(',') + ']';
65
+ return '{' + Object.keys(value).sort().map(k => encode(k) + ':' + stableJson(value[k])).join(',') + '}';
66
+ }
67
+ export function pageRows(rows, page, decode) {
68
+ const hasMore = rows.length > page.limit;
69
+ if (hasMore)
70
+ rows.pop();
71
+ if (page.edge === 'newest')
72
+ rows.reverse();
73
+ return immutable({ items: rows.map(decode), hasMore });
74
+ }
75
+ export function pageLimit(page) {
76
+ requireFact(page.edge === 'oldest' || page.edge === 'newest');
77
+ return Math.min(integer(page.limit, 1) + 1, Number.MAX_SAFE_INTEGER);
78
+ }
@@ -16,4 +16,4 @@ export declare function selectOwnSeat(square: Pick<SqliteSquare, 'participant' |
16
16
  }): Promise<Participant | undefined>;
17
17
  export { ACTIVITY_METADATA_MAX_BYTES } from './model.js';
18
18
  export { localUserOwner, harnessOwner, decodeHarnessOwner } from './owner.js';
19
- export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, migrateSquare, discover, inbox } from './sqlite-square.js';
19
+ export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, discover, inbox } from './sqlite-square.js';
@@ -27,4 +27,4 @@ export async function selectOwnSeat(square, owner, name, options) {
27
27
  }
28
28
  export { ACTIVITY_METADATA_MAX_BYTES } from './model.js';
29
29
  export { localUserOwner, harnessOwner, decodeHarnessOwner } from './owner.js';
30
- export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, migrateSquare, discover, inbox } from './sqlite-square.js';
30
+ export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, discover, inbox } from './sqlite-square.js';
@@ -26,10 +26,10 @@ export type Act = {
26
26
  at?: number;
27
27
  body?: string;
28
28
  metadata?: import('./model.js').ActivityMetadata;
29
- delivery?: import('./model.js').ActivityDelivery;
30
29
  mentions?: readonly Participant[];
31
30
  reach?: Reach;
32
31
  reply?: number;
32
+ messageIndex?: number;
33
33
  } | {
34
34
  kind: 'read';
35
35
  actor: Participant;
@@ -1,7 +1,16 @@
1
- import { type Seat, type StoredAct } from './model.js';
1
+ import { type ActDraft, type Seat, type StoredAct } from './model.js';
2
+ import { type EndpointGeneration } from './message.js';
2
3
  import type { ArtifactWriteAuthority, AttentionFact, LandedRecipient, ParticipantFact, SeatRef, SquareRead, SquareWrite } from './ports.js';
4
+ import type { ChannelWrite, EndpointRef } from './channel-ports.js';
5
+ import type { WatchLease } from './model.js';
6
+ /** Pending messages the live catch currently owns; the lower pipeline only needs this coverage. */
7
+ export declare function catchCoverage(read: SquareRead, name: string, lease: WatchLease): number[];
8
+ /** Artifact-private landing effect; sender capabilities never expose another recipient's claim. */
9
+ export declare function extendLandingCatchCoverage(read: SquareRead, putClaim: ChannelWrite['putReceiveClaim'], activity: StoredAct, audience: readonly LandedRecipient[]): void;
3
10
  export interface LandingEffect {
4
- readonly activity: StoredAct;
11
+ readonly activity: ActDraft & {
12
+ readonly index: number;
13
+ };
5
14
  readonly participant?: ParticipantFact;
6
15
  readonly removeListeningOf?: string;
7
16
  readonly removeListening?: {
@@ -14,18 +23,23 @@ export interface LandingEffect {
14
23
  export interface LandingFacts {
15
24
  readonly actor?: ParticipantFact;
16
25
  readonly standing: readonly ParticipantFact[];
26
+ /** Endpoint custody at landing time, so an address fences the generation it was written for. */
27
+ readonly generations: ReadonlyMap<string, EndpointGeneration>;
17
28
  readonly incoming: readonly AttentionFact[];
18
29
  readonly outgoing: readonly AttentionFact[];
19
30
  readonly replyAuthor?: string;
20
31
  }
21
32
  /** The single landing transition, shared by both adapters and explicit rebuild. */
22
- export declare function deriveLanding(activity: StoredAct, facts: LandingFacts): LandingEffect;
23
- export declare function landingFacts(read: SquareRead, activity: StoredAct): LandingFacts;
24
- export interface LandingWriter extends Pick<SquareWrite, 'markSeen' | 'putLease' | 'removeLease' | 'updateWakeAddress' | 'reattachSeat' | 'detachSeat' | 'cancelThrough' | 'putDeliveryDeferral' | 'renewDeliveryDeferral' | 'releaseDeliveryDeferral' | 'claimDelivery' | 'prepareDelivery' | 'completeDelivery' | 'observeNativeDelivery'> {
25
- /** Artifact-private seat persistence used only by this gate's lifecycle transition. */
33
+ export declare function deriveLanding(activity: ActDraft & {
34
+ readonly index: number;
35
+ }, facts: LandingFacts): LandingEffect;
36
+ export declare function landingFacts(read: SquareRead, activity: ActDraft): LandingFacts;
37
+ export interface LandingWriter extends Pick<SquareWrite, 'putLease' | 'removeLease' | 'updateWakeAddress' | 'reattachSeat' | 'detachSeat'> {
38
+ /** Artifact-private endpoint persistence used only by this gate's lifecycle transition. */
26
39
  storeSeat(seat: Seat): boolean;
27
- removeSeat(seat: import('./ports.js').SeatRef): boolean;
40
+ removeSeat(seat: EndpointRef): boolean;
28
41
  applyLanding(effect: LandingEffect): void;
42
+ readonly channel: ChannelWrite;
29
43
  }
30
44
  export interface SquareWriteGate {
31
45
  readonly facts: SquareWrite;
@@ -1,21 +1,51 @@
1
- import { SquareError, formatActivityId, parseActivityId, sameName, validateActivityDelivery, validateOwner, validateName } from './model.js';
1
+ import { SquareError, nameKey, sameName, validateOwner, validateName } from './model.js';
2
+ import { wrapChannelWrite } from './channel-domain.js';
3
+ import { newEndpointGeneration } from './message.js';
2
4
  import { resolveAudience } from './attention.js';
3
- /** The single landing transition, shared by both adapters and explicit rebuild. */
4
- export function deriveLanding(activity, facts) {
5
- if (activity.kind === 'say' && activity.delivery?.mode === 'direct') {
6
- const audience = [];
7
- for (const mention of activity.mentions ?? []) {
8
- const recipient = facts.standing.find(item => sameName(item.name, mention))?.name;
9
- if (recipient !== undefined && !audience.some(item => sameName(item.recipient, recipient)))
10
- audience.push({ recipient, route: 'mention' });
5
+ import { validateEndpointGeneration as endpointGeneration } from './message.js';
6
+ import { leaseOwnsNotification } from './catch-selection.js';
7
+ /** Pending messages the live catch currently owns; the lower pipeline only needs this coverage. */
8
+ export function catchCoverage(read, name, lease) {
9
+ const covered = [];
10
+ let cursor = -1;
11
+ for (;;) {
12
+ const page = read.channel.directed(name, { afterIndex: cursor, unseenOnly: true, currentGeneration: true }, { edge: 'oldest', limit: 100 });
13
+ for (const head of page.items) {
14
+ const relation = read.activityByMessage(head.index);
15
+ const route = relation === undefined ? undefined : read.directedAt(name, relation.index)?.route;
16
+ if (route !== undefined && leaseOwnsNotification(lease, { actor: head.from, route, recipient: name }))
17
+ covered.push(head.index);
11
18
  }
12
- return { activity, audience };
19
+ if (!page.hasMore || page.items.length === 0)
20
+ break;
21
+ cursor = page.items[page.items.length - 1].index;
22
+ }
23
+ return covered;
24
+ }
25
+ /** Artifact-private landing effect; sender capabilities never expose another recipient's claim. */
26
+ export function extendLandingCatchCoverage(read, putClaim, activity, audience) {
27
+ if (activity.kind !== 'say')
28
+ return;
29
+ const index = activity.messageIndex;
30
+ for (const recipient of audience) {
31
+ const claim = read.channel.claimLease(recipient.recipient, 'agora');
32
+ if (claim === undefined || claim.expiresAt <= activity.at || recipient.generation !== undefined && claim.generation !== recipient.generation)
33
+ continue;
34
+ const lease = read.lease(recipient.recipient);
35
+ if (lease === undefined || lease.leaseId !== claim.id
36
+ || !leaseOwnsNotification(lease, { actor: activity.actor, route: recipient.route, recipient: recipient.recipient }))
37
+ continue;
38
+ if (!claim.suppressed.includes(index))
39
+ putClaim({ ...claim, suppressed: [...claim.suppressed, index] });
13
40
  }
41
+ }
42
+ /** The single landing transition, shared by both adapters and explicit rebuild. */
43
+ export function deriveLanding(activity, facts) {
14
44
  const audience = activity.kind === 'say' ? resolveAudience({
15
45
  actor: activity.actor, recipients: facts.standing.map((item) => item.name),
16
46
  mentions: activity.mentions, replyAuthor: facts.replyAuthor, reach: activity.reach,
17
47
  incoming: facts.incoming, sameIdentity: sameName,
18
- }) : [];
48
+ }).map(recipient => ({ ...recipient, generation: facts.generations.get(nameKey(recipient.recipient)) })) : [];
19
49
  const actor = activity.actor;
20
50
  const previous = facts.actor ?? { name: actor, firstIndex: activity.index, joined: false, done: false, sayCount: 0 };
21
51
  const participant = {
@@ -45,6 +75,7 @@ export function deriveLanding(activity, facts) {
45
75
  export function landingFacts(read, activity) {
46
76
  return {
47
77
  actor: read.participant(activity.actor),
78
+ generations: new Map(read.channel.endpoints().map(endpoint => [nameKey(endpoint.participant), endpoint.generation])),
48
79
  standing: activity.kind === 'say' ? read.roster().filter((item) => item.joined) : [],
49
80
  incoming: activity.kind === 'say' ? read.incomingAttention(activity.actor) : [],
50
81
  outgoing: activity.kind === 'listen' || activity.kind === 'ignore' || activity.kind === 'unlisten' ? read.attention(activity.actor) : [],
@@ -52,8 +83,7 @@ export function landingFacts(read, activity) {
52
83
  };
53
84
  }
54
85
  function validateGeneration(value) {
55
- if (typeof value !== 'string' || parseActivityId(value) === undefined)
56
- throw new SquareError('invalid_args', 'Artifact authority requires a valid join generation.');
86
+ endpointGeneration(value);
57
87
  }
58
88
  /** Read probes and writes share the same captured-coordinate and attachment fence. */
59
89
  export function assertCurrentSeat(read, expected, allowDetached = false) {
@@ -63,14 +93,21 @@ export function assertCurrentSeat(read, expected, allowDetached = false) {
63
93
  if (!allowDetached && current.attachment !== 'attached')
64
94
  throw new SquareError('already_joined', 'This participant is away; join again before writing.', { reason: 'needs_seat', activityUnsent: true });
65
95
  }
96
+ function immutableAct(activity) {
97
+ const copy = { ...activity, ...(activity.kind === 'say' && activity.mentions !== undefined ? { mentions: [...activity.mentions] } : {}) };
98
+ if (copy.kind === 'say' && copy.mentions !== undefined)
99
+ Object.freeze(copy.mentions);
100
+ return Object.freeze(copy);
101
+ }
66
102
  /** The artifact transaction supplies one captured authority; every mutation checks it here. */
67
103
  export function wrapSquareWrite(read, writes, authority) {
68
104
  if (authority === undefined || typeof authority !== 'object' || authority === null)
69
105
  throw new SquareError('invalid_args', 'Artifact writes require an explicit authority.');
70
- const { applyLanding, ...mutations } = writes;
71
- const seated = authority.kind === 'participant' || authority.kind === 'host-cleanup';
72
- const expectedName = seated ? authority.seat?.participant : authority.kind === 'send' ? authority.actor : authority.participant;
73
- if (!['participant', 'host-cleanup', 'join', 'takeover', 'send'].includes(authority.kind) || typeof expectedName !== 'string')
106
+ const { applyLanding, channel, ...mutations } = writes;
107
+ const seated = authority.kind === 'participant' || authority.kind === 'host-cleanup' || authority.kind === 'endpoint-cleanup';
108
+ const expectedName = seated ? authority.seat?.participant : authority.kind === 'send' ? authority.actor
109
+ : authority.kind === 'endpoint' ? authority.participant : authority.participant;
110
+ if (!['participant', 'host-cleanup', 'endpoint-cleanup', 'endpoint', 'join', 'takeover', 'send'].includes(authority.kind) || typeof expectedName !== 'string')
74
111
  throw new SquareError('invalid_args', 'Artifact write authority is malformed.');
75
112
  validateName(expectedName);
76
113
  if (seated) {
@@ -82,7 +119,7 @@ export function wrapSquareWrite(read, writes, authority) {
82
119
  if (authority.kind === 'takeover')
83
120
  validateGeneration(authority.expectedGeneration);
84
121
  }
85
- const assertSeat = (seat, expected = authority.kind === 'participant' ? authority.seat : undefined, allowDetached = false) => {
122
+ const assertSeat = (seat, expected = authority.kind === 'participant' || authority.kind === 'host-cleanup' || authority.kind === 'endpoint-cleanup' ? authority.seat : undefined, allowDetached = false) => {
86
123
  if (expected === undefined || !sameName(expected.participant, seat.participant)
87
124
  || seat.generation !== expected.generation || seat.owner !== expected.owner)
88
125
  throw new SquareError('already_joined', 'This write needs the current participant seat.');
@@ -91,16 +128,20 @@ export function wrapSquareWrite(read, writes, authority) {
91
128
  const assertParticipant = (name, generation) => {
92
129
  if (authority.kind !== 'participant')
93
130
  throw new SquareError('already_joined', 'This write needs the current participant seat.');
94
- assertSeat({ ...authority.seat, participant: name, generation: generation === undefined ? authority.seat.generation : generation });
131
+ assertSeat({ ...authority.seat, participant: name, generation: generation ?? authority.seat.generation });
95
132
  };
96
133
  let reconnectSeat;
97
134
  let takeoverPhase = authority.kind === 'takeover' ? 'ready' : undefined;
135
+ let establishedSeat;
98
136
  if (authority.kind === 'participant') {
99
137
  assertSeat(authority.seat);
100
138
  }
101
- else if (authority.kind === 'host-cleanup') {
139
+ else if (authority.kind === 'host-cleanup' || authority.kind === 'endpoint-cleanup') {
102
140
  // A late host cleanup is allowed to inspect a stale coordinate and return a no-op.
103
141
  }
142
+ else if (authority.kind === 'endpoint') {
143
+ // Endpoint attach/takeover owns no activity.
144
+ }
104
145
  else if (authority.kind === 'join') {
105
146
  const participant = read.participant(expectedName), seat = read.seat(expectedName);
106
147
  if (participant?.joined) {
@@ -115,7 +156,12 @@ export function wrapSquareWrite(read, writes, authority) {
115
156
  throw new SquareError('already_joined', 'The participant seat changed before takeover.');
116
157
  }
117
158
  let lastActivity;
159
+ const lowerAuthority = authority.kind === 'join' || authority.kind === 'takeover'
160
+ ? { kind: 'endpoint', participant: authority.participant, owner: authority.owner }
161
+ : authority.kind === 'host-cleanup' ? { kind: 'endpoint-cleanup', seat: authority.seat } : authority;
162
+ const gatedChannel = wrapChannelWrite(read.channel, channel, lowerAuthority);
118
163
  const facts = Object.assign({}, read, {
164
+ channel: gatedChannel,
119
165
  land(activity) {
120
166
  const actor = 'actor' in activity ? activity.actor : undefined;
121
167
  if (actor === undefined)
@@ -123,11 +169,8 @@ export function wrapSquareWrite(read, writes, authority) {
123
169
  validateName(actor);
124
170
  if (!sameName(actor, expectedName))
125
171
  throw new SquareError('already_joined', 'Activity actor does not match the artifact authority.');
126
- if (activity.kind === 'say')
127
- validateActivityDelivery(activity.delivery);
128
- if (authority.kind === 'send') {
129
- if (activity.kind !== 'say' || activity.delivery?.mode !== 'direct' || activity.reach !== undefined || activity.reply !== undefined)
130
- throw new SquareError('invalid_args', 'Send authority may only land direct messages without bell or reply.');
172
+ if (authority.kind === 'send' || authority.kind === 'endpoint' || authority.kind === 'endpoint-cleanup') {
173
+ throw new SquareError('invalid_args', 'This authority may not publish Agora activities.');
131
174
  }
132
175
  else if (authority.kind === 'participant') {
133
176
  assertSeat(authority.seat);
@@ -160,37 +203,50 @@ export function wrapSquareWrite(read, writes, authority) {
160
203
  const index = read.header().nextActIndex;
161
204
  if (!Number.isSafeInteger(index) || index < 0 || index >= Number.MAX_SAFE_INTEGER)
162
205
  throw new SquareError('invalid_args', 'Square activity index is exhausted.');
163
- const captured = authority.kind === 'takeover' && activity.kind === 'done'
164
- ? read.seat(expectedName) : undefined;
165
- const stored = { ...activity, index };
166
- applyLanding(deriveLanding(stored, landingFacts(read, stored)));
206
+ const captured = authority.kind === 'takeover' && activity.kind === 'done' ? read.seat(expectedName) : undefined;
207
+ const effect = deriveLanding({ ...activity, index }, landingFacts(read, activity));
208
+ let stored;
209
+ if (activity.kind === 'say') {
210
+ const published = gatedChannel.publishMessage({ from: activity.actor, at: activity.at,
211
+ body: activity.body ?? '', ...(activity.metadata === undefined ? {} : { metadata: activity.metadata }),
212
+ to: effect.audience.map(({ recipient, generation }) => ({ recipient, ...(generation === undefined ? {} : { generation }) })) });
213
+ // The published content lives in the message; only the relation is stored above it.
214
+ const { body: _body, metadata: _metadata, ...relation } = activity;
215
+ stored = { ...relation, index, messageIndex: published.index };
216
+ }
217
+ else {
218
+ stored = { ...activity, index };
219
+ }
220
+ applyLanding({ ...effect, activity: stored });
167
221
  if (stored.kind === 'join') {
168
222
  if (authority.kind !== 'join' && authority.kind !== 'takeover')
169
223
  throw new SquareError('invalid_args', 'Seat authority cannot establish another seat.');
170
224
  const seat = {
171
225
  participant: stored.actor,
172
226
  owner: authority.owner,
173
- generation: formatActivityId(stored.index),
227
+ generation: newEndpointGeneration(),
174
228
  attachment: 'attached',
175
229
  ...(authority.wakeAddress === undefined ? {} : { wakeAddress: authority.wakeAddress }),
176
230
  };
177
231
  mutations.storeSeat(seat);
232
+ establishedSeat = seat;
178
233
  const established = read.seat(seat.participant);
179
234
  if (established === undefined || established.owner !== seat.owner || established.generation !== seat.generation)
180
- throw new SquareError('invalid_args', 'The join activity did not establish its artifact seat.');
235
+ throw new SquareError('invalid_args', 'The join activity did not establish its artifact endpoint.');
181
236
  if (authority.kind === 'takeover')
182
237
  takeoverPhase = 'joined';
183
238
  }
184
239
  else if (stored.kind === 'done' && seated) {
185
240
  if (!mutations.removeSeat(authority.seat))
186
- throw new SquareError('already_joined', 'The participant seat changed while it was being completed.');
241
+ throw new SquareError('already_joined', 'The participant endpoint changed while it was being completed.');
187
242
  }
188
243
  else if (stored.kind === 'done' && authority.kind === 'takeover') {
189
244
  if (captured === undefined || !mutations.removeSeat({ participant: expectedName, owner: captured.owner, generation: captured.generation }))
190
- throw new SquareError('already_joined', 'The participant seat changed while takeover was completing it.');
245
+ throw new SquareError('already_joined', 'The participant endpoint changed while takeover was completing it.');
191
246
  takeoverPhase = 'done';
192
247
  }
193
- lastActivity = read.activity(index);
248
+ // The persisted fact is the relation; callers that present a say resolve its message content.
249
+ lastActivity = immutableAct(stored);
194
250
  return lastActivity;
195
251
  },
196
252
  updateWakeAddress(seat, wakeAddress) {
@@ -216,71 +272,14 @@ export function wrapSquareWrite(read, writes, authority) {
216
272
  return false;
217
273
  return mutations.detachSeat(seat);
218
274
  },
219
- cancelThrough(seat, through) {
220
- assertSeat(seat);
221
- if (!Number.isSafeInteger(through) || through < 0 || through >= read.header().nextActIndex)
222
- throw new SquareError('invalid_args', 'Cancellation boundary must be an assigned activity index below the allocator.');
223
- return mutations.cancelThrough(seat, through);
224
- },
225
- markSeen(name, indexes, at) {
226
- assertParticipant(name);
227
- return mutations.markSeen(name, indexes, at);
228
- },
229
275
  putLease(name, lease, generation) {
230
276
  assertParticipant(name, generation);
231
- return mutations.putLease(name, lease, generation);
277
+ return mutations.putLease(name, lease, generation, catchCoverage(read, name, lease));
232
278
  },
233
279
  removeLease(name, leaseId, generation) {
234
280
  assertParticipant(name, generation);
235
281
  return mutations.removeLease(name, leaseId, generation);
236
282
  },
237
- putDeliveryDeferral(...args) {
238
- assertSeat(args[0]);
239
- return mutations.putDeliveryDeferral(...args);
240
- },
241
- renewDeliveryDeferral(...args) {
242
- assertSeat(args[0]);
243
- return mutations.renewDeliveryDeferral(...args);
244
- },
245
- releaseDeliveryDeferral(...args) {
246
- assertSeat(args[0]);
247
- return mutations.releaseDeliveryDeferral(...args);
248
- },
249
- claimDelivery(input) {
250
- assertSeat(input.seat);
251
- const evidence = read.deliveryEvidence(input.seat.participant, input.seat.generation, input.activity);
252
- if (input.observationOf !== undefined) {
253
- // Receiver adapters validate native identity/content before entering this transaction.
254
- // This path records that existing message, rather than acquiring a second send.
255
- if (input.kind !== 'presentation' || !evidence.some(row => row.kind === 'wake'
256
- && row.claimToken === input.observationOf && ['dispatching', 'accepted', 'unknown'].includes(row.outcome)))
257
- return { status: 'stale' };
258
- }
259
- else {
260
- const blocker = evidence.find(row => row.kind !== input.kind
261
- && row.outcome !== 'failed' && row.outcome !== 'released');
262
- if (blocker !== undefined) {
263
- if (blocker.outcome === 'dispatching' && blocker.expiresAt !== undefined && blocker.expiresAt <= input.at) {
264
- // Normalize abandoned custody to unknown in the same transaction, never steal it.
265
- return mutations.claimDelivery({ ...input, kind: blocker.kind });
266
- }
267
- return { status: blocker.outcome === 'dispatching' ? 'busy' : 'terminal', evidence: blocker };
268
- }
269
- }
270
- return mutations.claimDelivery(input);
271
- },
272
- prepareDelivery(input) {
273
- assertSeat(input.seat);
274
- return mutations.prepareDelivery(input);
275
- },
276
- completeDelivery(input) {
277
- assertSeat(input.seat);
278
- return mutations.completeDelivery(input);
279
- },
280
- observeNativeDelivery(input) {
281
- assertSeat(input.seat);
282
- return mutations.observeNativeDelivery(input);
283
- },
284
283
  });
285
284
  return {
286
285
  facts,
@@ -288,8 +287,8 @@ export function wrapSquareWrite(read, writes, authority) {
288
287
  if (authority.kind !== 'takeover')
289
288
  return;
290
289
  const seat = read.seat(expectedName);
291
- if (takeoverPhase !== 'joined' || lastActivity?.kind !== 'join' || seat === undefined
292
- || seat.owner !== authority.owner || seat.generation !== formatActivityId(lastActivity.index))
290
+ if (takeoverPhase !== 'joined' || lastActivity?.kind !== 'join' || seat === undefined || establishedSeat === undefined
291
+ || seat.owner !== establishedSeat.owner || seat.generation !== establishedSeat.generation)
293
292
  throw new SquareError('already_joined', 'Takeover did not complete its replacement join.');
294
293
  },
295
294
  };
@@ -1,12 +1,17 @@
1
1
  import type { DeliveryDeferral, MetadataMatch } from './delivery-deferral.js';
2
2
  export type { DeliveryDeferral, MetadataMatch, MetadataScalar } from './delivery-deferral.js';
3
3
  import type { ActivityId, Perception, Reach } from './square-core.js';
4
- import type { Owner, StoredAct, WakeAddress, ActivityMetadata, ActivityDelivery, DirectedNotificationRoute } from './model.js';
4
+ import type { Owner, HydratedAct, WakeAddress, ActivityMetadata, DirectedNotificationRoute } from './model.js';
5
5
  import type { WakeTransportPort, SquareResourceOwner } from './ports.js';
6
+ import type { OperationControl } from './neutral.js';
7
+ import type { MessageSearch as ActivitySearch, AckResult } from './channel-facade.js';
8
+ export type { ChannelTruncate, ChannelSendInput, ChannelSendResult, ChannelEndpointSummary, ChannelMessage, ChannelPendingResult, MessageCursor, MessageQuery, ChannelHistoryQuery, ChannelPendingOptions, ChannelHistoryResult, ChannelReceiveResult, MessageSearch as ActivitySearch, AckResult, ChannelEndpointHold as HeldSeat } from './channel-facade.js';
9
+ export type { CatchWaitStart, OperationProgress, OperationControl, OperationFailureDetail, OperationError } from './neutral.js';
6
10
  export type { ActivityId, Perception, Reach } from './square-core.js';
7
- export type { Owner, WakeAddress, ActivityMetadata, ActivityDelivery, JsonValue, DirectedNotificationRoute } from './model.js';
8
- /** Indexed archive event, including read bookkeeping. */
9
- export type ArchiveRecord = Readonly<StoredAct>;
11
+ export type { MessageId } from './message.js';
12
+ export type { Owner, WakeAddress, ActivityMetadata, JsonValue, DirectedNotificationRoute } from './model.js';
13
+ /** Indexed archive event, including read bookkeeping and a say's linked content. */
14
+ export type ArchiveRecord = Readonly<HydratedAct>;
10
15
  export interface Activity {
11
16
  readonly id: ActivityId;
12
17
  readonly at: number;
@@ -14,11 +19,12 @@ export interface Activity {
14
19
  readonly actor: string;
15
20
  readonly body?: string;
16
21
  readonly metadata?: ActivityMetadata;
17
- readonly delivery?: ActivityDelivery;
18
22
  readonly mentions: readonly string[];
19
23
  readonly reach?: Reach;
20
24
  readonly target?: string;
21
25
  readonly reply?: ActivityId;
26
+ /** Channel message owning a say's body and metadata. */
27
+ readonly messageId?: import('./message.js').MessageId;
22
28
  }
23
29
  export interface PerceivedActivity extends Activity {
24
30
  readonly perception: Perception;
@@ -50,22 +56,6 @@ export interface ExpressResult {
50
56
  readonly hint?: string;
51
57
  readonly observation?: ExpressObservation;
52
58
  }
53
- export type ChannelTruncate = number | false;
54
- export interface ChannelSendInput {
55
- readonly from: string;
56
- readonly to: string | readonly string[];
57
- readonly body: string;
58
- readonly truncate?: ChannelTruncate;
59
- readonly metadata?: ActivityMetadata;
60
- }
61
- export interface ChannelSendResult {
62
- readonly id: ActivityId;
63
- readonly at: number;
64
- readonly from: string;
65
- readonly to: readonly string[];
66
- readonly body: string;
67
- readonly metadata?: ActivityMetadata;
68
- }
69
59
  export interface ListenerChangeResult {
70
60
  readonly activity: Activity | null;
71
61
  readonly observation?: ListenerObservation;
@@ -86,10 +76,6 @@ export interface CatchOptions extends ObservationOptions {
86
76
  readonly limit?: number;
87
77
  readonly replace?: boolean;
88
78
  }
89
- export interface ActivitySearch {
90
- readonly text: string;
91
- readonly mode: 'literal' | 'regex';
92
- }
93
79
  export interface ActivityCursor {
94
80
  readonly id: ActivityId;
95
81
  readonly direction: 'before' | 'after';
@@ -111,10 +97,6 @@ export interface PendingResult {
111
97
  /** Matching unconsumed activities outside this page. */
112
98
  readonly remaining: number;
113
99
  }
114
- export interface AckResult {
115
- /** Newly consumed addressed IDs; repeated acknowledgement is a no-op. */
116
- readonly consumed: number;
117
- }
118
100
  export type CatchReason = 'drained' | 'idle' | 'capped' | 'quorum' | 'replaced' | 'already-active';
119
101
  export interface CatchResult {
120
102
  readonly activities: readonly PerceivedActivity[];
@@ -124,37 +106,6 @@ export interface CatchResult {
124
106
  readonly hint?: string;
125
107
  readonly observation?: CatchObservation;
126
108
  }
127
- export interface CatchWaitStart {
128
- readonly idleMs: number;
129
- readonly replacementRequested: boolean;
130
- readonly replaced: boolean;
131
- }
132
- export type OperationProgress = {
133
- readonly kind: 'waiting';
134
- readonly reason: 'throttled';
135
- readonly delayMs?: number;
136
- } | ({
137
- readonly kind: 'catch-started';
138
- readonly participant: string;
139
- } & CatchWaitStart);
140
- export interface OperationControl {
141
- readonly signal?: AbortSignal;
142
- readonly onProgress?: (progress: OperationProgress) => void;
143
- }
144
- export interface OperationFailureDetail {
145
- readonly operation: 'express' | 'join' | 'catch' | 'history' | 'other';
146
- readonly subject?: 'caller' | 'filter' | 'recipient';
147
- readonly participant?: string;
148
- readonly expressEffect?: 'unsent' | 'landed' | 'unknown';
149
- readonly landedId?: ActivityId;
150
- readonly historyBounds?: {
151
- readonly maxLimit: number;
152
- readonly totalMatches: number;
153
- };
154
- }
155
- export interface OperationError extends Error {
156
- readonly operation?: OperationFailureDetail;
157
- }
158
109
  export interface HistoryQuery extends ActivityQuery, ObservationOptions {
159
110
  /** Ten by default; null explicitly reads the full archive. */
160
111
  readonly limit?: number | null;
@@ -202,7 +153,7 @@ export interface JoinResult {
202
153
  }
203
154
  export interface Participant {
204
155
  readonly name: string;
205
- readonly generation: ActivityId;
156
+ readonly generation: string;
206
157
  express(body: string | undefined, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
207
158
  express(input: ExpressInput, control?: OperationControl): Promise<ExpressResult>;
208
159
  listen(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
@@ -221,11 +172,6 @@ export interface Participant {
221
172
  renewDeliveryDeferral(id: string, durationMs: number, control?: OperationControl): Promise<DeliveryDeferral | null>;
222
173
  releaseDeliveryDeferral(id: string, control?: OperationControl): Promise<boolean>;
223
174
  }
224
- export interface HeldSeat {
225
- readonly name: string;
226
- readonly generation: ActivityId;
227
- readonly attachment: 'attached' | 'detached';
228
- }
229
175
  export type ParticipantPresence = 'never-joined' | 'active' | 'watching' | 'away' | 'done';
230
176
  export interface ParticipantSummary {
231
177
  readonly name: string;
@@ -1,5 +1,5 @@
1
1
  import { type VersionObserver } from './file-changes.js';
2
- import { type Owner } from './model.js';
2
+ import { type Owner } from './neutral.js';
3
3
  /** A rebuildable discovery hint: the owner that may have used an artifact and its path. */
4
4
  export interface SquareIndexEntry {
5
5
  readonly location: string;
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
5
5
  import { withFileLock } from './file-lock.js';
6
6
  import { canonicalPath } from './canonical-path.js';
7
7
  import { observeFileMetadata } from './file-changes.js';
8
- import { validateOwner } from './model.js';
8
+ import { validateOwner } from './neutral.js';
9
9
  export function squareIndexPath(env = process.env) {
10
10
  const configured = env.SQUARE_INDEX?.trim();
11
11
  return path.resolve(configured ?? path.join(os.homedir(), '.square', 'index.ndjsonl'));
@@ -1,2 +1,7 @@
1
- import type { SquareLocation } from './square-facade.js';
1
+ interface SquareLocation {
2
+ readonly path?: string;
3
+ readonly cwd?: string;
4
+ readonly env?: NodeJS.ProcessEnv;
5
+ }
2
6
  export declare function squareLocation(input: SquareLocation): string;
7
+ export {};
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { SquareError } from './model.js';
2
+ import { SquareError } from './neutral.js';
3
3
  export function squareLocation(input) {
4
4
  const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
5
5
  if (selected === undefined || selected.trim() === '')