@astrosheep/square 0.3.92 → 0.3.94

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 (119) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +2 -2
  4. package/dist/boundary-presentation.d.ts +2 -2
  5. package/dist/catch-decisions.d.ts +4 -6
  6. package/dist/catch-decisions.js +4 -4
  7. package/dist/claude-delivery-status.js +10 -4
  8. package/dist/claude-delivery.js +19 -11
  9. package/dist/claude-mod.js +24 -53
  10. package/dist/cli/context.d.ts +9 -4
  11. package/dist/cli/context.js +30 -25
  12. package/dist/cli/express-presentation.d.ts +3 -1
  13. package/dist/cli/express-presentation.js +26 -14
  14. package/dist/cli/json.d.ts +3 -0
  15. package/dist/cli/json.js +14 -0
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.d.ts +1 -3
  18. package/dist/cli/observation-commands.js +43 -56
  19. package/dist/cli/program.js +2 -2
  20. package/dist/cli/registry.js +6 -3
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +46 -54
  23. package/dist/codex-hook.d.ts +3 -2
  24. package/dist/codex-hook.js +13 -4
  25. package/dist/codex-queue.js +4 -4
  26. package/dist/decisions.d.ts +5 -11
  27. package/dist/decisions.js +9 -20
  28. package/dist/delivery-deferral.js +2 -2
  29. package/dist/delivery-health.d.ts +0 -1
  30. package/dist/delivery-health.js +1 -3
  31. package/dist/delivery-operations.d.ts +2 -2
  32. package/dist/delivery-operations.js +38 -38
  33. package/dist/delivery.d.ts +1 -1
  34. package/dist/delivery.js +1 -1
  35. package/dist/harness-links.d.ts +1 -10
  36. package/dist/harness-links.js +144 -109
  37. package/dist/harness.js +4 -13
  38. package/dist/help.js +12 -10
  39. package/dist/inbox.d.ts +9 -22
  40. package/dist/inbox.js +23 -27
  41. package/dist/index.d.ts +1 -3
  42. package/dist/index.js +1 -1
  43. package/dist/mcp.d.ts +3 -0
  44. package/dist/mcp.js +23 -24
  45. package/dist/model.d.ts +8 -15
  46. package/dist/model.js +4 -7
  47. package/dist/notifications.d.ts +1 -1
  48. package/dist/notifications.js +6 -5
  49. package/dist/operation-presentation.d.ts +5 -4
  50. package/dist/operation-presentation.js +50 -45
  51. package/dist/owner.d.ts +11 -0
  52. package/dist/owner.js +32 -0
  53. package/dist/paseo-delivery.d.ts +4 -1
  54. package/dist/paseo-delivery.js +7 -12
  55. package/dist/paseo-state.d.ts +2 -2
  56. package/dist/paseo-state.js +6 -4
  57. package/dist/plugins/opencode/delivery.js +27 -15
  58. package/dist/ports.d.ts +13 -33
  59. package/dist/presentation-operations.js +4 -4
  60. package/dist/presentation.d.ts +5 -8
  61. package/dist/presentation.js +33 -43
  62. package/dist/runtime.d.ts +1 -7
  63. package/dist/runtime.js +0 -14
  64. package/dist/seats.d.ts +2 -3
  65. package/dist/seats.js +7 -15
  66. package/dist/session-lifecycle.d.ts +6 -2
  67. package/dist/session-lifecycle.js +39 -11
  68. package/dist/sqlite-square.d.ts +36 -22
  69. package/dist/sqlite-square.js +138 -161
  70. package/dist/square-api.d.ts +15 -10
  71. package/dist/square-api.js +26 -22
  72. package/dist/square-core.d.ts +0 -5
  73. package/dist/square-core.js +0 -9
  74. package/dist/square-domain.d.ts +4 -2
  75. package/dist/square-domain.js +56 -21
  76. package/dist/square-facade.d.ts +45 -87
  77. package/dist/square-index.d.ts +5 -13
  78. package/dist/square-index.js +13 -38
  79. package/dist/square-location.d.ts +1 -14
  80. package/dist/square-location.js +4 -69
  81. package/dist/square-migration-host-ledger.d.ts +2 -2
  82. package/dist/square-migration-host-ledger.js +6 -5
  83. package/dist/square-migration.d.ts +2 -1
  84. package/dist/square-migration.js +52 -30
  85. package/dist/square-sqlite.d.ts +6 -2
  86. package/dist/square-sqlite.js +54 -28
  87. package/dist/square-tools.d.ts +7 -10
  88. package/dist/square-tools.js +22 -54
  89. package/dist/stream.js +4 -5
  90. package/dist/views.d.ts +21 -10
  91. package/dist/views.js +38 -38
  92. package/dist/wakes.d.ts +1 -2
  93. package/dist/wakes.js +1 -1
  94. package/guides/application-facade-delivery-evidence.md +1 -1
  95. package/guides/claude-native.md +18 -9
  96. package/guides/codex-delivery.md +17 -6
  97. package/guides/mcp-connection-lifetime.md +1 -1
  98. package/guides/mcp.md +3 -3
  99. package/guides/migration.md +142 -151
  100. package/guides/operation-cancellation.md +1 -1
  101. package/guides/participant-ownership.md +78 -47
  102. package/guides/pi-tools.md +33 -4
  103. package/guides/primitive-history-consolidation.md +1 -1
  104. package/guides/row-storage-design.md +8 -8
  105. package/guides/square-api.md +244 -88
  106. package/package.json +2 -1
  107. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  108. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  109. package/plugins/opencode/delivery.ts +27 -17
  110. package/plugins/paseo/client/palette.ts +18 -18
  111. package/plugins/paseo/client/square-message.tsx +17 -23
  112. package/plugins/pi/square-pi.js +17 -17
  113. package/dist/square-backend.d.ts +0 -13
  114. package/dist/square-backend.js +0 -1
  115. package/dist/square-stdio.d.ts +0 -22
  116. package/dist/square-stdio.js +0 -235
  117. package/dist/stdio-server.d.ts +0 -3
  118. package/dist/stdio-server.js +0 -212
  119. package/guides/backends.md +0 -193
@@ -1,5 +1,5 @@
1
1
  import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
2
- import { sameBody, SquareError, nameKey, isWakeRouteKind, validateBody, validateActivityMetadata, activityMetadataHasContent } from './model.js';
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
5
  import { legacyRowFramingIssue } from './square-legacy-framing.js';
@@ -11,7 +11,9 @@ const V1_USER_VERSION = 1;
11
11
  export const V2_USER_VERSION = 2;
12
12
  export const V3_USER_VERSION = 3;
13
13
  export const V4_USER_VERSION = 4;
14
- export const CURRENT_USER_VERSION = 5;
14
+ export const V5_USER_VERSION = 5;
15
+ export const V6_USER_VERSION = 6;
16
+ export const CURRENT_USER_VERSION = 7;
15
17
  export const V1_SNAPSHOT_TABLE = 'square_snapshot';
16
18
  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)`;
17
19
  const V1_SNAPSHOT_COLUMNS = [
@@ -87,6 +89,7 @@ export const DELIVERY_DEFERRALS_SQL = `CREATE TABLE delivery_deferrals (
87
89
  PRIMARY KEY (participant_key_json,generation_json,deferral_id_json)
88
90
  ) WITHOUT ROWID;
89
91
  CREATE INDEX delivery_deferral_expiry ON delivery_deferrals(participant_key_json,generation_json,expires_at);`;
92
+ const ATTACHMENT_COLUMN_SQL = "attachment TEXT NOT NULL DEFAULT 'attached' CHECK (attachment IN ('attached','detached'))";
90
93
  export const SCHEMA_SQL = `CREATE TABLE square_meta (
91
94
  id INTEGER PRIMARY KEY CHECK (id = 1),
92
95
  revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),
@@ -119,13 +122,12 @@ CREATE TABLE catch_leases (
119
122
  CREATE TABLE seats (
120
123
  participant_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
121
124
  participant_json TEXT NOT NULL,
122
- channel_json TEXT NOT NULL,
123
- session_json TEXT NOT NULL,
124
- agent_json TEXT,
125
+ owner_json TEXT NOT NULL,
125
126
  generation_json TEXT NOT NULL,
126
- wake_address_json TEXT CHECK (wake_address_json IS NULL OR json_valid(wake_address_json))
127
+ wake_address_json TEXT CHECK (wake_address_json IS NULL OR json_valid(wake_address_json)),
128
+ ${ATTACHMENT_COLUMN_SQL}
127
129
  ) WITHOUT ROWID;
128
- CREATE INDEX seat_body ON seats(channel_json, session_json, agent_json);
130
+ CREATE INDEX seat_owner ON seats(owner_json);
129
131
  CREATE TABLE delivery_evidence (
130
132
  participant_key_json TEXT COLLATE BINARY NOT NULL,
131
133
  participant_json TEXT NOT NULL,
@@ -180,7 +182,11 @@ CREATE TABLE landed_audience (
180
182
  UNIQUE (act_index, recipient_order)
181
183
  ) WITHOUT ROWID;
182
184
  CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
183
- export const FORMAT4_SCHEMA_SQL = SCHEMA_SQL.replace(DELIVERY_DEFERRALS_SQL, '');
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 FORMAT5_SCHEMA_SQL = FORMAT6_SCHEMA_SQL.replace(`,\n ${ATTACHMENT_COLUMN_SQL}`, '');
187
+ export const FORMAT4_SCHEMA_SQL = FORMAT5_SCHEMA_SQL.replace(DELIVERY_DEFERRALS_SQL, '');
188
+ const FORMAT6_SCHEMA = FORMAT6_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
189
+ const FORMAT5_SCHEMA = FORMAT5_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
184
190
  const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
185
191
  const FORMAT4_SCHEMA = FORMAT4_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
186
192
  const encode = JSON.stringify;
@@ -372,17 +378,19 @@ function wakeAddress(value) {
372
378
  stableJson(entries);
373
379
  return immutable({ kind: address.kind, address: entries });
374
380
  }
375
- function bodyFrom(row) {
376
- const body = { channel: decoded(row.channel_json, true), session: decoded(row.session_json, true), ...(row.agent_json === null ? {} : { agent: decoded(row.agent_json, true) }) };
377
- validateBody(body);
378
- return immutable(body);
381
+ function ownerFrom(row) {
382
+ const owner = decoded(row.owner_json, true);
383
+ validateOwner(owner);
384
+ return owner;
379
385
  }
380
386
  function seat(row) {
381
387
  const participant = decoded(row.participant_json, true);
382
388
  requireFact(row.participant_key_json === key(participant));
383
389
  const generation = activityId(decoded(row.generation_json, true));
384
390
  const parsedWake = row.wake_address_json === null ? undefined : wakeAddress(json(row.wake_address_json));
385
- return immutable({ participant, body: bodyFrom(row), generation, ...(parsedWake === undefined ? {} : { wakeAddress: parsedWake }) });
391
+ requireFact(row.attachment === 'attached' || row.attachment === 'detached', 'seat attachment is malformed.');
392
+ requireFact(row.attachment !== 'detached' || parsedWake === undefined, 'a detached seat has a wake address.');
393
+ return immutable({ participant, owner: ownerFrom(row), generation, attachment: row.attachment, ...(parsedWake === undefined ? {} : { wakeAddress: parsedWake }) });
386
394
  }
387
395
  function nativeCorrelation(value) {
388
396
  const item = shape(value, ['harness'], ['token', 'messageId', 'payloadHash']);
@@ -453,7 +461,7 @@ export function validateRowFraming(db, version, location = '') {
453
461
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
454
462
  requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
455
463
  requireFact(identity.user_version === version, 'unsupported SQLite format version.');
456
- validateOwnedSchema(db, version === V4_USER_VERSION ? FORMAT4_SCHEMA : EXPECTED_SCHEMA_SQL, location);
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);
457
465
  }
458
466
  export function validateSqliteHeader(db, location = '') {
459
467
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
@@ -462,7 +470,7 @@ export function validateSqliteHeader(db, location = '') {
462
470
  validateLegacyFraming(db, APPLICATION_ID, location);
463
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' });
464
472
  }
465
- if (identity.user_version === V2_USER_VERSION || identity.user_version === V3_USER_VERSION || identity.user_version === V4_USER_VERSION) {
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) {
466
474
  validateRowFraming(db, identity.user_version, location);
467
475
  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' });
468
476
  }
@@ -543,17 +551,16 @@ export function createSqliteDriver(db) {
543
551
  const latestSeats = `seats s JOIN participants p ON p.name_key_json=s.participant_key_json
544
552
  JOIN activities a ON a.act_index=p.last_join_index AND a.kind='join' AND a.actor_key_json=p.name_key_json
545
553
  AND s.generation_json='"act/'||a.act_index||'"'`;
546
- function bodySeats(body, lifecycle) {
547
- validateBody(body);
548
- return immutable(all(`SELECT s.* FROM ${latestSeats} WHERE p.${lifecycle}=1 AND s.channel_json=? AND s.session_json=? AND s.agent_json IS ? ORDER BY s.participant_key_json`, [encode(body.channel), encode(body.session), body.agent === undefined ? null : encode(body.agent)]).map(seat));
554
+ function ownerSeats(owner) {
555
+ validateOwner(owner);
556
+ return immutable(all(`SELECT s.* FROM ${latestSeats} WHERE p.joined=1 AND s.owner_json=? ORDER BY s.participant_key_json`, [encode(owner)]).map(seat));
549
557
  }
550
558
  const read = {
551
559
  seat(name) {
552
560
  const r = one(`SELECT s.* FROM ${latestSeats} WHERE p.joined=1 AND s.participant_key_json=?`, [key(name)]);
553
561
  return r ? seat(r) : undefined;
554
562
  },
555
- seatsForBody: body => bodySeats(body, 'joined'),
556
- completedSeatsForBody: body => bodySeats(body, 'done'),
563
+ seatsForOwner: ownerSeats,
557
564
  deliveryEvidence(participant, generation, activityIndex) {
558
565
  const args = [key(participant), encode(generation)];
559
566
  const filter = activityIndex === undefined ? '' : ' AND act_index=?';
@@ -713,14 +720,14 @@ export function createSqliteDriver(db) {
713
720
  };
714
721
  const writes = {
715
722
  storeSeat(value) {
716
- validateBody(value.body);
723
+ validateOwner(value.owner);
717
724
  activityId(value.generation);
718
725
  string(value.participant, true);
719
726
  const existing = read.seat(value.participant);
720
727
  if (existing !== undefined && stableJson(existing) === stableJson(value))
721
728
  return false;
722
729
  const wake = value.wakeAddress === undefined ? null : encode(value.wakeAddress);
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]);
730
+ run('INSERT INTO seats(participant_key_json,participant_json,owner_json,generation_json,wake_address_json,attachment) VALUES(?,?,?,?,?,?) ON CONFLICT(participant_key_json) DO UPDATE SET participant_json=excluded.participant_json,owner_json=excluded.owner_json,generation_json=excluded.generation_json,wake_address_json=excluded.wake_address_json,attachment=excluded.attachment', [key(value.participant), encode(value.participant), encode(value.owner), encode(value.generation), wake, value.attachment]);
724
731
  run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
725
732
  run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(value.participant)]);
726
733
  deliveryChanged = true;
@@ -739,12 +746,28 @@ export function createSqliteDriver(db) {
739
746
  deliveryChanged = true;
740
747
  return count > 0;
741
748
  },
749
+ reattachSeat(value, address) {
750
+ const current = read.seat(value.participant);
751
+ if (current?.attachment !== 'detached')
752
+ return false;
753
+ run("UPDATE seats SET attachment='attached',wake_address_json=? WHERE participant_key_json=? AND generation_json=?", [address === undefined ? null : encode(wakeAddress(address)), key(value.participant), encode(value.generation)]);
754
+ deliveryChanged = true;
755
+ return true;
756
+ },
757
+ detachSeat(value) {
758
+ const count = run("UPDATE seats SET attachment='detached',wake_address_json=NULL WHERE participant_key_json=? AND generation_json=? AND attachment='attached'", [key(value.participant), encode(value.generation)]);
759
+ if (!count)
760
+ return false;
761
+ run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
762
+ run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND generation_json=?', [key(value.participant), encode(value.generation)]);
763
+ deliveryChanged = true;
764
+ return true;
765
+ },
742
766
  removeSeat(value) {
743
- // Done hides current authority; retain only its latest coordinate for exact-body resume.
744
767
  const row = one(`SELECT s.* FROM ${latestSeats} WHERE p.done=1 AND s.participant_key_json=?`, [key(value.participant)]);
745
- if (row === undefined || decoded(row.generation_json) !== value.generation)
768
+ if (row === undefined || decoded(row.generation_json) !== value.generation || ownerFrom(row) !== value.owner)
746
769
  return false;
747
- run('UPDATE seats SET wake_address_json=NULL WHERE participant_key_json=?', [key(value.participant)]);
770
+ run('DELETE FROM seats WHERE participant_key_json=?', [key(value.participant)]);
748
771
  run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
749
772
  run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(value.participant)]);
750
773
  changed = true;
@@ -872,7 +895,7 @@ export function createSqliteDriver(db) {
872
895
  wakeAddress(input.route);
873
896
  const currentSeat = read.seat(input.seat.participant);
874
897
  const item = read.activity(index);
875
- if (currentSeat === undefined || currentSeat.generation !== input.seat.generation || !sameBody(currentSeat.body, input.seat.body)
898
+ if (currentSeat === undefined || currentSeat.attachment !== 'attached' || currentSeat.generation !== input.seat.generation || currentSeat.owner !== input.seat.owner
876
899
  || item?.kind !== 'say' || read.directedAt(input.seat.participant, index) === undefined)
877
900
  return { status: 'stale' };
878
901
  if (matchingDeferrals(read.deliveryDeferrals(currentSeat.participant, currentSeat.generation), item.metadata, input.at).length > 0)
@@ -1023,15 +1046,18 @@ export function exportSqliteState(db) {
1023
1046
  }
1024
1047
  /** Full read-only integrity inspection, including maintained-index replay comparison. */
1025
1048
  export function inspectSqlite(db) {
1049
+ return inspectRows(db, seat);
1050
+ }
1051
+ function inspectRows(db, inspectSeat) {
1026
1052
  const check = db.prepare('PRAGMA integrity_check').all();
1027
1053
  requireFact(check.length === 1 && check[0].integrity_check === 'ok', 'SQLite integrity check failed.');
1028
1054
  const state = exportSqliteState(db), d = createSqliteDriver(db);
1029
1055
  d.roster();
1030
1056
  for (const row of db.prepare('SELECT * FROM seats').all())
1031
- seat(row);
1057
+ inspectSeat(row);
1032
1058
  for (const row of db.prepare('SELECT * FROM delivery_evidence').all())
1033
1059
  deliveryEvidence(row);
1034
- if (db.prepare('PRAGMA user_version').get().user_version === CURRENT_USER_VERSION) {
1060
+ if (Number(db.prepare('PRAGMA user_version').get().user_version) >= V5_USER_VERSION) {
1035
1061
  for (const row of db.prepare('SELECT * FROM delivery_deferrals').all())
1036
1062
  deliveryDeferral(row);
1037
1063
  }
@@ -1,20 +1,17 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { type Square } from './square-api.js';
3
- import type { OperationControl } from './square-facade.js';
3
+ import type { Owner, WakeAddress, OperationControl } from './square-facade.js';
4
+ export interface ToolIdentity {
5
+ readonly owner?: Owner;
6
+ readonly wakeAddress?: WakeAddress;
7
+ }
4
8
  type JsonObject = Record<string, unknown>;
5
- type Outcome = Promise<object>;
6
- type ToolRender = (result: JsonObject, args: JsonObject) => string | undefined;
7
9
  interface ToolDefinition {
8
10
  name: string;
9
11
  description: string;
10
12
  inputSchema: z.ZodObject<z.ZodRawShape>;
11
- run(square: Square, args: JsonObject, control: ToolControl): Outcome;
12
- /** Human text identical to what the CLI prints; transports show this to people. */
13
- render: ToolRender;
14
- }
15
- /** A transport that shows people the square asks the same operation for its rendering facts. */
16
- export interface ToolControl extends OperationControl {
17
- readonly observe?: boolean;
13
+ /** Human text identical to what the CLI prints; domain receipts stay inside the adapter. */
14
+ run(square: Square, args: JsonObject, control: OperationControl, identity: ToolIdentity): Promise<string>;
18
15
  }
19
16
  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.";
20
17
  export declare const SQUARE_TOOLS: readonly ToolDefinition[];
@@ -1,51 +1,22 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { isSquareError } from './model.js';
3
- import { CATCH_DEFAULT_LIMIT } from './square-api.js';
3
+ import { requireOwner, requireJoinName, selectOwnSeat, CATCH_DEFAULT_LIMIT } from './square-api.js';
4
4
  import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
5
- const location = z.string().optional().describe('Which square: a .square file path, or a backend URI like stdio://name/key. Relative paths start from the working directory. Leave it out for the configured square.');
5
+ const location = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Leave it out for the configured square.');
6
6
  const participant = z.string().optional().describe('Your name in the square. Leave it out if you hold exactly one name here.');
7
7
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
8
8
  const stringArray = z.array(z.string());
9
- /** Keep the observation request internal: it is not an argument a caller decides. */
10
- function observation(control) {
11
- return control.observe === true ? { observe: true } : {};
12
- }
13
- /** Tool receipts are plain JSON; each renderer re-declares the facts it reads. */
9
+ /** Each renderer declares the operation facts it reads. */
14
10
  function rendering(render) {
15
11
  return (result, args) => render(result, args);
16
12
  }
17
- /**
18
- * Tool receipts are the one machine surface without activity metadata: a receipt copy drops every
19
- * property named `metadata` while domain activities keep theirs for SDK, CLI, stdio and stream readers.
20
- */
21
- function withoutActivityMetadata(value) {
22
- if (Array.isArray(value))
23
- return value.map(withoutActivityMetadata);
24
- if (value === null || typeof value !== 'object')
25
- return value;
26
- const prototype = Object.getPrototypeOf(value);
27
- if (prototype !== Object.prototype && prototype !== null)
28
- return value;
29
- return Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'metadata')
30
- .map(([key, item]) => [key, withoutActivityMetadata(item)]));
31
- }
32
- /**
33
- * Human text renders from the unfiltered receipt, so metadata-only says keep the placeholder the
34
- * CLI prints and every other sentence stays identical. Machines only ever receive the filtered copy.
35
- */
36
- const renderingReceipts = new WeakMap();
37
- function renderingReceipt(receipt) {
38
- return (renderingReceipts.get(receipt) ?? receipt);
39
- }
40
13
  function tool(name, description, shape, run, render) {
41
14
  const inputSchema = z.object({ ...shape, location }).strict();
42
- return { name, description, inputSchema, render: (receipt, args) => render(renderingReceipt(receipt), args),
43
- run: async (square, args, control) => {
15
+ return { name, description, inputSchema,
16
+ run: async (square, args, control, identity) => {
44
17
  const { location: _location, ...operationArgs } = inputSchema.parse(args);
45
- const receipt = await run(square, operationArgs, control);
46
- const filtered = withoutActivityMetadata(receipt);
47
- renderingReceipts.set(filtered, receipt);
48
- return filtered;
18
+ const receipt = await run(square, operationArgs, control, identity);
19
+ return render(receipt, operationArgs) ?? '';
49
20
  } };
50
21
  }
51
22
  export 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.';
@@ -53,10 +24,7 @@ const target = z.string().describe('The participant to turn toward or away from.
53
24
  export const SQUARE_TOOLS = [
54
25
  tool('join', 'Step into the square under your name. Do this once before you speak or catch. You arrive seeing what was said most recently.', {
55
26
  as: z.string().optional().describe('Your name in the square. Give it explicitly when joining.'), takeover: z.boolean().optional().describe('Claim this name even though another session holds it now. That session loses it.'),
56
- }, async (square, { as, takeover }, control) => {
57
- const { participant, ...receipt } = await square.join(as, { takeover, ...observation(control) }, control);
58
- return { ...receipt, participant: participant.name };
59
- }, rendering((result) => renderJoinResult(result))),
27
+ }, (square, { as, takeover }, control, identity) => square.join(requireJoinName(as), { ...identity, owner: requireOwner(identity.owner), takeover, observe: true }, control), rendering((result) => renderJoinResult(result))),
60
28
  tool('express', 'Speak or act in the square. Plain words, half-formed ideas and *actions* all belong here. Mention or reply to someone to be heard by them; ring the bell to be heard by everyone. Said to no one in particular, your words reach those who listen to you.', {
61
29
  as: participant,
62
30
  body: z.string().describe('What you say or do. Markdown works; *asterisks* mark an action.'),
@@ -66,10 +34,7 @@ export const SQUARE_TOOLS = [
66
34
  noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
67
35
  force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
68
36
  noWait: z.boolean().optional().describe('If a hand is raised or you are speaking too fast, return at once instead of waiting.'),
69
- }, async (square, { body, as, ...options }, control) => {
70
- const { delivery: _delivery, ...result } = await (await square.participant(as)).express(body, { ...options, ...observation(control) }, control);
71
- return result;
72
- }, rendering((result) => renderExpression(result.observation?.location ?? '', result))),
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))),
73
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.', {
74
39
  as: participant,
75
40
  id: activityId.optional().describe('Take in this activity in full, like act/12, even if you caught it before. Do not combine with idle, from, mention or limit.'),
@@ -77,7 +42,7 @@ export const SQUARE_TOOLS = [
77
42
  from: stringArray.optional().describe('Only what these participants said.'),
78
43
  mention: z.boolean().optional().describe('Only what was addressed to you, and bells.'),
79
44
  limit: z.number().int().min(1).optional().describe('At most this many at once (default 10, up to 100). The rest keeps waiting.'),
80
- }, async (square, { as, ...options }, control) => (await square.participant(as)).catch({ ...options, ...observation(control) }, control), rendering((result, args) => {
45
+ }, async (square, { as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).catch({ ...options, observe: true }, control), rendering((result, args) => {
81
46
  // The CLI always spells the default cap in its continuation, so the tool does too.
82
47
  const options = args;
83
48
  const renderOptions = options.id === undefined ? { ...options, limit: options.limit ?? CATCH_DEFAULT_LIMIT } : options;
@@ -92,25 +57,28 @@ export const SQUARE_TOOLS = [
92
57
  fixed: z.string().optional().describe('Only activities containing this exact text.'),
93
58
  from: stringArray.optional().describe('Only what these participants said.'),
94
59
  mention: z.string().optional().describe('Only what was addressed to this participant.'),
95
- }, (square, args, control) => square.history({ ...args, ...observation(control) }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: args }))),
60
+ }, (square, args, control) => square.history({ ...args, observe: true }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: args }))),
96
61
  tool('attention', 'Turn toward someone or away from them, from now on. Listen: hear everything they say, even to others. Unlisten: stop listening without clearing an ignore; mentions and replies still reach you unless ignored. Ignore: hear nothing from them but bells. What was already said stays as it was.', {
97
62
  as: participant, action: z.enum(['listen', 'unlisten', 'ignore']).describe('"listen", "unlisten" or "ignore". Listening again ends an ignore.'), target,
98
- }, async (square, { as, action, target }, control) => {
99
- const self = await square.participant(as);
100
- return self[action](target, observation(control), control);
63
+ }, async (square, { as, action, target }, control, identity) => {
64
+ const self = await selectOwnSeat(square, identity.owner, as);
65
+ return self[action](target, { observe: true }, control);
101
66
  }, rendering((result, args) => renderListenerChange(result.observation?.location ?? '', args.action, result))),
102
67
  tool('hold', 'Raise a hand to pause the conversation; lower it with release. While a hand is up, new words wait to land.', {
103
68
  as: participant,
104
69
  reason: z.string().optional().describe('Why you are pausing. Everyone sees it.'),
105
70
  release: z.boolean().optional().describe('true lowers the raised hand so the conversation goes on.'),
106
- }, async (square, { as, reason, release }, control) => {
107
- const self = await square.participant(as);
108
- return release === true ? self.resume(observation(control), control) : self.hold(reason, observation(control), control);
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);
109
74
  }, rendering((result, args) => renderLifecycleResult(result.observation?.location ?? '', args.release === true ? 'resume' : 'hold', result))),
110
- 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) => (await square.participant(as)).done(undefined, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', 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))),
111
76
  tool('status', 'Look around the square: who is here, whether a hand is raised, the latest activity, and who you listen to.', {
112
77
  as: z.string().optional().describe('Whose listening to show. Leave it out if you hold exactly one name here.'),
113
- }, (square, { as }, control) => square.status({ viewer: as, ...observation(control) }, control), rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
78
+ }, async (square, { as }, control, identity) => {
79
+ const viewer = (await selectOwnSeat(square, identity.owner, as, { optional: true }))?.name;
80
+ return square.status({ viewer, observe: true }, control);
81
+ }, rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
114
82
  ];
115
83
  export function squareToolFailure(error, signal) {
116
84
  const normalized = signal?.aborted
package/dist/stream.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { SquareError } from './model.js';
3
3
  import { createArtifactPort, observeSquareRevision } from './artifact.js';
4
- import { streamProjection, streamTailProjection } from './views.js';
4
+ import { parseRequiredActivityId, streamProjection, streamTailProjection } from './views.js';
5
5
  /** Pull-based observation; return interrupts an outstanding pull before awaiting its cleanup. */
6
6
  export function streamSquare(squarePath, options = {}, control, borrowed) {
7
7
  return {
@@ -28,7 +28,7 @@ export function streamSquare(squarePath, options = {}, control, borrowed) {
28
28
  }
29
29
  signal.throwIfAborted();
30
30
  const start = options.start ?? { kind: 'tail', last: 10 };
31
- let cursor = start.kind === 'after' ? start.after : -1;
31
+ let cursor = start.kind === 'after' ? parseRequiredActivityId(start.after) : -1;
32
32
  let initial = start.kind === 'tail' ? start : undefined;
33
33
  const observer = await observeSquareRevision(squarePath);
34
34
  const close = () => observer.close();
@@ -60,10 +60,9 @@ export function streamSquare(squarePath, options = {}, control, borrowed) {
60
60
  await observer.hinted(generation, Infinity, signal);
61
61
  continue;
62
62
  }
63
- for (const item of projection.activities) {
63
+ for (const activity of projection.activities) {
64
64
  signal.throwIfAborted();
65
- yield { seq: item.activity.index, square: squarePath, ...item.activity,
66
- ...(item.route === undefined ? {} : { route: item.route }) };
65
+ yield activity;
67
66
  }
68
67
  cursor = projection.cursor;
69
68
  initial = undefined;
package/dist/views.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { type ActivityId } from './square-core.js';
2
2
  import { type StoredAct } from './model.js';
3
3
  import type { ActivityFilter, SquareRead } from './ports.js';
4
- import type { Activity, ArchiveRecord, HistoryQuery, HistoryContinuation, ParticipantsResult, SquareSnapshot, EntryPresentation, ListPresentation, StatusResult, SquareObservation, CatchPresence, StreamProjection } from './square-facade.js';
5
- export type { EntryPresentation, ListPresentation, CatchPresence, StreamProjection } from './square-facade.js';
4
+ import type { Activity, ArchiveRecord, PerceivedActivity, HistoryQuery, HistoryContinuation, ParticipantsResult, SquareSnapshot, EntryPresentation, StatusResult, SquareObservation, CatchPresence } from './square-facade.js';
6
5
  export declare const HISTORY_DEFAULT_LIMIT = 10;
7
6
  export declare const HISTORY_MAX_LIMIT = 100;
8
7
  export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume", "listen", "ignore", "unlisten"];
9
8
  /** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
10
9
  export declare function toPublicActivity(stored: StoredAct): Activity;
10
+ export declare function toPerceivedActivity(facts: SquareRead, stored: StoredAct, viewer: string): PerceivedActivity;
11
11
  export declare function parseRequiredActivityId(id: ActivityId): number;
12
12
  /** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
13
13
  export declare function selectHistory(facts: SquareRead, query: HistoryQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
@@ -19,20 +19,31 @@ export declare function selectHistory(facts: SquareRead, query: HistoryQuery, li
19
19
  export declare function publicActivities(activities: readonly ArchiveRecord[]): Activity[];
20
20
  export declare function snapshot(facts: SquareRead, now: number): SquareSnapshot;
21
21
  export declare function recoveryObservation(facts: SquareRead, name: string, now: number): {
22
- ownActivityCount: number;
23
- hardCap: import("./model.js").HardCap;
24
- holdReason?: string | undefined;
25
22
  participant: string;
26
- roster: string[];
27
23
  activitySummaries: import("./square-facade.js").UnreadActivitySummary[];
28
24
  standingCount: number;
29
25
  held: boolean;
26
+ ownActivityCount: number;
27
+ hardCap: import("./model.js").HardCap;
30
28
  };
31
29
  export declare function entryPresentation(facts: SquareRead, name: string, lastN?: number | null): EntryPresentation;
32
- type ParticipantQueryContext = Pick<SquareObservation, 'location' | 'now' | 'automaticDeliveryAvailable'>;
30
+ type ParticipantQueryContext = Pick<SquareObservation, 'location' | 'now'>;
33
31
  export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext, observe?: boolean): ParticipantsResult;
34
- export declare function listPresentation(facts: SquareRead): ListPresentation;
32
+ export declare function listPresentation(facts: SquareRead): {
33
+ context: readonly string[];
34
+ participants: string[];
35
+ activities: number;
36
+ };
35
37
  export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer?: string, observe?: boolean): StatusResult;
36
38
  export declare function catchPresence(facts: SquareRead, now: number): CatchPresence;
37
- export declare function streamProjection(facts: SquareRead, cursor: number, recipient?: string): StreamProjection;
38
- export declare function streamTailProjection(facts: SquareRead, last?: number, recipient?: string): StreamProjection;
39
+ export declare function streamProjection(facts: SquareRead, cursor: number, recipient?: string): {
40
+ activities: Activity[];
41
+ cursor: number;
42
+ hasMore: boolean;
43
+ };
44
+ export declare function streamTailProjection(facts: SquareRead, last?: number, recipient?: string): {
45
+ activities: Activity[];
46
+ cursor: number;
47
+ hasMore: boolean;
48
+ };
49
+ export {};
package/dist/views.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { formatActivityId, parseActivityId } from './square-core.js';
2
2
  import { compareParticipantActivity, coreParticipants, resolveKnownName, unreadActivitySummaries } from './decisions.js';
3
3
  import { SquareError, nameKey } from './model.js';
4
- import { inSquareCount, readCursor, rosterNames } from './runtime.js';
4
+ import { inSquareCount } from './runtime.js';
5
5
  import { perceiveActivity } from './perception-projection.js';
6
6
  import { compileSearchPattern } from './search.js';
7
7
  const PAGE_SIZE = 100;
@@ -22,6 +22,11 @@ export function toPublicActivity(stored) {
22
22
  mentions: stored.kind === 'say' ? stored.mentions ?? [] : [], ...('target' in stored ? { target: stored.target } : {}),
23
23
  ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}) };
24
24
  }
25
+ export function toPerceivedActivity(facts, stored, viewer) {
26
+ const via = facts.directedAt(viewer, stored.index)?.route;
27
+ return { ...toPublicActivity(stored), perception: perceiveActivity(facts, stored, viewer),
28
+ ...(via === undefined ? {} : { via }) };
29
+ }
25
30
  export function parseRequiredActivityId(id) {
26
31
  const index = parseActivityId(id);
27
32
  if (index === undefined)
@@ -105,12 +110,6 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
105
110
  export function publicActivities(activities) {
106
111
  return activities.flatMap((activity) => activity.kind === 'read' || activity.actor === undefined ? [] : [toPublicActivity(activity)]);
107
112
  }
108
- function statuses(facts, now) {
109
- return coreParticipants(facts, now).filter((p) => p.state !== 'not joined').map((p) => {
110
- const cursor = readCursor(facts, p.name);
111
- return { name: p.name, state: p.state === 'done' ? 'done' : 'joined', consumedThrough: cursor < 0 ? null : formatActivityId(cursor), watching: p.presence === 'watching', listening: p.listening };
112
- });
113
- }
114
113
  export function snapshot(facts, now) {
115
114
  const header = facts.header(), hold = facts.hold();
116
115
  const names = new Map(facts.roster().map((p) => [nameKey(p.name), p.name]));
@@ -118,30 +117,31 @@ export function snapshot(facts, now) {
118
117
  return { context: [...header.preamble, ...header.warmup].join('\n'), actCount: facts.activityCount({ kinds: NON_READ }), hardCap: header.hardCap,
119
118
  ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
120
119
  held: hold.active && hold.actor !== undefined ? { by: hold.actor, ...(hold.reason === undefined ? {} : { reason: hold.reason }) } : null,
121
- participants: statuses(facts, now), delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
120
+ participants: coreParticipants(facts, now).filter((p) => p.presence !== 'never-joined').map(({ name, presence, listening }) => ({ name, presence, listening })),
121
+ delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
122
122
  }
123
123
  export function recoveryObservation(facts, name, now) {
124
124
  const known = resolveKnownName(facts, name), hold = facts.hold();
125
- return { participant: known, roster: rosterNames(facts), activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts), held: hold.active,
126
- ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
125
+ return { participant: known, activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts), held: hold.active,
126
+ ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
127
127
  }
128
128
  export function entryPresentation(facts, name, lastN = HISTORY_DEFAULT_LIMIT) {
129
129
  const header = facts.header();
130
130
  const recentActivities = selectHistory(facts, {}, lastN, ['say', 'done']).activities;
131
131
  return { joinedBefore: facts.activityCount({ kinds: ['join'], actors: [name] }) > 1,
132
132
  scene: header.warmup.join('\n').trim(),
133
- joinContext: (header.preamble.at(-1) === '---' ? header.preamble.slice(0, -1) : header.preamble).join('\n').trim(), recentActivities,
134
- perceptions: Object.fromEntries(recentActivities.map((a) => [a.index, perceiveActivity(facts, a, name)])),
135
- sayNumbers: facts.sayOrdinals(recentActivities.map((a) => a.index)) };
133
+ joinContext: (header.preamble.at(-1) === '---' ? header.preamble.slice(0, -1) : header.preamble).join('\n').trim(),
134
+ recentActivities: recentActivities.map((activity) => ({ ...toPublicActivity(activity), perception: perceiveActivity(facts, activity, name) })) };
136
135
  }
137
136
  function classifyPresence(participants, now) {
138
- const here = participants.filter((participant) => participant.state === 'active' && (participant.presence === 'watching'
137
+ const standing = (participant) => participant.presence === 'active' || participant.presence === 'watching';
138
+ const here = participants.filter((participant) => standing(participant) && (participant.presence === 'watching'
139
139
  || (participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= STATUS_HERE_WINDOW_MS)));
140
140
  const hereNames = new Set(here.map((participant) => participant.name));
141
141
  return { orderedHereNames: [...here.filter((participant) => participant.presence === 'watching'), ...here.filter((participant) => participant.presence !== 'watching')].map((participant) => participant.name),
142
- lingeringCount: participants.filter((participant) => participant.state === 'active' && !hereNames.has(participant.name)).length,
143
- classificationByName: Object.fromEntries(participants.map((participant) => [participant.name, participant.state === 'done' ? 'done'
144
- : participant.state === 'not joined' ? 'not-joined' : hereNames.has(participant.name) ? 'here' : 'lingering'])) };
142
+ lingeringCount: participants.filter((participant) => standing(participant) && !hereNames.has(participant.name)).length,
143
+ classificationByName: Object.fromEntries(participants.map((participant) => [participant.name, participant.presence === 'done' ? 'done'
144
+ : participant.presence === 'never-joined' ? 'not-joined' : participant.presence === 'away' ? 'away' : hereNames.has(participant.name) ? 'here' : 'lingering'])) };
145
145
  }
146
146
  export function queryParticipants(facts, context, observe = false) {
147
147
  const participants = coreParticipants(facts, context.now);
@@ -158,61 +158,61 @@ export function listPresentation(facts) {
158
158
  export function queryStatus(facts, context, viewer = '', observe = false) {
159
159
  const participants = coreParticipants(facts, context.now);
160
160
  const header = facts.header(), hold = facts.hold(), standingCount = inSquareCount(facts);
161
- const listener = participants.find((p) => p.state !== 'not joined' && nameKey(p.name) === nameKey(viewer));
161
+ const listener = participants.find((p) => p.presence !== 'never-joined' && nameKey(p.name) === nameKey(viewer));
162
162
  const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
163
163
  const latestAct = latest === undefined ? undefined : facts.activity(latest.index);
164
164
  const data = {
165
165
  hardCap: header.hardCap,
166
166
  ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
167
- activeCount: standingCount, doneCount: participants.filter((p) => p.state === 'done').length,
168
- hold: hold.active ? {
167
+ held: hold.active ? {
169
168
  ...(hold.actor === undefined ? {} : { by: hold.actor }),
170
169
  ...(hold.reason === undefined ? {} : { reason: hold.reason }),
171
170
  ...(hold.at === undefined ? {} : { at: hold.at }),
172
171
  } : null,
173
172
  ...(listener === undefined ? {} : { listening: listener.listening }),
174
- participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
173
+ participants: participants.filter((p) => p.presence !== 'never-joined'), now: context.now,
175
174
  ...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
176
175
  };
177
176
  if (!observe)
178
177
  return data;
179
178
  const afterIndex = latestAct?.index ?? -1;
180
- const joins = facts.activities(facts.activityHeads({ kinds: ['join'], afterIndex }, { edge: 'newest', limit: 3 }).items.map((h) => h.index)).map((activity) => ({ activity,
181
- rejoined: facts.activityCount({ kinds: ['done'], actors: [activity.actor], actorMatch: 'exact', beforeIndex: activity.index }) > 0 }));
179
+ const joins = facts.activities(facts.activityHeads({ kinds: ['join'], afterIndex }, { edge: 'newest', limit: 3 }).items.map((h) => h.index)).map((activity) => ({
180
+ rejoined: facts.activityCount({ kinds: ['done'], actors: [activity.actor], actorMatch: 'exact', beforeIndex: activity.index }) > 0,
181
+ activity: toPublicActivity(activity)
182
+ }));
182
183
  const canonicalViewer = facts.participant(viewer)?.name ?? viewer;
183
184
  return {
184
185
  ...data,
185
186
  observation: {
186
- ...context, ...classifyPresence(participants, context.now), standingCount, held: hold.active,
187
+ location: context.location,
188
+ ...classifyPresence(participants, context.now), standingCount,
187
189
  knownParticipantCount: participants.length, viewer: canonicalViewer,
188
190
  ...(canonicalViewer === '' ? {} : { participant: canonicalViewer }),
189
191
  joins, joinCount: facts.activityCount({ kinds: ['join'], afterIndex }),
190
192
  attentionNames: participants.filter((p) => canonicalViewer === '' || nameKey(p.name) === nameKey(canonicalViewer)).map((p) => p.name),
191
193
  ...(latestAct === undefined ? {} : { latestPerception: perceiveActivity(facts, latestAct, canonicalViewer) }),
192
- ...(latestAct?.kind === 'say' ? { latestActNumber: facts.sayOrdinals([latestAct.index])[latestAct.index] } : {}),
193
194
  },
194
195
  };
195
196
  }
196
197
  export function catchPresence(facts, now) {
197
- const participants = coreParticipants(facts, now).filter((participant) => participant.state === 'done' || participant.presence === 'watching'
198
+ const participants = coreParticipants(facts, now).filter((participant) => participant.presence === 'done' || participant.presence === 'away' || participant.presence === 'watching'
198
199
  || (participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= PRESENCE_WINDOW_MS));
199
200
  return { participants, now };
200
201
  }
201
202
  export function streamProjection(facts, cursor, recipient) {
202
- const page = facts.activityHeads({ afterIndex: cursor }, { edge: 'oldest', limit: PAGE_SIZE });
203
- const heads = page.items.flatMap((head) => {
204
- const route = recipient === undefined ? undefined : facts.directedAt(recipient, head.index)?.route;
205
- return recipient !== undefined && route === undefined ? [] : [{ index: head.index, route }];
206
- });
207
- return { activities: facts.activities(heads.map((h) => h.index)).map((activity) => {
208
- const route = heads.find((h) => h.index === activity.index).route;
209
- return { activity, ...(route === undefined ? {} : { route }) };
210
- }), cursor: page.items.at(-1)?.index ?? cursor, hasMore: page.hasMore };
203
+ const page = facts.activityHeads({ afterIndex: cursor, kinds: NON_READ, actorRequired: true }, { edge: 'oldest', limit: PAGE_SIZE });
204
+ const heads = recipient === undefined ? page.items : page.items.filter((head) => facts.directedAt(recipient, head.index) !== undefined);
205
+ const activities = facts.activities(heads.map((head) => head.index)).map((activity) => recipient === undefined
206
+ ? toPublicActivity(activity) : toPerceivedActivity(facts, activity, recipient));
207
+ return { activities, cursor: page.items.at(-1)?.index ?? cursor, hasMore: page.hasMore };
211
208
  }
212
209
  export function streamTailProjection(facts, last = HISTORY_DEFAULT_LIMIT, recipient) {
213
210
  if (!Number.isSafeInteger(last) || last < 0 || last > PAGE_SIZE)
214
211
  throw new SquareError('invalid_args', `Invalid stream tail: expected a non-negative safe integer no greater than ${PAGE_SIZE}.`);
215
- const heads = last === 0 ? [] : recipient === undefined ? facts.activityHeads({}, { edge: 'newest', limit: last }).items : facts.directed(recipient, {}, { edge: 'newest', limit: last }).items;
216
- return { activities: facts.activities(heads.map((h) => h.index)).map((activity) => ({ activity,
217
- ...(recipient === undefined ? {} : { route: facts.directedAt(recipient, activity.index).route }) })), cursor: facts.header().lastActIndex, hasMore: false };
212
+ const heads = last === 0 ? [] : recipient === undefined
213
+ ? facts.activityHeads({ kinds: NON_READ, actorRequired: true }, { edge: 'newest', limit: last }).items
214
+ : facts.directed(recipient, {}, { edge: 'newest', limit: last }).items;
215
+ const activities = facts.activities(heads.map((head) => head.index)).map((activity) => recipient === undefined
216
+ ? toPublicActivity(activity) : toPerceivedActivity(facts, activity, recipient));
217
+ return { activities, cursor: facts.header().lastActIndex, hasMore: false };
218
218
  }