@astrosheep/square 0.3.14 → 0.3.15

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 (62) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/dist/activity-feed.d.ts +2 -3
  3. package/dist/activity-feed.js +6 -15
  4. package/dist/activity.js +1 -1
  5. package/dist/artifact.js +17 -26
  6. package/dist/attention-presentation.d.ts +13 -0
  7. package/dist/attention-presentation.js +22 -0
  8. package/dist/automatic-session.js +6 -8
  9. package/dist/boundary-presentation.js +35 -14
  10. package/dist/claude-hook.js +5 -3
  11. package/dist/cli/context.d.ts +4 -4
  12. package/dist/cli/context.js +25 -4
  13. package/dist/cli/harness-command.js +1 -1
  14. package/dist/cli/maintenance-commands.js +5 -4
  15. package/dist/cli/observation-commands.js +24 -16
  16. package/dist/cli/program.js +1 -1
  17. package/dist/cli/registry.js +4 -1
  18. package/dist/cli/square-commands.d.ts +7 -0
  19. package/dist/cli/square-commands.js +123 -27
  20. package/dist/codex-hook.js +5 -3
  21. package/dist/decisions.d.ts +19 -0
  22. package/dist/decisions.js +45 -12
  23. package/dist/delivery.d.ts +14 -21
  24. package/dist/delivery.js +40 -74
  25. package/dist/help.js +8 -5
  26. package/dist/inbox.js +1 -0
  27. package/dist/index.d.ts +1 -10
  28. package/dist/index.js +0 -32
  29. package/dist/landing.d.ts +12 -0
  30. package/dist/landing.js +35 -4
  31. package/dist/model.d.ts +8 -11
  32. package/dist/model.js +2 -2
  33. package/dist/notifications.js +35 -11
  34. package/dist/paseo-delivery.js +7 -4
  35. package/dist/paseo.d.ts +6 -0
  36. package/dist/paseo.js +5 -0
  37. package/dist/presence.d.ts +3 -0
  38. package/dist/presence.js +40 -10
  39. package/dist/presentation.d.ts +6 -2
  40. package/dist/presentation.js +22 -9
  41. package/dist/presented.d.ts +2 -0
  42. package/dist/presented.js +20 -0
  43. package/dist/registry.js +4 -1
  44. package/dist/routes.d.ts +5 -0
  45. package/dist/routes.js +17 -0
  46. package/dist/runtime.d.ts +4 -5
  47. package/dist/runtime.js +35 -18
  48. package/dist/square-core.d.ts +17 -0
  49. package/dist/square-core.js +51 -1
  50. package/dist/square-facade.d.ts +9 -1
  51. package/dist/square-wiring.d.ts +8 -0
  52. package/dist/square-wiring.js +32 -3
  53. package/dist/views.d.ts +9 -3
  54. package/dist/views.js +16 -14
  55. package/dist/wake-port.d.ts +4 -1
  56. package/dist/wake-port.js +5 -0
  57. package/dist/wakes.js +3 -5
  58. package/dist/watch.js +1 -0
  59. package/package.json +19 -3
  60. package/skills/brainstorm/SKILL.md +1 -1
  61. package/skills/square/.claude-plugin/plugin.json +1 -1
  62. package/skills/square/SKILL.md +1 -1
package/dist/presence.js CHANGED
@@ -1,14 +1,17 @@
1
- import { extractMentions, formatActivityId, perceive } from './square-core.js';
2
- import { ackPeerDelta, deliveryDelta, filteredRoomChanges, matchesFeedFilter, peerPublicActs } from './activity-feed.js';
3
- import { markDeliveredNotifications } from './delivery.js';
1
+ import { extractMentions, formatActivityId } from './square-core.js';
2
+ import { deliveryDelta, filteredRoomChanges, matchesFeedFilter, peerPublicActs } from './activity-feed.js';
3
+ import { deriveDeliveryModel, markSeenNotifications, perceiveActivity } from './delivery.js';
4
4
  import { SquareError } from './model.js';
5
+ import { openSquare } from './square-file-adapter.js';
6
+ import { closeOpenSquare } from './open-square.js';
5
7
  import { resolveKnownName } from './decisions.js';
6
8
  import { readCursor } from './runtime.js';
7
- function expose(activity, viewer) {
9
+ import { recordObservation } from './runtime.js';
10
+ function expose(squareState, activity, viewer) {
8
11
  if (activity.kind === 'read' || activity.actor === undefined)
9
12
  throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
10
- const perception = perceive(activity, viewer);
11
- const result = { id: formatActivityId(activity.index), at: activity.at, kind: activity.kind, actor: activity.actor, mentions: activity.kind === 'say' ? extractMentions(activity.body) : [], ...('body' in activity && activity.body !== undefined ? { body: activity.body } : {}), ...(activity.kind === 'say' && activity.reply !== undefined ? { reply: formatActivityId(activity.reply) } : {}) };
13
+ const perception = perceiveActivity(squareState, activity, viewer);
14
+ const result = { id: formatActivityId(activity.index), at: activity.at, kind: activity.kind, actor: activity.actor, mentions: activity.kind === 'say' ? extractMentions(activity.body) : [], ...('body' in activity && activity.body !== undefined ? { body: activity.body } : {}), ...('target' in activity ? { target: activity.target } : {}), ...(activity.kind === 'say' && activity.reply !== undefined ? { reply: formatActivityId(activity.reply) } : {}) };
12
15
  if (perception === 'full' || !('body' in result))
13
16
  return { ...result, perception };
14
17
  const { body: _body, ...withoutBody } = result;
@@ -25,13 +28,14 @@ export async function catchUp(square, name, options = {}) {
25
28
  const viewer = resolveKnownName(state, name);
26
29
  const delta = deliveryDelta(state, viewer);
27
30
  const filter = { ...(options.from === undefined ? {} : { participants: [...options.from] }), ...(options.mention === true ? { mention: viewer } : {}) };
28
- const delivered = [...peerPublicActs(delta, viewer).filter((activity) => matchesFeedFilter(activity, filter)), ...filteredRoomChanges(delta, viewer, filter)]
31
+ const delivery = deriveDeliveryModel(state);
32
+ const delivered = [...peerPublicActs(delta, viewer).filter((activity) => matchesFeedFilter(activity, filter, activity.kind === 'say' ? delivery.plan(activity).map((planned) => planned.recipient) : undefined)), ...filteredRoomChanges(delta, viewer, filter)]
29
33
  .filter((activity, index, activities) => activities.findIndex((candidate) => candidate.index === activity.index) === index)
30
34
  .sort((left, right) => left.index - right.index);
31
- const cursorChanged = ackPeerDelta(state, viewer, delta, at);
32
- const receiptsChanged = markDeliveredNotifications(state, viewer, delivered, at);
35
+ const seenChanged = delivered.reduce((changed, activity) => recordObservation(state, viewer, activity.index, 'seen', at) || changed, false)
36
+ || markSeenNotifications(state, viewer, delivered, at);
33
37
  const cursor = readCursor(state, viewer);
34
- return { ...(cursorChanged || receiptsChanged ? { state } : {}), result: { version, caught: { activities: delivered.map((activity) => expose(activity, viewer)), consumedThrough: cursor < 0 ? null : formatActivityId(cursor), idleExpired: false } } };
38
+ return { ...(seenChanged ? { state } : {}), result: { version, caught: { activities: delivered.map((activity) => expose(state, activity, viewer)), consumedThrough: cursor < 0 ? null : formatActivityId(cursor), idleExpired: false } } };
35
39
  });
36
40
  if (attempt.caught.activities.length > 0 || idle === 0)
37
41
  return attempt.caught;
@@ -40,3 +44,29 @@ export async function catchUp(square, name, options = {}) {
40
44
  return { ...attempt.caught, idleExpired: true };
41
45
  }
42
46
  }
47
+ /** Commit seen only for complete, actually rendered boundary bodies. */
48
+ export async function markBoundarySeen(squarePath, name, ownerId, actIndexes, at = Date.now()) {
49
+ let square;
50
+ try {
51
+ square = await openSquare(squarePath);
52
+ }
53
+ catch {
54
+ return;
55
+ }
56
+ try {
57
+ await square.cell.transact((state) => {
58
+ let changed = false;
59
+ for (const index of actIndexes)
60
+ changed = recordObservation(state, name, index, 'seen', at, ownerId) || changed;
61
+ return changed ? { state, result: undefined } : { result: undefined };
62
+ });
63
+ }
64
+ finally {
65
+ await closeOpenSquare(square);
66
+ }
67
+ }
68
+ export async function markNotificationNotified(square, name, actIndex, ownerId, at = Date.now()) {
69
+ await square.cell.transact((state) => recordObservation(state, name, actIndex, 'notified', at, ownerId)
70
+ ? { state, result: undefined }
71
+ : { result: undefined });
72
+ }
@@ -1,5 +1,6 @@
1
1
  import { type StoredAct, type SquareState, type PublicAct, type RoomChangeAct } from './model.js';
2
2
  export { participantIdentity } from './participant-identity.js';
3
+ import { type Perception } from './square-core.js';
3
4
  import type { UnreadActivitySummary, ParticipantStatus } from './decisions.js';
4
5
  export type WatchStatus = 'stale' | 'empty-now' | 'quorum' | 'capped';
5
6
  interface HeaderOptions {
@@ -66,12 +67,14 @@ export declare function renderAmbientEvent(event: StoredAct, viewer: string, opt
66
67
  preview?: number;
67
68
  actNumber?: number;
68
69
  mention?: string;
70
+ squareState?: SquareState;
71
+ perception?: Perception;
69
72
  }): string;
70
- export declare function renderPendingFeed(history: StoredAct[], publicItems: PublicAct[], roomChanges: RoomChangeAct[], viewer?: string): string;
73
+ export declare function renderPendingFeed(history: StoredAct[], publicItems: PublicAct[], roomChanges: RoomChangeAct[], viewer: string | undefined, squareState: SquareState): string;
71
74
  export declare function renderActivityBlocked(opts: ActivityBlockedOptions): string;
72
75
  export declare function renderExpressWaiting(opts: ExpressWaitingOptions): string;
73
76
  export declare function renderExpressNoWait(opts: ExpressNoWaitOptions): string;
74
- export declare function renderPublicTail(events: StoredAct[], lastN: number | null | undefined, now?: number, viewer?: string): string;
77
+ export declare function renderPublicTail(squareState: SquareState, events: StoredAct[], lastN: number | null | undefined, now?: number, viewer?: string): string;
75
78
  export declare function renderActivitiesView(squareState: SquareState, visible: StoredAct[], lastN: number | null | undefined, full: boolean | undefined, squarePath: string, viewer?: string, mode?: 'ambient' | 'archive'): string;
76
79
  export declare function renderGrepActivitiesView(visible: StoredAct[], totalMatches: number, full: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean): string;
77
80
  export declare function renderActivityLimit(opts: ActivityLimitOptions): string;
@@ -88,4 +91,5 @@ export declare function renderWatchOutput(history: StoredAct[], publicItems: Pub
88
91
  mention?: string;
89
92
  viewer: string;
90
93
  showCatchHint?: boolean;
94
+ squareState: SquareState;
91
95
  }): string;
@@ -3,7 +3,8 @@ 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, perceive } from './square-core.js';
6
+ import { audienceIncludes, audienceOf } from './square-core.js';
7
+ import { perceiveActivity } from './delivery.js';
7
8
  import { actId, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
8
9
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
9
10
  import { grepSnippet } from './search.js';
@@ -128,6 +129,10 @@ export function renderRoomChangeText(event) {
128
129
  return `${identity} raised a hand${event.body ? ` — ${event.body}` : ''}`;
129
130
  case 'resume':
130
131
  return `${identity} lowered the hand`;
132
+ case 'listen':
133
+ return `${identity} turns an ear toward ${participantIdentity(event.target)}`;
134
+ case 'ignore':
135
+ return `${identity} turns away from ${participantIdentity(event.target)}`;
131
136
  }
132
137
  }
133
138
  function renderedBody(body, maxChars) {
@@ -150,6 +155,9 @@ export function renderEventCli(event, opts = {}) {
150
155
  return `· ${renderRoomChangeText(event)}`;
151
156
  case 'resume':
152
157
  return `✓ ${renderRoomChangeText(event)}`;
158
+ case 'listen':
159
+ case 'ignore':
160
+ return `· ${renderRoomChangeText(event)}`;
153
161
  case 'say': {
154
162
  const body = renderedBody(event.body, maxBody);
155
163
  const mention = opts.mention;
@@ -176,7 +184,10 @@ function renderPresenceOnlySay(event, opts = {}) {
176
184
  export function renderAmbientEvent(event, viewer, opts = {}) {
177
185
  if (event.kind !== 'say')
178
186
  return renderEventCli(event, opts);
179
- const seen = perceive(event, viewer);
187
+ if (opts.perception === undefined && opts.squareState === undefined) {
188
+ throw new Error('Ambient say rendering requires a settled perception or SquareState');
189
+ }
190
+ const seen = opts.perception ?? perceiveActivity(opts.squareState, event, viewer);
180
191
  if (seen === 'presence')
181
192
  return renderPresenceOnlySay(event, opts);
182
193
  return renderEventCli(event, opts);
@@ -191,10 +202,10 @@ function renderUnreadSummary(opts) {
191
202
  return [
192
203
  ...opts.activitySummaries.flatMap((item) => [
193
204
  ...item.previews.slice(-1).map((preview) => {
194
- const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number });
205
+ const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number, perception: preview.perception });
195
206
  if (rendered === '')
196
207
  return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago`;
197
- if (preview.act.kind === 'say' && perceive(preview.act, opts.viewer) === 'presence') {
208
+ if (preview.perception === 'presence') {
198
209
  return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered.replace(/\n/g, ' ')}`;
199
210
  }
200
211
  return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`;
@@ -203,11 +214,12 @@ function renderUnreadSummary(opts) {
203
214
  ...opts.roomChanges.map((act) => ` · ${renderRoomChangeText(act)}`),
204
215
  ];
205
216
  }
206
- export function renderPendingFeed(history, publicItems, roomChanges, viewer = '') {
217
+ export function renderPendingFeed(history, publicItems, roomChanges, viewer = '', squareState) {
207
218
  const lines = [];
208
219
  for (const act of publicItems) {
209
220
  const rendered = renderAmbientEvent(act, viewer, {
210
221
  actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
222
+ ...(squareState === undefined ? {} : { squareState }),
211
223
  });
212
224
  if (rendered !== '')
213
225
  lines.push(rendered);
@@ -255,12 +267,12 @@ export function renderExpressNoWait(opts) {
255
267
  ];
256
268
  return withPathOutput(opts.squarePath, lines.join('\n'), { participantCount: opts.participantCount, held: opts.held });
257
269
  }
258
- export function renderPublicTail(events, lastN, now, viewer = '') {
270
+ export function renderPublicTail(squareState, events, lastN, now, viewer = '') {
259
271
  const publicItems = publicActs(events);
260
272
  const selected = lastN == null ? publicItems : publicItems.slice(-lastN);
261
273
  const preview = lastN == null ? undefined : BODY_PREVIEW_LENGTH;
262
274
  return selected
263
- .map((event) => renderAmbientEvent(event, viewer, { now, preview, actNumber: event.kind === 'say' ? sayNumberFor(events, event) : undefined }))
275
+ .map((event) => renderAmbientEvent(event, viewer, { now, preview, actNumber: event.kind === 'say' ? sayNumberFor(events, event) : undefined, squareState }))
264
276
  .filter(Boolean)
265
277
  .join('\n\n');
266
278
  }
@@ -297,7 +309,7 @@ export function renderActivitiesView(squareState, visible, lastN, full, squarePa
297
309
  };
298
310
  const rendered = mode === 'archive'
299
311
  ? renderEventCli(act, opts)
300
- : renderAmbientEvent(act, viewer, opts);
312
+ : renderAmbientEvent(act, viewer, { ...opts, squareState });
301
313
  if (rendered !== '')
302
314
  chunks.push(rendered);
303
315
  for (const participant of markers.get(act.index) ?? []) {
@@ -307,7 +319,7 @@ export function renderActivitiesView(squareState, visible, lastN, full, squarePa
307
319
  if (chunks.length === 0)
308
320
  return 'latest\n ○ no public activity in this view';
309
321
  if (previewLen !== undefined) {
310
- const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen && (mode === 'archive' || perceive(act, viewer) === 'full'));
322
+ const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen && (mode === 'archive' || perceiveActivity(squareState, act, viewer) === 'full'));
311
323
  if (truncated)
312
324
  chunks.push(`» ${commandPrefix(squarePath)} history --full`);
313
325
  }
@@ -440,6 +452,7 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
440
452
  .map((act) => renderAmbientEvent(act, opts.viewer, {
441
453
  actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
442
454
  mention: opts.mention,
455
+ squareState: opts.squareState,
443
456
  }))
444
457
  .filter(Boolean)
445
458
  .join('\n\n');
@@ -3,6 +3,8 @@ export declare function presentedPath(env?: NodeJS.ProcessEnv): string;
3
3
  export declare function hasPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
4
4
  /** True when any current participant owner has already received this attention. */
5
5
  export declare function hasPresentedAttention(squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
6
+ /** Record presentation by a transport that delivered the bounded attention body. */
7
+ export declare function recordPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, at?: number): void;
6
8
  /**
7
9
  * Serialize presentation only for the affected participants. Delivery runs
8
10
  * outside the short ledger-write lock, so unrelated owners never wait on an
package/dist/presented.js CHANGED
@@ -106,6 +106,26 @@ export function hasPresentedAttention(squarePath, name, actIndex, env = process.
106
106
  return false;
107
107
  return [...ownerIds].some((ownerId) => hasPresentedForOwner(ownerId, squarePath, name, actIndex, env, now));
108
108
  }
109
+ /** Record presentation by a transport that delivered the bounded attention body. */
110
+ export function recordPresentedForOwner(ownerId, squarePath, name, actIndex, env = process.env, at = Date.now()) {
111
+ const filePath = presentedPath(env);
112
+ const row = {
113
+ v: 2,
114
+ ts: at,
115
+ owner_id: ownerId,
116
+ square_path: canonicalSquarePath(squarePath),
117
+ name,
118
+ act_index: actIndex,
119
+ };
120
+ withAttentionLocks(filePath, [{ name, squarePath, notifications: [] }], () => {
121
+ withFileLockSync(`${filePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
122
+ const rows = readRows(filePath, at);
123
+ const known = new Set(rows.map(rowKey));
124
+ if (!known.has(rowKey(row)))
125
+ writeRows(filePath, [...rows, row]);
126
+ });
127
+ });
128
+ }
109
129
  /**
110
130
  * Serialize presentation only for the affected participants. Delivery runs
111
131
  * outside the short ledger-write lock, so unrelated owners never wait on an
package/dist/registry.js CHANGED
@@ -336,6 +336,9 @@ export function recordSessionDone(sessionId, name, squarePath, channel, env = pr
336
336
  return false;
337
337
  const at = Date.now();
338
338
  recordDone(sessionId, binding.name, binding.squarePath, { channel, at });
339
- retireOwnerWakeRoutes(binding.ownerId, { at, env });
339
+ const remaining = lookupParticipant(squarePath, binding.name, at)
340
+ .some((candidate) => candidate.ownerId === binding.ownerId);
341
+ if (!remaining)
342
+ retireOwnerWakeRoutes(binding.ownerId, { at, env });
340
343
  return true;
341
344
  }
package/dist/routes.d.ts CHANGED
@@ -17,6 +17,11 @@ export declare function retireOwnerWakeRoutes(ownerId: string, opts?: {
17
17
  at?: number;
18
18
  env?: NodeJS.ProcessEnv;
19
19
  }): void;
20
+ /** Retire one capability without touching sibling identities in the same claim. */
21
+ export declare function retireWakeRoute(route: Pick<WakeRoute, 'ownerId' | 'sessionId' | 'kind'>, opts?: {
22
+ at?: number;
23
+ env?: NodeJS.ProcessEnv;
24
+ }): void;
20
25
  export interface WakeRouteEvidence {
21
26
  sessionId: string;
22
27
  address: Record<string, string>;
package/dist/routes.js CHANGED
@@ -108,6 +108,23 @@ export function retireOwnerWakeRoutes(ownerId, opts = {}) {
108
108
  }, env);
109
109
  }
110
110
  }
111
+ /** Retire one capability without touching sibling identities in the same claim. */
112
+ export function retireWakeRoute(route, opts = {}) {
113
+ const at = opts.at ?? Date.now();
114
+ const env = opts.env ?? process.env;
115
+ const current = readWakeRoutes({ ownerId: route.ownerId, now: at, env })
116
+ .find((candidate) => candidate.sessionId === route.sessionId && candidate.kind === route.kind);
117
+ if (current === undefined)
118
+ return;
119
+ appendRouteRow({
120
+ v: 1,
121
+ ts: at,
122
+ op: 'retire',
123
+ owner_id: route.ownerId,
124
+ session_id: route.sessionId,
125
+ kind: route.kind,
126
+ }, env);
127
+ }
111
128
  /** Publication requires a non-blank session and a non-empty address of non-blank values. */
112
129
  function completeRouteEvidence(value) {
113
130
  if (value === undefined || value === null)
package/dist/runtime.d.ts CHANGED
@@ -1,5 +1,5 @@
1
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';
2
+ import { type StoredAct, type SquareState, type HoldState, type ActivityObservation, type ObservationState, type WatchLease } from './model.js';
3
3
  export declare const WATCH_HEARTBEAT_MS: number;
4
4
  export declare const SLEEP_MS: number;
5
5
  export declare const STALE_MS: number;
@@ -30,14 +30,13 @@ export declare function isCurrentlyJoined(acts: StoredAct[], name: string): bool
30
30
  export declare function lastJoinIndex(acts: StoredAct[], name: string): number | undefined;
31
31
  export declare function actStableIndex(act: StoredAct): number;
32
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
33
  export declare function currentHold(acts: StoredAct[]): HoldState;
36
34
  export declare function publicActs(acts: StoredAct[]): Array<Extract<StoredAct, {
37
35
  kind: 'say' | 'done';
38
36
  }>>;
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;
37
+ export declare function observationFor(squareState: SquareState, name: string, index: number): ActivityObservation | undefined;
38
+ export declare function recordObservation(squareState: SquareState, name: string, index: number, state: ObservationState, at?: number, ownerId?: string): boolean;
39
+ export declare function readCursor(squareState: SquareState, name: string): number;
41
40
  export declare function latestActIndex(acts: StoredAct[]): number;
42
41
  export declare function freshWatchLease(squareState: SquareState, name: string, at?: number): WatchLease | undefined;
43
42
  export declare function watchLease(squareState: SquareState, name: string): WatchLease | undefined;
package/dist/runtime.js CHANGED
@@ -126,11 +126,12 @@ export function actId(actOrIndex) {
126
126
  const index = typeof actOrIndex === 'number' ? actOrIndex : actStableIndex(actOrIndex);
127
127
  return formatActivityId(index);
128
128
  }
129
- export function getReadState(squareState, name) {
130
- return squareState.runtime.cursors[name] ?? Object.entries(squareState.runtime.cursors).find(([participant]) => sameName(participant, name))?.[1];
129
+ function observationRecipient(squareState, name) {
130
+ return resolveRosterName(squareState, name) ?? name;
131
131
  }
132
- export function readCursor(squareState, name) {
133
- return getReadState(squareState, name)?.consumedThroughIndex ?? -1;
132
+ function observationMap(squareState, name) {
133
+ const key = observationRecipient(squareState, name);
134
+ return squareState.runtime.observations?.[key] ?? {};
134
135
  }
135
136
  export function currentHold(acts) {
136
137
  const hold = fold(acts).hold;
@@ -142,25 +143,41 @@ export function publicActs(acts) {
142
143
  function canonicalRuntimeName(squareState, name) {
143
144
  return resolveRosterName(squareState, name) ?? name;
144
145
  }
145
- export function advanceCursor(squareState, name, index, updatedAt = Date.now()) {
146
- return touchPresenceCursor(squareState, name, updatedAt, index);
146
+ export function observationFor(squareState, name, index) {
147
+ return observationMap(squareState, name)[formatActivityId(index)];
147
148
  }
148
- export function touchPresenceCursor(squareState, name, at, consumedThroughIndex) {
149
- if (!Number.isFinite(at))
149
+ export function recordObservation(squareState, name, index, state, at = Date.now(), ownerId) {
150
+ if (!Number.isSafeInteger(index) || index < 0 || !Number.isFinite(at))
150
151
  return false;
151
- if (consumedThroughIndex !== undefined && (!Number.isInteger(consumedThroughIndex) || consumedThroughIndex < 0))
152
- return false;
153
- const key = canonicalRuntimeName(squareState, name);
154
- const current = getReadState(squareState, key);
155
- const nextIndex = consumedThroughIndex === undefined
156
- ? (current?.consumedThroughIndex ?? readCursor(squareState, key))
157
- : Math.max(current?.consumedThroughIndex ?? -1, consumedThroughIndex);
158
- const updatedAt = current === undefined ? at : Math.max(current.updatedAt, at);
159
- if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt)
152
+ const key = observationRecipient(squareState, name);
153
+ const id = formatActivityId(index);
154
+ const current = squareState.runtime.observations[key]?.[id];
155
+ if (current?.state === 'seen' || (current?.state === state && current.at >= at && current.ownerId === ownerId))
160
156
  return false;
161
- squareState.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt };
157
+ const next = {
158
+ state: state === 'seen' ? 'seen' : 'notified',
159
+ at: Math.max(current?.at ?? -Infinity, at),
160
+ ...(ownerId === undefined ? (current?.ownerId === undefined ? {} : { ownerId: current.ownerId }) : { ownerId }),
161
+ };
162
+ ((squareState.runtime.observations ??= {})[key] ??= {})[id] = next;
162
163
  return true;
163
164
  }
165
+ export function readCursor(squareState, name) {
166
+ const boundary = lastJoinIndex(squareState.acts, name) ?? -1;
167
+ let cursor = boundary;
168
+ for (const act of squareState.acts) {
169
+ if (act.index <= boundary || act.kind === 'read' || act.actor === undefined)
170
+ continue;
171
+ if (sameName(act.actor, name)) {
172
+ cursor = act.index;
173
+ continue;
174
+ }
175
+ if (observationFor(squareState, name, act.index)?.state !== 'seen')
176
+ break;
177
+ cursor = act.index;
178
+ }
179
+ return cursor;
180
+ }
164
181
  export function latestActIndex(acts) {
165
182
  return acts.reduce((max, act) => Math.max(max, act.index), -1);
166
183
  }
@@ -45,6 +45,16 @@ export type Act = {
45
45
  actor: Participant;
46
46
  at?: number;
47
47
  through: number;
48
+ } | {
49
+ kind: 'listen';
50
+ actor: Participant;
51
+ target: Participant;
52
+ at?: number;
53
+ } | {
54
+ kind: 'ignore';
55
+ actor: Participant;
56
+ target: Participant;
57
+ at?: number;
48
58
  };
49
59
  export interface SquareValidationOptions {
50
60
  hardCap?: number | null;
@@ -68,6 +78,7 @@ export interface FoldedSquareState {
68
78
  done: Participant[];
69
79
  throttleActivityAts: number[];
70
80
  bellSayAtsByActor: Map<string, number[]>;
81
+ listening: Map<string, Participant[]>;
71
82
  }
72
83
  export type ValidationResult = {
73
84
  ok: true;
@@ -105,6 +116,12 @@ export declare function audienceOf(say: {
105
116
  }): Audience;
106
117
  export declare function audienceIncludes(audience: Audience, name: string): boolean;
107
118
  export declare function resolveAudience(audience: Audience, candidateNames: readonly string[]): string[];
119
+ export declare function activeListeners(state: FoldedSquareState, sender: string): string[];
120
+ export declare function audienceBefore(acts: readonly Act[], say: Extract<Act, {
121
+ kind: 'say';
122
+ }>): string[];
123
+ export declare function listeningTo(state: FoldedSquareState, listener: string): string[];
124
+ export declare function isListening(state: FoldedSquareState, listener: string, sender: string): boolean;
108
125
  export declare function fold(acts: readonly Act[]): FoldedSquareState;
109
126
  export declare function validate(state: FoldedSquareState, act: Act, options?: SquareValidationOptions): ValidationResult;
110
127
  export declare function perceive(act: Act, viewer: Participant | string): Perception;
@@ -20,7 +20,7 @@ function sameName(a, b) {
20
20
  }
21
21
  export function extractMentions(body) {
22
22
  const matches = [];
23
- const re = /@([\p{L}\p{N}_-]+)/gu;
23
+ const re = /@([\p{L}\p{N}_-]+(?:\/[\p{L}\p{N}_-]+)*)/gu;
24
24
  let match;
25
25
  while ((match = re.exec(body)) !== null)
26
26
  matches.push(match[1]);
@@ -57,6 +57,30 @@ export function resolveAudience(audience, candidateNames) {
57
57
  }
58
58
  return resolved;
59
59
  }
60
+ export function activeListeners(state, sender) {
61
+ return state.participants
62
+ .filter((participant) => participant.joined && (state.listening.get(nameKey(participant.name)) ?? []).some((target) => sameName(target, sender)))
63
+ .map((participant) => participant.name);
64
+ }
65
+ export function audienceBefore(acts, say) {
66
+ const position = acts.findIndex((act) => act === say || ('index' in act && 'index' in say && act.index === say.index));
67
+ const before = fold(position < 0 ? acts : acts.slice(0, position));
68
+ const mentionTargets = resolveAudience(audienceOf(say), before.joined);
69
+ const listeners = activeListeners(before, say.actor);
70
+ const recipients = [];
71
+ for (const name of [...mentionTargets, ...listeners]) {
72
+ if (sameName(name, say.actor) || recipients.some((existing) => sameName(existing, name)))
73
+ continue;
74
+ recipients.push(name);
75
+ }
76
+ return recipients;
77
+ }
78
+ export function listeningTo(state, listener) {
79
+ return [...(state.listening.get(nameKey(listener)) ?? [])];
80
+ }
81
+ export function isListening(state, listener, sender) {
82
+ return listeningTo(state, listener).some((target) => sameName(target, sender));
83
+ }
60
84
  function actorOf(act) {
61
85
  if ('actor' in act && typeof act.actor === 'string')
62
86
  return act.actor;
@@ -115,6 +139,7 @@ export function fold(acts) {
115
139
  done: [],
116
140
  throttleActivityAts: [],
117
141
  bellSayAtsByActor: new Map(),
142
+ listening: new Map(),
118
143
  };
119
144
  for (const act of acts) {
120
145
  const actor = actorOf(act);
@@ -132,8 +157,26 @@ export function fold(acts) {
132
157
  snapshot.joined = false;
133
158
  snapshot.done = true;
134
159
  snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
160
+ state.listening.delete(nameKey(snapshot.name));
135
161
  }
136
162
  break;
163
+ case 'listen': {
164
+ const key = nameKey(act.actor);
165
+ const targets = state.listening.get(key) ?? [];
166
+ if (!targets.some((target) => sameName(target, act.target)))
167
+ targets.push(act.target);
168
+ state.listening.set(key, targets);
169
+ break;
170
+ }
171
+ case 'ignore': {
172
+ const key = nameKey(act.actor);
173
+ const targets = (state.listening.get(key) ?? []).filter((target) => !sameName(target, act.target));
174
+ if (targets.length === 0)
175
+ state.listening.delete(key);
176
+ else
177
+ state.listening.set(key, targets);
178
+ break;
179
+ }
137
180
  case 'say':
138
181
  if (snapshot !== undefined) {
139
182
  snapshot.activityCount += 1;
@@ -178,6 +221,13 @@ export function validate(state, act, options = {}) {
178
221
  if (current?.joined !== true)
179
222
  return { ok: false, reason: 'not_joined' };
180
223
  return { ok: true };
224
+ case 'listen':
225
+ case 'ignore':
226
+ if (current?.done)
227
+ return { ok: false, reason: 'done' };
228
+ if (current?.joined !== true)
229
+ return { ok: false, reason: 'not_joined' };
230
+ return { ok: true };
181
231
  case 'say': {
182
232
  if (current?.done)
183
233
  return { ok: false, reason: 'done' };
@@ -2,10 +2,11 @@ import type { ActivityId, Perception, Reach } from './square-core.js';
2
2
  export interface Activity {
3
3
  readonly id: ActivityId;
4
4
  readonly at: number;
5
- readonly kind: 'join' | 'say' | 'done' | 'hold' | 'resume';
5
+ readonly kind: 'join' | 'say' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore';
6
6
  readonly actor: string;
7
7
  readonly body?: string;
8
8
  readonly mentions: readonly string[];
9
+ readonly target?: string;
9
10
  readonly reply?: ActivityId;
10
11
  }
11
12
  export interface WakeNotifier {
@@ -22,6 +23,9 @@ export interface ExpressOptions {
22
23
  export interface ExpressResult {
23
24
  readonly activity: Activity;
24
25
  }
26
+ export interface ListenerChangeResult {
27
+ readonly activity: Activity | null;
28
+ }
25
29
  export interface CatchOptions {
26
30
  readonly idle?: number;
27
31
  readonly from?: readonly string[];
@@ -46,6 +50,7 @@ export interface ParticipantStatus {
46
50
  readonly state: 'joined' | 'done';
47
51
  readonly consumedThrough: ActivityId | null;
48
52
  readonly watching: boolean;
53
+ readonly listening: readonly string[];
49
54
  }
50
55
  export interface SquareSnapshot {
51
56
  readonly context: string;
@@ -78,6 +83,9 @@ export interface SquareBuildInput extends SquareSource {
78
83
  export interface Participant {
79
84
  readonly name: string;
80
85
  express(body: string, options?: ExpressOptions): Promise<ExpressResult>;
86
+ listen(target: string): Promise<ListenerChangeResult>;
87
+ ignore(target: string): Promise<ListenerChangeResult>;
88
+ listening(): Promise<readonly string[]>;
81
89
  catch(options?: CatchOptions): Promise<CatchResult>;
82
90
  history(query?: HistoryQuery): Promise<PerceivedActivity[]>;
83
91
  hold(reason?: string): Promise<ExpressResult>;
@@ -1,3 +1,4 @@
1
+ import { type OpenSquare } from './open-square.js';
1
2
  import type { Activity, HistoryQuery, ParticipantStatus, SquareSnapshot } from './square-facade.js';
2
3
  import type { Participant, SquareAtInput, SquareBuildInput } from './square-facade.js';
3
4
  export declare class Square {
@@ -8,11 +9,18 @@ export declare class Square {
8
9
  static build(input: SquareBuildInput): Promise<Square>;
9
10
  static inMemory(input: Omit<SquareBuildInput, 'path'>): Square;
10
11
  join(name: string): Promise<Participant>;
12
+ implicitJoin(name: string): Promise<{
13
+ readonly state: 'joined' | 'active' | 'done';
14
+ readonly participant?: Participant;
15
+ }>;
11
16
  participants(): Promise<ParticipantStatus[]>;
12
17
  snapshot(): Promise<SquareSnapshot>;
13
18
  history(query?: HistoryQuery): Promise<Activity[]>;
19
+ recognize(env: NodeJS.ProcessEnv): Promise<Participant | null>;
14
20
  close(): Promise<void>;
15
21
  }
22
+ export declare function markBoundarySeen(squarePath: string, name: string, ownerId: string | undefined, actIndexes: readonly number[], at?: number): Promise<void>;
23
+ export declare function markNotificationNotified(square: OpenSquare, name: string, actIndex: number, ownerId: string | undefined, at?: number): Promise<void>;
16
24
  export declare function openParticipant(input: SquareAtInput, name: string): Promise<{
17
25
  readonly participant: Participant;
18
26
  close(): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import { closeOpenSquare } from './open-square.js';
2
2
  import { buildMemorySquare, buildSquare, openSquare } from './square-file-adapter.js';
3
- import { done, express, hold, join, resume } from './landing.js';
4
- import { catchUp } from './presence.js';
5
- import { history, participantHistory, participants, resolveParticipant, snapshot } from './views.js';
3
+ import { done, express, hold, ignore, implicitJoin, join, listen, listening, resume } from './landing.js';
4
+ import { catchUp, markBoundarySeen as recordBoundarySeen, markNotificationNotified as recordNotificationNotified } from './presence.js';
5
+ import { currentParticipant, history, participantHistory, participants, resolveParticipant, snapshot } from './views.js';
6
+ import { localParticipantName } from './registry.js';
6
7
  class ParticipantHandle {
7
8
  name;
8
9
  square;
@@ -13,6 +14,15 @@ class ParticipantHandle {
13
14
  express(body, options) {
14
15
  return express(this.square, this.name, body, options);
15
16
  }
17
+ listen(target) {
18
+ return listen(this.square, this.name, target);
19
+ }
20
+ ignore(target) {
21
+ return ignore(this.square, this.name, target);
22
+ }
23
+ listening() {
24
+ return listening(this.square, this.name);
25
+ }
16
26
  catch(options) {
17
27
  return catchUp(this.square, this.name, options);
18
28
  }
@@ -49,11 +59,30 @@ export class Square {
49
59
  const joined = await join(this.square, name);
50
60
  return new ParticipantHandle(joined.name, this.square);
51
61
  }
62
+ async implicitJoin(name) {
63
+ const joined = await implicitJoin(this.square, name);
64
+ return joined.state === 'done'
65
+ ? { state: joined.state }
66
+ : { state: joined.state, participant: new ParticipantHandle(joined.name, this.square) };
67
+ }
52
68
  participants() { return participants(this.square); }
53
69
  snapshot() { return snapshot(this.square); }
54
70
  history(query) { return history(this.square, query); }
71
+ async recognize(env) {
72
+ const registered = localParticipantName(this.square.location, env);
73
+ if (registered === undefined)
74
+ return null;
75
+ const canonicalName = await currentParticipant(this.square, registered);
76
+ return canonicalName === undefined ? null : new ParticipantHandle(canonicalName, this.square);
77
+ }
55
78
  close() { return closeOpenSquare(this.square); }
56
79
  }
80
+ export function markBoundarySeen(squarePath, name, ownerId, actIndexes, at) {
81
+ return recordBoundarySeen(squarePath, name, ownerId, actIndexes, at);
82
+ }
83
+ export function markNotificationNotified(square, name, actIndex, ownerId, at) {
84
+ return recordNotificationNotified(square, name, actIndex, ownerId, at);
85
+ }
57
86
  export async function openParticipant(input, name) {
58
87
  const square = await openSquare(input.path, input);
59
88
  try {