@astrosheep/square 0.3.106 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/activity-feed.js +1 -1
  2. package/dist/artifact-store.d.ts +41 -0
  3. package/dist/artifact-store.js +272 -0
  4. package/dist/artifact.d.ts +8 -12
  5. package/dist/artifact.js +66 -343
  6. package/dist/attention-presentation.d.ts +33 -7
  7. package/dist/attention-presentation.js +33 -69
  8. package/dist/boundary-presentation.d.ts +0 -2
  9. package/dist/boundary-presentation.js +17 -22
  10. package/dist/catch-decisions.d.ts +2 -2
  11. package/dist/catch-decisions.js +14 -4
  12. package/dist/catch-selection.d.ts +8 -0
  13. package/dist/catch-selection.js +5 -0
  14. package/dist/channel-artifact.d.ts +10 -0
  15. package/dist/channel-artifact.js +55 -0
  16. package/dist/channel-delivery.d.ts +11 -0
  17. package/dist/channel-delivery.js +51 -0
  18. package/dist/channel-domain.d.ts +3 -0
  19. package/dist/channel-domain.js +83 -0
  20. package/dist/channel-facade.d.ts +76 -0
  21. package/dist/channel-facade.js +1 -0
  22. package/dist/channel-internal.d.ts +126 -0
  23. package/dist/channel-internal.js +440 -0
  24. package/dist/channel-metadata.d.ts +11 -0
  25. package/dist/channel-metadata.js +1 -0
  26. package/dist/channel-ports.d.ts +241 -0
  27. package/dist/channel-ports.js +1 -0
  28. package/dist/channel-presentation.d.ts +17 -0
  29. package/dist/channel-presentation.js +22 -0
  30. package/dist/channel-sqlite-schema.d.ts +13 -0
  31. package/dist/channel-sqlite-schema.js +81 -0
  32. package/dist/channel-transport.d.ts +62 -0
  33. package/dist/channel-transport.js +1 -0
  34. package/dist/channel-values.d.ts +27 -0
  35. package/dist/channel-values.js +90 -0
  36. package/dist/channel.d.ts +31 -21
  37. package/dist/channel.js +42 -74
  38. package/dist/claude-delivery-status.js +1 -1
  39. package/dist/claude-delivery.d.ts +2 -5
  40. package/dist/claude-delivery.js +56 -103
  41. package/dist/claude-mod.js +4 -4
  42. package/dist/claude-transport.d.ts +5 -0
  43. package/dist/claude-transport.js +58 -0
  44. package/dist/cli/context.d.ts +2 -2
  45. package/dist/cli/context.js +0 -1
  46. package/dist/cli/maintenance-commands.d.ts +0 -4
  47. package/dist/cli/maintenance-commands.js +2 -29
  48. package/dist/cli/program.js +1 -1
  49. package/dist/cli/registry.js +1 -2
  50. package/dist/codex-address.d.ts +1 -1
  51. package/dist/codex-queue.d.ts +2 -2
  52. package/dist/codex-queue.js +1 -3
  53. package/dist/decisions.js +3 -3
  54. package/dist/delivery-deferral.d.ts +9 -18
  55. package/dist/delivery-deferral.js +4 -4
  56. package/dist/delivery-health.d.ts +4 -4
  57. package/dist/delivery-health.js +11 -11
  58. package/dist/delivery-operations.d.ts +15 -2
  59. package/dist/delivery-operations.js +60 -30
  60. package/dist/delivery.d.ts +44 -33
  61. package/dist/delivery.js +49 -75
  62. package/dist/help.js +1 -6
  63. package/dist/http-delivery.d.ts +2 -2
  64. package/dist/http-delivery.js +2 -4
  65. package/dist/inbox.d.ts +10 -18
  66. package/dist/inbox.js +44 -53
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/mcp.d.ts +2 -1
  70. package/dist/mcp.js +40 -70
  71. package/dist/message.d.ts +7 -0
  72. package/dist/message.js +23 -0
  73. package/dist/model.d.ts +41 -77
  74. package/dist/model.js +7 -139
  75. package/dist/neutral.d.ts +72 -0
  76. package/dist/neutral.js +27 -0
  77. package/dist/notifications.d.ts +2 -8
  78. package/dist/notifications.js +15 -50
  79. package/dist/operation-presentation.d.ts +1 -2
  80. package/dist/operation-presentation.js +1 -7
  81. package/dist/owner.d.ts +2 -2
  82. package/dist/owner.js +1 -1
  83. package/dist/paseo-delivery.d.ts +1 -1
  84. package/dist/paseo-delivery.js +2 -4
  85. package/dist/plugins/opencode/delivery.js +27 -42
  86. package/dist/ports.d.ts +46 -168
  87. package/dist/presentation-operations.d.ts +1 -14
  88. package/dist/presentation-operations.js +16 -16
  89. package/dist/presentation.d.ts +5 -5
  90. package/dist/presentation.js +1 -0
  91. package/dist/runtime.js +2 -2
  92. package/dist/search.js +1 -1
  93. package/dist/sqlite-channel.d.ts +15 -0
  94. package/dist/sqlite-channel.js +600 -0
  95. package/dist/sqlite-framing.d.ts +8 -0
  96. package/dist/sqlite-framing.js +31 -0
  97. package/dist/sqlite-square.d.ts +12 -13
  98. package/dist/sqlite-square.js +192 -248
  99. package/dist/sqlite-values.d.ts +21 -0
  100. package/dist/sqlite-values.js +78 -0
  101. package/dist/square-api.d.ts +1 -1
  102. package/dist/square-api.js +1 -1
  103. package/dist/square-core.d.ts +1 -1
  104. package/dist/square-domain.d.ts +21 -7
  105. package/dist/square-domain.js +93 -94
  106. package/dist/square-facade.d.ts +12 -66
  107. package/dist/square-index.d.ts +1 -1
  108. package/dist/square-index.js +1 -1
  109. package/dist/square-location.d.ts +6 -1
  110. package/dist/square-location.js +1 -1
  111. package/dist/square-receive.d.ts +2 -3
  112. package/dist/square-sqlite.d.ts +45 -44
  113. package/dist/square-sqlite.js +308 -764
  114. package/dist/square-tools.d.ts +8 -0
  115. package/dist/square-tools.js +39 -0
  116. package/dist/views.d.ts +6 -6
  117. package/dist/views.js +15 -9
  118. package/dist/wake-preview.d.ts +7 -0
  119. package/dist/wake-preview.js +15 -0
  120. package/dist/wake-sink.d.ts +2 -2
  121. package/dist/wake-sink.js +1 -1
  122. package/dist/wake-transports.d.ts +7 -0
  123. package/dist/wake-transports.js +42 -0
  124. package/guides/claude-native.md +13 -10
  125. package/guides/codex-delivery.md +3 -1
  126. package/guides/participant-ownership.md +1 -1
  127. package/guides/row-runtime-delivery-evidence.md +2 -0
  128. package/guides/row-storage-design.md +19 -44
  129. package/guides/square-api.md +137 -60
  130. package/package.json +1 -1
  131. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  132. package/plugins/claude/types.d.ts +1 -1
  133. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  134. package/plugins/opencode/delivery.ts +28 -41
  135. package/plugins/pi/square-pi.js +28 -51
  136. package/dist/square-legacy-framing.d.ts +0 -11
  137. package/dist/square-legacy-framing.js +0 -64
  138. package/dist/square-migration-host-ledger.d.ts +0 -26
  139. package/dist/square-migration-host-ledger.js +0 -169
  140. package/dist/square-migration.d.ts +0 -30
  141. package/dist/square-migration.js +0 -599
  142. package/guides/migration.md +0 -156
@@ -1,161 +1,47 @@
1
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';
4
- 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';
2
+ import { validateActivityMetadata, activityMetadataHasContent } from './model.js';
3
+ import { catchCoverage, deriveLanding, landingFacts, extendLandingCatchCoverage } from './square-domain.js';
8
4
  import { formatActivityId } from './square-core.js';
9
5
  import { compileSearchPattern } from './search.js';
6
+ import { createChannelSqlite, endpoint, endpointGeneration, messageAddress, deliveryEvidence, deliveryDeferral, receiveClaim } from './sqlite-channel.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, stableJson, pageRows, pageLimit } from './sqlite-values.js';
9
+ export { invalidArtifact } from './sqlite-values.js';
10
+ export { endpointGeneration } from './sqlite-channel.js';
11
+ export const APPLICATION_ID = 0x53515245;
12
+ export const CURRENT_USER_VERSION = 11;
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
+ /** Agora owns only its allocator/configuration header and social tables; neutral storage owns the revision header. */
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),
34
+ message_index INTEGER CHECK (message_index IS NULL OR message_index BETWEEN 0 AND 9007199254740991),
145
35
  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
-
36
+ CHECK ((kind = 'say') = (message_index IS NOT NULL)),
37
+ CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
38
+ );
39
+ CREATE INDEX activity_kind_index ON activities(kind, act_index);
40
+ CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index);
41
+ CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';
42
+ CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';
43
+ 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;
159
45
  CREATE TABLE participants (
160
46
  name_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
161
47
  name_json TEXT NOT NULL,
@@ -180,74 +66,33 @@ CREATE TABLE landed_audience (
180
66
  act_index INTEGER NOT NULL,
181
67
  recipient_key_json TEXT COLLATE BINARY NOT NULL,
182
68
  recipient_order INTEGER NOT NULL CHECK (recipient_order >= 0),
183
- route TEXT NOT NULL CHECK (route IN ('mention','reply','attention','bell')),
69
+ reason TEXT NOT NULL CHECK (reason IN ('mention','reply','attention','bell')),
184
70
  PRIMARY KEY (act_index, recipient_key_json),
185
71
  UNIQUE (act_index, recipient_order)
186
72
  ) WITHOUT ROWID;
187
73
  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);
74
+ export function initializeAgoraSqlite(db, configuration = {}) {
75
+ db.exec(AGORA_SCHEMA_SQL);
76
+ db.prepare('INSERT INTO square_meta(id,next_act_index,hard_cap,throttle_per_minute,preamble_json,warmup_json) VALUES(1,0,?,?,?,?)')
77
+ .run(configuration.hardCap ?? null, configuration.throttlePerMinute ?? null, JSON.stringify(configuration.preamble ?? []), JSON.stringify(configuration.warmup ?? []));
205
78
  }
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
- }
79
+ export function resetAgoraSqlite(db) {
80
+ for (const table of ['landed_audience', 'attention', 'participants', 'activities', 'square_meta'])
81
+ db.exec(`DROP TABLE ${table}`);
225
82
  }
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;
83
+ function validateSquareFraming(db, lower, location = '') {
84
+ const identity = db.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
85
+ 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);
243
88
  }
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;
89
+ /** Explicit full-Square framing and reset, composed from the neutral store and the supplied Channel schema. */
90
+ export function squareStoreFormat(lower, resetStorage) {
91
+ return {
92
+ 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); },
95
+ };
251
96
  }
252
97
  const kinds = ['join', 'done', 'say', 'read', 'listen', 'ignore', 'unlisten'];
253
98
  function head(row) {
@@ -258,10 +103,14 @@ function head(row) {
258
103
  requireFact(row.reach === null || row.kind === 'say' && row.reach === 'bell');
259
104
  if (row.reply_index !== null)
260
105
  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));
261
108
  return immutable({
262
- index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index })
109
+ index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index }),
110
+ ...(messageIndex === undefined ? {} : { messageIndex }),
263
111
  });
264
112
  }
113
+ /** A persisted upper activity. A say is a relation: it may not carry content and must name its message. */
265
114
  export function validateStoredActivity(value) {
266
115
  const a = object(value);
267
116
  integer(a.index);
@@ -271,34 +120,15 @@ export function validateStoredActivity(value) {
271
120
  string(a.actor, true);
272
121
  switch (a.kind) {
273
122
  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'))
285
- string(a.body);
286
- if (Object.hasOwn(a, 'metadata')) {
287
- requireFact(a.metadata !== undefined, 'say metadata is malformed.');
288
- try {
289
- validateActivityMetadata(a.metadata);
290
- }
291
- catch {
292
- throw invalidArtifact('say metadata is malformed.');
293
- }
294
- }
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.');
123
+ shape(a, [...common], ['mentions', 'reach', 'reply', 'messageIndex']);
124
+ requireFact(Object.hasOwn(a, 'messageIndex'), 'say is missing its message.');
296
125
  if (Object.hasOwn(a, 'mentions'))
297
126
  strings(a.mentions);
298
127
  if (Object.hasOwn(a, 'reach'))
299
128
  requireFact(a.reach === 'bell');
300
129
  if (Object.hasOwn(a, 'reply'))
301
130
  requireFact(integer(a.reply) < a.index);
131
+ integer(a.messageIndex);
302
132
  break;
303
133
  case 'done':
304
134
  shape(a, common, ['body']);
@@ -319,13 +149,45 @@ export function validateStoredActivity(value) {
319
149
  }
320
150
  return value;
321
151
  }
152
+ /** The explicit upper projection of a stored say onto its authoritative message content. */
153
+ export function validateHydratedActivity(value) {
154
+ const a = object(value);
155
+ if (a.kind !== 'say')
156
+ 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 }) };
171
+ }
172
+ /** Stored Agora relation only: say content lives in its linked Channel message. */
322
173
  function activity(row) {
323
174
  const h = head(row), payload = object(json(row.payload_json));
324
- // 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'] : [];
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'] : [];
326
177
  requireFact(Object.keys(payload).every(k => allowed.includes(k)));
327
178
  return immutable(validateStoredActivity({ ...h, ...payload }));
328
179
  }
180
+ /** Explicit upper projection: the stored say plus its authoritative Channel content. */
181
+ 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 }) }));
190
+ }
329
191
  function participant(row) {
330
192
  const name = decoded(row.name_json, true);
331
193
  requireFact(row.name_key_json === key(name) && [0, 1].includes(row.joined) && [0, 1].includes(row.done) && !(row.joined && row.done));
@@ -357,135 +219,14 @@ export function validateWatchLease(value) {
357
219
  }
358
220
  return value;
359
221
  }
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.');
222
+ export function readSquareHeader(db) {
223
+ const rows = db.prepare('SELECT m.*, (SELECT COALESCE(MAX(act_index),-1) FROM activities) AS last_act_index FROM square_meta m').all();
224
+ requireFact(rows.length === 1 && rows[0].id === 1, 'header row is malformed.');
225
+ const r = rows[0], nextActIndex = integer(r.next_act_index), lastActIndex = integer(r.last_act_index, -1);
226
+ requireFact(nextActIndex > lastActIndex, 'nextActIndex is behind the activity history.');
227
+ return immutable({ revision: readStorageRevision(db), deliveryRevision: readStorageRevision(db, true), nextActIndex, lastActIndex,
228
+ hardCap: r.hard_cap === null ? null : integer(r.hard_cap, 1), ...(r.throttle_per_minute === null ? {} : { throttlePerMinute: integer(r.throttle_per_minute, 1) }),
229
+ preamble: strings(json(r.preamble_json)), warmup: strings(json(r.warmup_json)) });
489
230
  }
490
231
  function activitySelection(filter, alias = 'a') {
491
232
  const terms = [], args = [];
@@ -513,7 +254,8 @@ function activitySelection(filter, alias = 'a') {
513
254
  terms.push(`NOT EXISTS (
514
255
  SELECT 1 FROM json_each(?) field WHERE NOT EXISTS (
515
256
  WITH RECURSIVE walk(value,type,depth) AS (
516
- SELECT json_extract(${alias}.payload_json,'$.metadata'),json_type(${alias}.payload_json,'$.metadata'),0
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
517
259
  UNION ALL
518
260
  SELECT child.value,child.type,walk.depth+1
519
261
  FROM walk JOIN json_each(CASE WHEN walk.type='object' THEN walk.value ELSE '{}' END) child
@@ -529,134 +271,100 @@ function activitySelection(filter, alias = 'a') {
529
271
  args.push(encode(filter.metadataMatch.fields));
530
272
  }
531
273
  if (filter.search !== undefined) {
532
- terms.push(`square_search(?,?,${alias}.act_index,${alias}.actor_json,json_extract(${alias}.payload_json,'$.body'))=1`);
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`);
533
275
  args.push(filter.search.mode, filter.search.text);
534
276
  }
535
277
  return { sql: terms.length ? terms.join(' AND ') : '1', args };
536
278
  }
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
- export function createSqliteDriver(db) {
550
- let changed = false, deliveryChanged = false;
279
+ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db)) {
280
+ let changed = false;
551
281
  const all = (sql, args = []) => db.prepare(sql).all(...args);
552
282
  const one = (sql, args = []) => db.prepare(sql).get(...args);
553
283
  const run = (sql, args = []) => Number(db.prepare(sql).run(...args).changes);
554
284
  let searchRegistered = false;
285
+ function ensureSearch() {
286
+ if (searchRegistered)
287
+ return;
288
+ const patterns = new Map();
289
+ db.function('square_search', { deterministic: true }, (mode, text, index, actor, body) => {
290
+ const cacheKey = encode([mode, text]);
291
+ let pattern = patterns.get(cacheKey);
292
+ if (pattern === undefined) {
293
+ pattern = compileSearchPattern(string(text), mode === 'literal');
294
+ patterns.set(cacheKey, pattern);
295
+ }
296
+ return Number([string(index), decoded(actor, true), body === null ? '' : string(body)]
297
+ .some(field => pattern.test(field)));
298
+ });
299
+ searchRegistered = true;
300
+ }
555
301
  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
- }
302
+ if (filter.search !== undefined)
303
+ ensureSearch();
570
304
  return activitySelection(filter);
571
305
  }
306
+ const read = {};
307
+ const channel = channelDriver, channelRead = channel, channelWrite = channel;
308
+ const currentEndpoint = (name) => channel.endpoint(name);
572
309
  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);
574
310
  function directedSelection(recipient, filter) {
575
- const p = read.participant(recipient), base = selectActivity({ ...filter, actors: filter.senders });
311
+ const participant = read.participant(recipient), base = selectActivity({ ...filter, actors: filter.senders });
576
312
  base.sql += ' AND d.recipient_key_json=?';
577
- base.args.push(p ? key(p.name) : '');
313
+ base.args.push(participant ? key(participant.name) : '');
314
+ if (filter.currentGeneration) {
315
+ base.sql += ' AND (ma.generation_json IS NULL OR ma.generation_json=?)';
316
+ base.args.push(participant ? encode(currentEndpoint(participant.name)?.generation ?? '') : '');
317
+ }
578
318
  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));
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));
581
321
  }
582
322
  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
- });
592
- }
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));
323
+ base.sql += " AND d.reason IN ('mention','reply','bell')";
324
+ return { base, participant };
602
325
  }
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
- },
326
+ // ----- Upper Agora read capabilities -----
327
+ const lower = channelRead;
328
+ 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),
620
333
  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
- },
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
+ },
342
+ header() { return readSquareHeader(db); },
343
+ participant(name) { const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]); return r ? participant(r) : undefined; },
637
344
  participantCounts() {
638
345
  const r = one('SELECT COUNT(*) AS known, COALESCE(SUM(joined),0) AS joined, COALESCE(SUM(done),0) AS done FROM participants');
639
346
  return immutable({ known: integer(r.known), joined: integer(r.joined), done: integer(r.done) });
640
347
  },
641
348
  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)])),
349
+ 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)),
350
+ 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
351
  activity(index) {
645
352
  const r = one('SELECT * FROM activities WHERE act_index=?', [integer(index)]);
646
353
  return r ? activity(r) : undefined;
647
354
  },
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
+ },
648
359
  activityHeads(filter, page) {
649
360
  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);
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);
651
362
  },
652
363
  activities(indexes) {
653
364
  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);
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));
659
366
  },
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); },
660
368
  contextIndexes(centers, before, after) {
661
369
  integer(before);
662
370
  integer(after);
@@ -676,7 +384,6 @@ export function createSqliteDriver(db) {
676
384
  sayOrdinals(indexes) {
677
385
  indexes.forEach(v => integer(v));
678
386
  const result = {};
679
- // Only an actor's latest say can use its authoritative lifetime count.
680
387
  for (const r of all(`SELECT a.act_index, CASE
681
388
  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
389
  THEN p.say_count
@@ -699,131 +406,63 @@ export function createSqliteDriver(db) {
699
406
  args.push(integer(take));
700
407
  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
408
  },
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) }))); },
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)) }) }))); },
703
410
  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));
411
+ 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]);
413
+ return pageRows(rows, page, r => {
414
+ const h = head(r);
415
+ return { index: h.index, at: h.at, actor: h.actor, recipient: p?.name ?? recipient,
416
+ route: routeKind(r.reason), messageIndex: h.messageIndex,
417
+ ...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }) };
418
+ });
706
419
  },
707
420
  directedAt(recipient, index) { return read.directed(recipient, { afterIndex: integer(index) - 1, throughIndex: index }, { edge: 'oldest', limit: 1 }).items[0]; },
708
421
  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 });
422
+ 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);
727
424
  },
728
425
  receipt(name, index) {
729
- const r = one('SELECT at FROM receipts WHERE owner_json=? AND act_index=?', [encode(owner(name)), integer(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)]);
730
427
  return r ? immutable({ at: finite(r.at) }) : undefined;
731
428
  },
732
429
  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)]);
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)]);
734
433
  return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorActivityIndex: integer(e.latest) }) });
735
434
  },
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
435
  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;
751
- },
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));
444
+ },
445
+ deliveryDeferrals: (participant, generation) => lower.deliveryDeferrals(participant, generation),
752
446
  };
447
+ Object.assign(read, readMethods, { channel: channelRead });
448
+ // ----- Upper Agora writes / landing -----
753
449
  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;
779
- return count > 0;
780
- },
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)]);
805
- changed = true;
806
- deliveryChanged = true;
807
- return true;
808
- },
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;
818
- },
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); },
819
456
  applyLanding(effect) {
820
457
  const a = validateStoredActivity(effect.activity);
821
458
  requireFact(a.index >= read.header().nextActIndex && a.index < Number.MAX_SAFE_INTEGER, 'activity index cannot advance.');
822
- 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 })) });
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.
463
+ 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]);
465
+ });
827
466
  if (effect.participant) {
828
467
  const p = effect.participant;
829
468
  string(p.name, true);
@@ -853,199 +492,77 @@ export function createSqliteDriver(db) {
853
492
  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
493
  }
855
494
  run('UPDATE square_meta SET next_act_index=? WHERE id=1', [index + 1]);
495
+ extendLandingCatchCoverage(read, channelWrite.putReceiveClaim, a, effect.audience);
856
496
  changed = true;
857
497
  },
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
- putLease(name, value, generation) {
498
+ putLease(name, value, generation, suppressed) {
872
499
  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
500
  if (generation !== undefined)
875
- activityId(generation);
876
- if (row !== undefined && generation !== undefined && decoded(row.generation_json) !== '' && decoded(row.generation_json) !== generation)
501
+ endpointGeneration(generation);
502
+ const existing = channelRead.claimLease(canonical, 'agora');
503
+ if (existing !== undefined && generation !== undefined && existing.generation !== generation)
877
504
  return false;
878
- if (old && stableJson(old) === stableJson(l) && (generation === undefined || row?.generation_json === encode(generation)))
879
- 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)]);
882
- changed = true;
883
- return true;
884
- },
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;
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 });
910
509
  },
911
510
  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)
511
+ const canonical = owner(name), existing = channelRead.claimLease(canonical, 'agora');
512
+ if (existing === undefined || existing.id !== expected || generation !== undefined && existing.generation !== generation)
914
513
  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;
918
- return count > 0;
514
+ return channelWrite.removeReceiveClaim(canonical, 'agora', expected);
919
515
  },
920
516
  };
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
- };
517
+ function importLanding(activityValue) {
518
+ 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;
531
+ }
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;
954
542
  }
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;
1026
- },
1027
- };
1028
- return Object.defineProperties(Object.assign(read, writes, delivery), {
1029
- changed: { get: () => changed },
1030
- deliveryChanged: { get: () => deliveryChanged },
543
+ const driver = Object.defineProperties(Object.assign(read, writes, { channel, importLanding }), {
544
+ changed: { get: () => changed || channel.changed },
545
+ deliveryChanged: { get: () => channel.deliveryChanged },
1031
546
  });
547
+ return driver;
1032
548
  }
1033
- /** Explicit import only. Caller owns transaction, legacy validation and format publication. */
1034
- export function initializeSqlite(db, state, revision = 0) {
549
+ /** Explicit import replay. Caller owns the transaction and format publication. */
550
+ export function initializeSqlite(db, state, revision, lower) {
1035
551
  integer(revision);
1036
552
  integer(state.runtime.nextActIndex);
1037
553
  if (state.hardCap !== null)
1038
554
  integer(state.hardCap, 1);
1039
555
  if (state.throttlePerMinute !== undefined)
1040
556
  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)));
557
+ initializeStorageMeta(db, revision);
558
+ lower.initialize(db);
559
+ initializeAgoraSqlite(db, { ...state, preamble: strings(state.preamble), warmup: strings(state.warmup) });
1043
560
  const driver = createSqliteDriver(db);
1044
561
  for (const activity of state.acts)
1045
- driver.applyLanding(deriveLanding(validateStoredActivity(activity), landingFacts(driver, activity)));
562
+ driver.importLanding(activity);
1046
563
  requireFact(state.runtime.nextActIndex > driver.header().lastActIndex, 'nextActIndex is behind the activity history.');
1047
564
  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(?,?,?)');
565
+ const insert = db.prepare('INSERT INTO receipts(owner_key_json,msg_index,at) VALUES(?,?,?)');
1049
566
  for (const [owner, observations] of Object.entries(state.runtime.observations)) {
1050
567
  string(owner, true);
1051
568
  for (const [id, observation] of Object.entries(observations)) {
@@ -1054,69 +571,79 @@ export function initializeSqlite(db, state, revision = 0) {
1054
571
  const index = integer(Number(match[1]));
1055
572
  requireFact(index < state.runtime.nextActIndex, 'receipt references an unassigned activity index.');
1056
573
  requireFact(observation.state === 'seen');
1057
- insert.run(encode(owner), index, finite(observation.at));
574
+ const activity = driver.activity(index);
575
+ if (activity?.kind !== 'say' || activity.messageIndex === undefined)
576
+ continue;
577
+ insert.run(encode(owner), activity.messageIndex, finite(observation.at));
1058
578
  }
1059
579
  }
1060
580
  for (const [owner, value] of Object.entries(state.runtime.leases)) {
1061
581
  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));
582
+ 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)));
1064
585
  }
1065
586
  }
1066
587
  export function exportSqliteState(db) {
1067
588
  const d = createSqliteDriver(db), h = d.header();
1068
589
  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.');
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;
1072
596
  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) };
597
+ Object.defineProperty(observations, owner, { value: {}, enumerable: true, writable: true });
598
+ observations[owner][formatActivityId(say.index)] = { state: 'seen', at: finite(row.at) };
1077
599
  }
1078
- for (const row of db.prepare('SELECT * FROM catch_leases').all()) {
1079
- const owner = decoded(row.owner_json, true);
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));
1080
603
  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) })
604
+ leaseId: decoded(row.claim_id_json, true), heartbeatAt: finite(row.heartbeat_at), expiresAt: finite(row.expires_at), ...(selector.filter === undefined ? {} : { filter: selector.filter })
1082
605
  }), enumerable: true });
1083
606
  }
1084
607
  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
- }
608
+ 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),
610
+ runtime: { nextActIndex: h.nextActIndex, observations, leases }
1088
611
  };
1089
612
  }
1090
- /** Full read-only integrity inspection, including maintained-index replay comparison. */
1091
- export function inspectSqlite(db) {
1092
- return inspectRows(db, seat);
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);
1093
616
  }
1094
- function inspectRows(db, inspectSeat) {
617
+ function inspectRows(db, inspectEndpoint, lower) {
1095
618
  const check = db.prepare('PRAGMA integrity_check').all();
1096
619
  requireFact(check.length === 1 && check[0].integrity_check === 'ok', 'SQLite integrity check failed.');
1097
620
  const state = exportSqliteState(db), d = createSqliteDriver(db);
1098
621
  d.roster();
1099
- for (const row of db.prepare('SELECT * FROM seats').all())
1100
- inspectSeat(row);
622
+ for (const row of db.prepare('SELECT * FROM endpoints').all())
623
+ inspectEndpoint(row);
1101
624
  for (const row of db.prepare('SELECT * FROM delivery_evidence').all())
1102
625
  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);
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);
1106
633
  }
1107
634
  for (const p of d.roster()) {
1108
635
  d.attention(p.name);
1109
636
  d.incomingAttention(p.name);
1110
637
  }
1111
638
  for (const a of state.acts)
1112
- d.audience(a.index);
1113
- // A transient memory database checks derived indices without altering authority.
639
+ if (a.kind === 'say')
640
+ d.audience(a.index);
641
+ const standingCount = d.roster().filter(p => p.joined).length;
642
+ if (lower === undefined)
643
+ return { standingCount };
1114
644
  const rebuilt = new MemoryDatabase(':memory:');
1115
645
  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.
646
+ initializeSqlite(rebuilt, state, d.header().revision, lower);
1120
647
  const retainName = rebuilt.prepare('UPDATE participants SET name_json=? WHERE name_key_json=?');
1121
648
  for (const p of d.roster())
1122
649
  retainName.run(encode(p.name), key(p.name));
@@ -1125,7 +652,24 @@ function inspectRows(db, inspectSeat) {
1125
652
  const expected = rebuilt.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
1126
653
  requireFact(stableJson(original) === stableJson(expected), `derived ${table} index is inconsistent.`);
1127
654
  }
1128
- return { standingCount: d.roster().filter(p => p.joined).length };
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 };
1129
673
  }
1130
674
  finally {
1131
675
  rebuilt.close();