@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.
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity-feed.d.ts +2 -3
- package/dist/activity-feed.js +6 -15
- package/dist/activity.js +1 -1
- package/dist/artifact.js +17 -26
- package/dist/attention-presentation.d.ts +13 -0
- package/dist/attention-presentation.js +22 -0
- package/dist/automatic-session.js +6 -8
- package/dist/boundary-presentation.js +35 -14
- package/dist/claude-hook.js +5 -3
- package/dist/cli/context.d.ts +4 -4
- package/dist/cli/context.js +25 -4
- package/dist/cli/harness-command.js +1 -1
- package/dist/cli/maintenance-commands.js +5 -4
- package/dist/cli/observation-commands.js +24 -16
- package/dist/cli/program.js +1 -1
- package/dist/cli/registry.js +4 -1
- package/dist/cli/square-commands.d.ts +7 -0
- package/dist/cli/square-commands.js +123 -27
- package/dist/codex-hook.js +5 -3
- package/dist/decisions.d.ts +19 -0
- package/dist/decisions.js +45 -12
- package/dist/delivery.d.ts +14 -21
- package/dist/delivery.js +40 -74
- package/dist/help.js +8 -5
- package/dist/inbox.js +1 -0
- package/dist/index.d.ts +1 -10
- package/dist/index.js +0 -32
- package/dist/landing.d.ts +12 -0
- package/dist/landing.js +35 -4
- package/dist/model.d.ts +8 -11
- package/dist/model.js +2 -2
- package/dist/notifications.js +35 -11
- package/dist/paseo-delivery.js +7 -4
- package/dist/paseo.d.ts +6 -0
- package/dist/paseo.js +5 -0
- package/dist/presence.d.ts +3 -0
- package/dist/presence.js +40 -10
- package/dist/presentation.d.ts +6 -2
- package/dist/presentation.js +22 -9
- package/dist/presented.d.ts +2 -0
- package/dist/presented.js +20 -0
- package/dist/registry.js +4 -1
- package/dist/routes.d.ts +5 -0
- package/dist/routes.js +17 -0
- package/dist/runtime.d.ts +4 -5
- package/dist/runtime.js +35 -18
- package/dist/square-core.d.ts +17 -0
- package/dist/square-core.js +51 -1
- package/dist/square-facade.d.ts +9 -1
- package/dist/square-wiring.d.ts +8 -0
- package/dist/square-wiring.js +32 -3
- package/dist/views.d.ts +9 -3
- package/dist/views.js +16 -14
- package/dist/wake-port.d.ts +4 -1
- package/dist/wake-port.js +5 -0
- package/dist/wakes.js +3 -5
- package/dist/watch.js +1 -0
- package/package.json +19 -3
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +1 -1
package/dist/presence.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { extractMentions, formatActivityId
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
32
|
-
|
|
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 { ...(
|
|
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
|
+
}
|
package/dist/presentation.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/presentation.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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' ||
|
|
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');
|
package/dist/presented.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
40
|
-
export declare function
|
|
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
|
-
|
|
130
|
-
return squareState
|
|
129
|
+
function observationRecipient(squareState, name) {
|
|
130
|
+
return resolveRosterName(squareState, name) ?? name;
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
146
|
-
return
|
|
146
|
+
export function observationFor(squareState, name, index) {
|
|
147
|
+
return observationMap(squareState, name)[formatActivityId(index)];
|
|
147
148
|
}
|
|
148
|
-
export function
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/square-core.d.ts
CHANGED
|
@@ -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;
|
package/dist/square-core.js
CHANGED
|
@@ -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' };
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -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>;
|
package/dist/square-wiring.d.ts
CHANGED
|
@@ -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>;
|
package/dist/square-wiring.js
CHANGED
|
@@ -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 {
|