@astrosheep/square 0.4.0 → 0.4.2

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 (196) hide show
  1. package/dist/artifact.d.ts +1 -3
  2. package/dist/artifact.js +12 -36
  3. package/dist/attention-presentation.d.ts +2 -22
  4. package/dist/attention-presentation.js +9 -12
  5. package/dist/boundary-presentation.d.ts +3 -14
  6. package/dist/boundary-presentation.js +29 -104
  7. package/dist/card-presentation.d.ts +1 -0
  8. package/dist/card-presentation.js +2 -0
  9. package/dist/catch-decisions.js +1 -7
  10. package/dist/claude-boundary-state.d.ts +20 -0
  11. package/dist/claude-boundary-state.js +84 -0
  12. package/dist/claude-hook.d.ts +2 -0
  13. package/dist/claude-hook.js +231 -0
  14. package/dist/claude-mcp-context.d.ts +1 -1
  15. package/dist/claude-mcp-context.js +12 -3
  16. package/dist/cli/context.js +7 -9
  17. package/dist/cli/observation-commands.d.ts +4 -1
  18. package/dist/cli/observation-commands.js +37 -4
  19. package/dist/cli/registry.js +5 -2
  20. package/dist/codex-address.d.ts +2 -2
  21. package/dist/codex-boundary-state.d.ts +1 -5
  22. package/dist/codex-boundary-state.js +0 -4
  23. package/dist/codex-hook.d.ts +7 -10
  24. package/dist/codex-hook.js +69 -33
  25. package/dist/codex-queue.d.ts +10 -11
  26. package/dist/codex-queue.js +32 -32
  27. package/dist/codex-watch.d.ts +2 -0
  28. package/dist/codex-watch.js +100 -0
  29. package/dist/decisions.js +2 -2
  30. package/dist/delivery-deferral.d.ts +8 -17
  31. package/dist/delivery-deferral.js +0 -28
  32. package/dist/delivery-frame.d.ts +7 -0
  33. package/dist/delivery-frame.js +11 -0
  34. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  35. package/dist/delivery-ledger-sqlite.js +496 -0
  36. package/dist/delivery-ledger.d.ts +196 -0
  37. package/dist/delivery-ledger.js +268 -0
  38. package/dist/delivery-lifetime.d.ts +6 -0
  39. package/dist/delivery-lifetime.js +45 -0
  40. package/dist/harness-claude.js +3 -4
  41. package/dist/harness-links.js +63 -7
  42. package/dist/harness.js +12 -17
  43. package/dist/help.js +4 -1
  44. package/dist/http-delivery.d.ts +5 -9
  45. package/dist/http-delivery.js +20 -77
  46. package/dist/inbox.d.ts +6 -49
  47. package/dist/inbox.js +24 -371
  48. package/dist/index.d.ts +6 -3
  49. package/dist/index.js +4 -1
  50. package/dist/mcp.js +2 -3
  51. package/dist/model.d.ts +7 -47
  52. package/dist/model.js +1 -7
  53. package/dist/notifications.d.ts +24 -20
  54. package/dist/notifications.js +39 -108
  55. package/dist/operation-control.d.ts +4 -0
  56. package/dist/operation-control.js +20 -0
  57. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  58. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  59. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  60. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  61. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  62. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  63. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  64. package/dist/packages/agent-delivery/src/codex.js +41 -4
  65. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  66. package/dist/packages/agent-delivery/src/index.js +1 -1
  67. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  68. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  69. package/dist/paseo-delivery.d.ts +5 -5
  70. package/dist/paseo-delivery.js +19 -97
  71. package/dist/paseo.d.ts +1 -1
  72. package/dist/paseo.js +1 -1
  73. package/dist/plugins/opencode/index.d.ts +5 -3
  74. package/dist/plugins/opencode/index.js +99 -21
  75. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  76. package/dist/plugins/opencode-shared/delivery.js +109 -0
  77. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  78. package/dist/plugins/opencode-v1/index.js +41 -0
  79. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  80. package/dist/plugins/opencode-v1/runtime.js +152 -0
  81. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  82. package/dist/plugins/opencode-v1/transport.js +50 -0
  83. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  84. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  85. package/dist/ports.d.ts +31 -90
  86. package/dist/session-lifecycle.d.ts +8 -7
  87. package/dist/session-lifecycle.js +40 -42
  88. package/dist/sqlite-square.d.ts +14 -23
  89. package/dist/sqlite-square.js +132 -139
  90. package/dist/square-api.js +4 -7
  91. package/dist/square-core.d.ts +0 -1
  92. package/dist/square-domain.d.ts +5 -17
  93. package/dist/square-domain.js +104 -221
  94. package/dist/square-facade.d.ts +18 -15
  95. package/dist/square-post.d.ts +53 -0
  96. package/dist/square-post.js +4 -0
  97. package/dist/square-posting.d.ts +13 -0
  98. package/dist/square-posting.js +174 -0
  99. package/dist/square-producer-sqlite.d.ts +11 -0
  100. package/dist/square-producer-sqlite.js +210 -0
  101. package/dist/square-sqlite.d.ts +15 -25
  102. package/dist/square-sqlite.js +150 -226
  103. package/dist/views.d.ts +2 -2
  104. package/dist/views.js +0 -1
  105. package/dist/wake-sink.d.ts +4 -11
  106. package/dist/wake-sink.js +8 -32
  107. package/dist/wake-transports.d.ts +4 -7
  108. package/dist/wake-transports.js +7 -39
  109. package/examples/symmetric-domains.mjs +29 -249
  110. package/guides/change-waits.md +13 -53
  111. package/guides/claude-native.md +7 -171
  112. package/guides/codex-delivery.md +55 -80
  113. package/guides/delivery-primitives.md +48 -207
  114. package/guides/mcp.md +1 -1
  115. package/guides/operation-cancellation.md +2 -5
  116. package/guides/participant-ownership.md +25 -89
  117. package/guides/pi-native-delivery.md +19 -56
  118. package/guides/pi-tools.md +6 -8
  119. package/guides/square-api.md +113 -615
  120. package/package.json +8 -8
  121. package/plugins/claude/.claude-plugin/plugin.json +2 -3
  122. package/plugins/claude/hooks/hooks.json +11 -1
  123. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  124. package/plugins/codex/hooks/hooks.json +11 -23
  125. package/plugins/opencode/index.ts +91 -23
  126. package/plugins/opencode-shared/delivery.ts +91 -0
  127. package/plugins/opencode-v1/index.ts +36 -0
  128. package/plugins/opencode-v1/runtime.ts +134 -0
  129. package/plugins/opencode-v1/transport.ts +41 -0
  130. package/plugins/paseo/OVERVIEW.md +5 -5
  131. package/plugins/paseo/README.md +7 -6
  132. package/plugins/paseo/client/markdown.tsx +5 -5
  133. package/plugins/paseo/client/square-message.tsx +28 -24
  134. package/plugins/paseo/client/transform.ts +8 -5
  135. package/plugins/paseo/shared/card-presentation.ts +16 -0
  136. package/plugins/paseo/shared/message.ts +6 -11
  137. package/plugins/pi/square-pi.js +156 -417
  138. package/dist/channel-artifact.d.ts +0 -10
  139. package/dist/channel-artifact.js +0 -55
  140. package/dist/channel-delivery.d.ts +0 -11
  141. package/dist/channel-delivery.js +0 -51
  142. package/dist/channel-domain.d.ts +0 -3
  143. package/dist/channel-domain.js +0 -83
  144. package/dist/channel-facade.d.ts +0 -76
  145. package/dist/channel-facade.js +0 -1
  146. package/dist/channel-internal.d.ts +0 -126
  147. package/dist/channel-internal.js +0 -440
  148. package/dist/channel-metadata.d.ts +0 -11
  149. package/dist/channel-metadata.js +0 -1
  150. package/dist/channel-ports.d.ts +0 -241
  151. package/dist/channel-ports.js +0 -1
  152. package/dist/channel-presentation.d.ts +0 -17
  153. package/dist/channel-presentation.js +0 -22
  154. package/dist/channel-sqlite-schema.d.ts +0 -13
  155. package/dist/channel-sqlite-schema.js +0 -81
  156. package/dist/channel-transport.d.ts +0 -62
  157. package/dist/channel-transport.js +0 -1
  158. package/dist/channel.d.ts +0 -84
  159. package/dist/channel.js +0 -60
  160. package/dist/claude-delivery-status.d.ts +0 -13
  161. package/dist/claude-delivery-status.js +0 -46
  162. package/dist/claude-delivery.d.ts +0 -12
  163. package/dist/claude-delivery.js +0 -108
  164. package/dist/claude-mod.d.ts +0 -8
  165. package/dist/claude-mod.js +0 -172
  166. package/dist/claude-transport.d.ts +0 -5
  167. package/dist/claude-transport.js +0 -58
  168. package/dist/cmd/notify-once.d.ts +0 -2
  169. package/dist/cmd/notify-once.js +0 -29
  170. package/dist/delivery-attempts.d.ts +0 -52
  171. package/dist/delivery-attempts.js +0 -73
  172. package/dist/delivery-effects.d.ts +0 -139
  173. package/dist/delivery-effects.js +0 -163
  174. package/dist/delivery-health.d.ts +0 -51
  175. package/dist/delivery-health.js +0 -173
  176. package/dist/delivery-operations.d.ts +0 -22
  177. package/dist/delivery-operations.js +0 -198
  178. package/dist/delivery-primitives.d.ts +0 -5
  179. package/dist/delivery-primitives.js +0 -3
  180. package/dist/delivery.d.ts +0 -56
  181. package/dist/delivery.js +0 -80
  182. package/dist/plugins/opencode/delivery.d.ts +0 -19
  183. package/dist/plugins/opencode/delivery.js +0 -223
  184. package/dist/presentation-operations.d.ts +0 -5
  185. package/dist/presentation-operations.js +0 -86
  186. package/dist/sqlite-channel.d.ts +0 -15
  187. package/dist/sqlite-channel.js +0 -600
  188. package/dist/square-index.d.ts +0 -16
  189. package/dist/square-index.js +0 -106
  190. package/dist/wake-preview.d.ts +0 -7
  191. package/dist/wake-preview.js +0 -15
  192. package/plugins/claude/hooks/register.js +0 -132
  193. package/plugins/claude/types.d.ts +0 -7
  194. package/plugins/opencode/delivery.ts +0 -198
  195. /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
  196. /package/dist/{channel-values.js → square-values.js} +0 -0
@@ -1,20 +1,20 @@
1
- import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
2
1
  import { validateActivityMetadata, activityMetadataHasContent } from './model.js';
3
- import { catchCoverage, deriveLanding, landingFacts, extendLandingCatchCoverage } from './square-domain.js';
2
+ import { deriveLanding, landingFacts } from './square-domain.js';
4
3
  import { formatActivityId } from './square-core.js';
5
4
  import { compileSearchPattern } from './search.js';
6
- import { createChannelSqlite, endpoint, endpointGeneration, messageAddress, deliveryEvidence, deliveryDeferral, receiveClaim } from './sqlite-channel.js';
5
+ import { validateEndpointGeneration as endpointGeneration } from './message.js';
6
+ import { createProducerDriver, producerSchemaSql, initializeProducerSqlite, resetProducerSqlite } from './square-producer-sqlite.js';
7
7
  import { readStorageRevision, initializeStorageMeta, STORAGE_META_SQL, validateSchema } from './sqlite-framing.js';
8
- import { encode, key, invalidArtifact, requireFact, integer, finite, string, json, object, shape, strings, decoded, immutable, stableJson, pageRows, pageLimit } from './sqlite-values.js';
8
+ import { encode, key, invalidArtifact, requireFact, integer, finite, string, json, object, shape, strings, decoded, immutable, pageRows, pageLimit } from './sqlite-values.js';
9
9
  export { invalidArtifact } from './sqlite-values.js';
10
- export { endpointGeneration } from './sqlite-channel.js';
10
+ export { validateEndpointGeneration as endpointGeneration } from './message.js';
11
11
  export const APPLICATION_ID = 0x53515245;
12
- export const CURRENT_USER_VERSION = 11;
12
+ export const CURRENT_USER_VERSION = 12;
13
13
  export function emptyRuntimeState(nextActIndex = 0) {
14
14
  return { nextActIndex, observations: {}, leases: {} };
15
15
  }
16
16
  const ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','read','listen','ignore','unlisten')";
17
- /** Agora owns only its allocator/configuration header and social tables; neutral storage owns the revision header. */
17
+ /** Square owns activity, audience, participant, and read-state facts. */
18
18
  export const AGORA_SCHEMA_SQL = `CREATE TABLE square_meta (
19
19
  id INTEGER PRIMARY KEY CHECK (id = 1),
20
20
  next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),
@@ -31,9 +31,7 @@ CREATE TABLE activities (
31
31
  actor_key_json TEXT COLLATE BINARY NOT NULL,
32
32
  reach TEXT CHECK (reach = 'bell'),
33
33
  reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),
34
- message_index INTEGER CHECK (message_index IS NULL OR message_index BETWEEN 0 AND 9007199254740991),
35
34
  payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
36
- CHECK ((kind = 'say') = (message_index IS NOT NULL)),
37
35
  CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
38
36
  );
39
37
  CREATE INDEX activity_kind_index ON activities(kind, act_index);
@@ -41,7 +39,6 @@ CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index);
41
39
  CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';
42
40
  CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';
43
41
  CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell';
44
- CREATE UNIQUE INDEX activity_message ON activities(message_index) WHERE message_index IS NOT NULL;
45
42
  CREATE TABLE participants (
46
43
  name_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
47
44
  name_json TEXT NOT NULL,
@@ -67,31 +64,47 @@ CREATE TABLE landed_audience (
67
64
  recipient_key_json TEXT COLLATE BINARY NOT NULL,
68
65
  recipient_order INTEGER NOT NULL CHECK (recipient_order >= 0),
69
66
  reason TEXT NOT NULL CHECK (reason IN ('mention','reply','attention','bell')),
67
+ owner_json TEXT,
68
+ generation_json TEXT,
70
69
  PRIMARY KEY (act_index, recipient_key_json),
71
70
  UNIQUE (act_index, recipient_order)
72
71
  ) WITHOUT ROWID;
73
- CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
72
+ CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);
73
+ CREATE TABLE participant_seats (
74
+ participant_key_json TEXT PRIMARY KEY NOT NULL, participant_json TEXT NOT NULL,
75
+ owner_json TEXT NOT NULL, generation_json TEXT NOT NULL,
76
+ wake_address_json TEXT, attachment TEXT NOT NULL CHECK (attachment IN ('attached','detached'))
77
+ ) WITHOUT ROWID;
78
+ CREATE INDEX participant_seat_owner ON participant_seats(owner_json);
79
+ CREATE TABLE caught_activity (
80
+ recipient_key_json TEXT NOT NULL, act_index INTEGER NOT NULL, at REAL NOT NULL,
81
+ PRIMARY KEY (recipient_key_json,act_index)
82
+ ) WITHOUT ROWID;
83
+ CREATE TABLE catch_leases (
84
+ recipient_key_json TEXT PRIMARY KEY NOT NULL, generation_json TEXT NOT NULL,
85
+ lease_json TEXT NOT NULL CHECK (json_valid(lease_json))
86
+ ) WITHOUT ROWID;`;
74
87
  export function initializeAgoraSqlite(db, configuration = {}) {
75
88
  db.exec(AGORA_SCHEMA_SQL);
76
89
  db.prepare('INSERT INTO square_meta(id,next_act_index,hard_cap,throttle_per_minute,preamble_json,warmup_json) VALUES(1,0,?,?,?,?)')
77
90
  .run(configuration.hardCap ?? null, configuration.throttlePerMinute ?? null, JSON.stringify(configuration.preamble ?? []), JSON.stringify(configuration.warmup ?? []));
78
91
  }
79
92
  export function resetAgoraSqlite(db) {
80
- for (const table of ['landed_audience', 'attention', 'participants', 'activities', 'square_meta'])
93
+ for (const table of ['catch_leases', 'caught_activity', 'participant_seats', 'landed_audience', 'attention', 'participants', 'activities', 'square_meta'])
81
94
  db.exec(`DROP TABLE ${table}`);
82
95
  }
83
- function validateSquareFraming(db, lower, location = '') {
96
+ function validateSquareFraming(db, location = '') {
84
97
  const identity = db.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
85
98
  requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
86
- requireFact(identity.user_version === CURRENT_USER_VERSION, 'unsupported SQLite format version; this build opens only fresh format-11 squares.');
87
- validateSchema(db, [STORAGE_META_SQL, lower.schemaSql, AGORA_SCHEMA_SQL].join('\n'), location);
99
+ requireFact(identity.user_version === CURRENT_USER_VERSION, 'unsupported SQLite format version; this build opens only fresh format-12 squares.');
100
+ validateSchema(db, [STORAGE_META_SQL, producerSchemaSql, AGORA_SCHEMA_SQL].join('\n'), location);
88
101
  }
89
- /** Explicit full-Square framing and reset, composed from the neutral store and the supplied Channel schema. */
90
- export function squareStoreFormat(lower, resetStorage) {
102
+ /** Full Square framing and reset, including producer state. */
103
+ export function squareStoreFormat(resetStorage) {
91
104
  return {
92
105
  applicationId: APPLICATION_ID, userVersion: CURRENT_USER_VERSION, observerKey: 'square',
93
- validate: (db, location) => validateSquareFraming(db, lower, location),
94
- reset(db) { lower.reset(db); resetAgoraSqlite(db); resetStorage(db); },
106
+ validate: (db, location) => validateSquareFraming(db, location),
107
+ reset(db) { resetProducerSqlite(db); resetAgoraSqlite(db); resetStorage(db); },
95
108
  };
96
109
  }
97
110
  const kinds = ['join', 'done', 'say', 'read', 'listen', 'ignore', 'unlisten'];
@@ -103,14 +116,11 @@ function head(row) {
103
116
  requireFact(row.reach === null || row.kind === 'say' && row.reach === 'bell');
104
117
  if (row.reply_index !== null)
105
118
  requireFact(row.kind === 'say' && integer(row.reply_index) < index);
106
- const messageIndex = row.message_index === null || row.message_index === undefined ? undefined : integer(row.message_index);
107
- requireFact((row.kind === 'say') === (messageIndex !== undefined));
108
119
  return immutable({
109
120
  index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index }),
110
- ...(messageIndex === undefined ? {} : { messageIndex }),
111
121
  });
112
122
  }
113
- /** A persisted upper activity. A say is a relation: it may not carry content and must name its message. */
123
+ /** Validate the complete persisted activity. */
114
124
  export function validateStoredActivity(value) {
115
125
  const a = object(value);
116
126
  integer(a.index);
@@ -120,15 +130,24 @@ export function validateStoredActivity(value) {
120
130
  string(a.actor, true);
121
131
  switch (a.kind) {
122
132
  case 'say':
123
- shape(a, [...common], ['mentions', 'reach', 'reply', 'messageIndex']);
124
- requireFact(Object.hasOwn(a, 'messageIndex'), 'say is missing its message.');
133
+ shape(a, [...common], ['mentions', 'reach', 'reply', 'body', 'metadata']);
134
+ if (a.body !== undefined)
135
+ string(a.body);
136
+ if (a.metadata !== undefined) {
137
+ try {
138
+ validateActivityMetadata(a.metadata);
139
+ }
140
+ catch {
141
+ throw invalidArtifact('say metadata is malformed.');
142
+ }
143
+ }
144
+ requireFact((typeof a.body === 'string' && a.body.trim() !== '') || activityMetadataHasContent(a.metadata), 'say must contain a non-empty body or non-empty metadata.');
125
145
  if (Object.hasOwn(a, 'mentions'))
126
146
  strings(a.mentions);
127
147
  if (Object.hasOwn(a, 'reach'))
128
148
  requireFact(a.reach === 'bell');
129
149
  if (Object.hasOwn(a, 'reply'))
130
150
  requireFact(integer(a.reply) < a.index);
131
- integer(a.messageIndex);
132
151
  break;
133
152
  case 'done':
134
153
  shape(a, common, ['body']);
@@ -149,44 +168,24 @@ export function validateStoredActivity(value) {
149
168
  }
150
169
  return value;
151
170
  }
152
- /** The explicit upper projection of a stored say onto its authoritative message content. */
171
+ /** Validate complete activity content. */
153
172
  export function validateHydratedActivity(value) {
154
173
  const a = object(value);
155
174
  if (a.kind !== 'say')
156
175
  return validateStoredActivity(a);
157
- const { body, metadata, ...relation } = a;
158
- const stored = validateStoredActivity(relation);
159
- if (body !== undefined)
160
- string(body);
161
- if (metadata !== undefined) {
162
- try {
163
- validateActivityMetadata(metadata);
164
- }
165
- catch {
166
- throw invalidArtifact('say metadata is malformed.');
167
- }
168
- }
169
- requireFact((typeof body === 'string' && body.trim() !== '') || activityMetadataHasContent(metadata), 'say must contain a non-empty body or non-empty metadata.');
170
- return { ...stored, ...(body === undefined ? {} : { body: body }), ...(metadata === undefined ? {} : { metadata: metadata }) };
176
+ return validateStoredActivity(a);
171
177
  }
172
- /** Stored Agora relation only: say content lives in its linked Channel message. */
178
+ /** Decode an activity with its complete payload. */
173
179
  function activity(row) {
174
180
  const h = head(row), payload = object(json(row.payload_json));
175
- // Payload cannot shadow indexed structural columns or message-owned content.
176
- const allowed = h.kind === 'say' ? ['mentions'] : h.kind === 'done' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
181
+ // Payload cannot shadow indexed structural columns.
182
+ const allowed = h.kind === 'say' ? ['mentions', 'body', 'metadata'] : h.kind === 'done' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
177
183
  requireFact(Object.keys(payload).every(k => allowed.includes(k)));
178
184
  return immutable(validateStoredActivity({ ...h, ...payload }));
179
185
  }
180
- /** Explicit upper projection: the stored say plus its authoritative Channel content. */
186
+ /** Activity and content are one stored fact. */
181
187
  function activityWithContent(row) {
182
- const stored = activity(row);
183
- if (stored.kind !== 'say')
184
- return stored;
185
- requireFact(row.body_json !== null && row.body_json !== undefined, 'say message is missing.');
186
- const metadata = row.metadata_json === null || row.metadata_json === undefined ? undefined : json(row.metadata_json);
187
- const body = decoded(row.body_json);
188
- return immutable(validateHydratedActivity({ ...stored,
189
- ...(body === '' ? {} : { body }), ...(metadata === undefined ? {} : { metadata }) }));
188
+ return activity(row);
190
189
  }
191
190
  function participant(row) {
192
191
  const name = decoded(row.name_json, true);
@@ -254,8 +253,7 @@ function activitySelection(filter, alias = 'a') {
254
253
  terms.push(`NOT EXISTS (
255
254
  SELECT 1 FROM json_each(?) field WHERE NOT EXISTS (
256
255
  WITH RECURSIVE walk(value,type,depth) AS (
257
- SELECT CASE WHEN ${alias}.kind='say' THEN json_extract(m.metadata_json,'$') ELSE json_extract(${alias}.payload_json,'$.metadata') END,
258
- CASE WHEN ${alias}.kind='say' THEN json_type(m.metadata_json,'$') ELSE json_type(${alias}.payload_json,'$.metadata') END,0
256
+ SELECT json_extract(${alias}.payload_json,'$.metadata'), json_type(${alias}.payload_json,'$.metadata'),0
259
257
  UNION ALL
260
258
  SELECT child.value,child.type,walk.depth+1
261
259
  FROM walk JOIN json_each(CASE WHEN walk.type='object' THEN walk.value ELSE '{}' END) child
@@ -271,12 +269,12 @@ function activitySelection(filter, alias = 'a') {
271
269
  args.push(encode(filter.metadataMatch.fields));
272
270
  }
273
271
  if (filter.search !== undefined) {
274
- terms.push(`square_search(?,?,'act/'||${alias}.act_index,${alias}.actor_json,CASE WHEN ${alias}.kind='say' THEN json_extract(m.body_json,'$') ELSE json_extract(${alias}.payload_json,'$.body') END)=1`);
272
+ terms.push(`square_search(?,?,'act/'||${alias}.act_index,${alias}.actor_json,json_extract(${alias}.payload_json,'$.body'))=1`);
275
273
  args.push(filter.search.mode, filter.search.text);
276
274
  }
277
275
  return { sql: terms.length ? terms.join(' AND ') : '1', args };
278
276
  }
279
- export function createSqliteDriver(db, channelDriver = createChannelSqlite(db)) {
277
+ export function createSqliteDriver(db) {
280
278
  let changed = false;
281
279
  const all = (sql, args = []) => db.prepare(sql).all(...args);
282
280
  const one = (sql, args = []) => db.prepare(sql).get(...args);
@@ -304,41 +302,34 @@ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db))
304
302
  return activitySelection(filter);
305
303
  }
306
304
  const read = {};
307
- const channel = channelDriver, channelRead = channel, channelWrite = channel;
308
- const currentEndpoint = (name) => channel.endpoint(name);
309
- const owner = (requested) => read.participant(requested)?.name ?? requested;
305
+ const producer = createProducerDriver(db);
306
+ const currentEndpoint = (name) => {
307
+ const row = one('SELECT * FROM participant_seats WHERE participant_key_json=?', [key(name)]);
308
+ if (!row)
309
+ return undefined;
310
+ return immutable({ participant: decoded(row.participant_json, true), owner: decoded(row.owner_json, true), generation: endpointGeneration(decoded(row.generation_json, true)), attachment: row.attachment, ...(row.wake_address_json === null ? {} : { wakeAddress: json(row.wake_address_json) }) });
311
+ };
310
312
  function directedSelection(recipient, filter) {
311
313
  const participant = read.participant(recipient), base = selectActivity({ ...filter, actors: filter.senders });
312
314
  base.sql += ' AND d.recipient_key_json=?';
313
315
  base.args.push(participant ? key(participant.name) : '');
314
316
  if (filter.currentGeneration) {
315
- base.sql += ' AND (ma.generation_json IS NULL OR ma.generation_json=?)';
317
+ base.sql += ' AND (d.generation_json IS NULL OR d.generation_json=?)';
316
318
  base.args.push(participant ? encode(currentEndpoint(participant.name)?.generation ?? '') : '');
317
319
  }
318
320
  if (filter.unseenOnly) {
319
- base.sql += ' AND NOT EXISTS (SELECT 1 FROM receipts r WHERE r.owner_key_json=? AND r.msg_index=a.message_index)';
320
- base.args.push(encode(participant?.name ?? recipient));
321
+ base.sql += ' AND NOT EXISTS (SELECT 1 FROM caught_activity r WHERE r.recipient_key_json=? AND r.act_index=a.act_index)';
322
+ base.args.push(key(participant?.name ?? recipient));
321
323
  }
322
324
  if (filter.attentionOnly)
323
325
  base.sql += " AND d.reason IN ('mention','reply','bell')";
324
326
  return { base, participant };
325
327
  }
326
328
  // ----- Upper Agora read capabilities -----
327
- const lower = channelRead;
328
329
  const readMethods = {
329
- seat(name) { const endpointFact = currentEndpoint(name); return endpointFact === undefined ? undefined : endpointFact; },
330
- seatsForOwner: (requested) => lower.endpointsForOwner(requested),
331
- deliveryEvidence: (participant, generation, activity) => lower.deliveryEvidence(participant, generation, activity),
332
- deliveryByToken: (token) => lower.deliveryByToken(token),
333
- cancelledThrough(participant, generation) {
334
- const boundary = lower.cancelledThrough(participant, generation);
335
- if (boundary === undefined)
336
- return undefined;
337
- if (boundary < 0)
338
- return -1;
339
- const row = one('SELECT MAX(act_index) AS act_index FROM activities WHERE message_index IS NOT NULL AND message_index<=?', [boundary]);
340
- return row?.act_index === null || row?.act_index === undefined ? -1 : integer(row.act_index);
341
- },
330
+ pendingPosts: producer.pendingPosts, postedPosts: producer.postedPosts, ownsPost: producer.ownsPost,
331
+ seat: currentEndpoint,
332
+ seatsForOwner: requested => immutable(all('SELECT participant_json FROM participant_seats WHERE owner_json=?', [encode(requested)]).map(row => currentEndpoint(decoded(row.participant_json, true)))),
342
333
  header() { return readSquareHeader(db); },
343
334
  participant(name) { const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]); return r ? participant(r) : undefined; },
344
335
  participantCounts() {
@@ -352,19 +343,15 @@ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db))
352
343
  const r = one('SELECT * FROM activities WHERE act_index=?', [integer(index)]);
353
344
  return r ? activity(r) : undefined;
354
345
  },
355
- activityByMessage(index) {
356
- const r = one("SELECT * FROM activities WHERE message_index=? AND kind='say'", [integer(index)]);
357
- return r ? activity(r) : undefined;
358
- },
359
346
  activityHeads(filter, page) {
360
347
  const s = selectActivity(filter), limit = pageLimit(page);
361
- return pageRows(all(`SELECT a.*,m.body_json,m.metadata_json FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index WHERE ${s.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...s.args, limit]), page, head);
348
+ return pageRows(all(`SELECT a.* FROM activities a WHERE ${s.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...s.args, limit]), page, head);
362
349
  },
363
350
  activities(indexes) {
364
351
  indexes.forEach(v => integer(v));
365
- return immutable(all('SELECT a.*,m.body_json,m.metadata_json FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index WHERE a.act_index IN (SELECT value FROM json_each(?)) ORDER BY a.act_index', [encode(indexes)]).map(activityWithContent));
352
+ return immutable(all('SELECT a.* FROM activities a WHERE a.act_index IN (SELECT value FROM json_each(?)) ORDER BY a.act_index', [encode(indexes)]).map(activityWithContent));
366
353
  },
367
- activityCount(filter) { const s = selectActivity(filter); return integer(one(`SELECT COUNT(*) AS n FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index WHERE ${s.sql}`, s.args).n); },
354
+ activityCount(filter) { const s = selectActivity(filter); return integer(one(`SELECT COUNT(*) AS n FROM activities a WHERE ${s.sql}`, s.args).n); },
368
355
  contextIndexes(centers, before, after) {
369
356
  integer(before);
370
357
  integer(after);
@@ -406,62 +393,66 @@ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db))
406
393
  args.push(integer(take));
407
394
  return immutable(all(`SELECT at FROM activities WHERE ${where} ORDER BY at DESC,act_index DESC LIMIT ?`, args).map(r => finite(r.at)).reverse());
408
395
  },
409
- audience(index) { return immutable(all('SELECT p.name_json,d.reason,ma.generation_json FROM landed_audience d JOIN participants p ON p.name_key_json=d.recipient_key_json JOIN activities a ON a.act_index=d.act_index JOIN message_addresses ma ON ma.msg_index=a.message_index AND ma.recipient_key_json=d.recipient_key_json WHERE d.act_index=? ORDER BY d.recipient_order', [integer(index)]).map(r => ({ recipient: decoded(r.name_json, true), route: routeKind(r.reason), ...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }) }))); },
396
+ audience(index) { return immutable(all('SELECT p.name_json,d.reason,d.generation_json,d.owner_json FROM landed_audience d JOIN participants p ON p.name_key_json=d.recipient_key_json WHERE d.act_index=? ORDER BY d.recipient_order', [integer(index)]).map(r => ({ recipient: decoded(r.name_json, true), route: routeKind(r.reason), ...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }), ...(r.owner_json === null ? {} : { owner: decoded(r.owner_json, true) }) }))); },
410
397
  directed(recipient, filter, page) {
411
398
  const { base, participant: p } = directedSelection(recipient, filter), limit = pageLimit(page);
412
- const rows = all(`SELECT a.*,m.body_json,m.metadata_json,d.reason,ma.generation_json FROM activities a JOIN messages m ON m.msg_index=a.message_index JOIN landed_audience d ON d.act_index=a.act_index JOIN message_addresses ma ON ma.msg_index=a.message_index AND ma.recipient_key_json=d.recipient_key_json WHERE ${base.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...base.args, limit]);
399
+ const rows = all(`SELECT a.*,d.reason,d.generation_json FROM activities a JOIN landed_audience d ON d.act_index=a.act_index WHERE ${base.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...base.args, limit]);
413
400
  return pageRows(rows, page, r => {
414
401
  const h = head(r);
415
402
  return { index: h.index, at: h.at, actor: h.actor, recipient: p?.name ?? recipient,
416
- route: routeKind(r.reason), messageIndex: h.messageIndex,
403
+ route: routeKind(r.reason),
417
404
  ...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }) };
418
405
  });
419
406
  },
420
407
  directedAt(recipient, index) { return read.directed(recipient, { afterIndex: integer(index) - 1, throughIndex: index }, { edge: 'oldest', limit: 1 }).items[0]; },
421
408
  directedCount(recipient, filter) {
422
409
  const { base } = directedSelection(recipient, filter);
423
- return integer(one(`SELECT COUNT(*) AS n FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index JOIN landed_audience d ON d.act_index=a.act_index JOIN message_addresses ma ON ma.msg_index=a.message_index AND ma.recipient_key_json=d.recipient_key_json WHERE ${base.sql}`, base.args).n);
410
+ return integer(one(`SELECT COUNT(*) AS n FROM activities a JOIN landed_audience d ON d.act_index=a.act_index WHERE ${base.sql}`, base.args).n);
424
411
  },
425
412
  receipt(name, index) {
426
- const r = one('SELECT r.at FROM receipts r JOIN activities a ON a.message_index=r.msg_index WHERE r.owner_key_json=? AND a.act_index=?', [encode(owner(name)), integer(index)]);
413
+ const r = one('SELECT at FROM caught_activity WHERE recipient_key_json=? AND act_index=?', [key(name), integer(index)]);
427
414
  return r ? immutable({ at: finite(r.at) }) : undefined;
428
415
  },
429
416
  receiptFacts(name, afterIndex) {
430
- const k = encode(owner(name));
431
- const r = one('SELECT MAX(at) AS latest FROM receipts WHERE owner_key_json=?', [k]);
432
- const e = one('SELECT MAX(a.act_index) AS latest FROM receipts r JOIN activities a ON a.message_index=r.msg_index WHERE r.owner_key_json=? AND a.act_index>?', [k, integer(afterIndex, -1)]);
417
+ const k = key(name);
418
+ const r = one('SELECT MAX(at) AS latest FROM caught_activity WHERE recipient_key_json=?', [k]);
419
+ const e = one('SELECT MAX(act_index) AS latest FROM caught_activity WHERE recipient_key_json=? AND act_index>?', [k, integer(afterIndex, -1)]);
433
420
  return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorActivityIndex: integer(e.latest) }) });
434
421
  },
435
422
  lease(name) {
436
- const lease = lower.claimLease(name, 'agora');
437
- if (lease === undefined)
438
- return undefined;
439
- const selector = object(lease.selector);
440
- const parsed = { leaseId: lease.id, heartbeatAt: lease.heartbeatAt, expiresAt: lease.expiresAt };
441
- if (selector.filter !== undefined)
442
- parsed.filter = selector.filter;
443
- return immutable(validateWatchLease(parsed));
423
+ const row = one('SELECT lease_json FROM catch_leases WHERE recipient_key_json=?', [key(name)]);
424
+ return row ? immutable(validateWatchLease(json(row.lease_json))) : undefined;
444
425
  },
445
- deliveryDeferrals: (participant, generation) => lower.deliveryDeferrals(participant, generation),
446
426
  };
447
- Object.assign(read, readMethods, { channel: channelRead });
427
+ Object.assign(read, readMethods);
448
428
  // ----- Upper Agora writes / landing -----
449
429
  const writes = {
450
- channel: channelWrite,
451
- storeSeat(value) { return channelWrite.storeEndpoint(value); },
452
- updateWakeAddress(value, wakeAddress) { return channelWrite.updateWakeAddress(value, wakeAddress); },
453
- reattachSeat(value, address) { return channelWrite.reattachEndpoint(value, address); },
454
- detachSeat(value) { return channelWrite.detachEndpoint(value); },
455
- removeSeat(value) { return channelWrite.removeEndpoint(value); },
430
+ insertNotice: producer.insertNotice, ackMatching: producer.ackMatching,
431
+ acquirePost: producer.acquirePost, settlePost: producer.settlePost, foldReceipt: producer.foldReceipt,
432
+ setSeatAttachment(seat, attachment) {
433
+ const count = run('UPDATE participant_seats SET attachment=? WHERE participant_key_json=? AND owner_json=? AND generation_json=? AND attachment<>?', [attachment, key(seat.participant), encode(seat.owner), encode(seat.generation), attachment]);
434
+ changed ||= count > 0;
435
+ return count > 0;
436
+ },
437
+ storeSeat(value) {
438
+ run('INSERT INTO participant_seats(participant_key_json,participant_json,owner_json,generation_json,wake_address_json,attachment) VALUES(?,?,?,?,?,?) ON CONFLICT(participant_key_json) DO UPDATE SET participant_json=excluded.participant_json,owner_json=excluded.owner_json,generation_json=excluded.generation_json,wake_address_json=excluded.wake_address_json,attachment=excluded.attachment', [key(value.participant), encode(value.participant), encode(value.owner), encode(value.generation), value.wakeAddress === undefined ? null : encode(value.wakeAddress), value.attachment]);
439
+ changed = true;
440
+ return true;
441
+ },
442
+ removeSeat(value) {
443
+ const count = run('DELETE FROM participant_seats WHERE participant_key_json=? AND owner_json=? AND generation_json=?', [key(value.participant), encode(value.owner), encode(value.generation)]);
444
+ if (count)
445
+ run('DELETE FROM catch_leases WHERE recipient_key_json=? AND generation_json=?', [key(value.participant), encode(value.generation)]);
446
+ changed ||= count > 0;
447
+ return count > 0;
448
+ },
456
449
  applyLanding(effect) {
457
450
  const a = validateStoredActivity(effect.activity);
458
451
  requireFact(a.index >= read.header().nextActIndex && a.index < Number.MAX_SAFE_INTEGER, 'activity index cannot advance.');
459
- const { index, at, kind, actor, reach, reply, messageIndex, ...payload } = a;
460
- const storedPayload = payload;
461
- run('INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,message_index,payload_json) VALUES(?,?,?,?,?,?,?,?,?)', [index, at, kind, encode(actor), key(actor), reach ?? null, reply ?? null, messageIndex ?? null, encode(storedPayload)]);
462
- // One message address per landed recipient is written by land() through publishMessage.
452
+ const { index, at, kind, actor, reach, reply, ...payload } = a;
453
+ run('INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json) VALUES(?,?,?,?,?,?,?,?)', [index, at, kind, encode(actor), key(actor), reach ?? null, reply ?? null, encode(payload)]);
463
454
  effect.audience.forEach((address, order) => {
464
- run('INSERT INTO landed_audience(act_index,recipient_key_json,recipient_order,reason) VALUES(?,?,?,?)', [index, key(address.recipient), order, address.route]);
455
+ run('INSERT INTO landed_audience(act_index,recipient_key_json,recipient_order,reason,owner_json,generation_json) VALUES(?,?,?,?,?,?)', [index, key(address.recipient), order, address.route, address.owner === undefined ? null : encode(address.owner), address.generation === undefined ? null : encode(address.generation)]);
465
456
  });
466
457
  if (effect.participant) {
467
458
  const p = effect.participant;
@@ -492,62 +483,54 @@ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db))
492
483
  run('INSERT INTO attention(listener_key_json,target_key_json,target_json,kind,since_index) VALUES(?,?,?,?,?) ON CONFLICT(listener_key_json,target_key_json) DO UPDATE SET target_json=excluded.target_json,kind=excluded.kind,since_index=excluded.since_index', [key(t.listener), key(t.target), encode(t.target), t.kind, t.sinceIndex]);
493
484
  }
494
485
  run('UPDATE square_meta SET next_act_index=? WHERE id=1', [index + 1]);
495
- extendLandingCatchCoverage(read, channelWrite.putReceiveClaim, a, effect.audience);
486
+ if (kind === 'say')
487
+ producer.consumeActivity(index, at);
496
488
  changed = true;
497
489
  },
498
- putLease(name, value, generation, suppressed) {
499
- const l = validateWatchLease(value), canonical = owner(name);
500
- if (generation !== undefined)
501
- endpointGeneration(generation);
502
- const existing = channelRead.claimLease(canonical, 'agora');
503
- if (existing !== undefined && generation !== undefined && existing.generation !== generation)
490
+ putLease(name, value, generation) {
491
+ const l = validateWatchLease(value), seat = currentEndpoint(name);
492
+ if (!seat || generation !== undefined && generation !== seat.generation)
504
493
  return false;
505
- return channelWrite.putReceiveClaim({ namespace: 'agora', id: l.leaseId, owner: canonical,
506
- generation: generation ?? existing?.generation ?? currentEndpoint(canonical)?.generation ?? 'detached',
507
- selector: { ...(l.filter === undefined ? {} : { filter: l.filter }) },
508
- suppressed: suppressed ?? [], heartbeatAt: l.heartbeatAt, expiresAt: l.expiresAt });
494
+ run('INSERT INTO catch_leases(recipient_key_json,generation_json,lease_json) VALUES(?,?,?) ON CONFLICT(recipient_key_json) DO UPDATE SET generation_json=excluded.generation_json,lease_json=excluded.lease_json', [key(name), encode(seat.generation), encode(l)]);
495
+ changed = true;
496
+ return true;
509
497
  },
510
498
  removeLease(name, expected, generation) {
511
- const canonical = owner(name), existing = channelRead.claimLease(canonical, 'agora');
512
- if (existing === undefined || existing.id !== expected || generation !== undefined && existing.generation !== generation)
499
+ if (read.lease(name)?.leaseId !== expected || generation !== undefined && currentEndpoint(name)?.generation !== generation)
513
500
  return false;
514
- return channelWrite.removeReceiveClaim(canonical, 'agora', expected);
501
+ const count = run('DELETE FROM catch_leases WHERE recipient_key_json=?', [key(name)]);
502
+ changed ||= count > 0;
503
+ return count > 0;
504
+ },
505
+ markSeen(name, indexes, at) {
506
+ finite(at);
507
+ let count = 0;
508
+ for (const index of indexes)
509
+ count += run('INSERT OR IGNORE INTO caught_activity(recipient_key_json,act_index,at) SELECT recipient_key_json,act_index,? FROM landed_audience WHERE recipient_key_json=? AND act_index=?', [at, key(name), integer(index)]);
510
+ changed ||= count > 0;
511
+ return count;
515
512
  },
516
513
  };
517
514
  function importLanding(activityValue) {
518
515
  const validated = validateHydratedActivity(activityValue);
519
- if (validated.kind !== 'say') {
520
- const effect = deriveLanding(validated, landingFacts(read, validated));
521
- driver.applyLanding(effect);
522
- // Imported custody: a join establishes the endpoint its activity coordinate names; a done drops it.
523
- if (validated.kind === 'join')
524
- channelWrite.storeEndpoint({ participant: validated.actor, owner: 'import', generation: formatActivityId(validated.index), attachment: 'attached' });
525
- else if (validated.kind === 'done') {
526
- const current = channelRead.endpoint(validated.actor);
527
- if (current !== undefined)
528
- channelWrite.removeEndpoint(current);
529
- }
530
- return validated;
516
+ driver.applyLanding(deriveLanding(validated, landingFacts(read, validated)));
517
+ if (validated.kind === 'join')
518
+ driver.storeSeat({ participant: validated.actor, owner: 'import', generation: formatActivityId(validated.index), attachment: 'attached' });
519
+ else if (validated.kind === 'done') {
520
+ const current = read.seat(validated.actor);
521
+ if (current)
522
+ driver.removeSeat(current);
531
523
  }
532
- const effect = deriveLanding(validated, landingFacts(read, validated));
533
- const published = channelWrite.publishMessage({ from: validated.actor, at: validated.at,
534
- body: validated.body ?? '', ...(validated.metadata === undefined ? {} : { metadata: validated.metadata }),
535
- to: effect.audience.map(({ recipient, generation }) => ({ recipient, ...(generation === undefined ? {} : { generation }) })) });
536
- const landed = { kind: 'say', index: validated.index, at: validated.at, actor: validated.actor, messageIndex: published.index,
537
- ...(validated.mentions === undefined ? {} : { mentions: validated.mentions }),
538
- ...(validated.reach === undefined ? {} : { reach: validated.reach }),
539
- ...(validated.reply === undefined ? {} : { reply: validated.reply }) };
540
- driver.applyLanding({ ...effect, activity: landed });
541
- return landed;
524
+ return validated;
542
525
  }
543
- const driver = Object.defineProperties(Object.assign(read, writes, { channel, importLanding }), {
544
- changed: { get: () => changed || channel.changed },
545
- deliveryChanged: { get: () => channel.deliveryChanged },
526
+ const driver = Object.defineProperties(Object.assign(read, writes, { importLanding }), {
527
+ changed: { get: () => changed || producer.changed },
528
+ deliveryChanged: { get: () => producer.deliveryChanged },
546
529
  });
547
530
  return driver;
548
531
  }
549
532
  /** Explicit import replay. Caller owns the transaction and format publication. */
550
- export function initializeSqlite(db, state, revision, lower) {
533
+ export function initializeSqlite(db, state, revision) {
551
534
  integer(revision);
552
535
  integer(state.runtime.nextActIndex);
553
536
  if (state.hardCap !== null)
@@ -555,14 +538,14 @@ export function initializeSqlite(db, state, revision, lower) {
555
538
  if (state.throttlePerMinute !== undefined)
556
539
  integer(state.throttlePerMinute, 1);
557
540
  initializeStorageMeta(db, revision);
558
- lower.initialize(db);
541
+ initializeProducerSqlite(db);
559
542
  initializeAgoraSqlite(db, { ...state, preamble: strings(state.preamble), warmup: strings(state.warmup) });
560
543
  const driver = createSqliteDriver(db);
561
544
  for (const activity of state.acts)
562
545
  driver.importLanding(activity);
563
546
  requireFact(state.runtime.nextActIndex > driver.header().lastActIndex, 'nextActIndex is behind the activity history.');
564
547
  db.prepare('UPDATE square_meta SET next_act_index=? WHERE id=1').run(state.runtime.nextActIndex);
565
- const insert = db.prepare('INSERT INTO receipts(owner_key_json,msg_index,at) VALUES(?,?,?)');
548
+ const insert = db.prepare('INSERT INTO caught_activity(recipient_key_json,act_index,at) VALUES(?,?,?)');
566
549
  for (const [owner, observations] of Object.entries(state.runtime.observations)) {
567
550
  string(owner, true);
568
551
  for (const [id, observation] of Object.entries(observations)) {
@@ -572,65 +555,41 @@ export function initializeSqlite(db, state, revision, lower) {
572
555
  requireFact(index < state.runtime.nextActIndex, 'receipt references an unassigned activity index.');
573
556
  requireFact(observation.state === 'seen');
574
557
  const activity = driver.activity(index);
575
- if (activity?.kind !== 'say' || activity.messageIndex === undefined)
558
+ if (activity?.kind !== 'say')
576
559
  continue;
577
- insert.run(encode(owner), activity.messageIndex, finite(observation.at));
560
+ insert.run(key(owner), index, finite(observation.at));
578
561
  }
579
562
  }
580
563
  for (const [owner, value] of Object.entries(state.runtime.leases)) {
581
564
  string(owner, true);
582
565
  const l = validateWatchLease(value), generation = driver.seat(owner)?.generation ?? 'detached';
583
- db.prepare('INSERT INTO receive_claims(namespace_key_json,owner_key_json,generation_json,claim_id_json,heartbeat_at,expires_at,selector_json,suppressed_json) VALUES(?,?,?,?,?,?,?,?)')
584
- .run(encode('agora'), encode(owner), encode(generation), encode(l.leaseId), l.heartbeatAt, l.expiresAt, encode({ ...(l.filter === undefined ? {} : { filter: l.filter }) }), encode(catchCoverage(driver, owner, l)));
566
+ db.prepare('INSERT INTO catch_leases(recipient_key_json,generation_json,lease_json) VALUES(?,?,?)').run(key(owner), encode(generation), encode(l));
585
567
  }
586
568
  }
587
569
  export function exportSqliteState(db) {
588
570
  const d = createSqliteDriver(db), h = d.header();
589
571
  const observations = {}, leases = {};
590
- // Receipts are message-coordinate now; export them under the establishing say's activity id.
591
- for (const row of db.prepare('SELECT r.owner_key_json AS owner_json,r.msg_index AS act_index,r.at FROM receipts r').all()) {
592
- const owner = decoded(row.owner_json, true);
593
- const say = d.activityByMessage(integer(row.act_index));
594
- if (say?.kind !== 'say')
595
- continue;
572
+ for (const row of db.prepare('SELECT p.name_json,r.act_index,r.at FROM caught_activity r JOIN participants p ON p.name_key_json=r.recipient_key_json').all()) {
573
+ const owner = decoded(row.name_json, true);
596
574
  if (!Object.hasOwn(observations, owner))
597
575
  Object.defineProperty(observations, owner, { value: {}, enumerable: true, writable: true });
598
- observations[owner][formatActivityId(say.index)] = { state: 'seen', at: finite(row.at) };
576
+ observations[owner][formatActivityId(integer(row.act_index))] = { state: 'seen', at: finite(row.at) };
599
577
  }
600
- for (const row of db.prepare("SELECT * FROM receive_claims WHERE namespace_key_json=?").all(encode('agora'))) {
601
- const owner = decoded(row.owner_key_json, true);
602
- const selector = object(json(row.selector_json));
603
- Object.defineProperty(leases, owner, { value: validateWatchLease({
604
- leaseId: decoded(row.claim_id_json, true), heartbeatAt: finite(row.heartbeat_at), expiresAt: finite(row.expires_at), ...(selector.filter === undefined ? {} : { filter: selector.filter })
605
- }), enumerable: true });
578
+ for (const row of db.prepare('SELECT p.name_json,l.lease_json FROM catch_leases l JOIN participants p ON p.name_key_json=l.recipient_key_json').all()) {
579
+ Object.defineProperty(leases, decoded(row.name_json, true), { value: validateWatchLease(json(row.lease_json)), enumerable: true });
606
580
  }
607
581
  return {
608
582
  hardCap: h.hardCap, ...(h.throttlePerMinute === undefined ? {} : { throttlePerMinute: h.throttlePerMinute }), preamble: [...h.preamble], warmup: [...h.warmup],
609
- acts: db.prepare('SELECT a.*,m.body_json,m.metadata_json FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index ORDER BY a.act_index').all().map(activityWithContent),
583
+ acts: db.prepare('SELECT * FROM activities ORDER BY act_index').all().map(activityWithContent),
610
584
  runtime: { nextActIndex: h.nextActIndex, observations, leases }
611
585
  };
612
586
  }
613
- /** Read-only integrity inspection; maintained-index replay runs when the lower schema is supplied. */
614
- export function inspectSqlite(db, lower) {
615
- return inspectRows(db, endpoint, lower);
616
- }
617
- function inspectRows(db, inspectEndpoint, lower) {
587
+ /** Read-only integrity inspection. */
588
+ export function inspectSqlite(db) {
618
589
  const check = db.prepare('PRAGMA integrity_check').all();
619
590
  requireFact(check.length === 1 && check[0].integrity_check === 'ok', 'SQLite integrity check failed.');
620
591
  const state = exportSqliteState(db), d = createSqliteDriver(db);
621
592
  d.roster();
622
- for (const row of db.prepare('SELECT * FROM endpoints').all())
623
- inspectEndpoint(row);
624
- for (const row of db.prepare('SELECT * FROM delivery_evidence').all())
625
- deliveryEvidence(row);
626
- for (const row of db.prepare('SELECT * FROM delivery_deferrals').all())
627
- deliveryDeferral(row);
628
- for (const row of db.prepare('SELECT * FROM receive_claims').all())
629
- receiveClaim(row);
630
- for (const row of db.prepare('SELECT ma.*,m.msg_index AS existing_msg_index FROM message_addresses ma LEFT JOIN messages m ON m.msg_index=ma.msg_index').all()) {
631
- requireFact(row.existing_msg_index !== null, 'message address references a missing message.');
632
- messageAddress(row);
633
- }
634
593
  for (const p of d.roster()) {
635
594
  d.attention(p.name);
636
595
  d.incomingAttention(p.name);
@@ -638,40 +597,5 @@ function inspectRows(db, inspectEndpoint, lower) {
638
597
  for (const a of state.acts)
639
598
  if (a.kind === 'say')
640
599
  d.audience(a.index);
641
- const standingCount = d.roster().filter(p => p.joined).length;
642
- if (lower === undefined)
643
- return { standingCount };
644
- const rebuilt = new MemoryDatabase(':memory:');
645
- try {
646
- initializeSqlite(rebuilt, state, d.header().revision, lower);
647
- const retainName = rebuilt.prepare('UPDATE participants SET name_json=? WHERE name_key_json=?');
648
- for (const p of d.roster())
649
- retainName.run(encode(p.name), key(p.name));
650
- for (const table of ['participants', 'attention', 'landed_audience']) {
651
- const original = db.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
652
- const expected = rebuilt.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
653
- requireFact(stableJson(original) === stableJson(expected), `derived ${table} index is inconsistent.`);
654
- }
655
- // Only reason-bearing Agora content replays from explicit state. Native Channel messages
656
- // and their unfenced addresses are not restorable from Agora activities; a null
657
- // generation stays unfenced here even though current landing would fence a new address.
658
- const upper = 'JOIN activities a ON a.message_index=ma.msg_index JOIN landed_audience d ON d.act_index=a.act_index';
659
- for (const [table, select] of [
660
- ['messages', 'SELECT DISTINCT m.* FROM messages m JOIN activities a ON a.message_index=m.msg_index JOIN landed_audience d ON d.act_index=a.act_index'],
661
- ['message_addresses', `SELECT DISTINCT ma.* FROM message_addresses ma ${upper} AND d.recipient_key_json=ma.recipient_key_json`],
662
- ]) {
663
- const original = db.prepare(`${select} ORDER BY 1,2`).all();
664
- const expected = rebuilt.prepare(`${select} ORDER BY 1,2`).all();
665
- const comparable = (rows) => table === 'message_addresses'
666
- ? rows.map(({ generation_json: _generation, recipient_json: _recipient, ...row }) => row) : rows;
667
- requireFact(stableJson(comparable(original)) === stableJson(comparable(expected)), `derived ${table} index is inconsistent.`);
668
- }
669
- const actualAddresses = db.prepare("SELECT a.message_index AS msg_index,ma.recipient_key_json,ma.recipient_order FROM activities a JOIN message_addresses ma ON ma.msg_index=a.message_index WHERE a.kind='say' AND EXISTS (SELECT 1 FROM landed_audience d WHERE d.act_index=a.act_index) ORDER BY 1,2").all();
670
- const expectedAddresses = db.prepare("SELECT a.message_index AS msg_index,d.recipient_key_json,d.recipient_order FROM activities a JOIN landed_audience d ON d.act_index=a.act_index WHERE a.kind='say' ORDER BY 1,2").all();
671
- requireFact(stableJson(actualAddresses) === stableJson(expectedAddresses), 'derived message address identity/order is inconsistent.');
672
- return { standingCount };
673
- }
674
- finally {
675
- rebuilt.close();
676
- }
600
+ return { standingCount: d.roster().filter(p => p.joined).length };
677
601
  }