@astrosheep/square 0.3.88 → 0.3.90

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 (39) hide show
  1. package/dist/artifact.js +11 -9
  2. package/dist/boundary-presentation.d.ts +3 -2
  3. package/dist/boundary-presentation.js +50 -77
  4. package/dist/delivery-deferral.d.ts +28 -0
  5. package/dist/delivery-deferral.js +83 -0
  6. package/dist/delivery-operations.js +8 -6
  7. package/dist/delivery.js +4 -1
  8. package/dist/inbox.js +21 -10
  9. package/dist/model.d.ts +1 -0
  10. package/dist/ports.d.ts +11 -3
  11. package/dist/presentation-operations.d.ts +20 -1
  12. package/dist/presentation-operations.js +68 -44
  13. package/dist/sqlite-square.js +29 -0
  14. package/dist/square-domain.d.ts +1 -1
  15. package/dist/square-domain.js +12 -0
  16. package/dist/square-facade.d.ts +8 -0
  17. package/dist/square-migration.d.ts +2 -1
  18. package/dist/square-migration.js +25 -26
  19. package/dist/square-sqlite.d.ts +5 -2
  20. package/dist/square-sqlite.js +79 -6
  21. package/dist/square-stdio.d.ts +1 -1
  22. package/dist/square-stdio.js +30 -17
  23. package/dist/stdio-server.js +23 -3
  24. package/guides/backends.md +26 -13
  25. package/guides/change-waits.md +40 -11
  26. package/guides/migration.md +30 -12
  27. package/guides/operation-cancellation.md +16 -1
  28. package/guides/participant-ownership.md +60 -186
  29. package/guides/square-api.md +72 -9
  30. package/package.json +1 -1
  31. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  32. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  33. package/plugins/paseo/README.md +7 -6
  34. package/plugins/paseo/client/markdown.tsx +125 -185
  35. package/plugins/paseo/client/square-message.tsx +9 -25
  36. package/plugins/paseo/client/transform.ts +5 -4
  37. package/plugins/paseo/index.client.tsx +14 -12
  38. package/plugins/paseo/package.json +3 -0
  39. package/plugins/paseo/shared/message.ts +50 -1
@@ -2,71 +2,95 @@ import { randomUUID } from 'node:crypto';
2
2
  import { formatActivityId, parseActivityId } from './square-core.js';
3
3
  import { isSquareError, sameBody } from './model.js';
4
4
  import { pendingHead, sameJoin } from './delivery.js';
5
+ import { deliveryDeferralExpiry } from './delivery-deferral.js';
5
6
  import { asSeatRef } from './seats.js';
6
- function currentFor(input, captured, participant, index) {
7
- return (facts) => {
8
- const seat = facts.seat(captured.participant);
9
- return seat !== undefined && sameBody(seat.body, captured.body) && seat.generation === captured.generation
10
- && sameJoin(facts, participant) && input.current?.(facts) !== false
11
- && pendingHead(facts, captured.participant, index) !== undefined;
12
- };
13
- }
14
- /** Presentation is external; its claim and receipt/evidence commit are generation-fenced in the artifact. */
15
- export async function presentPending(input) {
7
+ /** Prepare custody before any external effect. A batch prepares every included item first. */
8
+ export async function preparePendingPresentation(input) {
16
9
  const index = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
17
10
  if (index === undefined)
18
- return { presented: false };
11
+ return undefined;
19
12
  const initial = await input.artifact.read((facts) => {
20
13
  const participant = facts.participant(input.participant);
21
14
  const seat = facts.seat(input.participant);
22
15
  if (!participant?.joined || seat === undefined || input.generation !== undefined && input.generation !== seat.generation
23
16
  || input.seat !== undefined && (input.seat.generation !== seat.generation || !sameBody(input.seat.body, seat.body))
24
- || input.current?.(facts) === false || !pendingHead(facts, participant.name, index))
17
+ || input.current?.(facts) === false || !pendingHead(facts, participant.name, index)
18
+ || deliveryDeferralExpiry(facts, seat, index, input.now ?? Date.now()) !== undefined)
25
19
  return undefined;
26
20
  const item = facts.activity(index);
27
21
  return item?.kind === 'say' ? { participant, seat, item } : undefined;
28
22
  }, input.signal).catch(() => undefined);
29
23
  if (initial?.value === undefined)
30
- return { presented: false };
24
+ return undefined;
31
25
  const { participant, seat, item } = initial.value;
32
26
  const ref = asSeatRef(seat);
33
- const current = currentFor(input, seat, participant, index);
27
+ const owns = (facts) => {
28
+ const current = facts.seat(seat.participant);
29
+ return current !== undefined && sameBody(current.body, seat.body) && current.generation === seat.generation
30
+ && sameJoin(facts, participant);
31
+ };
34
32
  const claimToken = randomUUID();
35
33
  const claimed = await input.artifact.claimDelivery({ seat: ref, activity: formatActivityId(index), kind: 'presentation',
36
34
  at: input.now ?? Date.now(), leaseMs: input.timeoutMs ?? 5000, claimToken }, input.signal).catch(() => ({ status: 'stale' }));
37
35
  if (claimed.status !== 'acquired')
36
+ return undefined;
37
+ let effectObserved = false, released = false;
38
+ return {
39
+ activity: item,
40
+ async eligible() {
41
+ return (await input.artifact.read(facts => owns(facts) && input.current?.(facts) !== false
42
+ && pendingHead(facts, participant.name, index) !== undefined
43
+ && deliveryDeferralExpiry(facts, seat, index, input.now ?? Date.now()) === undefined, input.signal).catch(() => undefined))?.value === true;
44
+ },
45
+ async release(outcome = 'released', message) {
46
+ if (effectObserved || released)
47
+ return;
48
+ released = true;
49
+ await input.artifact.settleAs({ kind: 'participant', seat: ref }, facts => facts.completeDelivery({
50
+ seat: ref, activity: formatActivityId(index), claimToken, outcome, at: Date.now(),
51
+ ...(message === undefined ? {} : { message }),
52
+ })).catch(() => false);
53
+ },
54
+ async recordPresented() {
55
+ // Success is an observed external fact, not another eligibility decision. Neither
56
+ // post-effect cancellation nor a new deferral can erase it and authorize replay.
57
+ effectObserved = true;
58
+ const outcome = input.markSeen === false ? 'clipped' : 'presented';
59
+ const { value: committed } = await input.artifact.settleAs({ kind: 'participant', seat: ref }, facts => {
60
+ if (!owns(facts))
61
+ return false;
62
+ if (!facts.completeDelivery({ seat: ref, activity: formatActivityId(index), claimToken, outcome, at: input.now ?? Date.now() }))
63
+ return false;
64
+ if (input.markSeen !== false)
65
+ facts.markSeen(participant.name, [index], input.now ?? Date.now());
66
+ return true;
67
+ }).catch(error => {
68
+ if (isSquareError(error) && error.code === 'already_joined')
69
+ return { value: false };
70
+ throw error;
71
+ });
72
+ return committed ? { presented: true, activity: item } : { presented: false };
73
+ },
74
+ };
75
+ }
76
+ /** Eligibility precedes the sink; only captured ownership fences its truthful completion. */
77
+ export async function presentPending(input) {
78
+ const prepared = await preparePendingPresentation(input);
79
+ if (prepared === undefined)
38
80
  return { presented: false };
39
- const release = async (outcome, message) => input.artifact.completeDelivery({
40
- seat: ref, activity: formatActivityId(index), claimToken, outcome, at: Date.now(),
41
- ...(message === undefined ? {} : { message }),
42
- }, input.signal).catch(() => false);
43
- const fresh = await input.artifact.read(current, input.signal).catch(() => undefined);
44
- if (!fresh?.value) {
45
- await release('released', 'The notification changed before presentation.');
46
- return { presented: false };
47
- }
48
81
  try {
49
- await input.sink.present(item);
82
+ if (!await prepared.eligible() || input.signal?.aborted)
83
+ return { presented: false };
84
+ try {
85
+ await input.sink.present(prepared.activity);
86
+ }
87
+ catch (error) {
88
+ await prepared.release('failed', error instanceof Error ? error.message : String(error));
89
+ throw error;
90
+ }
91
+ return await prepared.recordPresented();
50
92
  }
51
- catch (error) {
52
- await release('failed', error instanceof Error ? error.message : String(error));
53
- throw error;
93
+ finally {
94
+ await prepared.release();
54
95
  }
55
- const outcome = input.markSeen === false ? 'clipped' : 'presented';
56
- const { value: committed } = await input.artifact.transactAs({ kind: 'participant', seat: ref }, (facts) => {
57
- if (!current(facts))
58
- return false;
59
- if (!facts.completeDelivery({ seat: ref, activity: formatActivityId(index), claimToken, outcome, at: input.now ?? Date.now() }))
60
- return false;
61
- if (input.markSeen === false)
62
- return true;
63
- return facts.markSeen(participant.name, [index], input.now ?? Date.now()) > 0;
64
- }, input.signal).catch(error => {
65
- if (isSquareError(error) && error.code === 'already_joined')
66
- return { value: false };
67
- throw error;
68
- });
69
- if (!committed)
70
- return { presented: false };
71
- return { presented: true, activity: item };
72
96
  }
@@ -1,4 +1,5 @@
1
1
  import { formatActivityId } from './square-core.js';
2
+ import { validateMetadataMatch, validateDeferralDuration, validateDeferralId } from './delivery-deferral.js';
2
3
  import { squareLocation, localSquareLocation } from './square-location.js';
3
4
  import crypto from 'node:crypto';
4
5
  import { setTimeout as sleep } from 'node:timers/promises';
@@ -382,6 +383,34 @@ export class SqliteSquare {
382
383
  await releaseWatchLease(square.artifact, seat, leaseId).catch(() => undefined);
383
384
  }
384
385
  }
386
+ async deferDelivery(input, control) {
387
+ checkControl(control);
388
+ const match = validateMetadataMatch(input?.match), duration = validateDeferralDuration(input?.durationMs);
389
+ const square = this.owner, seat = requireSeat(this.seat, square.body, this.name);
390
+ return (await square.artifact.transactAs(participantAuthority(seat), facts => {
391
+ const now = square.clock(), expiresAt = now + duration, id = crypto.randomUUID();
392
+ const result = { id, expiresAt: new Date(expiresAt).toISOString() };
393
+ facts.putDeliveryDeferral(asSeatRef(seat), id, match, expiresAt, now);
394
+ return result;
395
+ }, control?.signal)).value;
396
+ }
397
+ async renewDeliveryDeferral(id, durationMs, control) {
398
+ checkControl(control);
399
+ validateDeferralId(id);
400
+ const duration = validateDeferralDuration(durationMs), square = this.owner;
401
+ const seat = requireSeat(this.seat, square.body, this.name);
402
+ return (await square.artifact.transactAs(participantAuthority(seat), facts => {
403
+ const now = square.clock(), expiresAt = now + duration;
404
+ const result = { id, expiresAt: new Date(expiresAt).toISOString() };
405
+ return facts.renewDeliveryDeferral(asSeatRef(seat), id, expiresAt, now) ? result : null;
406
+ }, control?.signal)).value;
407
+ }
408
+ async releaseDeliveryDeferral(id, control) {
409
+ checkControl(control);
410
+ validateDeferralId(id);
411
+ const square = this.owner, seat = requireSeat(this.seat, square.body, this.name);
412
+ return (await square.artifact.transactAs(participantAuthority(seat), facts => facts.releaseDeliveryDeferral(asSeatRef(seat), id, square.clock()), control?.signal)).value;
413
+ }
385
414
  history(query, control) { return this.owner.history(query, control); }
386
415
  async lifecycle(verb, body, options, control) {
387
416
  if (verb === 'done')
@@ -21,7 +21,7 @@ export interface LandingFacts {
21
21
  /** The single landing transition, shared by both adapters and explicit rebuild. */
22
22
  export declare function deriveLanding(activity: StoredAct, facts: LandingFacts): LandingEffect;
23
23
  export declare function landingFacts(read: SquareRead, activity: StoredAct): LandingFacts;
24
- export interface LandingWriter extends Pick<SquareWrite, 'markSeen' | 'putLease' | 'removeLease' | 'updateWakeAddress' | 'cancelThrough' | 'claimDelivery' | 'prepareDelivery' | 'completeDelivery' | 'observeNativeDelivery'> {
24
+ export interface LandingWriter extends Pick<SquareWrite, 'markSeen' | 'putLease' | 'removeLease' | 'updateWakeAddress' | 'cancelThrough' | 'putDeliveryDeferral' | 'renewDeliveryDeferral' | 'releaseDeliveryDeferral' | 'claimDelivery' | 'prepareDelivery' | 'completeDelivery' | 'observeNativeDelivery'> {
25
25
  /** Artifact-private seat persistence used only by this gate's lifecycle transition. */
26
26
  storeSeat(seat: Seat): boolean;
27
27
  removeSeat(seat: import('./ports.js').SeatRef): boolean;
@@ -206,6 +206,18 @@ export function wrapSquareWrite(read, writes, authority) {
206
206
  assertParticipant(name, generation);
207
207
  return mutations.removeLease(name, leaseId, generation);
208
208
  },
209
+ putDeliveryDeferral(...args) {
210
+ assertSeat(args[0]);
211
+ return mutations.putDeliveryDeferral(...args);
212
+ },
213
+ renewDeliveryDeferral(...args) {
214
+ assertSeat(args[0]);
215
+ return mutations.renewDeliveryDeferral(...args);
216
+ },
217
+ releaseDeliveryDeferral(...args) {
218
+ assertSeat(args[0]);
219
+ return mutations.releaseDeliveryDeferral(...args);
220
+ },
209
221
  claimDelivery(input) {
210
222
  assertSeat(input.seat);
211
223
  return mutations.claimDelivery(input);
@@ -1,4 +1,6 @@
1
1
  import type { SquareBackends } from './square-backend.js';
2
+ import type { DeliveryDeferral, MetadataMatch } from './delivery-deferral.js';
3
+ export type { DeliveryDeferral, MetadataMatch, MetadataScalar } from './delivery-deferral.js';
2
4
  import type { ActivityId, Perception, Reach } from './square-core.js';
3
5
  import type { Body, StoredAct, WakeAddress, ActivityMetadata } from './model.js';
4
6
  import type { WakeTransportPort, DeliveryResult, SquareResourceOwner } from './ports.js';
@@ -203,6 +205,12 @@ export interface Participant {
203
205
  resume(options?: ObservationOptions, control?: OperationControl): Promise<LifecycleResult>;
204
206
  /** Leave without a message. Only an omitted or empty body is accepted. */
205
207
  done(body?: string, options?: ObservationOptions, control?: OperationControl): Promise<LifecycleResult>;
208
+ deferDelivery(input: Readonly<{
209
+ match: MetadataMatch;
210
+ durationMs: number;
211
+ }>, control?: OperationControl): Promise<DeliveryDeferral>;
212
+ renewDeliveryDeferral(id: string, durationMs: number, control?: OperationControl): Promise<DeliveryDeferral | null>;
213
+ releaseDeliveryDeferral(id: string, control?: OperationControl): Promise<boolean>;
206
214
  }
207
215
  export type CatchTerminalStatus = 'already-active' | 'replaced' | 'capped' | 'quorum';
208
216
  export type CatchStopStatus = CatchTerminalStatus | 'empty-now' | 'stale';
@@ -15,7 +15,8 @@ export interface MigrationOutcome {
15
15
  }
16
16
  /**
17
17
  * Explicit conversion of the supported legacy formats. Row-backed versions 2 and 3 are
18
- * preflighted read-only, then receive the format-4 ownership/evidence rows in one transaction.
18
+ * preflighted read-only, then receive ownership/evidence and deferral storage in one
19
+ * format-5 transaction. Format 4 adds only the empty deferral table and its index.
19
20
  * Legacy delivery evidence is copied from HostLedger only when its owner/session mapping is
20
21
  * unique; no source record is retired. Version 1 is not a supported source for this format transition.
21
22
  */
@@ -5,8 +5,8 @@ import { canonicalPath, canonicalPathSync } from './canonical-path.js';
5
5
  import { formatActivityId, isWakeRouteKind, nameKey, sameBody, validateBody } from './model.js';
6
6
  import { parseActivityId } from './square-core.js';
7
7
  import { readLegacyHostData } from './square-migration-host-ledger.js';
8
- import { legacyRowFramingIssue, FORMAT4_ADDED_OBJECTS, FORMAT4_REPLACED_OBJECTS } from './square-legacy-framing.js';
9
- import { APPLICATION_ID, ACTIVITIES_TABLE_SQL, ACTIVITY_INDEXES_SQL, CURRENT_USER_VERSION, V2_USER_VERSION, V3_USER_VERSION, SCHEMA_SQL, createSqliteDriver, invalidArtifact, validateStoredActivity } from './square-sqlite.js';
8
+ import { legacyRowFramingIssue, FORMAT4_ADDED_OBJECTS } from './square-legacy-framing.js';
9
+ import { APPLICATION_ID, ACTIVITIES_TABLE_SQL, ACTIVITY_INDEXES_SQL, CURRENT_USER_VERSION, V2_USER_VERSION, V3_USER_VERSION, V4_USER_VERSION, SCHEMA_SQL, FORMAT4_SCHEMA_SQL, DELIVERY_DEFERRALS_SQL, validateRowFraming, inspectSqlite, createSqliteDriver, invalidArtifact, validateStoredActivity } from './square-sqlite.js';
10
10
  function isObject(value) {
11
11
  return typeof value === 'object' && value !== null && !Array.isArray(value);
12
12
  }
@@ -140,26 +140,8 @@ function rebuildV2Activities(database) {
140
140
  ...ACTIVITY_INDEXES_SQL.map((sql) => `${sql};`),
141
141
  ].join('\n'));
142
142
  }
143
- function normalizedSql(sql) {
144
- return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1');
145
- }
146
- function currentFormat4Schema() {
147
- return SCHEMA_SQL.split(';').map((statement) => statement.trim()).filter(Boolean)
148
- .filter((statement) => {
149
- const match = /^(?:CREATE (?:UNIQUE )?TABLE|CREATE (?:UNIQUE )?INDEX)\s+([a-z_]+)/i.exec(statement);
150
- return match === null || !FORMAT4_REPLACED_OBJECTS.has(match[1]);
151
- }).map(normalizedSql).sort();
152
- }
153
- function validateFormat4Framing(database, location) {
154
- const before = identity(database);
155
- assertSource(before.applicationId === APPLICATION_ID, 'unrelated SQLite database.');
156
- assertSource(before.userVersion === CURRENT_USER_VERSION, 'unsupported SQLite format version.');
157
- const actual = database.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all()
158
- .map((row) => typeof row.sql === 'string' ? normalizedSql(row.sql) : '').sort();
159
- const expected = currentFormat4Schema();
160
- assertSource(actual.length === expected.length && actual.every((sql, index) => sql === expected[index]), 'unsupported row schema.');
161
- const journalMode = database.prepare('PRAGMA journal_mode').get().journal_mode;
162
- assertSource(journalMode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
143
+ function validateCurrentFraming(database, location) {
144
+ validateRowFraming(database, CURRENT_USER_VERSION, location);
163
145
  createSqliteDriver(database).header();
164
146
  }
165
147
  function assertSource(condition, detail = 'row is malformed.') {
@@ -474,28 +456,35 @@ function upgradeRowArtifact(database, location, plan) {
474
456
  currentAdditionStatements().forEach((statement) => database.exec(statement));
475
457
  importOwnershipAndEvidence(database, plan);
476
458
  database.exec('DROP TABLE routes;');
459
+ database.exec(DELIVERY_DEFERRALS_SQL);
477
460
  database.exec(`PRAGMA user_version = ${CURRENT_USER_VERSION};`);
478
- validateFormat4Framing(database, location);
461
+ validateCurrentFraming(database, location);
479
462
  }
480
463
  function validateMigrationSource(database, location) {
481
464
  const before = identity(database);
482
465
  if (before.applicationId !== APPLICATION_ID)
483
466
  throw invalidArtifact('unrelated SQLite database.');
484
467
  if (before.userVersion === V2_USER_VERSION || before.userVersion === V3_USER_VERSION) {
485
- const issue = legacyRowFramingIssue(database, APPLICATION_ID, before.userVersion, location, SCHEMA_SQL);
468
+ const issue = legacyRowFramingIssue(database, APPLICATION_ID, before.userVersion, location, FORMAT4_SCHEMA_SQL);
486
469
  if (issue !== undefined)
487
470
  throw invalidArtifact(issue);
488
471
  return before.userVersion;
489
472
  }
473
+ if (before.userVersion === V4_USER_VERSION) {
474
+ validateRowFraming(database, V4_USER_VERSION, location);
475
+ inspectSqlite(database);
476
+ return V4_USER_VERSION;
477
+ }
490
478
  if (before.userVersion === CURRENT_USER_VERSION) {
491
- validateFormat4Framing(database, location);
479
+ validateCurrentFraming(database, location);
492
480
  return undefined;
493
481
  }
494
482
  throw invalidArtifact('unsupported SQLite format version.');
495
483
  }
496
484
  /**
497
485
  * Explicit conversion of the supported legacy formats. Row-backed versions 2 and 3 are
498
- * preflighted read-only, then receive the format-4 ownership/evidence rows in one transaction.
486
+ * preflighted read-only, then receive ownership/evidence and deferral storage in one
487
+ * format-5 transaction. Format 4 adds only the empty deferral table and its index.
499
488
  * Legacy delivery evidence is copied from HostLedger only when its owner/session mapping is
500
489
  * unique; no source record is retired. Version 1 is not a supported source for this format transition.
501
490
  */
@@ -506,6 +495,16 @@ export async function migrateSquareFile(location, signal, env = process.env) {
506
495
  const source = await inSqliteTransaction(database, false, () => validateMigrationSource(database, squarePath), value => value, signal);
507
496
  if (source === undefined)
508
497
  return { status: 'already-current', standing: standing(database) };
498
+ if (source === V4_USER_VERSION) {
499
+ return await inSqliteTransaction(database, true, () => {
500
+ assertSource(validateMigrationSource(database, squarePath) === source, 'source format changed during migration preflight.');
501
+ }, () => {
502
+ database.exec(DELIVERY_DEFERRALS_SQL);
503
+ database.exec(`PRAGMA user_version = ${CURRENT_USER_VERSION};`);
504
+ validateCurrentFraming(database, squarePath);
505
+ return { status: 'migrated', standing: standing(database) };
506
+ }, signal);
507
+ }
509
508
  const host = await readLegacyHostData(squarePath, env, signal);
510
509
  const plan = await inSqliteTransaction(database, false, () => {
511
510
  assertSource(validateMigrationSource(database, squarePath) === source, 'source format changed during migration preflight.');
@@ -20,7 +20,8 @@ export declare function emptyRuntimeState(nextActIndex?: number): SquareRuntimeS
20
20
  export declare const APPLICATION_ID = 1397838405;
21
21
  export declare const V2_USER_VERSION = 2;
22
22
  export declare const V3_USER_VERSION = 3;
23
- export declare const CURRENT_USER_VERSION = 4;
23
+ export declare const V4_USER_VERSION = 4;
24
+ export declare const CURRENT_USER_VERSION = 5;
24
25
  export declare const V1_SNAPSHOT_TABLE = "square_snapshot";
25
26
  export interface LegacyFraming {
26
27
  readonly applicationId: number;
@@ -36,11 +37,13 @@ export interface LegacyFraming {
36
37
  export declare function validateLegacyFraming(database: DatabaseSync, applicationId: number, location?: string): LegacyFraming;
37
38
  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);";
38
39
  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'"];
40
+ 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);";
39
41
  export declare const SCHEMA_SQL: string;
42
+ export declare const FORMAT4_SCHEMA_SQL: string;
40
43
  export declare function invalidArtifact(detail: string): SquareError;
41
44
  export declare function validateStoredActivity(value: unknown): StoredAct;
42
45
  export declare function validateWatchLease(value: unknown): WatchLease;
43
- export declare function validateRowFraming(db: DatabaseSync, version: 2 | 3 | 4, location?: string): void;
46
+ export declare function validateRowFraming(db: DatabaseSync, version: 2 | 3 | 4 | 5, location?: string): void;
44
47
  export declare function validateSqliteHeader(db: DatabaseSync, location?: string): SquareHeader;
45
48
  export interface SqliteDriver extends SquareRead, LandingWriter {
46
49
  readonly changed: boolean;
@@ -1,5 +1,6 @@
1
1
  import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
2
- import { SquareError, nameKey, isWakeRouteKind, validateBody, validateActivityMetadata, activityMetadataHasContent } from './model.js';
2
+ import { sameBody, SquareError, nameKey, isWakeRouteKind, validateBody, validateActivityMetadata, activityMetadataHasContent } from './model.js';
3
+ import { validateMetadataMatch, matchingDeferrals, validateDeferralId } from './delivery-deferral.js';
3
4
  import { deriveLanding, landingFacts } from './square-domain.js';
4
5
  import { legacyRowFramingIssue } from './square-legacy-framing.js';
5
6
  export function emptyRuntimeState(nextActIndex = 0) {
@@ -9,7 +10,8 @@ export const APPLICATION_ID = 0x53515245;
9
10
  const V1_USER_VERSION = 1;
10
11
  export const V2_USER_VERSION = 2;
11
12
  export const V3_USER_VERSION = 3;
12
- export const CURRENT_USER_VERSION = 4;
13
+ export const V4_USER_VERSION = 4;
14
+ export const CURRENT_USER_VERSION = 5;
13
15
  export const V1_SNAPSHOT_TABLE = 'square_snapshot';
14
16
  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)`;
15
17
  const V1_SNAPSHOT_COLUMNS = [
@@ -75,6 +77,16 @@ export const ACTIVITY_INDEXES_SQL = [
75
77
  "CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'",
76
78
  "CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'",
77
79
  ];
80
+ export const DELIVERY_DEFERRALS_SQL = `CREATE TABLE delivery_deferrals (
81
+ participant_key_json TEXT COLLATE BINARY NOT NULL,
82
+ participant_json TEXT NOT NULL,
83
+ generation_json TEXT NOT NULL,
84
+ deferral_id_json TEXT NOT NULL,
85
+ match_json TEXT NOT NULL CHECK (json_valid(match_json)),
86
+ expires_at REAL NOT NULL,
87
+ PRIMARY KEY (participant_key_json,generation_json,deferral_id_json)
88
+ ) WITHOUT ROWID;
89
+ CREATE INDEX delivery_deferral_expiry ON delivery_deferrals(participant_key_json,generation_json,expires_at);`;
78
90
  export const SCHEMA_SQL = `CREATE TABLE square_meta (
79
91
  id INTEGER PRIMARY KEY CHECK (id = 1),
80
92
  revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),
@@ -137,6 +149,8 @@ CREATE TABLE delivery_cancellations (
137
149
  PRIMARY KEY (participant_key_json,generation_json)
138
150
  ) WITHOUT ROWID;
139
151
 
152
+ ${DELIVERY_DEFERRALS_SQL}
153
+
140
154
  CREATE TABLE participants (
141
155
  name_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
142
156
  name_json TEXT NOT NULL,
@@ -166,7 +180,9 @@ CREATE TABLE landed_audience (
166
180
  UNIQUE (act_index, recipient_order)
167
181
  ) WITHOUT ROWID;
168
182
  CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
183
+ export const FORMAT4_SCHEMA_SQL = SCHEMA_SQL.replace(DELIVERY_DEFERRALS_SQL, '');
169
184
  const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
185
+ const FORMAT4_SCHEMA = FORMAT4_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
170
186
  const encode = JSON.stringify;
171
187
  const key = (name) => encode(nameKey(name));
172
188
  export function invalidArtifact(detail) {
@@ -412,6 +428,15 @@ function deliveryEvidence(row) {
412
428
  ...(nativeObservations === undefined ? {} : { nativeObservations }),
413
429
  });
414
430
  }
431
+ function deliveryDeferral(row) {
432
+ const participant = decoded(row.participant_json, true);
433
+ requireFact(row.participant_key_json === key(participant));
434
+ const generation = activityId(decoded(row.generation_json, true));
435
+ const id = validateDeferralId(decoded(row.deferral_id_json, true));
436
+ const match = validateMetadataMatch(json(row.match_json));
437
+ requireFact(encode(match) === row.match_json, 'delivery deferral match encoding is malformed.');
438
+ return immutable({ participant, generation, id, match, expiresAt: finite(row.expires_at) });
439
+ }
415
440
  function normalizedSql(sql) { return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1'); }
416
441
  function validateOwnedSchema(db, expected, location) {
417
442
  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();
@@ -420,7 +445,7 @@ function validateOwnedSchema(db, expected, location) {
420
445
  }
421
446
  export function validateRowFraming(db, version, location = '') {
422
447
  if (version === V2_USER_VERSION || version === V3_USER_VERSION) {
423
- const issue = legacyRowFramingIssue(db, APPLICATION_ID, version, location, SCHEMA_SQL);
448
+ const issue = legacyRowFramingIssue(db, APPLICATION_ID, version, location, FORMAT4_SCHEMA_SQL);
424
449
  if (issue !== undefined)
425
450
  throw invalidArtifact(issue);
426
451
  return;
@@ -428,7 +453,7 @@ export function validateRowFraming(db, version, location = '') {
428
453
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
429
454
  requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
430
455
  requireFact(identity.user_version === version, 'unsupported SQLite format version.');
431
- validateOwnedSchema(db, EXPECTED_SCHEMA_SQL, location);
456
+ validateOwnedSchema(db, version === V4_USER_VERSION ? FORMAT4_SCHEMA : EXPECTED_SCHEMA_SQL, location);
432
457
  }
433
458
  export function validateSqliteHeader(db, location = '') {
434
459
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
@@ -437,7 +462,7 @@ export function validateSqliteHeader(db, location = '') {
437
462
  validateLegacyFraming(db, APPLICATION_ID, location);
438
463
  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' });
439
464
  }
440
- if (identity.user_version === V2_USER_VERSION || identity.user_version === V3_USER_VERSION) {
465
+ if (identity.user_version === V2_USER_VERSION || identity.user_version === V3_USER_VERSION || identity.user_version === V4_USER_VERSION) {
441
466
  validateRowFraming(db, identity.user_version, location);
442
467
  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' });
443
468
  }
@@ -669,6 +694,13 @@ export function createSqliteDriver(db) {
669
694
  return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorActivityIndex: integer(e.latest) }) });
670
695
  },
671
696
  receiptAssignments: () => immutable(all('SELECT owner_json,act_index FROM receipts ORDER BY owner_json,act_index').map(r => ({ owner: decoded(r.owner_json, true), index: integer(r.act_index) }))),
697
+ deliveryDeferrals(name, generation) {
698
+ const canonical = owner(name);
699
+ const current = read.seat(canonical);
700
+ if (current === undefined || current.generation !== generation)
701
+ return [];
702
+ 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));
703
+ },
672
704
  lease(name) {
673
705
  const canonical = owner(name), current = read.seat(canonical);
674
706
  const r = current === undefined
@@ -690,6 +722,7 @@ export function createSqliteDriver(db) {
690
722
  const wake = value.wakeAddress === undefined ? null : encode(value.wakeAddress);
691
723
  run('INSERT INTO seats(participant_key_json,participant_json,channel_json,session_json,agent_json,generation_json,wake_address_json) VALUES(?,?,?,?,?,?,?) ON CONFLICT(participant_key_json) DO UPDATE SET participant_json=excluded.participant_json,channel_json=excluded.channel_json,session_json=excluded.session_json,agent_json=excluded.agent_json,generation_json=excluded.generation_json,wake_address_json=excluded.wake_address_json', [key(value.participant), encode(value.participant), encode(value.body.channel), encode(value.body.session), value.body.agent === undefined ? null : encode(value.body.agent), encode(value.generation), wake]);
692
724
  run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
725
+ run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(value.participant)]);
693
726
  deliveryChanged = true;
694
727
  return true;
695
728
  },
@@ -713,6 +746,7 @@ export function createSqliteDriver(db) {
713
746
  return false;
714
747
  run('UPDATE seats SET wake_address_json=NULL WHERE participant_key_json=?', [key(value.participant)]);
715
748
  run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
749
+ run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(value.participant)]);
716
750
  changed = true;
717
751
  deliveryChanged = true;
718
752
  return true;
@@ -791,6 +825,32 @@ export function createSqliteDriver(db) {
791
825
  changed = true;
792
826
  return true;
793
827
  },
828
+ putDeliveryDeferral(seatRef, id, match, expiresAt, now) {
829
+ finite(now);
830
+ finite(expiresAt);
831
+ const canonicalMatch = validateMetadataMatch(match);
832
+ run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
833
+ 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]);
834
+ deliveryChanged = true;
835
+ return true;
836
+ },
837
+ renewDeliveryDeferral(seatRef, id, expiresAt, now) {
838
+ finite(now);
839
+ finite(expiresAt);
840
+ const pruned = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
841
+ 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]);
842
+ if (count || pruned)
843
+ deliveryChanged = true;
844
+ return count > 0;
845
+ },
846
+ releaseDeliveryDeferral(seatRef, id, now) {
847
+ finite(now);
848
+ const pruned = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
849
+ 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]);
850
+ if (count || pruned)
851
+ deliveryChanged = true;
852
+ return count > 0;
853
+ },
794
854
  removeLease(name, expected, generation) {
795
855
  const canonical = owner(name), row = one('SELECT generation_json FROM catch_leases WHERE owner_json=? AND lease_id_json=?', [encode(canonical), encode(expected)]);
796
856
  if (row === undefined || generation !== undefined && decoded(row.generation_json) !== generation)
@@ -810,8 +870,13 @@ export function createSqliteDriver(db) {
810
870
  string(input.claimToken, true);
811
871
  if (input.route !== undefined)
812
872
  wakeAddress(input.route);
813
- if (read.activity(index)?.kind !== 'say' || read.directedAt(input.seat.participant, index) === undefined)
873
+ const currentSeat = read.seat(input.seat.participant);
874
+ const item = read.activity(index);
875
+ if (currentSeat === undefined || currentSeat.generation !== input.seat.generation || !sameBody(currentSeat.body, input.seat.body)
876
+ || item?.kind !== 'say' || read.directedAt(input.seat.participant, index) === undefined)
814
877
  return { status: 'stale' };
878
+ if (matchingDeferrals(read.deliveryDeferrals(currentSeat.participant, currentSeat.generation), item.metadata, input.at).length > 0)
879
+ return { status: 'deferred' };
815
880
  const cancelledThrough = read.cancelledThrough(input.seat.participant, input.seat.generation);
816
881
  if (cancelledThrough !== undefined && index <= cancelledThrough)
817
882
  return { status: 'stale' };
@@ -962,6 +1027,14 @@ export function inspectSqlite(db) {
962
1027
  requireFact(check.length === 1 && check[0].integrity_check === 'ok', 'SQLite integrity check failed.');
963
1028
  const state = exportSqliteState(db), d = createSqliteDriver(db);
964
1029
  d.roster();
1030
+ for (const row of db.prepare('SELECT * FROM seats').all())
1031
+ seat(row);
1032
+ for (const row of db.prepare('SELECT * FROM delivery_evidence').all())
1033
+ deliveryEvidence(row);
1034
+ if (db.prepare('PRAGMA user_version').get().user_version === CURRENT_USER_VERSION) {
1035
+ for (const row of db.prepare('SELECT * FROM delivery_deferrals').all())
1036
+ deliveryDeferral(row);
1037
+ }
965
1038
  for (const p of d.roster()) {
966
1039
  d.attention(p.name);
967
1040
  d.incomingAttention(p.name);
@@ -2,7 +2,7 @@ import { type SquareErrorCode, type SquareErrorFacts } from './model.js';
2
2
  import type { SquareBackends, SquareBackend } from './square-backend.js';
3
3
  import type { ParsedSquareLocation } from './square-location.js';
4
4
  import type { OperationControl, OperationFailureDetail, SquareAtInput, SquareBuildInput } from './square-facade.js';
5
- export declare const STDIO_PROTOCOL_VERSION = 1;
5
+ export declare const STDIO_PROTOCOL_VERSION = 2;
6
6
  export declare const STDIO_MAX_FRAME_BYTES: number;
7
7
  export declare const identityVariables: (import("./participant-identity.js").HarnessSessionVariable | "CLAUDE_CODE_CHILD_SESSION")[];
8
8
  export declare function wireIdentity(env: NodeJS.ProcessEnv): Record<string, string>;