@astrosheep/square 0.3.70 → 0.3.71

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 (115) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/help.js +6 -1
  36. package/dist/inbox.d.ts +27 -0
  37. package/dist/inbox.js +55 -18
  38. package/dist/index.d.ts +2 -4
  39. package/dist/index.js +1 -1
  40. package/dist/list.d.ts +7 -1
  41. package/dist/list.js +49 -122
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +7 -37
  44. package/dist/model.d.ts +4 -44
  45. package/dist/model.js +1 -3
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/notifications.js +13 -7
  48. package/dist/open-square.d.ts +16 -2
  49. package/dist/open-square.js +40 -1
  50. package/dist/perception-projection.d.ts +6 -15
  51. package/dist/perception-projection.js +6 -26
  52. package/dist/plugins/opencode/delivery.js +9 -9
  53. package/dist/plugins/opencode/index.js +3 -3
  54. package/dist/ports.d.ts +156 -11
  55. package/dist/presentation-operations.d.ts +1 -1
  56. package/dist/presentation-operations.js +35 -44
  57. package/dist/presentation.d.ts +12 -19
  58. package/dist/presentation.js +33 -54
  59. package/dist/routes.d.ts +7 -14
  60. package/dist/routes.js +33 -62
  61. package/dist/runtime.d.ts +12 -39
  62. package/dist/runtime.js +33 -164
  63. package/dist/square-actions.d.ts +11 -21
  64. package/dist/square-actions.js +171 -271
  65. package/dist/square-application.d.ts +31 -55
  66. package/dist/square-application.js +637 -141
  67. package/dist/square-core.d.ts +3 -94
  68. package/dist/square-core.js +8 -353
  69. package/dist/square-domain.d.ts +24 -0
  70. package/dist/square-domain.js +77 -0
  71. package/dist/square-facade.d.ts +360 -9
  72. package/dist/square-migration.d.ts +24 -0
  73. package/dist/square-migration.js +211 -0
  74. package/dist/square-projections.d.ts +2 -42
  75. package/dist/square-projections.js +1 -37
  76. package/dist/square-sqlite.d.ts +59 -0
  77. package/dist/square-sqlite.js +702 -0
  78. package/dist/stream.d.ts +3 -10
  79. package/dist/stream.js +81 -52
  80. package/dist/views.d.ts +27 -95
  81. package/dist/views.js +217 -87
  82. package/dist/wake-eligibility.d.ts +15 -5
  83. package/dist/wake-eligibility.js +32 -16
  84. package/dist/wakes.d.ts +3 -3
  85. package/dist/wakes.js +31 -27
  86. package/guides/application-facade-delivery-evidence.md +200 -0
  87. package/guides/application-facade-design.md +95 -0
  88. package/guides/change-waits.md +4 -4
  89. package/guides/migration.md +77 -0
  90. package/guides/primitive-catch-consolidation.md +85 -0
  91. package/guides/primitive-history-consolidation.md +76 -0
  92. package/guides/primitive-status-consolidation.md +87 -0
  93. package/guides/primitive-storage-consolidation.md +128 -0
  94. package/guides/row-runtime-delivery-evidence.md +160 -0
  95. package/guides/row-storage-design.md +567 -0
  96. package/package.json +1 -1
  97. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  98. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  99. package/plugins/opencode/delivery.ts +11 -11
  100. package/plugins/opencode/index.ts +3 -3
  101. package/plugins/paseo/client/square-message.tsx +70 -52
  102. package/plugins/paseo/shared/message.ts +15 -0
  103. package/plugins/pi/square-pi.js +3 -0
  104. package/dist/activity.d.ts +0 -14
  105. package/dist/activity.js +0 -248
  106. package/dist/square-file-adapter.d.ts +0 -26
  107. package/dist/square-file-adapter.js +0 -85
  108. package/dist/square-state.d.ts +0 -6
  109. package/dist/square-state.js +0 -138
  110. package/dist/square-storage.d.ts +0 -14
  111. package/dist/square-storage.js +0 -188
  112. package/dist/square-wiring.d.ts +0 -33
  113. package/dist/square-wiring.js +0 -109
  114. package/dist/watch.d.ts +0 -8
  115. package/dist/watch.js +0 -252
@@ -1,13 +1,11 @@
1
- import { sameName } from './model.js';
1
+ import { sameName, isSquareError } from './model.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { participantIdentity } from './participant-identity.js';
5
5
  export { participantIdentity } from './participant-identity.js';
6
- import { audienceIncludes, audienceOf, MAX_IDENTITY_SET_SIZE } from './square-core.js';
7
- import { perceiveActivity } from './delivery.js';
8
- import { actId, sayNumberFor, HELD_WAIT_BUDGET_MS } from './runtime.js';
6
+ import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
7
+ import { HELD_WAIT_BUDGET_MS } from './runtime.js';
9
8
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
10
- import { compareParticipantActivity } from './decisions.js';
11
9
  import { grepSnippet } from './search.js';
12
10
  import { style } from './tty-style.js';
13
11
  export function displayPath(squarePath, cwd = process.cwd()) {
@@ -39,6 +37,11 @@ export function formatRefusal(squarePath, bodyLines, opts = {}) {
39
37
  return body === '' ? '' : `${body}\n`;
40
38
  return withPathOutput(squarePath, body, { ...opts, plain: true });
41
39
  }
40
+ export function refusalText(error) {
41
+ const text = error instanceof Error ? error.message : String(error);
42
+ const marked = isSquareError(error) && ['never_joined', 'not_standing', 'unmatched_mention'].includes(error.facts?.reason ?? '');
43
+ return marked ? text.split('\n').map((line) => `✕ ${line}`).join('\n') : text;
44
+ }
42
45
  export function quoteShell(value) {
43
46
  return `'${value.replace(/'/g, `'\\''`)}'`;
44
47
  }
@@ -69,11 +72,7 @@ function formatAge(ms) {
69
72
  return `${Math.max(0, ms)}ms`;
70
73
  return `${Math.floor(ms / 1000)}s`;
71
74
  }
72
- const PRESENCE_WINDOW_MS = 8 * 60 * 60 * 1000;
73
- export function renderPresenceLines(participants, now, limit = 5) {
74
- const recent = participants
75
- .filter((p) => p.state === 'done' || p.presence === 'watching' || (p.lastActiveAt !== undefined && now - p.lastActiveAt <= PRESENCE_WINDOW_MS))
76
- .sort(compareParticipantActivity);
75
+ export function renderPresenceLines(recent, now, limit = 5) {
77
76
  const shown = recent.slice(0, limit);
78
77
  if (shown.length === 0)
79
78
  return [style('dim', ' ○ nobody nearby')];
@@ -137,9 +136,11 @@ export function renderRoomChangeText(event) {
137
136
  case 'resume':
138
137
  return `${identity} lowered the hand`;
139
138
  case 'listen':
140
- return `${identity} turned an ear toward ${participantIdentity(event.target)}`;
139
+ return `${identity} turned an ear toward ${participantIdentity(event.target ?? 'someone')}`;
141
140
  case 'ignore':
142
- return `${identity} turned away from ${participantIdentity(event.target)}`;
141
+ return `${identity} turned away from ${participantIdentity(event.target ?? 'someone')}`;
142
+ default:
143
+ throw new Error('Expected a lifecycle activity');
143
144
  }
144
145
  }
145
146
  function renderedBody(body, maxChars) {
@@ -152,6 +153,12 @@ function bodySuffix(body) {
152
153
  return '';
153
154
  return `\n${body.split('\n').map((line) => ` ${line}`).join('\n')}`;
154
155
  }
156
+ function eventId(event) {
157
+ return 'index' in event ? formatActivityId(event.index) : event.id;
158
+ }
159
+ function replyId(reply) {
160
+ return typeof reply === 'number' ? formatActivityId(reply) : reply;
161
+ }
155
162
  function renderEventCliFacts(event, opts = {}) {
156
163
  const now = opts.now;
157
164
  const maxBody = opts.preview;
@@ -168,17 +175,17 @@ function renderEventCliFacts(event, opts = {}) {
168
175
  case 'say': {
169
176
  const body = renderedBody(event.body, maxBody);
170
177
  const mention = opts.mention;
171
- const mentionSuffix = mention !== undefined && audienceIncludes(audienceOf(event), mention)
178
+ const mentionSuffix = mention !== undefined && (event.reach === 'bell' || event.mentions?.some((name) => sameName(name, mention)))
172
179
  ? ` · calls your name across the square — ${participantIdentity(mention)}`
173
180
  : '';
174
- const replySuffix = event.reply === undefined ? '' : ` · replies to ${actId(event.reply)}`;
175
- const meta = ` #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}`;
181
+ const replySuffix = event.reply === undefined ? '' : ` · replies to ${replyId(event.reply)}`;
182
+ const meta = ` #${opts.actNumber ?? 1} · ${eventId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}`;
176
183
  return { text: `● ${participantIdentity(event.actor)}${style('dim', meta)}${bodySuffix(body.text)}`, bodyClipped: body.clipped };
177
184
  }
178
185
  case 'done': {
179
186
  const body = renderedBody(event.body, maxBody);
180
187
  return {
181
- text: `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${actId(event)} · ${formatRelativeTime(event.at, now)}`)}${bodySuffix(body.text)}`,
188
+ text: `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${eventId(event)} · ${formatRelativeTime(event.at, now)}`)}${bodySuffix(body.text)}`,
182
189
  bodyClipped: body.clipped,
183
190
  };
184
191
  }
@@ -190,9 +197,8 @@ export function renderEventCli(event, opts = {}) {
190
197
  return renderEventCliFacts(event, opts).text;
191
198
  }
192
199
  function renderPresenceOnlySay(event, opts = {}) {
193
- const audience = audienceOf(event);
194
- const targets = audience.kind === 'bell' ? [] : audience.names;
195
- const meta = style('dim', ` #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}`);
200
+ const targets = event.reach === 'bell' ? [] : event.mentions ?? [];
201
+ const meta = style('dim', ` #${opts.actNumber ?? 1} · ${eventId(event)} · ${formatRelativeTime(event.at, opts.now)}`);
196
202
  if (targets.length === 0) {
197
203
  return `● ${participantIdentity(event.actor)}${meta}`;
198
204
  }
@@ -207,10 +213,9 @@ function renderPresenceOnlySay(event, opts = {}) {
207
213
  export function renderAmbientEventFacts(event, viewer, opts = {}) {
208
214
  if (event.kind !== 'say')
209
215
  return renderEventCliFacts(event, opts);
210
- if (opts.perception === undefined && opts.squareState === undefined) {
211
- throw new Error('Ambient say rendering requires a settled perception or SquareState');
212
- }
213
- const seen = opts.perception ?? perceiveActivity(opts.squareState, event, viewer);
216
+ if (opts.perception === undefined)
217
+ throw new Error('Ambient say rendering requires a settled perception');
218
+ const seen = opts.perception;
214
219
  if (seen === 'presence') {
215
220
  const presence = renderPresenceOnlySay(event, opts);
216
221
  const body = opts.presencePreview === undefined ? { text: '', clipped: false } : renderedBody(event.body, opts.presencePreview);
@@ -248,17 +253,8 @@ function renderUnreadSummary(opts) {
248
253
  ...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
249
254
  ];
250
255
  }
251
- export function renderPendingFeed(history, publicItems, viewer = '', squareState) {
252
- const lines = [];
253
- for (const act of publicItems) {
254
- const rendered = renderAmbientEvent(act, viewer, {
255
- actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
256
- ...(squareState === undefined ? {} : { squareState }),
257
- });
258
- if (rendered !== '')
259
- lines.push(rendered);
260
- }
261
- return lines.join('\n\n');
256
+ export function renderPendingFeed(publicItems, sayNumbers, perceptions, viewer) {
257
+ return publicItems.map((act) => renderAmbientEvent(act, viewer, { actNumber: sayNumbers[act.index], perception: perceptions[act.index] })).filter(Boolean).join('\n\n');
262
258
  }
263
259
  export function renderActivityBlocked(opts) {
264
260
  const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch`;
@@ -344,7 +340,7 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
344
340
  let truncated = false;
345
341
  for (const act of publicVisible) {
346
342
  const rawBody = act.body ?? '';
347
- const header = `${style('dim', actId(act.index))} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
343
+ const header = `${style('dim', formatActivityId(act.index))} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
348
344
  if (perception?.(act) === 'presence') {
349
345
  chunks.push(header);
350
346
  continue;
@@ -370,10 +366,10 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
370
366
  chunks.push(`${header}\n ${text.trim()}${omitted}`);
371
367
  }
372
368
  if (publicVisible.length === 1) {
373
- chunks.push(`${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
369
+ chunks.push(`${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
374
370
  }
375
371
  else if (truncated && publicVisible.length > 1) {
376
- chunks.push(`${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2 --no-truncate`);
372
+ chunks.push(`${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2 --no-truncate`);
377
373
  }
378
374
  return chunks.join('\n\n');
379
375
  }
@@ -447,20 +443,3 @@ export function renderDoctorClean() {
447
443
  export function renderDoctorUnfixable(reason) {
448
444
  return ['✕ unreadable artifact', ` · ${truncateExternalDiagnostic(reason)}`].join('\n');
449
445
  }
450
- export function renderWatchOutput(history, publicItems, opts) {
451
- const sections = [];
452
- if (publicItems.length > 0) {
453
- const rendered = publicItems
454
- .map((act) => renderAmbientEvent(act, opts.viewer, {
455
- actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
456
- mention: opts.mention,
457
- ...(opts.perceptions?.has(act.index) ? { perception: opts.perceptions.get(act.index) } : {}),
458
- ...(opts.squareState === undefined ? {} : { squareState: opts.squareState }),
459
- }))
460
- .filter(Boolean)
461
- .join('\n\n');
462
- if (rendered !== '')
463
- sections.push(rendered);
464
- }
465
- return sections.join('\n\n') + '\n';
466
- }
package/dist/routes.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type WakeRoute, type WakeRouteKind } from './model.js';
2
+ import type { SquareRead, SquareWrite } from './ports.js';
2
3
  export { WAKE_ROUTE_KINDS } from './model.js';
3
4
  export type { WakeRoute, WakeRouteKind } from './model.js';
4
5
  export declare const ROUTE_FRESH_MS: number;
@@ -18,25 +19,17 @@ export declare function routeIdentityKey(route: Pick<WakeRoute, 'location' | 'pa
18
19
  export type RouteEpoch = {
19
20
  readonly epoch?: number;
20
21
  };
21
- export declare function applyWakeRouteToState(state: import('./model.js').SquareState, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, at?: number): void;
22
- export declare function dropSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string, expectedEpoch?: number): void;
23
- /**
24
- * Drop one ended session's routes at a location, atomically with the lifecycle decide.
25
- *
26
- * A route survives when the ending transaction did not commit `done` and this session
27
- * still holds the sole live presence for its participant — an in-flight same-session
28
- * rejoin. `liveParticipants === undefined` means the ledger could not be read, so
29
- * nothing is dropped on suspicion. An explicit epoch fence additionally keeps routes
30
- * published by a later generation.
31
- */
32
- export declare function dropEndedSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string, opts: {
22
+ export declare function applyWakeRoute(facts: SquareWrite, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, at?: number): void;
23
+ export declare function dropSessionWakeRoutes(facts: SquareWrite, location: string, sessionId: string, expectedEpoch?: number): void;
24
+ /** Keep an in-flight same-session rejoin; unknown host evidence is not a reason to retire it. */
25
+ export declare function dropEndedSessionWakeRoutes(facts: SquareWrite, location: string, sessionId: string, opts: {
33
26
  readonly expectedEpoch?: number;
34
27
  readonly liveParticipants?: ReadonlySet<string>;
35
28
  readonly force: boolean;
36
29
  readonly participant?: string;
37
30
  }): void;
38
- export declare function dropParticipantWakeRoutesFromState(state: import('./model.js').SquareState, location: string, participant: string): void;
39
- export declare function sessionCanEndParticipant(state: import('./model.js').SquareState, location: string, participant: string, sessionId: string, currentSessionId?: string): boolean;
31
+ export declare function dropParticipantWakeRoutes(facts: SquareWrite, location: string, participant: string): void;
32
+ export declare function sessionCanEndParticipant(facts: SquareRead, location: string, participant: string, sessionId: string, sessionIdNow?: string): boolean;
40
33
  export declare function publishWakeRoute(artifact: import('./ports.js').SquareArtifactPort, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, opts?: {
41
34
  at?: number;
42
35
  requireCurrentSession?: boolean;
package/dist/routes.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { nameKey } from './model.js';
2
- import { isCurrentlyJoined } from './runtime.js';
3
2
  import { canonicalPath, canonicalPathSync } from './canonical-path.js';
4
3
  export { WAKE_ROUTE_KINDS } from './model.js';
5
4
  export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
@@ -44,89 +43,61 @@ function sameRouteAddress(left, right) {
44
43
  const rightKeys = Object.keys(right).sort();
45
44
  return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => rightKeys[index] === key && left[key] === right[key]);
46
45
  }
47
- export function applyWakeRouteToState(state, route, at = Date.now()) {
46
+ export function applyWakeRoute(facts, route, at = Date.now()) {
48
47
  const location = canonicalPathSync(route.location);
49
- state.routes = [...(state.routes ?? []).filter((item) => {
50
- const itemLocation = canonicalPathSync(item.location);
51
- return !(itemLocation === location && routeIdentityKey(item, itemLocation) === routeIdentityKey(route, location));
52
- }), { ...route, location, updatedAt: at }];
48
+ const identity = routeIdentityKey(route, location);
49
+ facts.replaceRoutes([...facts.routes().filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== identity),
50
+ { ...route, location, updatedAt: at, extra: {} }]);
53
51
  }
54
- export function dropSessionWakeRoutesFromState(state, location, sessionId, expectedEpoch) {
52
+ export function dropSessionWakeRoutes(facts, location, sessionId, expectedEpoch) {
55
53
  const canonical = canonicalPathSync(location);
56
- state.routes = (state.routes ?? []).filter((item) => {
57
- if (item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical)
58
- return true;
59
- if (expectedEpoch === undefined)
60
- return false;
61
- return item.epoch !== expectedEpoch;
62
- });
54
+ facts.replaceRoutes(facts.routes().filter((item) => item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical
55
+ || (expectedEpoch !== undefined && item.epoch !== expectedEpoch)));
63
56
  }
64
- /**
65
- * Drop one ended session's routes at a location, atomically with the lifecycle decide.
66
- *
67
- * A route survives when the ending transaction did not commit `done` and this session
68
- * still holds the sole live presence for its participant — an in-flight same-session
69
- * rejoin. `liveParticipants === undefined` means the ledger could not be read, so
70
- * nothing is dropped on suspicion. An explicit epoch fence additionally keeps routes
71
- * published by a later generation.
72
- */
73
- export function dropEndedSessionWakeRoutesFromState(state, location, sessionId, opts) {
57
+ /** Keep an in-flight same-session rejoin; unknown host evidence is not a reason to retire it. */
58
+ export function dropEndedSessionWakeRoutes(facts, location, sessionId, opts) {
74
59
  const canonical = canonicalPathSync(location);
75
- state.routes = (state.routes ?? []).filter((item) => {
60
+ facts.replaceRoutes(facts.routes().filter((item) => {
76
61
  if (item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical)
77
62
  return true;
78
63
  if (opts.participant !== undefined && nameKey(item.participant) !== nameKey(opts.participant))
79
64
  return true;
80
- if (!opts.force) {
81
- // Keep only a route whose participant is still joined and whose ended session still
82
- // holds the sole live presence — an in-flight same-session rejoin. A no-longer-joined
83
- // participant is retired even when a stale presence row survives.
84
- if (isCurrentlyJoined(state.acts, item.participant)
85
- && (opts.liveParticipants === undefined || opts.liveParticipants.has(nameKey(item.participant))))
86
- return true;
87
- }
88
- if (opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch)
65
+ if (!opts.force && facts.participant(item.participant)?.joined
66
+ && (opts.liveParticipants === undefined || opts.liveParticipants.has(nameKey(item.participant))))
89
67
  return true;
90
- return false;
91
- });
68
+ return opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch;
69
+ }));
92
70
  }
93
- export function dropParticipantWakeRoutesFromState(state, location, participant) {
71
+ export function dropParticipantWakeRoutes(facts, location, participant) {
94
72
  const canonical = canonicalPathSync(location);
95
- state.routes = (state.routes ?? []).filter((item) => !(nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical));
73
+ facts.replaceRoutes(facts.routes().filter((item) => !(nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical)));
96
74
  }
97
- export function sessionCanEndParticipant(state, location, participant, sessionId, currentSessionId) {
75
+ export function sessionCanEndParticipant(facts, location, participant, sessionId, sessionIdNow) {
98
76
  const canonical = canonicalPathSync(location);
99
- const owned = (state.routes ?? []).filter((item) => nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical);
77
+ const owned = facts.routes().filter((item) => nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical);
100
78
  if (owned.length === 0)
101
- return currentSessionId === sessionId;
102
- return owned.every((item) => item.sessionId === sessionId) && (currentSessionId === undefined || currentSessionId === sessionId);
79
+ return sessionIdNow === sessionId;
80
+ return owned.every((item) => item.sessionId === sessionId) && (sessionIdNow === undefined || sessionIdNow === sessionId);
103
81
  }
104
82
  export async function publishWakeRoute(artifact, route, opts = {}) {
105
83
  const location = await canonicalPath(route.location);
106
84
  const at = opts.at ?? Date.now();
107
- await artifact.transact((state) => {
108
- if (opts.requireCurrentSession) {
109
- if (!isCurrentlyJoined(state.acts, route.participant)
110
- || (state.routes ?? []).some((item) => nameKey(item.participant) === nameKey(route.participant) && canonicalPathSync(item.location) === location && item.sessionId !== route.sessionId))
111
- return { state, result: undefined };
112
- }
113
- const identity = routeIdentityKey({ ...route, location });
114
- const matching = (state.routes ?? []).filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) === identity);
85
+ await artifact.transact((facts) => {
86
+ const routes = facts.routes();
87
+ if (opts.requireCurrentSession && (!facts.participant(route.participant)?.joined
88
+ || routes.some((item) => nameKey(item.participant) === nameKey(route.participant) && canonicalPathSync(item.location) === location && item.sessionId !== route.sessionId)))
89
+ return;
90
+ const identity = routeIdentityKey(route, location);
91
+ const matching = routes.filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) === identity);
115
92
  const existing = matching[0];
116
- const existingEpoch = existing?.epoch;
117
- const epochMatches = route.epoch === undefined ? existingEpoch === undefined : existingEpoch === route.epoch;
118
- if (matching.length === 1 && existing !== undefined && existing.updatedAt > at - ROUTE_FRESH_MS && existing.kind === route.kind && existing.channel === route.channel && sameRouteAddress(existing.address, route.address) && epochMatches)
119
- return { result: undefined };
120
- return { state: { ...state, routes: [...(state.routes ?? []).filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== identity), { ...route, location, updatedAt: at }] }, result: undefined };
93
+ if (matching.length === 1 && existing.updatedAt > at - ROUTE_FRESH_MS && existing.kind === route.kind
94
+ && existing.channel === route.channel && sameRouteAddress(existing.address, route.address) && existing.epoch === route.epoch)
95
+ return;
96
+ applyWakeRoute(facts, { ...route, location }, at);
121
97
  });
122
98
  }
123
99
  export async function retireWakeRouteFromArtifact(artifact, route, opts = {}) {
124
100
  const location = await canonicalPath(route.location);
125
- const target = routeIdentityKey({ ...route, location });
126
- await artifact.transact((state) => ({ state: { ...state, routes: (state.routes ?? []).filter((item) => {
127
- const itemLocation = canonicalPathSync(item.location);
128
- if (routeIdentityKey(item, itemLocation) !== target)
129
- return true;
130
- return opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch;
131
- }) }, result: undefined }));
101
+ const target = routeIdentityKey(route, location);
102
+ await artifact.transact((facts) => facts.replaceRoutes(facts.routes().filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== target || (opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch))));
132
103
  }
package/dist/runtime.d.ts CHANGED
@@ -1,48 +1,21 @@
1
- import { type ActivityId, type LandedAudienceReplay, type Reach } from './square-core.js';
2
- import { type StoredAct, type SquareState, type HoldState, type ObservationState, type WatchLease } from './model.js';
1
+ import { type ActivityId } from './square-core.js';
2
+ import { type StoredAct, type WatchLease } from './model.js';
3
+ import type { SquareRead } from './ports.js';
3
4
  export declare const WATCH_HEARTBEAT_MS: number;
4
5
  export declare const SLEEP_MS: number;
5
6
  export declare const STALE_MS: number;
6
7
  export declare const WATCH_STALE_MS: number;
7
8
  export declare const THROTTLE_WINDOW_MS: number;
8
9
  export declare const UNREAD_BLOCK_GRACE_MS: number;
9
- /** A held express waits for the hand to lower, but not forever: past this budget it gives up exactly like --no-wait (draft saved, retry printed). 0 disables waiting. */
10
10
  export declare const HELD_WAIT_BUDGET_MS: number;
11
+ export declare const FACT_PAGE_SIZE = 100;
11
12
  export declare function nowMs(): number;
12
- /** Pure directed-activity filter. Bell matches every viewer; mentions match by audience. */
13
- export declare function matchesMentionTarget(act: {
14
- body: string;
15
- mentions?: readonly string[];
16
- reach?: Reach;
17
- }, mention: string | true): boolean;
18
- export declare function foldedState(squareState: SquareState): import("./square-core.js").FoldedSquareState;
19
- export declare function rosterNames(squareState: SquareState): string[];
20
- export declare function inSquareCount(squareState: SquareState): number;
21
- export declare function resolveRosterName(squareState: SquareState, name: string): string | undefined;
22
- export declare function countSays(acts: StoredAct[], name: string): number;
23
- export declare function sayNumberFor(acts: StoredAct[], target: StoredAct): number;
24
- /** Shared terminal condition for a participant's watch lifecycle. */
25
- export declare function watchTerminalStatus(squareState: SquareState, name: string): 'capped' | 'quorum' | undefined;
26
- export declare function joinedNames(acts: StoredAct[]): Set<string>;
27
- export declare function isCurrentlyJoined(acts: StoredAct[], name: string): boolean;
28
- /** Stable index of the recipient's most recent join act, if any. */
29
- export declare function lastJoinIndex(acts: StoredAct[], name: string): number | undefined;
30
- export declare function actStableIndex(act: StoredAct): number;
13
+ export declare function rosterNames(facts: SquareRead): string[];
14
+ export declare function inSquareCount(facts: SquareRead): number;
31
15
  export declare function actId(actOrIndex: StoredAct | number): ActivityId;
32
- export declare function currentHold(acts: StoredAct[]): HoldState;
33
- export declare function publicActs(acts: StoredAct[]): Array<Extract<StoredAct, {
34
- kind: 'say' | 'done';
35
- }>>;
36
- export declare function recordObservation(squareState: SquareState, name: string, index: number, state: ObservationState, at?: number): boolean;
37
- export declare function readCursor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
38
- /**
39
- * The last public act the participant was verifiably present for, or -1 when there is no evidence.
40
- *
41
- * "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
42
- * and seen receipts after their latest join. Anything else — in particular acts merely
43
- * not addressed to them — says nothing about where they actually were.
44
- */
45
- export declare function presenceAnchor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
46
- export declare function freshWatchLease(squareState: SquareState, name: string, at?: number): WatchLease | undefined;
47
- export declare function writeWatchLease(squareState: SquareState, name: string, lease: WatchLease): void;
48
- export declare function removeWatchLease(squareState: SquareState, name: string, leaseId: string): boolean;
16
+ export declare function watchTerminalStatus(facts: SquareRead, name: string): 'capped' | 'quorum' | undefined;
17
+ /** A continuous prefix stops at the first unseen historical audience edge after join. */
18
+ export declare function readCursor(facts: SquareRead, name: string): number;
19
+ /** Presence rides on own activity or exact receipts, not nonaddressed cursor progress. */
20
+ export declare function presenceAnchor(facts: SquareRead, name: string): number;
21
+ export declare function freshWatchLease(facts: SquareRead, name: string, at?: number): WatchLease | undefined;
package/dist/runtime.js CHANGED
@@ -1,13 +1,12 @@
1
- import { audienceIncludes, audienceOf, fold, formatActivityId, replayLandedAudiences } from './square-core.js';
2
- import { SquareError, findParticipantName, nameKey, sameName, } from './model.js';
1
+ import { formatActivityId } from './square-core.js';
2
+ import { SquareError, sameName } from './model.js';
3
3
  function parseIntegerEnv(name, fallback) {
4
4
  const raw = process.env[name];
5
5
  if (raw === undefined)
6
6
  return fallback;
7
7
  const value = parseInt(raw, 10);
8
- if (!Number.isFinite(value) || value <= 0) {
8
+ if (!Number.isFinite(value) || value <= 0)
9
9
  throw new SquareError('invalid_args', `Invalid ${name}: expected a positive integer.`);
10
- }
11
10
  return value;
12
11
  }
13
12
  export const WATCH_HEARTBEAT_MS = parseIntegerEnv('SQUARE_WATCH_HEARTBEAT_MS', 60000);
@@ -16,177 +15,47 @@ export const STALE_MS = parseIntegerEnv('SQUARE_WATCH_QUIET_MS', 300000);
16
15
  export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', 180000);
17
16
  export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
18
17
  export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
19
- /** A held express waits for the hand to lower, but not forever: past this budget it gives up exactly like --no-wait (draft saved, retry printed). 0 disables waiting. */
20
18
  export const HELD_WAIT_BUDGET_MS = parseIntegerEnv('SQUARE_HELD_WAIT_BUDGET_MS', 10 * 60 * 1000);
19
+ export const FACT_PAGE_SIZE = 100;
21
20
  export function nowMs() {
22
21
  const override = process.env.SQUARE_NOW_MS;
23
22
  if (override === undefined)
24
23
  return Date.now();
25
24
  const value = parseInt(override, 10);
26
- if (!Number.isFinite(value)) {
25
+ if (!Number.isFinite(value))
27
26
  throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
28
- }
29
27
  return value;
30
28
  }
31
- /** Pure directed-activity filter. Bell matches every viewer; mentions match by audience. */
32
- export function matchesMentionTarget(act, mention) {
33
- const audience = audienceOf(act);
34
- if (mention === true)
35
- return audience.kind === 'bell' || audience.names.length > 0;
36
- return audienceIncludes(audience, mention);
37
- }
38
- export function foldedState(squareState) {
39
- return fold(squareState.acts);
40
- }
41
- export function rosterNames(squareState) {
42
- return foldedState(squareState).participants.map((participant) => participant.name);
43
- }
44
- export function inSquareCount(squareState) {
45
- return foldedState(squareState).joined.length;
46
- }
47
- export function resolveRosterName(squareState, name) {
48
- return findParticipantName(rosterNames(squareState), name);
49
- }
50
- export function countSays(acts, name) {
51
- return acts.filter((act) => act.kind === 'say' && sameName(act.actor, name)).length;
52
- }
53
- export function sayNumberFor(acts, target) {
54
- if (target.kind !== 'say')
55
- throw new Error('sayNumberFor only applies to say acts');
56
- let number = 0;
57
- for (const act of acts) {
58
- if (act.kind === 'say' && sameName(act.actor, target.actor))
59
- number++;
60
- if (act === target)
61
- return number;
62
- if (target.index !== undefined && act.index === target.index)
63
- return number;
64
- }
65
- throw new Error('target say act is not present in act history');
66
- }
67
- /** Shared terminal condition for a participant's watch lifecycle. */
68
- export function watchTerminalStatus(squareState, name) {
69
- if (squareState.hardCap !== null && countSays(squareState.acts, name) >= squareState.hardCap)
29
+ export function rosterNames(facts) { return facts.roster().map((participant) => participant.name); }
30
+ export function inSquareCount(facts) { return facts.roster().filter((participant) => participant.joined).length; }
31
+ export function actId(actOrIndex) { return formatActivityId(typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index); }
32
+ export function watchTerminalStatus(facts, name) {
33
+ const cap = facts.header().hardCap;
34
+ if (cap !== null && (facts.participant(name)?.sayCount ?? 0) >= cap)
70
35
  return 'capped';
71
- const done = new Set(fold(squareState.acts).done.map((participant) => nameKey(participant)));
72
- done.delete(nameKey(name));
73
- const peers = rosterNames(squareState).filter((participant) => !sameName(participant, name));
74
- return peers.length > 0 && peers.every((peer) => done.has(nameKey(peer))) ? 'quorum' : undefined;
75
- }
76
- export function joinedNames(acts) {
77
- return new Set(acts.filter((act) => act.kind === 'join').map((act) => nameKey(act.actor)));
78
- }
79
- export function isCurrentlyJoined(acts, name) {
80
- return fold(acts).participants.some((participant) => sameName(participant.name, name) && participant.joined);
81
- }
82
- /** Stable index of the recipient's most recent join act, if any. */
83
- export function lastJoinIndex(acts, name) {
84
- let last;
85
- for (const act of acts) {
86
- if (act.kind === 'join' && sameName(act.actor, name))
87
- last = actStableIndex(act);
88
- }
89
- return last;
90
- }
91
- export function actStableIndex(act) {
92
- if (act.index === undefined)
93
- throw new Error(`act ${act.kind} is missing a stable index`);
94
- return act.index;
95
- }
96
- export function actId(actOrIndex) {
97
- const index = typeof actOrIndex === 'number' ? actOrIndex : actStableIndex(actOrIndex);
98
- return formatActivityId(index);
99
- }
100
- function canonicalRuntimeName(squareState, name) {
101
- return resolveRosterName(squareState, name) ?? name;
102
- }
103
- export function currentHold(acts) {
104
- const hold = fold(acts).hold;
105
- return hold.active ? { active: true, at: hold.at, reason: hold.reason } : { active: false };
106
- }
107
- export function publicActs(acts) {
108
- return acts.filter((act) => act.kind === 'say' || act.kind === 'done');
109
- }
110
- export function recordObservation(squareState, name, index, state, at = Date.now()) {
111
- if (!Number.isSafeInteger(index) || index < 0 || !Number.isFinite(at))
112
- return false;
113
- const key = canonicalRuntimeName(squareState, name);
114
- const id = formatActivityId(index);
115
- const current = squareState.runtime.observations[key]?.[id];
116
- if (current?.state === 'seen' || (current?.state === state && current.at >= at))
117
- return false;
118
- const next = {
119
- state: 'seen',
120
- at: Math.max(current?.at ?? -Infinity, at),
121
- };
122
- ((squareState.runtime.observations ??= {})[key] ??= {})[id] = next;
123
- return true;
124
- }
125
- export function readCursor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
126
- const recipient = landed.resolveParticipant(name) ?? name;
127
- const boundary = landed.lastJoinIndex(recipient) ?? -1;
128
- let cursor = boundary;
129
- for (const act of squareState.acts) {
130
- if (act.index <= boundary || act.kind === 'read' || act.actor === undefined)
131
- continue;
132
- if (sameName(act.actor, recipient)) {
133
- cursor = act.index;
134
- continue;
135
- }
136
- if (!landed.includes(act, recipient)) {
137
- cursor = act.index;
138
- continue;
139
- }
140
- if (squareState.runtime.observations?.[recipient]?.[formatActivityId(act.index)]?.state !== 'seen')
141
- break;
142
- cursor = act.index;
143
- }
144
- return cursor;
145
- }
146
- /**
147
- * The last public act the participant was verifiably present for, or -1 when there is no evidence.
148
- *
149
- * "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
150
- * and seen receipts after their latest join. Anything else — in particular acts merely
151
- * not addressed to them — says nothing about where they actually were.
152
- */
153
- export function presenceAnchor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
154
- const recipient = landed.resolveParticipant(name) ?? name;
155
- const boundary = landed.lastJoinIndex(recipient) ?? -1;
36
+ const peers = facts.roster().filter((participant) => !sameName(participant.name, name));
37
+ return peers.length > 0 && peers.every((peer) => peer.done) ? 'quorum' : undefined;
38
+ }
39
+ /** A continuous prefix stops at the first unseen historical audience edge after join. */
40
+ export function readCursor(facts, name) {
41
+ const boundary = facts.participant(name)?.lastJoinIndex ?? -1;
42
+ const block = facts.directed(name, { afterIndex: boundary, unseenOnly: true }, { edge: 'oldest', limit: 1 }).items[0]?.index;
43
+ const last = facts.activityHeads({
44
+ afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }), actorRequired: true,
45
+ kinds: ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore'],
46
+ }, { edge: 'newest', limit: 1 }).items[0]?.index;
47
+ return Math.max(boundary, last ?? boundary);
48
+ }
49
+ /** Presence rides on own activity or exact receipts, not nonaddressed cursor progress. */
50
+ export function presenceAnchor(facts, name) {
51
+ const boundary = facts.participant(name)?.lastJoinIndex ?? -1;
156
52
  if (boundary < 0)
157
53
  return -1;
158
- let evidence = boundary;
159
- for (const act of squareState.acts) {
160
- if (act.index <= boundary || act.index <= evidence || act.actor === undefined)
161
- continue;
162
- if (sameName(act.actor, recipient)
163
- || squareState.runtime.observations?.[recipient]?.[formatActivityId(act.index)]?.state === 'seen') {
164
- evidence = act.index;
165
- }
166
- }
167
- for (let i = squareState.acts.length - 1; i >= 0; i--) {
168
- const event = squareState.acts[i];
169
- if (event.index <= boundary || event.index > evidence)
170
- continue;
171
- if (event.kind === 'say' || event.kind === 'done')
172
- return event.index;
173
- }
174
- return -1;
175
- }
176
- export function freshWatchLease(squareState, name, at = Date.now()) {
177
- const key = canonicalRuntimeName(squareState, name);
178
- const lease = squareState.runtime.leases[key];
179
- if (lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS)
180
- return undefined;
181
- return lease;
182
- }
183
- export function writeWatchLease(squareState, name, lease) {
184
- squareState.runtime.leases[canonicalRuntimeName(squareState, name)] = lease;
54
+ const own = facts.activityHeads({ afterIndex: boundary, actors: [name] }, { edge: 'newest', limit: 1 }).items[0]?.index;
55
+ const evidence = Math.max(boundary, own ?? boundary, facts.receiptFacts(name, boundary).latestActorActivityIndex ?? boundary);
56
+ return facts.activityHeads({ afterIndex: boundary, throughIndex: evidence, kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0]?.index ?? -1;
185
57
  }
186
- export function removeWatchLease(squareState, name, leaseId) {
187
- const key = canonicalRuntimeName(squareState, name);
188
- if (squareState.runtime.leases[key]?.leaseId !== leaseId)
189
- return false;
190
- delete squareState.runtime.leases[key];
191
- return true;
58
+ export function freshWatchLease(facts, name, at = Date.now()) {
59
+ const lease = facts.lease(name);
60
+ return lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS ? undefined : lease;
192
61
  }