@astrosheep/square 0.3.12 → 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 (121) 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 +111 -90
  7. package/dist/artifact.d.ts +20 -0
  8. package/dist/artifact.js +10 -29
  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 +5 -4
  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 +25 -30
  17. package/dist/cli/harness-command.d.ts +28 -0
  18. package/dist/cli/harness-command.js +29 -14
  19. package/dist/cli/maintenance-commands.d.ts +7 -0
  20. package/dist/cli/maintenance-commands.js +3 -3
  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 +155 -128
  24. package/dist/cli/program.d.ts +2 -0
  25. package/dist/cli/program.js +2 -12
  26. package/dist/cli/registry.d.ts +9 -0
  27. package/dist/cli/registry.js +1 -2
  28. package/dist/cli/square-commands.d.ts +34 -0
  29. package/dist/cli/square-commands.js +81 -74
  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 +83 -77
  35. package/dist/delivery-health.d.ts +21 -0
  36. package/dist/delivery-health.js +40 -34
  37. package/dist/delivery.d.ts +81 -0
  38. package/dist/delivery.js +60 -22
  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 +6 -5
  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 -69
  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 +40 -18
  62. package/dist/model.d.ts +147 -0
  63. package/dist/model.js +12 -2
  64. package/dist/notifications.d.ts +30 -0
  65. package/dist/notifications.js +75 -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 +33 -29
  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 +42 -34
  84. package/dist/search.d.ts +11 -0
  85. package/dist/square-core.d.ts +111 -0
  86. package/dist/square-core.js +7 -0
  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-evidence.d.ts +12 -0
  105. package/dist/wake-evidence.js +7 -10
  106. package/dist/wake-port.d.ts +20 -0
  107. package/dist/wake-sink.d.ts +12 -0
  108. package/dist/wakes.d.ts +40 -0
  109. package/dist/wakes.js +62 -0
  110. package/dist/watch.d.ts +2 -0
  111. package/dist/watch.js +89 -210
  112. package/extensions/square-opencode.js +27 -1
  113. package/extensions/square-pi.js +13 -0
  114. package/package.json +5 -2
  115. package/skills/brainstorm/SKILL.md +26 -26
  116. package/skills/square/.claude-plugin/plugin.json +1 -1
  117. package/skills/square/SKILL.md +47 -29
  118. package/skills/square/hooks/hooks.json +9 -0
  119. package/skills/square-feedback/SKILL.md +2 -2
  120. package/dist/compact.js +0 -26
  121. package/dist/square-application.js +0 -193
@@ -1,6 +1,8 @@
1
1
  import { sameName } from './model.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { participantIdentity } from './participant-identity.js';
5
+ export { participantIdentity } from './participant-identity.js';
4
6
  import { audienceIncludes, audienceOf, perceive } from './square-core.js';
5
7
  import { actId, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
6
8
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
@@ -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) {
@@ -151,31 +154,31 @@ export function renderEventCli(event, opts = {}) {
151
154
  const body = renderedBody(event.body, maxBody);
152
155
  const mention = opts.mention;
153
156
  const mentionSuffix = mention !== undefined && audienceIncludes(audienceOf(event), mention)
154
- ? ` · calls your name across the square — @${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) {
170
+ function renderPresenceOnlySay(event, opts = {}) {
168
171
  const audience = audienceOf(event);
169
172
  const targets = audience.kind === 'bell' ? [] : audience.names;
170
- const dest = targets.length === 0 ? '' : ` to ${targets.map((name) => `@${name}`).join(' and ')}`;
171
- return `*${event.actor} walks over${dest}*`;
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}`;
172
175
  }
173
176
  export function renderAmbientEvent(event, viewer, opts = {}) {
174
177
  if (event.kind !== 'say')
175
178
  return renderEventCli(event, opts);
176
179
  const seen = perceive(event, viewer);
177
180
  if (seen === 'presence')
178
- return renderPresenceOnlySay(event);
181
+ return renderPresenceOnlySay(event, opts);
179
182
  return renderEventCli(event, opts);
180
183
  }
181
184
  function draftSavedLines(draftPath) {
@@ -190,10 +193,11 @@ function renderUnreadSummary(opts) {
190
193
  ...item.previews.slice(-1).map((preview) => {
191
194
  const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number });
192
195
  if (rendered === '')
193
- return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago`;
194
- if (rendered.startsWith('*'))
195
- return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered}`;
196
- 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)}"`;
197
201
  }),
198
202
  ]),
199
203
  ...opts.roomChanges.map((act) => ` · ${renderRoomChangeText(act)}`),
@@ -260,10 +264,10 @@ export function renderPublicTail(events, lastN, now, viewer = '') {
260
264
  .filter(Boolean)
261
265
  .join('\n\n');
262
266
  }
263
- function lastPresenceAnchor(doc, name) {
264
- const cursor = readCursor(doc, name);
265
- for (let i = doc.acts.length - 1; i >= 0; i--) {
266
- const event = doc.acts[i];
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];
267
271
  const index = event.index;
268
272
  if (index > cursor)
269
273
  continue;
@@ -273,15 +277,15 @@ function lastPresenceAnchor(doc, name) {
273
277
  return -1;
274
278
  }
275
279
  function renderLastPresenceMarker(name) {
276
- return ${name}'s footprints reach here`;
280
+ return `→ ${participantIdentity(name)} was here`;
277
281
  }
278
- export function renderActivitiesView(doc, visible, lastN, full, squarePath, viewer = '', mode = 'ambient') {
282
+ export function renderActivitiesView(squareState, visible, lastN, full, squarePath, viewer = '', mode = 'ambient') {
279
283
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
280
284
  const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
281
285
  const previewLen = full ? undefined : BODY_PREVIEW_LENGTH;
282
286
  const markers = new Map();
283
- for (const participant of rosterNames(doc)) {
284
- const anchor = lastPresenceAnchor(doc, participant);
287
+ for (const participant of rosterNames(squareState)) {
288
+ const anchor = lastPresenceAnchor(squareState, participant);
285
289
  if (anchor >= 0)
286
290
  markers.set(anchor, [...(markers.get(anchor) ?? []), participant]);
287
291
  }
@@ -289,7 +293,7 @@ export function renderActivitiesView(doc, visible, lastN, full, squarePath, view
289
293
  for (const act of shown) {
290
294
  const opts = {
291
295
  preview: previewLen,
292
- actNumber: act.kind === 'say' ? sayNumberFor(doc.acts, act) : undefined,
296
+ actNumber: act.kind === 'say' ? sayNumberFor(squareState.acts, act) : undefined,
293
297
  };
294
298
  const rendered = mode === 'archive'
295
299
  ? renderEventCli(act, opts)
@@ -326,13 +330,13 @@ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath
326
330
  const rawBody = act.body ?? '';
327
331
  if (full === true) {
328
332
  const body = rawBody.split('\n').map((line) => ` ${line}`).join('\n');
329
- 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}`);
330
334
  continue;
331
335
  }
332
336
  const snippet = grepSnippet(rawBody, pattern, GREP_PREVIEW_CHARS, fixed);
333
337
  if (snippet === undefined) {
334
338
  const preview = previewBody(rawBody, GREP_PREVIEW_CHARS);
335
- 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}`}`);
336
340
  continue;
337
341
  }
338
342
  const clippedBefore = snippet.beforeOmitted > 0;
@@ -342,7 +346,7 @@ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath
342
346
  const omitted = clippedBefore || clippedAfter
343
347
  ? `\n · ${snippet.beforeOmitted} chars before · ${snippet.afterOmitted} chars after`
344
348
  : '';
345
- 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}`);
346
350
  }
347
351
  if (publicVisible.length === 1) {
348
352
  chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --full' : ''}`);
@@ -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
@@ -58,20 +58,20 @@ export function matchesMentionTarget(act, mention) {
58
58
  return audience.kind === 'bell' || audience.names.length > 0;
59
59
  return audienceIncludes(audience, mention);
60
60
  }
61
- export function foldedState(doc) {
62
- return fold(doc.acts);
61
+ export function foldedState(squareState) {
62
+ return fold(squareState.acts);
63
63
  }
64
- export function rosterNames(doc) {
65
- return foldedState(doc).participants.map((participant) => participant.name);
64
+ export function rosterNames(squareState) {
65
+ return foldedState(squareState).participants.map((participant) => participant.name);
66
66
  }
67
- export function inSquareCount(doc) {
68
- return foldedState(doc).joined.length;
67
+ export function inSquareCount(squareState) {
68
+ return foldedState(squareState).joined.length;
69
69
  }
70
- export function resolveRosterName(doc, name) {
71
- return findParticipantName(rosterNames(doc), name);
70
+ export function resolveRosterName(squareState, name) {
71
+ return findParticipantName(rosterNames(squareState), name);
72
72
  }
73
- export function hasQuorum(doc, name, outs) {
74
- 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));
75
75
  return peers.length > 0 && peers.every((peer) => outs.has(nameKey(peer)));
76
76
  }
77
77
  export function countSays(acts, name) {
@@ -91,6 +91,14 @@ export function sayNumberFor(acts, target) {
91
91
  }
92
92
  throw new Error('target say act is not present in act history');
93
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
+ }
94
102
  export function doneNames(acts) {
95
103
  return new Set(fold(acts).done.map((participant) => nameKey(participant)));
96
104
  }
@@ -118,11 +126,11 @@ export function actId(actOrIndex) {
118
126
  const index = typeof actOrIndex === 'number' ? actOrIndex : actStableIndex(actOrIndex);
119
127
  return formatActivityId(index);
120
128
  }
121
- export function getReadState(doc, name) {
122
- 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];
123
131
  }
124
- export function readCursor(doc, name) {
125
- return getReadState(doc, name)?.consumedThroughIndex ?? -1;
132
+ export function readCursor(squareState, name) {
133
+ return getReadState(squareState, name)?.consumedThroughIndex ?? -1;
126
134
  }
127
135
  export function currentHold(acts) {
128
136
  const hold = fold(acts).hold;
@@ -131,48 +139,48 @@ export function currentHold(acts) {
131
139
  export function publicActs(acts) {
132
140
  return acts.filter((act) => act.kind === 'say' || act.kind === 'done');
133
141
  }
134
- function canonicalRuntimeName(doc, name) {
135
- return resolveRosterName(doc, name) ?? name;
142
+ function canonicalRuntimeName(squareState, name) {
143
+ return resolveRosterName(squareState, name) ?? name;
136
144
  }
137
- export function advanceCursor(doc, name, index, updatedAt = Date.now()) {
138
- return touchPresenceCursor(doc, name, updatedAt, index);
145
+ export function advanceCursor(squareState, name, index, updatedAt = Date.now()) {
146
+ return touchPresenceCursor(squareState, name, updatedAt, index);
139
147
  }
140
- export function touchPresenceCursor(doc, name, at, consumedThroughIndex) {
148
+ export function touchPresenceCursor(squareState, name, at, consumedThroughIndex) {
141
149
  if (!Number.isFinite(at))
142
150
  return false;
143
151
  if (consumedThroughIndex !== undefined && (!Number.isInteger(consumedThroughIndex) || consumedThroughIndex < 0))
144
152
  return false;
145
- const key = canonicalRuntimeName(doc, name);
146
- const current = getReadState(doc, key);
153
+ const key = canonicalRuntimeName(squareState, name);
154
+ const current = getReadState(squareState, key);
147
155
  const nextIndex = consumedThroughIndex === undefined
148
- ? (current?.consumedThroughIndex ?? readCursor(doc, key))
156
+ ? (current?.consumedThroughIndex ?? readCursor(squareState, key))
149
157
  : Math.max(current?.consumedThroughIndex ?? -1, consumedThroughIndex);
150
158
  const updatedAt = current === undefined ? at : Math.max(current.updatedAt, at);
151
159
  if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt)
152
160
  return false;
153
- doc.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt };
161
+ squareState.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt };
154
162
  return true;
155
163
  }
156
164
  export function latestActIndex(acts) {
157
165
  return acts.reduce((max, act) => Math.max(max, act.index), -1);
158
166
  }
159
- export function freshWatchLease(doc, name, at = Date.now()) {
160
- const key = canonicalRuntimeName(doc, name);
161
- 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);
162
170
  if (lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS)
163
171
  return undefined;
164
172
  return lease;
165
173
  }
166
- export function watchLease(doc, name) {
167
- return doc.runtime.leases[canonicalRuntimeName(doc, name)];
174
+ export function watchLease(squareState, name) {
175
+ return squareState.runtime.leases[canonicalRuntimeName(squareState, name)];
168
176
  }
169
- export function writeWatchLease(doc, name, lease) {
170
- doc.runtime.leases[canonicalRuntimeName(doc, name)] = lease;
177
+ export function writeWatchLease(squareState, name, lease) {
178
+ squareState.runtime.leases[canonicalRuntimeName(squareState, name)] = lease;
171
179
  }
172
- export function removeWatchLease(doc, name, leaseId) {
173
- const key = canonicalRuntimeName(doc, name);
174
- 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)
175
183
  return false;
176
- delete doc.runtime.leases[key];
184
+ delete squareState.runtime.leases[key];
177
185
  return true;
178
186
  }
@@ -0,0 +1,11 @@
1
+ export declare function compileGrepPattern(pattern: string): RegExp;
2
+ export declare function compileFixedPattern(pattern: string): RegExp;
3
+ export declare function compileSearchPattern(pattern: string, fixed: boolean): RegExp;
4
+ export interface GrepSnippet {
5
+ before: string;
6
+ match: string;
7
+ after: string;
8
+ beforeOmitted: number;
9
+ afterOmitted: number;
10
+ }
11
+ export declare function grepSnippet(body: string, pattern: string, maxChars: number, fixed?: boolean): GrepSnippet | undefined;
@@ -0,0 +1,111 @@
1
+ export type Participant = string;
2
+ export type Reach = 'bell';
3
+ export type Audience = {
4
+ readonly kind: 'bell';
5
+ } | {
6
+ readonly kind: 'mentions';
7
+ readonly names: readonly string[];
8
+ };
9
+ export type ActivityId = `act/${number}`;
10
+ export declare function formatActivityId(index: number): ActivityId;
11
+ export declare function parseActivityId(value: string): number | undefined;
12
+ export interface Hold {
13
+ active: boolean;
14
+ at?: number;
15
+ reason?: string;
16
+ actor?: Participant;
17
+ }
18
+ export type Act = {
19
+ kind: 'join';
20
+ actor: Participant;
21
+ at?: number;
22
+ } | {
23
+ kind: 'done';
24
+ actor: Participant;
25
+ at?: number;
26
+ body?: string;
27
+ } | {
28
+ kind: 'say';
29
+ actor: Participant;
30
+ at?: number;
31
+ body: string;
32
+ reach?: Reach;
33
+ reply?: number;
34
+ } | {
35
+ kind: 'hold';
36
+ actor?: Participant;
37
+ at?: number;
38
+ body?: string;
39
+ } | {
40
+ kind: 'resume';
41
+ actor?: Participant;
42
+ at?: number;
43
+ } | {
44
+ kind: 'read';
45
+ actor: Participant;
46
+ at?: number;
47
+ through: number;
48
+ };
49
+ export interface SquareValidationOptions {
50
+ hardCap?: number | null;
51
+ throttlePerMinute?: number;
52
+ throttleWindowMs?: number;
53
+ }
54
+ export type Perception = 'full' | 'presence';
55
+ interface ParticipantSnapshot {
56
+ name: Participant;
57
+ key: string;
58
+ joined: boolean;
59
+ done: boolean;
60
+ activityCount: number;
61
+ lastActiveAt?: number;
62
+ lastReadThrough: number;
63
+ }
64
+ export interface FoldedSquareState {
65
+ participants: ParticipantSnapshot[];
66
+ hold: Hold;
67
+ joined: Participant[];
68
+ done: Participant[];
69
+ throttleActivityAts: number[];
70
+ bellSayAtsByActor: Map<string, number[]>;
71
+ }
72
+ export type ValidationResult = {
73
+ ok: true;
74
+ } | {
75
+ ok: false;
76
+ reason: 'already_joined';
77
+ } | {
78
+ ok: false;
79
+ reason: 'not_joined';
80
+ } | {
81
+ ok: false;
82
+ reason: 'done';
83
+ } | {
84
+ ok: false;
85
+ reason: 'held';
86
+ hold: Hold;
87
+ } | {
88
+ ok: false;
89
+ reason: 'hard_cap';
90
+ count: number;
91
+ hardCap: number;
92
+ } | {
93
+ ok: false;
94
+ reason: 'throttled';
95
+ delayMs: number;
96
+ } | {
97
+ ok: false;
98
+ reason: 'bell_quota';
99
+ nextAt: number;
100
+ };
101
+ export declare function extractMentions(body: string): string[];
102
+ export declare function audienceOf(say: {
103
+ body: string;
104
+ reach?: Reach;
105
+ }): Audience;
106
+ export declare function audienceIncludes(audience: Audience, name: string): boolean;
107
+ export declare function resolveAudience(audience: Audience, candidateNames: readonly string[]): string[];
108
+ export declare function fold(acts: readonly Act[]): FoldedSquareState;
109
+ export declare function validate(state: FoldedSquareState, act: Act, options?: SquareValidationOptions): ValidationResult;
110
+ export declare function perceive(act: Act, viewer: Participant | string): Perception;
111
+ export {};