@astrosheep/square 0.3.94 → 0.3.96

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 (99) hide show
  1. package/dist/artifact.js +1 -1
  2. package/dist/attention-presentation.d.ts +5 -4
  3. package/dist/attention-presentation.js +18 -8
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.js +50 -48
  7. package/dist/catch-decisions.d.ts +1 -2
  8. package/dist/catch-decisions.js +41 -21
  9. package/dist/channel.d.ts +72 -0
  10. package/dist/channel.js +86 -0
  11. package/dist/claude-delivery.js +2 -1
  12. package/dist/cli/context.js +0 -2
  13. package/dist/cli/express-presentation.js +3 -3
  14. package/dist/cli/json.js +1 -1
  15. package/dist/cli/program.js +1 -1
  16. package/dist/cli/registry.js +1 -3
  17. package/dist/cli/square-commands.d.ts +0 -2
  18. package/dist/cli/square-commands.js +21 -27
  19. package/dist/decisions.d.ts +0 -5
  20. package/dist/decisions.js +0 -13
  21. package/dist/delivery-attempts.d.ts +52 -0
  22. package/dist/delivery-attempts.js +73 -0
  23. package/dist/delivery-effects.d.ts +139 -0
  24. package/dist/delivery-effects.js +163 -0
  25. package/dist/delivery-operations.js +43 -68
  26. package/dist/delivery-primitives.d.ts +5 -0
  27. package/dist/delivery-primitives.js +3 -0
  28. package/dist/delivery.d.ts +1 -1
  29. package/dist/delivery.js +8 -7
  30. package/dist/help.js +4 -6
  31. package/dist/http-delivery.d.ts +16 -0
  32. package/dist/http-delivery.js +101 -0
  33. package/dist/inbox.js +3 -1
  34. package/dist/mcp.js +65 -63
  35. package/dist/model.d.ts +10 -6
  36. package/dist/model.js +20 -1
  37. package/dist/notifications.js +2 -0
  38. package/dist/operation-presentation.d.ts +1 -2
  39. package/dist/operation-presentation.js +12 -20
  40. package/dist/ports.d.ts +8 -14
  41. package/dist/presentation-operations.d.ts +15 -19
  42. package/dist/presentation-operations.js +47 -57
  43. package/dist/presentation.d.ts +2 -9
  44. package/dist/presentation.js +13 -48
  45. package/dist/receive.d.ts +66 -0
  46. package/dist/receive.js +54 -0
  47. package/dist/runtime.d.ts +0 -1
  48. package/dist/runtime.js +2 -3
  49. package/dist/sqlite-square.d.ts +3 -2
  50. package/dist/sqlite-square.js +83 -57
  51. package/dist/square-api.d.ts +1 -1
  52. package/dist/square-api.js +1 -1
  53. package/dist/square-core.d.ts +1 -9
  54. package/dist/square-domain.js +29 -36
  55. package/dist/square-facade.d.ts +25 -23
  56. package/dist/square-legacy-framing.d.ts +2 -0
  57. package/dist/square-legacy-framing.js +8 -0
  58. package/dist/square-migration.d.ts +3 -1
  59. package/dist/square-migration.js +46 -13
  60. package/dist/square-receive.d.ts +6 -0
  61. package/dist/square-receive.js +18 -0
  62. package/dist/square-sqlite.d.ts +5 -3
  63. package/dist/square-sqlite.js +100 -92
  64. package/dist/square-tools.d.ts +1 -0
  65. package/dist/square-tools.js +12 -12
  66. package/dist/views.d.ts +2 -3
  67. package/dist/views.js +14 -19
  68. package/dist/wakes.d.ts +0 -3
  69. package/dist/wakes.js +3 -5
  70. package/examples/symmetric-domains.mjs +250 -0
  71. package/guides/application-facade-delivery-evidence.md +5 -5
  72. package/guides/attention-primitive.md +32 -0
  73. package/guides/change-waits.md +2 -2
  74. package/guides/codex-delivery.md +1 -1
  75. package/guides/delivery-primitives.md +180 -0
  76. package/guides/mcp.md +1 -1
  77. package/guides/migration.md +12 -6
  78. package/guides/participant-ownership.md +1 -1
  79. package/guides/pi-tools.md +2 -2
  80. package/guides/primitive-catch-consolidation.md +5 -5
  81. package/guides/primitive-history-consolidation.md +2 -2
  82. package/guides/primitive-status-consolidation.md +3 -3
  83. package/guides/primitive-storage-consolidation.md +1 -1
  84. package/guides/receive-primitive.md +90 -0
  85. package/guides/row-runtime-delivery-evidence.md +1 -1
  86. package/guides/row-storage-design.md +17 -23
  87. package/guides/square-api.md +77 -10
  88. package/guides/symmetric-domains.md +92 -0
  89. package/package.json +18 -1
  90. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  91. package/plugins/claude/skills/square/SKILL.md +2 -6
  92. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  93. package/plugins/paseo/client/marble.ts +7 -0
  94. package/plugins/paseo/client/markdown.tsx +65 -9
  95. package/plugins/paseo/client/palette.ts +43 -20
  96. package/plugins/paseo/client/square-message.tsx +57 -48
  97. package/plugins/paseo/shared/message.ts +1 -2
  98. package/plugins/pi/square-pi.js +6 -4
  99. package/skills/square/SKILL.md +2 -6
@@ -15,6 +15,14 @@ CREATE INDEX route_session ON routes(session_json, position);`;
15
15
  function normalizedSql(sql) {
16
16
  return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1');
17
17
  }
18
+ /** Historical activity framing only; no runtime activity decoder accepts these kinds. */
19
+ export function legacyActivitySchema(currentSchemaSql) {
20
+ return currentSchemaSql
21
+ .replace("kind IN ('join','done','say','read','listen','ignore','unlisten')", "kind IN ('join','done','say','hold','resume','read','listen','ignore','unlisten')")
22
+ .replace(' actor_json TEXT COLLATE BINARY NOT NULL,', ' actor_json TEXT COLLATE BINARY,')
23
+ .replace(' actor_key_json TEXT COLLATE BINARY NOT NULL,', ' actor_key_json TEXT COLLATE BINARY,')
24
+ .replace(" CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))", " CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),\n CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),\n CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))");
25
+ }
18
26
  function legacyRowSchema(currentSchemaSql, version) {
19
27
  const statements = currentSchemaSql.split(';').map((statement) => statement.trim()).filter(Boolean)
20
28
  .filter((statement) => {
@@ -20,8 +20,10 @@ export interface MigrationOutcome {
20
20
  /**
21
21
  * Explicit conversion of the supported legacy formats. Row-backed versions 2 and 3 are
22
22
  * preflighted read-only, then receive ownership/evidence and deferral storage in one
23
- * format-7 transaction. Formats 4/5 gain attachment; format 4 also gains empty deferrals.
23
+ * format-8 transaction. Formats 4/5 gain attachment; format 4 also gains empty deferrals.
24
24
  * All legacy seats are encoded as opaque owners, preserving format-6 attachment.
25
+ * Every source drops obsolete activities and their identity references, clears dangling
26
+ * replies and rebuilds derived indices; surviving ids and allocation watermarks remain stable.
25
27
  * Legacy delivery evidence is copied from HostLedger only when its owner/session mapping is
26
28
  * unique; no source record is retired. Version 1 is not a supported source for this format transition.
27
29
  */
@@ -1,3 +1,4 @@
1
+ import { DatabaseSync } from 'node:sqlite';
1
2
  import { createHash } from 'node:crypto';
2
3
  import { openExistingSquareDatabase } from './artifact.js';
3
4
  import { inSqliteTransaction } from './sqlite-transaction.js';
@@ -7,7 +8,7 @@ import { formatActivityId, isWakeRouteKind, nameKey } from './model.js';
7
8
  import { parseActivityId } from './square-core.js';
8
9
  import { readLegacyHostData } from './square-migration-host-ledger.js';
9
10
  import { legacyRowFramingIssue, FORMAT4_ADDED_OBJECTS } from './square-legacy-framing.js';
10
- import { APPLICATION_ID, ACTIVITIES_TABLE_SQL, ACTIVITY_INDEXES_SQL, CURRENT_USER_VERSION, V2_USER_VERSION, V3_USER_VERSION, V4_USER_VERSION, V5_USER_VERSION, V6_USER_VERSION, SCHEMA_SQL, FORMAT4_SCHEMA_SQL, DELIVERY_DEFERRALS_SQL, validateRowFraming, inspectSqlite, createSqliteDriver, invalidArtifact, validateStoredActivity } from './square-sqlite.js';
11
+ import { APPLICATION_ID, ACTIVITIES_TABLE_SQL, ACTIVITY_INDEXES_SQL, CURRENT_USER_VERSION, V2_USER_VERSION, V3_USER_VERSION, V4_USER_VERSION, V5_USER_VERSION, V6_USER_VERSION, V7_USER_VERSION, SCHEMA_SQL, FORMAT4_SCHEMA_SQL, DELIVERY_DEFERRALS_SQL, validateRowFraming, inspectSqlite, createSqliteDriver, invalidArtifact, validateStoredActivity, exportSqliteState, initializeSqlite } from './square-sqlite.js';
11
12
  function isObject(value) {
12
13
  return typeof value === 'object' && value !== null && !Array.isArray(value);
13
14
  }
@@ -134,15 +135,44 @@ function standing(database) {
134
135
  return createSqliteDriver(database).roster().filter((participant) => participant.joined).length;
135
136
  }
136
137
  const ACTIVITY_INDEX_NAMES = ['activity_kind_index', 'activity_actor_index', 'activity_say_actor_index', 'activity_say_time', 'activity_bell_actor_time'];
137
- function rebuildV2Activities(database) {
138
+ function migrateActivities(database) {
139
+ // Only activity identity references are deleted. Range watermarks (read.through and
140
+ // delivery cancellation boundaries) remain meaningful even when their endpoint is gone.
141
+ for (const table of ['landed_audience', 'receipts', 'delivery_evidence']) {
142
+ database.exec(`DELETE FROM ${table} WHERE act_index IN (SELECT act_index FROM activities WHERE kind IN ('hold','resume'));`);
143
+ }
138
144
  database.exec([
145
+ "UPDATE activities SET reply_index=NULL WHERE reply_index IN (SELECT act_index FROM activities WHERE kind IN ('hold','resume'));",
139
146
  ...ACTIVITY_INDEX_NAMES.map((name) => `DROP INDEX ${name};`),
140
- 'ALTER TABLE activities RENAME TO activities_v2;',
147
+ 'ALTER TABLE activities RENAME TO activities_legacy;',
141
148
  ACTIVITIES_TABLE_SQL,
142
- 'INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json) SELECT act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json FROM activities_v2 ORDER BY act_index;',
143
- 'DROP TABLE activities_v2;',
149
+ "INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json) SELECT act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json FROM activities_legacy WHERE kind NOT IN ('hold','resume') ORDER BY act_index;",
150
+ 'DROP TABLE activities_legacy;',
144
151
  ...ACTIVITY_INDEXES_SQL.map((sql) => `${sql};`),
145
152
  ].join('\n'));
153
+ // Actor-bearing deleted activities and cleared replies can change maintained indices.
154
+ // Replay surviving facts only, without lowering the authoritative next allocator.
155
+ const names = database.prepare('SELECT name_key_json,name_json FROM participants').all();
156
+ const rebuilt = new DatabaseSync(':memory:');
157
+ try {
158
+ initializeSqlite(rebuilt, exportSqliteState(database));
159
+ // Canonical spelling is an established identity fact, not the spelling of
160
+ // whichever actor activity happens to survive this migration first.
161
+ const retainName = rebuilt.prepare('UPDATE participants SET name_json=? WHERE name_key_json=?');
162
+ for (const row of names)
163
+ retainName.run(row.name_json, row.name_key_json);
164
+ for (const table of ['participants', 'attention', 'landed_audience']) {
165
+ database.exec(`DELETE FROM ${table};`);
166
+ for (const row of rebuilt.prepare(`SELECT * FROM ${table}`).all()) {
167
+ const columns = Object.keys(row);
168
+ database.prepare(`INSERT INTO ${table}(${columns.join(',')}) VALUES(${columns.map(() => '?').join(',')})`)
169
+ .run(...Object.values(row));
170
+ }
171
+ }
172
+ }
173
+ finally {
174
+ rebuilt.close();
175
+ }
146
176
  }
147
177
  function validateCurrentFraming(database, location) {
148
178
  validateRowFraming(database, CURRENT_USER_VERSION, location);
@@ -282,7 +312,7 @@ function inspectLegacyRowArtifact(database, location, host) {
282
312
  const driver = createSqliteDriver(database);
283
313
  let activityCount = 0;
284
314
  let previous = -1;
285
- for (const row of database.prepare('SELECT act_index FROM activities ORDER BY act_index').iterate()) {
315
+ for (const row of database.prepare("SELECT act_index FROM activities WHERE kind NOT IN ('hold','resume') ORDER BY act_index").iterate()) {
286
316
  const index = sourceInteger(row.act_index);
287
317
  assertSource(index > previous && index < nextActIndex, 'activity index is malformed or unassigned.');
288
318
  const activity = driver.activity(index);
@@ -389,7 +419,7 @@ function planLegacyDeliveries(records, seats, driver) {
389
419
  continue;
390
420
  const joined = establishingJoin(driver, participant);
391
421
  const generation = formatActivityId(joined);
392
- if (index <= joined || driver.activity(index)?.kind !== 'say' || driver.directedAt(participant.name, index) === undefined)
422
+ if (index <= joined || driver.activityHeads({ indexes: [index], kinds: ['say'] }, { edge: 'oldest', limit: 1 }).items.length === 0 || driver.directedAt(participant.name, index) === undefined)
393
423
  continue;
394
424
  if (participant.done) {
395
425
  const done = driver.activityHeads({ actors: [participant.name], kinds: ['done'] }, { edge: 'newest', limit: 1 }).items[0];
@@ -488,6 +518,7 @@ function upgradeRowArtifact(database, location, plan) {
488
518
  importOwnershipAndEvidence(database, plan);
489
519
  database.exec('DROP TABLE routes;');
490
520
  database.exec(DELIVERY_DEFERRALS_SQL);
521
+ migrateActivities(database);
491
522
  database.exec(`PRAGMA user_version = ${CURRENT_USER_VERSION};`);
492
523
  validateCurrentFraming(database, location);
493
524
  }
@@ -501,7 +532,7 @@ function validateMigrationSource(database, location) {
501
532
  throw invalidArtifact(issue);
502
533
  return before.userVersion;
503
534
  }
504
- if (before.userVersion === V4_USER_VERSION || before.userVersion === V5_USER_VERSION || before.userVersion === V6_USER_VERSION) {
535
+ if (before.userVersion === V4_USER_VERSION || before.userVersion === V5_USER_VERSION || before.userVersion === V6_USER_VERSION || before.userVersion === V7_USER_VERSION) {
505
536
  validateRowFraming(database, before.userVersion, location);
506
537
  return before.userVersion;
507
538
  }
@@ -514,8 +545,10 @@ function validateMigrationSource(database, location) {
514
545
  /**
515
546
  * Explicit conversion of the supported legacy formats. Row-backed versions 2 and 3 are
516
547
  * preflighted read-only, then receive ownership/evidence and deferral storage in one
517
- * format-7 transaction. Formats 4/5 gain attachment; format 4 also gains empty deferrals.
548
+ * format-8 transaction. Formats 4/5 gain attachment; format 4 also gains empty deferrals.
518
549
  * All legacy seats are encoded as opaque owners, preserving format-6 attachment.
550
+ * Every source drops obsolete activities and their identity references, clears dangling
551
+ * replies and rebuilds derived indices; surviving ids and allocation watermarks remain stable.
519
552
  * Legacy delivery evidence is copied from HostLedger only when its owner/session mapping is
520
553
  * unique; no source record is retired. Version 1 is not a supported source for this format transition.
521
554
  */
@@ -526,13 +559,15 @@ export async function migrateSquareFile(location, signal, env = process.env) {
526
559
  const source = await inSqliteTransaction(database, false, () => validateMigrationSource(database, squarePath), value => value, signal);
527
560
  if (source === undefined)
528
561
  return { status: 'already-current', standing: standing(database) };
529
- if (source === V4_USER_VERSION || source === V5_USER_VERSION || source === V6_USER_VERSION) {
562
+ if (source === V4_USER_VERSION || source === V5_USER_VERSION || source === V6_USER_VERSION || source === V7_USER_VERSION) {
530
563
  return await inSqliteTransaction(database, true, () => {
531
564
  assertSource(validateMigrationSource(database, squarePath) === source, 'source format changed during migration preflight.');
532
565
  }, () => {
533
566
  if (source === V4_USER_VERSION)
534
567
  database.exec(DELIVERY_DEFERRALS_SQL);
535
- migrateSeatOwnership(database, source);
568
+ if (source !== V7_USER_VERSION)
569
+ migrateSeatOwnership(database, source);
570
+ migrateActivities(database);
536
571
  database.exec(`PRAGMA user_version = ${CURRENT_USER_VERSION};`);
537
572
  validateCurrentFraming(database, squarePath);
538
573
  return { status: 'migrated', standing: standing(database) };
@@ -552,8 +587,6 @@ export async function migrateSquareFile(location, signal, env = process.env) {
552
587
  assertSource(lockedPlan.sourceToken === plan.sourceToken, 'source changed during migration preflight.');
553
588
  result.plan = lockedPlan;
554
589
  }, () => {
555
- if (source === 2)
556
- rebuildV2Activities(database);
557
590
  const migratedPlan = result.plan;
558
591
  upgradeRowArtifact(database, squarePath, migratedPlan);
559
592
  result.outcome = { status: 'migrated', standing: migratedPlan.standing, counts: migratedPlan.counts };
@@ -0,0 +1,6 @@
1
+ import type { AudienceRoute } from './attention.js';
2
+ import type { StoredAct } from './model.js';
3
+ import type { DirectedFilter, SquareRead } from './ports.js';
4
+ import type { ReceiveRead } from './receive.js';
5
+ /** Transaction-local projections only; the artifact still owns indexed selection and bodies. */
6
+ export declare function squareReceiveRead(facts: SquareRead): ReceiveRead<number, StoredAct, AudienceRoute, DirectedFilter>;
@@ -0,0 +1,18 @@
1
+ /** Transaction-local projections only; the artifact still owns indexed selection and bodies. */
2
+ export function squareReceiveRead(facts) {
3
+ const filter = (query, unconsumed) => {
4
+ const { unseenOnly: _, ...rest } = query;
5
+ return unconsumed ? { ...rest, unseenOnly: true } : rest;
6
+ };
7
+ return {
8
+ heads: (recipient, query, options) => facts.directed(recipient, filter(query, options.unconsumed), { edge: 'oldest', limit: options.limit })
9
+ .items.map(item => ({ key: item.index, via: item.route })),
10
+ addressed(recipient, index) {
11
+ const head = facts.directedAt(recipient, index);
12
+ return head === undefined ? undefined : { key: head.index, via: head.route };
13
+ },
14
+ load: keys => new Map(facts.activities(keys).map(item => [item.index, item])),
15
+ count: (recipient, query, unconsumed) => facts.directedCount(recipient, filter(query, unconsumed)),
16
+ receipt: ref => facts.receipt(ref.recipient, ref.item),
17
+ };
18
+ }
@@ -23,7 +23,8 @@ export declare const V3_USER_VERSION = 3;
23
23
  export declare const V4_USER_VERSION = 4;
24
24
  export declare const V5_USER_VERSION = 5;
25
25
  export declare const V6_USER_VERSION = 6;
26
- export declare const CURRENT_USER_VERSION = 7;
26
+ export declare const V7_USER_VERSION = 7;
27
+ export declare const CURRENT_USER_VERSION = 8;
27
28
  export declare const V1_SNAPSHOT_TABLE = "square_snapshot";
28
29
  export interface LegacyFraming {
29
30
  readonly applicationId: number;
@@ -37,17 +38,18 @@ export interface LegacyFraming {
37
38
  * migration. Anything else is unrelated or unsupported, not repaired.
38
39
  */
39
40
  export declare function validateLegacyFraming(database: DatabaseSync, applicationId: number, location?: string): LegacyFraming;
40
- export declare const ACTIVITIES_TABLE_SQL = "CREATE TABLE activities (\n act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),\n at REAL NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('join','done','say','hold','resume','read','listen','ignore','unlisten')),\n actor_json TEXT COLLATE BINARY,\n actor_key_json TEXT COLLATE BINARY,\n reach TEXT CHECK (reach = 'bell'),\n reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),\n CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),\n CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),\n CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))\n);";
41
+ export declare const ACTIVITIES_TABLE_SQL = "CREATE TABLE activities (\n act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),\n at REAL NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('join','done','say','read','listen','ignore','unlisten')),\n actor_json TEXT COLLATE BINARY NOT NULL,\n actor_key_json TEXT COLLATE BINARY NOT NULL,\n reach TEXT CHECK (reach = 'bell'),\n reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),\n CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))\n);";
41
42
  export declare const ACTIVITY_INDEXES_SQL: readonly ["CREATE INDEX activity_kind_index ON activities(kind, act_index)", "CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index)", "CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say'", "CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'", "CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'"];
42
43
  export declare const DELIVERY_DEFERRALS_SQL = "CREATE TABLE delivery_deferrals (\n participant_key_json TEXT COLLATE BINARY NOT NULL,\n participant_json TEXT NOT NULL,\n generation_json TEXT NOT NULL,\n deferral_id_json TEXT NOT NULL,\n match_json TEXT NOT NULL CHECK (json_valid(match_json)),\n expires_at REAL NOT NULL,\n PRIMARY KEY (participant_key_json,generation_json,deferral_id_json)\n) WITHOUT ROWID;\nCREATE INDEX delivery_deferral_expiry ON delivery_deferrals(participant_key_json,generation_json,expires_at);";
43
44
  export declare const SCHEMA_SQL: string;
45
+ export declare const FORMAT7_SCHEMA_SQL: string;
44
46
  export declare const FORMAT6_SCHEMA_SQL: string;
45
47
  export declare const FORMAT5_SCHEMA_SQL: string;
46
48
  export declare const FORMAT4_SCHEMA_SQL: string;
47
49
  export declare function invalidArtifact(detail: string): SquareError;
48
50
  export declare function validateStoredActivity(value: unknown): StoredAct;
49
51
  export declare function validateWatchLease(value: unknown): WatchLease;
50
- export declare function validateRowFraming(db: DatabaseSync, version: 2 | 3 | 4 | 5 | 6 | 7, location?: string): void;
52
+ export declare function validateRowFraming(db: DatabaseSync, version: 2 | 3 | 4 | 5 | 6 | 7 | 8, location?: string): void;
51
53
  export declare function validateSqliteHeader(db: DatabaseSync, location?: string): SquareHeader;
52
54
  export interface SqliteDriver extends SquareRead, LandingWriter {
53
55
  readonly changed: boolean;
@@ -1,8 +1,10 @@
1
1
  import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
2
- import { SquareError, nameKey, isWakeRouteKind, validateOwner, validateActivityMetadata, activityMetadataHasContent } from './model.js';
2
+ import { SquareError, nameKey, isWakeRouteKind, validateOwner, validateActivityMetadata, validateActivityDelivery, activityMetadataHasContent } from './model.js';
3
3
  import { validateMetadataMatch, matchingDeferrals, validateDeferralId } from './delivery-deferral.js';
4
4
  import { deriveLanding, landingFacts } from './square-domain.js';
5
- import { legacyRowFramingIssue } from './square-legacy-framing.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';
6
8
  export function emptyRuntimeState(nextActIndex = 0) {
7
9
  return { nextActIndex, observations: {}, leases: {} };
8
10
  }
@@ -13,7 +15,8 @@ export const V3_USER_VERSION = 3;
13
15
  export const V4_USER_VERSION = 4;
14
16
  export const V5_USER_VERSION = 5;
15
17
  export const V6_USER_VERSION = 6;
16
- export const CURRENT_USER_VERSION = 7;
18
+ export const V7_USER_VERSION = 7;
19
+ export const CURRENT_USER_VERSION = 8;
17
20
  export const V1_SNAPSHOT_TABLE = 'square_snapshot';
18
21
  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)`;
19
22
  const V1_SNAPSHOT_COLUMNS = [
@@ -58,18 +61,16 @@ export function validateLegacyFraming(database, applicationId, location = '') {
58
61
  function normalizedLegacySql(sql) {
59
62
  return sql.trim().replace(/\s+/g, ' ').replace(/\s*(>=|[(),=])\s*/g, '$1');
60
63
  }
61
- const CURRENT_ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','hold','resume','read','listen','ignore','unlisten')";
64
+ const CURRENT_ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','read','listen','ignore','unlisten')";
62
65
  export const ACTIVITIES_TABLE_SQL = `CREATE TABLE activities (
63
66
  act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),
64
67
  at REAL NOT NULL,
65
68
  kind TEXT NOT NULL CHECK (${CURRENT_ACTIVITY_KIND_CHECK}),
66
- actor_json TEXT COLLATE BINARY,
67
- actor_key_json TEXT COLLATE BINARY,
69
+ actor_json TEXT COLLATE BINARY NOT NULL,
70
+ actor_key_json TEXT COLLATE BINARY NOT NULL,
68
71
  reach TEXT CHECK (reach = 'bell'),
69
72
  reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),
70
73
  payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
71
- CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),
72
- CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),
73
74
  CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
74
75
  );`;
75
76
  export const ACTIVITY_INDEXES_SQL = [
@@ -182,9 +183,11 @@ CREATE TABLE landed_audience (
182
183
  UNIQUE (act_index, recipient_order)
183
184
  ) WITHOUT ROWID;
184
185
  CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
185
- export const FORMAT6_SCHEMA_SQL = 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);');
186
+ export const FORMAT7_SCHEMA_SQL = legacyActivitySchema(SCHEMA_SQL);
187
+ 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);');
186
188
  export const FORMAT5_SCHEMA_SQL = FORMAT6_SCHEMA_SQL.replace(`,\n ${ATTACHMENT_COLUMN_SQL}`, '');
187
189
  export const FORMAT4_SCHEMA_SQL = FORMAT5_SCHEMA_SQL.replace(DELIVERY_DEFERRALS_SQL, '');
190
+ const FORMAT7_SCHEMA = FORMAT7_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
188
191
  const FORMAT6_SCHEMA = FORMAT6_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
189
192
  const FORMAT5_SCHEMA = FORMAT5_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
190
193
  const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
@@ -244,17 +247,17 @@ function immutable(value) {
244
247
  }
245
248
  return value;
246
249
  }
247
- const kinds = ['join', 'done', 'say', 'hold', 'resume', 'read', 'listen', 'ignore', 'unlisten'];
250
+ const kinds = ['join', 'done', 'say', 'read', 'listen', 'ignore', 'unlisten'];
248
251
  function head(row) {
249
252
  const index = integer(row.act_index), at = finite(row.at);
250
253
  requireFact(kinds.includes(string(row.kind)));
251
- const actor = row.actor_json === null ? undefined : decoded(row.actor_json, true);
252
- requireFact(actor === undefined ? row.actor_key_json === null && ['hold', 'resume'].includes(row.kind) : row.actor_key_json === key(actor));
254
+ const actor = decoded(row.actor_json, true);
255
+ requireFact(row.actor_key_json === key(actor));
253
256
  requireFact(row.reach === null || row.kind === 'say' && row.reach === 'bell');
254
257
  if (row.reply_index !== null)
255
258
  requireFact(row.kind === 'say' && integer(row.reply_index) < index);
256
259
  return immutable({
257
- index, at, kind: row.kind, ...(actor === undefined ? {} : { actor }), ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index })
260
+ index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index })
258
261
  });
259
262
  }
260
263
  export function validateStoredActivity(value) {
@@ -262,16 +265,20 @@ export function validateStoredActivity(value) {
262
265
  integer(a.index);
263
266
  finite(a.at);
264
267
  requireFact(kinds.includes(string(a.kind)));
265
- const common = ['kind', 'at', 'index'];
266
- if (a.kind !== 'hold' && a.kind !== 'resume') {
267
- common.push('actor');
268
- string(a.actor, true);
269
- }
270
- else if (Object.hasOwn(a, 'actor'))
271
- string(a.actor, true);
268
+ const common = ['kind', 'at', 'index', 'actor'];
269
+ string(a.actor, true);
272
270
  switch (a.kind) {
273
271
  case 'say':
274
- shape(a, [...common], ['body', 'mentions', 'reach', 'reply', 'metadata']);
272
+ shape(a, [...common], ['body', 'mentions', 'reach', 'reply', 'metadata', 'delivery']);
273
+ if (Object.hasOwn(a, 'delivery')) {
274
+ requireFact(a.delivery !== undefined, 'say delivery is malformed.');
275
+ try {
276
+ validateActivityDelivery(a.delivery);
277
+ }
278
+ catch {
279
+ throw invalidArtifact('say delivery is malformed.');
280
+ }
281
+ }
275
282
  if (Object.hasOwn(a, 'body'))
276
283
  string(a.body);
277
284
  if (Object.hasOwn(a, 'metadata')) {
@@ -292,8 +299,7 @@ export function validateStoredActivity(value) {
292
299
  requireFact(integer(a.reply) < a.index);
293
300
  break;
294
301
  case 'done':
295
- case 'hold':
296
- shape(a, common, ['body', ...(a.kind === 'hold' ? ['actor'] : [])]);
302
+ shape(a, common, ['body']);
297
303
  if (Object.hasOwn(a, 'body'))
298
304
  string(a.body);
299
305
  break;
@@ -307,14 +313,14 @@ export function validateStoredActivity(value) {
307
313
  shape(a, [...common, 'target']);
308
314
  string(a.target, true);
309
315
  break;
310
- default: shape(a, common, a.kind === 'resume' ? ['actor'] : []);
316
+ default: shape(a, common);
311
317
  }
312
318
  return value;
313
319
  }
314
320
  function activity(row) {
315
321
  const h = head(row), payload = object(json(row.payload_json));
316
322
  // Payload cannot shadow indexed structural columns.
317
- const allowed = h.kind === 'say' ? ['body', 'mentions', 'metadata'] : h.kind === 'done' || h.kind === 'hold' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
323
+ 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'] : [];
318
324
  requireFact(Object.keys(payload).every(k => allowed.includes(k)));
319
325
  return immutable(validateStoredActivity({ ...h, ...payload }));
320
326
  }
@@ -403,8 +409,7 @@ function nativeCorrelation(value) {
403
409
  ...(item.messageId === undefined ? {} : { messageId: item.messageId }),
404
410
  ...(item.payloadHash === undefined ? {} : { payloadHash: item.payloadHash }) });
405
411
  }
406
- function deliveryEvidence(row) {
407
- const payload = object(json(row.payload_json));
412
+ function deliveryEvidence(row, payload = object(json(row.payload_json))) {
408
413
  const participant = decoded(row.participant_json, true);
409
414
  requireFact(row.participant_key_json === key(participant));
410
415
  const generation = activityId(decoded(row.generation_json, true));
@@ -461,7 +466,7 @@ export function validateRowFraming(db, version, location = '') {
461
466
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
462
467
  requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
463
468
  requireFact(identity.user_version === version, 'unsupported SQLite format version.');
464
- validateOwnedSchema(db, version === V4_USER_VERSION ? FORMAT4_SCHEMA : version === V5_USER_VERSION ? FORMAT5_SCHEMA : version === V6_USER_VERSION ? FORMAT6_SCHEMA : EXPECTED_SCHEMA_SQL, location);
469
+ 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);
465
470
  }
466
471
  export function validateSqliteHeader(db, location = '') {
467
472
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
@@ -470,7 +475,7 @@ export function validateSqliteHeader(db, location = '') {
470
475
  validateLegacyFraming(db, APPLICATION_ID, location);
471
476
  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' });
472
477
  }
473
- 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) {
478
+ 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) {
474
479
  validateRowFraming(db, identity.user_version, location);
475
480
  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' });
476
481
  }
@@ -494,8 +499,6 @@ function activitySelection(filter, alias = 'a') {
494
499
  add(`${alias}.act_index <= ?`, integer(filter.throughIndex, -1));
495
500
  if (filter.afterAt !== undefined)
496
501
  add(`${alias}.at > ?`, finite(filter.afterAt));
497
- if (filter.actorRequired)
498
- terms.push(`${alias}.actor_json IS NOT NULL`);
499
502
  for (const [column, values] of [['act_index', filter.indexes], ['kind', filter.kinds], [filter.actorMatch === 'exact' ? 'actor_json' : 'actor_key_json', filter.actors?.map(name => filter.actorMatch === 'exact' ? encode(name) : key(name))]]) {
500
503
  if (values !== undefined) {
501
504
  if (column === 'act_index')
@@ -540,7 +543,7 @@ export function createSqliteDriver(db) {
540
543
  }
541
544
  function directedHead(row, recipient) {
542
545
  const h = head(row);
543
- requireFact(h.kind === 'say' && h.actor !== undefined);
546
+ requireFact(h.kind === 'say');
544
547
  return immutable({
545
548
  index: h.index, at: h.at, actor: h.actor, recipient, route: routeKind(row.route)
546
549
  });
@@ -566,7 +569,7 @@ export function createSqliteDriver(db) {
566
569
  const filter = activityIndex === undefined ? '' : ' AND act_index=?';
567
570
  if (activityIndex !== undefined)
568
571
  args.push(integer(Number(activityIndex.slice(4))));
569
- return immutable(all(`SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=?${filter} ORDER BY act_index,kind,attempt_n`, args).map(deliveryEvidence));
572
+ 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)));
570
573
  },
571
574
  deliveryByToken(claimToken) {
572
575
  const row = one('SELECT * FROM delivery_evidence WHERE claim_token_json=?', [encode(claimToken)]);
@@ -596,18 +599,6 @@ export function createSqliteDriver(db) {
596
599
  roster: () => immutable(all('SELECT * FROM participants ORDER BY first_index').map(participant)),
597
600
  attention: name => immutable(attentionRows('t.listener_key_json=?', [key(name)])),
598
601
  incomingAttention: name => immutable(attentionRows('t.target_key_json=?', [key(name)])),
599
- hold() {
600
- const r = one("SELECT * FROM activities WHERE kind IN ('hold','resume') ORDER BY act_index DESC LIMIT 1");
601
- if (!r)
602
- return immutable({ active: false });
603
- const a = activity(r);
604
- if (a.kind === 'resume')
605
- return immutable({ active: false });
606
- requireFact(a.kind === 'hold');
607
- return immutable({
608
- active: true, at: a.at, ...(a.body === undefined ? {} : { reason: a.body }), ...(a.actor === undefined ? {} : { actor: a.actor })
609
- });
610
- },
611
602
  activity(index) {
612
603
  const r = one('SELECT * FROM activities WHERE act_index=?', [integer(index)]);
613
604
  return r ? activity(r) : undefined;
@@ -685,7 +676,7 @@ export function createSqliteDriver(db) {
685
676
  const groups = indices.map(r => {
686
677
  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)]);
687
678
  const h = head(rows[0]);
688
- requireFact(h.kind === 'say' && h.actor !== undefined);
679
+ requireFact(h.kind === 'say');
689
680
  return {
690
681
  index: h.index, at: h.at, actor: h.actor, recipients: rows.map(v => ({ recipient: decoded(v.name_json, true), route: routeKind(v.route) }))
691
682
  };
@@ -788,7 +779,10 @@ export function createSqliteDriver(db) {
788
779
  const a = validateStoredActivity(effect.activity);
789
780
  requireFact(a.index >= read.header().nextActIndex && a.index < Number.MAX_SAFE_INTEGER, 'activity index cannot advance.');
790
781
  const { index, at, kind, actor, reach, reply, ...payload } = a;
791
- run('INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json) VALUES(?,?,?,?,?,?,?,?)', [index, at, kind, actor === undefined ? null : encode(actor), actor === undefined ? null : key(actor), reach ?? null, reply ?? null, encode(payload)]);
782
+ receive({
783
+ 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)]); },
784
+ 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]); },
785
+ }, { item: { key: index, data: a }, audience: effect.audience.map(r => ({ recipient: r.recipient, via: r.route })) });
792
786
  if (effect.participant) {
793
787
  const p = effect.participant;
794
788
  string(p.name, true);
@@ -817,7 +811,6 @@ export function createSqliteDriver(db) {
817
811
  requireFact(t.kind === 'listen' || t.kind === 'ignore');
818
812
  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]);
819
813
  }
820
- effect.audience.forEach((r, order) => run('INSERT INTO landed_audience(act_index,recipient_key_json,recipient_order,route) VALUES(?,?,?,?)', [index, key(r.recipient), order, r.route]));
821
814
  run('UPDATE square_meta SET next_act_index=? WHERE id=1', [index + 1]);
822
815
  changed = true;
823
816
  },
@@ -825,11 +818,11 @@ export function createSqliteDriver(db) {
825
818
  finite(at);
826
819
  const next = read.header().nextActIndex;
827
820
  indexes.forEach(index => requireFact(integer(index) < next, 'receipt references an unassigned activity index.'));
828
- let count = 0;
829
- const encoded = encode(owner(name));
830
- string(owner(name), true);
831
- for (const index of indexes)
832
- count += run('INSERT OR IGNORE INTO receipts(owner_json,act_index,at) VALUES(?,?,?)', [encoded, index, at]);
821
+ const canonical = owner(name);
822
+ string(canonical, true);
823
+ const count = recordConsumed({
824
+ insertReceipt: (ref, time) => run('INSERT OR IGNORE INTO receipts(owner_json,act_index,at) VALUES(?,?,?)', [encode(ref.recipient), ref.item, time]) > 0,
825
+ }, indexes.map(item => ({ recipient: canonical, item })), at);
833
826
  if (count)
834
827
  changed = true;
835
828
  return count;
@@ -884,6 +877,40 @@ export function createSqliteDriver(db) {
884
877
  return count > 0;
885
878
  },
886
879
  };
880
+ function attempt(row) {
881
+ const data = object(json(row.payload_json)), evidence = deliveryEvidence(row, data);
882
+ return { purpose: evidence.kind === 'wake' ? 'dispatch' : 'presentation', token: evidence.claimToken ?? '',
883
+ ordinal: evidence.attemptN, outcome: evidence.outcome, at: evidence.at, expiresAt: evidence.expiresAt, data };
884
+ }
885
+ function attemptRows(seat, index, kind, known) {
886
+ const scope = [key(seat.participant), encode(seat.generation), index, kind];
887
+ let sources = [];
888
+ return {
889
+ list() {
890
+ sources = all('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND act_index=? AND kind=? ORDER BY attempt_n DESC', scope);
891
+ return sources.map(attempt);
892
+ },
893
+ evidence(value) {
894
+ const source = sources.find(row => row.attempt_n === value.ordinal);
895
+ return deliveryEvidence({ ...source, outcome: value.outcome, at: value.at, expires_at: value.expiresAt ?? null }, value.data);
896
+ },
897
+ byToken(token) {
898
+ if (known?.token === token)
899
+ return known;
900
+ 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)]);
901
+ return row === undefined ? undefined : attempt(row);
902
+ },
903
+ insert(value) {
904
+ 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)]);
905
+ deliveryChanged = true;
906
+ },
907
+ replace(value) {
908
+ 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)]);
909
+ if (count)
910
+ deliveryChanged = true;
911
+ },
912
+ };
913
+ }
887
914
  const delivery = {
888
915
  claimDelivery(input) {
889
916
  requireFact(input.kind === 'wake' || input.kind === 'presentation');
@@ -903,42 +930,24 @@ export function createSqliteDriver(db) {
903
930
  const cancelledThrough = read.cancelledThrough(input.seat.participant, input.seat.generation);
904
931
  if (cancelledThrough !== undefined && index <= cancelledThrough)
905
932
  return { status: 'stale' };
906
- const rows = all('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND act_index=? AND kind=? ORDER BY attempt_n DESC', [key(input.seat.participant), encode(input.seat.generation), index, input.kind]);
907
- const decoded = rows.map(deliveryEvidence);
908
- const terminal = decoded.find((row) => input.kind === 'wake'
909
- ? row.outcome === 'accepted' || row.outcome === 'unknown'
910
- : row.outcome === 'presented' || row.outcome === 'unknown');
911
- if (terminal !== undefined)
912
- return { status: 'terminal', evidence: terminal };
913
- const dispatching = decoded.find((row) => row.outcome === 'dispatching');
914
- if (dispatching !== undefined && (dispatching.expiresAt === undefined || dispatching.expiresAt > input.at))
915
- return { status: 'busy', evidence: dispatching };
916
- if (dispatching !== undefined) {
917
- const updated = { ...dispatching, outcome: 'unknown', at: input.at, expiresAt: undefined, diagnostic: 'interrupted dispatch' };
918
- const source = rows.find((row) => row.attempt_n === dispatching.attemptN);
919
- run('UPDATE delivery_evidence SET outcome=?,at=?,expires_at=NULL,payload_json=? WHERE participant_key_json=? AND generation_json=? AND act_index=? AND kind=? AND attempt_n=?', ['unknown', input.at, encode({ ...object(json(source.payload_json)), diagnostic: 'interrupted dispatch' }), key(input.seat.participant), encode(input.seat.generation), index, input.kind, dispatching.attemptN]);
920
- deliveryChanged = true;
921
- return { status: 'terminal', evidence: updated };
922
- }
923
- const attemptN = (decoded[0]?.attemptN ?? -1) + 1;
924
- const payload = input.route === undefined ? {} : { route: input.route };
925
- const expiry = finite(input.at + leaseMs);
926
- 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(?,?,?,?,?,?,?,?,?,?,?)', [key(input.seat.participant), encode(input.seat.participant), encode(input.seat.generation), index, input.kind, attemptN, 'dispatching', input.at, expiry, encode(input.claimToken), encode(payload)]);
927
- deliveryChanged = true;
928
- return { status: 'acquired', claimToken: input.claimToken, attemptN };
933
+ const rows = attemptRows(input.seat, index, input.kind);
934
+ const result = claimAttempt({ ...rows, replace(value) {
935
+ rows.replace({ ...value, data: { ...value.data, diagnostic: 'interrupted dispatch' } });
936
+ } }, { purpose: input.kind === 'wake' ? 'dispatch' : 'presentation', token: input.claimToken,
937
+ at: input.at, leaseMs, data: input.route === undefined ? {} : { route: input.route } });
938
+ if (result.status === 'acquired')
939
+ return { status: 'acquired', claimToken: result.token, attemptN: result.ordinal };
940
+ const evidence = result.interrupted ? { ...result.evidence, data: { ...result.evidence.data, diagnostic: 'interrupted dispatch' } } : result.evidence;
941
+ return { status: result.status, evidence: rows.evidence(evidence) };
929
942
  },
930
943
  prepareDelivery(input) {
931
944
  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)]);
932
945
  if (row === undefined || row.outcome !== 'dispatching')
933
946
  return false;
934
- const correlation = nativeCorrelation(input.correlation);
935
- const payload = object(json(row.payload_json));
947
+ const previous = attempt(row), correlation = nativeCorrelation(input.correlation), payload = previous.data;
936
948
  if (stableJson(payload.correlation ?? null) === stableJson(correlation))
937
949
  return true;
938
- const count = run('UPDATE delivery_evidence SET payload_json=? WHERE participant_key_json=? AND generation_json=? AND claim_token_json=?', [encode({ ...payload, correlation }), key(input.seat.participant), encode(input.seat.generation), encode(input.claimToken)]);
939
- if (count)
940
- deliveryChanged = true;
941
- return count > 0;
950
+ return prepareAttempt(attemptRows(input.seat, integer(row.act_index), row.kind, previous), { token: input.claimToken, data: { ...payload, correlation } });
942
951
  },
943
952
  completeDelivery(input) {
944
953
  const index = activityIndex(input.activity);
@@ -948,16 +957,9 @@ export function createSqliteDriver(db) {
948
957
  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)]);
949
958
  if (row === undefined)
950
959
  return false;
951
- if (row.outcome === 'accepted' && input.outcome === 'accepted')
952
- return true;
953
- if (row.outcome !== 'dispatching' && !(row.outcome === 'unknown' && input.outcome === 'accepted'))
954
- return false;
955
- const previous = object(json(row.payload_json));
956
- const payload = { ...previous, ...(input.signature === undefined ? {} : { signature: input.signature }), ...(input.message === undefined ? {} : { message: input.message }), ...(input.diagnostic === undefined ? {} : { diagnostic: input.diagnostic }), ...(input.correlation === undefined ? {} : { correlation: input.correlation }) };
957
- const count = run('UPDATE delivery_evidence SET outcome=?,at=?,expires_at=NULL,payload_json=? WHERE participant_key_json=? AND generation_json=? AND act_index=? AND claim_token_json=?', [input.outcome, input.at, encode(payload), key(input.seat.participant), encode(input.seat.generation), index, encode(input.claimToken)]);
958
- if (count)
959
- deliveryChanged = true;
960
- return count > 0;
960
+ const previous = attempt(row);
961
+ 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 }) };
962
+ return settleAttempt(attemptRows(input.seat, index, row.kind, previous), { token: input.claimToken, outcome: input.outcome, at: input.at, data: payload });
961
963
  },
962
964
  observeNativeDelivery(input) {
963
965
  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)]);
@@ -1071,6 +1073,12 @@ function inspectRows(db, inspectSeat) {
1071
1073
  const rebuilt = new MemoryDatabase(':memory:');
1072
1074
  try {
1073
1075
  initializeSqlite(rebuilt, state, d.header().revision);
1076
+ // A migration may remove the activity that established canonical casing.
1077
+ // Retained participant spelling stays authoritative while replay checks
1078
+ // every derived lifecycle/count fact against surviving activities.
1079
+ const retainName = rebuilt.prepare('UPDATE participants SET name_json=? WHERE name_key_json=?');
1080
+ for (const p of d.roster())
1081
+ retainName.run(encode(p.name), key(p.name));
1074
1082
  for (const table of ['participants', 'attention', 'landed_audience']) {
1075
1083
  const original = db.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
1076
1084
  const expected = rebuilt.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
@@ -13,6 +13,7 @@ interface ToolDefinition {
13
13
  /** Human text identical to what the CLI prints; domain receipts stay inside the adapter. */
14
14
  run(square: Square, args: JsonObject, control: OperationControl, identity: ToolIdentity): Promise<string>;
15
15
  }
16
+ export declare function squareToolsEnabled(env?: NodeJS.ProcessEnv): boolean;
16
17
  export declare const SQUARE_TOOL_INSTRUCTIONS = "Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.";
17
18
  export declare const SQUARE_TOOLS: readonly ToolDefinition[];
18
19
  export declare function squareToolFailure(error: unknown, signal?: AbortSignal): JsonObject;