@astrosheep/square 0.3.94 → 0.3.95

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 (85) hide show
  1. package/dist/artifact.js +1 -1
  2. package/dist/attention.d.ts +19 -0
  3. package/dist/attention.js +30 -0
  4. package/dist/boundary-presentation.js +33 -40
  5. package/dist/catch-decisions.d.ts +1 -2
  6. package/dist/catch-decisions.js +41 -21
  7. package/dist/cli/context.js +0 -2
  8. package/dist/cli/express-presentation.js +3 -3
  9. package/dist/cli/json.js +1 -1
  10. package/dist/cli/program.js +1 -1
  11. package/dist/cli/registry.js +1 -3
  12. package/dist/cli/square-commands.d.ts +0 -2
  13. package/dist/cli/square-commands.js +21 -27
  14. package/dist/decisions.d.ts +0 -5
  15. package/dist/decisions.js +0 -13
  16. package/dist/delivery-attempts.d.ts +52 -0
  17. package/dist/delivery-attempts.js +73 -0
  18. package/dist/delivery-effects.d.ts +139 -0
  19. package/dist/delivery-effects.js +163 -0
  20. package/dist/delivery-operations.js +43 -68
  21. package/dist/delivery-primitives.d.ts +5 -0
  22. package/dist/delivery-primitives.js +3 -0
  23. package/dist/delivery.d.ts +1 -1
  24. package/dist/delivery.js +6 -5
  25. package/dist/help.js +4 -6
  26. package/dist/http-delivery.d.ts +16 -0
  27. package/dist/http-delivery.js +101 -0
  28. package/dist/model.d.ts +4 -6
  29. package/dist/model.js +1 -1
  30. package/dist/notifications.js +2 -0
  31. package/dist/operation-presentation.d.ts +1 -2
  32. package/dist/operation-presentation.js +12 -20
  33. package/dist/ports.d.ts +5 -14
  34. package/dist/presentation-operations.d.ts +14 -19
  35. package/dist/presentation-operations.js +47 -57
  36. package/dist/presentation.d.ts +2 -9
  37. package/dist/presentation.js +13 -48
  38. package/dist/receive.d.ts +66 -0
  39. package/dist/receive.js +54 -0
  40. package/dist/runtime.d.ts +0 -1
  41. package/dist/runtime.js +2 -3
  42. package/dist/sqlite-square.d.ts +0 -1
  43. package/dist/sqlite-square.js +29 -55
  44. package/dist/square-api.d.ts +1 -1
  45. package/dist/square-api.js +1 -1
  46. package/dist/square-core.d.ts +0 -9
  47. package/dist/square-domain.js +9 -31
  48. package/dist/square-facade.d.ts +5 -21
  49. package/dist/square-legacy-framing.d.ts +2 -0
  50. package/dist/square-legacy-framing.js +8 -0
  51. package/dist/square-migration.d.ts +3 -1
  52. package/dist/square-migration.js +46 -13
  53. package/dist/square-receive.d.ts +6 -0
  54. package/dist/square-receive.js +18 -0
  55. package/dist/square-sqlite.d.ts +5 -3
  56. package/dist/square-sqlite.js +89 -90
  57. package/dist/square-tools.js +3 -11
  58. package/dist/views.d.ts +2 -3
  59. package/dist/views.js +13 -19
  60. package/dist/wakes.d.ts +0 -3
  61. package/dist/wakes.js +3 -5
  62. package/examples/symmetric-domains.mjs +250 -0
  63. package/guides/application-facade-delivery-evidence.md +5 -5
  64. package/guides/attention-primitive.md +32 -0
  65. package/guides/change-waits.md +2 -2
  66. package/guides/codex-delivery.md +1 -1
  67. package/guides/delivery-primitives.md +180 -0
  68. package/guides/mcp.md +1 -1
  69. package/guides/migration.md +12 -6
  70. package/guides/participant-ownership.md +1 -1
  71. package/guides/pi-tools.md +2 -2
  72. package/guides/primitive-catch-consolidation.md +5 -5
  73. package/guides/primitive-history-consolidation.md +2 -2
  74. package/guides/primitive-status-consolidation.md +3 -3
  75. package/guides/primitive-storage-consolidation.md +1 -1
  76. package/guides/receive-primitive.md +90 -0
  77. package/guides/row-runtime-delivery-evidence.md +1 -1
  78. package/guides/row-storage-design.md +17 -23
  79. package/guides/square-api.md +77 -10
  80. package/guides/symmetric-domains.md +92 -0
  81. package/package.json +14 -1
  82. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  83. package/plugins/claude/skills/square/SKILL.md +2 -6
  84. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  85. package/skills/square/SKILL.md +2 -6
@@ -2,7 +2,9 @@ import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
2
2
  import { SquareError, nameKey, isWakeRouteKind, validateOwner, validateActivityMetadata, 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,13 +265,8 @@ 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
272
  shape(a, [...common], ['body', 'mentions', 'reach', 'reply', 'metadata']);
@@ -292,8 +290,7 @@ export function validateStoredActivity(value) {
292
290
  requireFact(integer(a.reply) < a.index);
293
291
  break;
294
292
  case 'done':
295
- case 'hold':
296
- shape(a, common, ['body', ...(a.kind === 'hold' ? ['actor'] : [])]);
293
+ shape(a, common, ['body']);
297
294
  if (Object.hasOwn(a, 'body'))
298
295
  string(a.body);
299
296
  break;
@@ -307,14 +304,14 @@ export function validateStoredActivity(value) {
307
304
  shape(a, [...common, 'target']);
308
305
  string(a.target, true);
309
306
  break;
310
- default: shape(a, common, a.kind === 'resume' ? ['actor'] : []);
307
+ default: shape(a, common);
311
308
  }
312
309
  return value;
313
310
  }
314
311
  function activity(row) {
315
312
  const h = head(row), payload = object(json(row.payload_json));
316
313
  // 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'] : [];
314
+ const allowed = h.kind === 'say' ? ['body', 'mentions', 'metadata'] : h.kind === 'done' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
318
315
  requireFact(Object.keys(payload).every(k => allowed.includes(k)));
319
316
  return immutable(validateStoredActivity({ ...h, ...payload }));
320
317
  }
@@ -403,8 +400,7 @@ function nativeCorrelation(value) {
403
400
  ...(item.messageId === undefined ? {} : { messageId: item.messageId }),
404
401
  ...(item.payloadHash === undefined ? {} : { payloadHash: item.payloadHash }) });
405
402
  }
406
- function deliveryEvidence(row) {
407
- const payload = object(json(row.payload_json));
403
+ function deliveryEvidence(row, payload = object(json(row.payload_json))) {
408
404
  const participant = decoded(row.participant_json, true);
409
405
  requireFact(row.participant_key_json === key(participant));
410
406
  const generation = activityId(decoded(row.generation_json, true));
@@ -461,7 +457,7 @@ export function validateRowFraming(db, version, location = '') {
461
457
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
462
458
  requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
463
459
  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);
460
+ 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
461
  }
466
462
  export function validateSqliteHeader(db, location = '') {
467
463
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
@@ -470,7 +466,7 @@ export function validateSqliteHeader(db, location = '') {
470
466
  validateLegacyFraming(db, APPLICATION_ID, location);
471
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' });
472
468
  }
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) {
469
+ 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
470
  validateRowFraming(db, identity.user_version, location);
475
471
  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
472
  }
@@ -494,8 +490,6 @@ function activitySelection(filter, alias = 'a') {
494
490
  add(`${alias}.act_index <= ?`, integer(filter.throughIndex, -1));
495
491
  if (filter.afterAt !== undefined)
496
492
  add(`${alias}.at > ?`, finite(filter.afterAt));
497
- if (filter.actorRequired)
498
- terms.push(`${alias}.actor_json IS NOT NULL`);
499
493
  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
494
  if (values !== undefined) {
501
495
  if (column === 'act_index')
@@ -540,7 +534,7 @@ export function createSqliteDriver(db) {
540
534
  }
541
535
  function directedHead(row, recipient) {
542
536
  const h = head(row);
543
- requireFact(h.kind === 'say' && h.actor !== undefined);
537
+ requireFact(h.kind === 'say');
544
538
  return immutable({
545
539
  index: h.index, at: h.at, actor: h.actor, recipient, route: routeKind(row.route)
546
540
  });
@@ -566,7 +560,7 @@ export function createSqliteDriver(db) {
566
560
  const filter = activityIndex === undefined ? '' : ' AND act_index=?';
567
561
  if (activityIndex !== undefined)
568
562
  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));
563
+ 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
564
  },
571
565
  deliveryByToken(claimToken) {
572
566
  const row = one('SELECT * FROM delivery_evidence WHERE claim_token_json=?', [encode(claimToken)]);
@@ -596,18 +590,6 @@ export function createSqliteDriver(db) {
596
590
  roster: () => immutable(all('SELECT * FROM participants ORDER BY first_index').map(participant)),
597
591
  attention: name => immutable(attentionRows('t.listener_key_json=?', [key(name)])),
598
592
  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
593
  activity(index) {
612
594
  const r = one('SELECT * FROM activities WHERE act_index=?', [integer(index)]);
613
595
  return r ? activity(r) : undefined;
@@ -685,7 +667,7 @@ export function createSqliteDriver(db) {
685
667
  const groups = indices.map(r => {
686
668
  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
669
  const h = head(rows[0]);
688
- requireFact(h.kind === 'say' && h.actor !== undefined);
670
+ requireFact(h.kind === 'say');
689
671
  return {
690
672
  index: h.index, at: h.at, actor: h.actor, recipients: rows.map(v => ({ recipient: decoded(v.name_json, true), route: routeKind(v.route) }))
691
673
  };
@@ -788,7 +770,10 @@ export function createSqliteDriver(db) {
788
770
  const a = validateStoredActivity(effect.activity);
789
771
  requireFact(a.index >= read.header().nextActIndex && a.index < Number.MAX_SAFE_INTEGER, 'activity index cannot advance.');
790
772
  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)]);
773
+ receive({
774
+ 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)]); },
775
+ 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]); },
776
+ }, { item: { key: index, data: a }, audience: effect.audience.map(r => ({ recipient: r.recipient, via: r.route })) });
792
777
  if (effect.participant) {
793
778
  const p = effect.participant;
794
779
  string(p.name, true);
@@ -817,7 +802,6 @@ export function createSqliteDriver(db) {
817
802
  requireFact(t.kind === 'listen' || t.kind === 'ignore');
818
803
  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
804
  }
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
805
  run('UPDATE square_meta SET next_act_index=? WHERE id=1', [index + 1]);
822
806
  changed = true;
823
807
  },
@@ -825,11 +809,11 @@ export function createSqliteDriver(db) {
825
809
  finite(at);
826
810
  const next = read.header().nextActIndex;
827
811
  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]);
812
+ const canonical = owner(name);
813
+ string(canonical, true);
814
+ const count = recordConsumed({
815
+ insertReceipt: (ref, time) => run('INSERT OR IGNORE INTO receipts(owner_json,act_index,at) VALUES(?,?,?)', [encode(ref.recipient), ref.item, time]) > 0,
816
+ }, indexes.map(item => ({ recipient: canonical, item })), at);
833
817
  if (count)
834
818
  changed = true;
835
819
  return count;
@@ -884,6 +868,40 @@ export function createSqliteDriver(db) {
884
868
  return count > 0;
885
869
  },
886
870
  };
871
+ function attempt(row) {
872
+ const data = object(json(row.payload_json)), evidence = deliveryEvidence(row, data);
873
+ return { purpose: evidence.kind === 'wake' ? 'dispatch' : 'presentation', token: evidence.claimToken ?? '',
874
+ ordinal: evidence.attemptN, outcome: evidence.outcome, at: evidence.at, expiresAt: evidence.expiresAt, data };
875
+ }
876
+ function attemptRows(seat, index, kind, known) {
877
+ const scope = [key(seat.participant), encode(seat.generation), index, kind];
878
+ let sources = [];
879
+ return {
880
+ list() {
881
+ sources = all('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND act_index=? AND kind=? ORDER BY attempt_n DESC', scope);
882
+ return sources.map(attempt);
883
+ },
884
+ evidence(value) {
885
+ const source = sources.find(row => row.attempt_n === value.ordinal);
886
+ return deliveryEvidence({ ...source, outcome: value.outcome, at: value.at, expires_at: value.expiresAt ?? null }, value.data);
887
+ },
888
+ byToken(token) {
889
+ if (known?.token === token)
890
+ return known;
891
+ 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)]);
892
+ return row === undefined ? undefined : attempt(row);
893
+ },
894
+ insert(value) {
895
+ 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)]);
896
+ deliveryChanged = true;
897
+ },
898
+ replace(value) {
899
+ 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)]);
900
+ if (count)
901
+ deliveryChanged = true;
902
+ },
903
+ };
904
+ }
887
905
  const delivery = {
888
906
  claimDelivery(input) {
889
907
  requireFact(input.kind === 'wake' || input.kind === 'presentation');
@@ -903,42 +921,24 @@ export function createSqliteDriver(db) {
903
921
  const cancelledThrough = read.cancelledThrough(input.seat.participant, input.seat.generation);
904
922
  if (cancelledThrough !== undefined && index <= cancelledThrough)
905
923
  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 };
924
+ const rows = attemptRows(input.seat, index, input.kind);
925
+ const result = claimAttempt({ ...rows, replace(value) {
926
+ rows.replace({ ...value, data: { ...value.data, diagnostic: 'interrupted dispatch' } });
927
+ } }, { purpose: input.kind === 'wake' ? 'dispatch' : 'presentation', token: input.claimToken,
928
+ at: input.at, leaseMs, data: input.route === undefined ? {} : { route: input.route } });
929
+ if (result.status === 'acquired')
930
+ return { status: 'acquired', claimToken: result.token, attemptN: result.ordinal };
931
+ const evidence = result.interrupted ? { ...result.evidence, data: { ...result.evidence.data, diagnostic: 'interrupted dispatch' } } : result.evidence;
932
+ return { status: result.status, evidence: rows.evidence(evidence) };
929
933
  },
930
934
  prepareDelivery(input) {
931
935
  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
936
  if (row === undefined || row.outcome !== 'dispatching')
933
937
  return false;
934
- const correlation = nativeCorrelation(input.correlation);
935
- const payload = object(json(row.payload_json));
938
+ const previous = attempt(row), correlation = nativeCorrelation(input.correlation), payload = previous.data;
936
939
  if (stableJson(payload.correlation ?? null) === stableJson(correlation))
937
940
  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;
941
+ return prepareAttempt(attemptRows(input.seat, integer(row.act_index), row.kind, previous), { token: input.claimToken, data: { ...payload, correlation } });
942
942
  },
943
943
  completeDelivery(input) {
944
944
  const index = activityIndex(input.activity);
@@ -948,16 +948,9 @@ export function createSqliteDriver(db) {
948
948
  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
949
  if (row === undefined)
950
950
  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;
951
+ const previous = attempt(row);
952
+ 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 }) };
953
+ return settleAttempt(attemptRows(input.seat, index, row.kind, previous), { token: input.claimToken, outcome: input.outcome, at: input.at, data: payload });
961
954
  },
962
955
  observeNativeDelivery(input) {
963
956
  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 +1064,12 @@ function inspectRows(db, inspectSeat) {
1071
1064
  const rebuilt = new MemoryDatabase(':memory:');
1072
1065
  try {
1073
1066
  initializeSqlite(rebuilt, state, d.header().revision);
1067
+ // A migration may remove the activity that established canonical casing.
1068
+ // Retained participant spelling stays authoritative while replay checks
1069
+ // every derived lifecycle/count fact against surviving activities.
1070
+ const retainName = rebuilt.prepare('UPDATE participants SET name_json=? WHERE name_key_json=?');
1071
+ for (const p of d.roster())
1072
+ retainName.run(encode(p.name), key(p.name));
1074
1073
  for (const table of ['participants', 'attention', 'landed_audience']) {
1075
1074
  const original = db.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
1076
1075
  const expected = rebuilt.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
@@ -33,7 +33,7 @@ export const SQUARE_TOOLS = [
33
33
  reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
34
34
  noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
35
35
  force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
36
- noWait: z.boolean().optional().describe('If a hand is raised or you are speaking too fast, return at once instead of waiting.'),
36
+ noWait: z.boolean().optional().describe('If you are speaking too fast, return at once instead of waiting.'),
37
37
  }, async (square, { body, as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, observe: true }, control), rendering((result) => renderExpression(result.observation?.location ?? '', result))),
38
38
  tool('catch', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you catch no longer waits for you; history lets you look back.', {
39
39
  as: participant,
@@ -64,16 +64,8 @@ export const SQUARE_TOOLS = [
64
64
  const self = await selectOwnSeat(square, identity.owner, as);
65
65
  return self[action](target, { observe: true }, control);
66
66
  }, rendering((result, args) => renderListenerChange(result.observation?.location ?? '', args.action, result))),
67
- tool('hold', 'Raise a hand to pause the conversation; lower it with release. While a hand is up, new words wait to land.', {
68
- as: participant,
69
- reason: z.string().optional().describe('Why you are pausing. Everyone sees it.'),
70
- release: z.boolean().optional().describe('true lowers the raised hand so the conversation goes on.'),
71
- }, async (square, { as, reason, release }, control, identity) => {
72
- const self = await selectOwnSeat(square, identity.owner, as);
73
- return release === true ? self.resume({ observe: true }, control) : self.hold(reason, { observe: true }, control);
74
- }, rendering((result, args) => renderLifecycleResult(result.observation?.location ?? '', args.release === true ? 'resume' : 'hold', result))),
75
- tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).done(undefined, { observe: true }, control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', result))),
76
- tool('status', 'Look around the square: who is here, whether a hand is raised, the latest activity, and who you listen to.', {
67
+ tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).done(undefined, { observe: true }, control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', result))),
68
+ tool('status', 'Look around the square: who is here, the latest activity, and who you listen to.', {
77
69
  as: z.string().optional().describe('Whose listening to show. Leave it out if you hold exactly one name here.'),
78
70
  }, async (square, { as }, control, identity) => {
79
71
  const viewer = (await selectOwnSeat(square, identity.owner, as, { optional: true }))?.name;
package/dist/views.d.ts CHANGED
@@ -4,8 +4,8 @@ import type { ActivityFilter, SquareRead } from './ports.js';
4
4
  import type { Activity, ArchiveRecord, PerceivedActivity, HistoryQuery, HistoryContinuation, ParticipantsResult, SquareSnapshot, EntryPresentation, StatusResult, SquareObservation, CatchPresence } from './square-facade.js';
5
5
  export declare const HISTORY_DEFAULT_LIMIT = 10;
6
6
  export declare const HISTORY_MAX_LIMIT = 100;
7
- export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume", "listen", "ignore", "unlisten"];
8
- /** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
7
+ export declare const NON_READ: readonly ["join", "done", "say", "listen", "ignore", "unlisten"];
8
+ /** The one StoredAct-to-public-Activity field mapping. Rejects read bookkeeping. */
9
9
  export declare function toPublicActivity(stored: StoredAct): Activity;
10
10
  export declare function toPerceivedActivity(facts: SquareRead, stored: StoredAct, viewer: string): PerceivedActivity;
11
11
  export declare function parseRequiredActivityId(id: ActivityId): number;
@@ -22,7 +22,6 @@ export declare function recoveryObservation(facts: SquareRead, name: string, now
22
22
  participant: string;
23
23
  activitySummaries: import("./square-facade.js").UnreadActivitySummary[];
24
24
  standingCount: number;
25
- held: boolean;
26
25
  ownActivityCount: number;
27
26
  hardCap: import("./model.js").HardCap;
28
27
  };
package/dist/views.js CHANGED
@@ -9,11 +9,11 @@ export const HISTORY_DEFAULT_LIMIT = 10;
9
9
  export const HISTORY_MAX_LIMIT = 100;
10
10
  const STATUS_HERE_WINDOW_MS = 30 * 60_000;
11
11
  const PRESENCE_WINDOW_MS = 8 * 60 * 60_000;
12
- export const NON_READ = ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore', 'unlisten'];
13
- const ARCHIVE = new Set(['join', 'done', 'say', 'hold', 'resume']);
14
- /** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
12
+ export const NON_READ = ['join', 'done', 'say', 'listen', 'ignore', 'unlisten'];
13
+ const ARCHIVE = new Set(['join', 'done', 'say']);
14
+ /** The one StoredAct-to-public-Activity field mapping. Rejects read bookkeeping. */
15
15
  export function toPublicActivity(stored) {
16
- if (stored.kind === 'read' || stored.actor === undefined)
16
+ if (stored.kind === 'read')
17
17
  throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
18
18
  return { id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
19
19
  ...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}),
@@ -108,21 +108,20 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
108
108
  return { activities, visibleActivities, totalMatches, ...(continuation === undefined ? {} : { continuation }) };
109
109
  }
110
110
  export function publicActivities(activities) {
111
- return activities.flatMap((activity) => activity.kind === 'read' || activity.actor === undefined ? [] : [toPublicActivity(activity)]);
111
+ return activities.flatMap((activity) => activity.kind === 'read' ? [] : [toPublicActivity(activity)]);
112
112
  }
113
113
  export function snapshot(facts, now) {
114
- const header = facts.header(), hold = facts.hold();
114
+ const header = facts.header();
115
115
  const names = new Map(facts.roster().map((p) => [nameKey(p.name), p.name]));
116
116
  const receipts = new Set(facts.receiptAssignments().map((r) => JSON.stringify([r.owner, r.index])));
117
117
  return { context: [...header.preamble, ...header.warmup].join('\n'), actCount: facts.activityCount({ kinds: NON_READ }), hardCap: header.hardCap,
118
118
  ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
119
- held: hold.active && hold.actor !== undefined ? { by: hold.actor, ...(hold.reason === undefined ? {} : { reason: hold.reason }) } : null,
120
119
  participants: coreParticipants(facts, now).filter((p) => p.presence !== 'never-joined').map(({ name, presence, listening }) => ({ name, presence, listening })),
121
120
  delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
122
121
  }
123
122
  export function recoveryObservation(facts, name, now) {
124
- const known = resolveKnownName(facts, name), hold = facts.hold();
125
- return { participant: known, activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts), held: hold.active,
123
+ const known = resolveKnownName(facts, name);
124
+ return { participant: known, activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts),
126
125
  ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
127
126
  }
128
127
  export function entryPresentation(facts, name, lastN = HISTORY_DEFAULT_LIMIT) {
@@ -146,7 +145,7 @@ function classifyPresence(participants, now) {
146
145
  export function queryParticipants(facts, context, observe = false) {
147
146
  const participants = coreParticipants(facts, context.now);
148
147
  return { participants, ...(!observe ? {} : { observation: { ...context, ...classifyPresence(participants, context.now),
149
- standingCount: inSquareCount(facts), held: facts.hold().active } }) };
148
+ standingCount: inSquareCount(facts) } }) };
150
149
  }
151
150
  export function listPresentation(facts) {
152
151
  const participants = facts.roster().filter((p) => p.joined).map((p) => {
@@ -157,21 +156,16 @@ export function listPresentation(facts) {
157
156
  }
158
157
  export function queryStatus(facts, context, viewer = '', observe = false) {
159
158
  const participants = coreParticipants(facts, context.now);
160
- const header = facts.header(), hold = facts.hold(), standingCount = inSquareCount(facts);
159
+ const header = facts.header(), standingCount = inSquareCount(facts);
161
160
  const listener = participants.find((p) => p.presence !== 'never-joined' && nameKey(p.name) === nameKey(viewer));
162
161
  const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
163
162
  const latestAct = latest === undefined ? undefined : facts.activity(latest.index);
164
163
  const data = {
165
164
  hardCap: header.hardCap,
166
165
  ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
167
- held: hold.active ? {
168
- ...(hold.actor === undefined ? {} : { by: hold.actor }),
169
- ...(hold.reason === undefined ? {} : { reason: hold.reason }),
170
- ...(hold.at === undefined ? {} : { at: hold.at }),
171
- } : null,
172
166
  ...(listener === undefined ? {} : { listening: listener.listening }),
173
167
  participants: participants.filter((p) => p.presence !== 'never-joined'), now: context.now,
174
- ...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
168
+ ...(latestAct === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
175
169
  };
176
170
  if (!observe)
177
171
  return data;
@@ -200,7 +194,7 @@ export function catchPresence(facts, now) {
200
194
  return { participants, now };
201
195
  }
202
196
  export function streamProjection(facts, cursor, recipient) {
203
- const page = facts.activityHeads({ afterIndex: cursor, kinds: NON_READ, actorRequired: true }, { edge: 'oldest', limit: PAGE_SIZE });
197
+ const page = facts.activityHeads({ afterIndex: cursor, kinds: NON_READ }, { edge: 'oldest', limit: PAGE_SIZE });
204
198
  const heads = recipient === undefined ? page.items : page.items.filter((head) => facts.directedAt(recipient, head.index) !== undefined);
205
199
  const activities = facts.activities(heads.map((head) => head.index)).map((activity) => recipient === undefined
206
200
  ? toPublicActivity(activity) : toPerceivedActivity(facts, activity, recipient));
@@ -210,7 +204,7 @@ export function streamTailProjection(facts, last = HISTORY_DEFAULT_LIMIT, recipi
210
204
  if (!Number.isSafeInteger(last) || last < 0 || last > PAGE_SIZE)
211
205
  throw new SquareError('invalid_args', `Invalid stream tail: expected a non-negative safe integer no greater than ${PAGE_SIZE}.`);
212
206
  const heads = last === 0 ? [] : recipient === undefined
213
- ? facts.activityHeads({ kinds: NON_READ, actorRequired: true }, { edge: 'newest', limit: last }).items
207
+ ? facts.activityHeads({ kinds: NON_READ }, { edge: 'newest', limit: last }).items
214
208
  : facts.directed(recipient, {}, { edge: 'newest', limit: last }).items;
215
209
  const activities = facts.activities(heads.map((head) => head.index)).map((activity) => recipient === undefined
216
210
  ? toPublicActivity(activity) : toPerceivedActivity(facts, activity, recipient));
package/dist/wakes.d.ts CHANGED
@@ -11,9 +11,6 @@ export type WatchLeaseStart = {
11
11
  };
12
12
  export type WatchLeasePulse = {
13
13
  readonly type: 'replaced';
14
- } | {
15
- readonly type: 'held';
16
- readonly heartbeatAt?: number;
17
14
  } | {
18
15
  readonly type: 'terminal';
19
16
  readonly status: 'capped' | 'quorum';
package/dist/wakes.js CHANGED
@@ -21,15 +21,13 @@ export async function pulseWatchLease(artifact, seat, at, leaseId, options, hear
21
21
  const known = resolveKnownName(facts, name), lease = freshWatchLease(facts, known, at);
22
22
  if (lease?.leaseId !== leaseId)
23
23
  return { type: 'replaced' };
24
- const held = facts.hold().active;
25
- const terminal = held ? undefined : watchTerminalStatus(facts, known);
24
+ const terminal = watchTerminalStatus(facts, known);
26
25
  if (terminal !== undefined)
27
26
  return { type: 'terminal', status: terminal };
28
- const type = held ? 'held' : 'sleep';
29
27
  if (!heartbeatDue)
30
- return { type };
28
+ return { type: 'sleep' };
31
29
  facts.putLease(known, newLease(leaseId, at, options), seat.generation);
32
- return { type, heartbeatAt: at };
30
+ return { type: 'sleep', heartbeatAt: at };
33
31
  }, signal);
34
32
  return value;
35
33
  }