@astrosheep/square 0.3.11 → 0.3.13

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 (127) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/codex-plugin/hooks/hooks.json +9 -0
  3. package/dist/activity-feed.d.ts +14 -0
  4. package/dist/activity-feed.js +5 -5
  5. package/dist/activity.d.ts +9 -0
  6. package/dist/activity.js +112 -96
  7. package/dist/artifact.d.ts +20 -0
  8. package/dist/artifact.js +317 -648
  9. package/dist/automatic-session.d.ts +5 -0
  10. package/dist/automatic-session.js +86 -0
  11. package/dist/boundary-presentation.d.ts +5 -0
  12. package/dist/boundary-presentation.js +6 -5
  13. package/dist/claude-hook.d.ts +9 -0
  14. package/dist/claude-hook.js +35 -3
  15. package/dist/cli/context.d.ts +35 -0
  16. package/dist/cli/context.js +26 -31
  17. package/dist/cli/harness-command.d.ts +28 -0
  18. package/dist/cli/harness-command.js +30 -15
  19. package/dist/cli/maintenance-commands.d.ts +7 -0
  20. package/dist/cli/maintenance-commands.js +12 -69
  21. package/dist/cli/meta-commands.d.ts +5 -0
  22. package/dist/cli/observation-commands.d.ts +20 -0
  23. package/dist/cli/observation-commands.js +159 -139
  24. package/dist/cli/program.d.ts +2 -0
  25. package/dist/cli/program.js +3 -12
  26. package/dist/cli/registry.d.ts +9 -0
  27. package/dist/cli/registry.js +2 -4
  28. package/dist/cli/square-commands.d.ts +34 -0
  29. package/dist/cli/square-commands.js +90 -84
  30. package/dist/cmd/notify-once.d.ts +2 -0
  31. package/dist/codex-hook.d.ts +10 -0
  32. package/dist/codex-hook.js +35 -3
  33. package/dist/decisions.d.ts +92 -0
  34. package/dist/decisions.js +93 -81
  35. package/dist/delivery-health.d.ts +21 -0
  36. package/dist/delivery-health.js +41 -34
  37. package/dist/delivery.d.ts +81 -0
  38. package/dist/delivery.js +71 -69
  39. package/dist/file-lock.d.ts +6 -0
  40. package/dist/harness-claude.d.ts +19 -0
  41. package/dist/harness-claude.js +8 -3
  42. package/dist/harness-codex.d.ts +29 -0
  43. package/dist/harness-codex.js +2 -2
  44. package/dist/harness-links.d.ts +17 -0
  45. package/dist/harness-links.js +2 -2
  46. package/dist/harness-pi.d.ts +11 -0
  47. package/dist/harness-pi.js +2 -2
  48. package/dist/harness-stage.d.ts +6 -0
  49. package/dist/harness.d.ts +20 -0
  50. package/dist/harness.js +5 -3
  51. package/dist/help.d.ts +6 -0
  52. package/dist/help.js +12 -13
  53. package/dist/identity.d.ts +13 -0
  54. package/dist/inbox.d.ts +2 -0
  55. package/dist/inbox.js +15 -19
  56. package/dist/index.d.ts +13 -0
  57. package/dist/index.js +31 -67
  58. package/dist/landing.d.ts +10 -0
  59. package/dist/landing.js +95 -0
  60. package/dist/list.d.ts +1 -0
  61. package/dist/list.js +39 -61
  62. package/dist/model.d.ts +147 -0
  63. package/dist/model.js +12 -8
  64. package/dist/notifications.d.ts +30 -0
  65. package/dist/notifications.js +83 -91
  66. package/dist/open-square.d.ts +11 -0
  67. package/dist/open-square.js +4 -0
  68. package/dist/participant-identity.d.ts +1 -0
  69. package/dist/participant-identity.js +3 -0
  70. package/dist/paseo-connection.d.ts +15 -0
  71. package/dist/paseo-delivery.d.ts +14 -0
  72. package/dist/paseo-state.d.ts +11 -0
  73. package/dist/paseo-timeline.d.ts +16 -0
  74. package/dist/presence.d.ts +3 -0
  75. package/dist/presence.js +42 -0
  76. package/dist/presentation.d.ts +91 -0
  77. package/dist/presentation.js +52 -63
  78. package/dist/presented.d.ts +11 -0
  79. package/dist/registry.d.ts +58 -0
  80. package/dist/registry.js +36 -3
  81. package/dist/routes.d.ts +42 -0
  82. package/dist/runtime.d.ts +45 -0
  83. package/dist/runtime.js +48 -55
  84. package/dist/search.d.ts +11 -0
  85. package/dist/square-core.d.ts +111 -0
  86. package/dist/square-core.js +63 -9
  87. package/dist/square-facade.d.ts +86 -0
  88. package/dist/square-facade.js +1 -0
  89. package/dist/square-file-adapter.d.ts +18 -0
  90. package/dist/square-file-adapter.js +70 -0
  91. package/dist/square-storage.d.ts +20 -0
  92. package/dist/square-storage.js +171 -0
  93. package/dist/square-wiring.d.ts +19 -0
  94. package/dist/square-wiring.js +70 -0
  95. package/dist/square.d.ts +2 -0
  96. package/dist/state-cell.d.ts +14 -0
  97. package/dist/state-cell.js +1 -0
  98. package/dist/stream.d.ts +3 -0
  99. package/dist/stream.js +19 -23
  100. package/dist/time.d.ts +6 -0
  101. package/dist/views.d.ts +95 -0
  102. package/dist/views.js +81 -0
  103. package/dist/wake-attempts.d.ts +39 -0
  104. package/dist/wake-attempts.js +8 -4
  105. package/dist/wake-evidence.d.ts +12 -0
  106. package/dist/wake-evidence.js +7 -10
  107. package/dist/wake-port.d.ts +20 -0
  108. package/dist/wake-sink.d.ts +12 -0
  109. package/dist/wakes.d.ts +40 -0
  110. package/dist/wakes.js +62 -0
  111. package/dist/watch.d.ts +2 -0
  112. package/dist/watch.js +89 -210
  113. package/extensions/square-opencode.js +27 -1
  114. package/extensions/square-pi.js +13 -0
  115. package/guides/participant.md +7 -174
  116. package/package.json +5 -4
  117. package/skills/brainstorm/SKILL.md +30 -30
  118. package/skills/square/.claude-plugin/plugin.json +1 -1
  119. package/skills/square/SKILL.md +56 -29
  120. package/skills/square/hooks/hooks.json +9 -0
  121. package/skills/square-feedback/SKILL.md +9 -9
  122. package/dist/compact.js +0 -26
  123. package/dist/doctor.js +0 -36
  124. package/dist/square-application.js +0 -262
  125. package/template.md +0 -4
  126. package/templates/architect.md +0 -4
  127. package/templates/brainstorm.md +0 -4
@@ -1,8 +1,10 @@
1
1
  import { sameName } from './model.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { fold, perceive } from './square-core.js';
5
- import { actId, actStableIndex, extractMentions, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
4
+ import { participantIdentity } from './participant-identity.js';
5
+ export { participantIdentity } from './participant-identity.js';
6
+ import { audienceIncludes, audienceOf, perceive } from './square-core.js';
7
+ import { actId, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
6
8
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
7
9
  import { grepSnippet } from './search.js';
8
10
  function headerLine(squarePath, opts = {}) {
@@ -49,7 +51,7 @@ function pluralize(count, singular, plural = `${singular}s`) {
49
51
  const PRESENCE_WINDOW_MS = 8 * 60 * 60 * 1000;
50
52
  function presenceGlyph(participant) {
51
53
  if (participant.state === 'done')
52
- return '×';
54
+ return '';
53
55
  if (participant.presence === 'watching')
54
56
  return '◎';
55
57
  if (participant.presenceAt !== undefined)
@@ -77,7 +79,7 @@ export function renderPresenceLines(participants, now, limit = 5) {
77
79
  const shown = recent.slice(0, limit);
78
80
  if (shown.length === 0)
79
81
  return [' ○ nobody nearby'];
80
- const lines = shown.map((p) => ` ${presenceGlyph(p)} ${p.name} · ${presenceText(p, now)}`);
82
+ const lines = shown.map((p) => ` ${presenceGlyph(p)} ${participantIdentity(p.name)} · ${presenceText(p, now)}`);
81
83
  const remaining = recent.length - shown.length;
82
84
  if (remaining > 0)
83
85
  lines.push(` ○ …and ${remaining} more`);
@@ -116,15 +118,16 @@ export function previewActivityBody(body) {
116
118
  }
117
119
  export function renderRoomChangeText(event) {
118
120
  const actor = event.actor ?? 'someone';
121
+ const identity = actor === 'someone' ? actor : participantIdentity(actor);
119
122
  switch (event.kind) {
120
123
  case 'join':
121
- return `${actor} stepped into the square`;
124
+ return `${identity} stepped into the square`;
122
125
  case 'done':
123
- return `${actor} stepped out of the square`;
126
+ return `${identity} stepped out of the square`;
124
127
  case 'hold':
125
- return `${actor} raised a hand${event.body ? ` — ${event.body}` : ''}`;
128
+ return `${identity} raised a hand${event.body ? ` — ${event.body}` : ''}`;
126
129
  case 'resume':
127
- return `${actor} lowered the hand`;
130
+ return `${identity} lowered the hand`;
128
131
  }
129
132
  }
130
133
  function renderedBody(body, maxChars) {
@@ -150,38 +153,32 @@ export function renderEventCli(event, opts = {}) {
150
153
  case 'say': {
151
154
  const body = renderedBody(event.body, maxBody);
152
155
  const mention = opts.mention;
153
- const mentionSuffix = mention !== undefined && extractMentions(event.body).some((name) => sameName(name, mention))
154
- ? ` · calls your name across the square — @${mention}`
156
+ const mentionSuffix = mention !== undefined && audienceIncludes(audienceOf(event), mention)
157
+ ? ` · calls your name across the square — ${participantIdentity(mention)}`
155
158
  : '';
156
159
  const replySuffix = event.reply === undefined ? '' : ` · replies to ${actId(event.reply)}`;
157
- return `● ${event.actor} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}${bodySuffix(body)}`;
160
+ return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}${bodySuffix(body)}`;
158
161
  }
159
162
  case 'done': {
160
163
  const body = renderedBody(event.body, maxBody);
161
- return ${event.actor} stepped out of the square — done · ${actId(event)} · ${formatRelativeTime(event.at, now)}${bodySuffix(body)}`;
164
+ return `○ ${participantIdentity(event.actor)} stepped out of the square — done · ${actId(event)} · ${formatRelativeTime(event.at, now)}${bodySuffix(body)}`;
162
165
  }
163
166
  case 'read':
164
167
  return '';
165
168
  }
166
169
  }
167
- function renderPresenceOnlySay(event) {
168
- if (event.kind !== 'say' || event.reach === undefined || event.reach === 'bell')
169
- return '';
170
- return `*walks over to @${event.reach.beside}*`;
171
- }
172
- function perceptionFor(history, event, viewer) {
173
- const cutoff = history.findIndex((item) => actStableIndex(item) === actStableIndex(event));
174
- const acts = cutoff >= 0 ? history.slice(0, cutoff) : history;
175
- return perceive(fold(acts), event, viewer);
170
+ function renderPresenceOnlySay(event, opts = {}) {
171
+ const audience = audienceOf(event);
172
+ const targets = audience.kind === 'bell' ? [] : audience.names;
173
+ const dest = targets.length === 0 ? '' : ` ${targets.map((name) => participantIdentity(name)).join(' and ')}`;
174
+ return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}\n talked to${dest}`;
176
175
  }
177
- export function renderVisibleEvent(history, event, viewer, opts = {}) {
176
+ export function renderAmbientEvent(event, viewer, opts = {}) {
178
177
  if (event.kind !== 'say')
179
178
  return renderEventCli(event, opts);
180
- const seen = perceptionFor(history, event, viewer);
181
- if (seen === 'none')
182
- return '';
179
+ const seen = perceive(event, viewer);
183
180
  if (seen === 'presence')
184
- return renderPresenceOnlySay(event);
181
+ return renderPresenceOnlySay(event, opts);
185
182
  return renderEventCli(event, opts);
186
183
  }
187
184
  function draftSavedLines(draftPath) {
@@ -194,12 +191,13 @@ function renderUnreadSummary(opts) {
194
191
  return [
195
192
  ...opts.activitySummaries.flatMap((item) => [
196
193
  ...item.previews.slice(-1).map((preview) => {
197
- const rendered = renderVisibleEvent([preview.act], preview.act, opts.viewer, { actNumber: preview.number });
194
+ const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number });
198
195
  if (rendered === '')
199
- return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago`;
200
- if (rendered.startsWith('*'))
201
- return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered}`;
202
- return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`;
196
+ return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago`;
197
+ if (preview.act.kind === 'say' && perceive(preview.act, opts.viewer) === 'presence') {
198
+ return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered.replace(/\n/g, ' ')}`;
199
+ }
200
+ return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`;
203
201
  }),
204
202
  ]),
205
203
  ...opts.roomChanges.map((act) => ` · ${renderRoomChangeText(act)}`),
@@ -208,7 +206,7 @@ function renderUnreadSummary(opts) {
208
206
  export function renderPendingFeed(history, publicItems, roomChanges, viewer = '') {
209
207
  const lines = [];
210
208
  for (const act of publicItems) {
211
- const rendered = renderVisibleEvent(history, act, viewer, {
209
+ const rendered = renderAmbientEvent(act, viewer, {
212
210
  actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
213
211
  });
214
212
  if (rendered !== '')
@@ -262,15 +260,15 @@ export function renderPublicTail(events, lastN, now, viewer = '') {
262
260
  const selected = lastN == null ? publicItems : publicItems.slice(-lastN);
263
261
  const preview = lastN == null ? undefined : BODY_PREVIEW_LENGTH;
264
262
  return selected
265
- .map((event) => renderVisibleEvent(events, event, viewer, { now, preview, actNumber: event.kind === 'say' ? sayNumberFor(events, event) : undefined }))
263
+ .map((event) => renderAmbientEvent(event, viewer, { now, preview, actNumber: event.kind === 'say' ? sayNumberFor(events, event) : undefined }))
266
264
  .filter(Boolean)
267
265
  .join('\n\n');
268
266
  }
269
- function lastPresenceAnchor(doc, name) {
270
- const cursor = readCursor(doc, name);
271
- for (let i = doc.acts.length - 1; i >= 0; i--) {
272
- const event = doc.acts[i];
273
- const index = actStableIndex(event);
267
+ function lastPresenceAnchor(squareState, name) {
268
+ const cursor = readCursor(squareState, name);
269
+ for (let i = squareState.acts.length - 1; i >= 0; i--) {
270
+ const event = squareState.acts[i];
271
+ const index = event.index;
274
272
  if (index > cursor)
275
273
  continue;
276
274
  if (event.kind === 'say' || event.kind === 'done')
@@ -279,24 +277,27 @@ function lastPresenceAnchor(doc, name) {
279
277
  return -1;
280
278
  }
281
279
  function renderLastPresenceMarker(name) {
282
- return ${name}'s footprints reach here`;
280
+ return `→ ${participantIdentity(name)} was here`;
283
281
  }
284
- export function renderActivitiesView(doc, visible, lastN, full, squarePath, viewer = '') {
282
+ export function renderActivitiesView(squareState, visible, lastN, full, squarePath, viewer = '', mode = 'ambient') {
285
283
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
286
284
  const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
287
285
  const previewLen = full ? undefined : BODY_PREVIEW_LENGTH;
288
286
  const markers = new Map();
289
- for (const participant of rosterNames(doc)) {
290
- const anchor = lastPresenceAnchor(doc, participant);
287
+ for (const participant of rosterNames(squareState)) {
288
+ const anchor = lastPresenceAnchor(squareState, participant);
291
289
  if (anchor >= 0)
292
290
  markers.set(anchor, [...(markers.get(anchor) ?? []), participant]);
293
291
  }
294
292
  const chunks = [];
295
293
  for (const act of shown) {
296
- const rendered = renderVisibleEvent(doc.acts, act, viewer, {
294
+ const opts = {
297
295
  preview: previewLen,
298
- actNumber: act.kind === 'say' ? sayNumberFor(doc.acts, act) : undefined,
299
- });
296
+ actNumber: act.kind === 'say' ? sayNumberFor(squareState.acts, act) : undefined,
297
+ };
298
+ const rendered = mode === 'archive'
299
+ ? renderEventCli(act, opts)
300
+ : renderAmbientEvent(act, viewer, opts);
300
301
  if (rendered !== '')
301
302
  chunks.push(rendered);
302
303
  for (const participant of markers.get(act.index) ?? []) {
@@ -306,7 +307,7 @@ export function renderActivitiesView(doc, visible, lastN, full, squarePath, view
306
307
  if (chunks.length === 0)
307
308
  return 'latest\n ○ no public activity in this view';
308
309
  if (previewLen !== undefined) {
309
- const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen);
310
+ const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen && (mode === 'archive' || perceive(act, viewer) === 'full'));
310
311
  if (truncated)
311
312
  chunks.push(`» ${commandPrefix(squarePath)} history --full`);
312
313
  }
@@ -329,13 +330,13 @@ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath
329
330
  const rawBody = act.body ?? '';
330
331
  if (full === true) {
331
332
  const body = rawBody.split('\n').map((line) => ` ${line}`).join('\n');
332
- chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}\n${body}`);
333
+ chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}\n${body}`);
333
334
  continue;
334
335
  }
335
336
  const snippet = grepSnippet(rawBody, pattern, GREP_PREVIEW_CHARS, fixed);
336
337
  if (snippet === undefined) {
337
338
  const preview = previewBody(rawBody, GREP_PREVIEW_CHARS);
338
- chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}${preview === '' ? '' : `\n ${preview}`}`);
339
+ chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}${preview === '' ? '' : `\n ${preview}`}`);
339
340
  continue;
340
341
  }
341
342
  const clippedBefore = snippet.beforeOmitted > 0;
@@ -345,7 +346,7 @@ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath
345
346
  const omitted = clippedBefore || clippedAfter
346
347
  ? `\n · ${snippet.beforeOmitted} chars before · ${snippet.afterOmitted} chars after`
347
348
  : '';
348
- chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}\n ${text.trim()}${omitted}`);
349
+ chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}\n ${text.trim()}${omitted}`);
349
350
  }
350
351
  if (publicVisible.length === 1) {
351
352
  chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --full' : ''}`);
@@ -412,20 +413,8 @@ function renderRoomChanges(changes) {
412
413
  export function renderDoctorClean() {
413
414
  return '✓ no problems found';
414
415
  }
415
- export function renderDoctorProblems(problems) {
416
- return [`✕ ${problems.length} ${pluralize(problems.length, 'problem')} found`, ...problems.map((problem) => ` · ${problem.kind}: ${problem.message}`)].join('\n');
417
- }
418
416
  export function renderDoctorUnfixable(reason) {
419
- return ['✕ cannot repair', ` · ${reason}`].join('\n');
420
- }
421
- export function renderDoctorRepaired(actions, quarantinedCount, sidecarPath) {
422
- if (actions.length === 0)
423
- return '✓ no problems found';
424
- return [
425
- '✓ repaired',
426
- ...actions.map((action) => ` · ${action.message}`),
427
- ...(quarantinedCount > 0 && sidecarPath !== undefined ? [` · quarantined ${quarantinedCount} act block(s)`, ` · sidecar ${sidecarPath}`] : []),
428
- ].join('\n');
417
+ return ['✕ unreadable artifact', ` · ${reason}`].join('\n');
429
418
  }
430
419
  export function renderWatchOutput(history, publicItems, roomChanges, opts) {
431
420
  const sections = [];
@@ -448,7 +437,7 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
448
437
  sections.push(room);
449
438
  if (publicItems.length > 0) {
450
439
  const rendered = publicItems
451
- .map((act) => renderVisibleEvent(history, act, opts.viewer, {
440
+ .map((act) => renderAmbientEvent(act, opts.viewer, {
452
441
  actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
453
442
  mention: opts.mention,
454
443
  }))
@@ -0,0 +1,11 @@
1
+ import { type InboxMembership } from './model.js';
2
+ export declare function presentedPath(env?: NodeJS.ProcessEnv): string;
3
+ export declare function hasPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
4
+ /** True when any current participant owner has already received this attention. */
5
+ export declare function hasPresentedAttention(squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
6
+ /**
7
+ * Serialize presentation only for the affected participants. Delivery runs
8
+ * outside the short ledger-write lock, so unrelated owners never wait on an
9
+ * adapter. A throwing callback leaves no row and remains unpresented.
10
+ */
11
+ export declare function presentOnce<T>(sessionId: string, lookup: (sessionId: string) => InboxMembership[], deliver: (inbox: InboxMembership[]) => T, env?: NodeJS.ProcessEnv, at?: number): T | undefined;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Machine-local participant discovery cache.
3
+ *
4
+ * The Square artifact remains authoritative for membership. This append-only
5
+ * cache only maps native harness sessions and optional Paseo agent ids back to
6
+ * active (square path, participant name) pairs.
7
+ */
8
+ import { type StoredAct } from './model.js';
9
+ export type SessionChannel = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'paseo' | 'unknown';
10
+ export interface RegistryBinding {
11
+ sessionId: string;
12
+ name: string;
13
+ squarePath: string;
14
+ channel: SessionChannel;
15
+ child: boolean;
16
+ paseoAgentId?: string;
17
+ ownerId: string;
18
+ updatedAt: number;
19
+ }
20
+ export interface RegistryWriteOptions {
21
+ channel?: SessionChannel;
22
+ child?: boolean;
23
+ paseoAgentId?: string;
24
+ ownerId?: string;
25
+ at?: number;
26
+ }
27
+ export declare function registryPath(): string;
28
+ export declare function canonicalSquarePath(squarePath: string): string;
29
+ export declare function recordJoin(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): void;
30
+ export declare function recordDone(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): void;
31
+ export declare function lookupSessionBindings(sessionId: string, now?: number): RegistryBinding[];
32
+ export declare function lookupSession(sessionId: string, now?: number): Array<{
33
+ name: string;
34
+ squarePath: string;
35
+ }>;
36
+ export declare function lookupParticipant(squarePath: string, name: string, now?: number): RegistryBinding[];
37
+ /** Resolve the current local harness owner for a participant, if one is registered. */
38
+ export declare function localParticipantOwner(squarePath: string, name: string, env?: NodeJS.ProcessEnv, now?: number): string | undefined;
39
+ export declare function localParticipantName(squarePath: string, env?: NodeJS.ProcessEnv): string | undefined;
40
+ export interface RegistryPruneResult {
41
+ removed: number;
42
+ kept: number;
43
+ }
44
+ /** Compact the registry and remove only bindings disproved by their authoritative artifact. */
45
+ export declare function pruneRegistry(readActs: (squarePath: string) => StoredAct[] | undefined, now?: number): RegistryPruneResult;
46
+ export interface LocalSessionIdentity {
47
+ sessionId: string;
48
+ channel: SessionChannel;
49
+ child: boolean;
50
+ paseoAgentId?: string;
51
+ }
52
+ export declare function localSessionIdentities(env?: NodeJS.ProcessEnv): LocalSessionIdentity[];
53
+ /** True when this process belongs to a harness that can deliver Square attention without a foreground catch. */
54
+ export declare function hasAutomaticDeliveryIdentity(env?: NodeJS.ProcessEnv): boolean;
55
+ export declare function recordLocalJoin(name: string, squarePath: string, env?: NodeJS.ProcessEnv): void;
56
+ export declare function recordLocalDone(name: string, squarePath: string, env?: NodeJS.ProcessEnv): void;
57
+ export declare function recordSessionJoin(sessionId: string, name: string, squarePath: string, channel: SessionChannel, env?: NodeJS.ProcessEnv): string;
58
+ export declare function recordSessionDone(sessionId: string, name: string, squarePath: string, channel: SessionChannel, env?: NodeJS.ProcessEnv): boolean;
package/dist/registry.js CHANGED
@@ -36,8 +36,8 @@ export function canonicalSquarePath(squarePath) {
36
36
  return absolute;
37
37
  }
38
38
  }
39
- function bindingKey(sessionId, squarePath, name) {
40
- return JSON.stringify([sessionId, canonicalSquarePath(squarePath), nameKey(name)]);
39
+ function bindingKey(sessionId, squarePath, name, channel) {
40
+ return JSON.stringify([sessionId, canonicalSquarePath(squarePath), nameKey(name), channel]);
41
41
  }
42
42
  function participantKey(squarePath, name) {
43
43
  return JSON.stringify([canonicalSquarePath(squarePath), nameKey(name)]);
@@ -94,7 +94,7 @@ function foldRegistry(raw, now) {
94
94
  continue;
95
95
  order++;
96
96
  const ownerId = entry.owner_id ?? `legacy:${order}`;
97
- state.set(bindingKey(entry.session_id, entry.square_path, entry.name), {
97
+ state.set(bindingKey(entry.session_id, entry.square_path, entry.name, entry.channel), {
98
98
  entry,
99
99
  updatedAt: Date.parse(entry.ts),
100
100
  ownerId,
@@ -231,6 +231,11 @@ export function localParticipantOwner(squarePath, name, env = process.env, now =
231
231
  return undefined;
232
232
  return lookupParticipant(squarePath, name, now).find((binding) => sessionIds.has(binding.sessionId))?.ownerId;
233
233
  }
234
+ export function localParticipantName(squarePath, env = process.env) {
235
+ const identities = localSessionIdentities(env);
236
+ const names = new Set(identities.flatMap((identity) => lookupSession(identity.sessionId).filter((item) => canonicalSquarePath(item.squarePath) === canonicalSquarePath(squarePath)).map((item) => item.name)));
237
+ return names.size === 1 ? [...names][0] : undefined;
238
+ }
234
239
  function bindingIsProvablyObsolete(binding, acts) {
235
240
  return acts !== undefined && !isCurrentlyJoined(acts, binding.name);
236
241
  }
@@ -306,3 +311,31 @@ export function recordLocalDone(name, squarePath, env = process.env) {
306
311
  retireOwnerWakeRoutes(ownerId, { at, env });
307
312
  }
308
313
  }
314
+ export function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) {
315
+ const at = Date.now();
316
+ const ownerId = nextOwnerId();
317
+ const current = lookupParticipant(squarePath, name, at);
318
+ for (const binding of current) {
319
+ recordDone(binding.sessionId, binding.name, binding.squarePath, {
320
+ channel: binding.channel,
321
+ child: binding.child,
322
+ ...(binding.paseoAgentId ? { paseoAgentId: binding.paseoAgentId } : {}),
323
+ at,
324
+ });
325
+ retireOwnerWakeRoutes(binding.ownerId, { at, env });
326
+ }
327
+ recordJoin(sessionId, name, squarePath, { channel, at, ownerId });
328
+ publishWakeRoutes(ownerId, { at, env });
329
+ return ownerId;
330
+ }
331
+ export function recordSessionDone(sessionId, name, squarePath, channel, env = process.env) {
332
+ const binding = lookupSessionBindings(sessionId).find((item) => canonicalSquarePath(item.squarePath) === canonicalSquarePath(squarePath)
333
+ && sameName(item.name, name)
334
+ && item.channel === channel);
335
+ if (binding === undefined)
336
+ return false;
337
+ const at = Date.now();
338
+ recordDone(sessionId, binding.name, binding.squarePath, { channel, at });
339
+ retireOwnerWakeRoutes(binding.ownerId, { at, env });
340
+ return true;
341
+ }
@@ -0,0 +1,42 @@
1
+ import { type WakeRoute, type WakeRouteKind } from './model.js';
2
+ export { isWakeRouteKind, WAKE_ROUTE_KINDS } from './model.js';
3
+ export type { WakeRoute, WakeRouteKind } from './model.js';
4
+ export declare const ROUTE_FRESH_MS: number;
5
+ export declare function routesPath(env?: NodeJS.ProcessEnv): string;
6
+ export declare function readWakeRoutes(opts?: {
7
+ ownerId?: string;
8
+ freshOnly?: boolean;
9
+ now?: number;
10
+ env?: NodeJS.ProcessEnv;
11
+ }): WakeRoute[];
12
+ export declare function upsertWakeRoute(route: Omit<WakeRoute, 'updatedAt'>, opts?: {
13
+ at?: number;
14
+ env?: NodeJS.ProcessEnv;
15
+ }): void;
16
+ export declare function retireOwnerWakeRoutes(ownerId: string, opts?: {
17
+ at?: number;
18
+ env?: NodeJS.ProcessEnv;
19
+ }): void;
20
+ export interface WakeRouteEvidence {
21
+ sessionId: string;
22
+ address: Record<string, string>;
23
+ }
24
+ /** A provider's self-check: complete, callable endpoint evidence for one kind, or nothing. */
25
+ export type WakeRouteProbe = (env: NodeJS.ProcessEnv) => WakeRouteEvidence | undefined;
26
+ /**
27
+ * One probe per kind. A probe publishes only when its provider's complete
28
+ * endpoint evidence is present; session identity alone never publishes. The
29
+ * four native transports have no endpoint lifecycle in this delivery, so
30
+ * their probes publish nothing until those transports land.
31
+ */
32
+ export declare const WAKE_ROUTE_PROBES: Readonly<Record<WakeRouteKind, WakeRouteProbe>>;
33
+ /** The kind-neutral publication loop; probes supply complete evidence per kind. */
34
+ export declare function publishWakeRoutesFrom(ownerId: string, probes: Readonly<Record<WakeRouteKind, WakeRouteProbe>>, opts?: {
35
+ at?: number;
36
+ env?: NodeJS.ProcessEnv;
37
+ }): void;
38
+ /** Publication boundary: every route written is complete provider evidence. */
39
+ export declare function publishWakeRoutes(ownerId: string, opts?: {
40
+ at?: number;
41
+ env?: NodeJS.ProcessEnv;
42
+ }): void;
@@ -0,0 +1,45 @@
1
+ import { type ActivityId, type Reach } from './square-core.js';
2
+ import { type StoredAct, type SquareState, type HoldState, type ReadCursor, type WatchLease } from './model.js';
3
+ export declare const WATCH_HEARTBEAT_MS: number;
4
+ export declare const SLEEP_MS: number;
5
+ export declare const STALE_MS: number;
6
+ export declare const WATCH_STALE_MS: number;
7
+ export declare const LOCK_RETRY_MS: number;
8
+ export declare const LOCK_STALE_MS: number;
9
+ export declare const THROTTLE_WINDOW_MS: number;
10
+ export declare const UNREAD_BLOCK_GRACE_MS: number;
11
+ 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
+ reach?: Reach;
16
+ }, mention: string | true): boolean;
17
+ export declare function foldedState(squareState: SquareState): import("./square-core.js").FoldedSquareState;
18
+ export declare function rosterNames(squareState: SquareState): string[];
19
+ export declare function inSquareCount(squareState: SquareState): number;
20
+ export declare function resolveRosterName(squareState: SquareState, name: string): string | undefined;
21
+ export declare function hasQuorum(squareState: SquareState, name: string, outs: Set<string>): boolean;
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 doneNames(acts: StoredAct[]): Set<string>;
27
+ export declare function joinedNames(acts: StoredAct[]): Set<string>;
28
+ export declare function isCurrentlyJoined(acts: StoredAct[], name: string): boolean;
29
+ /** Stable index of the recipient's most recent join act, if any. */
30
+ export declare function lastJoinIndex(acts: StoredAct[], name: string): number | undefined;
31
+ export declare function actStableIndex(act: StoredAct): number;
32
+ export declare function actId(actOrIndex: StoredAct | number): ActivityId;
33
+ export declare function getReadState(squareState: SquareState, name: string): ReadCursor | undefined;
34
+ export declare function readCursor(squareState: SquareState, name: string): number;
35
+ export declare function currentHold(acts: StoredAct[]): HoldState;
36
+ export declare function publicActs(acts: StoredAct[]): Array<Extract<StoredAct, {
37
+ kind: 'say' | 'done';
38
+ }>>;
39
+ export declare function advanceCursor(squareState: SquareState, name: string, index: number, updatedAt?: number): boolean;
40
+ export declare function touchPresenceCursor(squareState: SquareState, name: string, at: number, consumedThroughIndex?: number): boolean;
41
+ export declare function latestActIndex(acts: StoredAct[]): number;
42
+ export declare function freshWatchLease(squareState: SquareState, name: string, at?: number): WatchLease | undefined;
43
+ export declare function watchLease(squareState: SquareState, name: string): WatchLease | undefined;
44
+ export declare function writeWatchLease(squareState: SquareState, name: string, lease: WatchLease): void;
45
+ export declare function removeWatchLease(squareState: SquareState, name: string, leaseId: string): boolean;
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { fold } from './square-core.js';
1
+ import { audienceIncludes, audienceOf, fold, formatActivityId } from './square-core.js';
2
2
  import { SquareError, findParticipantName, nameKey, sameName, } from './model.js';
3
3
  function parseIntegerEnvValue(name, raw, fallback) {
4
4
  if (raw === undefined)
@@ -51,42 +51,27 @@ export function nowMs() {
51
51
  }
52
52
  return value;
53
53
  }
54
- export function extractMentions(body) {
55
- const matches = [];
56
- const re = /@([\p{L}\p{N}_-]+)/gu;
57
- let match;
58
- while ((match = re.exec(body)) !== null)
59
- matches.push(match[1]);
60
- return matches;
61
- }
62
- /** Pure directed-activity filter. Broadcast bodies (no @) match any named viewer. */
54
+ /** Pure directed-activity filter. Bell matches every viewer; mentions match by audience. */
63
55
  export function matchesMentionTarget(act, mention) {
64
- if (act.reach === 'bell')
65
- return true;
66
- if (act.reach !== undefined) {
67
- return mention === true || sameName(act.reach.beside, mention);
68
- }
69
- const mentions = extractMentions(act.body);
56
+ const audience = audienceOf(act);
70
57
  if (mention === true)
71
- return mentions.length > 0;
72
- if (mentions.length === 0)
73
- return true;
74
- return mentions.some((name) => sameName(name, mention));
58
+ return audience.kind === 'bell' || audience.names.length > 0;
59
+ return audienceIncludes(audience, mention);
75
60
  }
76
- export function foldedState(doc) {
77
- return fold(doc.acts);
61
+ export function foldedState(squareState) {
62
+ return fold(squareState.acts);
78
63
  }
79
- export function rosterNames(doc) {
80
- return foldedState(doc).participants.map((participant) => participant.name);
64
+ export function rosterNames(squareState) {
65
+ return foldedState(squareState).participants.map((participant) => participant.name);
81
66
  }
82
- export function inSquareCount(doc) {
83
- return foldedState(doc).joined.length;
67
+ export function inSquareCount(squareState) {
68
+ return foldedState(squareState).joined.length;
84
69
  }
85
- export function resolveRosterName(doc, name) {
86
- return findParticipantName(rosterNames(doc), name);
70
+ export function resolveRosterName(squareState, name) {
71
+ return findParticipantName(rosterNames(squareState), name);
87
72
  }
88
- export function hasQuorum(doc, name, outs) {
89
- const peers = rosterNames(doc).filter((participant) => !sameName(participant, name));
73
+ export function hasQuorum(squareState, name, outs) {
74
+ const peers = rosterNames(squareState).filter((participant) => !sameName(participant, name));
90
75
  return peers.length > 0 && peers.every((peer) => outs.has(nameKey(peer)));
91
76
  }
92
77
  export function countSays(acts, name) {
@@ -106,6 +91,14 @@ export function sayNumberFor(acts, target) {
106
91
  }
107
92
  throw new Error('target say act is not present in act history');
108
93
  }
94
+ /** Shared terminal condition for a participant's watch lifecycle. */
95
+ export function watchTerminalStatus(squareState, name) {
96
+ if (squareState.hardCap !== null && countSays(squareState.acts, name) >= squareState.hardCap)
97
+ return 'capped';
98
+ const done = doneNames(squareState.acts);
99
+ done.delete(nameKey(name));
100
+ return hasQuorum(squareState, name, done) ? 'quorum' : undefined;
101
+ }
109
102
  export function doneNames(acts) {
110
103
  return new Set(fold(acts).done.map((participant) => nameKey(participant)));
111
104
  }
@@ -131,13 +124,13 @@ export function actStableIndex(act) {
131
124
  }
132
125
  export function actId(actOrIndex) {
133
126
  const index = typeof actOrIndex === 'number' ? actOrIndex : actStableIndex(actOrIndex);
134
- return `act_${index}`;
127
+ return formatActivityId(index);
135
128
  }
136
- export function getReadState(doc, name) {
137
- return doc.runtime.cursors[name] ?? Object.entries(doc.runtime.cursors).find(([participant]) => sameName(participant, name))?.[1];
129
+ export function getReadState(squareState, name) {
130
+ return squareState.runtime.cursors[name] ?? Object.entries(squareState.runtime.cursors).find(([participant]) => sameName(participant, name))?.[1];
138
131
  }
139
- export function readCursor(doc, name) {
140
- return getReadState(doc, name)?.consumedThroughIndex ?? -1;
132
+ export function readCursor(squareState, name) {
133
+ return getReadState(squareState, name)?.consumedThroughIndex ?? -1;
141
134
  }
142
135
  export function currentHold(acts) {
143
136
  const hold = fold(acts).hold;
@@ -146,48 +139,48 @@ export function currentHold(acts) {
146
139
  export function publicActs(acts) {
147
140
  return acts.filter((act) => act.kind === 'say' || act.kind === 'done');
148
141
  }
149
- function canonicalRuntimeName(doc, name) {
150
- return resolveRosterName(doc, name) ?? name;
142
+ function canonicalRuntimeName(squareState, name) {
143
+ return resolveRosterName(squareState, name) ?? name;
151
144
  }
152
- export function advanceCursor(doc, name, index, updatedAt = Date.now()) {
153
- return touchPresenceCursor(doc, name, updatedAt, index);
145
+ export function advanceCursor(squareState, name, index, updatedAt = Date.now()) {
146
+ return touchPresenceCursor(squareState, name, updatedAt, index);
154
147
  }
155
- export function touchPresenceCursor(doc, name, at, consumedThroughIndex) {
148
+ export function touchPresenceCursor(squareState, name, at, consumedThroughIndex) {
156
149
  if (!Number.isFinite(at))
157
150
  return false;
158
151
  if (consumedThroughIndex !== undefined && (!Number.isInteger(consumedThroughIndex) || consumedThroughIndex < 0))
159
152
  return false;
160
- const key = canonicalRuntimeName(doc, name);
161
- const current = getReadState(doc, key);
153
+ const key = canonicalRuntimeName(squareState, name);
154
+ const current = getReadState(squareState, key);
162
155
  const nextIndex = consumedThroughIndex === undefined
163
- ? (current?.consumedThroughIndex ?? readCursor(doc, key))
156
+ ? (current?.consumedThroughIndex ?? readCursor(squareState, key))
164
157
  : Math.max(current?.consumedThroughIndex ?? -1, consumedThroughIndex);
165
158
  const updatedAt = current === undefined ? at : Math.max(current.updatedAt, at);
166
159
  if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt)
167
160
  return false;
168
- doc.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt };
161
+ squareState.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt };
169
162
  return true;
170
163
  }
171
164
  export function latestActIndex(acts) {
172
165
  return acts.reduce((max, act) => Math.max(max, act.index), -1);
173
166
  }
174
- export function freshWatchLease(doc, name, at = Date.now()) {
175
- const key = canonicalRuntimeName(doc, name);
176
- const lease = watchLease(doc, key);
167
+ export function freshWatchLease(squareState, name, at = Date.now()) {
168
+ const key = canonicalRuntimeName(squareState, name);
169
+ const lease = watchLease(squareState, key);
177
170
  if (lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS)
178
171
  return undefined;
179
172
  return lease;
180
173
  }
181
- export function watchLease(doc, name) {
182
- return doc.runtime.leases[canonicalRuntimeName(doc, name)];
174
+ export function watchLease(squareState, name) {
175
+ return squareState.runtime.leases[canonicalRuntimeName(squareState, name)];
183
176
  }
184
- export function writeWatchLease(doc, name, lease) {
185
- doc.runtime.leases[canonicalRuntimeName(doc, name)] = lease;
177
+ export function writeWatchLease(squareState, name, lease) {
178
+ squareState.runtime.leases[canonicalRuntimeName(squareState, name)] = lease;
186
179
  }
187
- export function removeWatchLease(doc, name, leaseId) {
188
- const key = canonicalRuntimeName(doc, name);
189
- if (doc.runtime.leases[key]?.leaseId !== leaseId)
180
+ export function removeWatchLease(squareState, name, leaseId) {
181
+ const key = canonicalRuntimeName(squareState, name);
182
+ if (squareState.runtime.leases[key]?.leaseId !== leaseId)
190
183
  return false;
191
- delete doc.runtime.leases[key];
184
+ delete squareState.runtime.leases[key];
192
185
  return true;
193
186
  }