@astrosheep/square 0.3.5 → 0.3.7

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 (57) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/codex-plugin/hooks/hooks.json +3 -14
  3. package/dist/activity-feed.js +26 -18
  4. package/dist/activity.js +10 -10
  5. package/dist/artifact.js +138 -203
  6. package/dist/boundary-presentation.js +77 -0
  7. package/dist/claude-hook.js +4 -94
  8. package/dist/cli/context.js +7 -7
  9. package/dist/cli/maintenance-commands.js +10 -26
  10. package/dist/cli/meta-commands.js +3 -6
  11. package/dist/cli/observation-commands.js +48 -53
  12. package/dist/cli/program.js +4 -4
  13. package/dist/cli/registry.js +5 -5
  14. package/dist/cli/square-commands.js +27 -20
  15. package/dist/cmd/notify-once.js +23 -21
  16. package/dist/codex-hook.js +22 -0
  17. package/dist/compact.js +1 -1
  18. package/dist/decisions.js +61 -88
  19. package/dist/delivery-health.js +104 -210
  20. package/dist/delivery.js +68 -18
  21. package/dist/doctor.js +9 -8
  22. package/dist/harness-claude.js +38 -245
  23. package/dist/harness-codex.js +82 -616
  24. package/dist/harness-stage.js +36 -0
  25. package/dist/harness.js +3 -5
  26. package/dist/help.js +43 -35
  27. package/dist/inbox.js +12 -11
  28. package/dist/index.js +10 -121
  29. package/dist/list.js +1 -1
  30. package/dist/model.js +0 -6
  31. package/dist/notification-failures.js +54 -0
  32. package/dist/notifications.js +47 -62
  33. package/dist/paseo-delivery.js +160 -0
  34. package/dist/paseo-state.js +31 -0
  35. package/dist/paseo-timeline.js +58 -188
  36. package/dist/presentation.js +57 -64
  37. package/dist/presented.js +9 -8
  38. package/dist/registry.js +55 -45
  39. package/dist/runtime.js +27 -84
  40. package/dist/square-application.js +135 -130
  41. package/dist/square-core.js +3 -11
  42. package/dist/stream.js +27 -126
  43. package/dist/wake-sink.js +3 -214
  44. package/dist/watch.js +65 -122
  45. package/extensions/square-opencode.js +8 -73
  46. package/extensions/square-pi.js +8 -132
  47. package/guides/architect.md +3 -3
  48. package/guides/participant.md +25 -16
  49. package/package.json +2 -2
  50. package/skills/brainstorm/SKILL.md +25 -32
  51. package/skills/square/.claude-plugin/plugin.json +1 -1
  52. package/skills/square/SKILL.md +39 -107
  53. package/skills/square/hooks/hooks.json +2 -13
  54. package/skills/square-feedback/SKILL.md +4 -4
  55. package/dist/harness-lifecycle.js +0 -102
  56. package/dist/square-store.js +0 -111
  57. package/dist/terminal.js +0 -125
@@ -83,16 +83,16 @@ export function renderPresenceLines(participants, now, limit = 5) {
83
83
  lines.push(` ○ …and ${remaining} more`);
84
84
  return lines;
85
85
  }
86
- const ACT_HINTS = [
86
+ const EXPRESS_HINTS = [
87
87
  '*asterisks* are your body — *slams table*, *sketches in the air*, *shrugs*',
88
- "you're standing in a square, not posting to a feed",
88
+ "you're standing in a square words and gestures both land",
89
89
  'half-shaped is welcome — a sketch, an objection, a joke, a fragment',
90
90
  '@ only who you need — step back with catch --mention',
91
91
  ];
92
- export function actHintLine(ownActCount) {
93
- if (ownActCount !== 1 && ownActCount % 5 !== 0)
92
+ export function expressHintLine(ownActivityCount) {
93
+ if (ownActivityCount !== 1 && ownActivityCount % 5 !== 0)
94
94
  return undefined;
95
- const hint = ACT_HINTS[Math.floor(ownActCount / 5) % ACT_HINTS.length];
95
+ const hint = EXPRESS_HINTS[Math.floor(ownActivityCount / 5) % EXPRESS_HINTS.length];
96
96
  return `· ${hint}`;
97
97
  }
98
98
  const BODY_PREVIEW_LENGTH = 200;
@@ -153,7 +153,8 @@ export function renderEventCli(event, opts = {}) {
153
153
  const mentionSuffix = mention !== undefined && extractMentions(event.body).some((name) => sameName(name, mention))
154
154
  ? ` · calls your name across the square — @${mention}`
155
155
  : '';
156
- return `● ${event.actor} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${bodySuffix(body)}`;
156
+ 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)}`;
157
158
  }
158
159
  case 'done': {
159
160
  const body = renderedBody(event.body, maxBody);
@@ -201,26 +202,21 @@ function renderUnreadSummary(opts) {
201
202
  return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`;
202
203
  }),
203
204
  ]),
204
- ...opts.roomChanges.map(({ act }) => ` · ${renderRoomChangeText(act)}`),
205
+ ...opts.roomChanges.map((act) => ` · ${renderRoomChangeText(act)}`),
205
206
  ];
206
207
  }
207
- export function renderPendingFeed(publicItems, roomChanges, viewer = '') {
208
+ export function renderPendingFeed(history, publicItems, roomChanges, viewer = '') {
208
209
  const lines = [];
209
- for (const item of publicItems) {
210
- if (item.act.kind === 'say' && item.number !== undefined && item.act.at) {
211
- const rendered = renderVisibleEvent(publicItems
212
- .filter((entry) => (entry.act.kind === 'say' || entry.act.kind === 'done') && entry.act.at !== undefined)
213
- .map((entry, index) => ({ ...entry.act, index })), { kind: 'say', actor: item.act.actor, at: item.act.at, body: item.act.body ?? '', index: item.index }, viewer, { actNumber: item.number });
214
- if (rendered !== '')
215
- lines.push(rendered);
216
- }
217
- else if (item.act.kind === 'done' && item.act.at) {
218
- lines.push(renderEventCli({ kind: 'done', actor: item.act.actor, at: item.act.at, body: item.act.body ?? '', index: item.index }));
219
- }
210
+ for (const act of publicItems) {
211
+ const rendered = renderVisibleEvent(history, act, viewer, {
212
+ actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
213
+ });
214
+ if (rendered !== '')
215
+ lines.push(rendered);
220
216
  }
221
217
  const publicIndexes = new Set(publicItems.map((item) => item.index));
222
- for (const { act, index } of roomChanges) {
223
- if (publicIndexes.has(index))
218
+ for (const act of roomChanges) {
219
+ if (publicIndexes.has(act.index))
224
220
  continue;
225
221
  lines.push(`· ${renderRoomChangeText(act)}`);
226
222
  }
@@ -229,32 +225,23 @@ export function renderPendingFeed(publicItems, roomChanges, viewer = '') {
229
225
  export function renderActivityBlocked(opts) {
230
226
  const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch --now`;
231
227
  return withPathOutput(opts.squarePath, [
232
- "✕ your act doesn't land — the square moved behind your back",
228
+ "✕ your activity doesn't land — the square moved behind your back",
233
229
  ...renderUnreadSummary({ activitySummaries: opts.activitySummaries, roomChanges: opts.unreadRoomChanges, viewer: opts.name }),
234
230
  ...draftSavedLines(opts.draftPath),
235
231
  `» ${readNowCommand}`,
236
- ' read, then act again',
232
+ ' take it in, then express again',
237
233
  `» ${withDraftInput(opts.forceCommand, opts.draftPath)}`,
238
- ' only if you truly mean to speak over them',
234
+ ' only if you truly mean to express over unread activity',
239
235
  ].join('\n'), { participantCount: opts.participantCount, held: opts.held });
240
236
  }
241
- export function withJoinNextOutput(squarePath, body, opts = {}) {
242
- return withPathOutput(squarePath, body.trimEnd(), opts);
243
- }
244
- export function withActivityNextOutput(squarePath, body = '', opts = {}) {
245
- return withPathOutput(squarePath, body.trimEnd(), opts);
246
- }
247
- export function withWatchNextOutput(squarePath, body, opts = {}) {
248
- return withPathOutput(squarePath, body.trimEnd(), opts);
249
- }
250
- export function renderActWaiting(opts) {
237
+ export function renderExpressWaiting(opts) {
251
238
  if (opts.reason === 'throttled') {
252
- return ['✕ the square is packed', ` · your act is waiting · next opening in ${formatDuration(opts.delayMs)}`].join('\n');
239
+ return ['✕ the square is packed', ` · your activity is waiting · next opening in ${formatDuration(opts.delayMs)}`].join('\n');
253
240
  }
254
- return ["✕ your act doesn't land — a hand is raised", ' · your act is waiting'].join('\n');
241
+ return ["✕ your activity doesn't land — a hand is raised", ' · your activity is waiting'].join('\n');
255
242
  }
256
- export function renderActNoWait(opts) {
257
- const retryCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} act -`;
243
+ export function renderExpressNoWait(opts) {
244
+ const retryCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} express -`;
258
245
  const lines = opts.reason === 'throttled'
259
246
  ? [
260
247
  '✕ the square is packed',
@@ -263,7 +250,7 @@ export function renderActNoWait(opts) {
263
250
  `» ${withDraftInput(retryCommand, opts.draftPath)}`,
264
251
  ]
265
252
  : [
266
- "✕ your act doesn't land — a hand is raised",
253
+ "✕ your activity doesn't land — a hand is raised",
267
254
  ` · ${opts.holdReason ?? 'the square holds its breath'}`,
268
255
  ...draftSavedLines(opts.draftPath),
269
256
  `» ${withDraftInput(retryCommand, opts.draftPath)}`,
@@ -295,7 +282,7 @@ function renderLastPresenceMarker(name) {
295
282
  return `· ${name}'s footprints reach here`;
296
283
  }
297
284
  export function renderActivitiesView(doc, visible, lastN, full, squarePath, viewer = '') {
298
- const publicVisible = visible.filter((item) => item.act.kind === 'say' || item.act.kind === 'done');
285
+ const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
299
286
  const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
300
287
  const previewLen = full ? undefined : BODY_PREVIEW_LENGTH;
301
288
  const markers = new Map();
@@ -305,23 +292,23 @@ export function renderActivitiesView(doc, visible, lastN, full, squarePath, view
305
292
  markers.set(anchor, [...(markers.get(anchor) ?? []), participant]);
306
293
  }
307
294
  const chunks = [];
308
- for (const item of shown) {
309
- const rendered = renderVisibleEvent(doc.acts, item.act, viewer, {
295
+ for (const act of shown) {
296
+ const rendered = renderVisibleEvent(doc.acts, act, viewer, {
310
297
  preview: previewLen,
311
- actNumber: item.act.kind === 'say' ? sayNumberFor(doc.acts, item.act) : undefined,
298
+ actNumber: act.kind === 'say' ? sayNumberFor(doc.acts, act) : undefined,
312
299
  });
313
300
  if (rendered !== '')
314
301
  chunks.push(rendered);
315
- for (const participant of markers.get(item.index) ?? []) {
302
+ for (const participant of markers.get(act.index) ?? []) {
316
303
  chunks.push(renderLastPresenceMarker(participant));
317
304
  }
318
305
  }
319
306
  if (chunks.length === 0)
320
307
  return 'latest\n ○ no public activity in this view';
321
308
  if (previewLen !== undefined) {
322
- const truncated = shown.some((item) => item.act.kind === 'say' && item.act.body.length > previewLen);
309
+ const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen);
323
310
  if (truncated)
324
- chunks.push(`» ${commandPrefix(squarePath)} echo --full`);
311
+ chunks.push(`» ${commandPrefix(squarePath)} history --full`);
325
312
  }
326
313
  return chunks.join('\n\n');
327
314
  }
@@ -332,22 +319,25 @@ function highlightGrepMatch(text) {
332
319
  return `\x1b[38;5;222m\x1b[1m${text}\x1b[0m`;
333
320
  }
334
321
  export function renderGrepActivitiesView(visible, totalMatches, full, squarePath, pattern, fixed = false) {
335
- const publicVisible = visible.filter((item) => item.act.kind === 'say' || item.act.kind === 'done');
322
+ const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
336
323
  if (totalMatches === 0)
337
324
  return `○ no activity matched ${quoteShell(pattern)}`;
338
325
  const matchLabel = totalMatches === 1 ? 'match' : 'matches';
339
326
  const chunks = [publicVisible.length === totalMatches ? `${totalMatches} ${matchLabel}` : `${publicVisible.length} of ${totalMatches} ${matchLabel}`];
340
327
  let truncated = false;
341
- for (const item of publicVisible) {
342
- const rawBody = item.act.body ?? '';
328
+ for (const act of publicVisible) {
329
+ const rawBody = act.body ?? '';
343
330
  if (full === true) {
344
331
  const body = rawBody.split('\n').map((line) => ` ${line}`).join('\n');
345
- chunks.push(`${actId(item.index)} · ${item.act.actor ?? 'unknown'} · ${formatTimestamp(item.act.at)}\n${body}`);
332
+ chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}\n${body}`);
346
333
  continue;
347
334
  }
348
335
  const snippet = grepSnippet(rawBody, pattern, GREP_PREVIEW_CHARS, fixed);
349
- if (snippet === undefined)
336
+ if (snippet === undefined) {
337
+ const preview = previewBody(rawBody, GREP_PREVIEW_CHARS);
338
+ chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}${preview === '' ? '' : `\n ${preview}`}`);
350
339
  continue;
340
+ }
351
341
  const clippedBefore = snippet.beforeOmitted > 0;
352
342
  const clippedAfter = snippet.afterOmitted > 0;
353
343
  truncated ||= clippedBefore || clippedAfter;
@@ -355,13 +345,13 @@ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath
355
345
  const omitted = clippedBefore || clippedAfter
356
346
  ? `\n · ${snippet.beforeOmitted} chars before · ${snippet.afterOmitted} chars after`
357
347
  : '';
358
- chunks.push(`${actId(item.index)} · ${item.act.actor ?? 'unknown'} · ${formatTimestamp(item.act.at)}\n ${text.trim()}${omitted}`);
348
+ chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}\n ${text.trim()}${omitted}`);
359
349
  }
360
350
  if (publicVisible.length === 1) {
361
- chunks.push(`» ${commandPrefix(squarePath)} echo --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --full' : ''}`);
351
+ chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --full' : ''}`);
362
352
  }
363
353
  else if (truncated && publicVisible.length > 1) {
364
- chunks.push(`» ${commandPrefix(squarePath)} echo --at ${actId(publicVisible[0].index)} -C 2 --full`);
354
+ chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2 --full`);
365
355
  }
366
356
  return chunks.join('\n\n');
367
357
  }
@@ -369,7 +359,7 @@ function renderActivityLimitBody(opts) {
369
359
  const countText = opts.count !== undefined && opts.hardCap !== undefined ? ` (${opts.count}/${opts.hardCap})` : '';
370
360
  const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} done -`;
371
361
  return [
372
- `✕ your act doesn't land — the cap is reached${countText}`,
362
+ `✕ your activity doesn't land — the cap is reached${countText}`,
373
363
  ...draftSavedLines(opts.draftPath),
374
364
  `» ${withDraftInput(doneCommand, opts.draftPath)}`,
375
365
  ].join('\n');
@@ -377,11 +367,8 @@ function renderActivityLimitBody(opts) {
377
367
  export function renderActivityLimit(opts) {
378
368
  return withPathOutput(opts.squarePath, renderActivityLimitBody(opts), { participantCount: opts.participantCount, held: opts.held });
379
369
  }
380
- export function renderWatchInterrupted(_opts) {
381
- return '✕ catch stopped';
382
- }
383
370
  export function renderWatchAlreadyActive(opts) {
384
- return ['✕ you are already catching', `» ${participantCommandPrefix(opts.squarePath, opts.name)} catch --force`].join('\n');
371
+ return ['✕ you are already catching', `» ${participantCommandPrefix(opts.squarePath, opts.name)} catch --idle 30m --replace`].join('\n');
385
372
  }
386
373
  export function renderWatchForceTakeover(_opts) {
387
374
  return '✓ your new catch takes over';
@@ -400,8 +387,11 @@ export function renderWatchStatus(opts) {
400
387
  case 'stale':
401
388
  case 'empty-now': {
402
389
  const prefix = participantCommandPrefix(opts.squarePath, opts.name);
390
+ const quiet = opts.status === 'stale' && opts.idleMs !== undefined
391
+ ? `○ ${formatDuration(opts.idleMs)} of quiet — nothing new for you`
392
+ : '○ only footsteps in the square — nothing new for you';
403
393
  return [
404
- '○ only footsteps in the square — nothing new for you',
394
+ quiet,
405
395
  ...(opts.showCatchHint === false
406
396
  ? []
407
397
  : [`» ${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
@@ -417,7 +407,7 @@ export function renderWatchStatus(opts) {
417
407
  function renderRoomChanges(changes) {
418
408
  if (changes.length === 0)
419
409
  return '';
420
- return ['▲ while your back was turned', ...changes.map(({ act }) => ` · ${renderRoomChangeText(act)}`)].join('\n');
410
+ return ['▲ while your back was turned', ...changes.map((act) => ` · ${renderRoomChangeText(act)}`)].join('\n');
421
411
  }
422
412
  export function renderDoctorClean() {
423
413
  return '✓ no problems found';
@@ -441,8 +431,11 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
441
431
  const sections = [];
442
432
  if (opts.stalePartial) {
443
433
  const prefix = participantCommandPrefix(opts.squarePath, opts.viewer);
434
+ const quiet = opts.idleMs === undefined
435
+ ? '○ only footsteps in the square — nothing new for you'
436
+ : `○ ${formatDuration(opts.idleMs)} of quiet — nothing else for you`;
444
437
  sections.push([
445
- '○ only footsteps in the square — nothing new for you',
438
+ quiet,
446
439
  ...(opts.showCatchHint === false
447
440
  ? []
448
441
  : [`» ${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
@@ -455,8 +448,8 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
455
448
  sections.push(room);
456
449
  if (publicItems.length > 0) {
457
450
  const rendered = publicItems
458
- .map((item) => renderVisibleEvent(history, item.act, opts.viewer, {
459
- actNumber: item.act.kind === 'say' ? sayNumberFor(history, item.act) : undefined,
451
+ .map((act) => renderVisibleEvent(history, act, opts.viewer, {
452
+ actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
460
453
  mention: opts.mention,
461
454
  }))
462
455
  .filter(Boolean)
package/dist/presented.js CHANGED
@@ -35,7 +35,6 @@ function readRows(filePath, now = Date.now()) {
35
35
  if (parsed.v !== 2 ||
36
36
  typeof parsed.ts !== 'number' ||
37
37
  typeof parsed.owner_id !== 'string' ||
38
- typeof parsed.presenter_session_id !== 'string' ||
39
38
  typeof parsed.square_path !== 'string' ||
40
39
  typeof parsed.name !== 'string' ||
41
40
  typeof parsed.act_index !== 'number' ||
@@ -155,17 +154,20 @@ function selectUnpresented(sessionId, inbox, rows) {
155
154
  : [{ membership: { ...membership, notifications }, ownerId }];
156
155
  });
157
156
  }
158
- /** True when the current participant owner has already received this attention. */
159
- export function hasPresentedAttention(squarePath, name, actIndex, env = process.env) {
160
- const ownerIds = new Set(lookupParticipant(squarePath, name).map((binding) => binding.ownerId));
161
- if (ownerIds.size === 0)
162
- return false;
157
+ export function hasPresentedForOwner(ownerId, squarePath, name, actIndex, env = process.env) {
163
158
  const resolved = canonicalSquarePath(squarePath);
164
- return readRows(presentedPath(env)).some((row) => ownerIds.has(row.owner_id) &&
159
+ return readRows(presentedPath(env)).some((row) => row.owner_id === ownerId &&
165
160
  canonicalSquarePath(row.square_path) === resolved &&
166
161
  sameName(row.name, name) &&
167
162
  row.act_index === actIndex);
168
163
  }
164
+ /** True when any current participant owner has already received this attention. */
165
+ export function hasPresentedAttention(squarePath, name, actIndex, env = process.env) {
166
+ const ownerIds = new Set(lookupParticipant(squarePath, name).map((binding) => binding.ownerId));
167
+ if (ownerIds.size === 0)
168
+ return false;
169
+ return [...ownerIds].some((ownerId) => hasPresentedForOwner(ownerId, squarePath, name, actIndex, env));
170
+ }
169
171
  /**
170
172
  * Serialize presentation only for the affected participants. Delivery runs
171
173
  * outside the short ledger-write lock, so unrelated owners never wait on an
@@ -192,7 +194,6 @@ export function presentOnce(sessionId, lookup, deliver, env = process.env, at =
192
194
  v: 2,
193
195
  ts: at,
194
196
  owner_id: ownerId,
195
- presenter_session_id: sessionId,
196
197
  square_path: canonicalSquarePath(membership.squarePath),
197
198
  name: membership.name,
198
199
  act_index: notification.actIndex,
package/dist/registry.js CHANGED
@@ -9,11 +9,19 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { randomUUID } from 'node:crypto';
12
+ import { loadSquare } from './artifact.js';
12
13
  import { nameKey, sameName } from './model.js';
14
+ import { isCurrentlyJoined } from './runtime.js';
13
15
  const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
14
16
  const COMPACT_BYTES = 64 * 1024;
15
17
  const COMPACT_LINES = 1000;
16
18
  const VALID_CHANNELS = new Set(['claude-code', 'codex', 'opencode', 'pi', 'paseo', 'unknown']);
19
+ const LOCAL_SESSION_SOURCES = [
20
+ { variable: 'CLAUDE_CODE_SESSION_ID', channel: 'claude-code', child: 'CLAUDE_CODE_CHILD_SESSION' },
21
+ { variable: 'CODEX_THREAD_ID', channel: 'codex' },
22
+ { variable: 'OPENCODE_SESSION_ID', channel: 'opencode' },
23
+ { variable: 'SQUARE_PI_SESSION_ID', channel: 'pi' },
24
+ ];
17
25
  export function registryPath() {
18
26
  if (process.env['SQUARE_REGISTRY'])
19
27
  return process.env['SQUARE_REGISTRY'];
@@ -113,9 +121,8 @@ function foldRegistry(raw, now) {
113
121
  }
114
122
  return active.sort((a, b) => b.updatedAt - a.updatedAt);
115
123
  }
116
- function compactRegistry(filePath, raw, now) {
117
- const active = foldRegistry(raw, now);
118
- const compacted = active
124
+ function writeRegistryBindings(filePath, bindings) {
125
+ const compacted = bindings
119
126
  .slice()
120
127
  .reverse()
121
128
  .map((binding) => JSON.stringify({
@@ -135,6 +142,9 @@ function compactRegistry(filePath, raw, now) {
135
142
  fs.writeFileSync(temporary, compacted === '' ? '' : `${compacted}\n`, { mode: 0o600 });
136
143
  fs.renameSync(temporary, filePath);
137
144
  }
145
+ function compactRegistry(filePath, raw, now) {
146
+ writeRegistryBindings(filePath, foldRegistry(raw, now));
147
+ }
138
148
  function maybeCompactRegistry(filePath, now) {
139
149
  let stat;
140
150
  try {
@@ -214,53 +224,53 @@ export function lookupParticipant(squarePath, name, now = Date.now()) {
214
224
  const canonicalPath = canonicalSquarePath(squarePath);
215
225
  return readActiveBindings(now).filter((binding) => binding.squarePath === canonicalPath && sameName(binding.name, name));
216
226
  }
217
- export function localSessionIdentities(env = process.env) {
218
- const paseoAgentId = env['PASEO_AGENT_ID']?.trim() || undefined;
219
- const identities = [];
220
- const claudeSessionId = env['CLAUDE_CODE_SESSION_ID']?.trim();
221
- if (claudeSessionId) {
222
- identities.push({
223
- sessionId: claudeSessionId,
224
- channel: 'claude-code',
225
- child: env['CLAUDE_CODE_CHILD_SESSION'] === '1',
226
- ...(paseoAgentId ? { paseoAgentId } : {}),
227
- });
227
+ /** Resolve the current local harness owner for a participant, if one is registered. */
228
+ export function localParticipantOwner(squarePath, name, env = process.env, now = Date.now()) {
229
+ const sessionIds = new Set(localSessionIdentities(env).map((identity) => identity.sessionId));
230
+ if (sessionIds.size === 0)
231
+ return undefined;
232
+ return lookupParticipant(squarePath, name, now).find((binding) => sessionIds.has(binding.sessionId))?.ownerId;
233
+ }
234
+ function bindingIsProvablyObsolete(binding) {
235
+ if (!fs.existsSync(binding.squarePath))
236
+ return true;
237
+ try {
238
+ return !isCurrentlyJoined(loadSquare(binding.squarePath).acts, binding.name);
228
239
  }
229
- const codexThreadId = env['CODEX_THREAD_ID']?.trim();
230
- if (codexThreadId && !identities.some((identity) => identity.sessionId === codexThreadId)) {
231
- identities.push({
232
- sessionId: codexThreadId,
233
- channel: 'codex',
234
- child: false,
235
- ...(paseoAgentId ? { paseoAgentId } : {}),
236
- });
240
+ catch {
241
+ // A temporarily unreadable artifact is uncertain, so preserve its binding.
242
+ return false;
237
243
  }
238
- const openCodeSessionId = env['OPENCODE_SESSION_ID']?.trim();
239
- if (openCodeSessionId && !identities.some((identity) => identity.sessionId === openCodeSessionId)) {
240
- identities.push({
241
- sessionId: openCodeSessionId,
242
- channel: 'opencode',
243
- child: false,
244
- ...(paseoAgentId ? { paseoAgentId } : {}),
245
- });
244
+ }
245
+ /** Compact the registry and remove only bindings disproved by their authoritative artifact. */
246
+ export function pruneRegistry(now = Date.now()) {
247
+ const filePath = registryPath();
248
+ let raw;
249
+ try {
250
+ raw = fs.readFileSync(filePath, 'utf8');
246
251
  }
247
- const piSessionId = env['SQUARE_PI_SESSION_ID']?.trim();
248
- if (piSessionId && !identities.some((identity) => identity.sessionId === piSessionId)) {
249
- identities.push({
250
- sessionId: piSessionId,
251
- channel: 'pi',
252
- child: false,
253
- ...(paseoAgentId ? { paseoAgentId } : {}),
254
- });
252
+ catch (error) {
253
+ if (error.code === 'ENOENT')
254
+ return { removed: 0, kept: 0 };
255
+ throw error;
255
256
  }
256
- if (paseoAgentId && !identities.some((identity) => identity.sessionId === paseoAgentId)) {
257
- identities.push({
258
- sessionId: paseoAgentId,
259
- channel: 'paseo',
260
- child: false,
261
- paseoAgentId,
262
- });
257
+ const active = foldRegistry(raw, now);
258
+ const kept = active.filter((binding) => !bindingIsProvablyObsolete(binding));
259
+ writeRegistryBindings(filePath, kept);
260
+ return { removed: active.length - kept.length, kept: kept.length };
261
+ }
262
+ function addLocalSession(identities, sessionId, channel, child, paseoAgentId) {
263
+ if (!sessionId || identities.some((identity) => identity.sessionId === sessionId))
264
+ return;
265
+ identities.push({ sessionId, channel, child, ...(paseoAgentId ? { paseoAgentId } : {}) });
266
+ }
267
+ export function localSessionIdentities(env = process.env) {
268
+ const paseoAgentId = env['PASEO_AGENT_ID']?.trim() || undefined;
269
+ const identities = [];
270
+ for (const source of LOCAL_SESSION_SOURCES) {
271
+ addLocalSession(identities, env[source.variable]?.trim(), source.channel, source.child !== undefined && env[source.child] === '1', paseoAgentId);
263
272
  }
273
+ addLocalSession(identities, paseoAgentId, 'paseo', false, paseoAgentId);
264
274
  return identities;
265
275
  }
266
276
  /** True when this process belongs to a harness that can deliver Square attention without a foreground catch. */
package/dist/runtime.js CHANGED
@@ -59,8 +59,13 @@ export function extractMentions(body) {
59
59
  matches.push(match[1]);
60
60
  return matches;
61
61
  }
62
- /** Pure mention filter for say acts. Broadcast bodies (no @) match any named viewer. */
62
+ /** Pure directed-activity filter. Broadcast bodies (no @) match any named viewer. */
63
63
  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
+ }
64
69
  const mentions = extractMentions(act.body);
65
70
  if (mention === true)
66
71
  return mentions.length > 0;
@@ -104,24 +109,12 @@ export function sayNumberFor(acts, target) {
104
109
  export function doneNames(acts) {
105
110
  return new Set(fold(acts).done.map((participant) => nameKey(participant)));
106
111
  }
107
- export function hasJoined(acts, name) {
108
- return fold(acts).participants.some((participant) => sameName(participant.name, name) && participant.joined);
109
- }
110
112
  export function joinedNames(acts) {
111
113
  return new Set(acts.filter((act) => act.kind === 'join').map((act) => nameKey(act.actor)));
112
114
  }
113
115
  export function isCurrentlyJoined(acts, name) {
114
116
  return fold(acts).participants.some((participant) => sameName(participant.name, name) && participant.joined);
115
117
  }
116
- /** Timestamp of the recipient's most recent join act, if any. */
117
- export function lastJoinAt(acts, name) {
118
- let last;
119
- for (const act of acts) {
120
- if (act.kind === 'join' && sameName(act.actor, name))
121
- last = act.at;
122
- }
123
- return last;
124
- }
125
118
  /** Stable index of the recipient's most recent join act, if any. */
126
119
  export function lastJoinIndex(acts, name) {
127
120
  let last;
@@ -131,11 +124,6 @@ export function lastJoinIndex(acts, name) {
131
124
  }
132
125
  return last;
133
126
  }
134
- /** Notifications are live only when they land after the recipient joined. */
135
- export function isPostJoinActivity(acts, name, actIndex) {
136
- const joinIndex = lastJoinIndex(acts, name);
137
- return joinIndex !== undefined && actIndex > joinIndex;
138
- }
139
127
  export function actStableIndex(act) {
140
128
  if (act.index === undefined)
141
129
  throw new Error(`act ${act.kind} is missing a stable index`);
@@ -158,26 +146,13 @@ export function currentHold(acts) {
158
146
  export function publicActs(acts) {
159
147
  return acts.filter((act) => act.kind === 'say' || act.kind === 'done');
160
148
  }
161
- export function roomChangeActs(acts) {
162
- return acts.filter((act) => act.kind !== 'say' && act.kind !== 'read');
163
- }
164
- export function throttleDelayMs(doc, at) {
165
- const limit = doc.throttlePerMinute;
166
- if (limit === undefined)
167
- return 0;
168
- const recent = foldedState(doc).throttleActivityAts.filter((eventAt) => at - eventAt < THROTTLE_WINDOW_MS).sort((a, b) => a - b);
169
- if (recent.length < limit)
170
- return 0;
171
- const releaseAt = recent[recent.length - limit] + THROTTLE_WINDOW_MS;
172
- return Math.max(1, releaseAt - at);
173
- }
174
149
  function canonicalRuntimeName(doc, name) {
175
150
  return resolveRosterName(doc, name) ?? name;
176
151
  }
177
- export function advanceCursor(doc, name, index, source = 'watch', updatedAt = Date.now()) {
178
- return touchPresenceCursor(doc, name, updatedAt, source, index);
152
+ export function advanceCursor(doc, name, index, updatedAt = Date.now()) {
153
+ return touchPresenceCursor(doc, name, updatedAt, index);
179
154
  }
180
- export function touchPresenceCursor(doc, name, at, source, consumedThroughIndex) {
155
+ export function touchPresenceCursor(doc, name, at, consumedThroughIndex) {
181
156
  if (!Number.isFinite(at))
182
157
  return false;
183
158
  if (consumedThroughIndex !== undefined && (!Number.isInteger(consumedThroughIndex) || consumedThroughIndex < 0))
@@ -188,63 +163,31 @@ export function touchPresenceCursor(doc, name, at, source, consumedThroughIndex)
188
163
  ? (current?.consumedThroughIndex ?? readCursor(doc, key))
189
164
  : Math.max(current?.consumedThroughIndex ?? -1, consumedThroughIndex);
190
165
  const updatedAt = current === undefined ? at : Math.max(current.updatedAt, at);
191
- if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt && current.source === source)
166
+ if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt)
192
167
  return false;
193
- doc.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt, source };
168
+ doc.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt };
194
169
  return true;
195
170
  }
196
- /** The canonical delivery ledger is keyed by (recipient, stable act id). */
197
- export function deliveryReceipt(doc, name, actOrIndex) {
198
- const id = actId(actOrIndex);
199
- // Delivery planners and runtime writers pass roster-canonical names. Keeping
200
- // this a direct lookup is important: pending/status scans must stay linear in
201
- // activities rather than folding the whole square once per notification.
202
- return doc.runtime.mentionReceipts[name]?.[id];
203
- }
204
- export function isDeliveryDelivered(doc, name, actOrIndex) {
205
- return deliveryReceipt(doc, name, actOrIndex)?.status === 'delivered';
206
- }
207
- export function recordDeliveredDelivery(doc, name, actOrIndex, receipt) {
208
- const key = canonicalRuntimeName(doc, name);
209
- const id = actId(actOrIndex);
210
- const current = deliveryReceipt(doc, key, actOrIndex);
211
- if (current?.status === 'delivered')
212
- return false;
213
- const receipts = doc.runtime.mentionReceipts[key] ?? {};
214
- receipts[id] = { status: 'delivered', ...receipt };
215
- doc.runtime.mentionReceipts[key] = receipts;
216
- return true;
217
- }
218
- export function markDeliveredMention(doc, name, actOrIndex, at = Date.now()) {
219
- return recordDeliveredDelivery(doc, name, actOrIndex, { at });
220
- }
221
- export function mentionDeliveredStatus(doc, name, actOrIndex) {
222
- return isDeliveryDelivered(doc, name, actOrIndex) ? 'delivered' : undefined;
223
- }
224
- export function markDeliveredMentions(doc, name, delivered, at = Date.now()) {
225
- let changed = false;
226
- for (const item of delivered) {
227
- const act = item.act;
228
- if (act.kind !== 'say')
229
- continue;
230
- // A cursor says only where feed reading reached. The recipient/act receipt is
231
- // the delivery fact, so never create one for a broadcast or unrelated say.
232
- const directed = act.reach === 'bell' ||
233
- (act.reach !== undefined && sameName(act.reach.beside, name)) ||
234
- extractMentions(act.body).some((mention) => sameName(mention, name));
235
- if (!directed)
236
- continue;
237
- changed = markDeliveredMention(doc, name, actStableIndex(act), at) || changed;
238
- }
239
- return changed;
240
- }
241
- export function latestIndexedActIndex(items) {
242
- return items.reduce((max, item) => Math.max(max, item.index), -1);
171
+ export function latestActIndex(acts) {
172
+ return acts.reduce((max, act) => Math.max(max, act.index), -1);
243
173
  }
244
174
  export function freshWatchLease(doc, name, at = Date.now()) {
245
175
  const key = canonicalRuntimeName(doc, name);
246
- const lease = doc.runtime.leases[key];
176
+ const lease = watchLease(doc, key);
247
177
  if (lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS)
248
178
  return undefined;
249
179
  return lease;
250
180
  }
181
+ export function watchLease(doc, name) {
182
+ return doc.runtime.leases[canonicalRuntimeName(doc, name)];
183
+ }
184
+ export function writeWatchLease(doc, name, lease) {
185
+ doc.runtime.leases[canonicalRuntimeName(doc, name)] = lease;
186
+ }
187
+ export function removeWatchLease(doc, name, leaseId) {
188
+ const key = canonicalRuntimeName(doc, name);
189
+ if (doc.runtime.leases[key]?.leaseId !== leaseId)
190
+ return false;
191
+ delete doc.runtime.leases[key];
192
+ return true;
193
+ }