@astrosheep/square 0.3.33 → 0.3.35

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 (61) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +10 -10
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/dist/activity-feed.js +1 -1
  5. package/dist/activity.d.ts +1 -0
  6. package/dist/activity.js +3 -2
  7. package/dist/artifact.js +2 -1
  8. package/dist/automatic-session.js +17 -1
  9. package/dist/boundary-presentation.d.ts +1 -0
  10. package/dist/boundary-presentation.js +16 -1
  11. package/dist/catch-decisions.d.ts +4 -0
  12. package/dist/catch-decisions.js +23 -7
  13. package/dist/cli/context.d.ts +1 -0
  14. package/dist/cli/context.js +26 -1
  15. package/dist/cli/observation-commands.d.ts +7 -1
  16. package/dist/cli/observation-commands.js +149 -63
  17. package/dist/cli/program.js +8 -2
  18. package/dist/cli/square-commands.d.ts +3 -1
  19. package/dist/cli/square-commands.js +42 -16
  20. package/dist/codex-queue.d.ts +1 -0
  21. package/dist/codex-queue.js +1 -1
  22. package/dist/decisions.d.ts +1 -0
  23. package/dist/decisions.js +29 -14
  24. package/dist/delivery-health.d.ts +1 -0
  25. package/dist/delivery-health.js +19 -6
  26. package/dist/delivery-operations.js +5 -5
  27. package/dist/delivery.d.ts +2 -0
  28. package/dist/delivery.js +1 -0
  29. package/dist/harness-links.js +15 -3
  30. package/dist/harness.js +3 -1
  31. package/dist/help.js +25 -16
  32. package/dist/inbox.d.ts +2 -0
  33. package/dist/inbox.js +4 -2
  34. package/dist/list.js +77 -24
  35. package/dist/model.d.ts +1 -3
  36. package/dist/paseo-connection.js +9 -3
  37. package/dist/paseo-state.d.ts +4 -1
  38. package/dist/paseo-state.js +2 -2
  39. package/dist/presentation.d.ts +4 -0
  40. package/dist/presentation.js +41 -12
  41. package/dist/runtime.d.ts +1 -0
  42. package/dist/square-actions.d.ts +5 -0
  43. package/dist/square-actions.js +31 -7
  44. package/dist/square-core.d.ts +11 -1
  45. package/dist/square-core.js +12 -9
  46. package/dist/square-facade.d.ts +5 -2
  47. package/dist/square-file-adapter.d.ts +1 -1
  48. package/dist/square-file-adapter.js +11 -4
  49. package/dist/square-wiring.d.ts +1 -0
  50. package/dist/square-wiring.js +5 -1
  51. package/dist/stream.d.ts +8 -1
  52. package/dist/stream.js +17 -6
  53. package/dist/views.d.ts +11 -5
  54. package/dist/views.js +42 -16
  55. package/dist/wake-sink.d.ts +2 -0
  56. package/dist/wake-sink.js +10 -1
  57. package/dist/watch.js +26 -9
  58. package/extensions/square-pi.js +41 -7
  59. package/package.json +1 -1
  60. package/skills/brainstorm/SKILL.md +8 -6
  61. package/skills/square/SKILL.md +10 -10
@@ -1,8 +1,8 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { waitForPaseoToolBoundary } from './paseo-timeline.js';
3
- export function discoverPaseoAgents(timeoutMs = 5000) {
3
+ export function discoverPaseoAgents(timeoutMs = 5000, opts = {}) {
4
4
  try {
5
- const raw = execFileSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['ls', '--global', '--json'], {
5
+ const raw = execFileSync(opts.bin ?? process.env.SQUARE_PASEO_BIN ?? 'paseo', [...(opts.args ?? []), 'ls', '--global', '--json'], {
6
6
  encoding: 'utf8',
7
7
  timeout: timeoutMs,
8
8
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -30,6 +30,7 @@ interface ExpressWaitingOptions {
30
30
  }
31
31
  interface ExpressNoWaitOptions extends ParticipantOutputOptions {
32
32
  reason: 'throttled' | 'held';
33
+ forceCommand: string;
33
34
  delayMs?: number;
34
35
  holdReason?: string;
35
36
  draftPath?: string;
@@ -54,6 +55,7 @@ export declare function truncateChars(body: string, maxChars: number): {
54
55
  text: string;
55
56
  remaining: number;
56
57
  };
58
+ export declare function truncateExternalDiagnostic(diagnostic: string): string;
57
59
  export declare function previewActivityBody(body: string): string;
58
60
  export declare function renderRoomChangeText(event: RoomChangeAct): string;
59
61
  export declare function renderEventCli(event: StoredAct, opts?: {
@@ -65,6 +67,7 @@ export declare function renderEventCli(event: StoredAct, opts?: {
65
67
  export declare function renderAmbientEvent(event: StoredAct, viewer: string, opts?: {
66
68
  now?: number;
67
69
  preview?: number;
70
+ presencePreview?: number;
68
71
  actNumber?: number;
69
72
  mention?: string;
70
73
  squareState?: SquareState;
@@ -81,6 +84,7 @@ export declare function renderGrepActivitiesView(visible: StoredAct[], totalMatc
81
84
  export declare function renderActivityLimit(opts: ActivityLimitOptions): string;
82
85
  export declare function renderWatchAlreadyActive(opts: ParticipantOutputOptions): string;
83
86
  export declare function renderWatchForceTakeover(_opts: ParticipantOutputOptions): string;
87
+ export declare function renderWatchReplaceMissing(_opts: ParticipantOutputOptions): string;
84
88
  export declare function renderWatchReplaced(_opts: ParticipantOutputOptions): string;
85
89
  export declare function renderWatchStatus(opts: WatchStatusOptions): string;
86
90
  export declare function renderDoctorClean(): string;
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { participantIdentity } from './participant-identity.js';
5
5
  export { participantIdentity } from './participant-identity.js';
6
- import { audienceIncludes, audienceOf } from './square-core.js';
6
+ import { audienceIncludes, audienceOf, MAX_IDENTITY_SET_SIZE } from './square-core.js';
7
7
  import { perceiveActivity } from './delivery.js';
8
8
  import { actId, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
9
9
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
@@ -15,7 +15,7 @@ function headerLine(squarePath, opts = {}) {
15
15
  }
16
16
  export function displayPath(squarePath, cwd = process.cwd()) {
17
17
  if (!path.isAbsolute(squarePath))
18
- return squarePath;
18
+ return squarePath.split(path.sep).join('/');
19
19
  const comparableCwd = fs.realpathSync.native(cwd);
20
20
  let comparableSquarePath = squarePath;
21
21
  try {
@@ -25,7 +25,8 @@ export function displayPath(squarePath, cwd = process.cwd()) {
25
25
  // Some error outputs name a path before it exists; lexical comparison remains useful there.
26
26
  }
27
27
  const relative = path.relative(comparableCwd, comparableSquarePath);
28
- return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) ? relative : squarePath;
28
+ const displayed = relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) ? relative : squarePath;
29
+ return displayed.split(path.sep).join('/');
29
30
  }
30
31
  export function withPathOutput(squarePath, body = '', opts = {}) {
31
32
  return [headerLine(squarePath, opts), ...(body === '' ? [] : ['', body])].join('\n') + '\n';
@@ -105,6 +106,12 @@ export function truncateChars(body, maxChars) {
105
106
  return { text: body, remaining: 0 };
106
107
  return { text: chars.slice(0, maxChars).join('').trimEnd(), remaining: chars.length - maxChars };
107
108
  }
109
+ const EXTERNAL_DIAGNOSTIC_MAX_CHARS = 160;
110
+ export function truncateExternalDiagnostic(diagnostic) {
111
+ if (truncateChars(diagnostic, EXTERNAL_DIAGNOSTIC_MAX_CHARS).remaining === 0)
112
+ return diagnostic;
113
+ return `${truncateChars(diagnostic, EXTERNAL_DIAGNOSTIC_MAX_CHARS - 1).text}…`;
114
+ }
108
115
  function previewBody(body, maxLen = BODY_PREVIEW_LENGTH) {
109
116
  const preview = truncateChars(body, maxLen);
110
117
  return preview.remaining === 0 ? preview.text : `${preview.text}\n… ${preview.remaining} more chars`;
@@ -181,7 +188,12 @@ function renderPresenceOnlySay(event, opts = {}) {
181
188
  if (targets.length === 0) {
182
189
  return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}`;
183
190
  }
184
- const dest = ` ${targets.map((name) => participantIdentity(name)).join(' and ')}`;
191
+ const visibleTargets = targets.slice(0, MAX_IDENTITY_SET_SIZE).map((name) => participantIdentity(name));
192
+ const remainingTargets = targets.length - visibleTargets.length;
193
+ const dest = ` ${[
194
+ ...visibleTargets,
195
+ ...(remainingTargets === 0 ? [] : [`${remainingTargets} ${remainingTargets === 1 ? 'other' : 'others'}`]),
196
+ ].join(' and ')}`;
185
197
  return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}\n talked to${dest}`;
186
198
  }
187
199
  export function renderAmbientEvent(event, viewer, opts = {}) {
@@ -191,8 +203,11 @@ export function renderAmbientEvent(event, viewer, opts = {}) {
191
203
  throw new Error('Ambient say rendering requires a settled perception or SquareState');
192
204
  }
193
205
  const seen = opts.perception ?? perceiveActivity(opts.squareState, event, viewer);
194
- if (seen === 'presence')
195
- return renderPresenceOnlySay(event, opts);
206
+ if (seen === 'presence') {
207
+ const presence = renderPresenceOnlySay(event, opts);
208
+ const body = opts.presencePreview === undefined ? '' : renderedBody(event.body, opts.presencePreview);
209
+ return `${presence}${bodySuffix(body)}`;
210
+ }
196
211
  return renderEventCli(event, opts);
197
212
  }
198
213
  function draftSavedLines(draftPath) {
@@ -202,8 +217,10 @@ function withDraftInput(command, draftPath) {
202
217
  return draftPath === undefined ? command : `${command} < ${quoteShell(draftPath)}`;
203
218
  }
204
219
  function renderUnreadSummary(opts) {
220
+ const visibleSummaries = opts.activitySummaries.slice(0, MAX_IDENTITY_SET_SIZE);
221
+ const remainingSummaries = opts.activitySummaries.length - visibleSummaries.length;
205
222
  return [
206
- ...opts.activitySummaries.flatMap((item) => [
223
+ ...visibleSummaries.flatMap((item) => [
207
224
  ...item.previews.slice(-1).map((preview) => {
208
225
  const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number, perception: preview.perception });
209
226
  if (rendered === '')
@@ -214,6 +231,7 @@ function renderUnreadSummary(opts) {
214
231
  return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`;
215
232
  }),
216
233
  ]),
234
+ ...(remainingSummaries === 0 ? [] : [` · ${remainingSummaries} more participants have unread activity`]),
217
235
  ...opts.roomChanges.map((act) => ` · ${renderRoomChangeText(act)}`),
218
236
  ];
219
237
  }
@@ -254,7 +272,7 @@ export function renderExpressWaiting(opts) {
254
272
  return ["✕ your activity doesn't land — a hand is raised", ' · your activity is waiting'].join('\n');
255
273
  }
256
274
  export function renderExpressNoWait(opts) {
257
- const retryCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} express -`;
275
+ const retryCommand = opts.forceCommand;
258
276
  const lines = opts.reason === 'throttled'
259
277
  ? [
260
278
  '✕ the square is packed',
@@ -292,8 +310,11 @@ function lastPresenceAnchor(squareState, name) {
292
310
  return -1;
293
311
  }
294
312
  export function renderPresenceAnchor(names) {
295
- const participants = names.map((name) => participantIdentity(name)).join(', ');
296
- return names.length === 1 ? `→ ${participants} was here` : `→ ${participants} were here`;
313
+ const visibleNames = names.slice(0, MAX_IDENTITY_SET_SIZE);
314
+ const participants = visibleNames.map((name) => participantIdentity(name)).join(', ');
315
+ const remaining = names.length - visibleNames.length;
316
+ const suffix = remaining === 0 ? '' : ` and ${remaining} more`;
317
+ return names.length === 1 ? `→ ${participants} was here` : `→ ${participants}${suffix} were here`;
297
318
  }
298
319
  export function renderActivitiesView(squareState, visible, lastN, noTruncate, squarePath, viewer = '', mode = 'ambient') {
299
320
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
@@ -389,11 +410,19 @@ export function renderActivityLimit(opts) {
389
410
  return withPathOutput(opts.squarePath, renderActivityLimitBody(opts), { participantCount: opts.participantCount, held: opts.held });
390
411
  }
391
412
  export function renderWatchAlreadyActive(opts) {
392
- return ['✕ you are already catching', `» ${participantCommandPrefix(opts.squarePath, opts.name)} catch --idle 30m --replace`].join('\n');
413
+ return [
414
+ '✕ you are already catching',
415
+ ` · an active catch is already running for ${participantIdentity(opts.name)}`,
416
+ ' · wait for it to finish before starting another catch',
417
+ `» ${commandPrefix(opts.squarePath)} participants`,
418
+ ].join('\n');
393
419
  }
394
420
  export function renderWatchForceTakeover(_opts) {
395
421
  return '✓ your new catch takes over';
396
422
  }
423
+ export function renderWatchReplaceMissing(_opts) {
424
+ return '⚠ nothing to replace — no active catch was running; your catch started normally';
425
+ }
397
426
  export function renderWatchReplaced(_opts) {
398
427
  return '✕ a newer catch took over';
399
428
  }
@@ -434,7 +463,7 @@ export function renderDoctorClean() {
434
463
  return '✓ no problems found';
435
464
  }
436
465
  export function renderDoctorUnfixable(reason) {
437
- return ['✕ unreadable artifact', ` · ${reason}`].join('\n');
466
+ return ['✕ unreadable artifact', ` · ${truncateExternalDiagnostic(reason)}`].join('\n');
438
467
  }
439
468
  export function renderWatchOutput(history, publicItems, roomChanges, opts) {
440
469
  const sections = [];
package/dist/runtime.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare function nowMs(): number;
12
12
  /** Pure directed-activity filter. Bell matches every viewer; mentions match by audience. */
13
13
  export declare function matchesMentionTarget(act: {
14
14
  body: string;
15
+ mentions?: readonly string[];
15
16
  reach?: Reach;
16
17
  }, mention: string | true): boolean;
17
18
  export declare function foldedState(squareState: SquareState): import("./square-core.js").FoldedSquareState;
@@ -16,6 +16,11 @@ export declare function join(square: OperationContext, name: string): Promise<{
16
16
  readonly name: string;
17
17
  readonly activity: Activity | null;
18
18
  }>;
19
+ /** End the standing participant and immediately let the caller reclaim the name. */
20
+ export declare function takeover(square: OperationContext, name: string, oldSessionIds?: readonly string[]): Promise<{
21
+ readonly name: string;
22
+ readonly activities: readonly Activity[];
23
+ }>;
19
24
  export declare function implicitJoin(square: OperationContext, name: string): Promise<{
20
25
  readonly name: string;
21
26
  readonly state: 'joined' | 'active' | 'done';
@@ -1,6 +1,6 @@
1
- import { extractMentions, formatActivityId, parseActivityId } from './square-core.js';
2
- import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, decideAct, decideImplicitJoin, decideJoin } from './decisions.js';
3
- import { SquareError } from './model.js';
1
+ import { formatActivityId, parseActivityId } from './square-core.js';
2
+ import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, decideAct, decideImplicitJoin, decideJoin, resolveKnownName } from './decisions.js';
3
+ import { nameKey, SquareError } from './model.js';
4
4
  import { participantIdentity } from './participant-identity.js';
5
5
  import { deliverPending } from './delivery-operations.js';
6
6
  import { decideCatch } from './catch-decisions.js';
@@ -48,7 +48,7 @@ function exposeCaught(activity, perception) {
48
48
  throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
49
49
  const result = {
50
50
  id: formatActivityId(activity.index), at: activity.at, kind: activity.kind, actor: activity.actor,
51
- mentions: activity.kind === 'say' ? extractMentions(activity.body) : [],
51
+ mentions: activity.kind === 'say' ? activity.mentions ?? [] : [],
52
52
  ...('body' in activity && activity.body !== undefined ? { body: activity.body } : {}),
53
53
  ...('target' in activity ? { target: activity.target } : {}),
54
54
  ...(activity.kind === 'say' && activity.reply !== undefined ? { reply: formatActivityId(activity.reply) } : {}),
@@ -75,11 +75,12 @@ export async function catchUp(square, name, options = {}, project) {
75
75
  activities: attempt.decision.delivered.map((activity) => exposeCaught(activity, attempt.decision.perceptions.get(activity.index) ?? 'full')),
76
76
  consumedThrough: attempt.decision.consumedThrough,
77
77
  idleExpired: false,
78
+ remaining: attempt.decision.remaining,
78
79
  };
79
80
  }
80
81
  const remaining = deadline - Date.now();
81
82
  if (remaining <= 0 || !await square.artifact.changed(attempt.version, remaining)) {
82
- return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true };
83
+ return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true, remaining: 0 };
83
84
  }
84
85
  }
85
86
  }
@@ -105,7 +106,7 @@ function exposeActivity(stored) {
105
106
  return {
106
107
  id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
107
108
  ...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
108
- mentions: stored.kind === 'say' ? extractMentions(stored.body) : [],
109
+ mentions: stored.kind === 'say' ? stored.mentions ?? [] : [],
109
110
  ...('target' in stored ? { target: stored.target } : {}),
110
111
  ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}),
111
112
  };
@@ -128,6 +129,29 @@ export async function join(square, name) {
128
129
  await publishIdentityRoute(square, committed.name);
129
130
  return { name: committed.name, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
130
131
  }
132
+ /** End the standing participant and immediately let the caller reclaim the name. */
133
+ export async function takeover(square, name, oldSessionIds = []) {
134
+ const now = square.clock();
135
+ const oldSessions = new Set(oldSessionIds);
136
+ const committed = await square.artifact.transact((state) => {
137
+ const joinedName = resolveKnownName(state, name);
138
+ const done = coreDone(state, joinedName, '', now);
139
+ const storedDone = committedActivity(storeActs(state, [done]), 'kick');
140
+ const decision = decideJoin(state, joinedName, now);
141
+ if (decision.joinAct === undefined)
142
+ throw new SquareError('already_joined', `${participantIdentity(joinedName)} could not be reclaimed`);
143
+ const storedJoin = committedActivity(storeActs(state, [decision.joinAct]), 'join');
144
+ const participantSessions = new Set([
145
+ ...oldSessions,
146
+ ...(state.routes ?? []).filter((route) => nameKey(route.participant) === nameKey(joinedName)).map((route) => route.sessionId),
147
+ ]);
148
+ state.routes = (state.routes ?? []).filter((route) => nameKey(route.participant) !== nameKey(joinedName) && !participantSessions.has(route.sessionId));
149
+ return { state, result: { name: joinedName, stored: [storedDone, storedJoin] } };
150
+ });
151
+ await ensureLocalPresence(square, committed.name);
152
+ await publishIdentityRoute(square, committed.name);
153
+ return { name: committed.name, activities: committed.stored.map(exposeActivity) };
154
+ }
131
155
  export async function implicitJoin(square, name) {
132
156
  const now = square.clock();
133
157
  const committed = await square.artifact.transact((state) => {
@@ -147,7 +171,7 @@ export async function express(square, name, body, options = {}) {
147
171
  const now = square.clock();
148
172
  const reply = options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
149
173
  const committed = await square.artifact.transact((state) => {
150
- const decision = decideAct(state, { name, body, force: options.force ?? false, now, ...(options.reach === undefined ? {} : { reach: options.reach }), ...(reply === undefined ? {} : { reply }) });
174
+ const decision = decideAct(state, { name, body, force: options.force ?? false, now, mentions: options.mentions, ...(options.reach === undefined ? {} : { reach: options.reach }), ...(reply === undefined ? {} : { reply }) });
151
175
  if (decision.type === 'blocked') {
152
176
  const pending = decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) + decision.unreadRoomChanges.length;
153
177
  throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending });
@@ -7,6 +7,7 @@ export type Audience = {
7
7
  readonly names: readonly string[];
8
8
  };
9
9
  export type ActivityId = `act/${number}`;
10
+ export declare const MAX_IDENTITY_SET_SIZE = 10;
10
11
  export declare function formatActivityId(index: number): ActivityId;
11
12
  export declare function parseActivityId(value: string): number | undefined;
12
13
  export interface Hold {
@@ -29,6 +30,7 @@ export type Act = {
29
30
  actor: Participant;
30
31
  at?: number;
31
32
  body: string;
33
+ mentions: readonly Participant[];
32
34
  reach?: Reach;
33
35
  reply?: number;
34
36
  } | {
@@ -119,10 +121,18 @@ export type ValidationResult = {
119
121
  ok: false;
120
122
  reason: 'bell_quota';
121
123
  nextAt: number;
124
+ } | {
125
+ ok: false;
126
+ reason: 'mention_limit';
127
+ limit: number;
128
+ } | {
129
+ ok: false;
130
+ reason: 'listening_limit';
131
+ limit: number;
122
132
  };
123
- export declare function extractMentions(body: string): string[];
124
133
  export declare function audienceOf(say: {
125
134
  body: string;
135
+ mentions?: readonly Participant[];
126
136
  reach?: Reach;
127
137
  }): Audience;
128
138
  export declare function audienceIncludes(audience: Audience, name: string): boolean;
@@ -1,3 +1,4 @@
1
+ export const MAX_IDENTITY_SET_SIZE = 10;
1
2
  export function formatActivityId(index) {
2
3
  if (!Number.isSafeInteger(index) || index < 0) {
3
4
  throw new Error(`Invalid activity index: ${index}`);
@@ -18,14 +19,6 @@ function nameKey(name) {
18
19
  function sameName(a, b) {
19
20
  return nameKey(a) === nameKey(b);
20
21
  }
21
- export function extractMentions(body) {
22
- const matches = [];
23
- const re = /@([\p{L}\p{N}_-]+(?:\/[\p{L}\p{N}_-]+)*)/gu;
24
- let match;
25
- while ((match = re.exec(body)) !== null)
26
- matches.push(match[1]);
27
- return matches;
28
- }
29
22
  function uniqueMentionNames(names) {
30
23
  const unique = [];
31
24
  for (const name of names) {
@@ -38,7 +31,7 @@ function uniqueMentionNames(names) {
38
31
  export function audienceOf(say) {
39
32
  if (say.reach === 'bell')
40
33
  return { kind: 'bell' };
41
- return { kind: 'mentions', names: uniqueMentionNames(extractMentions(say.body)) };
34
+ return { kind: 'mentions', names: uniqueMentionNames(say.mentions ?? []) };
42
35
  }
43
36
  export function audienceIncludes(audience, name) {
44
37
  if (audience.kind === 'bell')
@@ -300,12 +293,22 @@ export function validate(state, act, options = {}) {
300
293
  return { ok: false, reason: 'done' };
301
294
  if (current?.joined !== true)
302
295
  return { ok: false, reason: 'not_joined' };
296
+ if (act.kind === 'listen'
297
+ && !isListening(state, act.actor, act.target)
298
+ && listeningTo(state, act.actor).length >= MAX_IDENTITY_SET_SIZE) {
299
+ return { ok: false, reason: 'listening_limit', limit: MAX_IDENTITY_SET_SIZE };
300
+ }
303
301
  return { ok: true };
304
302
  case 'say': {
305
303
  if (current?.done)
306
304
  return { ok: false, reason: 'done' };
307
305
  if (current?.joined !== true)
308
306
  return { ok: false, reason: 'not_joined' };
307
+ const audience = audienceOf(act);
308
+ const mentionCount = audience.kind === 'mentions' ? audience.names.length : 0;
309
+ if (mentionCount > MAX_IDENTITY_SET_SIZE) {
310
+ return { ok: false, reason: 'mention_limit', limit: MAX_IDENTITY_SET_SIZE };
311
+ }
309
312
  if (state.hold.active)
310
313
  return { ok: false, reason: 'held', hold: state.hold };
311
314
  const activityCount = current?.activityCount ?? 0;
@@ -15,6 +15,7 @@ export interface PerceivedActivity extends Activity {
15
15
  }
16
16
  export interface ExpressOptions {
17
17
  readonly force?: boolean;
18
+ readonly mentions?: readonly string[];
18
19
  readonly reach?: Reach;
19
20
  readonly reply?: ActivityId;
20
21
  }
@@ -29,11 +30,13 @@ export interface CatchOptions {
29
30
  readonly idle?: number;
30
31
  readonly from?: readonly string[];
31
32
  readonly mention?: boolean;
33
+ readonly limit?: number;
32
34
  }
33
35
  export interface CatchResult {
34
36
  readonly activities: readonly PerceivedActivity[];
35
37
  readonly consumedThrough: ActivityId | null;
36
38
  readonly idleExpired: boolean;
39
+ readonly remaining: number;
37
40
  }
38
41
  export interface HistoryQuery {
39
42
  readonly limit?: number;
@@ -42,7 +45,7 @@ export interface HistoryQuery {
42
45
  readonly after?: ActivityId;
43
46
  readonly grep?: string;
44
47
  readonly from?: readonly string[];
45
- readonly mention?: boolean;
48
+ readonly mention?: string;
46
49
  }
47
50
  export interface ParticipantStatus {
48
51
  readonly name: string;
@@ -90,7 +93,7 @@ export interface Participant {
90
93
  ignore(target: string): Promise<ListenerChangeResult>;
91
94
  listening(): Promise<readonly string[]>;
92
95
  catch(options?: CatchOptions): Promise<CatchResult>;
93
- history(query?: HistoryQuery): Promise<PerceivedActivity[]>;
96
+ history(query?: HistoryQuery): Promise<Activity[]>;
94
97
  hold(reason?: string): Promise<ExpressResult>;
95
98
  resume(): Promise<ExpressResult>;
96
99
  done(body?: string): Promise<ExpressResult>;
@@ -27,4 +27,4 @@ export type SquareChangeWaitResult<T> = {
27
27
  } | {
28
28
  status: 'expired';
29
29
  };
30
- export declare function waitForSquareChanges<T>(squarePaths: readonly string[], timeoutMs: number, signal?: AbortSignal, afterReady?: () => Promise<T | undefined>): Promise<SquareChangeWaitResult<T>>;
30
+ export declare function waitForSquareChanges<T>(squarePaths: readonly string[], timeoutMs: number, signal?: AbortSignal, afterReady?: () => Promise<T | undefined>, onArmed?: (armed: boolean) => void): Promise<SquareChangeWaitResult<T>>;
@@ -83,9 +83,11 @@ export function buildMemorySquare(options) {
83
83
  function memoryArtifact(cell) {
84
84
  return { read: () => cell.read(), transact: (fn) => cell.transact(fn), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
85
85
  }
86
- export async function waitForSquareChanges(squarePaths, timeoutMs, signal, afterReady) {
87
- if (timeoutMs <= 0 || signal?.aborted || squarePaths.length === 0)
86
+ export async function waitForSquareChanges(squarePaths, timeoutMs, signal, afterReady, onArmed) {
87
+ if (timeoutMs <= 0 || signal?.aborted || squarePaths.length === 0) {
88
+ onArmed?.(false);
88
89
  return { status: 'expired' };
90
+ }
89
91
  const squares = [];
90
92
  try {
91
93
  for (const squarePath of [...new Set(squarePaths)]) {
@@ -94,21 +96,26 @@ export async function waitForSquareChanges(squarePaths, timeoutMs, signal, after
94
96
  }
95
97
  catch { /* stale binding */ }
96
98
  }
97
- if (squares.length === 0)
99
+ if (squares.length === 0) {
100
+ onArmed?.(false);
98
101
  return { status: 'expired' };
102
+ }
99
103
  const baselines = await Promise.all(squares.map(async (square) => ({
100
104
  square,
101
105
  version: (await square.artifact.read()).version,
102
106
  })));
103
107
  const ready = await afterReady?.();
104
- if (ready !== undefined)
108
+ if (ready !== undefined) {
109
+ onArmed?.(false);
105
110
  return { status: 'ready', value: ready };
111
+ }
106
112
  const waits = baselines.map(async ({ square, version }) => {
107
113
  const changed = await square.artifact.changed(version, timeoutMs).catch(() => false);
108
114
  if (changed)
109
115
  return true;
110
116
  throw new Error('square wait expired');
111
117
  });
118
+ onArmed?.(true);
112
119
  let abortWait;
113
120
  const abort = new Promise((resolve) => {
114
121
  abortWait = () => resolve(false);
@@ -13,6 +13,7 @@ export declare class Square {
13
13
  readonly participant: Participant;
14
14
  readonly activity: Activity | null;
15
15
  }>;
16
+ takeover(name: string, oldSessionIds?: readonly string[]): Promise<Participant>;
16
17
  implicitJoin(name: string): Promise<{
17
18
  readonly state: 'joined' | 'active' | 'done';
18
19
  readonly participant?: Participant;
@@ -1,6 +1,6 @@
1
1
  import { closeOpenSquare } from './open-square.js';
2
2
  import { buildMemorySquare, buildSquare, openSquare } from './square-file-adapter.js';
3
- import { catchUp, done, express, hold, ignore, implicitJoin, join, listen, listening, resume, } from './square-actions.js';
3
+ import { catchUp, done, express, hold, ignore, implicitJoin, join, takeover, listen, listening, resume, } from './square-actions.js';
4
4
  import { markBoundarySeen as recordBoundarySeen } from './presence.js';
5
5
  import { history, participantHistory, participants, resolveParticipant, snapshot } from './views.js';
6
6
  import { currentParticipant } from './views.js';
@@ -69,6 +69,10 @@ export class Square {
69
69
  const joined = await join(this.context, name);
70
70
  return { participant: new ParticipantHandle(joined.name, this.square, this.context), activity: joined.activity };
71
71
  }
72
+ async takeover(name, oldSessionIds = []) {
73
+ const result = await takeover(this.context, name, oldSessionIds);
74
+ return new ParticipantHandle(result.name, this.square, this.context);
75
+ }
72
76
  async implicitJoin(name) {
73
77
  const joined = await implicitJoin(this.context, name);
74
78
  return joined.state === 'done'
package/dist/stream.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ export type StreamStart = {
2
+ readonly kind: 'tail';
3
+ readonly last: number;
4
+ } | {
5
+ readonly kind: 'after';
6
+ readonly after: number;
7
+ };
1
8
  /** Machine-readable tailing stays available; interactive terminal rendering was retired. */
2
- export declare function cmdStreamNdjson(squarePath: string, recipient?: string): Promise<void>;
9
+ export declare function cmdStreamNdjson(squarePath: string, recipient?: string, start?: StreamStart): Promise<void>;
3
10
  export declare function cmdStream(squarePath: string): Promise<void>;
package/dist/stream.js CHANGED
@@ -1,33 +1,44 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { once } from 'node:events';
3
4
  import { setTimeout as sleep } from 'node:timers/promises';
4
5
  import { quoteShell } from './presentation.js';
5
6
  import { SLEEP_MS } from './runtime.js';
6
7
  import { openSquare } from './square-file-adapter.js';
7
8
  import { closeOpenSquare } from './open-square.js';
8
- import { streamProjection } from './views.js';
9
+ import { streamProjection, streamTailProjection } from './views.js';
10
+ async function writeNdjson(value) {
11
+ if (!process.stdout.write(`${JSON.stringify(value)}\n`))
12
+ await once(process.stdout, 'drain');
13
+ }
9
14
  /** Machine-readable tailing stays available; interactive terminal rendering was retired. */
10
- export async function cmdStreamNdjson(squarePath, recipient) {
15
+ export async function cmdStreamNdjson(squarePath, recipient, start = { kind: 'tail', last: 10 }) {
11
16
  if (!fs.existsSync(squarePath)) {
12
17
  process.stderr.write(`square not found: ${squarePath}\n`);
13
18
  process.exitCode = 2;
14
19
  return;
15
20
  }
16
- let cursor = -1;
21
+ let cursor = start.kind === 'after' ? start.after : -1;
22
+ let initial = start.kind === 'tail' ? start : undefined;
17
23
  while (true) {
18
24
  try {
19
25
  const square = await openSquare(squarePath);
20
26
  try {
21
- const projection = await streamProjection(square, cursor, recipient);
27
+ const projection = initial === undefined
28
+ ? await streamProjection(square, cursor, recipient)
29
+ : await streamTailProjection(square, initial.last, recipient);
22
30
  for (const item of projection.activities) {
23
- process.stdout.write(`${JSON.stringify({
31
+ await writeNdjson({
24
32
  seq: item.activity.index,
25
33
  square: squarePath,
26
34
  ...item.activity,
27
35
  ...(item.route === undefined ? {} : { route: item.route }),
28
- })}\n`);
36
+ });
29
37
  }
30
38
  cursor = projection.cursor;
39
+ initial = undefined;
40
+ if (projection.hasMore)
41
+ continue;
31
42
  }
32
43
  finally {
33
44
  await closeOpenSquare(square);
package/dist/views.d.ts CHANGED
@@ -3,7 +3,7 @@ import { coreParticipants, coreStatus } from './decisions.js';
3
3
  import { type DeliveryModel, type PlannedNotification } from './delivery.js';
4
4
  import { type ActivitiesOptions, type ActivityObservation, type InboxNotification, type PublicAct, type RoomChangeAct, type SquareState, type StoredAct } from './model.js';
5
5
  import type { OpenSquare } from './open-square.js';
6
- import type { Activity, HistoryQuery, ParticipantStatus, PerceivedActivity, SquareSnapshot } from './square-facade.js';
6
+ import type { Activity, HistoryQuery, ParticipantStatus, SquareSnapshot } from './square-facade.js';
7
7
  export interface ActivityPresentation {
8
8
  readonly name: string;
9
9
  readonly roster: readonly string[];
@@ -28,9 +28,7 @@ export interface EntryPresentation {
28
28
  readonly participantCount: number;
29
29
  }
30
30
  export interface HistoryPresentation {
31
- readonly activities: readonly (StoredAct & {
32
- readonly perception: 'full' | 'presence';
33
- })[];
31
+ readonly activities: readonly StoredAct[];
34
32
  readonly sayNumbers: Readonly<Record<number, number>>;
35
33
  readonly presenceAnchors: Readonly<Record<number, readonly string[]>>;
36
34
  readonly participantCount: number;
@@ -67,13 +65,14 @@ export interface StreamProjection {
67
65
  readonly route?: string;
68
66
  }[];
69
67
  readonly cursor: number;
68
+ readonly hasMore: boolean;
70
69
  }
71
70
  export interface PendingDeliveryProjection {
72
71
  readonly recipient: string;
73
72
  readonly notifications: readonly PlannedNotification[];
74
73
  }
75
74
  export declare function history(square: OpenSquare, query?: HistoryQuery): Promise<Activity[]>;
76
- export declare function participantHistory(square: OpenSquare, name: string, query?: HistoryQuery): Promise<PerceivedActivity[]>;
75
+ export declare function participantHistory(square: OpenSquare, _name: string, query?: HistoryQuery): Promise<Activity[]>;
77
76
  export declare function resolveParticipant(square: OpenSquare, name: string): Promise<{
78
77
  readonly name: string;
79
78
  readonly roster: readonly string[];
@@ -101,6 +100,13 @@ export declare function streamProjection(square: OpenSquare | {
101
100
  }>;
102
101
  };
103
102
  }, cursor: number, recipient?: string): Promise<StreamProjection>;
103
+ export declare function streamTailProjection(square: OpenSquare | {
104
+ readonly cell: {
105
+ read(): Promise<{
106
+ state: SquareState;
107
+ }>;
108
+ };
109
+ }, last?: number, recipient?: string): Promise<StreamProjection>;
104
110
  export declare function notificationForAct(square: OpenSquare, actIndex: number): Promise<readonly PlannedNotification[]>;
105
111
  export declare function pendingDeliveriesFromState(state: SquareState, delivery?: DeliveryModel): readonly PendingDeliveryProjection[];
106
112
  export declare function pendingDeliveries(square: OpenSquare): Promise<readonly PendingDeliveryProjection[]>;