@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.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity-feed.d.ts +2 -2
- package/dist/activity-feed.js +5 -7
- package/dist/automatic-session.js +1 -1
- package/dist/cli/observation-commands.js +13 -6
- package/dist/decisions.d.ts +4 -3
- package/dist/decisions.js +22 -20
- package/dist/delivery.d.ts +11 -3
- package/dist/delivery.js +42 -22
- package/dist/notifications.d.ts +4 -1
- package/dist/notifications.js +27 -14
- package/dist/presence.d.ts +2 -1
- package/dist/presence.js +10 -11
- package/dist/presentation.d.ts +3 -1
- package/dist/presentation.js +8 -6
- package/dist/presented.d.ts +8 -0
- package/dist/presented.js +9 -0
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +7 -6
- package/dist/square-core.d.ts +11 -5
- package/dist/square-core.js +138 -89
- package/dist/square-storage.js +18 -9
- package/dist/views.d.ts +2 -1
- package/dist/views.js +27 -22
- package/dist/wake-evidence.d.ts +8 -0
- package/dist/wake-evidence.js +78 -24
- package/dist/watch.d.ts +2 -1
- package/dist/watch.js +12 -6
- package/package.json +1 -1
package/dist/activity-feed.d.ts
CHANGED
|
@@ -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;
|
package/dist/activity-feed.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { sameName } from './model.js';
|
|
2
|
-
import {
|
|
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,
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
package/dist/decisions.d.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
104
|
-
const
|
|
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:
|
|
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 <=
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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;
|
package/dist/delivery.d.ts
CHANGED
|
@@ -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 {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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
|
|
88
|
+
export function planActNotifications(squareState, item, delivery = deriveDeliveryModel(squareState)) {
|
|
89
|
+
return delivery.plan(item);
|
|
66
90
|
}
|
|
67
|
-
export function perceiveActivity(squareState, item, viewer) {
|
|
68
|
-
|
|
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
|
|
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;
|
package/dist/notifications.d.ts
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[]>;
|
package/dist/notifications.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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 =
|
|
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)]);
|
package/dist/presence.d.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
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.
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
package/dist/presentation.d.ts
CHANGED
|
@@ -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
|
|
95
|
+
squareState?: SquareState;
|
|
96
|
+
perceptions?: ReadonlyMap<number, Perception>;
|
|
95
97
|
}): string;
|
package/dist/presentation.js
CHANGED
|
@@ -291,8 +291,9 @@ function lastPresenceAnchor(squareState, name) {
|
|
|
291
291
|
}
|
|
292
292
|
return -1;
|
|
293
293
|
}
|
|
294
|
-
function
|
|
295
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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');
|