@astrosheep/square 0.3.23 → 0.3.25

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -5,10 +5,10 @@ export interface ActivityFeedFilter {
5
5
  }
6
6
  export declare function actDelta(acts: StoredAct[], cursor: number): StoredAct[];
7
7
  /** Visible activities after the participant's derived continuous-seen prefix. */
8
- export declare function deliveryDelta(squareState: SquareState, name: string): StoredAct[];
8
+ export declare function deliveryDelta(squareState: SquareState, name: string, delivery?: import("./delivery.js").DeliveryModel): StoredAct[];
9
9
  export declare function peerRoomChanges(delta: StoredAct[], name: string): RoomChangeAct[];
10
10
  export declare function peerPublicActs(delta: StoredAct[], name: string): PublicAct[];
11
- export declare function directedPeerSays(squareState: SquareState, delta: StoredAct[], name: string): Extract<StoredAct, {
11
+ export declare function directedPeerSays(squareState: SquareState, delta: StoredAct[], name: string, delivery?: import("./delivery.js").DeliveryModel): Extract<StoredAct, {
12
12
  kind: 'say';
13
13
  }>[];
14
14
  export declare function matchesFeedFilter(act: StoredAct, filter: ActivityFeedFilter, recipients?: readonly string[]): boolean;
@@ -1,13 +1,11 @@
1
1
  import { sameName } from './model.js';
2
- import { landedAudienceIncludes } from './square-core.js';
3
- import { readCursor } from './runtime.js';
4
- import { matchesCatchFilter } from './delivery.js';
2
+ import { deriveDeliveryModel, matchesCatchFilter } from './delivery.js';
5
3
  export function actDelta(acts, cursor) {
6
4
  return acts.filter((act) => act.index > cursor);
7
5
  }
8
6
  /** Visible activities after the participant's derived continuous-seen prefix. */
9
- export function deliveryDelta(squareState, name) {
10
- return actDelta(squareState.acts, readCursor(squareState, name));
7
+ export function deliveryDelta(squareState, name, delivery = deriveDeliveryModel(squareState)) {
8
+ return actDelta(squareState.acts, delivery.cursorFor(name));
11
9
  }
12
10
  export function peerRoomChanges(delta, name) {
13
11
  return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && act.kind !== 'say' && act.kind !== 'read');
@@ -15,8 +13,8 @@ export function peerRoomChanges(delta, name) {
15
13
  export function peerPublicActs(delta, name) {
16
14
  return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && (act.kind === 'say' || act.kind === 'done'));
17
15
  }
18
- export function directedPeerSays(squareState, delta, name) {
19
- return delta.filter((act) => landedAudienceIncludes(squareState.acts, act, name));
16
+ export function directedPeerSays(squareState, delta, name, delivery = deriveDeliveryModel(squareState)) {
17
+ return delta.filter((act) => delivery.directedTo(act, name));
20
18
  }
21
19
  function matchesParticipants(act, participants) {
22
20
  return participants === undefined || (act.actor !== undefined && participants.some((participant) => sameName(participant, act.actor)));
@@ -38,7 +38,7 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
38
38
  return undefined;
39
39
  const channel = provider === 'claude' ? 'claude-code' : provider;
40
40
  recordSessionJoin(sessionId, name, squarePath, channel, { ...env, [providerEnv[provider]]: sessionId });
41
- return `${name} came back`;
41
+ return undefined;
42
42
  }
43
43
  finally {
44
44
  await square.close();
@@ -4,7 +4,7 @@ import { sessionInbox } from '../inbox.js';
4
4
  import { sweepPendingNotifications } from '../notifications.js';
5
5
  import { cmdListSquares } from '../list.js';
6
6
  import { parseActivityId, sameName } from '../model.js';
7
- import { commandPrefix, participantCommandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
7
+ import { commandPrefix, participantCommandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, renderPresenceAnchor, withPathOutput, } from '../presentation.js';
8
8
  import { actId, nowMs } from '../runtime.js';
9
9
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
10
10
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
@@ -13,6 +13,7 @@ import { openSquare } from '../square-file-adapter.js';
13
13
  import { closeOpenSquare } from '../open-square.js';
14
14
  import { historyPresentation, participantsPresentation, statusPresentation } from '../views.js';
15
15
  import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdinSync, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
16
+ const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
16
17
  export const listCommand = {
17
18
  parse: (argv) => argv,
18
19
  async execute(argv, context) {
@@ -95,8 +96,9 @@ export const catchCommand = {
95
96
  },
96
97
  async execute(intent, context) {
97
98
  const squarePath = requireSquarePath(context);
98
- await cmdWatch(squarePath, requireParticipant(context.name), intent);
99
- await sweepPendingNotifications(squarePath);
99
+ const caught = await cmdWatch(squarePath, requireParticipant(context.name), intent);
100
+ if (caught !== false)
101
+ await sweepPendingNotifications(squarePath);
100
102
  },
101
103
  present: () => { },
102
104
  };
@@ -293,8 +295,9 @@ function renderHistoryProjection(projection, visible, full, squarePath, viewer,
293
295
  : renderAmbientEvent(activity, viewer, options);
294
296
  if (rendered !== '')
295
297
  chunks.push(rendered);
296
- for (const name of projection.presenceAnchors[activity.index] ?? [])
297
- chunks.push(`→ ${participantIdentity(name)} was here`);
298
+ const participants = projection.presenceAnchors[activity.index];
299
+ if (participants !== undefined)
300
+ chunks.push(renderPresenceAnchor(participants));
298
301
  }
299
302
  if (chunks.length === 0)
300
303
  return 'latest\n ○ no public activity in this view';
@@ -381,7 +384,7 @@ export const statusCommand = {
381
384
  return aViewer ? -1 : 1;
382
385
  return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
383
386
  });
384
- const people = active.length === 0 ? [' ○ nobody in the square'] : active.map((participant) => {
387
+ const people = active.length === 0 ? [' ○ nobody in the square'] : active.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
385
388
  const glyph = participant.presence === 'watching'
386
389
  ? '◎'
387
390
  : participant.activityCount > 0 ? '●' : '○';
@@ -400,6 +403,10 @@ export const statusCommand = {
400
403
  : 'caught up';
401
404
  return ` ${glyph} ${participantIdentity(participant.name)} · ${summary}${attention === '' ? '' : ` · ${attention}`}`;
402
405
  });
406
+ if (active.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
407
+ people.push(` ○ … ${active.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more participants`);
408
+ people.push(`» ${commandPrefix(squarePath)} participants`);
409
+ }
403
410
  const cap = result.hardCap === null ? 'unlimited' : String(result.hardCap);
404
411
  const hold = result.holdActive
405
412
  ? `· ${result.holdActor === undefined ? 'someone' : participantIdentity(result.holdActor)} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''} · ${result.holdAt === undefined
@@ -1,6 +1,7 @@
1
1
  import { type ActivitiesOptions, type Act, type StoredAct, type SquareState, type Reach, type HardCap } from './model.js';
2
2
  import { directedPeerSays, peerRoomChanges } from './activity-feed.js';
3
3
  import { type Perception } from './square-core.js';
4
+ import { type DeliveryModel } from './delivery.js';
4
5
  export interface UnreadActivitySummary {
5
6
  name: string;
6
7
  count: number;
@@ -106,6 +107,6 @@ export interface StatusResult {
106
107
  latestAct: StoredAct | undefined;
107
108
  now: number;
108
109
  }
109
- export declare function coreStatus(squareState: SquareState, now: number): StatusResult;
110
- export declare function coreParticipants(squareState: SquareState, now: number): ParticipantStatus[];
111
- export declare function coreActivities(squareState: SquareState, opts: ActivitiesOptions): StoredAct[];
110
+ export declare function coreStatus(squareState: SquareState, now: number, delivery?: DeliveryModel): StatusResult;
111
+ export declare function coreParticipants(squareState: SquareState, now: number, delivery?: DeliveryModel): ParticipantStatus[];
112
+ export declare function coreActivities(squareState: SquareState, opts: ActivitiesOptions, suppliedDelivery?: DeliveryModel): StoredAct[];
package/dist/decisions.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { SquareError, sameName, validateName, } from './model.js';
2
2
  import { participantIdentity } from './participant-identity.js';
3
- import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, readCursor, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
3
+ import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
4
4
  import { actDelta, directedPeerSays } from './activity-feed.js';
5
5
  import { formatActivityId, isIgnored, isListening, listeningTo, validate } from './square-core.js';
6
- import { deriveDeliveryModel, perceiveActivity } from './delivery.js';
6
+ import { deriveDeliveryModel } from './delivery.js';
7
7
  import { compileSearchPattern } from './search.js';
8
8
  export function resolveKnownName(squareState, name) {
9
9
  validateName(name);
@@ -100,8 +100,9 @@ export function decideAct(squareState, input) {
100
100
  if (result.reason === 'not_joined')
101
101
  throw new SquareError('not_joined', `${name} has not joined this square`);
102
102
  }
103
- const delta = actDelta(squareState.acts, readCursor(squareState, name));
104
- const unreadPublic = directedPeerSays(squareState, delta, name);
103
+ const delivery = deriveDeliveryModel(squareState);
104
+ const delta = actDelta(squareState.acts, delivery.cursorFor(name));
105
+ const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
105
106
  const unreadRoomChanges = [];
106
107
  const sayCountByActor = new Map();
107
108
  const unreadByParticipant = new Map();
@@ -119,7 +120,7 @@ export function decideAct(squareState, input) {
119
120
  latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
120
121
  previews: [
121
122
  ...(currentSummary?.previews ?? []),
122
- { number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: perceiveActivity(squareState, item, name) },
123
+ { number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
123
124
  ].slice(-UNREAD_PREVIEW_LIMIT),
124
125
  });
125
126
  }
@@ -189,10 +190,10 @@ export function coreResume(squareState, actor, now) {
189
190
  requireStanding(squareState, act);
190
191
  return act;
191
192
  }
192
- function presenceFor(squareState, snapshot, name, now) {
193
+ function presenceFor(squareState, snapshot, name, now, delivery) {
193
194
  if (snapshot?.done)
194
195
  return { state: 'done', lastAt: snapshot.lastActiveAt };
195
- const cursorAt = squareState.acts.findLast((act) => act.index <= readCursor(squareState, name))?.at;
196
+ const cursorAt = squareState.acts.findLast((act) => act.index <= delivery.cursorFor(name))?.at;
196
197
  const lease = freshWatchLease(squareState, name, now);
197
198
  if (lease !== undefined)
198
199
  return { state: 'watching', lastAt: cursorAt ?? lease.heartbeatAt };
@@ -201,19 +202,19 @@ function presenceFor(squareState, snapshot, name, now) {
201
202
  ? { state: 'never-joined', lastAt: undefined }
202
203
  : { state: 'active', lastAt };
203
204
  }
204
- function buildParticipantStatuses(squareState, now, state = foldedState(squareState)) {
205
- const delivery = deriveDeliveryModel(squareState);
205
+ function buildParticipantStatuses(squareState, now, state = foldedState(squareState), suppliedDelivery) {
206
+ const delivery = suppliedDelivery ?? deriveDeliveryModel(squareState);
206
207
  return state.participants.map((snapshot) => {
207
208
  const participant = snapshot.name;
208
- const presence = presenceFor(squareState, snapshot, participant, now);
209
+ const presence = presenceFor(squareState, snapshot, participant, now, delivery);
209
210
  const participantStatus = snapshot?.done ? 'done' : snapshot?.joined ? 'active' : 'not joined';
210
- const consumedThrough = readCursor(squareState, participant);
211
+ const consumedThrough = delivery.cursorFor(participant);
211
212
  let unreadActivityCount = 0;
212
213
  if (snapshot?.joined) {
213
214
  for (const act of squareState.acts) {
214
215
  if (actStableIndex(act) <= consumedThrough)
215
216
  continue;
216
- if (directedPeerSays(squareState, [act], participant).length > 0)
217
+ if (directedPeerSays(squareState, [act], participant, delivery).length > 0)
217
218
  unreadActivityCount++;
218
219
  }
219
220
  }
@@ -230,7 +231,7 @@ function buildParticipantStatuses(squareState, now, state = foldedState(squareSt
230
231
  };
231
232
  });
232
233
  }
233
- export function coreStatus(squareState, now) {
234
+ export function coreStatus(squareState, now, delivery) {
234
235
  const state = foldedState(squareState);
235
236
  const latestAct = publicActs(squareState.acts).at(-1);
236
237
  return {
@@ -242,19 +243,21 @@ export function coreStatus(squareState, now) {
242
243
  holdReason: state.hold.reason,
243
244
  holdActor: state.hold.actor,
244
245
  holdAt: state.hold.at,
245
- participants: buildParticipantStatuses(squareState, now, state),
246
+ participants: buildParticipantStatuses(squareState, now, state, delivery),
246
247
  latestAct,
247
248
  now,
248
249
  };
249
250
  }
250
- export function coreParticipants(squareState, now) {
251
- return buildParticipantStatuses(squareState, now);
251
+ export function coreParticipants(squareState, now, delivery) {
252
+ return buildParticipantStatuses(squareState, now, undefined, delivery);
252
253
  }
253
- export function coreActivities(squareState, opts) {
254
+ export function coreActivities(squareState, opts, suppliedDelivery) {
254
255
  const participants = opts.participants ?? [];
255
256
  const canonicalParticipants = participants.map((participant) => resolveKnownName(squareState, participant));
256
257
  const viewer = opts.viewer !== undefined ? resolveKnownName(squareState, opts.viewer) : undefined;
257
258
  let acts = [...squareState.acts];
259
+ let delivery = suppliedDelivery;
260
+ const projected = () => delivery ??= deriveDeliveryModel(squareState);
258
261
  // --at establishes one or more context windows first; other filters AND inside their union.
259
262
  if (opts.atIndexes !== undefined && opts.atIndexes.length > 0) {
260
263
  const before = opts.beforeContext ?? 0;
@@ -280,13 +283,12 @@ export function coreActivities(squareState, opts) {
280
283
  acts = acts.filter((act) => act.at > opts.after);
281
284
  if (opts.mention != null) {
282
285
  const mention = resolveKnownName(squareState, opts.mention);
283
- const delivery = deriveDeliveryModel(squareState);
284
- acts = acts.filter((act) => act.kind === 'say' && delivery.plan(act).some((item) => sameName(item.recipient, mention)));
286
+ acts = acts.filter((act) => act.kind === 'say' && projected().plan(act).some((item) => sameName(item.recipient, mention)));
285
287
  }
286
288
  if (opts.pending) {
287
289
  if (viewer === undefined)
288
290
  return [];
289
- const pendingIndexes = new Set(deriveDeliveryModel(squareState).pendingFor(viewer).map((notification) => notification.item.index));
291
+ const pendingIndexes = new Set(projected().pendingFor(viewer).map((notification) => notification.item.index));
290
292
  acts = acts.filter((act) => pendingIndexes.has(act.index));
291
293
  }
292
294
  const search = opts.grep !== undefined ? { pattern: opts.grep, fixed: false } : opts.fixed !== undefined ? { pattern: opts.fixed, fixed: true } : undefined;
@@ -57,6 +57,14 @@ export interface CatchFilterShape {
57
57
  export interface DeliveryModel {
58
58
  plan(item: StoredAct): PlannedNotification[];
59
59
  pendingFor(recipient: string): PlannedNotification[];
60
+ directedTo(item: StoredAct, recipient: string): boolean;
61
+ perceive(item: StoredAct, viewer: string): Perception;
62
+ cursorFor(recipient: string): number;
63
+ isSeen(recipient: string, actOrIndex: StoredAct | number): boolean;
64
+ knownParticipant(name: string): string | undefined;
65
+ participants(): readonly string[];
66
+ joinedRecipients(): readonly string[];
67
+ readonly replayedActivityCount: number;
60
68
  }
61
69
  export declare function isActivitySeen(squareState: SquareState, name: string, actOrIndex: StoredAct | number): boolean;
62
70
  /**
@@ -64,10 +72,10 @@ export declare function isActivitySeen(squareState: SquareState, name: string, a
64
72
  * All consumers share these targets instead of reinterpreting artifact text or cursor state.
65
73
  */
66
74
  export declare function deriveDeliveryModel(squareState: SquareState): DeliveryModel;
67
- export declare function planActNotifications(squareState: SquareState, item: StoredAct): PlannedNotification[];
68
- export declare function perceiveActivity(squareState: SquareState, item: StoredAct, viewer: string): Perception;
75
+ export declare function planActNotifications(squareState: SquareState, item: StoredAct, delivery?: DeliveryModel): PlannedNotification[];
76
+ export declare function perceiveActivity(squareState: SquareState, item: StoredAct, viewer: string, delivery?: DeliveryModel): Perception;
69
77
  /** Mark only the notifications selected by the canonical catch projection as fully seen. */
70
- export declare function markSeenNotifications(squareState: SquareState, recipient: string, delivered: StoredAct[], at?: number): boolean;
78
+ export declare function markSeenNotifications(squareState: SquareState, recipient: string, delivered: StoredAct[], at?: number, delivery?: DeliveryModel): boolean;
71
79
  /** Canonical say-activity filter shared by catch selection and hook ownership. */
72
80
  export declare function matchesCatchFilter(activity: CatchFilterShape, filter: WatchLeaseFilter): boolean;
73
81
  /** True only when the live catch's own filters would deliver this notification. */
package/dist/delivery.js CHANGED
@@ -1,24 +1,29 @@
1
1
  import { findParticipantName, sameName, } from './model.js';
2
- import { audienceBefore, audienceOf, formatActivityId } from './square-core.js';
3
- import { isCurrentlyJoined, lastJoinIndex, matchesMentionTarget, observationFor, recordObservation, resolveRosterName, rosterNames } from './runtime.js';
2
+ import { audienceOf, formatActivityId, replayLandedAudiences } from './square-core.js';
3
+ import { matchesMentionTarget, readCursor, recordObservation } from './runtime.js';
4
4
  export function notificationMessageId(squarePath, actIndex) {
5
5
  return `square:${squarePath}#${formatActivityId(actIndex)}`;
6
6
  }
7
- function canonicalRecipient(squareState, name) {
8
- return resolveRosterName(squareState, name) ?? name;
9
- }
10
7
  export function isActivitySeen(squareState, name, actOrIndex) {
11
- const index = typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index;
12
- return observationFor(squareState, canonicalRecipient(squareState, name), index)?.state === 'seen';
8
+ return deriveDeliveryModel(squareState).isSeen(name, actOrIndex);
13
9
  }
14
10
  /**
15
11
  * Derive delivery behavior once from the decoded Square state.
16
12
  * All consumers share these targets instead of reinterpreting artifact text or cursor state.
17
13
  */
18
14
  export function deriveDeliveryModel(squareState) {
19
- const roster = rosterNames(squareState).filter((name) => isCurrentlyJoined(squareState.acts, name));
15
+ const landed = replayLandedAudiences(squareState.acts);
16
+ const roster = [...landed.joined];
20
17
  const plannedByIndex = new Map();
21
18
  let pendingByRecipient;
19
+ function canonicalRecipient(name) {
20
+ return landed.resolveParticipant(name) ?? name;
21
+ }
22
+ function isSeen(requestedRecipient, actOrIndex) {
23
+ const recipient = canonicalRecipient(requestedRecipient);
24
+ const index = typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index;
25
+ return squareState.runtime.observations?.[recipient]?.[formatActivityId(index)]?.state === 'seen';
26
+ }
22
27
  function plan(item) {
23
28
  if (item.kind !== 'say')
24
29
  return [];
@@ -27,7 +32,7 @@ export function deriveDeliveryModel(squareState) {
27
32
  return [...cached];
28
33
  const sayItem = item;
29
34
  const audience = audienceOf(sayItem);
30
- const recipients = audienceBefore(squareState.acts, sayItem);
35
+ const recipients = landed.recipientsFor(sayItem);
31
36
  const planned = recipients.map((recipient) => {
32
37
  const route = audience.kind === 'bell'
33
38
  ? 'bell'
@@ -43,7 +48,7 @@ export function deriveDeliveryModel(squareState) {
43
48
  return [];
44
49
  if (pendingByRecipient === undefined) {
45
50
  pendingByRecipient = new Map(roster.map((name) => [name, []]));
46
- const joinedAfter = new Map(roster.map((name) => [name, lastJoinIndex(squareState.acts, name)]));
51
+ const joinedAfter = new Map(roster.map((name) => [name, landed.lastJoinIndex(name)]));
47
52
  for (const act of squareState.acts) {
48
53
  if (act.kind !== 'say')
49
54
  continue;
@@ -51,7 +56,7 @@ export function deriveDeliveryModel(squareState) {
51
56
  const joinedAt = joinedAfter.get(planned.recipient);
52
57
  if (joinedAt === undefined || act.index <= joinedAt)
53
58
  continue;
54
- if (isActivitySeen(squareState, planned.recipient, act.index))
59
+ if (isSeen(planned.recipient, act.index))
55
60
  continue;
56
61
  pendingByRecipient.get(planned.recipient)?.push(planned);
57
62
  }
@@ -59,23 +64,38 @@ export function deriveDeliveryModel(squareState) {
59
64
  }
60
65
  return [...(pendingByRecipient.get(recipient) ?? [])];
61
66
  }
62
- return { plan, pendingFor };
67
+ function directedTo(item, recipient) {
68
+ return item.kind === 'say' && landed.includes(item, recipient);
69
+ }
70
+ function perceive(item, viewer) {
71
+ if (item.kind !== 'say' || sameName(item.actor, viewer))
72
+ return 'full';
73
+ return directedTo(item, viewer) ? 'full' : 'presence';
74
+ }
75
+ return {
76
+ plan,
77
+ pendingFor,
78
+ directedTo,
79
+ perceive,
80
+ cursorFor: (recipient) => readCursor(squareState, recipient, landed),
81
+ isSeen,
82
+ knownParticipant: (name) => landed.resolveParticipant(name),
83
+ participants: () => landed.participants,
84
+ joinedRecipients: () => roster,
85
+ replayedActivityCount: landed.replayedActivityCount,
86
+ };
63
87
  }
64
- export function planActNotifications(squareState, item) {
65
- return deriveDeliveryModel(squareState).plan(item);
88
+ export function planActNotifications(squareState, item, delivery = deriveDeliveryModel(squareState)) {
89
+ return delivery.plan(item);
66
90
  }
67
- export function perceiveActivity(squareState, item, viewer) {
68
- if (item.kind !== 'say' || sameName(item.actor, viewer))
69
- return 'full';
70
- return deriveDeliveryModel(squareState).plan(item).some((planned) => sameName(planned.recipient, viewer))
71
- ? 'full'
72
- : 'presence';
91
+ export function perceiveActivity(squareState, item, viewer, delivery = deriveDeliveryModel(squareState)) {
92
+ return delivery.perceive(item, viewer);
73
93
  }
74
94
  /** Mark only the notifications selected by the canonical catch projection as fully seen. */
75
- export function markSeenNotifications(squareState, recipient, delivered, at = Date.now()) {
95
+ export function markSeenNotifications(squareState, recipient, delivered, at = Date.now(), delivery = deriveDeliveryModel(squareState)) {
76
96
  const deliveredIndexes = new Set(delivered.map((item) => item.index));
77
97
  let changed = false;
78
- for (const notification of deriveDeliveryModel(squareState).pendingFor(recipient)) {
98
+ for (const notification of delivery.pendingFor(recipient)) {
79
99
  if (!deliveredIndexes.has(notification.item.index))
80
100
  continue;
81
101
  changed = recordObservation(squareState, notification.recipient, notification.item.index, 'seen', at) || changed;
@@ -1,4 +1,5 @@
1
- import { planActNotifications, type WakeAdapter } from './delivery.js';
1
+ import { planActNotifications, type DeliveryModel, type WakeAdapter } from './delivery.js';
2
+ import { type SquareState } from './model.js';
2
3
  import { matchesMentionTarget } from './runtime.js';
3
4
  import { type ActivityId } from './square-core.js';
4
5
  import type { WakeNotifier } from './square-facade.js';
@@ -26,5 +27,7 @@ export interface SweepPendingNotificationsOptions extends WorkerLaunchOptions {
26
27
  now?: number;
27
28
  limit?: number;
28
29
  }
30
+ /** Select sweep candidates from one frozen snapshot and one delivery replay. */
31
+ export declare function pendingNotificationSweepFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, limit: number, deriveDelivery?: (snapshot: SquareState) => DeliveryModel): number[];
29
32
  /** Reconsider old pending attention at a bounded action boundary using the existing worker. */
30
33
  export declare function sweepPendingNotifications(squarePath: string, opts?: SweepPendingNotificationsOptions): Promise<number[]>;
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { setTimeout as sleep } from 'node:timers/promises';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { leaseOwnsNotification, planActNotifications, } from './delivery.js';
5
+ import { deriveDeliveryModel, leaseOwnsNotification, planActNotifications, } from './delivery.js';
6
6
  import { sessionInbox } from './inbox.js';
7
7
  import { hasPresentedAttention } from './presented.js';
8
8
  import { SquareError } from './model.js';
@@ -14,10 +14,10 @@ import { retireWakeRoute } from './routes.js';
14
14
  import { openSquare } from './square-file-adapter.js';
15
15
  import { markNotificationNotified } from './square-wiring.js';
16
16
  import { closeOpenSquare } from './open-square.js';
17
- import { entryPresentation, notificationDelivered, notificationForAct, pendingDeliveries, resolveParticipant } from './views.js';
17
+ import { entryPresentation, notificationDelivered, notificationForAct, pendingDeliveriesFromState, resolveParticipant } from './views.js';
18
18
  import { claimNotificationLease, releaseNotificationLease, transitionNotificationLease } from './wakes.js';
19
19
  import { nextWakeAttemptNumber, recordRecoveredUnknown, recordWakeAttempt, } from './wake-attempts.js';
20
- import { wakeEvidence, wakeIsEligible } from './wake-evidence.js';
20
+ import { wakeEvidence, wakeEvidenceProjectionFromState, wakeIsEligible } from './wake-evidence.js';
21
21
  import { WakePort } from './wake-port.js';
22
22
  const NOTIFY_LEASE_MS = 5 * 60 * 1000;
23
23
  export { planActNotifications, matchesMentionTarget };
@@ -256,6 +256,23 @@ export function wakeNotifierForSquare(squarePath, env = process.env) {
256
256
  },
257
257
  };
258
258
  }
259
+ /** Select sweep candidates from one frozen snapshot and one delivery replay. */
260
+ export function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery = deriveDeliveryModel) {
261
+ const delivery = deriveDelivery(state);
262
+ const pending = pendingDeliveriesFromState(state, delivery);
263
+ const evidence = wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery);
264
+ const indexes = new Set();
265
+ for (const recipient of pending) {
266
+ for (const note of recipient.notifications) {
267
+ if (now - note.item.at <= wakeGraceMs(env))
268
+ continue;
269
+ if (!wakeIsEligible(evidence.evidence(recipient.recipient, note.item.index)))
270
+ continue;
271
+ indexes.add(note.item.index);
272
+ }
273
+ }
274
+ return [...indexes].sort((left, right) => left - right).slice(0, Math.max(0, limit));
275
+ }
259
276
  /** Reconsider old pending attention at a bounded action boundary using the existing worker. */
260
277
  export async function sweepPendingNotifications(squarePath, opts = {}) {
261
278
  const env = opts.env ?? process.env;
@@ -264,18 +281,14 @@ export async function sweepPendingNotifications(squarePath, opts = {}) {
264
281
  const now = opts.now ?? Date.now();
265
282
  const limit = opts.limit ?? 8;
266
283
  const square = await openSquare(squarePath, { clock: () => now });
267
- const pending = await pendingDeliveries(square).finally(() => closeOpenSquare(square));
268
- const indexes = new Set();
269
- for (const delivery of pending) {
270
- for (const note of delivery.notifications) {
271
- if (now - note.item.at <= wakeGraceMs(env))
272
- continue;
273
- if (!wakeIsEligible(await wakeEvidence(squarePath, delivery.recipient, note.item.index, now, env)))
274
- continue;
275
- indexes.add(note.item.index);
276
- }
284
+ let state;
285
+ try {
286
+ ({ state } = await entryPresentation(square, ''));
287
+ }
288
+ finally {
289
+ await closeOpenSquare(square);
277
290
  }
278
- const selected = [...indexes].sort((a, b) => a - b).slice(0, Math.max(0, limit));
291
+ const selected = pendingNotificationSweepFromState(squarePath, state, now, env, limit);
279
292
  const workerPath = fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url));
280
293
  for (const actIndex of selected) {
281
294
  (opts.launchWorker ?? launchWorker)(workerPath, ['--location', squarePath, '--act-index', String(actIndex)]);
@@ -1,6 +1,7 @@
1
+ import { type DeliveryModel } from './delivery.js';
1
2
  import type { OpenSquare } from './open-square.js';
2
3
  import type { CatchOptions, CatchResult } from './square-facade.js';
3
- export declare function catchUp(square: OpenSquare, name: string, options?: CatchOptions): Promise<CatchResult>;
4
+ export declare function catchUp(square: OpenSquare, name: string, options?: CatchOptions, deriveDelivery?: (state: import('./model.js').SquareState) => DeliveryModel): Promise<CatchResult>;
4
5
  /** Commit seen only for complete, actually rendered boundary bodies. */
5
6
  export declare function markBoundarySeen(squarePath: string, name: string, ownerId: string | undefined, actIndexes: readonly number[], at?: number): Promise<void>;
6
7
  export declare function markNotificationNotified(square: OpenSquare, name: string, actIndex: number, ownerId: string | undefined, at?: number): Promise<void>;
package/dist/presence.js CHANGED
@@ -1,23 +1,22 @@
1
1
  import { extractMentions, formatActivityId } from './square-core.js';
2
2
  import { deliveryDelta, directedPeerSays, matchesFeedFilter } from './activity-feed.js';
3
- import { markSeenNotifications, perceiveActivity } from './delivery.js';
3
+ import { deriveDeliveryModel, markSeenNotifications } from './delivery.js';
4
4
  import { SquareError } from './model.js';
5
5
  import { openSquare } from './square-file-adapter.js';
6
6
  import { closeOpenSquare } from './open-square.js';
7
7
  import { resolveKnownName } from './decisions.js';
8
- import { readCursor } from './runtime.js';
9
8
  import { recordObservation } from './runtime.js';
10
- function expose(squareState, activity, viewer) {
9
+ function expose(squareState, activity, viewer, delivery) {
11
10
  if (activity.kind === 'read' || activity.actor === undefined)
12
11
  throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
13
- const perception = perceiveActivity(squareState, activity, viewer);
12
+ const perception = delivery.perceive(activity, viewer);
14
13
  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) } : {}) };
15
14
  if (perception === 'full' || !('body' in result))
16
15
  return { ...result, perception };
17
16
  const { body: _body, ...withoutBody } = result;
18
17
  return { ...withoutBody, perception };
19
18
  }
20
- export async function catchUp(square, name, options = {}) {
19
+ export async function catchUp(square, name, options = {}, deriveDelivery = deriveDeliveryModel) {
21
20
  const idle = options.idle ?? 0;
22
21
  if (!Number.isFinite(idle) || idle < 0)
23
22
  throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
@@ -26,15 +25,15 @@ export async function catchUp(square, name, options = {}) {
26
25
  const attempt = await square.cell.transact((state, version) => {
27
26
  const at = square.clock();
28
27
  const viewer = resolveKnownName(state, name);
29
- const delta = deliveryDelta(state, viewer);
28
+ const delivery = deriveDelivery(state);
29
+ const delta = deliveryDelta(state, viewer, delivery);
30
30
  const filter = { ...(options.from === undefined ? {} : { participants: [...options.from] }), ...(options.mention === true ? { mention: viewer } : {}) };
31
- const delivered = directedPeerSays(state, delta, viewer).filter((activity) => matchesFeedFilter(activity, filter))
31
+ const delivered = directedPeerSays(state, delta, viewer, delivery).filter((activity) => matchesFeedFilter(activity, filter))
32
32
  .filter((activity, index, activities) => activities.findIndex((candidate) => candidate.index === activity.index) === index)
33
33
  .sort((left, right) => left.index - right.index);
34
- const seenChanged = delivered.reduce((changed, activity) => recordObservation(state, viewer, activity.index, 'seen', at) || changed, false)
35
- || markSeenNotifications(state, viewer, delivered, at);
36
- const cursor = readCursor(state, viewer);
37
- return { ...(seenChanged ? { state } : {}), result: { version, caught: { activities: delivered.map((activity) => expose(state, activity, viewer)), consumedThrough: cursor < 0 ? null : formatActivityId(cursor), idleExpired: false } } };
34
+ const seenChanged = delivered.length === 0 ? false : markSeenNotifications(state, viewer, delivered, at, delivery);
35
+ const cursor = delivery.cursorFor(viewer);
36
+ return { ...(seenChanged ? { state } : {}), result: { version, caught: { activities: delivered.map((activity) => expose(state, activity, viewer, delivery)), consumedThrough: cursor < 0 ? null : formatActivityId(cursor), idleExpired: false } } };
38
37
  });
39
38
  if (attempt.caught.activities.length > 0 || idle === 0)
40
39
  return attempt.caught;
@@ -75,6 +75,7 @@ export declare function renderActivityBlocked(opts: ActivityBlockedOptions): str
75
75
  export declare function renderExpressWaiting(opts: ExpressWaitingOptions): string;
76
76
  export declare function renderExpressNoWait(opts: ExpressNoWaitOptions): string;
77
77
  export declare function renderPublicTail(squareState: SquareState, events: StoredAct[], lastN: number | null | undefined, now?: number, viewer?: string): string;
78
+ export declare function renderPresenceAnchor(names: readonly string[]): string;
78
79
  export declare function renderActivitiesView(squareState: SquareState, visible: StoredAct[], lastN: number | null | undefined, full: boolean | undefined, squarePath: string, viewer?: string, mode?: 'ambient' | 'archive'): string;
79
80
  export declare function renderGrepActivitiesView(visible: StoredAct[], totalMatches: number, full: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean): string;
80
81
  export declare function renderActivityLimit(opts: ActivityLimitOptions): string;
@@ -91,5 +92,6 @@ export declare function renderWatchOutput(history: StoredAct[], publicItems: Pub
91
92
  mention?: string;
92
93
  viewer: string;
93
94
  showCatchHint?: boolean;
94
- squareState: SquareState;
95
+ squareState?: SquareState;
96
+ perceptions?: ReadonlyMap<number, Perception>;
95
97
  }): string;
@@ -291,8 +291,9 @@ function lastPresenceAnchor(squareState, name) {
291
291
  }
292
292
  return -1;
293
293
  }
294
- function renderLastPresenceMarker(name) {
295
- return `→ ${participantIdentity(name)} was here`;
294
+ export function renderPresenceAnchor(names) {
295
+ const participants = names.map((name) => participantIdentity(name)).join(', ');
296
+ return names.length === 1 ? `→ ${participants} was here` : `→ ${participants} were here`;
296
297
  }
297
298
  export function renderActivitiesView(squareState, visible, lastN, full, squarePath, viewer = '', mode = 'ambient') {
298
299
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
@@ -315,9 +316,9 @@ export function renderActivitiesView(squareState, visible, lastN, full, squarePa
315
316
  : renderAmbientEvent(act, viewer, { ...opts, squareState });
316
317
  if (rendered !== '')
317
318
  chunks.push(rendered);
318
- for (const participant of markers.get(act.index) ?? []) {
319
- chunks.push(renderLastPresenceMarker(participant));
320
- }
319
+ const participants = markers.get(act.index);
320
+ if (participants !== undefined)
321
+ chunks.push(renderPresenceAnchor(participants));
321
322
  }
322
323
  if (chunks.length === 0)
323
324
  return 'latest\n ○ no public activity in this view';
@@ -455,7 +456,8 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
455
456
  .map((act) => renderAmbientEvent(act, opts.viewer, {
456
457
  actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
457
458
  mention: opts.mention,
458
- squareState: opts.squareState,
459
+ ...(opts.perceptions?.has(act.index) ? { perception: opts.perceptions.get(act.index) } : {}),
460
+ ...(opts.squareState === undefined ? {} : { squareState: opts.squareState }),
459
461
  }))
460
462
  .filter(Boolean)
461
463
  .join('\n\n');