@astrosheep/square 0.3.106 → 0.4.1

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 (206) 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 +6 -12
  5. package/dist/artifact.js +43 -344
  6. package/dist/attention-presentation.d.ts +13 -7
  7. package/dist/attention-presentation.js +29 -68
  8. package/dist/boundary-presentation.d.ts +3 -16
  9. package/dist/boundary-presentation.js +29 -109
  10. package/dist/card-presentation.d.ts +1 -0
  11. package/dist/card-presentation.js +2 -0
  12. package/dist/catch-decisions.d.ts +2 -2
  13. package/dist/catch-decisions.js +7 -3
  14. package/dist/catch-selection.d.ts +8 -0
  15. package/dist/catch-selection.js +5 -0
  16. package/dist/claude-delivery.d.ts +14 -12
  17. package/dist/claude-delivery.js +42 -142
  18. package/dist/claude-mcp-context.js +2 -2
  19. package/dist/claude-mod.js +28 -140
  20. package/dist/claude-transport.d.ts +5 -0
  21. package/dist/claude-transport.js +34 -0
  22. package/dist/cli/context.d.ts +2 -2
  23. package/dist/cli/context.js +7 -10
  24. package/dist/cli/maintenance-commands.d.ts +0 -4
  25. package/dist/cli/maintenance-commands.js +2 -29
  26. package/dist/cli/observation-commands.d.ts +2 -0
  27. package/dist/cli/observation-commands.js +12 -2
  28. package/dist/cli/program.js +1 -1
  29. package/dist/cli/registry.js +4 -3
  30. package/dist/codex-address.d.ts +2 -2
  31. package/dist/codex-boundary-state.d.ts +1 -5
  32. package/dist/codex-boundary-state.js +0 -4
  33. package/dist/codex-hook.d.ts +7 -10
  34. package/dist/codex-hook.js +69 -33
  35. package/dist/codex-queue.d.ts +10 -11
  36. package/dist/codex-queue.js +32 -34
  37. package/dist/codex-watch.d.ts +2 -0
  38. package/dist/codex-watch.js +100 -0
  39. package/dist/decisions.js +3 -3
  40. package/dist/delivery-deferral.d.ts +1 -19
  41. package/dist/delivery-deferral.js +1 -29
  42. package/dist/delivery-frame.d.ts +7 -0
  43. package/dist/delivery-frame.js +11 -0
  44. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  45. package/dist/delivery-ledger-sqlite.js +496 -0
  46. package/dist/delivery-ledger.d.ts +196 -0
  47. package/dist/delivery-ledger.js +268 -0
  48. package/dist/delivery-lifetime.d.ts +6 -0
  49. package/dist/delivery-lifetime.js +45 -0
  50. package/dist/harness-links.js +61 -7
  51. package/dist/harness.js +12 -17
  52. package/dist/help.js +3 -6
  53. package/dist/http-delivery.d.ts +5 -9
  54. package/dist/http-delivery.js +20 -79
  55. package/dist/inbox.d.ts +6 -57
  56. package/dist/inbox.js +24 -380
  57. package/dist/index.d.ts +7 -4
  58. package/dist/index.js +5 -2
  59. package/dist/mcp.d.ts +2 -1
  60. package/dist/mcp.js +40 -70
  61. package/dist/message.d.ts +7 -0
  62. package/dist/message.js +23 -0
  63. package/dist/model.d.ts +15 -91
  64. package/dist/model.js +2 -140
  65. package/dist/neutral.d.ts +72 -0
  66. package/dist/neutral.js +27 -0
  67. package/dist/notifications.d.ts +24 -26
  68. package/dist/notifications.js +39 -143
  69. package/dist/operation-control.d.ts +4 -0
  70. package/dist/operation-control.js +20 -0
  71. package/dist/operation-presentation.d.ts +1 -2
  72. package/dist/operation-presentation.js +1 -7
  73. package/dist/owner.d.ts +2 -2
  74. package/dist/owner.js +1 -1
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  77. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  78. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  79. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  80. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  81. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  82. package/dist/packages/agent-delivery/src/codex.js +41 -4
  83. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  84. package/dist/packages/agent-delivery/src/index.js +1 -1
  85. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  86. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  87. package/dist/paseo-delivery.d.ts +5 -5
  88. package/dist/paseo-delivery.js +19 -99
  89. package/dist/paseo.d.ts +1 -1
  90. package/dist/paseo.js +1 -1
  91. package/dist/plugins/opencode/index.d.ts +5 -3
  92. package/dist/plugins/opencode/index.js +99 -21
  93. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  94. package/dist/plugins/opencode-shared/delivery.js +109 -0
  95. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  96. package/dist/plugins/opencode-v1/index.js +41 -0
  97. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  98. package/dist/plugins/opencode-v1/runtime.js +152 -0
  99. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  100. package/dist/plugins/opencode-v1/transport.js +50 -0
  101. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  102. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  103. package/dist/ports.d.ts +33 -214
  104. package/dist/presentation.d.ts +5 -5
  105. package/dist/presentation.js +1 -0
  106. package/dist/runtime.js +2 -2
  107. package/dist/search.js +1 -1
  108. package/dist/session-lifecycle.d.ts +8 -7
  109. package/dist/session-lifecycle.js +40 -42
  110. package/dist/sqlite-framing.d.ts +8 -0
  111. package/dist/sqlite-framing.js +31 -0
  112. package/dist/sqlite-square.d.ts +16 -26
  113. package/dist/sqlite-square.js +199 -262
  114. package/dist/sqlite-values.d.ts +21 -0
  115. package/dist/sqlite-values.js +78 -0
  116. package/dist/square-api.d.ts +1 -1
  117. package/dist/square-api.js +5 -8
  118. package/dist/square-core.d.ts +0 -1
  119. package/dist/square-domain.d.ts +12 -10
  120. package/dist/square-domain.js +105 -223
  121. package/dist/square-facade.d.ts +25 -76
  122. package/dist/square-location.d.ts +6 -1
  123. package/dist/square-location.js +1 -1
  124. package/dist/square-post.d.ts +53 -0
  125. package/dist/square-post.js +4 -0
  126. package/dist/square-posting.d.ts +13 -0
  127. package/dist/square-posting.js +174 -0
  128. package/dist/square-producer-sqlite.d.ts +11 -0
  129. package/dist/square-producer-sqlite.js +210 -0
  130. package/dist/square-receive.d.ts +2 -3
  131. package/dist/square-sqlite.d.ts +33 -42
  132. package/dist/square-sqlite.js +236 -768
  133. package/dist/square-tools.d.ts +8 -0
  134. package/dist/square-tools.js +39 -0
  135. package/dist/square-values.d.ts +27 -0
  136. package/dist/square-values.js +90 -0
  137. package/dist/views.d.ts +6 -6
  138. package/dist/views.js +14 -9
  139. package/dist/wake-sink.d.ts +4 -11
  140. package/dist/wake-sink.js +8 -32
  141. package/dist/wake-transports.d.ts +4 -0
  142. package/dist/wake-transports.js +11 -0
  143. package/examples/symmetric-domains.mjs +29 -249
  144. package/guides/change-waits.md +13 -53
  145. package/guides/claude-native.md +19 -172
  146. package/guides/codex-delivery.md +55 -78
  147. package/guides/delivery-primitives.md +48 -207
  148. package/guides/operation-cancellation.md +2 -5
  149. package/guides/participant-ownership.md +25 -89
  150. package/guides/pi-native-delivery.md +19 -56
  151. package/guides/pi-tools.md +6 -8
  152. package/guides/row-runtime-delivery-evidence.md +2 -0
  153. package/guides/row-storage-design.md +19 -44
  154. package/guides/square-api.md +113 -538
  155. package/package.json +8 -8
  156. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  157. package/plugins/claude/hooks/hooks.json +1 -1
  158. package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
  159. package/plugins/claude/types.d.ts +1 -1
  160. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  161. package/plugins/codex/hooks/hooks.json +11 -23
  162. package/plugins/opencode/index.ts +91 -23
  163. package/plugins/opencode-shared/delivery.ts +91 -0
  164. package/plugins/opencode-v1/index.ts +36 -0
  165. package/plugins/opencode-v1/runtime.ts +134 -0
  166. package/plugins/opencode-v1/transport.ts +41 -0
  167. package/plugins/paseo/OVERVIEW.md +5 -5
  168. package/plugins/paseo/README.md +7 -6
  169. package/plugins/paseo/client/markdown.tsx +5 -5
  170. package/plugins/paseo/client/square-message.tsx +28 -24
  171. package/plugins/paseo/client/transform.ts +8 -5
  172. package/plugins/paseo/shared/card-presentation.ts +16 -0
  173. package/plugins/paseo/shared/message.ts +6 -11
  174. package/plugins/pi/square-pi.js +167 -451
  175. package/dist/channel.d.ts +0 -74
  176. package/dist/channel.js +0 -92
  177. package/dist/claude-delivery-status.d.ts +0 -13
  178. package/dist/claude-delivery-status.js +0 -46
  179. package/dist/cmd/notify-once.d.ts +0 -2
  180. package/dist/cmd/notify-once.js +0 -29
  181. package/dist/delivery-attempts.d.ts +0 -52
  182. package/dist/delivery-attempts.js +0 -73
  183. package/dist/delivery-effects.d.ts +0 -139
  184. package/dist/delivery-effects.js +0 -163
  185. package/dist/delivery-health.d.ts +0 -51
  186. package/dist/delivery-health.js +0 -173
  187. package/dist/delivery-operations.d.ts +0 -9
  188. package/dist/delivery-operations.js +0 -168
  189. package/dist/delivery-primitives.d.ts +0 -5
  190. package/dist/delivery-primitives.js +0 -3
  191. package/dist/delivery.d.ts +0 -45
  192. package/dist/delivery.js +0 -106
  193. package/dist/plugins/opencode/delivery.d.ts +0 -19
  194. package/dist/plugins/opencode/delivery.js +0 -238
  195. package/dist/presentation-operations.d.ts +0 -18
  196. package/dist/presentation-operations.js +0 -86
  197. package/dist/square-index.d.ts +0 -16
  198. package/dist/square-index.js +0 -106
  199. package/dist/square-legacy-framing.d.ts +0 -11
  200. package/dist/square-legacy-framing.js +0 -64
  201. package/dist/square-migration-host-ledger.d.ts +0 -26
  202. package/dist/square-migration-host-ledger.js +0 -169
  203. package/dist/square-migration.d.ts +0 -30
  204. package/dist/square-migration.js +0 -599
  205. package/guides/migration.md +0 -156
  206. package/plugins/opencode/delivery.ts +0 -211
@@ -1,161 +1,44 @@
1
- import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
2
- import { SquareError, nameKey, isWakeRouteKind, validateOwner, validateActivityMetadata, validateActivityDelivery, activityMetadataHasContent } from './model.js';
3
- import { validateMetadataMatch, matchingDeferrals, validateDeferralId } from './delivery-deferral.js';
1
+ import { validateActivityMetadata, activityMetadataHasContent } from './model.js';
4
2
  import { deriveLanding, landingFacts } from './square-domain.js';
5
- import { claimAttempt, prepareAttempt, settleAttempt } from './delivery-attempts.js';
6
- import { legacyRowFramingIssue, legacyActivitySchema } from './square-legacy-framing.js';
7
- import { receive, recordConsumed } from './receive.js';
8
3
  import { formatActivityId } from './square-core.js';
9
4
  import { compileSearchPattern } from './search.js';
5
+ import { validateEndpointGeneration as endpointGeneration } from './message.js';
6
+ import { createProducerDriver, producerSchemaSql, initializeProducerSqlite, resetProducerSqlite } from './square-producer-sqlite.js';
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, pageRows, pageLimit } from './sqlite-values.js';
9
+ export { invalidArtifact } from './sqlite-values.js';
10
+ export { validateEndpointGeneration as endpointGeneration } from './message.js';
11
+ export const APPLICATION_ID = 0x53515245;
12
+ export const CURRENT_USER_VERSION = 12;
10
13
  export function emptyRuntimeState(nextActIndex = 0) {
11
14
  return { nextActIndex, observations: {}, leases: {} };
12
15
  }
13
- export const APPLICATION_ID = 0x53515245;
14
- const V1_USER_VERSION = 1;
15
- export const V2_USER_VERSION = 2;
16
- export const V3_USER_VERSION = 3;
17
- export const V4_USER_VERSION = 4;
18
- export const V5_USER_VERSION = 5;
19
- export const V6_USER_VERSION = 6;
20
- export const V7_USER_VERSION = 7;
21
- export const CURRENT_USER_VERSION = 8;
22
- export const V1_SNAPSHOT_TABLE = 'square_snapshot';
23
- const V1_SNAPSHOT_DDL = `CREATE TABLE ${V1_SNAPSHOT_TABLE} (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL CHECK(revision>=0), state TEXT NOT NULL)`;
24
- const V1_SNAPSHOT_COLUMNS = [
25
- { cid: 0, name: 'id', type: 'INTEGER', notnull: 0, dflt_value: null, pk: 1, hidden: 0 },
26
- { cid: 1, name: 'revision', type: 'INTEGER', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
27
- { cid: 2, name: 'state', type: 'TEXT', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
28
- ];
29
- /**
30
- * Read-only legacy framing: application id, format version, journal mode and the
31
- * exact owned snapshot schema. It decodes no snapshot payload, writes no pragma
32
- * and runs no DDL, so ordinary open can establish ownership before requiring
33
- * migration. Anything else is unrelated or unsupported, not repaired.
34
- */
35
- export function validateLegacyFraming(database, applicationId, location = '') {
36
- const identity = database.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
37
- if (identity.application_id !== applicationId)
38
- throw invalidArtifact('unrelated SQLite database.');
39
- if (identity.user_version !== V1_USER_VERSION)
40
- throw invalidArtifact('unsupported SQLite format version.');
41
- const journalMode = database.prepare('PRAGMA journal_mode').get().journal_mode;
42
- if (typeof journalMode !== 'string' || journalMode !== (location === ':memory:' ? 'memory' : 'delete'))
43
- throw invalidArtifact('unsupported journal mode.');
44
- const objects = database.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' ORDER BY type, name")
45
- .all();
46
- if (objects.length !== 1)
47
- throw invalidArtifact('unsupported snapshot schema.');
48
- const [object] = objects;
49
- if (object.type !== 'table' || object.name !== V1_SNAPSHOT_TABLE || object.tbl_name !== V1_SNAPSHOT_TABLE
50
- || typeof object.sql !== 'string' || normalizedLegacySql(object.sql) !== normalizedLegacySql(V1_SNAPSHOT_DDL))
51
- throw invalidArtifact('unsupported snapshot schema.');
52
- const columns = database.prepare('SELECT cid, name, type, "notnull", dflt_value, pk, hidden FROM pragma_table_xinfo(?) ORDER BY cid')
53
- .all(V1_SNAPSHOT_TABLE);
54
- if (columns.length !== V1_SNAPSHOT_COLUMNS.length || !columns.every((column, index) => {
55
- const expected = V1_SNAPSHOT_COLUMNS[index];
56
- return column.cid === expected.cid && column.name === expected.name && column.type === expected.type
57
- && column.notnull === expected.notnull && column.dflt_value === expected.dflt_value
58
- && column.pk === expected.pk && column.hidden === expected.hidden;
59
- }))
60
- throw invalidArtifact('unsupported snapshot schema.');
61
- return { applicationId, userVersion: V1_USER_VERSION, journalMode };
62
- }
63
- function normalizedLegacySql(sql) {
64
- return sql.trim().replace(/\s+/g, ' ').replace(/\s*(>=|[(),=])\s*/g, '$1');
65
- }
66
- const CURRENT_ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','read','listen','ignore','unlisten')";
67
- export const ACTIVITIES_TABLE_SQL = `CREATE TABLE activities (
68
- act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),
69
- at REAL NOT NULL,
70
- kind TEXT NOT NULL CHECK (${CURRENT_ACTIVITY_KIND_CHECK}),
71
- actor_json TEXT COLLATE BINARY NOT NULL,
72
- actor_key_json TEXT COLLATE BINARY NOT NULL,
73
- reach TEXT CHECK (reach = 'bell'),
74
- reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),
75
- payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
76
- CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
77
- );`;
78
- export const ACTIVITY_INDEXES_SQL = [
79
- 'CREATE INDEX activity_kind_index ON activities(kind, act_index)',
80
- 'CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index)',
81
- "CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say'",
82
- "CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'",
83
- "CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'",
84
- ];
85
- export const DELIVERY_DEFERRALS_SQL = `CREATE TABLE delivery_deferrals (
86
- participant_key_json TEXT COLLATE BINARY NOT NULL,
87
- participant_json TEXT NOT NULL,
88
- generation_json TEXT NOT NULL,
89
- deferral_id_json TEXT NOT NULL,
90
- match_json TEXT NOT NULL CHECK (json_valid(match_json)),
91
- expires_at REAL NOT NULL,
92
- PRIMARY KEY (participant_key_json,generation_json,deferral_id_json)
93
- ) WITHOUT ROWID;
94
- CREATE INDEX delivery_deferral_expiry ON delivery_deferrals(participant_key_json,generation_json,expires_at);`;
95
- const ATTACHMENT_COLUMN_SQL = "attachment TEXT NOT NULL DEFAULT 'attached' CHECK (attachment IN ('attached','detached'))";
96
- export const SCHEMA_SQL = `CREATE TABLE square_meta (
16
+ const ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','read','listen','ignore','unlisten')";
17
+ /** Square owns activity, audience, participant, and read-state facts. */
18
+ export const AGORA_SCHEMA_SQL = `CREATE TABLE square_meta (
97
19
  id INTEGER PRIMARY KEY CHECK (id = 1),
98
- revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),
99
- delivery_revision INTEGER NOT NULL CHECK (delivery_revision BETWEEN 0 AND 9007199254740991),
100
20
  next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),
101
21
  hard_cap INTEGER CHECK (hard_cap BETWEEN 1 AND 9007199254740991),
102
22
  throttle_per_minute INTEGER CHECK (throttle_per_minute BETWEEN 1 AND 9007199254740991),
103
23
  preamble_json TEXT NOT NULL CHECK (json_valid(preamble_json)),
104
24
  warmup_json TEXT NOT NULL CHECK (json_valid(warmup_json))
105
25
  );
106
- ${ACTIVITIES_TABLE_SQL}
107
- ${ACTIVITY_INDEXES_SQL.join(';\n')};
108
-
109
- CREATE TABLE receipts (
110
- owner_json TEXT COLLATE BINARY NOT NULL,
111
- act_index INTEGER NOT NULL CHECK (act_index BETWEEN 0 AND 9007199254740991),
112
- at REAL NOT NULL,
113
- PRIMARY KEY (owner_json, act_index)
114
- ) WITHOUT ROWID;
115
- CREATE INDEX receipt_owner_time ON receipts(owner_json, at);
116
- CREATE TABLE catch_leases (
117
- owner_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
118
- generation_json TEXT NOT NULL DEFAULT '""',
119
- lease_id_json TEXT NOT NULL,
120
- heartbeat_at REAL NOT NULL,
121
- expires_at REAL NOT NULL CHECK (expires_at >= heartbeat_at),
122
- filter_json TEXT CHECK (filter_json IS NULL OR json_valid(filter_json))
123
- ) WITHOUT ROWID;
124
-
125
- CREATE TABLE seats (
126
- participant_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
127
- participant_json TEXT NOT NULL,
128
- owner_json TEXT NOT NULL,
129
- generation_json TEXT NOT NULL,
130
- wake_address_json TEXT CHECK (wake_address_json IS NULL OR json_valid(wake_address_json)),
131
- ${ATTACHMENT_COLUMN_SQL}
132
- ) WITHOUT ROWID;
133
- CREATE INDEX seat_owner ON seats(owner_json);
134
- CREATE TABLE delivery_evidence (
135
- participant_key_json TEXT COLLATE BINARY NOT NULL,
136
- participant_json TEXT NOT NULL,
137
- generation_json TEXT NOT NULL,
138
- act_index INTEGER NOT NULL CHECK (act_index BETWEEN 0 AND 9007199254740991),
139
- kind TEXT NOT NULL CHECK (kind IN ('wake','presentation')),
140
- attempt_n INTEGER NOT NULL CHECK (attempt_n >= 0),
141
- outcome TEXT NOT NULL CHECK (outcome IN ('dispatching','accepted','failed','unknown','released','presented','clipped')),
26
+ CREATE TABLE activities (
27
+ act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),
142
28
  at REAL NOT NULL,
143
- expires_at REAL,
144
- claim_token_json TEXT,
29
+ kind TEXT NOT NULL CHECK (${ACTIVITY_KIND_CHECK}),
30
+ actor_json TEXT COLLATE BINARY NOT NULL,
31
+ actor_key_json TEXT COLLATE BINARY NOT NULL,
32
+ reach TEXT CHECK (reach = 'bell'),
33
+ reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),
145
34
  payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
146
- PRIMARY KEY (participant_key_json,generation_json,act_index,kind,attempt_n)
147
- ) WITHOUT ROWID;
148
- CREATE UNIQUE INDEX delivery_claim_token ON delivery_evidence(claim_token_json) WHERE claim_token_json IS NOT NULL;
149
- CREATE INDEX delivery_activity ON delivery_evidence(participant_key_json,generation_json,act_index,kind,attempt_n);
150
- CREATE TABLE delivery_cancellations (
151
- participant_key_json TEXT COLLATE BINARY NOT NULL,
152
- generation_json TEXT NOT NULL,
153
- through_index INTEGER NOT NULL CHECK (through_index BETWEEN 0 AND 9007199254740991),
154
- PRIMARY KEY (participant_key_json,generation_json)
155
- ) WITHOUT ROWID;
156
-
157
- ${DELIVERY_DEFERRALS_SQL}
158
-
35
+ CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
36
+ );
37
+ CREATE INDEX activity_kind_index ON activities(kind, act_index);
38
+ CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index);
39
+ CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';
40
+ CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';
41
+ CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell';
159
42
  CREATE TABLE participants (
160
43
  name_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
161
44
  name_json TEXT NOT NULL,
@@ -180,74 +63,49 @@ CREATE TABLE landed_audience (
180
63
  act_index INTEGER NOT NULL,
181
64
  recipient_key_json TEXT COLLATE BINARY NOT NULL,
182
65
  recipient_order INTEGER NOT NULL CHECK (recipient_order >= 0),
183
- route TEXT NOT NULL CHECK (route IN ('mention','reply','attention','bell')),
66
+ reason TEXT NOT NULL CHECK (reason IN ('mention','reply','attention','bell')),
67
+ owner_json TEXT,
68
+ generation_json TEXT,
184
69
  PRIMARY KEY (act_index, recipient_key_json),
185
70
  UNIQUE (act_index, recipient_order)
186
71
  ) WITHOUT ROWID;
187
- CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
188
- export const FORMAT7_SCHEMA_SQL = legacyActivitySchema(SCHEMA_SQL);
189
- export const FORMAT6_SCHEMA_SQL = FORMAT7_SCHEMA_SQL.replace(` owner_json TEXT NOT NULL,\n generation_json TEXT NOT NULL,`, ` channel_json TEXT NOT NULL,\n session_json TEXT NOT NULL,\n agent_json TEXT,\n generation_json TEXT NOT NULL,`).replace('CREATE INDEX seat_owner ON seats(owner_json);', 'CREATE INDEX seat_body ON seats(channel_json, session_json, agent_json);');
190
- export const FORMAT5_SCHEMA_SQL = FORMAT6_SCHEMA_SQL.replace(`,\n ${ATTACHMENT_COLUMN_SQL}`, '');
191
- export const FORMAT4_SCHEMA_SQL = FORMAT5_SCHEMA_SQL.replace(DELIVERY_DEFERRALS_SQL, '');
192
- const FORMAT7_SCHEMA = FORMAT7_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
193
- const FORMAT6_SCHEMA = FORMAT6_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
194
- const FORMAT5_SCHEMA = FORMAT5_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
195
- const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
196
- const FORMAT4_SCHEMA = FORMAT4_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
197
- const encode = JSON.stringify;
198
- const key = (name) => encode(nameKey(name));
199
- export function invalidArtifact(detail) {
200
- return new SquareError('invalid_args', `Invalid square artifact: ${detail}`);
201
- }
202
- function requireFact(ok, detail = 'row is malformed.') {
203
- if (!ok)
204
- throw invalidArtifact(detail);
205
- }
206
- function integer(value, minimum = 0) {
207
- requireFact(Number.isSafeInteger(value) && value >= minimum);
208
- return value;
209
- }
210
- function finite(value) {
211
- requireFact(typeof value === 'number' && Number.isFinite(value));
212
- return value;
213
- }
214
- function string(value, nonempty = false) {
215
- requireFact(typeof value === 'string' && (!nonempty || value.length > 0));
216
- return value;
217
- }
218
- function json(value) {
219
- try {
220
- return JSON.parse(string(value));
221
- }
222
- catch {
223
- throw invalidArtifact('row contains invalid JSON.');
224
- }
225
- }
226
- function object(value) {
227
- requireFact(typeof value === 'object' && value !== null && !Array.isArray(value));
228
- return value;
229
- }
230
- function shape(value, required, optional = []) {
231
- const result = object(value);
232
- requireFact(required.every(k => Object.hasOwn(result, k)) && Object.keys(result).every(k => required.includes(k) || optional.includes(k)));
233
- return result;
234
- }
235
- function strings(value) {
236
- requireFact(Array.isArray(value) && value.every(v => typeof v === 'string'));
237
- return value;
238
- }
239
- function decoded(value, nonempty = false) {
240
- const result = string(json(value), nonempty);
241
- requireFact(encode(result) === value, 'string encoding is malformed.');
242
- return result;
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;`;
87
+ export function initializeAgoraSqlite(db, configuration = {}) {
88
+ db.exec(AGORA_SCHEMA_SQL);
89
+ db.prepare('INSERT INTO square_meta(id,next_act_index,hard_cap,throttle_per_minute,preamble_json,warmup_json) VALUES(1,0,?,?,?,?)')
90
+ .run(configuration.hardCap ?? null, configuration.throttlePerMinute ?? null, JSON.stringify(configuration.preamble ?? []), JSON.stringify(configuration.warmup ?? []));
91
+ }
92
+ export function resetAgoraSqlite(db) {
93
+ for (const table of ['catch_leases', 'caught_activity', 'participant_seats', 'landed_audience', 'attention', 'participants', 'activities', 'square_meta'])
94
+ db.exec(`DROP TABLE ${table}`);
95
+ }
96
+ function validateSquareFraming(db, location = '') {
97
+ const identity = db.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
98
+ requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
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);
243
101
  }
244
- function immutable(value) {
245
- if (value !== null && typeof value === 'object') {
246
- for (const item of Object.values(value))
247
- immutable(item);
248
- Object.freeze(value);
249
- }
250
- return value;
102
+ /** Full Square framing and reset, including producer state. */
103
+ export function squareStoreFormat(resetStorage) {
104
+ return {
105
+ applicationId: APPLICATION_ID, userVersion: CURRENT_USER_VERSION, observerKey: 'square',
106
+ validate: (db, location) => validateSquareFraming(db, location),
107
+ reset(db) { resetProducerSqlite(db); resetAgoraSqlite(db); resetStorage(db); },
108
+ };
251
109
  }
252
110
  const kinds = ['join', 'done', 'say', 'read', 'listen', 'ignore', 'unlisten'];
253
111
  function head(row) {
@@ -259,9 +117,10 @@ function head(row) {
259
117
  if (row.reply_index !== null)
260
118
  requireFact(row.kind === 'say' && integer(row.reply_index) < index);
261
119
  return immutable({
262
- index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index })
120
+ index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index }),
263
121
  });
264
122
  }
123
+ /** Validate the complete persisted activity. */
265
124
  export function validateStoredActivity(value) {
266
125
  const a = object(value);
267
126
  integer(a.index);
@@ -271,20 +130,10 @@ export function validateStoredActivity(value) {
271
130
  string(a.actor, true);
272
131
  switch (a.kind) {
273
132
  case 'say':
274
- shape(a, [...common], ['body', 'mentions', 'reach', 'reply', 'metadata', 'delivery']);
275
- if (Object.hasOwn(a, 'delivery')) {
276
- requireFact(a.delivery !== undefined, 'say delivery is malformed.');
277
- try {
278
- validateActivityDelivery(a.delivery);
279
- }
280
- catch {
281
- throw invalidArtifact('say delivery is malformed.');
282
- }
283
- }
284
- if (Object.hasOwn(a, 'body'))
133
+ shape(a, [...common], ['mentions', 'reach', 'reply', 'body', 'metadata']);
134
+ if (a.body !== undefined)
285
135
  string(a.body);
286
- if (Object.hasOwn(a, 'metadata')) {
287
- requireFact(a.metadata !== undefined, 'say metadata is malformed.');
136
+ if (a.metadata !== undefined) {
288
137
  try {
289
138
  validateActivityMetadata(a.metadata);
290
139
  }
@@ -292,7 +141,7 @@ export function validateStoredActivity(value) {
292
141
  throw invalidArtifact('say metadata is malformed.');
293
142
  }
294
143
  }
295
- requireFact((Object.hasOwn(a, 'body') && typeof a.body === 'string' && a.body.trim() !== '') || activityMetadataHasContent(a.metadata), 'say must contain a non-empty body or non-empty metadata.');
144
+ requireFact((typeof a.body === 'string' && a.body.trim() !== '') || activityMetadataHasContent(a.metadata), 'say must contain a non-empty body or non-empty metadata.');
296
145
  if (Object.hasOwn(a, 'mentions'))
297
146
  strings(a.mentions);
298
147
  if (Object.hasOwn(a, 'reach'))
@@ -319,13 +168,25 @@ export function validateStoredActivity(value) {
319
168
  }
320
169
  return value;
321
170
  }
171
+ /** Validate complete activity content. */
172
+ export function validateHydratedActivity(value) {
173
+ const a = object(value);
174
+ if (a.kind !== 'say')
175
+ return validateStoredActivity(a);
176
+ return validateStoredActivity(a);
177
+ }
178
+ /** Decode an activity with its complete payload. */
322
179
  function activity(row) {
323
180
  const h = head(row), payload = object(json(row.payload_json));
324
181
  // Payload cannot shadow indexed structural columns.
325
- const allowed = h.kind === 'say' ? ['body', 'mentions', 'metadata', 'delivery'] : h.kind === 'done' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
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'] : [];
326
183
  requireFact(Object.keys(payload).every(k => allowed.includes(k)));
327
184
  return immutable(validateStoredActivity({ ...h, ...payload }));
328
185
  }
186
+ /** Activity and content are one stored fact. */
187
+ function activityWithContent(row) {
188
+ return activity(row);
189
+ }
329
190
  function participant(row) {
330
191
  const name = decoded(row.name_json, true);
331
192
  requireFact(row.name_key_json === key(name) && [0, 1].includes(row.joined) && [0, 1].includes(row.done) && !(row.joined && row.done));
@@ -357,135 +218,14 @@ export function validateWatchLease(value) {
357
218
  }
358
219
  return value;
359
220
  }
360
- // Sorted object keys express small-value structural equality, never whole-state comparison.
361
- function stableJson(value) {
362
- if (value === null || typeof value !== 'object') {
363
- requireFact(value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value), 'value contains a non-finite or unsupported JSON value.');
364
- return encode(value);
365
- }
366
- if (Array.isArray(value))
367
- return '[' + value.map(stableJson).join(',') + ']';
368
- return '{' + Object.keys(value).sort().map(k => encode(k) + ':' + stableJson(value[k])).join(',') + '}';
369
- }
370
- function activityId(value) {
371
- const parsed = typeof value === 'string' ? /^act\/(0|[1-9]\d*)$/.exec(value) : null;
372
- requireFact(parsed !== null, 'activity generation is malformed.');
373
- const index = Number(parsed[1]);
374
- requireFact(Number.isSafeInteger(index));
375
- return value;
376
- }
377
- function activityIndex(value) {
378
- const parsed = activityId(value);
379
- return integer(Number(parsed.slice(4)));
380
- }
381
- function wakeAddress(value) {
382
- const address = shape(value, ['kind', 'address']);
383
- requireFact(isWakeRouteKind(address.kind));
384
- const entries = object(address.address);
385
- requireFact(Object.values(entries).every((item) => typeof item === 'string'));
386
- stableJson(entries);
387
- return immutable({ kind: address.kind, address: entries });
388
- }
389
- function ownerFrom(row) {
390
- const owner = decoded(row.owner_json, true);
391
- validateOwner(owner);
392
- return owner;
393
- }
394
- function seat(row) {
395
- const participant = decoded(row.participant_json, true);
396
- requireFact(row.participant_key_json === key(participant));
397
- const generation = activityId(decoded(row.generation_json, true));
398
- const parsedWake = row.wake_address_json === null ? undefined : wakeAddress(json(row.wake_address_json));
399
- requireFact(row.attachment === 'attached' || row.attachment === 'detached', 'seat attachment is malformed.');
400
- requireFact(row.attachment !== 'detached' || parsedWake === undefined, 'a detached seat has a wake address.');
401
- return immutable({ participant, owner: ownerFrom(row), generation, attachment: row.attachment, ...(parsedWake === undefined ? {} : { wakeAddress: parsedWake }) });
402
- }
403
- function nativeCorrelation(value) {
404
- const item = shape(value, ['harness'], ['token', 'messageId', 'payloadHash']);
405
- requireFact(['claude', 'opencode', 'pi'].includes(string(item.harness)));
406
- for (const key of ['token', 'messageId', 'payloadHash'])
407
- if (Object.hasOwn(item, key))
408
- string(item[key], true);
409
- return immutable({ harness: item.harness,
410
- ...(item.token === undefined ? {} : { token: item.token }),
411
- ...(item.messageId === undefined ? {} : { messageId: item.messageId }),
412
- ...(item.payloadHash === undefined ? {} : { payloadHash: item.payloadHash }) });
413
- }
414
- function deliveryEvidence(row, payload = object(json(row.payload_json))) {
415
- const participant = decoded(row.participant_json, true);
416
- requireFact(row.participant_key_json === key(participant));
417
- const generation = activityId(decoded(row.generation_json, true));
418
- const activity = activityId(`act/${integer(row.act_index)}`);
419
- requireFact(['wake', 'presentation'].includes(string(row.kind)) && ['dispatching', 'accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(string(row.outcome)));
420
- requireFact(Number.isSafeInteger(row.attempt_n) && row.attempt_n >= 0);
421
- const nativeObservations = payload.nativeObservations === undefined ? undefined : (() => {
422
- requireFact(Array.isArray(payload.nativeObservations));
423
- return payload.nativeObservations.map((value) => {
424
- const item = shape(value, ['eventId', 'messageId', 'state', 'observedAt'], ['reason']);
425
- requireFact(['held', 'released', 'dropped'].includes(string(item.state)));
426
- return {
427
- eventId: string(item.eventId, true), messageId: string(item.messageId, true),
428
- state: item.state,
429
- ...(Object.hasOwn(item, 'reason') ? { reason: string(item.reason) } : {}), observedAt: finite(item.observedAt),
430
- };
431
- });
432
- })();
433
- return immutable({
434
- participant, generation, activity, kind: row.kind,
435
- outcome: row.outcome, attemptN: integer(row.attempt_n), at: finite(row.at),
436
- ...(row.expires_at === null ? {} : { expiresAt: finite(row.expires_at) }),
437
- ...(row.claim_token_json === null ? {} : { claimToken: decoded(row.claim_token_json, true) }),
438
- ...(payload.route === undefined ? {} : { route: wakeAddress(payload.route) }),
439
- ...(payload.signature === undefined ? {} : { signature: string(payload.signature) }),
440
- ...(payload.message === undefined ? {} : { message: string(payload.message) }),
441
- ...(payload.diagnostic === undefined ? {} : { diagnostic: payload.diagnostic }),
442
- ...(payload.correlation === undefined ? {} : { correlation: nativeCorrelation(payload.correlation) }),
443
- ...(nativeObservations === undefined ? {} : { nativeObservations }),
444
- });
445
- }
446
- function deliveryDeferral(row) {
447
- const participant = decoded(row.participant_json, true);
448
- requireFact(row.participant_key_json === key(participant));
449
- const generation = activityId(decoded(row.generation_json, true));
450
- const id = validateDeferralId(decoded(row.deferral_id_json, true));
451
- const match = validateMetadataMatch(json(row.match_json));
452
- requireFact(encode(match) === row.match_json, 'delivery deferral match encoding is malformed.');
453
- return immutable({ participant, generation, id, match, expiresAt: finite(row.expires_at) });
454
- }
455
- function normalizedSql(sql) { return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1'); }
456
- function validateOwnedSchema(db, expected, location) {
457
- const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
458
- requireFact(actual.length === expected.length && actual.every((sql, i) => sql === expected[i]), 'unsupported row schema.');
459
- requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
460
- }
461
- export function validateRowFraming(db, version, location = '') {
462
- if (version === V2_USER_VERSION || version === V3_USER_VERSION) {
463
- const issue = legacyRowFramingIssue(db, APPLICATION_ID, version, location, FORMAT4_SCHEMA_SQL);
464
- if (issue !== undefined)
465
- throw invalidArtifact(issue);
466
- return;
467
- }
468
- const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
469
- requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
470
- requireFact(identity.user_version === version, 'unsupported SQLite format version.');
471
- validateOwnedSchema(db, version === V4_USER_VERSION ? FORMAT4_SCHEMA : version === V5_USER_VERSION ? FORMAT5_SCHEMA : version === V6_USER_VERSION ? FORMAT6_SCHEMA : version === V7_USER_VERSION ? FORMAT7_SCHEMA : EXPECTED_SCHEMA_SQL, location);
472
- }
473
- export function validateSqliteHeader(db, location = '') {
474
- const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
475
- requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
476
- if (identity.user_version === 1) {
477
- validateLegacyFraming(db, APPLICATION_ID, location);
478
- throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
479
- }
480
- if (identity.user_version === V2_USER_VERSION || identity.user_version === V3_USER_VERSION || identity.user_version === V4_USER_VERSION || identity.user_version === V5_USER_VERSION || identity.user_version === V6_USER_VERSION || identity.user_version === V7_USER_VERSION) {
481
- validateRowFraming(db, identity.user_version, location);
482
- throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
483
- }
484
- if (identity.user_version === CURRENT_USER_VERSION) {
485
- validateRowFraming(db, CURRENT_USER_VERSION, location);
486
- return createSqliteDriver(db).header();
487
- }
488
- throw invalidArtifact('unsupported SQLite format version.');
221
+ export function readSquareHeader(db) {
222
+ const rows = db.prepare('SELECT m.*, (SELECT COALESCE(MAX(act_index),-1) FROM activities) AS last_act_index FROM square_meta m').all();
223
+ requireFact(rows.length === 1 && rows[0].id === 1, 'header row is malformed.');
224
+ const r = rows[0], nextActIndex = integer(r.next_act_index), lastActIndex = integer(r.last_act_index, -1);
225
+ requireFact(nextActIndex > lastActIndex, 'nextActIndex is behind the activity history.');
226
+ return immutable({ revision: readStorageRevision(db), deliveryRevision: readStorageRevision(db, true), nextActIndex, lastActIndex,
227
+ hardCap: r.hard_cap === null ? null : integer(r.hard_cap, 1), ...(r.throttle_per_minute === null ? {} : { throttlePerMinute: integer(r.throttle_per_minute, 1) }),
228
+ preamble: strings(json(r.preamble_json)), warmup: strings(json(r.warmup_json)) });
489
229
  }
490
230
  function activitySelection(filter, alias = 'a') {
491
231
  const terms = [], args = [];
@@ -513,7 +253,7 @@ function activitySelection(filter, alias = 'a') {
513
253
  terms.push(`NOT EXISTS (
514
254
  SELECT 1 FROM json_each(?) field WHERE NOT EXISTS (
515
255
  WITH RECURSIVE walk(value,type,depth) AS (
516
- SELECT json_extract(${alias}.payload_json,'$.metadata'),json_type(${alias}.payload_json,'$.metadata'),0
256
+ SELECT json_extract(${alias}.payload_json,'$.metadata'), json_type(${alias}.payload_json,'$.metadata'),0
517
257
  UNION ALL
518
258
  SELECT child.value,child.type,walk.depth+1
519
259
  FROM walk JOIN json_each(CASE WHEN walk.type='object' THEN walk.value ELSE '{}' END) child
@@ -529,134 +269,89 @@ function activitySelection(filter, alias = 'a') {
529
269
  args.push(encode(filter.metadataMatch.fields));
530
270
  }
531
271
  if (filter.search !== undefined) {
532
- terms.push(`square_search(?,?,${alias}.act_index,${alias}.actor_json,json_extract(${alias}.payload_json,'$.body'))=1`);
272
+ terms.push(`square_search(?,?,'act/'||${alias}.act_index,${alias}.actor_json,json_extract(${alias}.payload_json,'$.body'))=1`);
533
273
  args.push(filter.search.mode, filter.search.text);
534
274
  }
535
275
  return { sql: terms.length ? terms.join(' AND ') : '1', args };
536
276
  }
537
- function pageRows(rows, page, decode) {
538
- const hasMore = rows.length > page.limit;
539
- if (hasMore)
540
- rows.pop();
541
- if (page.edge === 'newest')
542
- rows.reverse();
543
- return immutable({ items: rows.map(decode), hasMore });
544
- }
545
- function pageLimit(page) {
546
- requireFact(page.edge === 'oldest' || page.edge === 'newest');
547
- return Math.min(integer(page.limit, 1) + 1, Number.MAX_SAFE_INTEGER);
548
- }
549
277
  export function createSqliteDriver(db) {
550
- let changed = false, deliveryChanged = false;
278
+ let changed = false;
551
279
  const all = (sql, args = []) => db.prepare(sql).all(...args);
552
280
  const one = (sql, args = []) => db.prepare(sql).get(...args);
553
281
  const run = (sql, args = []) => Number(db.prepare(sql).run(...args).changes);
554
282
  let searchRegistered = false;
283
+ function ensureSearch() {
284
+ if (searchRegistered)
285
+ return;
286
+ const patterns = new Map();
287
+ db.function('square_search', { deterministic: true }, (mode, text, index, actor, body) => {
288
+ const cacheKey = encode([mode, text]);
289
+ let pattern = patterns.get(cacheKey);
290
+ if (pattern === undefined) {
291
+ pattern = compileSearchPattern(string(text), mode === 'literal');
292
+ patterns.set(cacheKey, pattern);
293
+ }
294
+ return Number([string(index), decoded(actor, true), body === null ? '' : string(body)]
295
+ .some(field => pattern.test(field)));
296
+ });
297
+ searchRegistered = true;
298
+ }
555
299
  function selectActivity(filter) {
556
- if (filter.search !== undefined && !searchRegistered) {
557
- const patterns = new Map();
558
- db.function('square_search', { deterministic: true }, (mode, text, index, actor, body) => {
559
- const key = encode([mode, text]);
560
- let pattern = patterns.get(key);
561
- if (pattern === undefined) {
562
- pattern = compileSearchPattern(string(text), mode === 'literal');
563
- patterns.set(key, pattern);
564
- }
565
- return Number([formatActivityId(integer(index)), decoded(actor, true), body === null ? '' : string(body)]
566
- .some(field => pattern.test(field)));
567
- });
568
- searchRegistered = true;
569
- }
300
+ if (filter.search !== undefined)
301
+ ensureSearch();
570
302
  return activitySelection(filter);
571
303
  }
572
- const owner = (requested) => read.participant(requested)?.name ?? requested;
573
- const attentionRows = (condition, args) => all(`SELECT t.*,p.name_json FROM attention t JOIN participants p ON p.name_key_json=t.listener_key_json WHERE ${condition} ORDER BY t.since_index`, args).map(attention);
304
+ const read = {};
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
+ };
574
312
  function directedSelection(recipient, filter) {
575
- const p = read.participant(recipient), base = selectActivity({ ...filter, actors: filter.senders });
313
+ const participant = read.participant(recipient), base = selectActivity({ ...filter, actors: filter.senders });
576
314
  base.sql += ' AND d.recipient_key_json=?';
577
- base.args.push(p ? key(p.name) : '');
315
+ base.args.push(participant ? key(participant.name) : '');
316
+ if (filter.currentGeneration) {
317
+ base.sql += ' AND (d.generation_json IS NULL OR d.generation_json=?)';
318
+ base.args.push(participant ? encode(currentEndpoint(participant.name)?.generation ?? '') : '');
319
+ }
578
320
  if (filter.unseenOnly) {
579
- base.sql += ' AND NOT EXISTS (SELECT 1 FROM receipts r WHERE r.owner_json=? AND r.act_index=a.act_index)';
580
- base.args.push(encode(p?.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));
581
323
  }
582
324
  if (filter.attentionOnly)
583
- base.sql += " AND d.route IN ('mention','reply','bell')";
584
- return base;
585
- }
586
- function directedHead(row, recipient) {
587
- const h = head(row);
588
- requireFact(h.kind === 'say');
589
- return immutable({
590
- index: h.index, at: h.at, actor: h.actor, recipient, route: routeKind(row.route)
591
- });
325
+ base.sql += " AND d.reason IN ('mention','reply','bell')";
326
+ return { base, participant };
592
327
  }
593
- const directedFrom = 'landed_audience d JOIN activities a ON a.act_index=d.act_index';
594
- const pendingFrom = `landed_audience d JOIN activities a ON a.act_index=d.act_index JOIN participants p ON p.name_key_json=d.recipient_key_json`;
595
- const pendingWhere = `p.joined=1 AND d.act_index>COALESCE(p.last_join_index,-1) AND NOT EXISTS (SELECT 1 FROM receipts r WHERE r.owner_json=p.name_json AND r.act_index=d.act_index)`;
596
- const latestSeats = `seats s JOIN participants p ON p.name_key_json=s.participant_key_json
597
- JOIN activities a ON a.act_index=p.last_join_index AND a.kind='join' AND a.actor_key_json=p.name_key_json
598
- AND s.generation_json='"act/'||a.act_index||'"'`;
599
- function ownerSeats(owner) {
600
- validateOwner(owner);
601
- return immutable(all(`SELECT s.* FROM ${latestSeats} WHERE p.joined=1 AND s.owner_json=? ORDER BY s.participant_key_json`, [encode(owner)]).map(seat));
602
- }
603
- const read = {
604
- seat(name) {
605
- const r = one(`SELECT s.* FROM ${latestSeats} WHERE p.joined=1 AND s.participant_key_json=?`, [key(name)]);
606
- return r ? seat(r) : undefined;
607
- },
608
- seatsForOwner: ownerSeats,
609
- deliveryEvidence(participant, generation, activityIndex) {
610
- const args = [key(participant), encode(generation)];
611
- const filter = activityIndex === undefined ? '' : ' AND act_index=?';
612
- if (activityIndex !== undefined)
613
- args.push(integer(Number(activityIndex.slice(4))));
614
- return immutable(all(`SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=?${filter} ORDER BY act_index,kind,attempt_n`, args).map(row => deliveryEvidence(row)));
615
- },
616
- deliveryByToken(claimToken) {
617
- const row = one('SELECT * FROM delivery_evidence WHERE claim_token_json=?', [encode(claimToken)]);
618
- return row === undefined ? undefined : deliveryEvidence(row);
619
- },
620
- cancelledThrough(participant, generation) {
621
- const r = one('SELECT through_index FROM delivery_cancellations WHERE participant_key_json=? AND generation_json=?', [key(participant), encode(generation)]);
622
- return r === undefined ? undefined : integer(r.through_index);
623
- },
624
- header() {
625
- const rows = all('SELECT m.*, (SELECT COALESCE(MAX(act_index),-1) FROM activities) AS last_act_index FROM square_meta m');
626
- requireFact(rows.length === 1 && rows[0].id === 1, 'header row is malformed.');
627
- const r = rows[0], nextActIndex = integer(r.next_act_index), lastActIndex = integer(r.last_act_index, -1);
628
- requireFact(nextActIndex > lastActIndex, 'nextActIndex is behind the activity history.');
629
- return immutable({
630
- revision: integer(r.revision), deliveryRevision: integer(r.delivery_revision), nextActIndex, lastActIndex, hardCap: r.hard_cap === null ? null : integer(r.hard_cap, 1), ...(r.throttle_per_minute === null ? {} : { throttlePerMinute: integer(r.throttle_per_minute, 1) }), preamble: strings(json(r.preamble_json)), warmup: strings(json(r.warmup_json))
631
- });
632
- },
633
- participant(name) {
634
- const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]);
635
- return r ? participant(r) : undefined;
636
- },
328
+ // ----- Upper Agora read capabilities -----
329
+ const readMethods = {
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)))),
333
+ header() { return readSquareHeader(db); },
334
+ participant(name) { const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]); return r ? participant(r) : undefined; },
637
335
  participantCounts() {
638
336
  const r = one('SELECT COUNT(*) AS known, COALESCE(SUM(joined),0) AS joined, COALESCE(SUM(done),0) AS done FROM participants');
639
337
  return immutable({ known: integer(r.known), joined: integer(r.joined), done: integer(r.done) });
640
338
  },
641
339
  roster: () => immutable(all('SELECT * FROM participants ORDER BY first_index').map(participant)),
642
- attention: name => immutable(attentionRows('t.listener_key_json=?', [key(name)])),
643
- incomingAttention: name => immutable(attentionRows('t.target_key_json=?', [key(name)])),
340
+ attention: name => immutable(all('SELECT t.*,p.name_json FROM attention t JOIN participants p ON p.name_key_json=t.listener_key_json WHERE t.listener_key_json=? ORDER BY t.since_index', [key(name)]).map(attention)),
341
+ incomingAttention: name => immutable(all('SELECT t.*,p.name_json FROM attention t JOIN participants p ON p.name_key_json=t.listener_key_json WHERE t.target_key_json=? ORDER BY t.since_index', [key(name)]).map(attention)),
644
342
  activity(index) {
645
343
  const r = one('SELECT * FROM activities WHERE act_index=?', [integer(index)]);
646
344
  return r ? activity(r) : undefined;
647
345
  },
648
346
  activityHeads(filter, page) {
649
347
  const s = selectActivity(filter), limit = pageLimit(page);
650
- return pageRows(all(`SELECT a.act_index,a.at,a.kind,a.actor_json,a.actor_key_json,a.reach,a.reply_index FROM activities a 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);
651
349
  },
652
350
  activities(indexes) {
653
351
  indexes.forEach(v => integer(v));
654
- return immutable(all('SELECT * FROM activities WHERE act_index IN (SELECT value FROM json_each(?)) ORDER BY act_index', [encode(indexes)]).map(activity));
655
- },
656
- activityCount(filter) {
657
- const s = selectActivity(filter);
658
- return integer(one(`SELECT COUNT(*) AS n FROM activities a WHERE ${s.sql}`, s.args).n);
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));
659
353
  },
354
+ activityCount(filter) { const s = selectActivity(filter); return integer(one(`SELECT COUNT(*) AS n FROM activities a WHERE ${s.sql}`, s.args).n); },
660
355
  contextIndexes(centers, before, after) {
661
356
  integer(before);
662
357
  integer(after);
@@ -676,7 +371,6 @@ export function createSqliteDriver(db) {
676
371
  sayOrdinals(indexes) {
677
372
  indexes.forEach(v => integer(v));
678
373
  const result = {};
679
- // Only an actor's latest say can use its authoritative lifetime count.
680
374
  for (const r of all(`SELECT a.act_index, CASE
681
375
  WHEN p.say_count IS NOT NULL AND a.act_index=(SELECT MAX(s.act_index) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json)
682
376
  THEN p.say_count
@@ -699,131 +393,67 @@ export function createSqliteDriver(db) {
699
393
  args.push(integer(take));
700
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());
701
395
  },
702
- audience(index) { return immutable(all('SELECT p.name_json,d.route 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.route) }))); },
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) }) }))); },
703
397
  directed(recipient, filter, page) {
704
- const s = directedSelection(recipient, filter), limit = pageLimit(page), canonical = owner(recipient);
705
- return pageRows(all(`SELECT a.act_index,a.at,a.kind,a.actor_json,a.actor_key_json,a.reach,a.reply_index,d.route FROM ${directedFrom} WHERE ${s.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...s.args, limit]), page, r => directedHead(r, canonical));
398
+ const { base, participant: p } = directedSelection(recipient, filter), limit = pageLimit(page);
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]);
400
+ return pageRows(rows, page, r => {
401
+ const h = head(r);
402
+ return { index: h.index, at: h.at, actor: h.actor, recipient: p?.name ?? recipient,
403
+ route: routeKind(r.reason),
404
+ ...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }) };
405
+ });
706
406
  },
707
407
  directedAt(recipient, index) { return read.directed(recipient, { afterIndex: integer(index) - 1, throughIndex: index }, { edge: 'oldest', limit: 1 }).items[0]; },
708
408
  directedCount(recipient, filter) {
709
- const s = directedSelection(recipient, filter);
710
- return integer(one(`SELECT COUNT(*) AS n FROM ${directedFrom} WHERE ${s.sql}`, s.args).n);
711
- },
712
- pendingAudience(after, through, limit) {
713
- const args = [integer(after, -1), integer(through, -1)], extra = pageLimit({ edge: 'oldest', limit });
714
- const indices = all(`SELECT DISTINCT d.act_index FROM ${pendingFrom} WHERE ${pendingWhere} AND d.act_index>? AND d.act_index<=? ORDER BY d.act_index LIMIT ?`, [...args, extra]);
715
- const hasMore = indices.length > limit;
716
- if (hasMore)
717
- indices.pop();
718
- const groups = indices.map(r => {
719
- const rows = all(`SELECT a.act_index,a.at,a.kind,a.actor_json,a.actor_key_json,a.reach,a.reply_index,p.name_json,d.route FROM ${pendingFrom} WHERE ${pendingWhere} AND d.act_index=? ORDER BY d.recipient_order`, [integer(r.act_index)]);
720
- const h = head(rows[0]);
721
- requireFact(h.kind === 'say');
722
- return {
723
- index: h.index, at: h.at, actor: h.actor, recipients: rows.map(v => ({ recipient: decoded(v.name_json, true), route: routeKind(v.route) }))
724
- };
725
- });
726
- return immutable({ groups, hasMore });
409
+ const { base } = directedSelection(recipient, filter);
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);
727
411
  },
728
412
  receipt(name, index) {
729
- const r = one('SELECT at FROM receipts WHERE owner_json=? AND 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)]);
730
414
  return r ? immutable({ at: finite(r.at) }) : undefined;
731
415
  },
732
416
  receiptFacts(name, afterIndex) {
733
- const k = encode(owner(name)), r = one('SELECT MAX(at) AS latest FROM receipts WHERE owner_json=?', [k]), e = one('SELECT MAX(r.act_index) AS latest FROM receipts r JOIN activities a ON a.act_index=r.act_index WHERE r.owner_json=? AND r.act_index>? AND a.actor_json IS NOT NULL', [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)]);
734
420
  return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorActivityIndex: integer(e.latest) }) });
735
421
  },
736
- deliveryDeferrals(name, generation) {
737
- const canonical = owner(name);
738
- const current = read.seat(canonical);
739
- if (current === undefined || current.generation !== generation)
740
- return [];
741
- return immutable(all('SELECT * FROM delivery_deferrals WHERE participant_key_json=? AND generation_json=? ORDER BY expires_at,deferral_id_json', [key(canonical), encode(generation)]).map(deliveryDeferral));
742
- },
743
422
  lease(name) {
744
- const canonical = owner(name), current = read.seat(canonical);
745
- const r = current === undefined
746
- ? one('SELECT * FROM catch_leases WHERE owner_json=?', [encode(canonical)])
747
- : one('SELECT * FROM catch_leases WHERE owner_json=? AND (generation_json=? OR generation_json=?)', [encode(canonical), encode(current.generation), encode('')]);
748
- return r ? immutable(validateWatchLease({
749
- leaseId: decoded(r.lease_id_json, true), heartbeatAt: finite(r.heartbeat_at), expiresAt: finite(r.expires_at), ...(r.filter_json === null ? {} : { filter: json(r.filter_json) })
750
- })) : undefined;
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;
751
425
  },
752
426
  };
427
+ Object.assign(read, readMethods);
428
+ // ----- Upper Agora writes / landing -----
753
429
  const writes = {
754
- storeSeat(value) {
755
- validateOwner(value.owner);
756
- activityId(value.generation);
757
- string(value.participant, true);
758
- const existing = read.seat(value.participant);
759
- if (existing !== undefined && stableJson(existing) === stableJson(value))
760
- return false;
761
- const wake = value.wakeAddress === undefined ? null : encode(value.wakeAddress);
762
- run('INSERT INTO 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), wake, value.attachment]);
763
- run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
764
- run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(value.participant)]);
765
- deliveryChanged = true;
766
- return true;
767
- },
768
- updateWakeAddress(value, wakeAddress) {
769
- const current = one('SELECT wake_address_json FROM seats WHERE participant_key_json=? AND generation_json=?', [key(value.participant), encode(value.generation)]);
770
- if (wakeAddress !== undefined) {
771
- requireFact(isWakeRouteKind(wakeAddress.kind));
772
- requireFact(Object.values(wakeAddress.address).every((item) => typeof item === 'string'));
773
- }
774
- if (stableJson(current.wake_address_json === null ? null : json(current.wake_address_json)) === stableJson(wakeAddress ?? null))
775
- return false;
776
- const count = run('UPDATE seats SET wake_address_json=? WHERE participant_key_json=? AND generation_json=?', [wakeAddress === undefined ? null : encode(wakeAddress), key(value.participant), encode(value.generation)]);
777
- if (count)
778
- deliveryChanged = true;
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;
779
435
  return count > 0;
780
436
  },
781
- reattachSeat(value, address) {
782
- const current = read.seat(value.participant);
783
- if (current?.attachment !== 'detached')
784
- return false;
785
- run("UPDATE seats SET attachment='attached',wake_address_json=? WHERE participant_key_json=? AND generation_json=?", [address === undefined ? null : encode(wakeAddress(address)), key(value.participant), encode(value.generation)]);
786
- deliveryChanged = true;
787
- return true;
788
- },
789
- detachSeat(value) {
790
- const count = run("UPDATE seats SET attachment='detached',wake_address_json=NULL WHERE participant_key_json=? AND generation_json=? AND attachment='attached'", [key(value.participant), encode(value.generation)]);
791
- if (!count)
792
- return false;
793
- run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
794
- run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND generation_json=?', [key(value.participant), encode(value.generation)]);
795
- deliveryChanged = true;
796
- return true;
797
- },
798
- removeSeat(value) {
799
- const row = one(`SELECT s.* FROM ${latestSeats} WHERE p.done=1 AND s.participant_key_json=?`, [key(value.participant)]);
800
- if (row === undefined || decoded(row.generation_json) !== value.generation || ownerFrom(row) !== value.owner)
801
- return false;
802
- run('DELETE FROM seats WHERE participant_key_json=?', [key(value.participant)]);
803
- run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
804
- run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(value.participant)]);
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]);
805
439
  changed = true;
806
- deliveryChanged = true;
807
440
  return true;
808
441
  },
809
- cancelThrough(value, through) {
810
- activityId(value.generation);
811
- integer(through);
812
- const prior = read.cancelledThrough(value.participant, value.generation) ?? -1;
813
- if (through <= prior)
814
- return false;
815
- run('INSERT INTO delivery_cancellations(participant_key_json,generation_json,through_index) VALUES(?,?,?) ON CONFLICT(participant_key_json,generation_json) DO UPDATE SET through_index=excluded.through_index', [key(value.participant), encode(value.generation), through]);
816
- deliveryChanged = true;
817
- return true;
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;
818
448
  },
819
449
  applyLanding(effect) {
820
450
  const a = validateStoredActivity(effect.activity);
821
451
  requireFact(a.index >= read.header().nextActIndex && a.index < Number.MAX_SAFE_INTEGER, 'activity index cannot advance.');
822
452
  const { index, at, kind, actor, reach, reply, ...payload } = a;
823
- receive({
824
- insertItem: () => { 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)]); },
825
- insertAddress: (item, address, order) => { run('INSERT INTO landed_audience(act_index,recipient_key_json,recipient_order,route) VALUES(?,?,?,?)', [item, key(address.recipient), order, address.via]); },
826
- }, { item: { key: index, data: a }, audience: effect.audience.map(r => ({ recipient: r.recipient, via: r.route })) });
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)]);
454
+ effect.audience.forEach((address, order) => {
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)]);
456
+ });
827
457
  if (effect.participant) {
828
458
  const p = effect.participant;
829
459
  string(p.name, true);
@@ -853,199 +483,69 @@ export function createSqliteDriver(db) {
853
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]);
854
484
  }
855
485
  run('UPDATE square_meta SET next_act_index=? WHERE id=1', [index + 1]);
486
+ if (kind === 'say')
487
+ producer.consumeActivity(index, at);
856
488
  changed = true;
857
489
  },
858
- markSeen(name, indexes, at) {
859
- finite(at);
860
- const next = read.header().nextActIndex;
861
- indexes.forEach(index => requireFact(integer(index) < next, 'receipt references an unassigned activity index.'));
862
- const canonical = owner(name);
863
- string(canonical, true);
864
- const count = recordConsumed({
865
- insertReceipt: (ref, time) => run('INSERT OR IGNORE INTO receipts(owner_json,act_index,at) VALUES(?,?,?)', [encode(ref.recipient), ref.item, time]) > 0,
866
- }, indexes.map(item => ({ recipient: canonical, item })), at);
867
- if (count)
868
- changed = true;
869
- return count;
870
- },
871
490
  putLease(name, value, generation) {
872
- const l = validateWatchLease(value), canonical = owner(name);
873
- const old = read.lease(canonical), row = one('SELECT generation_json FROM catch_leases WHERE owner_json=?', [encode(canonical)]);
874
- if (generation !== undefined)
875
- activityId(generation);
876
- if (row !== undefined && generation !== undefined && decoded(row.generation_json) !== '' && decoded(row.generation_json) !== generation)
877
- return false;
878
- if (old && stableJson(old) === stableJson(l) && (generation === undefined || row?.generation_json === encode(generation)))
491
+ const l = validateWatchLease(value), seat = currentEndpoint(name);
492
+ if (!seat || generation !== undefined && generation !== seat.generation)
879
493
  return false;
880
- string(canonical, true);
881
- run('INSERT INTO catch_leases(owner_json,generation_json,lease_id_json,heartbeat_at,expires_at,filter_json) VALUES(?,?,?,?,?,?) ON CONFLICT(owner_json) DO UPDATE SET generation_json=excluded.generation_json,lease_id_json=excluded.lease_id_json,heartbeat_at=excluded.heartbeat_at,expires_at=excluded.expires_at,filter_json=excluded.filter_json', [encode(canonical), encode(generation ?? ''), encode(l.leaseId), l.heartbeatAt, l.expiresAt, l.filter === undefined ? null : encode(l.filter)]);
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)]);
882
495
  changed = true;
883
496
  return true;
884
497
  },
885
- putDeliveryDeferral(seatRef, id, match, expiresAt, now) {
886
- finite(now);
887
- finite(expiresAt);
888
- const canonicalMatch = validateMetadataMatch(match);
889
- run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
890
- run('INSERT INTO delivery_deferrals(participant_key_json,participant_json,generation_json,deferral_id_json,match_json,expires_at) VALUES(?,?,?,?,?,?)', [key(seatRef.participant), encode(seatRef.participant), encode(seatRef.generation), encode(id), encode(canonicalMatch), expiresAt]);
891
- deliveryChanged = true;
892
- return true;
893
- },
894
- renewDeliveryDeferral(seatRef, id, expiresAt, now) {
895
- finite(now);
896
- finite(expiresAt);
897
- const pruned = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
898
- const count = run('UPDATE delivery_deferrals SET expires_at=? WHERE participant_key_json=? AND generation_json=? AND deferral_id_json=? AND expires_at>?', [expiresAt, key(seatRef.participant), encode(seatRef.generation), encode(id), now]);
899
- if (count || pruned)
900
- deliveryChanged = true;
901
- return count > 0;
902
- },
903
- releaseDeliveryDeferral(seatRef, id, now) {
904
- finite(now);
905
- const pruned = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
906
- const count = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND generation_json=? AND deferral_id_json=? AND expires_at>?', [key(seatRef.participant), encode(seatRef.generation), encode(id), now]);
907
- if (count || pruned)
908
- deliveryChanged = true;
909
- return count > 0;
910
- },
911
498
  removeLease(name, expected, generation) {
912
- const canonical = owner(name), row = one('SELECT generation_json FROM catch_leases WHERE owner_json=? AND lease_id_json=?', [encode(canonical), encode(expected)]);
913
- if (row === undefined || generation !== undefined && decoded(row.generation_json) !== generation)
499
+ if (read.lease(name)?.leaseId !== expected || generation !== undefined && currentEndpoint(name)?.generation !== generation)
914
500
  return false;
915
- const count = run('DELETE FROM catch_leases WHERE owner_json=? AND lease_id_json=? AND generation_json=?', [encode(canonical), encode(expected), row.generation_json]);
916
- if (count)
917
- changed = true;
501
+ const count = run('DELETE FROM catch_leases WHERE recipient_key_json=?', [key(name)]);
502
+ changed ||= count > 0;
918
503
  return count > 0;
919
504
  },
920
- };
921
- function attempt(row) {
922
- const data = object(json(row.payload_json)), evidence = deliveryEvidence(row, data);
923
- return { purpose: evidence.kind === 'wake' ? 'dispatch' : 'presentation', token: evidence.claimToken ?? '',
924
- ordinal: evidence.attemptN, outcome: evidence.outcome, at: evidence.at, expiresAt: evidence.expiresAt, data };
925
- }
926
- function attemptRows(seat, index, kind, known) {
927
- const scope = [key(seat.participant), encode(seat.generation), index, kind];
928
- let sources = [];
929
- return {
930
- list() {
931
- sources = all('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND act_index=? AND kind=? ORDER BY attempt_n DESC', scope);
932
- return sources.map(attempt);
933
- },
934
- evidence(value) {
935
- const source = sources.find(row => row.attempt_n === value.ordinal);
936
- return deliveryEvidence({ ...source, outcome: value.outcome, at: value.at, expires_at: value.expiresAt ?? null }, value.data);
937
- },
938
- byToken(token) {
939
- if (known?.token === token)
940
- return known;
941
- const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND act_index=? AND kind=? AND claim_token_json=?', [...scope, encode(token)]);
942
- return row === undefined ? undefined : attempt(row);
943
- },
944
- insert(value) {
945
- run('INSERT INTO delivery_evidence(participant_key_json,participant_json,generation_json,act_index,kind,attempt_n,outcome,at,expires_at,claim_token_json,payload_json) VALUES(?,?,?,?,?,?,?,?,?,?,?)', [scope[0], encode(seat.participant), scope[1], index, kind, value.ordinal, value.outcome, value.at, value.expiresAt ?? null, encode(value.token), encode(value.data)]);
946
- deliveryChanged = true;
947
- },
948
- replace(value) {
949
- const count = run('UPDATE delivery_evidence SET outcome=?,at=?,expires_at=?,payload_json=? WHERE participant_key_json=? AND generation_json=? AND act_index=? AND kind=? AND attempt_n=? AND claim_token_json IS ?', [value.outcome, value.at, value.expiresAt ?? null, encode(value.data), ...scope, value.ordinal, value.token === '' ? null : encode(value.token)]);
950
- if (count)
951
- deliveryChanged = true;
952
- },
953
- };
954
- }
955
- const delivery = {
956
- claimDelivery(input) {
957
- requireFact(input.kind === 'wake' || input.kind === 'presentation');
958
- const index = activityIndex(input.activity);
959
- finite(input.at);
960
- const leaseMs = integer(input.leaseMs, 1);
961
- string(input.claimToken, true);
962
- if (input.route !== undefined)
963
- wakeAddress(input.route);
964
- const currentSeat = read.seat(input.seat.participant);
965
- const item = read.activity(index);
966
- if (currentSeat === undefined || currentSeat.attachment !== 'attached' || currentSeat.generation !== input.seat.generation || currentSeat.owner !== input.seat.owner
967
- || item?.kind !== 'say' || read.directedAt(input.seat.participant, index) === undefined)
968
- return { status: 'stale' };
969
- if (matchingDeferrals(read.deliveryDeferrals(currentSeat.participant, currentSeat.generation), item.metadata, input.at).length > 0)
970
- return { status: 'deferred' };
971
- const cancelledThrough = read.cancelledThrough(input.seat.participant, input.seat.generation);
972
- if (cancelledThrough !== undefined && index <= cancelledThrough)
973
- return { status: 'stale' };
974
- const rows = attemptRows(input.seat, index, input.kind);
975
- const result = claimAttempt({ ...rows, replace(value) {
976
- rows.replace({ ...value, data: { ...value.data, diagnostic: 'interrupted dispatch' } });
977
- } }, { purpose: input.kind === 'wake' ? 'dispatch' : 'presentation', token: input.claimToken,
978
- at: input.at, leaseMs, data: input.route === undefined ? {} : { route: input.route } });
979
- if (result.status === 'acquired')
980
- return { status: 'acquired', claimToken: result.token, attemptN: result.ordinal };
981
- const evidence = result.interrupted ? { ...result.evidence, data: { ...result.evidence.data, diagnostic: 'interrupted dispatch' } } : result.evidence;
982
- return { status: result.status, evidence: rows.evidence(evidence) };
983
- },
984
- prepareDelivery(input) {
985
- const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND claim_token_json=?', [key(input.seat.participant), encode(input.seat.generation), encode(input.claimToken)]);
986
- if (row === undefined || row.outcome !== 'dispatching')
987
- return false;
988
- const previous = attempt(row), correlation = nativeCorrelation(input.correlation), payload = previous.data;
989
- if (stableJson(payload.correlation ?? null) === stableJson(correlation))
990
- return true;
991
- return prepareAttempt(attemptRows(input.seat, integer(row.act_index), row.kind, previous), { token: input.claimToken, data: { ...payload, correlation } });
992
- },
993
- completeDelivery(input) {
994
- const index = activityIndex(input.activity);
995
- finite(input.at);
996
- requireFact(['accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(input.outcome));
997
- string(input.claimToken, true);
998
- const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND act_index=? AND claim_token_json=?', [key(input.seat.participant), encode(input.seat.generation), index, encode(input.claimToken)]);
999
- if (row === undefined)
1000
- return false;
1001
- const previous = attempt(row);
1002
- const payload = { ...previous.data, ...(input.signature === undefined ? {} : { signature: input.signature }), ...(input.message === undefined ? {} : { message: input.message }), ...(input.diagnostic === undefined ? {} : { diagnostic: input.diagnostic }), ...(input.correlation === undefined ? {} : { correlation: input.correlation }) };
1003
- return settleAttempt(attemptRows(input.seat, index, row.kind, previous), { token: input.claimToken, outcome: input.outcome, at: input.at, data: payload });
1004
- },
1005
- observeNativeDelivery(input) {
1006
- const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND claim_token_json=?', [key(input.seat.participant), encode(input.seat.generation), encode(input.claimToken)]);
1007
- if (row === undefined || !['dispatching', 'accepted', 'unknown'].includes(string(row.outcome)))
1008
- return false;
1009
- const observation = input.observation;
1010
- requireFact(typeof observation.eventId === 'string' && observation.eventId.length > 0);
1011
- requireFact(typeof observation.messageId === 'string' && observation.messageId.length > 0);
1012
- requireFact(observation.state === 'held' || observation.state === 'released' || observation.state === 'dropped');
1013
- finite(observation.observedAt);
1014
- if (observation.reason !== undefined)
1015
- string(observation.reason);
1016
- const payload = object(json(row.payload_json));
1017
- const observations = payload.nativeObservations === undefined ? [] : payload.nativeObservations;
1018
- requireFact(Array.isArray(observations));
1019
- if (observations.some((item) => object(item).eventId === observation.eventId))
1020
- return true;
1021
- const next = [...observations, observation];
1022
- const count = run('UPDATE delivery_evidence SET payload_json=? WHERE participant_key_json=? AND generation_json=? AND claim_token_json=?', [encode({ ...payload, nativeObservations: next }), key(input.seat.participant), encode(input.seat.generation), encode(input.claimToken)]);
1023
- if (count)
1024
- deliveryChanged = true;
1025
- return count > 0;
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;
1026
512
  },
1027
513
  };
1028
- return Object.defineProperties(Object.assign(read, writes, delivery), {
1029
- changed: { get: () => changed },
1030
- deliveryChanged: { get: () => deliveryChanged },
514
+ function importLanding(activityValue) {
515
+ const validated = validateHydratedActivity(activityValue);
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);
523
+ }
524
+ return validated;
525
+ }
526
+ const driver = Object.defineProperties(Object.assign(read, writes, { importLanding }), {
527
+ changed: { get: () => changed || producer.changed },
528
+ deliveryChanged: { get: () => producer.deliveryChanged },
1031
529
  });
530
+ return driver;
1032
531
  }
1033
- /** Explicit import only. Caller owns transaction, legacy validation and format publication. */
1034
- export function initializeSqlite(db, state, revision = 0) {
532
+ /** Explicit import replay. Caller owns the transaction and format publication. */
533
+ export function initializeSqlite(db, state, revision) {
1035
534
  integer(revision);
1036
535
  integer(state.runtime.nextActIndex);
1037
536
  if (state.hardCap !== null)
1038
537
  integer(state.hardCap, 1);
1039
538
  if (state.throttlePerMinute !== undefined)
1040
539
  integer(state.throttlePerMinute, 1);
1041
- db.exec(SCHEMA_SQL);
1042
- db.prepare('INSERT INTO square_meta(id,revision,delivery_revision,next_act_index,hard_cap,throttle_per_minute,preamble_json,warmup_json) VALUES(1,?,0,0,?,?,?,?)').run(revision, state.hardCap, state.throttlePerMinute ?? null, encode(strings(state.preamble)), encode(strings(state.warmup)));
540
+ initializeStorageMeta(db, revision);
541
+ initializeProducerSqlite(db);
542
+ initializeAgoraSqlite(db, { ...state, preamble: strings(state.preamble), warmup: strings(state.warmup) });
1043
543
  const driver = createSqliteDriver(db);
1044
544
  for (const activity of state.acts)
1045
- driver.applyLanding(deriveLanding(validateStoredActivity(activity), landingFacts(driver, activity)));
545
+ driver.importLanding(activity);
1046
546
  requireFact(state.runtime.nextActIndex > driver.header().lastActIndex, 'nextActIndex is behind the activity history.');
1047
547
  db.prepare('UPDATE square_meta SET next_act_index=? WHERE id=1').run(state.runtime.nextActIndex);
1048
- const insert = db.prepare('INSERT INTO receipts(owner_json,act_index,at) VALUES(?,?,?)');
548
+ const insert = db.prepare('INSERT INTO caught_activity(recipient_key_json,act_index,at) VALUES(?,?,?)');
1049
549
  for (const [owner, observations] of Object.entries(state.runtime.observations)) {
1050
550
  string(owner, true);
1051
551
  for (const [id, observation] of Object.entries(observations)) {
@@ -1054,80 +554,48 @@ export function initializeSqlite(db, state, revision = 0) {
1054
554
  const index = integer(Number(match[1]));
1055
555
  requireFact(index < state.runtime.nextActIndex, 'receipt references an unassigned activity index.');
1056
556
  requireFact(observation.state === 'seen');
1057
- insert.run(encode(owner), index, finite(observation.at));
557
+ const activity = driver.activity(index);
558
+ if (activity?.kind !== 'say')
559
+ continue;
560
+ insert.run(key(owner), index, finite(observation.at));
1058
561
  }
1059
562
  }
1060
563
  for (const [owner, value] of Object.entries(state.runtime.leases)) {
1061
564
  string(owner, true);
1062
- const l = validateWatchLease(value);
1063
- db.prepare('INSERT INTO catch_leases(owner_json,lease_id_json,heartbeat_at,expires_at,filter_json) VALUES(?,?,?,?,?)').run(encode(owner), encode(l.leaseId), l.heartbeatAt, l.expiresAt, l.filter === undefined ? null : encode(l.filter));
565
+ const l = validateWatchLease(value), generation = driver.seat(owner)?.generation ?? 'detached';
566
+ db.prepare('INSERT INTO catch_leases(recipient_key_json,generation_json,lease_json) VALUES(?,?,?)').run(key(owner), encode(generation), encode(l));
1064
567
  }
1065
568
  }
1066
569
  export function exportSqliteState(db) {
1067
570
  const d = createSqliteDriver(db), h = d.header();
1068
571
  const observations = {}, leases = {};
1069
- for (const row of db.prepare('SELECT * FROM receipts').all()) {
1070
- const owner = decoded(row.owner_json, true), index = integer(row.act_index);
1071
- requireFact(index < h.nextActIndex, 'receipt references an unassigned activity index.');
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);
1072
574
  if (!Object.hasOwn(observations, owner))
1073
- Object.defineProperty(observations, owner, {
1074
- value: {}, enumerable: true, writable: true
1075
- });
1076
- observations[owner][`act/${index}`] = { state: 'seen', at: finite(row.at) };
575
+ Object.defineProperty(observations, owner, { value: {}, enumerable: true, writable: true });
576
+ observations[owner][formatActivityId(integer(row.act_index))] = { state: 'seen', at: finite(row.at) };
1077
577
  }
1078
- for (const row of db.prepare('SELECT * FROM catch_leases').all()) {
1079
- const owner = decoded(row.owner_json, true);
1080
- Object.defineProperty(leases, owner, { value: validateWatchLease({
1081
- leaseId: decoded(row.lease_id_json, true), heartbeatAt: finite(row.heartbeat_at), expiresAt: finite(row.expires_at), ...(row.filter_json === null ? {} : { filter: json(row.filter_json) })
1082
- }), 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 });
1083
580
  }
1084
581
  return {
1085
- hardCap: h.hardCap, ...(h.throttlePerMinute === undefined ? {} : { throttlePerMinute: h.throttlePerMinute }), preamble: [...h.preamble], warmup: [...h.warmup], acts: db.prepare('SELECT * FROM activities ORDER BY act_index').all().map(activity), runtime: {
1086
- nextActIndex: h.nextActIndex, observations, leases
1087
- }
582
+ hardCap: h.hardCap, ...(h.throttlePerMinute === undefined ? {} : { throttlePerMinute: h.throttlePerMinute }), preamble: [...h.preamble], warmup: [...h.warmup],
583
+ acts: db.prepare('SELECT * FROM activities ORDER BY act_index').all().map(activityWithContent),
584
+ runtime: { nextActIndex: h.nextActIndex, observations, leases }
1088
585
  };
1089
586
  }
1090
- /** Full read-only integrity inspection, including maintained-index replay comparison. */
587
+ /** Read-only integrity inspection. */
1091
588
  export function inspectSqlite(db) {
1092
- return inspectRows(db, seat);
1093
- }
1094
- function inspectRows(db, inspectSeat) {
1095
589
  const check = db.prepare('PRAGMA integrity_check').all();
1096
590
  requireFact(check.length === 1 && check[0].integrity_check === 'ok', 'SQLite integrity check failed.');
1097
591
  const state = exportSqliteState(db), d = createSqliteDriver(db);
1098
592
  d.roster();
1099
- for (const row of db.prepare('SELECT * FROM seats').all())
1100
- inspectSeat(row);
1101
- for (const row of db.prepare('SELECT * FROM delivery_evidence').all())
1102
- deliveryEvidence(row);
1103
- if (Number(db.prepare('PRAGMA user_version').get().user_version) >= V5_USER_VERSION) {
1104
- for (const row of db.prepare('SELECT * FROM delivery_deferrals').all())
1105
- deliveryDeferral(row);
1106
- }
1107
593
  for (const p of d.roster()) {
1108
594
  d.attention(p.name);
1109
595
  d.incomingAttention(p.name);
1110
596
  }
1111
597
  for (const a of state.acts)
1112
- d.audience(a.index);
1113
- // A transient memory database checks derived indices without altering authority.
1114
- const rebuilt = new MemoryDatabase(':memory:');
1115
- try {
1116
- initializeSqlite(rebuilt, state, d.header().revision);
1117
- // A migration may remove the activity that established canonical casing.
1118
- // Retained participant spelling stays authoritative while replay checks
1119
- // every derived lifecycle/count fact against surviving activities.
1120
- const retainName = rebuilt.prepare('UPDATE participants SET name_json=? WHERE name_key_json=?');
1121
- for (const p of d.roster())
1122
- retainName.run(encode(p.name), key(p.name));
1123
- for (const table of ['participants', 'attention', 'landed_audience']) {
1124
- const original = db.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
1125
- const expected = rebuilt.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
1126
- requireFact(stableJson(original) === stableJson(expected), `derived ${table} index is inconsistent.`);
1127
- }
1128
- return { standingCount: d.roster().filter(p => p.joined).length };
1129
- }
1130
- finally {
1131
- rebuilt.close();
1132
- }
598
+ if (a.kind === 'say')
599
+ d.audience(a.index);
600
+ return { standingCount: d.roster().filter(p => p.joined).length };
1133
601
  }